diff --git a/bom/pom.xml b/bom/pom.xml index 49e4931b70a..751eda9e6e5 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -681,6 +681,11 @@ helidon-common-task ${helidon.version} + + io.helidon.common + helidon-common-types + ${helidon.version} + io.helidon.common.testing helidon-common-testing-junit5 @@ -1405,36 +1410,65 @@ helidon-builder-processor ${helidon.version} + + io.helidon.builder + helidon-builder-config + ${helidon.version} + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + io.helidon.pico - helidon-pico + helidon-pico-api ${helidon.version} io.helidon.pico - helidon-pico-types + helidon-pico-tools ${helidon.version} io.helidon.pico - helidon-pico-tools + helidon-pico-processor ${helidon.version} - - - io.helidon.pico.builder.config - helidon-pico-builder-config + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + io.helidon.pico + helidon-pico-testing ${helidon.version} - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.pico + helidon-pico-services ${helidon.version} + + + io.helidon.pico.configdriven + helidon-pico-configdriven-api + ${helidon.version} + + + io.helidon.pico.configdriven + helidon-pico-configdriven-services + ${helidon.version} + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + diff --git a/builder/README.md b/builder/README.md index 62884107602..3a72dcd91d7 100644 --- a/builder/README.md +++ b/builder/README.md @@ -3,7 +3,7 @@ The Helidon Builder provides compile-time code generation for fluent builders. It was inspired by [Lombok]([https://projectlombok.org/), but the implementation here in Helidon is different in a few ways:
  1. The Builder annotation targets interface or annotation types only. Your interface effectively contains the attributes of your getter as well as serving as the contract for your getter methods.
  2. -
  3. Generated classes implement your target interface (or annotation) and provide a fluent builder that will always have an implementation of toString(), hashCode(), and equals(). implemented
  4. +
  5. Generated classes implement your target interface (or annotation or abstract class) and provide a fluent builder that will always have an implementation of toString(), hashCode(), and equals(). implemented
  6. Generated classes always behave like a SuperBuilder from Lombok. Basically this means that builders can form a hierarchy on the types they target (e.g., Level2 derives from Level1 derives from Level0, etc.).
  7. Lombok uses AOP while the Helidon Builder generates source code. You can use the Builder annotation (as well as other annotations in the package and ConfiguredOption) to control the naming and other features of what and how the implementation classes are generated and behave.
  8. @@ -38,7 +38,35 @@ public interface MyConfigBean { } ``` 2. Annotate your interface definition with Builder, and optionally use ConfiguredOption, Singular, etc. Remember to review the annotation attributes javadoc for any customizations. -3. Compile (using the builder-processor in your annotation classpath). +3. Builder (using the builder-processor in your annotation classpath). +```xml + ... + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + + ... +``` The result of this will create (under ./target/generated-sources/annotations): * MyConfigBeanImpl (in the same package as MyConfigBean) that will support multi-inheritance builders named MyConfigBeanImpl.Builder. @@ -49,16 +77,28 @@ The result of this will create (under ./target/generated-sources/annotations): * Support for attribute validation (see ConfiguredOption#required() and [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)). * Support for builder interception (i.e., including decoration or mutation). (see [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)). +Also note that the generated code from Helidon Builder processors may add other dependencies that you will need to add (typically in provided scope). +```xml + + jakarta.annotation + jakarta.annotation-api + provided + true + +``` + ## Modules * [builder](./builder) - provides the compile-time annotations, as well as optional runtime supporting types. * [processor-spi](./processor-spi) - defines the Builder Processor SPI runtime definitions used by builder tooling. This module is only needed at compile time. * [processor-tools](./processor-tools) - provides the concrete creators & code generators. This module is only needed at compile time. * [processor](./processor) - the annotation processor which delegates to the processor-tools module for the main processing logic. This module is only needed at compile time. -* [tests/builder](./tests/builder) - tests that can also serve as examples for usage. +* [builder-config](./builder-config) - extension to the builder to additionally support [Helidon (Common) Config](../common/config) and [@ConfigBean](./builder-config/src/main/java/io/helidon/builder/config/ConfigBean.java). +* [builder-config-processor](./builder-config-processor) - defines the ConfigBean builder. +* [tests](./tests) - tests that can also serve as examples for usage. -## Customizations +## Customizations and Extensibility To implement your own custom Builder: -* See [pico/builder-config](../pico/builder-config) for an example. +* See [builder-config](../builder-config) serving as an example. ## Usage See [tests/builder](./tests/builder) for usage examples. diff --git a/builder/builder-config-processor/README.md b/builder/builder-config-processor/README.md new file mode 100644 index 00000000000..e3cecbdd9ef --- /dev/null +++ b/builder/builder-config-processor/README.md @@ -0,0 +1,4 @@ +# builder-config-processor + +This module adds support for ConfigBean annotation. +This module should typically only be used during compile time, in the APT compiler path only. diff --git a/pico/builder-config/processor/pom.xml b/builder/builder-config-processor/pom.xml similarity index 76% rename from pico/builder-config/processor/pom.xml rename to builder/builder-config-processor/pom.xml index 194b7c1549c..a2d166456e2 100644 --- a/pico/builder-config/processor/pom.xml +++ b/builder/builder-config-processor/pom.xml @@ -19,36 +19,33 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - io.helidon.pico.builder.config - helidon-pico-builder-config-project + io.helidon.builder + helidon-builder-project 4.0.0-SNAPSHOT ../pom.xml 4.0.0 - helidon-pico-builder-config-processor - - Helidon Pico Builder ConfigBean Processor + helidon-builder-config-processor + Helidon Builder ConfigBean Processor - io.helidon.pico.builder.config - helidon-pico-builder-config + io.helidon.builder + helidon-builder-config io.helidon.builder helidon-builder-processor - io.helidon.pico - helidon-pico-types - - - io.helidon.pico - helidon-pico + io.helidon.common + helidon-common-types + + io.helidon.common helidon-common-config diff --git a/pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/ConfigBeanBuilderCreator.java b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java similarity index 60% rename from pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/ConfigBeanBuilderCreator.java rename to builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java index 235ffd69d60..3e6042adfc8 100644 --- a/pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/ConfigBeanBuilderCreator.java +++ b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,11 +14,10 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.processor; +package io.helidon.builder.config.processor; import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -26,42 +25,54 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Supplier; - -import io.helidon.builder.processor.spi.TypeInfo; +import java.util.stream.Collectors; + +import io.helidon.builder.config.ConfigBean; +import io.helidon.builder.config.spi.ConfigBeanBuilderValidator; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.ConfigResolver; +import io.helidon.builder.config.spi.DefaultConfigResolverRequest; +import io.helidon.builder.config.spi.GeneratedConfigBean; +import io.helidon.builder.config.spi.GeneratedConfigBeanBase; +import io.helidon.builder.config.spi.GeneratedConfigBeanBuilder; +import io.helidon.builder.config.spi.GeneratedConfigBeanBuilderBase; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.builder.config.spi.ResolutionContext; import io.helidon.builder.processor.tools.BodyContext; -import io.helidon.builder.processor.tools.BuilderTypeTools; import io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider; import io.helidon.common.Weight; import io.helidon.common.Weighted; import io.helidon.common.config.Config; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.Contract; -import io.helidon.pico.ExternalContracts; -import io.helidon.pico.builder.config.ConfigBean; -import io.helidon.pico.builder.config.spi.ConfigBeanBase; -import io.helidon.pico.builder.config.spi.ConfigBeanBuilderBase; -import io.helidon.pico.builder.config.spi.ConfigBeanBuilderValidator; -import io.helidon.pico.builder.config.spi.ConfigBeanInfo; -import io.helidon.pico.builder.config.spi.ConfigResolver; -import io.helidon.pico.builder.config.spi.DefaultConfigResolverRequest; -import io.helidon.pico.builder.config.spi.MetaConfigBeanInfo; -import io.helidon.pico.builder.config.spi.ResolutionContext; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.DefaultAnnotationAndValue; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; + +import static io.helidon.builder.config.spi.ConfigBeanInfo.LevelType; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_AT_LEAST_ONE; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_DRIVES_ACTIVATION; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_KEY; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_LEVEL_TYPE; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_REPEATABLE; +import static io.helidon.builder.config.spi.ConfigBeanInfo.TAG_WANT_DEFAULT_CONFIG_BEAN; /** - * A specialization of {@link io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider} that supports the additional add-ons to the builder generated classes that - * binds to the config sub-system. + * A specialization of {@link io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider} that supports the additional + * add-ons to the builder generated classes that binds to the config sub-system. * - * @see io.helidon.pico.builder.config.spi.ConfigBean - * @see io.helidon.pico.builder.config.spi.ConfigBeanBuilder + * @see GeneratedConfigBean + * @see GeneratedConfigBeanBuilder */ @Weight(Weighted.DEFAULT_WEIGHT) public class ConfigBeanBuilderCreator extends DefaultBuilderCreatorProvider { + static final String PICO_CONTRACT_TYPENAME = "io.helidon.pico.Contract"; + static final String PICO_EXTERNAL_CONTRACT_TYPENAME = "io.helidon.pico.ExternalContracts"; + static final String PICO_CONFIGUREDBY_TYPENAME = "io.helidon.pico.configdriven.ConfiguredBy"; /** * Default constructor. @@ -73,39 +84,81 @@ public ConfigBeanBuilderCreator() { @Override public Set> supportedAnnotationTypes() { - return Collections.singleton(ConfigBean.class); + return Set.of(ConfigBean.class); } @Override protected void preValidate(TypeName implTypeName, TypeInfo typeInfo, - AnnotationAndValue builderAnnotation) { - assertNoAnnotation(Contract.class.getName(), typeInfo); - assertNoAnnotation(ExternalContracts.class.getName(), typeInfo); - // note to self: add this when ConfiguredBy is introduced --jtrent -// assertNoAnnotation(ConfiguredBy.class.getName(), typeInfo); + AnnotationAndValue configBeanAnno) { + assertNoAnnotation(PICO_CONTRACT_TYPENAME, typeInfo); + assertNoAnnotation(PICO_EXTERNAL_CONTRACT_TYPENAME, typeInfo); + assertNoAnnotation(PICO_CONFIGUREDBY_TYPENAME, typeInfo); assertNoAnnotation(jakarta.inject.Singleton.class.getName(), typeInfo); assertNoAnnotation("javax.inject.Singleton", typeInfo); - if (!typeInfo.typeKind().equals("INTERFACE")) { - throw new IllegalStateException("@" + builderAnnotation.typeName().className() + if (!typeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE)) { + throw new IllegalStateException("@" + configBeanAnno.typeName().className() + " is only supported on interface types: " + typeInfo.typeName()); } + + boolean drivesActivation = Boolean.parseBoolean(configBeanAnno.value(TAG_DRIVES_ACTIVATION).orElseThrow()); + LevelType levelType = LevelType.valueOf(configBeanAnno.value(TAG_LEVEL_TYPE).orElseThrow()); + if (drivesActivation && levelType != LevelType.ROOT) { + throw new IllegalStateException("only levelType {" + LevelType.ROOT + "} config beans can drive activation for: " + + typeInfo.typeName()); + } + + boolean wantDefaultConfigBean = Boolean.parseBoolean(configBeanAnno.value(TAG_WANT_DEFAULT_CONFIG_BEAN).orElseThrow()); + if (wantDefaultConfigBean && levelType != LevelType.ROOT) { + throw new IllegalStateException("only levelType {" + LevelType.ROOT + "} config beans can have a default bean for: " + + typeInfo.typeName()); + } + + assertNoGenericMaps(typeInfo); + + super.preValidate(implTypeName, typeInfo, configBeanAnno); + } + + /** + * Generic/simple map types are not supported on config beans, only <String, <Known ConfigBean types>>. + */ + private void assertNoGenericMaps(TypeInfo typeInfo) { + List list = typeInfo.elementInfo().stream() + .filter(it -> it.typeName().isMap()) + .filter(it -> { + TypeName typeName = it.typeName(); + List componentArgs = typeName.typeArguments(); + boolean bad = (componentArgs.size() != 2); + if (!bad) { + bad = !componentArgs.get(0).name().equals(String.class.getName()); + // right now we will accept any component type - ConfigBean Type or other (just not generic) +// bad |= !typeInfo.referencedTypeNamesToAnnotations().containsKey(componentArgs.get(1)); + bad |= componentArgs.get(1).generic(); + } + return bad; + }) + .collect(Collectors.toList()); + + if (!list.isEmpty()) { + throw new IllegalStateException(list + ": only methods returning Map> are supported " + + "for: " + typeInfo.typeName()); + } } @Override - protected String generatedStickerFor(BodyContext ctx) { - return BuilderTypeTools.generatedStickerFor(getClass().getName(), Versions.CURRENT_PICO_CONFIG_BUILDER_VERSION); + protected String generatedVersionFor(BodyContext ctx) { + return Versions.CURRENT_BUILDER_CONFIG_VERSION; } @Override protected Optional baseExtendsTypeName(BodyContext ctx) { - return Optional.of(DefaultTypeName.create(ConfigBeanBase.class)); + return Optional.of(DefaultTypeName.create(GeneratedConfigBeanBase.class)); } @Override protected Optional baseExtendsBuilderTypeName(BodyContext ctx) { - return Optional.of(DefaultTypeName.create(ConfigBeanBuilderBase.class)); + return Optional.of(DefaultTypeName.create(GeneratedConfigBeanBuilderBase.class)); } @Override @@ -117,7 +170,9 @@ protected String instanceIdRef(BodyContext ctx) { protected void appendExtraImports(StringBuilder builder, BodyContext ctx) { builder.append("\nimport ").append(AtomicInteger.class.getName()).append(";\n"); + builder.append("import ").append(Optional.class.getName()).append(";\n"); + builder.append("import ").append(Function.class.getName()).append(";\n\n"); builder.append("import ").append(Supplier.class.getName()).append(";\n\n"); super.appendExtraImports(builder, ctx); @@ -178,7 +233,8 @@ protected void appendExtraCtorCode(StringBuilder builder, BodyContext ctx, String builderTag) { if (!ctx.hasParent()) { - builder.append("\t\tsuper(b, String.valueOf(__INSTANCE_ID.getAndIncrement()));\n"); + builder.append("\t\tsuper(b, b.__config().isPresent() ? String.valueOf(__INSTANCE_ID.getAndIncrement()) : " + + "\"-1\");\n"); } super.appendExtraCtorCode(builder, ctx, builderTag); @@ -214,7 +270,7 @@ protected void appendExtraBuilderMethods(StringBuilder builder, builder.append("\t\tpublic void acceptConfig" + "(Config cfg, ConfigResolver resolver, ConfigBeanBuilderValidator validator) {\n"); builder.append("\t\t\t").append(ResolutionContext.class.getName()) - .append(" ctx = createResolutionContext(__configBeanType(), cfg, resolver, validator);\n"); + .append(" ctx = createResolutionContext(__configBeanType(), cfg, resolver, validator, __mappers());\n"); builder.append("\t\t\t__config(ctx.config());\n"); builder.append("\t\t\t__acceptAndResolve(ctx);\n"); builder.append("\t\t\tsuper.finishedResolution(ctx);\n"); @@ -248,10 +304,9 @@ protected void appendExtraBuilderMethods(StringBuilder builder, TypeName mapKeyType = null; TypeName mapKeyComponentType = null; boolean isMap = typeName.equals(Map.class.getName()); - boolean isCollection = ( - typeName.equals(Collection.class.getName()) - || typeName.equals(Set.class.getName()) - || typeName.equals(List.class.getName())); + boolean isCollection = (typeName.equals(Collection.class.getName()) + || typeName.equals(Set.class.getName()) + || typeName.equals(List.class.getName())); if (isCollection) { ofClause = "ofCollection"; type = type.typeArguments().get(0); @@ -284,45 +339,84 @@ protected void appendExtraBuilderMethods(StringBuilder builder, } if (isMap) { builder.append(".keyType(").append(Objects.requireNonNull(mapKeyType)).append(".class)"); - if (Objects.nonNull(mapKeyComponentType)) { + if (mapKeyComponentType != null) { builder.append(".keyComponentType(").append(mapKeyComponentType.name()).append(".class)"); } } - builder.append(".build())\n\t\t\t\t\t.ifPresent((val) -> this.").append(method.elementName()).append("(("); + builder.append(".build())\n\t\t\t\t\t.ifPresent(val -> this.").append(method.elementName()).append("(("); builder.append(outerTypeName).append(") val));\n"); i++; } + builder.append("\t\t}\n\n"); + + builder.append("\t\t@Override\n"); + builder.append("\t\tpublic Class __configBeanType() {\n" + + "\t\t\treturn ") + .append(ctx.typeInfo().typeName().name()).append(".class;\n\t\t}\n\n"); + builder.append("\t\t@Override\n"); + builder.append("\t\tpublic Map, Function> __mappers() {\n" + + "\t\t\tMap, Function> result = "); + if (ctx.hasParent()) { + builder.append("super.__mappers();\n"); + } else { + builder.append("new java.util.LinkedHashMap<>();\n"); + } + appendAvailableReferencedBuilders(builder, "\t\t\tresult.", ctx.typeInfo()); + builder.append("\t\t\treturn result;\n"); builder.append("\t\t}\n\n"); } - builder.append("\t\t@Override\n"); - builder.append("\t\tpublic Class __configBeanType() {\n" - + "\t\t\treturn ") - .append(ctx.typeInfo().typeName().name()).append(".class;\n\t\t}\n\n"); - super.appendExtraBuilderMethods(builder, ctx); } + private void appendAvailableReferencedBuilders(StringBuilder builder, + String prefix, + TypeInfo typeInfo) { + typeInfo.referencedTypeNamesToAnnotations().forEach((k, v) -> { + AnnotationAndValue builderAnnotation = DefaultAnnotationAndValue + .findFirst(io.helidon.builder.Builder.class.getName(), v).orElse(null); + if (builderAnnotation == null) { + builderAnnotation = DefaultAnnotationAndValue + .findFirst(ConfigBean.class.getName(), v).orElse(null); + } + + if (builderAnnotation != null) { + TypeName referencedBuilderTypeName = toBuilderImplTypeName(k, builderAnnotation); + builder.append(prefix).append("put(").append(k.name()).append(".class, "); + builder.append(referencedBuilderTypeName).append("::toBuilder);\n"); + } + }); + } + @Override protected boolean overridesVisitAttributes(BodyContext ctx) { return true; } + @Override + protected String toConfigKey(String name, + boolean isAttribute) { + return (isAttribute) ? ConfigBeanInfo.toConfigAttributeName(name) : ConfigBeanInfo.toConfigBeanName(name); + } + private void appendConfigBeanInfoAttributes(StringBuilder builder, TypeInfo typeInfo, AnnotationAndValue configBeanAnno) { - String configKey = configBeanAnno.value("key").orElse(null); - configKey = normalizeConfiguredOptionKey(configKey, typeInfo.typeName().className(), null); - - builder.append("\t\t\t\t\t\t.key(\"") - .append(Objects.requireNonNull(configKey)).append("\")\n"); - builder.append("\t\t\t\t\t\t.repeatable(") - .append(configBeanAnno.value("repeatable").orElseThrow()).append(")\n"); - builder.append("\t\t\t\t\t\t.drivesActivation(") - .append(configBeanAnno.value("drivesActivation").orElseThrow()).append(")\n"); - builder.append("\t\t\t\t\t\t.atLeastOne(") - .append(configBeanAnno.value("atLeastOne").orElseThrow()).append(")\n"); + String configKey = configBeanAnno.value(TAG_KEY).orElse(null); + configKey = Objects.requireNonNull(normalizeConfiguredOptionKey(configKey, typeInfo.typeName().className(), false)); + builder.append("\t\t\t\t\t\t.value(\"") + .append(configKey).append("\")\n"); + builder.append("\t\t\t\t\t\t.").append(TAG_REPEATABLE).append("(") + .append(configBeanAnno.value(TAG_REPEATABLE).orElseThrow()).append(")\n"); + builder.append("\t\t\t\t\t\t.").append(TAG_DRIVES_ACTIVATION).append("(") + .append(configBeanAnno.value(TAG_DRIVES_ACTIVATION).orElseThrow()).append(")\n"); + builder.append("\t\t\t\t\t\t.").append(TAG_AT_LEAST_ONE).append("(") + .append(configBeanAnno.value(TAG_AT_LEAST_ONE).orElseThrow()).append(")\n"); + builder.append("\t\t\t\t\t\t.").append(TAG_WANT_DEFAULT_CONFIG_BEAN).append("(") + .append(configBeanAnno.value(TAG_WANT_DEFAULT_CONFIG_BEAN).orElseThrow()).append(")\n"); + builder.append("\t\t\t\t\t\t.").append(TAG_LEVEL_TYPE).append("(").append(LevelType.class.getCanonicalName()).append(".") + .append(configBeanAnno.value(TAG_LEVEL_TYPE).orElseThrow()).append(")\n"); } private void javaDocMetaAttributesGetter(StringBuilder builder) { @@ -363,13 +457,13 @@ private String toConfigKey(String attrName, if (configuredOptions.isPresent()) { configKey = configuredOptions.get().value("key").orElse(null); } - if (Objects.isNull(configKey) || configKey.isBlank()) { - configKey = ConfigBeanInfo.toConfigKey(attrName); + if (configKey == null || configKey.isBlank()) { + configKey = ConfigBeanInfo.toConfigAttributeName(attrName); } return configKey; } - private void assertNoAnnotation(String annoTypeName, + private static void assertNoAnnotation(String annoTypeName, TypeInfo typeInfo) { Optional anno = DefaultAnnotationAndValue .findFirst(annoTypeName, typeInfo.annotations()); diff --git a/pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/Versions.java b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/Versions.java similarity index 68% rename from pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/Versions.java rename to builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/Versions.java index 8dc1b56f74c..1d10c150438 100644 --- a/pico/builder-config/processor/src/main/java/io/helidon/pico/builder/config/processor/Versions.java +++ b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/Versions.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.processor; +package io.helidon.builder.config.processor; /** - * Keeps track of the Pico Config Builder Interop Versions. + * Keeps track of the Helidon {@link io.helidon.builder.config.processor.ConfigBeanBuilderCreator} Builder Interop Versions. *

    - * Since Pico Config Builder performs code-generation, each previously generated artifact version may need to be discoverable in + * Since ConfigBean Builder performs code-generation, each previously generated artifact version may need to be discoverable in * order to determine interoperability with previous release versions. This class will only track version changes for anything * that might affect interoperability - it will not be rev'ed for general code enhancements and fixes. *

    @@ -30,12 +30,12 @@ public class Versions { /** * Version 1 - the initial release of Builder. */ - public static final String PICO_CONFIG_BUILDER_VERSION_1 = "1"; + public static final String BUILDER_CONFIG_VERSION_1 = "1"; /** - * The current release is {@link #PICO_CONFIG_BUILDER_VERSION_1}. + * The current release is {@link #BUILDER_CONFIG_VERSION_1}. */ - public static final String CURRENT_PICO_CONFIG_BUILDER_VERSION = PICO_CONFIG_BUILDER_VERSION_1; + public static final String CURRENT_BUILDER_CONFIG_VERSION = BUILDER_CONFIG_VERSION_1; private Versions() { } diff --git a/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/package-info.java b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/package-info.java new file mode 100644 index 00000000000..28da5f089d4 --- /dev/null +++ b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Helidon ConfigBean Builder Processor Extensions. + */ +package io.helidon.builder.config.processor; diff --git a/pico/builder-config/processor/src/main/java/module-info.java b/builder/builder-config-processor/src/main/java/module-info.java similarity index 61% rename from pico/builder-config/processor/src/main/java/module-info.java rename to builder/builder-config-processor/src/main/java/module-info.java index 32bc81b9d04..ba982e394a8 100644 --- a/pico/builder-config/processor/src/main/java/module-info.java +++ b/builder/builder-config-processor/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,22 +15,23 @@ */ /** - * Helidon Pico ConfigBean Builder Processor (Tools) module. + * Helidon ConfigBean Builder Processor module. */ -module io.helidon.pico.builder.config.processor { +module io.helidon.builder.config.processor { + requires java.compiler; requires jakarta.inject; + requires io.helidon.builder; + requires io.helidon.builder.config; + requires io.helidon.builder.processor.tools; + requires io.helidon.common.types; requires io.helidon.common; requires io.helidon.common.config; requires io.helidon.config.metadata; - requires io.helidon.pico.builder.config; - requires io.helidon.builder.processor; - requires io.helidon.builder.processor.spi; - requires io.helidon.builder.processor.tools; - requires io.helidon.pico.types; - requires io.helidon.pico; + requires transitive io.helidon.builder.processor; + requires transitive io.helidon.builder.processor.spi; - exports io.helidon.pico.builder.config.processor; + exports io.helidon.builder.config.processor; provides io.helidon.builder.processor.spi.BuilderCreatorProvider - with io.helidon.pico.builder.config.processor.ConfigBeanBuilderCreator; + with io.helidon.builder.config.processor.ConfigBeanBuilderCreator; } diff --git a/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java b/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java new file mode 100644 index 00000000000..09561dea8db --- /dev/null +++ b/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.processor; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeInfo; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ConfigBeanBuilderCreatorTest { + final ConfigBeanBuilderCreator creator = new ConfigBeanBuilderCreator(); + + @Test + void supportedAnnotationTypes() { + assertThat(creator.supportedAnnotationTypes().toString(), creator.supportedAnnotationTypes().size(), + is(1)); + assertThat(creator.supportedAnnotationTypes().iterator().next(), + equalTo(ConfigBean.class)); + } + + @Test + void preValidateConfigBeansMustBeInterfaces() { + TypeName implTypeName = DefaultTypeName.create(getClass()); + TypeInfo typeInfo = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_CLASS) + .typeName(DefaultTypeName.create(getClass())) + .build(); + DefaultAnnotationAndValue configBeanAnno = DefaultAnnotationAndValue.builder() + .typeName(DefaultTypeName.create(ConfigBean.class)) + .values(Map.of( + ConfigBeanInfo.TAG_LEVEL_TYPE, ConfigBean.LevelType.ROOT.name(), + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "true")) + .build(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> creator.preValidate(implTypeName, typeInfo, configBeanAnno)); + assertThat(e.getMessage(), + equalTo("@ConfigBean is only supported on interface types: " + getClass().getName())); + } + + @Test + void preValidateConfigBeansMustBeRootToDriveActivation() { + TypeName implTypeName = DefaultTypeName.create(getClass()); + TypeInfo typeInfo = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_INTERFACE) + .typeName(DefaultTypeName.create(getClass())) + .build(); + DefaultAnnotationAndValue configBeanAnno = DefaultAnnotationAndValue.builder() + .typeName(DefaultTypeName.create(ConfigBean.class)) + .values(Map.of( + ConfigBeanInfo.TAG_LEVEL_TYPE, ConfigBean.LevelType.NESTED.name(), + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "true")) + .build(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> creator.preValidate(implTypeName, typeInfo, configBeanAnno)); + assertThat(e.getMessage(), + equalTo("only levelType {ROOT} config beans can drive activation for: " + getClass().getName())); + } + + @Test + void preValidateConfigBeansMustBeRootToHaveDefaults() { + TypeName implTypeName = DefaultTypeName.create(getClass()); + TypeInfo typeInfo = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_INTERFACE) + .typeName(DefaultTypeName.create(getClass())) + .build(); + DefaultAnnotationAndValue configBeanAnno = DefaultAnnotationAndValue.builder() + .typeName(DefaultTypeName.create(ConfigBean.class)) + .values(Map.of( + ConfigBeanInfo.TAG_LEVEL_TYPE, ConfigBean.LevelType.NESTED.name(), + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "false", + ConfigBeanInfo.TAG_WANT_DEFAULT_CONFIG_BEAN, "true")) + .build(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> creator.preValidate(implTypeName, typeInfo, configBeanAnno)); + assertThat(e.getMessage(), + equalTo("only levelType {ROOT} config beans can have a default bean for: " + getClass().getName())); + } + + @Test + void preValidateConfigBeansMustNotHaveDuplicateSingularNames() { + TypedElementName method1 = DefaultTypedElementName.builder() + .elementName("socket") + .typeName(String.class) + .build(); + TypedElementName method2 = DefaultTypedElementName.builder() + .elementName("socketSet") + .typeName(String.class) + .addAnnotation(DefaultAnnotationAndValue.create(Singular.class, "socket")) + .build(); + TypeName implTypeName = DefaultTypeName.create(getClass()); + TypeInfo typeInfo = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_INTERFACE) + .typeName(DefaultTypeName.create(getClass())) + .elementInfo(Set.of(method1, method2)) + .build(); + DefaultAnnotationAndValue configBeanAnno = DefaultAnnotationAndValue.builder() + .typeName(DefaultTypeName.create(ConfigBean.class)) + .values(Map.of( + ConfigBeanInfo.TAG_LEVEL_TYPE, ConfigBean.LevelType.NESTED.name(), + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "false", + ConfigBeanInfo.TAG_WANT_DEFAULT_CONFIG_BEAN, "false")) + .build(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> creator.preValidate(implTypeName, typeInfo, configBeanAnno)); + assertThat(e.getMessage(), + startsWith("duplicate methods are using the same names [socket] for: ")); + } + + @Test + void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { + TypedElementName method1 = DefaultTypedElementName.builder() + .elementName("socket") + .typeName(DefaultTypeName.builder() + .type(Map.class) + .typeArguments(List.of( + DefaultTypeName.create(String.class), + DefaultTypeName.create(String.class))) + .build()) + .build(); + TypeName implTypeName = DefaultTypeName.create(getClass()); + TypeInfo typeInfo = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_INTERFACE) + .typeName(DefaultTypeName.create(getClass())) + .elementInfo(Set.of(method1)) + .build(); + DefaultAnnotationAndValue configBeanAnno = DefaultAnnotationAndValue.builder() + .typeName(DefaultTypeName.create(ConfigBean.class)) + .values(Map.of( + ConfigBeanInfo.TAG_LEVEL_TYPE, ConfigBean.LevelType.NESTED.name(), + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "false", + ConfigBeanInfo.TAG_WANT_DEFAULT_CONFIG_BEAN, "false")) + .build(); + // Map is supported +// IllegalStateException e = assertThrows(IllegalStateException.class, +// () -> creator.preValidate(implTypeName, typeInfo, configBeanAnno)); +// assertThat(e.getMessage(), startsWith( +// "[java.util.Map socket]: only methods returning Map> are supported for: ")); + creator.preValidate(implTypeName, typeInfo, configBeanAnno); + + // now we will validate the exceptions when ConfigBeans are attempted to be embedded + TypedElementName method2 = DefaultTypedElementName.builder() + .elementName("unsupported1") + .typeName(DefaultTypeName.builder() + .type(Map.class) + // here we register a known config bean value (see below) + .typeArguments(List.of( + DefaultTypeName.create(String.class), + DefaultTypeName.create(getClass()))) + .build()) + .build(); + TypedElementName method3 = DefaultTypedElementName.builder() + .elementName("unsupported2") + .typeName(DefaultTypeName.builder() + .type(Map.class) + // here we will just leave it generic, and this should fail +// .typeArguments(List.of( +// DefaultTypeName.create(String.class), +// DefaultTypeName.create(getClass()))) + .build()) + .build(); + TypeInfo typeInfo2 = DefaultTypeInfo.builder() + .typeKind(TypeInfo.KIND_INTERFACE) + .typeName(DefaultTypeName.create(getClass())) + .elementInfo(List.of(method2, method3)) + .referencedTypeNamesToAnnotations(Map.of(DefaultTypeName.create(getClass()), + List.of(DefaultAnnotationAndValue.create(Builder.class)))) + .build(); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> creator.preValidate(implTypeName, typeInfo2, configBeanAnno)); + assertThat(e.getMessage(), startsWith( + "[java.util.Map unsupported2]: only methods returning Map> are supported for: ")); + } + +} diff --git a/builder/builder-config/README.md b/builder/builder-config/README.md new file mode 100644 index 00000000000..747181d4235 --- /dev/null +++ b/builder/builder-config/README.md @@ -0,0 +1,43 @@ +# builder-config + +This module can be used at compile time or at runtime. + +The primary usage for this module involves the [ConfigBean](./src/main/java/io/helidon/builder/config/ConfigBean.java) annotation. +A {@code ConfigBean} is another {@link io.helidon.builder.BuilderTrigger} that extends the {@link io.helidon.builder.Builder} concept to support the integration to Helidon's configuration sub-system. It basically provides everything that [io.helidon.builder.Builder](../builder) provides. However, unlike the base Builder generated classes that can handle any object type, the types used within your target ConfigBean-annotated interface must have all of its attribute getter method types resolvable by Helidon's [Config](../../common/config) sub-system. + +One should write a ConfigBean-annotated interface in such a way as to group the collection of configurable elements that logically belong together to then be delivered (and perhaps drive an activation of) one or more java service types that are said to be "[ConfiguredBy](../../pico/configdriven)" the given ConfigBean instance. + +The ConfigBean is therefore a logical grouping for the "pure configuration set of attributes (and sub-ConfigBean attributes) that typically originate from an external media store (e.g., property files, config maps, etc.), and are integrated via Helidon's [Config](../../common/config) subsystem at runtime. + +The [builder-config-processor](../builder-config-processor) module is required to be on the APT classpath in order to code-generate the implementation classes for the {@code ConfigBean}. This can replace the normal use of the [builder-processor](../processor) that supports just the Builder annotation. Using the builder-config-processor will support both Builder and ConfigBean annotation types as part of its processing. + +## Example +```java +@ConfigBean +public interface MyConfigBean { + String getName(); + int getPort(); +} +``` +When [Helidon Pico](../../pico) services are incorporated into the application lifecycle at runtime, the configuration sub-system is scanned at startup and ConfigBean instances are created and fed into the ConfigBeanRegistry. This mapping occurs based upon the [io.helidon.config.metadata.ConfiguredOption#key()](../../config/metadata/src/main/java/io/helidon/config/metadata/ConfiguredOption.java) on each of the ConfigBean's attributes. If no such ConfiguredOption annotation is found then the type name is used as the key (e.g., MyConfigBean would map to "my-config-bean"). + +Here is a modified example that shows the use of ConfiguredOption having default values applied. + +```java +@ConfigBean("server") +public interface ServerConfig { + @ConfiguredOption("0.0.0.0") + String host(); + + @ConfiguredOption("0") + int port(); +} +``` + +ConfigBean generated sources have a few extra methods on them. The most notable of these methods is the toBuilder(Config cfg) static method as demonstrated below. +```java +Config cfg = ... +ServerConfig config = DefaultServerConfig.toBuilder(cfg).build(); +``` + +The above can be used to programmatically create ConfigBean instances directly. However, when using [Helidon Pico](../../pico), and using various annotation attributes (see [the javadoc](./src/main/java/io/helidon/builder/config/ConfigBean.java) for details) on ConfigBean, the runtime behavior can be simplified further to automatically create these bean instances, and further drive startup activation of the services that are declared to be "configured by" these config bean instances. This means that simply having configuration present from your config subsystem will drive bean and service activations accordingly. diff --git a/pico/builder-config/builder-config/pom.xml b/builder/builder-config/pom.xml similarity index 82% rename from pico/builder-config/builder-config/pom.xml rename to builder/builder-config/pom.xml index 78056393c23..084b5d23aa3 100644 --- a/pico/builder-config/builder-config/pom.xml +++ b/builder/builder-config/pom.xml @@ -21,16 +21,15 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - io.helidon.pico.builder.config - helidon-pico-builder-config-project + io.helidon.builder + helidon-builder-project 4.0.0-SNAPSHOT ../pom.xml 4.0.0 - helidon-pico-builder-config - - Helidon Pico Config Builder API / SPI + helidon-builder-config + Helidon Builder ConfigBean Builder @@ -42,6 +41,13 @@ helidon-builder-processor provided + + jakarta.annotation + jakarta.annotation-api + provided + true + + io.helidon.common helidon-common-config @@ -49,12 +55,8 @@ jakarta.inject jakarta.inject-api - provided - - - io.helidon.config - helidon-config-metadata - provided + provided + true io.helidon.common.testing diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/ConfigBean.java b/builder/builder-config/src/main/java/io/helidon/builder/config/ConfigBean.java new file mode 100644 index 00000000000..f2be3f7e891 --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/ConfigBean.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.builder.BuilderTrigger; + +/** + * A {@code ConfigBean} is another {@link io.helidon.builder.BuilderTrigger} that extends the + * {@link io.helidon.builder.Builder} concept to support the integration to Helidon's configuration sub-system. It basically + * provides everything that {@link io.helidon.builder.Builder} provides. However, unlike the base + * {@link io.helidon.builder.Builder} generated classes that can handle any object type, the types used within your target + * {@code ConfigBean}-annotated interface must have all of its attribute getter method types resolvable by Helidon's configuration + * sub-system. + *

    + * The @code ConfigBean} is therefore a logical grouping for the "pure configuration" set of attributes (and + * sub-ConfigBean attributes) that typically originate from an external media store (e.g., property files, config maps, + * etc.), and are integrated via Helidon's {@link io.helidon.common.config.Config} subsystem at runtime. + *

    + * One should write a {@code ConfigBean}-annotated interface in such a way as to group the collection of configurable elements + * that logically belong together to then be delivered (and perhaps drive an activation of) one or more java service types that + * are said to be {@code ConfiguredBy} the given {@link ConfigBean} instance. + *

    + * The {@code builder-config-processor} module is required to be on the APT classpath to code-generate the implementation + * classes for the {@code ConfigBean}. + *

    + * Example: + *

    {@code
    + * @ConfigBean
    + * public interface MyConfigBean {
    + *     String getName();
    + *     int getPort();
    + * }
    + * }
    + *

    + * When {@code Pico} services are incorporated into the application lifecycle at runtime, the configuration + * sub-system is scanned at startup and {@code ConfigBean} instances are created and fed into the {@code ConfigBeanRegistry}. + * This mapping occurs based upon the {@link io.helidon.config.metadata.ConfiguredOption#key()} on each of + * the {@code ConfigBean}'s attributes. If no such {@code ConfiguredOption} is found then the type name is used as the key + * (e.g., MyConfigBean would map to "my-config-bean"). + *

    + * Also see {@code ConfiguredBy} in Pico's config-driven module. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(java.lang.annotation.ElementType.TYPE) +@BuilderTrigger +public @interface ConfigBean { + + /** + * The overridden key to use. If not set this will default to use the expanded name from the config subsystem, + * (e.g. MyConfig -> "my-config"). + * + * @return the overriding key to use + */ + String value() default ""; + + /** + * Determines whether an instance of this config bean in the bean registry will result in the backing service + * {@code ConfiguredBy} this bean to be activated. + *

    + * As of the current release, only {@link LevelType#ROOT} level config beans can drive activation. + *

    + * The default value is {@code false}. + * + * @return true if this config bean should drive {@code ConfiguredBy} service activation + */ + boolean drivesActivation() default false; + + /** + * An instance of this bean will be created if there are no instances discovered by the configuration provider(s) post + * startup, and will use all default values annotated using {@code ConfiguredOptions} from the bean interface methods. + *

    + * The default value is {@code false}. + * + * @return the default config bean instance using defaults + */ + boolean atLeastOne() default false; + + /** + * Determines whether there can be more than one bean instance of this type. + *

    + * If false then only 0..1 behavior will be permissible for active beans in the config registry. If true then {@code > 1} + * instances will be permitted. + *

    + * Note: this attribute is dynamic in nature, and therefore cannot be validated at compile time. All violations found to this + * policy will be observed during PicoServices activation. + *

    + * The default value is {@code true}. + * + * @return true if repeatable + */ + boolean repeatable() default true; + + /** + * An instance of this bean will be created if there are no instances discovered by the configuration provider(s) post + * startup, and will use all default values annotated on the bean interface. + *

    + * As of the current release, only {@link LevelType#ROOT} level config beans can be defaulted. + *

    + * The default value is {@code false}. + * + * @return use the default config instance + */ + boolean wantDefaultConfigBean() default false; + + /** + * The {@link LevelType} of this config bean. + *

    + * The default level type is {@link LevelType#ROOT}. + * + * @return the level type of this config bean + */ + LevelType levelType() default LevelType.ROOT; + + + /** + * Represents the level in the config tree to search for config bean instances. Currently, only + * {@link ConfigBean.LevelType#ROOT} is supported. + */ + enum LevelType { + /** + * The config bean {@link #value()} must be at the root of the {@link io.helidon.common.config.ConfigValue} tree in order + * to trigger config bean instance creation. + *

    + * As of the current release, only {@code ROOT} level config beans can {@link #drivesActivation()}. + */ + ROOT, + + /** + * The config bean {@link #value()} must be at a depth > 0 of the config tree. + * As of the current release, {@code NESTED} config beans are unable to {@link #drivesActivation()}. + */ + NESTED, + + } + +} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/package-info.java b/builder/builder-config/src/main/java/io/helidon/builder/config/package-info.java similarity index 81% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/package-info.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/package-info.java index fa8c61aee55..caf6563aa77 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/package-info.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -15,6 +15,6 @@ */ /** - * Helidon Pico ConfigBean Builder API. + * Helidon Builder ConfigBean Support. */ -package io.helidon.pico.builder.config; +package io.helidon.builder.config; diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidator.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidator.java similarity index 84% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidator.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidator.java index e8b64253edc..50b90f8b95f 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidator.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.List; import java.util.Map; @@ -24,7 +24,7 @@ import io.helidon.builder.AttributeVisitor; /** - * Validates a {@link io.helidon.pico.builder.config.ConfigBean} generated builder type instance bean the builder build() is + * Validates a {@link io.helidon.builder.config.ConfigBean} generated builder type instance bean the builder build() is * called and the result is consumed. * * @param the config bean builder type @@ -42,10 +42,9 @@ public interface ConfigBeanBuilderValidator { ValidationRound createValidationRound(CBB builder, Class configBeanBuilderType); - /** * The validation issue severity level. - * @see ConfigBeanBuilderValidator.ValidationIssue#getSeverity() + * @see ConfigBeanBuilderValidator.ValidationIssue#severity() */ enum Severity { @@ -73,7 +72,7 @@ interface ValidationRound extends AttributeVisitor { * * @return all issues found */ - List getIssues(); + List issues(); /** * Returns true if there were any issues found including warnings or errors. @@ -81,7 +80,7 @@ interface ValidationRound extends AttributeVisitor { * @return true if any issues were found */ default boolean hasIssues() { - return !getIssues().isEmpty(); + return !issues().isEmpty(); } /** @@ -90,7 +89,7 @@ default boolean hasIssues() { * @return true if any issues were found of type {@link ConfigBeanBuilderValidator.Severity#ERROR} */ default boolean hasErrors() { - return getIssues().stream().anyMatch(it -> it.getSeverity() == Severity.ERROR); + return issues().stream().anyMatch(it -> it.severity() == Severity.ERROR); } /** @@ -110,7 +109,10 @@ default boolean hasErrors() { * @param cbType the attribute type * @return the validation round continuation as a fluent-builder */ - ValidationRound validate(String attributeName, Supplier valueSupplier, Class cbType, Map meta); + ValidationRound validate(String attributeName, + Supplier valueSupplier, + Class cbType, + Map meta); @Override default void visit(String attributeName, @@ -149,7 +151,9 @@ class ValidationIssue { * @param attributeName the attribute name in question * @param message the message */ - public ValidationIssue(Severity severity, String attributeName, String message) { + public ValidationIssue(Severity severity, + String attributeName, + String message) { this.severity = Objects.requireNonNull(severity); this.attributeName = Objects.requireNonNull(attributeName); this.message = Objects.requireNonNull(message); @@ -160,7 +164,7 @@ public ValidationIssue(Severity severity, String attributeName, String message) * * @return the severity */ - public Severity getSeverity() { + public Severity severity() { return severity; } @@ -169,7 +173,7 @@ public Severity getSeverity() { * * @return the attribute name */ - public String getAttributeName() { + public String attributeName() { return attributeName; } @@ -178,13 +182,13 @@ public String getAttributeName() { * * @return the user-friendly message */ - public String getMessage() { + public String message() { return message; } @Override public String toString() { - return getMessage(); + return message(); } } diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorHolder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorHolder.java similarity index 91% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorHolder.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorHolder.java index 8072d6430b3..f2d7e646268 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorHolder.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import java.util.ServiceLoader; @@ -38,7 +38,7 @@ private ConfigBeanBuilderValidatorHolder() { * Provides the global service-loader instance of {@link ConfigBeanBuilderValidator}. *

    * Note that the current expectation here is that the global instance is capable for validating any - * {@link io.helidon.pico.builder.config.ConfigBean}-annotated type. The parameter used for the configBeanBuilderType serves + * {@link io.helidon.builder.config.ConfigBean}-annotated type. The parameter used for the configBeanBuilderType serves * to both type case the validator, and also reserves the possibility that the returned instance may be nuanced per builder * type at some point in the future. * diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorProvider.java similarity index 91% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorProvider.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorProvider.java index 43789b7c5c7..ca5eed02aef 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderValidatorProvider.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanBuilderValidatorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; /** * Java {@link java.util.ServiceLoader} provider interface for delivering the {@link ConfigBeanBuilderValidator} instance. diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanInfo.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanInfo.java new file mode 100644 index 00000000000..90b27fef177 --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanInfo.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +import java.util.Map; +import java.util.Objects; + +import io.helidon.builder.Builder; +import io.helidon.builder.config.ConfigBean; + +/** + * Represents all the attributes belonging to {@link io.helidon.builder.config.ConfigBean} available in a + * {@link io.helidon.builder.Builder} style usage pattern. + */ +@Builder(implPrefix = "Meta") +public interface ConfigBeanInfo extends ConfigBean { + + /** + * The attribute name for {@link #value()} ()}. + */ + String TAG_KEY = "value"; + + /** + * The attribute name for {@link #drivesActivation()}. + */ + String TAG_DRIVES_ACTIVATION = "drivesActivation"; + + /** + * The attribute name for {@link #atLeastOne()}. + */ + String TAG_AT_LEAST_ONE = "atLeastOne"; + + /** + * The attribute name for {@link #repeatable()}. + */ + String TAG_REPEATABLE = "repeatable"; + + /** + * The attribute name for {@link #wantDefaultConfigBean()}. + */ + String TAG_WANT_DEFAULT_CONFIG_BEAN = "wantDefaultConfigBean"; + + /** + * The attribute name for {@link #levelType()}. + */ + String TAG_LEVEL_TYPE = "levelType"; + + /** + * Builds meta information appropriate for config integration from a + * {@link io.helidon.builder.config.ConfigBean} instance. This will use the key if {@link #value()} is present, and + * if not present will default to the simple class name of the bean type. + * + * @param val the config bean instance + * @param cfgBeanType the config bean type + * @return the meta information for the config bean + */ + static MetaConfigBeanInfo toMetaConfigBeanInfo(ConfigBean val, + Class cfgBeanType) { + Objects.requireNonNull(val); + Objects.requireNonNull(cfgBeanType); + MetaConfigBeanInfo.Builder builder = MetaConfigBeanInfo.toBuilder(val); + String key = val.value(); + if (!key.isBlank()) { + builder.value(toConfigBeanName(cfgBeanType.getSimpleName())); + } + return builder.build(); + } + + /** + * Builds meta information appropriate for config integration from a + * meta attribute map. + * + * @param meta the meta attribute map + * @return the meta information for the config bean + */ + static MetaConfigBeanInfo toMetaConfigBeanInfo(Map meta) { + return MetaConfigBeanInfo.builder() + .value((String) meta.get("value")) + .repeatable(Boolean.parseBoolean((String) meta.get(TAG_REPEATABLE))) + .drivesActivation(Boolean.parseBoolean((String) meta.get(TAG_DRIVES_ACTIVATION))) + .atLeastOne(Boolean.parseBoolean((String) meta.get(TAG_AT_LEAST_ONE))) + .wantDefaultConfigBean(Boolean.parseBoolean((String) meta.get(TAG_WANT_DEFAULT_CONFIG_BEAN))) + .levelType(LevelType.valueOf((String) meta.get(TAG_LEVEL_TYPE))) + .build(); + } + + /** + * Converts the name (e.g., simple class name) into a config key. + *

    + * Name is camel case - such as WebServer result is dash separated and lower cased web-server. + *

    + * Unlike {@link #toConfigAttributeName(String)}, the behavior here is modified slightly for config bean type names + * in that any configuration ending in "-config" is stripped off as a general convention (e.g., + * "Http2Config" maps to "http2"). + * + * @param name the input name + * @return the config key + */ + // note: a similar method is also found in ConfigMetadataHandler. + static String toConfigBeanName(String name) { + String result = toConfigAttributeName(name); + if (result.endsWith("-config")) { + result = result.substring(0, result.length() - 7); + } + return result; + } + + /** + * Converts the name (e.g., method element name) into a config key. + *

    + * Name is camel case - such as someAttributeValue result is dash separated and lower cased some-attribute-value. + * + * @param name the input name + * @return the config key + */ + static String toConfigAttributeName(String name) { + StringBuilder result = new StringBuilder(name.length() + 5); + + char[] chars = name.toCharArray(); + for (char aChar : chars) { + if (Character.isUpperCase(aChar)) { + if (result.length() == 0) { + result.append(Character.toLowerCase(aChar)); + } else { + result.append('-') + .append(Character.toLowerCase(aChar)); + } + } else { + result.append(aChar); + } + } + + return result.toString(); + } + +} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapper.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapper.java similarity index 63% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapper.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapper.java index e806ecdaf43..a306085377f 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapper.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,13 +14,15 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; + +import java.util.Optional; import io.helidon.common.config.Config; /** * Maps a {@link io.helidon.common.config.Config} instance to a newly created - * {@link io.helidon.pico.builder.config.ConfigBean}-annotated type instance. + * {@link io.helidon.builder.config.ConfigBean}-annotated type instance. */ @FunctionalInterface public interface ConfigBeanMapper /* extends ConfigMapper */ { @@ -28,12 +30,13 @@ public interface ConfigBeanMapper /* extends ConfigMapper */ { /** * Translate the provided configuration into the appropriate config bean for this service type. * - * @param cfg the config + * @param config the config * @param configBeanType the config bean type - * @param the config type - * @param the config bean type - * @return the config bean generated + * @param the config type + * @param the config bean type + * @return the config bean generated, or empty if not possible to create */ - T map(C cfg, Class configBeanType); + Optional toConfigBean(C config, + Class configBeanType); } diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperHolder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperHolder.java similarity index 90% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperHolder.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperHolder.java index 440baf0c5c1..4ac4fc529fc 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperHolder.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import java.util.ServiceLoader; @@ -37,7 +37,7 @@ private ConfigBeanMapperHolder() { * Provides the global service-loader instance of {@link ConfigBeanMapper}. *

    * Note that the current expectation here is that the global instance is capable for mapping any - * {@link io.helidon.pico.builder.config.ConfigBean}-annotated type. The parameter used for the configBeanType serves + * {@link io.helidon.builder.config.ConfigBean}-annotated type. The parameter used for the configBeanType serves * to both type case the mapper, and also reserves the possibility that the returned instance may be nuanced per bean * type at some point in the future. * diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperProvider.java similarity index 90% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperProvider.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperProvider.java index d4faef6026e..df92d380814 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanMapperProvider.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; /** * Java {@link java.util.ServiceLoader} provider interface for delivering the {@link ConfigBeanMapper} instance. diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryHolder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryHolder.java new file mode 100644 index 00000000000..b7f095d1613 --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryHolder.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +import java.util.Optional; +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; + +/** + * Provides access to the active {@link HelidonConfigBeanRegistry} instance. + * + * @see HelidonConfigBeanRegistry + */ +public class ConfigBeanRegistryHolder { + private static final LazyValue> INSTANCE + = LazyValue.create(ConfigBeanRegistryHolder::load); + + private ConfigBeanRegistryHolder() { + } + + /** + * Provides the global service-loader instance of {@link HelidonConfigBeanRegistry}. + * + * @return the config bean registry + */ + public static Optional configBeanRegistry() { + return INSTANCE.get(); + } + + private static Optional load() { + return HelidonServiceLoader + .create(ServiceLoader.load(ConfigBeanRegistryProvider.class, ConfigBeanRegistryProvider.class.getClassLoader())) + .asList() + .stream() + .findFirst() + .map(ConfigBeanRegistryProvider::configBeanRegistry); + } + +} diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryProvider.java new file mode 100644 index 00000000000..66c75819897 --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigBeanRegistryProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +/** + * Java {@link java.util.ServiceLoader} provider interface for delivering the {@link HelidonConfigBeanRegistry} instance. + * + * @see ConfigBeanMapperHolder + */ +@FunctionalInterface +public interface ConfigBeanRegistryProvider { + + /** + * The service-loaded global {@link HelidonConfigBeanRegistry} instance. + * + * @return the global config bean registry instance + */ + HelidonConfigBeanRegistry configBeanRegistry(); + +} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigProvider.java similarity index 86% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigProvider.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigProvider.java index 03178d8df85..4303b784972 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigProvider.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import io.helidon.common.config.Config; /** - * Generated {@link io.helidon.pico.builder.config.ConfigBean}-annotated types and the associated builder types implement + * Generated {@link io.helidon.builder.config.ConfigBean}-annotated types and the associated builder types implement * this contract. */ @FunctionalInterface diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolver.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolver.java similarity index 86% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolver.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolver.java index 2731c257041..6f042bb84c9 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolver.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Collection; import java.util.Map; @@ -26,7 +26,7 @@ public interface ConfigResolver { /** - * Resolves a {@link io.helidon.pico.builder.config.ConfigBean} singular element value from the + * Resolves a {@link io.helidon.builder.config.ConfigBean} singular element value from the * backing {@link io.helidon.common.config.Config}. * * @param ctx the resolution context @@ -40,7 +40,7 @@ Optional of(ResolutionContext ctx, ConfigResolverRequest request); /** - * Resolves a {@link io.helidon.pico.builder.config.ConfigBean} collection-like element value from the + * Resolves a {@link io.helidon.builder.config.ConfigBean} collection-like element value from the * backing {@link io.helidon.common.config.Config}. * * @param ctx the resolution context @@ -54,7 +54,7 @@ Optional> ofCollection(ResolutionContext ctx, ConfigResolverRequest request); /** - * Resolves a {@link io.helidon.pico.builder.config.ConfigBean} map-like element value from the + * Resolves a {@link io.helidon.builder.config.ConfigBean} map-like element value from the * backing {@link io.helidon.common.config.Config}. * * @param ctx the resolution context diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverHolder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverHolder.java similarity index 94% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverHolder.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverHolder.java index fa456d70696..998e28b8c2d 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverHolder.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import java.util.ServiceLoader; diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverMapRequest.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverMapRequest.java similarity index 79% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverMapRequest.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverMapRequest.java index dfa0434dd75..7574b4eb004 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverMapRequest.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverMapRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import io.helidon.builder.Builder; /** - * An extension of {@link io.helidon.pico.builder.config.spi.ConfigResolverRequest} for handling {@link java.util.Map}-like - * requests into {@link io.helidon.pico.builder.config.spi.ConfigResolver}. + * An extension of {@link io.helidon.builder.config.spi.ConfigResolverRequest} for handling {@link java.util.Map}-like + * requests into {@link io.helidon.builder.config.spi.ConfigResolver}. * * @param the key type * @param the value type diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverProvider.java similarity index 90% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverProvider.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverProvider.java index 3fa5c2d8a79..88374f13a7e 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverProvider.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; /** * Java {@link java.util.ServiceLoader} provider interface to find implementation of {@link ConfigResolver}. diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverRequest.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverRequest.java similarity index 88% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverRequest.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverRequest.java index fd1a19c37c2..b2d4862bf3d 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigResolverRequest.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ConfigResolverRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import io.helidon.builder.Builder; /** - * Used in conjunction with {@link io.helidon.pico.builder.config.spi.ConfigResolver}. + * Used in conjunction with {@link io.helidon.builder.config.spi.ConfigResolver}. * * @param the attribute value type being resolved */ diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBean.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBean.java similarity index 78% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBean.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBean.java index 19ee7c36cdb..8667ad01a4f 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBean.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,14 +14,12 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; /** - * Every {@link io.helidon.pico.builder.config.ConfigBean}-annotated type will also implement this contract. - * - * @deprecated this is for internal use only + * Every {@link io.helidon.builder.config.ConfigBean}-annotated type will also implement this contract. */ -public interface ConfigBean extends ConfigBeanCommon { +public interface GeneratedConfigBean extends GeneratedConfigBeanCommon { /* Important Note: caution should be exercised to avoid any 0-arg or 1-arg method. This is because it might clash with generated diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBase.java similarity index 69% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBase.java index 0014605078d..d933c40817f 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,19 +14,17 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import io.helidon.common.config.Config; /** - * Minimal implementation for the {@link ConfigBeanCommon}. This is the base for generated config beans. - * - * @deprecated this is for internal use only + * Minimal implementation for the {@link GeneratedConfigBeanCommon}. This is the base for generated config beans. */ -public abstract class ConfigBeanBase implements ConfigBeanCommon { - private final Config cfg; +public abstract class GeneratedConfigBeanBase implements GeneratedConfigBeanCommon { + private final Config config; private String instanceId; /** @@ -34,16 +32,18 @@ public abstract class ConfigBeanBase implements ConfigBeanCommon { * * @param b the builder * @param instanceId the instance id + * @deprecated not intended to be created directly */ - protected ConfigBeanBase(ConfigBeanBuilder b, - String instanceId) { - this.cfg = b.__config().orElse(null); + @Deprecated + protected GeneratedConfigBeanBase(GeneratedConfigBeanBuilder b, + String instanceId) { + this.config = b.__config().orElse(null); this.instanceId = instanceId; } @Override public Optional __config() { - return Optional.ofNullable(cfg); + return Optional.ofNullable(config); } /** diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilder.java similarity index 80% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilder.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilder.java index 8792a9e7930..d2cae0b476e 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilder.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,16 +14,14 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import io.helidon.common.config.Config; /** - * Every {@link io.helidon.pico.builder.config.ConfigBean}-annotated *builder* type will implement this contract. - * - * @deprecated + * Every {@link io.helidon.builder.config.ConfigBean}-annotated *builder* type will implement this contract. */ -public interface ConfigBeanBuilder extends ConfigBeanCommon { +public interface GeneratedConfigBeanBuilder extends GeneratedConfigBeanCommon, GeneratedConfigBeanMappers { /* Important Note: caution should be exercised to avoid any 0-arg or 1-arg method. This is because it might clash with generated @@ -53,7 +51,7 @@ void acceptConfig(Config cfg, * @throws java.lang.IllegalStateException if there are any resolution or validation errors */ void acceptConfig(Config cfg, - ConfigResolver resolver, - ConfigBeanBuilderValidator validator); + ConfigResolver resolver, + ConfigBeanBuilderValidator validator); } diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderBase.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilderBase.java similarity index 77% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderBase.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilderBase.java index e592c256526..fdd3feb2070 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBuilderBase.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanBuilderBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,25 +14,28 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import io.helidon.common.config.Config; /** - * Minimal implementation for the {@link ConfigBeanBuilder}. - * - * @deprecated this is for internal use only + * Minimal implementation for the {@link GeneratedConfigBeanBuilder}. */ -public abstract class ConfigBeanBuilderBase implements ConfigBeanBuilder { +public abstract class GeneratedConfigBeanBuilderBase implements GeneratedConfigBeanBuilder { private Config cfg; /** - * Default constructor. Reserved for internal use. + * Constructor. + * + * @deprecated not intended to be created directly */ - protected ConfigBeanBuilderBase() { + @Deprecated + protected GeneratedConfigBeanBuilderBase() { } @Override @@ -69,14 +72,15 @@ public void __config(Config cfg) { * @param cfg the config * @param resolver the resolver * @param validator the config bean builder validator + * @param mappers the known config bean mappers related to this config bean context * @return the resolution context */ protected ResolutionContext createResolutionContext(Class configBeanType, Config cfg, ConfigResolver resolver, - ConfigBeanBuilderValidator validator) { - // note to self: that in the future we should probably accept a code-generated 'version id' here --jtrent - return ResolutionContext.create(configBeanType, cfg, resolver, validator); + ConfigBeanBuilderValidator validator, + Map, Function> mappers) { + return ResolutionContext.create(configBeanType, cfg, resolver, validator, mappers); } /** diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanCommon.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanCommon.java similarity index 71% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanCommon.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanCommon.java index 8c589020b27..8787371ff9a 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanCommon.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanCommon.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,20 +14,15 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; - -import java.util.Optional; +package io.helidon.builder.config.spi; import io.helidon.builder.AttributeVisitor; -import io.helidon.common.config.Config; /** - * These methods are common between generated {@link io.helidon.pico.builder.config.ConfigBean}-annotated type, as well + * These methods are common between generated {@link io.helidon.builder.config.ConfigBean}-annotated type, as well * as the associated builder for the same. - * - * @deprecated this is for internal use only */ -public interface ConfigBeanCommon extends ConfigProvider { +public interface GeneratedConfigBeanCommon extends ConfigProvider { /* Important Note: caution should be exercised to avoid any 0-arg or 1-arg method. This is because it might clash with generated @@ -36,15 +31,7 @@ public interface ConfigBeanCommon extends ConfigProvider { */ /** - * Returns any configuration assigned. - * - * @return the optional configuration assigned - */ - @Override - Optional __config(); - - /** - * Returns the {@link io.helidon.pico.builder.config.ConfigBean}-annotated type. + * Returns the {@link io.helidon.builder.config.ConfigBean}-annotated type. * * @return the config bean type */ diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanMappers.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanMappers.java new file mode 100644 index 00000000000..00e82d57f49 --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/GeneratedConfigBeanMappers.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import io.helidon.common.config.Config; + +/** + * Loosely modeled from {@code io.helidon.config.spi.ConfigMapperProvider}. + * + * @see ConfigBeanMapper + */ +public interface GeneratedConfigBeanMappers { + +/* + Important Note: caution should be exercised to avoid any 0-arg or 1-arg method. This is because it might clash with generated + methods. If its necessary to have a 0 or 1-arg method then the convention of prefixing the method with two underscores should be + used. + */ + + /** + * Returns a map of mapper functions associated with appropriate target type ({@code Class}. + * + * @return a map of config mapper functions, never {@code null} + */ + Map, Function> __mappers(); + + /** + * A simple mapping function from config node to a typed value based on the expected class. + * + * @param type type of the expected mapping result + * @param type returned from conversion + * @return function to convert config node to the expected type, or empty if the type is not supported by this provider + */ + @SuppressWarnings("unchecked") + default Optional> __mapper(Class type) { + return Optional.ofNullable((Function) __mappers().get(type)); + } + +} diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigBeanRegistry.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigBeanRegistry.java new file mode 100644 index 00000000000..00001bbfecf --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigBeanRegistry.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +import java.util.Collection; +import java.util.Map; + +/** + * The highest weighted service-loaded instance of this contract will be responsible for managing the active + * {@link io.helidon.builder.config.ConfigBean} instances running in the JVM. + */ +public interface HelidonConfigBeanRegistry { + + /** + * Returns all config beans indexed by its config key. + * + * @param the config bean type + * @return all config beans + */ + Map> allConfigBeans(); + +} diff --git a/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigResolver.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigResolver.java new file mode 100644 index 00000000000..8a1e11684ee --- /dev/null +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/HelidonConfigResolver.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.spi; + +import java.util.ArrayList; +import java.util.Collection; +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.Set; +import java.util.function.Function; + +import io.helidon.common.Builder; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.config.Config; +import io.helidon.common.config.ConfigValue; + +import jakarta.inject.Singleton; + +/** + * The basic implementation of {@link ConfigResolver} simply resolves against {@link io.helidon.common.config.Config} directly, + * not "full" Helidon config. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT - 1) // allow all other creators to take precedence over us... +@SuppressWarnings({"unchecked", "rawtypes"}) +public class HelidonConfigResolver implements ConfigResolver, ConfigResolverProvider { + + /** + * Tag that represents meta information about the attribute. Used in the maps for various methods herein. + */ + public static final String TAG_META = "__meta"; + + /** + * Tag that represents the component type. + */ + protected static final String TAG_COMPONENT_TYPE = "componentType"; + + /** + * Default constructor, service loader invoked. + * + * @deprecated needed for Java service loader + */ + @Deprecated + public HelidonConfigResolver() { + } + + @Override + public ConfigResolver configResolver() { + return this; + } + + @Override + public Optional of(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Config attrCfg = ctx.config().get(request.configKey()); + if (!attrCfg.exists()) { + return Optional.empty(); + } + + return optionalWrappedConfig(ctx, attrCfg, meta, request); + } + + @Override + public Optional> ofCollection(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Config attrCfg = ctx.config().get(request.configKey()); + if (!attrCfg.exists()) { + return Optional.empty(); + } + + return (Optional>) optionalWrappedConfig(ctx, attrCfg, meta, request); + } + + @Override + public Optional> ofMap(ResolutionContext ctx, + Map> meta, + ConfigResolverMapRequest request) { + Config attrCfg = ctx.config().get(request.configKey()); + if (!attrCfg.exists()) { + return Optional.empty(); + } + + return (Optional>) optionalWrappedConfig(ctx, attrCfg, meta, request); + } + + private Optional optionalWrappedConfig(ResolutionContext ctx, + Config attrCfg, + Map> meta, + ConfigResolverRequest request) { + Class componentType = request.valueComponentType().orElse(null); + Class type = request.valueType(); + boolean isOptional = Optional.class.equals(type); + if (isOptional) { + type = request.valueComponentType().orElseThrow(); + } + boolean isList = List.class.isAssignableFrom(type); + boolean isSet = Set.class.isAssignableFrom(type); + boolean isMap = Map.class.isAssignableFrom(type); + + boolean isCharArray = (char[].class == type); + if (isCharArray) { + type = String.class; + } + + Object val; + try { + Function mapper = (componentType == null) ? null : ctx.mappers().get(componentType); + if (mapper != null) { + if (attrCfg.isList() || isMap) { + if (!isList && !isSet && !isMap) { + throw new IllegalStateException("unable to convert node list to " + type + " for " + attrCfg); + } + + List cfgList = new ArrayList<>(); + Map cfgMap = new LinkedHashMap(); + List nodeList = attrCfg.asNodeList().get(); + for (Config subCfg : nodeList) { + Object subVal = Objects.requireNonNull(mapper.apply(subCfg)); + Builder builder = (Builder) subVal; + subVal = builder.build(); + cfgList.add(subVal); + Object prev = cfgMap.put(subCfg.key().name(), subVal); + assert (prev == null) : subCfg; + } + + if (isSet) { + val = new LinkedHashSet<>(cfgList); + } else if (isMap) { + val = cfgMap; + } else { + val = cfgList; + } + } else { + val = Objects.requireNonNull(mapper.apply(attrCfg)); + Builder builder = (Builder) val; + val = builder.build(); + + if (isList) { + val = List.of(val); + } else if (isSet) { + Set set = new LinkedHashSet<>(); + set.add(val); + val = set; + } + } + } else { // no config bean mapper (i.e., unknown component type) + ConfigValue attrVal; + if (isList) { + attrVal = attrCfg.asList(componentType); + val = attrVal.get(); + } else if (isSet) { + attrVal = attrCfg.asList(componentType); + val = new LinkedHashSet<>((List) attrVal.get()); + } else if (isMap) { + attrVal = attrCfg.asMap(); + val = attrVal.get(); + } else { + attrVal = attrCfg.as(type); + val = attrVal.get(); + } + if (isCharArray) { + val = ((String) val).toCharArray(); + } + } + + if (isOptional) { + return Optional.of((T) Optional.of(val)); + } + + return Optional.of((T) val); + } catch (Throwable e) { + String typeName = toTypeNameDescription(request.valueType(), componentType); + String configKey = attrCfg.key().toString(); + throw new IllegalStateException("Failed to convert " + typeName + + " for attribute: " + request.attributeName() + + " and config key: " + configKey, e); + } + } + + private static String toTypeNameDescription(Class type, + Class componentType) { + return type.getTypeName() + "<" + componentType.getTypeName() + ">"; + } + +} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ResolutionContext.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ResolutionContext.java similarity index 71% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ResolutionContext.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/ResolutionContext.java index 515c75dc279..6b15b8adc21 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ResolutionContext.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/ResolutionContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,10 +14,13 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import io.helidon.common.config.Config; @@ -29,6 +32,7 @@ public class ResolutionContext { private final Config cfg; private final ConfigResolver resolver; private final ConfigBeanBuilderValidator validator; + private final Map, Function> mappers; /** * Constructor for this type that takes a builder. @@ -40,6 +44,7 @@ protected ResolutionContext(Builder b) { this.cfg = Objects.requireNonNull(b.cfg); this.resolver = Objects.requireNonNull(b.resolver); this.validator = b.validator; + this.mappers = Map.copyOf(b.mappers); } /** @@ -78,6 +83,15 @@ public Optional> validator() { return Optional.ofNullable(validator); } + /** + * Return the known config bean mappers associated with this config bean context. + * + * @return the config bean mappers + */ + public Map, Function> mappers() { + return mappers; + } + @Override public String toString() { return config().toString(); @@ -96,21 +110,24 @@ public static Builder builder() { /** * Creates a resolution context from the provided arguments. * - * @param configBeanType the config bean type - * @param cfg the config - * @param resolver the resolver - * @param validator the bean builder validator + * @param configBeanType the config bean type + * @param cfg the config + * @param resolver the resolver + * @param validator the bean builder validator + * @param mappers the known config bean mappers related to this config bean context * @return the resolution context */ public static ResolutionContext create(Class configBeanType, Config cfg, ConfigResolver resolver, - ConfigBeanBuilderValidator validator) { + ConfigBeanBuilderValidator validator, + Map, Function> mappers) { return ResolutionContext.builder() .configBeanType(configBeanType) .config(cfg) .resolver(resolver) .validator(validator) + .mappers(mappers) .build(); } @@ -118,6 +135,7 @@ public static ResolutionContext create(Class configBeanType, * Fluent builder for {@link ResolutionContext}. */ public static class Builder implements io.helidon.common.Builder { + private final Map, Function> mappers = new LinkedHashMap<>(); private Class configBeanType; private Config cfg; private ConfigResolver resolver; @@ -147,7 +165,7 @@ public ResolutionContext build() { */ public Builder configBeanType(Class configBeanType) { this.configBeanType = Objects.requireNonNull(configBeanType); - return this; + return identity(); } /** @@ -182,6 +200,33 @@ public Builder validator(ConfigBeanBuilderValidator val) { this.validator = val; return identity(); } + + /** + * Sets the mappers to val. + * + * @param val the value + * @return this fluent builder + */ + public Builder mappers(Map, Function> val) { + Objects.requireNonNull(val); + this.mappers.clear(); + this.mappers.putAll(val); + return identity(); + } + + /** + * Adds a single mapper val. + * + * @param key the key + * @param val the value + * @return this fluent builder + */ + public Builder addMapper(Class key, + Function val) { + Objects.requireNonNull(val); + this.mappers.put(key, val); + return identity(); + } } } diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParser.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParser.java similarity index 67% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParser.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParser.java index 03bfe6d5d19..fa229fdc468 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParser.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; @@ -25,11 +25,12 @@ public interface StringValueParser { /** - * Parse the string into a type R instance. + * Parse the string into a type R instance. Note that this method returns an optional since some parsers may choose to + * map to null for certain types and value combinations (e.g., empty string "" mapping to {@code null} value). * - * @param val the string value to parse - * @param type the type of the result expected - * @param the return type + * @param val the string value to parse + * @param type the type of the result expected + * @param the return type * @return the optional nullable parsed value * @throws java.lang.IllegalArgumentException if the format is not parsable or the return type is not supported */ diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserHolder.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserHolder.java similarity index 94% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserHolder.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserHolder.java index 8ee0f8171f6..6dbac26d8f3 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserHolder.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; import java.util.Optional; import java.util.ServiceLoader; diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserProvider.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserProvider.java similarity index 85% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserProvider.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserProvider.java index 31ec291e252..143ddec7ecb 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/StringValueParserProvider.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/StringValueParserProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; /** * Java {@link java.util.ServiceLoader} provider interface for delivering the {@link StringValueParser} instance. * - * @see io.helidon.pico.builder.config.spi.StringValueParserHolder + * @see io.helidon.builder.config.spi.StringValueParserHolder */ public interface StringValueParserProvider { diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/package-info.java b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/package-info.java similarity index 81% rename from pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/package-info.java rename to builder/builder-config/src/main/java/io/helidon/builder/config/spi/package-info.java index dbf48f19b7e..74486616e35 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/package-info.java +++ b/builder/builder-config/src/main/java/io/helidon/builder/config/spi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,6 +15,6 @@ */ /** - * Helidon Pico Config Builder SPI. + * Helidon ConfigBean Builder SPI. */ -package io.helidon.pico.builder.config.spi; +package io.helidon.builder.config.spi; diff --git a/builder/builder-config/src/main/java/module-info.java b/builder/builder-config/src/main/java/module-info.java new file mode 100644 index 00000000000..d1a1875b5e6 --- /dev/null +++ b/builder/builder-config/src/main/java/module-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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 Helidon Config Builder API / SPI. + */ +module io.helidon.builder.config { + requires static jakarta.annotation; + requires static jakarta.inject; + requires io.helidon.builder; + requires io.helidon.common; + requires io.helidon.common.config; + + uses io.helidon.builder.config.spi.ConfigBeanBuilderValidatorProvider; + uses io.helidon.builder.config.spi.ConfigBeanMapperProvider; + uses io.helidon.builder.config.spi.ConfigBeanRegistryProvider; + uses io.helidon.builder.config.spi.ConfigResolverProvider; + uses io.helidon.builder.config.spi.StringValueParserProvider; + + exports io.helidon.builder.config; + exports io.helidon.builder.config.spi; + + provides io.helidon.builder.config.spi.ConfigResolverProvider + with io.helidon.builder.config.spi.HelidonConfigResolver; +} diff --git a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBeanMapperHolderTest.java b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBeanMapperHolderTest.java similarity index 85% rename from pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBeanMapperHolderTest.java rename to builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBeanMapperHolderTest.java index 0159d448ed7..110da133b8f 100644 --- a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBeanMapperHolderTest.java +++ b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBeanMapperHolderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi.test; +package io.helidon.builder.config; -import io.helidon.pico.builder.config.spi.ConfigBeanMapperHolder; +import io.helidon.builder.config.spi.ConfigBeanMapperHolder; import org.junit.jupiter.api.Test; diff --git a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBuilderValidatorHolderTest.java b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBuilderValidatorHolderTest.java similarity index 85% rename from pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBuilderValidatorHolderTest.java rename to builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBuilderValidatorHolderTest.java index c39c20c2554..a3c3d7de606 100644 --- a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigBuilderValidatorHolderTest.java +++ b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigBuilderValidatorHolderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi.test; +package io.helidon.builder.config; -import io.helidon.pico.builder.config.spi.ConfigBeanBuilderValidatorHolder; +import io.helidon.builder.config.spi.ConfigBeanBuilderValidatorHolder; import org.junit.jupiter.api.Test; diff --git a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigResolverHolderTest.java b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigResolverHolderTest.java similarity index 85% rename from pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigResolverHolderTest.java rename to builder/builder-config/src/test/java/io/helidon/builder/config/ConfigResolverHolderTest.java index 27f6ef116da..4bd3f9e703b 100644 --- a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/ConfigResolverHolderTest.java +++ b/builder/builder-config/src/test/java/io/helidon/builder/config/ConfigResolverHolderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi.test; +package io.helidon.builder.config; -import io.helidon.pico.builder.config.spi.ConfigResolverHolder; +import io.helidon.builder.config.spi.ConfigResolverHolder; import org.junit.jupiter.api.Test; diff --git a/builder/builder-config/src/test/java/io/helidon/builder/config/MetaConfigBeanInfoTest.java b/builder/builder-config/src/test/java/io/helidon/builder/config/MetaConfigBeanInfoTest.java new file mode 100644 index 00000000000..178e28031f1 --- /dev/null +++ b/builder/builder-config/src/test/java/io/helidon/builder/config/MetaConfigBeanInfoTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config; + +import java.util.Map; +import java.util.Objects; + +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@ConfigBean() +class MetaConfigBeanInfoTest { + + @Test + void testToMetaConfigBeanInfoFromConfigBean() { + ConfigBean cfg = Objects.requireNonNull(getClass().getAnnotation(ConfigBean.class)); + MetaConfigBeanInfo metaCfg = ConfigBeanInfo.toMetaConfigBeanInfo(cfg, ConfigBean.class); + assertThat(metaCfg.annotationType(), sameInstance(ConfigBean.class)); + assertThat(metaCfg.repeatable(), is(true)); + assertThat(metaCfg.drivesActivation(), is(false)); + assertThat(metaCfg.atLeastOne(), is(false)); + assertThat(metaCfg.wantDefaultConfigBean(), is(false)); + assertThat(metaCfg.value(), is("")); + } + + @Test + void testToMetaConfigBeanInfoFromMetaAttributes() { + Map metaMap = Map.of(ConfigBeanInfo.TAG_KEY, "fake-config", + ConfigBeanInfo.TAG_REPEATABLE, "true", + ConfigBeanInfo.TAG_DRIVES_ACTIVATION, "true", + ConfigBeanInfo.TAG_AT_LEAST_ONE, "true", + ConfigBeanInfo.TAG_WANT_DEFAULT_CONFIG_BEAN, "true", + ConfigBeanInfo.TAG_LEVEL_TYPE, "ROOT"); + MetaConfigBeanInfo metaCfg = ConfigBeanInfo.toMetaConfigBeanInfo(metaMap); + assertThat(metaCfg.annotationType(), sameInstance(ConfigBean.class)); + assertThat(metaCfg.repeatable(), is(true)); + assertThat(metaCfg.drivesActivation(), is(true)); + assertThat(metaCfg.atLeastOne(), is(true)); + assertThat(metaCfg.wantDefaultConfigBean(), is(true)); + assertThat(metaCfg.levelType(), is(ConfigBean.LevelType.ROOT)); + assertThat(metaCfg.value(), is("fake-config")); + } + + @Test + void toConfigAttributeName() { + assertAll( + () -> assertThat(ConfigBeanInfo.toConfigAttributeName("maxInitialLineLength"), is("max-initial-line-length")), + () -> assertThat(ConfigBeanInfo.toConfigAttributeName("port"), is("port")), + () -> assertThat(ConfigBeanInfo.toConfigAttributeName("listenAddress"), is("listen-address")), + () -> assertThat(ConfigBeanInfo.toConfigAttributeName("Http2Config"), is("http2-config")) + ); + } + + @Test + void toConfigABeanName() { + assertAll( + () -> assertThat(ConfigBeanInfo.toConfigBeanName("MyClient"), is("my-client")), + () -> assertThat(ConfigBeanInfo.toConfigBeanName("Http2Config"), is("http2")), + () -> assertThat(ConfigBeanInfo.toConfigBeanName("Http2ConfigTest"), is("http2-config-test")) + ); + } + +} diff --git a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/StringValueParserHolderTest.java b/builder/builder-config/src/test/java/io/helidon/builder/config/StringValueParserHolderTest.java similarity index 85% rename from pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/StringValueParserHolderTest.java rename to builder/builder-config/src/test/java/io/helidon/builder/config/StringValueParserHolderTest.java index 20586ad83f0..5d4810675b1 100644 --- a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/StringValueParserHolderTest.java +++ b/builder/builder-config/src/test/java/io/helidon/builder/config/StringValueParserHolderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.spi.test; +package io.helidon.builder.config; -import io.helidon.pico.builder.config.spi.StringValueParserHolder; +import io.helidon.builder.config.spi.StringValueParserHolder; import org.junit.jupiter.api.Test; diff --git a/builder/builder/src/main/java/io/helidon/builder/Builder.java b/builder/builder/src/main/java/io/helidon/builder/Builder.java index f005f97e908..83ce2f0d70b 100644 --- a/builder/builder/src/main/java/io/helidon/builder/Builder.java +++ b/builder/builder/src/main/java/io/helidon/builder/Builder.java @@ -43,8 +43,8 @@ */ @SuppressWarnings("rawtypes") @Target(ElementType.TYPE) -// note: runtime retention needed for cases when derived builders are inherited across modules -@Retention(RetentionPolicy.RUNTIME) +// note: class retention needed for cases when derived builders are inherited across modules +@Retention(RetentionPolicy.CLASS) @BuilderTrigger public @interface Builder { @@ -71,7 +71,7 @@ /** * The default value for {@link #includeGeneratedAnnotation()}. */ - boolean DEFAULT_INCLUDE_GENERATED_ANNOTATION = false; + boolean DEFAULT_INCLUDE_GENERATED_ANNOTATION = true; /** * The default value for {@link #defineDefaultMethods()}. diff --git a/builder/builder/src/main/java/io/helidon/builder/BuilderTrigger.java b/builder/builder/src/main/java/io/helidon/builder/BuilderTrigger.java index 668ea0ffd39..70434c273bd 100644 --- a/builder/builder/src/main/java/io/helidon/builder/BuilderTrigger.java +++ b/builder/builder/src/main/java/io/helidon/builder/BuilderTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -29,7 +29,7 @@ * then expected to handle creating the appropriate builder for the given annotation. */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Target(ElementType.ANNOTATION_TYPE) public @interface BuilderTrigger { diff --git a/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java b/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java index d3787ae8a8b..a39e3440253 100644 --- a/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java +++ b/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,9 +19,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Supplier; +import io.helidon.config.metadata.ConfiguredOption; + /** * An implementation of {@link AttributeVisitor} that will validate each attribute to enforce not-null in accordance with * {@link io.helidon.config.metadata.ConfiguredOption#required()}. @@ -67,7 +68,7 @@ public void visit(String attrName, Class type, Class... typeArgument) { String requiredStr = (String) meta.get("required"); - boolean requiredPresent = Objects.nonNull(requiredStr); + boolean requiredPresent = (requiredStr != null); boolean required = Boolean.parseBoolean(requiredStr); if (!required && requiredPresent) { return; @@ -78,7 +79,7 @@ public void visit(String attrName, } Object val = valueSupplier.get(); - if (Objects.nonNull(val)) { + if (val != null && !val.equals(ConfiguredOption.UNCONFIGURED)) { return; } diff --git a/builder/builder/src/main/java/io/helidon/builder/Singular.java b/builder/builder/src/main/java/io/helidon/builder/Singular.java index 0322013e50d..726de9a065c 100644 --- a/builder/builder/src/main/java/io/helidon/builder/Singular.java +++ b/builder/builder/src/main/java/io/helidon/builder/Singular.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -42,7 +42,7 @@ * will used the default method name, dropping any "s" that might be present at the end of the method name (e.g., pickles -> * pickle). * - * @return The singular name to add. + * @return The singular name to add */ String value() default ""; diff --git a/builder/builder/src/main/java/module-info.java b/builder/builder/src/main/java/module-info.java index 13f8575e737..f525b6d3e5f 100644 --- a/builder/builder/src/main/java/module-info.java +++ b/builder/builder/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,5 +18,7 @@ * The Builder API module. */ module io.helidon.builder { + requires static io.helidon.config.metadata; + exports io.helidon.builder; } diff --git a/builder/pom.xml b/builder/pom.xml index e42cfc97dd6..83b12263a72 100644 --- a/builder/pom.xml +++ b/builder/pom.xml @@ -46,6 +46,8 @@ processor-spi processor-tools processor + builder-config + builder-config-processor tests diff --git a/builder/processor-spi/README.md b/builder/processor-spi/README.md index 9e8cee95681..8185ac148b3 100644 --- a/builder/processor-spi/README.md +++ b/builder/processor-spi/README.md @@ -1,3 +1,3 @@ # builder-processor-spi -This module should typically only be used during compile time +This module should typically only be used during compile time, in the APT compiler path only. diff --git a/builder/processor-spi/pom.xml b/builder/processor-spi/pom.xml index 6f46aa9e7f3..0661992d8cc 100644 --- a/builder/processor-spi/pom.xml +++ b/builder/processor-spi/pom.xml @@ -36,10 +36,9 @@ io.helidon.common helidon-common - - io.helidon.pico - helidon-pico-types + io.helidon.common + helidon-common-types io.helidon.builder diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/BuilderCreatorProvider.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/BuilderCreatorProvider.java index e7e3c8f4f67..ea619f9b7a5 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/BuilderCreatorProvider.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/BuilderCreatorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,7 +20,8 @@ import java.util.List; import java.util.Set; -import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.TypeInfo; /** * Java {@link java.util.ServiceLoader} provider interface used to discover builder creators. @@ -53,6 +54,7 @@ public interface BuilderCreatorProvider { * @return the list of TypeAndBody sources to code-generate (tooling will handle the actual code generation aspects), or empty * list to signal that the target type is not handled */ - List create(TypeInfo typeInfo, AnnotationAndValue builderAnnotation); + List create(TypeInfo typeInfo, + AnnotationAndValue builderAnnotation); } diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeAndBody.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeAndBody.java index da6b48a3335..546b30dfc88 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeAndBody.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeAndBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,7 +16,7 @@ package io.helidon.builder.processor.spi; -import io.helidon.pico.types.TypeName; +import io.helidon.common.types.TypeName; /** * The default implementation of {@link TypeAndBody}. diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeAndBody.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeAndBody.java index 699cf86e1b5..0de7f8f3087 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeAndBody.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeAndBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,7 +16,7 @@ package io.helidon.builder.processor.spi; -import io.helidon.pico.types.TypeName; +import io.helidon.common.types.TypeName; /** * Represents the generated source as a model object. diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java deleted file mode 100644 index 361cdf81166..00000000000 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.builder.processor.spi; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; - -/** - * Represents the model object for an interface or an abstract type (i.e., one that was annotated with - * {@link io.helidon.builder.Builder}). - */ -public interface TypeInfo { - - /** - * The type name. - * - * @return the type name - */ - TypeName typeName(); - - /** - * The type element kind. - * - * @return the type element kind (e.g., "INTERFACE", "ANNOTATION_TYPE", etc.) - */ - String typeKind(); - - /** - * The annotations on the type. - * - * @return the annotations on the type - */ - List annotations(); - - /** - * The elements that make up the type that are relevant for processing. - * - * @return the elements that make up the type that are relevant for processing - */ - List elementInfo(); - - /** - * The elements that make up this type that are considered "other", or being skipped because they are irrelevant to processing. - * - * @return the elements that still make up the type, but are otherwise deemed irrelevant for processing - */ - List otherElementInfo(); - - /** - * The parent/super class for this type info. - * - * @return the super type - */ - Optional superTypeInfo(); - - /** - * Element modifiers. - * - * @return element modifiers - */ - Set modifierNames(); - -} diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java index a5fbcad60be..499327c87b2 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java @@ -21,12 +21,13 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.TypeElement; -import io.helidon.pico.types.TypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; /** * Java {@link java.util.ServiceLoader} provider interface used to discover type info creators. *

    - * Used to create a {@link TypeInfo} from the provided arguments. + * Used to create a {@link io.helidon.common.types.TypeInfo} from the provided arguments. */ public interface TypeInfoCreatorProvider { diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/package-info.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/package-info.java index c8d75d6a9e9..aa7ffcc5dfa 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/package-info.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,7 +15,7 @@ */ /** - * The Pico Builder Processor SPI module provides these definitions: + * The Builder Processor SPI module provides these definitions: *

      *
    1. {@link io.helidon.builder.processor.spi.BuilderCreatorProvider} - responsible for code generating the * implementation w/ a fluent builder.
    2. diff --git a/builder/processor-spi/src/main/java/module-info.java b/builder/processor-spi/src/main/java/module-info.java index 71e34822b14..8f118230e95 100644 --- a/builder/processor-spi/src/main/java/module-info.java +++ b/builder/processor-spi/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,7 +20,7 @@ module io.helidon.builder.processor.spi { requires java.compiler; requires io.helidon.builder; - requires io.helidon.pico.types; + requires io.helidon.common.types; requires io.helidon.common; exports io.helidon.builder.processor.spi; diff --git a/builder/processor-tools/README.md b/builder/processor-tools/README.md index f1241a09ca7..3f227ba6390 100644 --- a/builder/processor-tools/README.md +++ b/builder/processor-tools/README.md @@ -1,3 +1,3 @@ # builder-tools -This module should typically only be used during compile time. +This module should typically only be used during compile time, in the APT compiler path only. diff --git a/builder/processor-tools/pom.xml b/builder/processor-tools/pom.xml index 2023f2ca7b3..b46ab613995 100644 --- a/builder/processor-tools/pom.xml +++ b/builder/processor-tools/pom.xml @@ -41,8 +41,8 @@ helidon-builder-processor-spi - io.helidon.pico - helidon-pico-types + io.helidon.common + helidon-common-types io.helidon.common diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java index 93b4786757b..ba6f566493b 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,15 +16,43 @@ package io.helidon.builder.processor.tools; -import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import io.helidon.common.LazyValue; +import io.helidon.common.types.TypeName; + +import static io.helidon.common.types.TypeInfo.KIND_CLASS; +import static io.helidon.common.types.TypeInfo.KIND_ENUM; +import static io.helidon.common.types.TypeInfo.KIND_INTERFACE; +import static io.helidon.common.types.TypeInfo.KIND_PACKAGE; +import static io.helidon.common.types.TypeInfo.KIND_RECORD; +import static io.helidon.common.types.TypeInfo.MODIFIER_ABSTRACT; +import static io.helidon.common.types.TypeInfo.MODIFIER_FINAL; +import static io.helidon.common.types.TypeInfo.MODIFIER_PRIVATE; +import static io.helidon.common.types.TypeInfo.MODIFIER_PROTECTED; +import static io.helidon.common.types.TypeInfo.MODIFIER_PUBLIC; +import static io.helidon.common.types.TypeInfo.MODIFIER_STATIC; + /** * Provides functions to aid with bean naming and parsing. */ public class BeanUtils { + private static final LazyValue> RESERVED = LazyValue.create( + Set.of(KIND_CLASS, + KIND_INTERFACE, + KIND_PACKAGE, + KIND_ENUM, + MODIFIER_STATIC, + MODIFIER_FINAL, + MODIFIER_PUBLIC, + MODIFIER_PROTECTED, + MODIFIER_PRIVATE, + KIND_RECORD, + MODIFIER_ABSTRACT + )); private BeanUtils() { } @@ -110,10 +138,17 @@ public static boolean validateAndParseMethodName(String methodName, * @return true if it appears to be a reserved word */ public static boolean isReservedWord(String word) { - word = word.toLowerCase(); - return word.equals("class") || word.equals("interface") || word.equals("package") || word.equals("static") - || word.equals("final") || word.equals("public") || word.equals("protected") || word.equals("private") - || word.equals("abstract"); + return RESERVED.get().contains(word.toUpperCase()); + } + + /** + * Returns true if the given type is known to be a built-in java type (e.g., package name starts with "java"). + * + * @param type the fully qualified type name + * @return true if the given type is definitely known to be built-in Java type + */ + public static boolean isBuiltInJavaType(TypeName type) { + return type.primitive() || type.name().startsWith("java."); } private static boolean validMethod(String name, @@ -129,14 +164,14 @@ private static boolean validMethod(String name, c = Character.toLowerCase(c); String altName = "" + c + attrName.substring(1); - attributeNameRef.set(Optional.of(Collections.singletonList(isReservedWord(altName) ? name : altName))); + attributeNameRef.set(Optional.of(List.of(isReservedWord(altName) ? name : altName))); return true; } private static boolean validBooleanIsMethod(String name, - AtomicReference>> attributeNameRef, - boolean throwIfInvalid) { + AtomicReference>> attributeNameRef, + boolean throwIfInvalid) { assert (name.trim().equals(name)); char c = name.charAt(2); @@ -151,7 +186,9 @@ private static boolean validBooleanIsMethod(String name, return true; } - private static boolean validMethodCase(String name, char c, boolean throwIfInvalid) { + private static boolean validMethodCase(String name, + char c, + boolean throwIfInvalid) { if (!Character.isAlphabetic(c)) { return invalidMethod(name, throwIfInvalid, @@ -167,7 +204,9 @@ private static boolean validMethodCase(String name, char c, boolean throwIfInval return true; } - private static boolean invalidMethod(String methodName, boolean throwIfInvalid, String message) { + private static boolean invalidMethod(String methodName, + boolean throwIfInvalid, + String message) { if (throwIfInvalid) { throw new RuntimeException(message + ": " + methodName); } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java index 62f7e023822..633ed71cabc 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java @@ -17,7 +17,6 @@ package io.helidon.builder.processor.tools; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -26,13 +25,16 @@ import java.util.concurrent.atomic.AtomicReference; import io.helidon.builder.Builder; -import io.helidon.builder.processor.spi.TypeInfo; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.DefaultAnnotationAndValue; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; - +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; + +import static io.helidon.builder.processor.tools.BeanUtils.isBooleanType; +import static io.helidon.builder.processor.tools.BeanUtils.isReservedWord; +import static io.helidon.builder.processor.tools.BeanUtils.validateAndParseMethodName; import static io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider.BUILDER_ANNO_TYPE_NAME; import static io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider.DEFAULT_INCLUDE_META_ATTRIBUTES; import static io.helidon.builder.processor.tools.DefaultBuilderCreatorProvider.DEFAULT_LIST_TYPE; @@ -55,8 +57,6 @@ public class BodyContext { 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; @@ -77,6 +77,8 @@ public class BodyContext { private final String publicOrPackagePrivateDecl; private final TypeName interceptorTypeName; private final String interceptorCreateMethod; + private final TypeName parentTypeName; + private final TypeName parentAnnotationTypeName; /** * Constructor. @@ -104,19 +106,14 @@ public class BodyContext { this.listType = toListImplType(builderTriggerAnnotation, typeInfo); this.mapType = toMapImplType(builderTriggerAnnotation, typeInfo); this.setType = toSetImplType(builderTriggerAnnotation, typeInfo); - try { - gatherAllAttributeNames(this, typeInfo); - } catch (Exception e) { - throw new IllegalStateException("Failed while processing: " + typeInfo.typeName(), e); - } + this.parentTypeName = toParentTypeName(builderTriggerAnnotation, typeInfo); + this.parentAnnotationTypeName = toParentAnnotationTypeName(typeInfo); + gatherAllAttributeNames(typeInfo); assert (allTypeInfos.size() == allAttributeNames.size()); - this.hasParent = (parentTypeName.get() != null && hasBuilder(typeInfo.superTypeInfo(), builderTriggerAnnotation)); + this.hasParent = (parentTypeName != null && hasBuilder(typeInfo.superTypeInfo().orElse(null), builderTriggerAnnotation)); this.hasAnyBuilderClashingMethodNames = determineIfHasAnyClashingMethodNames(); - this.isExtendingAnAbstractClass = typeInfo.typeKind().equals("CLASS"); - this.ctorBuilderAcceptTypeName = (hasParent) - ? typeInfo.typeName() - : (Objects.nonNull(parentAnnotationType.get()) && typeInfo.elementInfo().isEmpty() - ? typeInfo.superTypeInfo().orElseThrow().typeName() : typeInfo.typeName()); + this.isExtendingAnAbstractClass = typeInfo.typeKind().equals(TypeInfo.KIND_CLASS); + this.ctorBuilderAcceptTypeName = toCtorBuilderAcceptTypeName(typeInfo, hasParent, parentAnnotationTypeName); this.genericBuilderClassDecl = "Builder"; this.genericBuilderAliasDecl = ("B".equals(typeInfo.typeName().className())) ? "BU" : "B"; this.genericBuilderAcceptAliasDecl = ("T".equals(typeInfo.typeName().className())) ? "TY" : "T"; @@ -127,9 +124,10 @@ public class BodyContext { searchForBuilderAnnotation("interceptorCreateMethod", builderTriggerAnnotation, typeInfo); this.interceptorCreateMethod = (interceptorCreateMethod == null || interceptorCreateMethod.isEmpty()) ? null : interceptorCreateMethod; - this.publicOrPackagePrivateDecl = (typeInfo.typeKind().equals("INTERFACE") + this.publicOrPackagePrivateDecl = (typeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE) || typeInfo.modifierNames().isEmpty() - || typeInfo.modifierNames().contains("PUBLIC")) ? "public " : ""; + || typeInfo.modifierNames().contains(TypeInfo.MODIFIER_PUBLIC)) + ? "public " : ""; } @Override @@ -206,17 +204,17 @@ public List allAttributeNames() { * * @return the parent type name */ - public AtomicReference parentTypeName() { - return parentTypeName; + public Optional parentTypeName() { + return Optional.ofNullable(parentTypeName); } /** - * Returns the parent annotation type. + * Returns the parent annotation type name. * * @return the parent annotation type */ - protected AtomicReference parentAnnotationType() { - return parentAnnotationType; + protected Optional parentAnnotationTypeName() { + return Optional.ofNullable(parentAnnotationTypeName); } /** @@ -440,8 +438,8 @@ public boolean hasOtherMethod(String 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(); + validateAndParseMethodName(method.elementName(), method.typeName().name(), isBeanStyleRequired, refAttrNames); + List attrNames = (refAttrNames.get().isEmpty()) ? List.of() : refAttrNames.get().get(); if (!isBeanStyleRequired) { return (!attrNames.isEmpty()) ? attrNames.get(0) : method.elementName(); } @@ -503,15 +501,6 @@ private static boolean toIncludeGeneratedAnnotation(AnnotationAndValue builderTr return (val == null) ? Builder.DEFAULT_INCLUDE_GENERATED_ANNOTATION : Boolean.parseBoolean(val); } - /** - * In support of {@link io.helidon.builder.Builder#defineDefaultMethods()}. - */ - private static boolean toDefineDefaultMethods(AnnotationAndValue builderTriggerAnnotation, - TypeInfo typeInfo) { - String val = searchForBuilderAnnotation("defineDefaultMethods", builderTriggerAnnotation, typeInfo); - return (val == null) ? Builder.DEFAULT_DEFINE_DEFAULT_METHODS : Boolean.parseBoolean(val); - } - /** * In support of {@link io.helidon.builder.Builder#listImplType()}. */ @@ -550,7 +539,7 @@ private static String searchForBuilderAnnotation(String key, if (!builderTriggerAnnotation.typeName().equals(BUILDER_ANNO_TYPE_NAME)) { AnnotationAndValue builderAnnotation = DefaultAnnotationAndValue .findFirst(BUILDER_ANNO_TYPE_NAME.name(), typeInfo.annotations()).orElse(null); - if (Objects.nonNull(builderAnnotation)) { + if (builderAnnotation != null) { val = builderAnnotation.value(key).orElse(null); } } @@ -562,70 +551,60 @@ private static String searchForBuilderAnnotation(String key, return val; } - private static void gatherAllAttributeNames(BodyContext ctx, - TypeInfo typeInfo) { + private void gatherAllAttributeNames(TypeInfo typeInfo) { TypeInfo superTypeInfo = typeInfo.superTypeInfo().orElse(null); - if (Objects.nonNull(superTypeInfo)) { + if (superTypeInfo != null) { Optional superBuilderAnnotation = DefaultAnnotationAndValue - .findFirst(ctx.builderTriggerAnnotation.typeName().name(), superTypeInfo.annotations()); + .findFirst(builderTriggerAnnotation.typeName().name(), superTypeInfo.annotations()); if (superBuilderAnnotation.isEmpty()) { - gatherAllAttributeNames(ctx, superTypeInfo); + gatherAllAttributeNames(superTypeInfo); } else { - populateMap(ctx.map, superTypeInfo, ctx.beanStyleRequired); - } - - if (Objects.isNull(ctx.parentTypeName.get()) - && superTypeInfo.typeKind().equals(DefaultBuilderCreatorProvider.INTERFACE)) { - ctx.parentTypeName.set(superTypeInfo.typeName()); - } else if (Objects.isNull(ctx.parentAnnotationType.get()) - && superTypeInfo.typeKind().equals("ANNOTATION_TYPE")) { - ctx.parentAnnotationType.set(superTypeInfo.typeName()); + populateMap(map, superTypeInfo, beanStyleRequired); } } for (TypedElementName method : typeInfo.elementInfo()) { - String beanAttributeName = toBeanAttributeName(method, ctx.beanStyleRequired); - TypedElementName existing = ctx.map.get(beanAttributeName); - if (Objects.nonNull(existing) - && BeanUtils.isBooleanType(method.typeName().name()) + String beanAttributeName = toBeanAttributeName(method, beanStyleRequired); + TypedElementName existing = map.get(beanAttributeName); + if (existing != null + && isBooleanType(method.typeName().name()) && method.elementName().startsWith("is")) { AtomicReference>> alternateNames = new AtomicReference<>(); - BeanUtils.validateAndParseMethodName(method.elementName(), + 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() + String currentAttrName = beanAttributeName; + Optional alternateName = alternateNames.get().orElse(List.of()).stream() .filter(it -> !it.equals(currentAttrName)) .findFirst(); - if (alternateName.isPresent() && !ctx.map.containsKey(alternateName.get()) - && !BeanUtils.isReservedWord(alternateName.get())) { + if (alternateName.isPresent() && !map.containsKey(alternateName.get()) + && !isReservedWord(alternateName.get())) { beanAttributeName = alternateName.get(); - existing = ctx.map.get(beanAttributeName); + existing = map.get(beanAttributeName); } } - if (Objects.nonNull(existing)) { + if (existing != null) { 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); + Objects.requireNonNull(map.put(beanAttributeName, method)); + int pos = allAttributeNames.indexOf(beanAttributeName); if (pos >= 0) { - ctx.allTypeInfos.set(pos, method); + allTypeInfos.set(pos, method); } continue; } - Object prev = ctx.map.put(beanAttributeName, method); - assert (Objects.isNull(prev)); + Object prev = map.put(beanAttributeName, method); + assert (prev == null); - ctx.allTypeInfos.add(method); - if (ctx.allAttributeNames.contains(beanAttributeName)) { + allTypeInfos.add(method); + if (allAttributeNames.contains(beanAttributeName)) { throw new IllegalStateException("duplicate attribute name: " + beanAttributeName + " processing " + typeInfo); } - ctx.allAttributeNames.add(beanAttributeName); + allAttributeNames.add(beanAttributeName); } } @@ -639,7 +618,7 @@ private static void populateMap(Map map, for (TypedElementName method : typeInfo.elementInfo()) { String beanAttributeName = toBeanAttributeName(method, isBeanStyleRequired); TypedElementName existing = map.get(beanAttributeName); - if (Objects.nonNull(existing)) { + if (existing != null) { if (!existing.typeName().equals(method.typeName())) { throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); } @@ -648,7 +627,7 @@ private static void populateMap(Map map, Objects.requireNonNull(map.put(beanAttributeName, method)); } else { Object prev = map.put(beanAttributeName, method); - assert (Objects.isNull(prev)); + assert (prev == null); } } } @@ -663,16 +642,55 @@ private boolean isBuilderClashingMethodName(String beanAttributeName) { || beanAttributeName.equals("toStringInner"); } - private boolean hasBuilder(Optional typeInfo, AnnotationAndValue builderTriggerAnnotation) { - if (typeInfo.isEmpty()) { + private boolean hasBuilder(TypeInfo typeInfo, + AnnotationAndValue builderTriggerAnnotation) { + if (typeInfo == null) { return false; } TypeName builderAnnoTypeName = builderTriggerAnnotation.typeName(); - boolean hasBuilder = typeInfo.get().annotations().stream() + boolean hasBuilder = typeInfo.annotations().stream() .map(AnnotationAndValue::typeName) .anyMatch(it -> it.equals(builderAnnoTypeName)); - return hasBuilder || hasBuilder(typeInfo.get().superTypeInfo(), builderTriggerAnnotation); + return hasBuilder || hasBuilder(typeInfo.superTypeInfo().orElse(null), builderTriggerAnnotation); + } + + private static TypeName toCtorBuilderAcceptTypeName(TypeInfo typeInfo, + boolean hasParent, + TypeName parentAnnotationTypeName) { + if (hasParent) { + return typeInfo.typeName(); + } + + return (parentAnnotationTypeName != null && typeInfo.elementInfo().isEmpty() + ? typeInfo.superTypeInfo().orElseThrow().typeName() : typeInfo.typeName()); + } + + private static TypeName toParentTypeName(AnnotationAndValue builderTriggerAnnotation, + TypeInfo typeInfo) { + TypeInfo superTypeInfo = typeInfo.superTypeInfo().orElse(null); + if (superTypeInfo != null) { + Optional superBuilderAnnotation = DefaultAnnotationAndValue + .findFirst(builderTriggerAnnotation.typeName().name(), superTypeInfo.annotations()); + if (superBuilderAnnotation.isEmpty()) { + return toParentTypeName(builderTriggerAnnotation, superTypeInfo); + } + + if (superTypeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE)) { + return superTypeInfo.typeName(); + } + } + + return null; + } + + private static TypeName toParentAnnotationTypeName(TypeInfo typeInfo) { + TypeInfo superTypeInfo = typeInfo.superTypeInfo().orElse(null); + if (superTypeInfo != null && superTypeInfo.typeKind().equals(TypeInfo.KIND_ANNOTATION_TYPE)) { + return superTypeInfo.typeName(); + } + + return null; } } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java index b9edbf070fc..3c3da5f6cff 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java @@ -44,17 +44,19 @@ import javax.lang.model.util.Elements; import javax.tools.Diagnostic; -import io.helidon.builder.processor.spi.DefaultTypeInfo; -import io.helidon.builder.processor.spi.TypeInfo; import io.helidon.builder.processor.spi.TypeInfoCreatorProvider; import io.helidon.common.Weight; import io.helidon.common.Weighted; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.DefaultAnnotationAndValue; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.DefaultTypedElementName; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeInfo; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; + +import static io.helidon.builder.processor.tools.BeanUtils.isBuiltInJavaType; /** * The default implementation for {@link io.helidon.builder.processor.spi.TypeInfoCreatorProvider}. This also contains an abundance of @@ -62,36 +64,27 @@ */ @Weight(Weighted.DEFAULT_WEIGHT - 1) public class BuilderTypeTools implements TypeInfoCreatorProvider { - private static final boolean ACCEPT_ABSTRACT_CLASS_TARGETS = true; - /** * Default constructor. Service loaded. * - * @deprecated + * @deprecated needed for service loader */ - // note: this needs to remain public since it will be resolved via service loader ... @Deprecated public BuilderTypeTools() { } @Override - public Optional createTypeInfo( - TypeName annotationTypeName, - TypeName typeName, - TypeElement element, - ProcessingEnvironment processingEnv, - boolean wantDefaultMethods) { + @SuppressWarnings("unchecked") + public Optional createTypeInfo(TypeName annotationTypeName, + TypeName typeName, + TypeElement element, + ProcessingEnvironment processingEnv, + boolean wantDefaultMethods) { Objects.requireNonNull(annotationTypeName); if (typeName.name().equals(Annotation.class.getName())) { return Optional.empty(); } - if (!isAcceptableBuilderTarget(element)) { - String msg = annotationTypeName + " is not intended to be targeted to this type: " + element; - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg); - throw new IllegalStateException(msg); - } - List problems = element.getEnclosedElements().stream() .filter(it -> it.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) @@ -113,37 +106,53 @@ public Optional createTypeInfo( return Optional.of(DefaultTypeInfo.builder() .typeName(typeName) .typeKind(String.valueOf(element.getKind())) - .annotations(BuilderTypeTools - .createAnnotationAndValueListFromElement(element, - processingEnv.getElementUtils())) + .annotations( + createAnnotationAndValueListFromElement(element, processingEnv.getElementUtils())) .elementInfo(elementInfo) .otherElementInfo(otherElementInfo) + .referencedTypeNamesToAnnotations( + toReferencedTypeNamesAndAnnotations( + processingEnv, typeName, elementInfo, otherElementInfo)) .modifierNames(modifierNames) .update(it -> toTypeInfo(annotationTypeName, element, processingEnv, wantDefaultMethods) .ifPresent(it::superTypeInfo)) .build()); } - /** - * Determines if the target element with the {@link io.helidon.builder.Builder} annotation is an acceptable element type. - * If it is not acceptable then the caller is expected to throw an exception or log an error, etc. - * - * @param element the element - * @return true if the element is acceptable - */ - protected boolean isAcceptableBuilderTarget( - Element element) { - final ElementKind kind = element.getKind(); - final Set modifiers = element.getModifiers(); - boolean isAcceptable = (kind == ElementKind.INTERFACE - || kind == ElementKind.ANNOTATION_TYPE - || (ACCEPT_ABSTRACT_CLASS_TARGETS - && (kind == ElementKind.CLASS && modifiers.contains(Modifier.ABSTRACT)))); - return isAcceptable; + @SuppressWarnings("unchecked") + private Map> toReferencedTypeNamesAndAnnotations(ProcessingEnvironment processingEnv, + TypeName typeName, + Collection... refs) { + Map> result = new LinkedHashMap<>(); + for (Collection ref : refs) { + for (TypedElementName typedElementName : ref) { + collectReferencedTypeNames(result, processingEnv, typeName, List.of(typedElementName.typeName())); + collectReferencedTypeNames(result, processingEnv, typeName, typedElementName.typeName().typeArguments()); + } + } + return result; + } + + private void collectReferencedTypeNames(Map> result, + ProcessingEnvironment processingEnv, + TypeName typeName, + Collection referencedColl) { + for (TypeName referenced : referencedColl) { + if (isBuiltInJavaType(referenced) || typeName.equals(referenced)) { + continue; + } + + // first time processing, we only need to do this on pass #1 + result.computeIfAbsent(referenced, (k) -> { + TypeElement typeElement = processingEnv.getElementUtils().getTypeElement(k.name()); + return (typeElement == null) + ? null : createAnnotationAndValueListFromElement(typeElement, processingEnv.getElementUtils()); + }); + } } /** - * Translation the arguments to a collection of {@link io.helidon.pico.types.TypedElementName}'s. + * Translation the arguments to a collection of {@link io.helidon.common.types.TypedElementName}'s. * * @param element the typed element (i.e., class) * @param processingEnv the processing env @@ -151,11 +160,10 @@ protected boolean isAcceptableBuilderTarget( * @param wantDefaultMethods true to process {@code default} methods * @return the collection of typed elements */ - protected Collection toElementInfo( - TypeElement element, - ProcessingEnvironment processingEnv, - boolean wantWhatWeCanAccept, - boolean wantDefaultMethods) { + protected Collection toElementInfo(TypeElement element, + ProcessingEnvironment processingEnv, + boolean wantWhatWeCanAccept, + boolean wantDefaultMethods) { return element.getEnclosedElements().stream() .filter(it -> it.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) @@ -172,9 +180,8 @@ protected Collection toElementInfo( * @param defineDefaultMethods true if we should also process default methods * @return true if not able to accept */ - protected boolean canAccept( - ExecutableElement ee, - boolean defineDefaultMethods) { + protected boolean canAccept(ExecutableElement ee, + boolean defineDefaultMethods) { Set mods = ee.getModifiers(); if (mods.contains(Modifier.ABSTRACT)) { return true; @@ -189,11 +196,10 @@ protected boolean canAccept( return false; } - private Optional toTypeInfo( - TypeName annotationTypeName, - TypeElement element, - ProcessingEnvironment processingEnv, - boolean wantDefaultMethods) { + private Optional toTypeInfo(TypeName annotationTypeName, + TypeElement element, + ProcessingEnvironment processingEnv, + boolean wantDefaultMethods) { List ifaces = element.getInterfaces(); if (ifaces.size() > 1) { processingEnv.getMessager() @@ -220,8 +226,7 @@ private Optional toTypeInfo( * @param typeMirror the type mirror * @return the type element */ - public static Optional toTypeElement( - TypeMirror typeMirror) { + public static Optional toTypeElement(TypeMirror typeMirror) { if (TypeKind.DECLARED == typeMirror.getKind()) { TypeElement te = (TypeElement) ((DeclaredType) typeMirror).asElement(); return (te.toString().equals(Object.class.getName())) ? Optional.empty() : Optional.of(te); @@ -235,8 +240,7 @@ public static Optional toTypeElement( * @param type the element type * @return the associated type name instance */ - public static Optional createTypeNameFromDeclaredType( - DeclaredType type) { + public static Optional createTypeNameFromDeclaredType(DeclaredType type) { return createTypeNameFromElement(type.asElement()); } @@ -246,8 +250,7 @@ public static Optional createTypeNameFromDeclaredType( * @param type the element type * @return the associated type name instance */ - public static Optional createTypeNameFromElement( - Element type) { + public static Optional createTypeNameFromElement(Element type) { if (type instanceof VariableElement) { return createTypeNameFromMirror(type.asType()); } @@ -258,7 +261,7 @@ public static Optional createTypeNameFromElement( List classNames = new ArrayList<>(); classNames.add(type.getSimpleName().toString()); - while (Objects.nonNull(type.getEnclosingElement()) + while (type.getEnclosingElement() != null && ElementKind.PACKAGE != type.getEnclosingElement().getKind()) { classNames.add(type.getEnclosingElement().getSimpleName().toString()); type = type.getEnclosingElement(); @@ -277,8 +280,7 @@ public static Optional createTypeNameFromElement( * @param typeMirror the type mirror * @return the type name associated with the type mirror, or empty for generic type variables */ - public static Optional createTypeNameFromMirror( - TypeMirror typeMirror) { + public static Optional createTypeNameFromMirror(TypeMirror typeMirror) { TypeKind kind = typeMirror.getKind(); if (kind.isPrimitive()) { Class type; @@ -355,9 +357,8 @@ public static Optional createTypeNameFromMirror( * @param ams the collection to search through * @return the annotation mirror, or empty if not found */ - public static Optional findAnnotationMirror( - String annotationType, - Collection ams) { + public static Optional findAnnotationMirror(String annotationType, + Collection ams) { return ams.stream() .filter(it -> annotationType.equals(it.getAnnotationType().toString())) .findFirst(); @@ -370,9 +371,8 @@ public static Optional findAnnotationMirror( * @param elements the elements * @return the new instance or empty if the annotation mirror passed is invalid */ - public static Optional createAnnotationAndValueFromMirror( - AnnotationMirror am, - Elements elements) { + public static Optional createAnnotationAndValueFromMirror(AnnotationMirror am, + Elements elements) { Optional val = createTypeNameFromMirror(am.getAnnotationType()); return val.map(it -> DefaultAnnotationAndValue.create(it, extractValues(am, elements))); @@ -385,9 +385,8 @@ public static Optional createAnnotationAndValueFromMirror( * @param elements the elements * @return the list of annotations extracted from the element */ - public static List createAnnotationAndValueListFromElement( - Element e, - Elements elements) { + public static List createAnnotationAndValueListFromElement(Element e, + Elements elements) { return e.getAnnotationMirrors().stream().map(it -> createAnnotationAndValueFromMirror(it, elements)) .filter(Optional::isPresent) .map(Optional::orElseThrow) @@ -398,12 +397,11 @@ public static List createAnnotationAndValueListFromElement( * Extracts values from the annotation mirror value. * * @param am the annotation mirror - * @param elements the optional elements + * @param elements the elements * @return the extracted values */ - public static Map extractValues( - AnnotationMirror am, - Elements elements) { + public static Map extractValues(AnnotationMirror am, + Elements elements) { return extractValues(elements.getElementValuesWithDefaults(am)); } @@ -413,13 +411,12 @@ public static Map extractValues( * @param values the element values * @return the extracted values */ - public static Map extractValues( - Map values) { + public static Map extractValues(Map values) { Map result = new LinkedHashMap<>(); values.forEach((el, val) -> { String name = el.getSimpleName().toString(); String value = val.accept(new ToStringAnnotationValueVisitor(), null); - if (Objects.nonNull(value)) { + if (value != null) { result.put(name, value); } }); @@ -427,16 +424,27 @@ public static Map extractValues( } /** - * Creates an instance of a {@link io.helidon.pico.types.TypedElementName} given its type and variable element from + * Extracts the singular {@code value()} value. Return value will always be non-null. + * + * @param am the annotation mirror + * @param elements the elements + * @return the extracted values + */ + public static String extractValue(AnnotationMirror am, + Elements elements) { + return Objects.requireNonNull(extractValues(elements.getElementValuesWithDefaults(am)).get("value")); + } + + /** + * Creates an instance of a {@link io.helidon.common.types.TypedElementName} given its type and variable element from * annotation processing. * * @param v the element (from annotation processing) * @param elements the elements * @return the created instance */ - public static TypedElementName createTypedElementNameFromElement( - Element v, - Elements elements) { + public static TypedElementName createTypedElementNameFromElement(Element v, + Elements elements) { TypeName type = createTypeNameFromElement(v).orElse(null); List componentTypeNames = null; String defaultValue = null; @@ -470,26 +478,27 @@ public static TypedElementName createTypedElementNameFromElement( } componentTypeNames = (componentTypeNames == null) ? List.of() : componentTypeNames; - return DefaultTypedElementName.builder() + DefaultTypedElementName.Builder builder = DefaultTypedElementName.builder() .typeName(type) .componentTypeNames(componentTypeNames) .elementName(v.getSimpleName().toString()) .elementKind(v.getKind().name()) - .defaultValue(defaultValue) .annotations(createAnnotationAndValueListFromElement(v, elements)) .elementTypeAnnotations(elementTypeAnnotations) - .modifierNames(modifierNames) - .build(); + .modifierNames(modifierNames); + + Optional.ofNullable(defaultValue).ifPresent(builder::defaultValue); + + return builder.build(); } /** * Helper method to determine if the value is present (i.e., non-null and non-blank). * * @param val the value to check - * @return true if the value provided is non-null and non-blank. + * @return true if the value provided is non-null and non-blank */ - static boolean hasNonBlankValue( - String val) { + static boolean hasNonBlankValue(String val) { return (val != null) && !val.isBlank(); } @@ -500,10 +509,21 @@ static boolean hasNonBlankValue( * @param versionId the generator version identifier * @return the generated sticker */ - public static String generatedStickerFor( - String generatorClassTypeName, - String versionId) { + public static String generatedStickerFor(String generatorClassTypeName, + String versionId) { return "value = \"" + Objects.requireNonNull(generatorClassTypeName) + "\", comments = \"version=" + versionId + "\""; } + + /** + * Produces the generated copy right header on code generated artifacts. + * + * @param generatorClassTypeName the generator class type name + * @return the generated comments + */ + public static String copyrightHeaderFor(String generatorClassTypeName) { + return "// This is a generated file (powered by Helidon). " + + "Do not edit or extend from this artifact as it is subject to change at any time!"; + } + } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java index 1c619b2cc12..341fba3df00 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java @@ -20,8 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -40,25 +39,26 @@ import io.helidon.builder.processor.spi.BuilderCreatorProvider; import io.helidon.builder.processor.spi.DefaultTypeAndBody; import io.helidon.builder.processor.spi.TypeAndBody; -import io.helidon.builder.processor.spi.TypeInfo; import io.helidon.common.Weight; import io.helidon.common.Weighted; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.DefaultAnnotationAndValue; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; import static io.helidon.builder.processor.tools.BodyContext.TAG_META_PROPS; import static io.helidon.builder.processor.tools.BodyContext.toBeanAttributeName; +import static io.helidon.builder.processor.tools.BuilderTypeTools.copyrightHeaderFor; +import static io.helidon.builder.processor.tools.BuilderTypeTools.hasNonBlankValue; /** * Default implementation for {@link io.helidon.builder.processor.spi.BuilderCreatorProvider}. */ @Weight(Weighted.DEFAULT_WEIGHT - 1) // allow all other creators to take precedence over us... public class DefaultBuilderCreatorProvider implements BuilderCreatorProvider { - static final String INTERFACE = "INTERFACE"; 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; @@ -81,7 +81,7 @@ public DefaultBuilderCreatorProvider() { @Override public Set> supportedAnnotationTypes() { - return Collections.singleton(Builder.class); + return Set.of(Builder.class); } @Override @@ -89,10 +89,10 @@ public List create(TypeInfo typeInfo, AnnotationAndValue builderAnnotation) { try { TypeName abstractImplTypeName = toAbstractImplTypeName(typeInfo.typeName(), builderAnnotation); - TypeName implTypeName = toImplTypeName(typeInfo.typeName(), builderAnnotation); + TypeName implTypeName = toBuilderImplTypeName(typeInfo.typeName(), builderAnnotation); preValidate(implTypeName, typeInfo, builderAnnotation); - LinkedList builds = new LinkedList<>(); + List builds = new ArrayList<>(); builds.add(DefaultTypeAndBody.builder() .typeName(abstractImplTypeName) .body(toBody(createBodyContext(false, abstractImplTypeName, typeInfo, builderAnnotation))) @@ -118,6 +118,25 @@ public List create(TypeInfo typeInfo, protected void preValidate(TypeName implTypeName, TypeInfo typeInfo, AnnotationAndValue builderAnnotation) { + assertNoDuplicateSingularNames(typeInfo); + } + + private void assertNoDuplicateSingularNames(TypeInfo typeInfo) { + Set names = new LinkedHashSet<>(); + Set duplicateNames = new LinkedHashSet<>(); + + typeInfo.elementInfo().stream() + .map(DefaultBuilderCreatorProvider::nameOf) + .forEach(name -> { + if (!names.add(name)) { + duplicateNames.add(name); + } + }); + + if (!duplicateNames.isEmpty()) { + throw new IllegalStateException("duplicate methods are using the same names " + duplicateNames + " for: " + + typeInfo.typeName()); + } } /** @@ -146,14 +165,14 @@ protected TypeName toAbstractImplTypeName(TypeName typeName, } /** - * Constructs the default implementation type name for what is code generated. + * Returns the default implementation Builder's class name for what is code generated. * * @param typeName the target interface that the builder applies to * @param builderAnnotation the builder annotation triggering the build * @return the type name of the implementation */ - protected TypeName toImplTypeName(TypeName typeName, - AnnotationAndValue builderAnnotation) { + public static TypeName toBuilderImplTypeName(TypeName typeName, + AnnotationAndValue builderAnnotation) { String toPackageName = toPackageName(typeName.packageName(), builderAnnotation); String prefix = toImplTypePrefix(builderAnnotation); String suffix = toImplTypeSuffix(builderAnnotation); @@ -173,7 +192,11 @@ protected BodyContext createBodyContext(boolean doingConcreteType, TypeName typeName, TypeInfo typeInfo, AnnotationAndValue builderAnnotation) { - return new BodyContext(doingConcreteType, typeName, typeInfo, builderAnnotation); + try { + return new BodyContext(doingConcreteType, typeName, typeInfo, builderAnnotation); + } catch (Throwable t) { + throw new IllegalStateException("Failed while processing: " + typeName, t); + } } /** @@ -335,6 +358,7 @@ protected void appendFields(StringBuilder builder, */ protected void appendHeader(StringBuilder builder, BodyContext ctx) { + builder.append(generatedCopyrightHeaderFor(ctx)).append("\n"); builder.append("package ").append(ctx.implTypeName().packageName()).append(";\n\n"); builder.append("import java.util.Collections;\n"); builder.append("import java.util.List;\n"); @@ -377,12 +401,12 @@ protected void appendHeader(StringBuilder builder, builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderTriggerAnnotation())); } else { if (ctx.hasParent()) { - builder.append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation())); + builder.append(toAbstractImplTypeName(ctx.parentTypeName().orElseThrow(), ctx.builderTriggerAnnotation())); } else if (baseExtendsTypeName.isPresent()) { builder.append(baseExtendsTypeName.get().fqName()); } - LinkedList impls = new LinkedList<>(); + List impls = new ArrayList<>(); if (!ctx.isExtendingAnAbstractClass()) { impls.add(ctx.typeInfo().typeName().fqName()); } @@ -400,14 +424,34 @@ protected void appendHeader(StringBuilder builder, builder.append(" {\n"); } + /** + * Returns the copyright level header comment. + * + * @param ctx the context + * @return the copyright level header + */ + protected String generatedCopyrightHeaderFor(BodyContext ctx) { + return copyrightHeaderFor(getClass().getName()); + } + /** * Returns the {@code Generated} sticker to be added. * * @param ctx the context - * @return the generated sticker. + * @return the generated sticker */ protected String generatedStickerFor(BodyContext ctx) { - return BuilderTypeTools.generatedStickerFor(getClass().getName(), Versions.CURRENT_BUILDER_VERSION); + return BuilderTypeTools.generatedStickerFor(getClass().getName(), generatedVersionFor(ctx)); + } + + /** + * Returns the {@code Generated} version identifier. + * + * @param ctx the context + * @return the generated version identifier + */ + protected String generatedVersionFor(BodyContext ctx) { + return Versions.CURRENT_BUILDER_VERSION; } /** @@ -712,6 +756,9 @@ protected void appendMetaProps(StringBuilder builder, BodyContext ctx, String tag, AtomicBoolean needsCustomMapOf) { + builder.append("\t\t").append(tag); + builder.append(".put(\"__generated\", Map.of(\"version\", \"") + .append(io.helidon.builder.processor.tools.Versions.BUILDER_VERSION_1).append("\"));\n"); ctx.map().forEach((attrName, method) -> builder.append("\t\t") .append(tag) @@ -725,15 +772,27 @@ protected void appendMetaProps(StringBuilder builder, /** * Normalize the configured option key. * - * @param key the key attribute - * @param attrName the attribute name - * @param method the method - * @return the key to write on the generated output. + * @param key the key attribute + * @param name the name + * @param isAttribute if the name represents an attribute value (otherwise is a config bean name) + * @return the key to write on the generated output */ protected String normalizeConfiguredOptionKey(String key, - String attrName, - TypedElementName method) { - return BuilderTypeTools.hasNonBlankValue(key) ? key : ""; + String name, + boolean isAttribute) { + return hasNonBlankValue(key) ? key : toConfigKey(name, isAttribute); + } + + /** + * Applicable if this builder is intended for config beans. + * + * @param name the name + * @param isAttribute if the name represents an attribute value (otherwise is a config bean name) + * @return the config key + */ + protected String toConfigKey(String name, + boolean isAttribute) { + return ""; } /** @@ -776,6 +835,19 @@ protected static String maybeSingularFormOf(String beanAttributeName) { return beanAttributeName; } + /** + * Attempts to use the singular name of the element, defaulting to the element name if no singular annotation exists. + * + * @param elem the element + * @return the (singular) name of the element + */ + protected static String nameOf(TypedElementName elem) { + return DefaultAnnotationAndValue.findFirst(Singular.class.getName(), elem.annotations()) + .flatMap(AnnotationAndValue::value) + .filter(BuilderTypeTools::hasNonBlankValue) + .orElseGet(elem::elementName); + } + /** * Append the setters for the given bean attribute name. * @@ -898,7 +970,7 @@ protected void appendAnnotations(StringBuilder builder, for (AnnotationAndValue methodAnno : annotations) { if (methodAnno.typeName().name().equals(Annotated.class.getName())) { String val = methodAnno.value().orElse(""); - if (!BuilderTypeTools.hasNonBlankValue(val)) { + if (!hasNonBlankValue(val)) { continue; } if (!val.startsWith("@")) { @@ -989,7 +1061,7 @@ protected static Optional toValue(Class annoType, boolean wantTypeElementDefaults, boolean avoidBlanks) { if (wantTypeElementDefaults && method.defaultValue().isPresent()) { - if (!avoidBlanks || BuilderTypeTools.hasNonBlankValue(method.defaultValue().orElse(null))) { + if (!avoidBlanks || hasNonBlankValue(method.defaultValue().orElse(null))) { return method.defaultValue(); } } @@ -1001,7 +1073,7 @@ protected static Optional toValue(Class annoType, if (!avoidBlanks) { return val; } - return BuilderTypeTools.hasNonBlankValue(val.orElse(null)) ? val : Optional.empty(); + return hasNonBlankValue(val.orElse(null)) ? val : Optional.empty(); } } @@ -1055,8 +1127,8 @@ private static char[] reverseBeanName(String beanName) { /** * In support of {@link io.helidon.builder.Builder#packageName()}. */ - private String toPackageName(String packageName, - AnnotationAndValue builderAnnotation) { + private static String toPackageName(String packageName, + AnnotationAndValue builderAnnotation) { String packageNameFromAnno = builderAnnotation.value("packageName").orElse(null); if (packageNameFromAnno == null || packageNameFromAnno.isBlank()) { return packageName; @@ -1077,14 +1149,14 @@ private String toAbstractImplTypePrefix(AnnotationAndValue builderAnnotation) { /** * In support of {@link io.helidon.builder.Builder#implPrefix()}. */ - private String toImplTypePrefix(AnnotationAndValue builderAnnotation) { + private static String toImplTypePrefix(AnnotationAndValue builderAnnotation) { return builderAnnotation.value("implPrefix").orElse(DEFAULT_IMPL_PREFIX); } /** * In support of {@link io.helidon.builder.Builder#implSuffix()}. */ - private String toImplTypeSuffix(AnnotationAndValue builderAnnotation) { + private static String toImplTypeSuffix(AnnotationAndValue builderAnnotation) { return builderAnnotation.value("implSuffix").orElse(DEFAULT_SUFFIX); } @@ -1340,7 +1412,7 @@ private void appendBuilderHeader(StringBuilder builder, builder.append(ctx.ctorBuilderAcceptTypeName()).append(">"); if (ctx.hasParent()) { builder.append(" extends ") - .append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation())) + .append(toAbstractImplTypeName(ctx.parentTypeName().orElseThrow(), ctx.builderTriggerAnnotation())) .append(".").append(ctx.genericBuilderClassDecl()); builder.append("<").append(ctx.genericBuilderAliasDecl()) .append(", ").append(ctx.genericBuilderAcceptAliasDecl()); @@ -1356,7 +1428,7 @@ private void appendBuilderHeader(StringBuilder builder, } } - LinkedList impls = new LinkedList<>(); + List impls = new ArrayList<>(); if (!ctx.isExtendingAnAbstractClass() && !ctx.hasAnyBuilderClashingMethodNames()) { impls.add(ctx.typeInfo().typeName().name()); } @@ -1417,7 +1489,7 @@ private void appendInterfaceBasedGetters(StringBuilder builder, i++; } - if (Objects.nonNull(ctx.parentAnnotationType().get())) { + if (ctx.parentAnnotationTypeName().isPresent()) { builder.append(prefix) .append("\t@Override\n"); builder.append(prefix) @@ -1643,7 +1715,7 @@ private void appendOverridesOfDefaultValues(StringBuilder builder, // candidate for override... String thisDefault = toConfiguredOptionValue(method, true, true).orElse(null); String superDefault = superValue(ctx.typeInfo().superTypeInfo(), beanAttributeName, ctx.beanStyleRequired()); - if (BuilderTypeTools.hasNonBlankValue(thisDefault) && !Objects.equals(thisDefault, superDefault)) { + if (hasNonBlankValue(thisDefault) && !Objects.equals(thisDefault, superDefault)) { appendDefaultOverride(builder, beanAttributeName, method, thisDefault); } } @@ -1662,7 +1734,7 @@ private String superValue(Optional optSuperTypeInfo, .findFirst(); if (method.isPresent()) { Optional defaultValue = toConfiguredOptionValue(method.get(), true, true); - if (defaultValue.isPresent() && BuilderTypeTools.hasNonBlankValue(defaultValue.get())) { + if (defaultValue.isPresent() && hasNonBlankValue(defaultValue.get())) { return defaultValue.orElse(null); } } else { @@ -1696,25 +1768,25 @@ private void appendCustomMapOf(StringBuilder builder) { private String mapOf(String attrName, TypedElementName method, AtomicBoolean needsCustomMapOf) { - final Optional configuredOptions = DefaultAnnotationAndValue + Optional configuredOptions = DefaultAnnotationAndValue .findFirst(ConfiguredOption.class.getName(), method.annotations()); TypeName typeName = method.typeName(); - String typeDecl = "\"type\", " + typeName.name() + ".class"; + String typeDecl = "\"__type\", " + typeName.name() + ".class"; if (!typeName.typeArguments().isEmpty()) { int pos = typeName.typeArguments().size() - 1; - typeDecl += ", \"componentType\", " + normalize(typeName.typeArguments().get(pos).name()) + ".class"; + typeDecl += ", \"__componentType\", " + normalize(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); + key = normalizeConfiguredOptionKey(key, attrName, true); + if (hasNonBlankValue(key)) { + typeDecl += ", " + quotedTupleOf(method.typeName(), "key", key); } String defaultValue = method.defaultValue().orElse(null); - if (configuredOptions.isEmpty() && !BuilderTypeTools.hasNonBlankValue(defaultValue)) { + if (configuredOptions.isEmpty() && !hasNonBlankValue(defaultValue)) { return "Map.of(" + typeDecl + ")"; } @@ -1723,18 +1795,22 @@ private String mapOf(String attrName, result.append("__mapOf(").append(typeDecl); if (configuredOptions.isEmpty()) { + result.append(", "); if (defaultValue.startsWith("{")) { defaultValue = "new String[] " + defaultValue; + result.append(quotedValueOf("value")); + result.append(", "); + result.append(defaultValue); + } else { + result.append(quotedTupleOf(typeName, "value", defaultValue)); } - result.append(", "); - result.append(quotedValueOf("value")).append(", ").append(defaultValue); } else { configuredOptions.get().values().entrySet().stream() - .filter(e -> BuilderTypeTools.hasNonBlankValue(e.getValue())) + .filter(e -> hasNonBlankValue(e.getValue())) .filter(e -> !e.getKey().equals("key")) - .forEach((e) -> { + .forEach(e -> { result.append(", "); - result.append(quotedTupleOf(e.getKey(), e.getValue())); + result.append(quotedTupleOf(typeName, e.getKey(), e.getValue())); }); } result.append(")"); @@ -1746,13 +1822,15 @@ private String normalize(String name) { return name.equals("?") ? "Object" : name; } - private String quotedTupleOf(String key, + private String quotedTupleOf(TypeName valType, + 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 { + assert (key != null); + assert (hasNonBlankValue(val)) : key; + boolean isEnumLikeType = isEnumLikeType(valType, key, val); + if (isEnumLikeType) { + val = valType + "." + val; + } else if (!key.equals("value") || !val.startsWith(ConfiguredOption.class.getName())) { val = quotedValueOf(val); } return quotedValueOf(key) + ", " + val; @@ -1766,6 +1844,25 @@ private String quotedValueOf(String val) { return "\"" + val + "\""; } + private boolean isEnumLikeType(TypeName valType, + String key, + String val) { + if (!hasNonBlankValue(val) || valType.primitive()) { + return false; + } + + int dotPos = key.indexOf("."); + if (dotPos < 0) { + return false; + } + + if (valType.isOptional() && !valType.typeArguments().isEmpty()) { + return isEnumLikeType(valType.typeArguments().get(0), key, val); + } + + return !BeanUtils.isBuiltInJavaType(valType); + } + private String maybeRequireNonNull(BodyContext ctx, String tag) { return ctx.allowNulls() ? tag : "Objects.requireNonNull(" + tag + ")"; diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java index da8401018d3..a1ad7a40b49 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java @@ -16,8 +16,8 @@ package io.helidon.builder.processor.tools; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; final class GenerateJavadoc { private GenerateJavadoc() { diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java index 89def95e341..fae124e05f1 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java @@ -20,8 +20,8 @@ import java.util.Objects; import java.util.Optional; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; final class GenerateMethod { static final String SINGULAR_PREFIX = "add"; diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java index b7427e75375..caa2d615ee9 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -88,7 +88,7 @@ static void appendExtraInnerClasses(StringBuilder builder, + "\t\t\t\t\t\t Class type,\n" + "\t\t\t\t\t\t Class... typeArgument) {\n" + "\t\t\tString requiredStr = (String) meta.get(\"required\");\n" - + "\t\t\tboolean requiredPresent = Objects.nonNull(requiredStr);\n" + + "\t\t\tboolean requiredPresent = (requiredStr != null);\n" + "\t\t\tboolean required = Boolean.parseBoolean(requiredStr);\n" + "\t\t\tif (!required && requiredPresent) {\n" + "\t\t\t\treturn;\n" @@ -99,7 +99,7 @@ static void appendExtraInnerClasses(StringBuilder builder, + "\t\t\t}\n" + "\t\t\t\n" + "\t\t\tObject val = valueSupplier.get();\n" - + "\t\t\tif (Objects.nonNull(val)) {\n" + + "\t\t\tif (val != null) {\n" + "\t\t\t\treturn;\n" + "\t\t\t}\n" + "\t\t\t\n" @@ -109,10 +109,12 @@ static void appendExtraInnerClasses(StringBuilder builder, + "\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\tthrow new java.lang.IllegalStateException(\"problems building configbean '\" + " + + ctx.typeInfo().typeName() + ".class.getName() + \"': \" + String.join(\", \", errors));\n" + "\t\t\t}\n" + "\t\t}\n" + "\t}\n"); } } + } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/ToStringAnnotationValueVisitor.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/ToStringAnnotationValueVisitor.java index 336a5882707..2d8976742b1 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/ToStringAnnotationValueVisitor.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/ToStringAnnotationValueVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -109,7 +108,7 @@ public String visitShort(short s, Object o) { @Override public String visitString(String s, Object o) { - if (mapEmptyStringToNull && Objects.nonNull(s) && s.isBlank()) { + if (mapEmptyStringToNull && s != null && s.isBlank()) { return null; } @@ -154,7 +153,7 @@ public String visitArray(List vals, Object o) { String result = String.join(", ", values); if (mapBlankArrayToNull && result.isBlank()) { result = null; - } else if (Objects.nonNull(result) && mapToSourceDeclaration) { + } else if (mapToSourceDeclaration) { result = "{"; for (AnnotationValue val : vals) { String stringVal = val.accept(this, null); diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/package-info.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/package-info.java index 67c2fd93fea..8e565bdcab9 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/package-info.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,7 +15,7 @@ */ /** - * The Helidon Pico Builder Processor Tools package. These are generally only needed by other Helidon modules like the Builder + * The Helidon Builder Processor Tools package. These are generally only needed by other Helidon modules like the Builder * annotation processor, etc. */ package io.helidon.builder.processor.tools; diff --git a/builder/processor-tools/src/main/java/module-info.java b/builder/processor-tools/src/main/java/module-info.java index 39041e73c44..99c5b5ed2b2 100644 --- a/builder/processor-tools/src/main/java/module-info.java +++ b/builder/processor-tools/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,7 +19,7 @@ */ module io.helidon.builder.processor.tools { requires java.compiler; - requires io.helidon.pico.types; + requires io.helidon.common.types; requires io.helidon.builder; requires io.helidon.builder.processor.spi; requires io.helidon.common; diff --git a/builder/processor-tools/src/test/java/io/helidon/builder/processor/tools/BeanUtilsTest.java b/builder/processor-tools/src/test/java/io/helidon/builder/processor/tools/BeanUtilsTest.java index 66128c96dfa..411d5c6bc3e 100644 --- a/builder/processor-tools/src/test/java/io/helidon/builder/processor/tools/BeanUtilsTest.java +++ b/builder/processor-tools/src/test/java/io/helidon/builder/processor/tools/BeanUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -23,40 +23,41 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import io.helidon.common.testing.junit5.OptionalMatcher; - -import org.hamcrest.Matchers; -import org.hamcrest.core.Is; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static io.helidon.builder.processor.tools.BeanUtils.isBooleanType; +import static io.helidon.builder.processor.tools.BeanUtils.isReservedWord; import static io.helidon.builder.processor.tools.BeanUtils.isValidMethodType; import static io.helidon.builder.processor.tools.BeanUtils.validateAndParseMethodName; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.core.Is.is; class BeanUtilsTest { @Test void testIsBooleanType() { - assertThat(isBooleanType(boolean.class), Is.is(true)); - assertThat(isBooleanType(Boolean.class), Is.is(true)); - assertThat(isBooleanType(String.class), Is.is(false)); - assertThat(isBooleanType(""), Is.is(false)); + assertThat(isBooleanType(boolean.class), is(true)); + assertThat(isBooleanType(Boolean.class), is(true)); + assertThat(isBooleanType(String.class), is(false)); + assertThat(isBooleanType(""), is(false)); } @Test void testIsValidMethodType() { - assertThat(isValidMethodType(boolean.class.getName()), Is.is(true)); - assertThat(isValidMethodType(String.class.getName()), Is.is(true)); - assertThat(isValidMethodType(Collection.class.getName()), Is.is(true)); - assertThat(isValidMethodType(Map.class.getName()), Is.is(true)); - assertThat(isValidMethodType(Set.class.getName()), Is.is(true)); - assertThat(isValidMethodType(List.class.getName()), Is.is(true)); - assertThat(isValidMethodType(Object.class.getName()), Is.is(true)); - assertThat(isValidMethodType(""), Is.is(false)); - assertThat(isValidMethodType(void.class.getName()), Is.is(false)); - assertThat(isValidMethodType(Void.class.getName()), Is.is(false)); + assertThat(isValidMethodType(boolean.class.getName()), is(true)); + assertThat(isValidMethodType(String.class.getName()), is(true)); + assertThat(isValidMethodType(Collection.class.getName()), is(true)); + assertThat(isValidMethodType(Map.class.getName()), is(true)); + assertThat(isValidMethodType(Set.class.getName()), is(true)); + assertThat(isValidMethodType(List.class.getName()), is(true)); + assertThat(isValidMethodType(Object.class.getName()), is(true)); + assertThat(isValidMethodType(""), is(false)); + assertThat(isValidMethodType(void.class.getName()), is(false)); + assertThat(isValidMethodType(Void.class.getName()), is(false)); } @Test @@ -65,84 +66,134 @@ void testValidateAndParseMethodName() { RuntimeException e = Assertions.assertThrows(RuntimeException.class, () -> validateAndParseMethodName("x", "", true, attrName)); - assertThat(e.getMessage(), Is.is("invalid return type: x")); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(e.getMessage(), is("invalid return type: x")); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("isAlpha", Boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha", "isAlpha"))); + assertThat(validateAndParseMethodName("isAlpha", Boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha", "isAlpha"))); - assertThat(validateAndParseMethodName("isAlpha", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha", "isAlpha"))); + assertThat(validateAndParseMethodName("isAlpha", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha", "isAlpha"))); - assertThat(validateAndParseMethodName("getAlpha", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha"))); + assertThat(validateAndParseMethodName("getAlpha", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha"))); - assertThat(validateAndParseMethodName("getAlpha", Boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha"))); + assertThat(validateAndParseMethodName("getAlpha", Boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha"))); - assertThat(validateAndParseMethodName("getAlpha", String.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha"))); + assertThat(validateAndParseMethodName("getAlpha", String.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha"))); - assertThat(validateAndParseMethodName("getAlpha", Object.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alpha"))); + assertThat(validateAndParseMethodName("getAlpha", Object.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alpha"))); - assertThat(validateAndParseMethodName("isAlphaNumeric", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alphaNumeric", "isAlphaNumeric"))); + assertThat(validateAndParseMethodName("isAlphaNumeric", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alphaNumeric", "isAlphaNumeric"))); - assertThat(validateAndParseMethodName("getAlphaNumeric", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("alphaNumeric"))); + assertThat(validateAndParseMethodName("getAlphaNumeric", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("alphaNumeric"))); - assertThat(validateAndParseMethodName("isX", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("x", "isX"))); + assertThat(validateAndParseMethodName("isX", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("x", "isX"))); - assertThat(validateAndParseMethodName("getX", boolean.class.getName(), false, attrName), Is.is(true)); - assertThat(attrName.get(), OptionalMatcher.optionalValue(Matchers.contains("x"))); + assertThat(validateAndParseMethodName("getX", boolean.class.getName(), false, attrName), is(true)); + assertThat(attrName.get(), optionalValue(contains("x"))); // negative cases ... - assertThat(validateAndParseMethodName("isX", String.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("isX", String.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); + + assertThat(validateAndParseMethodName("is_AlphaNumeric", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("is_AlphaNumeric", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("is", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("is", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("is", Boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("is", Boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("get", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("get", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("get", Boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("get", Boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("get1AlphaNumeric", Boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("get1AlphaNumeric", Boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("getalphaNumeric", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("getalphaNumeric", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("isalphaNumeric", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("isalphaNumeric", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("is9alphaNumeric", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("is9alphaNumeric", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("isAlphaNumeric", void.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("isAlphaNumeric", void.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("getAlphaNumeric", Void.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("getAlphaNumeric", Void.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("x", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("x", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("IsX", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); - assertThat(validateAndParseMethodName("IsX", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + assertThat(validateAndParseMethodName("GetX", boolean.class.getName(), false, attrName), is(false)); + assertThat(attrName.get(), optionalEmpty()); + } - assertThat(validateAndParseMethodName("GetX", boolean.class.getName(), false, attrName), Is.is(false)); - assertThat(attrName.get(), OptionalMatcher.optionalEmpty()); + @Test + void testIsReservedWord() { + assertThat(isReservedWord("whatever"), + is(false)); + assertThat(isReservedWord("class"), + is(true)); + assertThat(isReservedWord("Class"), + is(true)); + assertThat(isReservedWord("interface"), + is(true)); + assertThat(isReservedWord("INTERFACE"), + is(true)); + assertThat(isReservedWord("package"), + is(true)); + assertThat(isReservedWord("PACKAGE"), + is(true)); + assertThat(isReservedWord("enum"), + is(true)); + assertThat(isReservedWord("ENUM"), + is(true)); + assertThat(isReservedWord("static"), + is(true)); + assertThat(isReservedWord("STATIC"), + is(true)); + assertThat(isReservedWord("final"), + is(true)); + assertThat(isReservedWord("FINAL"), + is(true)); + assertThat(isReservedWord("public"), + is(true)); + assertThat(isReservedWord("PUBLIC"), + is(true)); + assertThat(isReservedWord("protected"), + is(true)); + assertThat(isReservedWord("PROTECTED"), + is(true)); + assertThat(isReservedWord("private"), + is(true)); + assertThat(isReservedWord("PRIVATE"), + is(true)); + assertThat(isReservedWord("record"), + is(true)); + assertThat(isReservedWord("RECORD"), + is(true)); + assertThat(isReservedWord("abstract"), + is(true)); + assertThat(isReservedWord("ABSTRACT"), + is(true)); } } diff --git a/builder/processor/README.md b/builder/processor/README.md new file mode 100644 index 00000000000..7657bc35d13 --- /dev/null +++ b/builder/processor/README.md @@ -0,0 +1,4 @@ +# builder-processor + +This module adds support for Builder an other builder trigger-type annotations. +This module should typically only be used during compile time, in the APT compiler path only. diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java b/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java index 70136ca46b5..9c989e5a703 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java @@ -41,14 +41,14 @@ import io.helidon.builder.processor.spi.BuilderCreatorProvider; import io.helidon.builder.processor.spi.TypeAndBody; -import io.helidon.builder.processor.spi.TypeInfo; import io.helidon.builder.processor.spi.TypeInfoCreatorProvider; import io.helidon.builder.processor.tools.BuilderTypeTools; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.Weights; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; /** * The processor for handling any annotation having a {@link io.helidon.builder.BuilderTrigger}. diff --git a/builder/processor/src/main/java/module-info.java b/builder/processor/src/main/java/module-info.java index 530a9243547..375d1962f41 100644 --- a/builder/processor/src/main/java/module-info.java +++ b/builder/processor/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -23,7 +23,7 @@ requires io.helidon.builder; requires io.helidon.builder.processor.spi; requires io.helidon.builder.processor.tools; - requires io.helidon.pico.types; + requires io.helidon.common.types; exports io.helidon.builder.processor; diff --git a/builder/tests/builder/pom.xml b/builder/tests/builder/pom.xml index 19a5ca45736..0ab22029a52 100644 --- a/builder/tests/builder/pom.xml +++ b/builder/tests/builder/pom.xml @@ -28,7 +28,7 @@ 4.0.0 - helidon-builder-test-builder + helidon-builder-tests-test-builder Helidon Builder Tests @@ -56,6 +56,7 @@ jakarta.annotation jakarta.annotation-api provided + true com.fasterxml.jackson.core diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ComplexCase.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ComplexCase.java index 867a7f1e683..3900422a774 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ComplexCase.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ComplexCase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -24,7 +24,7 @@ import io.helidon.builder.Singular; /** - * Used for demonstrating and testing the Pico Builder. + * Used for demonstrating and testing the Builder. * In this case we are enforcing bean style is used, and overriding the generated class to have a suffix of Impl on the class name. */ @Builder(requireLibraryDependencies = false, requireBeanStyle = true, implPrefix = "", implSuffix = "Impl") @@ -61,7 +61,7 @@ public interface ComplexCase extends MyConfigBean { Class getClassType(); /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ @@ -70,7 +70,7 @@ default String getCompositeName() { } /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ @@ -79,7 +79,7 @@ default boolean hasBeenEnabled() { } /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/CustomNamed.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/CustomNamed.java index c9a5285c494..df750ea1044 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/CustomNamed.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/CustomNamed.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -28,7 +28,7 @@ import io.helidon.builder.Singular; /** - * Used for demonstrating and testing the Pico Builder. + * Used for demonstrating and testing the Builder. *

      * In this case we are overriding the Map, Set, and List types, usages of annotations placed on the generated class, as well as * changing the package name targeted for the generated class. diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/EdgeCases.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/EdgeCases.java index 24005215d30..d6c31b1ea40 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/EdgeCases.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/EdgeCases.java @@ -25,7 +25,7 @@ import io.helidon.config.metadata.ConfiguredOption; /** - * Used for demonstrating and testing the Pico Builder. + * Used for demonstrating and testing the Builder. */ @Builder(includeMetaAttributes = false) public interface EdgeCases { diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java index 847869c0844..0b25935649d 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,7 +16,7 @@ package io.helidon.builder.test.testsubjects; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import io.helidon.common.Builder; @@ -27,7 +27,7 @@ @Deprecated @SuppressWarnings("unchecked") public class GeneralInterceptor { - private static final List INTERCEPT_CALLS = new LinkedList<>(); + private static final List INTERCEPT_CALLS = new ArrayList<>(); /** * Generic interceptor. diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyConfigBean.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyConfigBean.java index 08b4d572fc2..3bd1df18063 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyConfigBean.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyConfigBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -21,7 +21,7 @@ import io.helidon.config.metadata.ConfiguredValue; /** - * Used for demonstrating and testing the Pico Builder. + * Used for demonstrating and testing the Builder. * * @see MyDerivedConfigBean */ @@ -29,7 +29,7 @@ public interface MyConfigBean { /** - * Used for demonstrating and testing the Pico Builder. Here we can see that a {@code required=true} is placed on the configured + * Used for demonstrating and testing the Builder. Here we can see that a {@code required=true} is placed on the configured * option. * * @return ignored, here for testing purposes only diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyDerivedConfigBean.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyDerivedConfigBean.java index d4572021604..b0eee1ad2b2 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyDerivedConfigBean.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/MyDerivedConfigBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,7 +19,7 @@ import io.helidon.builder.Builder; /** - * Used for demonstrating and testing the Pico Builder. + * Used for demonstrating and testing the Builder. * * @see MyConfigBean */ diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ParentInterfaceNotABuilder.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ParentInterfaceNotABuilder.java index 995eae4bf97..b3edf8efa5c 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ParentInterfaceNotABuilder.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ParentInterfaceNotABuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -26,13 +26,13 @@ public interface ParentInterfaceNotABuilder extends ParentOfParentInterfaceIsABuilder { /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. */ default void ignoreMe() { } /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ @@ -41,7 +41,7 @@ default Optional maybeOverrideMe() { } /** - * The Pico Builder will ignore {@code default} and {@code static} functions. + * The Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/Pickle.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/Pickle.java index 6e6faa2673a..34ee2014bd8 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/Pickle.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/Pickle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -72,7 +72,7 @@ enum Size { /** * The type of pickle is marked as required; which means that we cannot be build unless the type is defined. * - * @return the type of pickle. + * @return the type of pickle */ @ConfiguredOption(required = true) Type type(); diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/ComplexCaseTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/ComplexCaseTest.java index 448d3c71340..f4f3f895a89 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/ComplexCaseTest.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/ComplexCaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import io.helidon.builder.test.testsubjects.ComplexCaseImpl; import io.helidon.builder.test.testsubjects.MyConfigBean; @@ -39,7 +40,7 @@ void testIt() { ComplexCaseImpl val = ComplexCaseImpl.builder() .name("name") .mapOfKeyToConfigBeans(mapWithNull) - .setOfLists(Collections.singleton(Collections.singletonList(null))) + .setOfLists(Set.of(Collections.singletonList(null))) .classType(Object.class) .build(); assertThat(val.toString(), diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/LevelTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/LevelTest.java index cdb1fa061f5..6ce725cdf36 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/LevelTest.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/LevelTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,6 +17,7 @@ package io.helidon.builder.test; import java.util.Collections; +import java.util.Set; import java.util.function.Supplier; import io.helidon.builder.test.testsubjects.Level0; @@ -44,7 +45,7 @@ void manualGeneric() { .level1booleanAttribute(false) .level1IntegerAttribute(null) .level2Level0Info(null) - .level2Level0Info(Collections.singleton(Level0ManualImpl.builder().build())) + .level2Level0Info(Set.of(Level0ManualImpl.builder().build())) .addLevel0(Level0ManualImpl.builder().build()) .addStringToLevel1("key", Level1ManualImpl.builder().build()) .build(); @@ -79,7 +80,7 @@ void manualNonGeneric() { .level1booleanAttribute(false) .level1IntegerAttribute(null) .level2Level0Info(null) - .level2Level0Info(Collections.singleton(Level0ManualImpl.builder().build())) + .level2Level0Info(Set.of(Level0ManualImpl.builder().build())) .addLevel0(Level0ManualImpl.builder().build()) .addStringToLevel1("key", Level1ManualImpl.builder().build()) .build(); @@ -116,7 +117,7 @@ void codeGen() { Level2 val2 = Level2Impl.builder() .level0StringAttribute("a") .level1booleanAttribute(false) - .level2Level0Info(Collections.singleton(Level0Impl.builder().build())) + .level2Level0Info(Set.of(Level0Impl.builder().build())) .addLevel0(Level0Impl.builder().build()) .addStringToLevel1("key", Level1Impl.builder().build()) .build(); diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/MyDerivedConfigBeanTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/MyDerivedConfigBeanTest.java index 4f310faeb0d..055f1224378 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/MyDerivedConfigBeanTest.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/MyDerivedConfigBeanTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -34,10 +34,11 @@ class MyDerivedConfigBeanTest { @Test void testIt() { assertThat(sort(MyDerivedConfigBeanImpl.__metaAttributes()).toString(), - equalTo("{enabled={type=boolean}, name={deprecated=false, experimental=false, kind=VALUE, " + equalTo("{__generated={version=1}, enabled={__type=boolean}, name={__type=class java.lang.String, " + + "deprecated=false, experimental=false, kind=VALUE, " + "mergeWithParent=false, provider=false, required=true, type=io.helidon.config.metadata" + ".ConfiguredOption, value=io.helidon.config.metadata.ConfiguredOption.UNCONFIGURED}, " - + "port={type=int}}")); + + "port={__type=int}}")); MyDerivedConfigBean cfg = MyDerivedConfigBeanImpl.builder().name("test").build(); assertThat(cfg.toString(), diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level0ManualImpl.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level0ManualImpl.java index 4835de2b9af..80535ab1c77 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level0ManualImpl.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level0ManualImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -135,7 +135,7 @@ public void accept(T val) { * @return ignored, here for testing only */ private void acceptThis(T val) { - if (Objects.isNull(val)) { + if (val == null) { return; } diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level1ManualImpl.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level1ManualImpl.java index 4d354aac3c0..fbe1a762354 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level1ManualImpl.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level1ManualImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -168,7 +168,7 @@ public void accept(T val) { * Used for testing purposes only. */ private void acceptThis(T val) { - if (Objects.isNull(val)) { + if (val == null) { return; } diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level2ManualImpl.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level2ManualImpl.java index da8673d9c12..63a9eba7ef3 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level2ManualImpl.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/Level2ManualImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,10 +16,10 @@ package io.helidon.builder.test.testsubjects; +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; @@ -36,12 +36,12 @@ public class Level2ManualImpl extends Level1ManualImpl implements Level2 { protected Level2ManualImpl(Builder builder) { super(builder); - this.level2Level0Info = Objects.isNull(builder.level2Level0Info) - ? Collections.emptyList() : Collections.unmodifiableList(new LinkedList<>(builder.level2Level0Info)); - this.level2ListOfLevel0s = Objects.isNull(builder.level2ListOfLevel0s) - ? Collections.emptyList() : Collections.unmodifiableList(new LinkedList<>(builder.level2ListOfLevel0s)); - this.level2MapOfStringToLevel1s = Objects.isNull(builder.level2MapOfStringToLevel1s) - ? Collections.emptyMap() : Collections.unmodifiableMap(new LinkedHashMap<>(builder.level2MapOfStringToLevel1s)); + this.level2Level0Info = (builder.level2Level0Info == null) + ? List.of() : Collections.unmodifiableList(new ArrayList<>(builder.level2Level0Info)); + this.level2ListOfLevel0s = (builder.level2ListOfLevel0s == null) + ? List.of() : Collections.unmodifiableList(new ArrayList<>(builder.level2ListOfLevel0s)); + this.level2MapOfStringToLevel1s = (builder.level2MapOfStringToLevel1s == null) + ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(builder.level2MapOfStringToLevel1s)); } @Override @@ -172,21 +172,21 @@ public void accept(T val) { } private void acceptThis(T val) { - if (Objects.isNull(val)) { + if (val == null) { return; } { Collection v = val.getLevel2Level0Info(); - this.level2Level0Info = Objects.isNull(v) ? null : new LinkedList<>(v); + this.level2Level0Info = (v == null) ? null : new ArrayList<>(v); } { Collection v = val.getLevel2ListOfLevel0s(); - this.level2ListOfLevel0s = Objects.isNull(v) ? null : new LinkedList<>(v); + this.level2ListOfLevel0s = (v == null) ? null : new ArrayList<>(v); } { Map v = val.getLevel2MapOfStringToLevel1s(); - this.level2MapOfStringToLevel1s = Objects.isNull(v) ? null : new LinkedHashMap<>(v); + this.level2MapOfStringToLevel1s = (v == null) ? null : new LinkedHashMap<>(v); } } @@ -196,30 +196,7 @@ private void acceptThis(T val) { * @return ignored, here for testing only */ public B level2Level0Info(Collection val) { - this.level2Level0Info = Objects.isNull(val) ? null : new LinkedList<>(val); - return identity(); - } - - /** - * Used for testing purposes only. - * - * @return ignored, here for testing only - */ - public B addlevel2Level0Info(Level0 val) { - if (Objects.isNull(level2Level0Info)) { - level2Level0Info = new LinkedList<>(); - } - level2Level0Info.add(val); - return identity(); - } - - /** - * Used for testing purposes only. - * - * @return ignored, here for testing only - */ - public B level2ListOfLevel0s(Collection val) { - this.level2ListOfLevel0s = Objects.isNull(val) ? null : new LinkedList<>(val); + this.level2Level0Info = (val == null) ? null : new ArrayList<>(val); return identity(); } @@ -229,30 +206,20 @@ public B level2ListOfLevel0s(Collection val) { * @return ignored, here for testing only */ public B addLevel0(Level0 val) { - if (Objects.isNull(level2ListOfLevel0s)) { - level2ListOfLevel0s = new LinkedList<>(); + if (level2ListOfLevel0s == null) { + level2ListOfLevel0s = new ArrayList<>(); } level2ListOfLevel0s.add(val); return identity(); } - /** - * Used for testing purposes only. - * - * @return ignored, here for testing only - */ - public B level2MapOfStringToLevel1s(Map val) { - this.level2MapOfStringToLevel1s = Objects.isNull(val) ? null : new LinkedHashMap<>(val); - return identity(); - } - /** * Used for testing purposes only. * * @return ignored, here for testing only */ public B addStringToLevel1(String key, Level1 val) { - if (Objects.isNull(level2MapOfStringToLevel1s)) { + if (level2MapOfStringToLevel1s == null) { level2MapOfStringToLevel1s = new LinkedHashMap<>(); } level2MapOfStringToLevel1s.put(key, val); diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/MyConfigBeanManualImpl.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/MyConfigBeanManualImpl.java index 354b37672d0..c8379f6187f 100644 --- a/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/MyConfigBeanManualImpl.java +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/testsubjects/MyConfigBeanManualImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -103,7 +103,7 @@ public static Builder builder() { public static Builder toBuilder(MyConfigBean val) { Builder b = new Builder(); - if (Objects.nonNull(val)) { + if (val != null) { b.name(val.getName()); b.port(val.getPort()); b.enabled(val.isEnabled()); diff --git a/pico/builder-config/tests/configbean/pom.xml b/builder/tests/configbean/pom.xml similarity index 79% rename from pico/builder-config/tests/configbean/pom.xml rename to builder/tests/configbean/pom.xml index 2864f8e3254..dabc0f9db49 100644 --- a/pico/builder-config/tests/configbean/pom.xml +++ b/builder/tests/configbean/pom.xml @@ -1,6 +1,5 @@ - io.helidon.pico.builder.config.tests - helidon-pico-builder-config-tests-project + io.helidon.builder.tests + helidon-builder-tests-project 4.0.0-SNAPSHOT ../pom.xml 4.0.0 - helidon-pico-builder-config-tests-test-config - Helidon Pico ConfigBean Tests + helidon-builder-tests-test-configbean + Helidon Builder ConfigBean Tests + Tests only the config bean basics (i.e., only common config, and no config-driven services) true @@ -43,13 +42,21 @@ - io.helidon.pico.builder.config - helidon-pico-builder-config + io.helidon.builder + helidon-builder-config + + + io.helidon.config + helidon-config-metadata + provided io.helidon.config helidon-config - test + + + io.helidon.config + helidon-config-yaml jakarta.inject @@ -61,11 +68,6 @@ jakarta.annotation-api provided - - io.helidon.pico.builder.config - helidon-pico-builder-config-processor - provided - io.helidon.common.testing helidon-common-testing-junit5 @@ -97,8 +99,8 @@ true - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.builder + helidon-builder-config-processor ${helidon.version} diff --git a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ClientConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestClientConfig.java similarity index 84% rename from pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ClientConfig.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestClientConfig.java index 6a260de4a14..19734ae8313 100644 --- a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ClientConfig.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestClientConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.testsubjects; +package io.helidon.builder.config.testsubjects; import java.util.Map; +import io.helidon.builder.config.ConfigBean; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; /** * For testing purpose. */ @ConfigBean(drivesActivation = false) -public interface ClientConfig extends CommonConfig { +public interface TestClientConfig extends TestCommonConfig { /** * For testing purpose. diff --git a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/CommonConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestCommonConfig.java similarity index 85% rename from pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/CommonConfig.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestCommonConfig.java index 364f7c228f9..e8588169887 100644 --- a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/CommonConfig.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestCommonConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.testsubjects; +package io.helidon.builder.config.testsubjects; import java.util.List; import io.helidon.builder.Builder; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; +import io.helidon.builder.config.ConfigBean; /** * For testing purpose. */ @ConfigBean @Builder(allowNulls = true) -public interface CommonConfig { +public interface TestCommonConfig { /** * For testing purpose. @@ -56,6 +56,6 @@ public interface CommonConfig { * * @return for testing purposes */ - char[] pwd(); + char[] pswd(); } diff --git a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ServerConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestServerConfig.java similarity index 82% rename from pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ServerConfig.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestServerConfig.java index f5a569a08ee..7b6b9d3125d 100644 --- a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/ServerConfig.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/TestServerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.testsubjects; +package io.helidon.builder.config.testsubjects; import java.util.Optional; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; +import io.helidon.builder.config.ConfigBean; /** * For testing purpose. */ @ConfigBean(atLeastOne = true) -public interface ServerConfig extends CommonConfig { +public interface TestServerConfig extends TestCommonConfig { /** * For testing purpose. diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeClientAuth.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeClientAuth.java similarity index 91% rename from pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeClientAuth.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeClientAuth.java index 65fb259f034..00e14b8e1ce 100644 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeClientAuth.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeClientAuth.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.fakes; +package io.helidon.builder.config.testsubjects.fakes; /** * Indicates whether the server requires authentication of tbe client by the certificate. diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeComponentTracingConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeComponentTracingConfig.java new file mode 100644 index 00000000000..44286929d94 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeComponentTracingConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.Map; + +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; + +/** + * aka ComponentTracing. + * + * A component is a single "layer" of the application that can trace. + * Component examples: + *

        + *
      • web-server: webServer adds the root tracing span + two additional spans (content-read and content-write)
      • + *
      • security: security adds the overall request security span, a span for authentication ("security:atn"), a span for + * authorization "security:atz", and a span for response processing ("security:response")
      • + *
      • jax-rs: JAX-RS integration adds spans for overall resource invocation
      • + *
      + */ +@ConfigBean +public interface FakeComponentTracingConfig extends FakeTraceableConfig { + + @Singular("span") // Builder::addSpan(String span, FakeSpanLogTracingConfigBean val), Impl::getSpan(String span), etc. + Map spanLogMap(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeyConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeyConfig.java new file mode 100644 index 00000000000..679e02fe9d6 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeyConfig.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.Builder; + +/** + * aka KeyConfig. + * + */ +@Builder +public interface FakeKeyConfig { + + /** + * The public key of this config if configured. + * + * @return the public key of this config or empty if not configured + */ + Optional publicKey(); + + /** + * The private key of this config if configured. + * + * @return the private key of this config or empty if not configured + */ + Optional privateKey(); + + /** + * The public X.509 Certificate if configured. + * + * @return the public certificate of this config or empty if not configured + */ + Optional publicCert(); + + /** + * The X.509 Certificate Chain. + * + * @return the certificate chain or empty list if not configured + */ + List certChain(); + + /** + * The X.509 Certificates. + * + * @return the certificates configured or empty list if none configured + */ + List certs(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeystoreConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeystoreConfig.java new file mode 100644 index 00000000000..4020998ace0 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeKeystoreConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.List; + +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; + +/** + * aka KeyConfig.Keystore.Builder + * + * This is a ConfigBean since it marries up to the backing config. + */ +@ConfigBean +public interface FakeKeystoreConfig { + + String DEFAULT_KEYSTORE_TYPE = "PKCS12"; + + @ConfiguredOption(key = "trust-store") + boolean trustStore(); + + @ConfiguredOption(key = "type", value = DEFAULT_KEYSTORE_TYPE) + String keystoreType(); + + @ConfiguredOption(key = "passphrase") + char[] keystorePassphrase(); + + @ConfiguredOption(key = "key.alias", value = "1") + String keyAlias(); + + @ConfiguredOption(key = "key.passphrase") + char[] keyPassphrase(); + + @ConfiguredOption(key = "cert.alias") + @Singular("certAlias") + List certAliases(); + + @ConfiguredOption(key = "cert-chain.alias") + String certChainAlias(); + +} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeNettyClientAuth.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeNettyClientAuth.java similarity index 86% rename from pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeNettyClientAuth.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeNettyClientAuth.java index 26809c9d12a..b1fdbad9ee1 100644 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeNettyClientAuth.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeNettyClientAuth.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.fakes; +package io.helidon.builder.config.testsubjects.fakes; public enum FakeNettyClientAuth { NONE, diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakePathTracingConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakePathTracingConfig.java new file mode 100644 index 00000000000..13eb926b51d --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakePathTracingConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.List; + +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; + +/** + * aka PathTracing. + * + * Traced system configuration for web server for a specific path. + */ +@ConfigBean +public interface FakePathTracingConfig { + + /** + * Path this configuration should configure. + * + * @return path on the web server + */ + String path(); + + /** + * Method(s) this configuration should be valid for. This can be used to restrict the configuration + * only to specific HTTP methods (such as {@code GET} or {@code POST}). + * + * @return list of methods, if empty, this configuration is valid for any method + */ + @Singular("method") // Builder::addMethod(String method); + List methods(); + + @ConfiguredOption(required = true) + FakeTracingConfig tracedConfig(); + +} diff --git a/pico/types/src/main/java/module-info.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeRoutingConfig.java similarity index 75% rename from pico/types/src/main/java/module-info.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeRoutingConfig.java index 5b28632adf3..2cdf61d8dda 100644 --- a/pico/types/src/main/java/module-info.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeRoutingConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,11 +14,11 @@ * limitations under the License. */ +package io.helidon.builder.config.testsubjects.fakes; + /** - * Pico minimal (spi) types module. + * aka Routing. */ -module io.helidon.pico.types { - requires io.helidon.common; +public interface FakeRoutingConfig extends FakeServerLifecycle { - exports io.helidon.pico.types; } diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerConfig.java new file mode 100644 index 00000000000..1565ecfec46 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerConfig.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * aka ServerConfiguration. + */ +@ConfigBean(drivesActivation = true) +public interface FakeServerConfig extends FakeSocketConfig { + + /** + * Returns the count of threads in the pool used to process HTTP requests. + *

      + * Default value is {@link Runtime#availableProcessors()}. + * + * @return a workers count + */ + int workersCount(); + + /** + * Returns a server port to listen on with the default server socket. If port is + * {@code 0} then any available ephemeral port will be used. + * + * @return the server port of the default server socket + */ + @Override + int port(); + + /** + * Returns a maximum length of the queue of incoming connections on the default server + * socket. + *

      + * Default value is {@link FakeSocketConfig#DEFAULT_BACKLOG_SIZE}. + * + * @return a maximum length of the queue of incoming connections + */ + @Override + int backlog(); + + /** + * Returns a default server socket timeout in milliseconds or {@code 0} for an infinite timeout. + * + * @return a default server socket timeout in milliseconds or {@code 0} + */ + @Override + int timeoutMillis(); + + /** + * Returns proposed value of the TCP receive window that is advertised to the remote peer on the + * default server socket. + *

      + * If {@code 0} then use implementation default. + * + * @return a buffer size in bytes of the default server socket or {@code 0} + */ + @Override + int receiveBufferSize(); + + /** + * A socket configuration of an additional named server socket. + *

      + * An additional named server socket may have a dedicated {@link FakeRoutingConfig} configured + * + * @param name the name of the additional server socket + * @return an additional named server socket configuration or {@code empty} if there is no such + * named server socket configured + */ + default Optional namedSocket(String name) { + return Optional.ofNullable(sockets().get(name)); + } + + // + // the socketList, socketSet, and sockets are sharing the same config key. This is atypical but here to ensure that the + // underlying builder machinery can handle these variants. We need to ensure that the attribute names do not clash, however, + // which is why we've used @Singular to disambiguate the attribute names where necessary. + // + + /** + * A list of all the configured sockets. This maps to the same underlying config as {@link #sockets()}. + * + * @return a list of all the configured server sockets, never null + */ + @Singular("sock") // note that singular names cannot clash + @ConfiguredOption(key = "sockets") + List socketList(); + + /** + * A set of all the configured sockets. This maps to the same underlying config as {@link #sockets()}. + * + * @return a set of all the configured server sockets, never null + */ + @ConfiguredOption(key = "sockets") + Set socketSet(); + + /** + * A map of all the configured server sockets; that is the default server socket. + * + * @return a map of all the configured server sockets, never null + */ + @Singular("socket") // note that singular names cannot clash + @ConfiguredOption(key = "sockets") + Map sockets(); + + /** + * The maximum amount of time that the server will wait to shut + * down regardless of the value of any additionally requested + * quiet period. + * + *

      The default implementation of this method returns {@link + * java.time.Duration#ofSeconds(long) Duration.ofSeconds(10L)}.

      + * + * @return the {@link java.time.Duration} to use + */ + @ConfiguredOption(key = "whatever") + default Duration maxShutdownTimeout() { + return Duration.ofSeconds(10L); + } + + /** + * The quiet period during which the webserver will wait for new + * incoming connections after it has been told to shut down. + * + *

      The webserver will wait no longer than the duration returned + * by the {@link #maxShutdownTimeout()} method.

      + * + *

      The default implementation of this method returns {@link + * java.time.Duration#ofSeconds(long) Duration.ofSeconds(0L)}, indicating + * that there will be no quiet period.

      + * + * @return the {@link java.time.Duration} to use + */ + default Duration shutdownQuietPeriod() { + return Duration.ofSeconds(0L); + } + + /** + * Whether to print details of HelidonFeatures. + * + * @return whether to print details + */ + boolean printFeatureDetails(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerLifecycle.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerLifecycle.java new file mode 100644 index 00000000000..f23aa1d2953 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeServerLifecycle.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +/** + * aka ServerLifecycle. + * Basic server lifecycle operations. + */ +public interface FakeServerLifecycle { + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSocketConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSocketConfig.java new file mode 100644 index 00000000000..89390654cdd --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSocketConfig.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.Optional; + +import io.helidon.builder.config.ConfigBean; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * aka ServerConfiguration. + * The SocketConfiguration configures a port to listen on and its associated server socket parameters. + */ +@ConfigBean(levelType = ConfigBean.LevelType.NESTED) +public interface FakeSocketConfig { + + /** + * The default backlog size to configure the server sockets with if no other value + * is provided. + */ + int DEFAULT_BACKLOG_SIZE = 1024; + + /** + * Name of this socket. + * Default to WebServer#DEFAULT_SOCKET_NAME for the main and + * default server socket. All other sockets must be named. + * + * @return name of this socket + */ + @ConfiguredOption("@default") + String name(); + + /** + * Returns a server port to listen on with the server socket. If port is + * {@code 0} then any available ephemeral port will be used. + * + * @return the server port of the server socket + */ + int port(); + + @ConfiguredOption(key = "bind-address") + String bindAddress(); + + /** + * Returns a maximum length of the queue of incoming connections on the server + * socket. + *

      + * Default value is {@link #DEFAULT_BACKLOG_SIZE}. + * + * @return a maximum length of the queue of incoming connections + */ + @ConfiguredOption("1024") + int backlog(); + + /** + * Returns a server socket timeout in milliseconds or {@code 0} for an infinite timeout. + * + * @return a server socket timeout in milliseconds or {@code 0} + */ + @ConfiguredOption(key = "timeout-millis") + int timeoutMillis(); + + /** + * Returns proposed value of the TCP receive window that is advertised to the remote peer on the + * server socket. + *

      + * If {@code 0} then use implementation default. + * + * @return a buffer size in bytes of the server socket or {@code 0} + */ + int receiveBufferSize(); + + /** + * Return a {@link FakeWebServerTlsConfig} containing server TLS configuration. When empty {@link java.util.Optional} is returned + * no TLS should be configured. + * + * @return web server tls configuration + */ + Optional tls(); + + /** + * Whether this socket is enabled (and will be opened on server startup), or disabled + * (and ignored on server startup). + * + * @return {@code true} for enabled socket, {@code false} for socket that should not be opened + */ + @ConfiguredOption("true") + boolean enabled(); + + /** + * Maximal size of all headers combined. + * + * @return size in bytes + */ + @ConfiguredOption(key = "max-header-size", value = "8192") + int maxHeaderSize(); + + /** + * Maximal length of the initial HTTP line. + * + * @return length + */ + @ConfiguredOption("4096") + int maxInitialLineLength(); + + /** + * Maximum size allowed for an HTTP payload in a client request. A negative + * value indicates that there is no maximum set. + * + * @return maximum payload size + */ + @ConfiguredOption("-1") + long maxPayloadSize(); + + /** + * Maximum length of the content of an upgrade request. + * + * @return maximum length of the content of an upgrade request + */ + @ConfiguredOption("65536") + int maxUpgradeContentLength(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanLogTracingConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanLogTracingConfig.java new file mode 100644 index 00000000000..6c57ee4be28 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanLogTracingConfig.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import io.helidon.builder.config.ConfigBean; + +/** + * aka SpanLogTracingConfig. + * Configuration of a single log event in a traced span. + */ +@ConfigBean +public interface FakeSpanLogTracingConfig extends FakeTraceableConfig { + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanTracingConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanTracingConfig.java new file mode 100644 index 00000000000..ed4a44bb166 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeSpanTracingConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; + +/** + * aka SpanTracingConfig. + * + * Configuration of a single traced span. + */ +@ConfigBean +public interface FakeSpanTracingConfig extends FakeTraceableConfig { + + /** + * When rename is desired, returns the new name. + * + * @return new name for this span or empty when rename is not desired + */ + Optional newName(); + + @Singular("spanLog") // B addSpanLog(String, FakeSpanLogTracingConfigBean); + Map spanLogMap(); + +} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTraceableConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTraceableConfig.java similarity index 87% rename from pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTraceableConfig.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTraceableConfig.java index 2a3d4a4e432..24a2a44cc58 100644 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTraceableConfig.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTraceableConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.fakes; +package io.helidon.builder.config.testsubjects.fakes; import java.util.Optional; -import io.helidon.pico.builder.config.ConfigBean; +import io.helidon.builder.config.ConfigBean; /** * aka Traceable. @@ -33,7 +33,7 @@ public interface FakeTraceableConfig { * {@code false} if it should not, * {@code empty} when this flag is not explicitly configured */ - /*protected*/ Optional isEnabled(); + Optional isEnabled(); /** * Name of this traceable unit. diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracer.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracer.java new file mode 100644 index 00000000000..7403f3f4688 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracer.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +/** + * Tracer abstraction. + * Tracer is the central point that collects tracing spans, and (probably) pushes them to backend. + */ +public interface FakeTracer { + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracingConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracingConfig.java new file mode 100644 index 00000000000..fc0646fdcbf --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeTracingConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.Map; + +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; + +/** + * aka TracingConfig. + * + * Tracing configuration that contains traced components (such as WebServer, Security) and their traced spans and span logs. + * Spans can be renamed through configuration, components, spans and span logs may be disabled through this configuration. + */ +@ConfigBean("tracing") +public interface FakeTracingConfig extends FakeTraceableConfig { + + @Singular("component") // Builder::addComponent(String component); Impl::getComponent(String component); + Map components(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeWebServerTlsConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeWebServerTlsConfig.java new file mode 100644 index 00000000000..c2b3db1d398 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/FakeWebServerTlsConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.security.SecureRandom; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +import javax.net.ssl.SSLContext; + +import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; +import io.helidon.common.LazyValue; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * aka WebServerTls. + * + * A class wrapping transport layer security (TLS) configuration for + * WebServer sockets. + */ +@ConfigBean(value = "tls", drivesActivation = false) +public interface FakeWebServerTlsConfig { + String PROTOCOL = "TLS"; + // secure random cannot be stored in native image, it must be initialized at runtime + LazyValue RANDOM = LazyValue.create(SecureRandom::new); + + /** + * This constant is a context classifier for the x509 client certificate if it is present. Callers may use this + * constant to lookup the client certificate associated with the current request context. + */ + String CLIENT_X509_CERTIFICATE = FakeWebServerTlsConfig.class.getName() + ".client-x509-certificate"; + + Set enabledTlsProtocols(); + + // TODO: had to make this Optional - we might need something like 'ExternalConfigBean' for this case ? + Optional sslContext(); + + @Singular("cipher") + @ConfiguredOption(key = "cipher") +// Set cipherSuite(); + List cipherSuite(); + + /** + * Whether this TLS config has security enabled (and the socket is going to be + * protected by one of the TLS protocols), or no (and the socket is going to be plain). + * + * @return {@code true} if this configuration represents a TLS configuration, {@code false} for plain configuration + */ + boolean enabled(); + +} diff --git a/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/SSLContextConfig.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/SSLContextConfig.java new file mode 100644 index 00000000000..28192cbad29 --- /dev/null +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/fakes/SSLContextConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.testsubjects.fakes; + +import java.util.Random; + +import io.helidon.builder.Builder; + +/** + * aka SSLContextBuilder. + * Note that this is just a normal builder, and will not be integrated with Config. + * Builder for configuring a new SslContext for creation. + */ +@Builder +public interface SSLContextConfig { + + String PROTOCOL = "TLS"; + Random RANDOM = new Random(); + + FakeKeyConfig privateKeyConfig(); + + FakeKeyConfig trustConfig(); + + long sessionCacheSize(); + + long sessionTimeout(); + +} diff --git a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/package-info.java b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/package-info.java similarity index 85% rename from pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/package-info.java rename to builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/package-info.java index 0c9849e207d..1dfc3ea4b78 100644 --- a/pico/builder-config/tests/configbean/src/main/java/io/helidon/pico/builder/config/testsubjects/package-info.java +++ b/builder/tests/configbean/src/main/java/io/helidon/builder/config/testsubjects/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -17,4 +17,4 @@ /** * ConfigBean test subjects. */ -package io.helidon.pico.builder.config.testsubjects; +package io.helidon.builder.config.testsubjects; diff --git a/pico/builder-config/tests/configbean/src/main/java/module-info.java b/builder/tests/configbean/src/main/java/module-info.java similarity index 72% rename from pico/builder-config/tests/configbean/src/main/java/module-info.java rename to builder/tests/configbean/src/main/java/module-info.java index e8fd093bb5f..6fd2df4edb4 100644 --- a/pico/builder-config/tests/configbean/src/main/java/module-info.java +++ b/builder/tests/configbean/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,17 +15,16 @@ */ /** - * Pico ConfigBean Builder test module. + * Helidon ConfigBean Builder test Module (i.e., only common config and w/o config-driven services). */ -module io.helidon.pico.builder.config.tests.test.config { +module io.helidon.builder.config.tests.test.config { requires static jakarta.inject; requires static jakarta.annotation; - requires static io.helidon.config.metadata; - requires io.helidon.common; requires io.helidon.common.config; - requires io.helidon.pico; - requires io.helidon.pico.builder.config; requires io.helidon.builder; + requires io.helidon.builder.config; + + exports io.helidon.builder.config.testsubjects; } diff --git a/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/AbstractConfigBeanTest.java b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/AbstractConfigBeanTest.java new file mode 100644 index 00000000000..3b8cd63d0bd --- /dev/null +++ b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/AbstractConfigBeanTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.test; + +import java.util.Map; + +import io.helidon.builder.config.testsubjects.fakes.FakeWebServerTlsConfig; +import io.helidon.config.ConfigSources; +import io.helidon.config.MapConfigSource; + +class AbstractConfigBeanTest { + static final String NESTED = "nested"; + static final String FAKE_SOCKET_CONFIG = "sockets"; + static final String FAKE_SERVER_CONFIG = "fake-server"; + + MapConfigSource.Builder createRootPlusOneSocketTestingConfigSource() { + return ConfigSources.create( + Map.of( + FAKE_SERVER_CONFIG + ".name", "root", + FAKE_SERVER_CONFIG + ".port", "8080", + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.name", "first", + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.port", "8081" + ), "config-nested-plus-one-socket"); + } + + MapConfigSource.Builder createNestedPlusOneSocketAndOneTlsTestingConfigSource() { + return ConfigSources.create( + Map.of( + NESTED + "." + FAKE_SERVER_CONFIG + ".name", "nested", + NESTED + "." + FAKE_SERVER_CONFIG + ".port", "8080", + NESTED + "." + FAKE_SERVER_CONFIG + ".worker-count", "2", + NESTED + "." + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.name", "first", + NESTED + "." + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.port", "8081", + NESTED + "." + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.tls.enabled", "true", + NESTED + "." + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.tls.cipher", "cipher-1", + NESTED + "." + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".1.tls.enabled-tls-protocols", + FakeWebServerTlsConfig.PROTOCOL + ), "config-nested-plus-one-socket-and-tls"); + } + +} diff --git a/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/BasicConfigBeanTest.java b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/BasicConfigBeanTest.java new file mode 100644 index 00000000000..537e16ba2cf --- /dev/null +++ b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/BasicConfigBeanTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.test; + +import java.util.List; +import java.util.Map; + +import io.helidon.builder.config.testsubjects.DefaultTestClientConfig; +import io.helidon.builder.config.testsubjects.DefaultTestServerConfig; +import io.helidon.builder.config.testsubjects.TestClientConfig; +import io.helidon.builder.config.testsubjects.TestServerConfig; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; + +class BasicConfigBeanTest { + + @Test + void acceptConfig() { + Config cfg = Config.builder( + ConfigSources.create( + Map.of("name", "server", + "port", "8080", + "description", "test", + "pswd", "pwd1", + "cipher-suites.0", "a", + "cipher-suites.1", "b", + "cipher-suites.2", "c", + "headers.0", "header1", + "headers.1", "header2"), + "my-simple-config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + TestServerConfig serverConfig = DefaultTestServerConfig.toBuilder(cfg).build(); + assertThat(serverConfig.description(), + optionalValue(equalTo("test"))); + assertThat(serverConfig.name(), + equalTo("server")); + assertThat(serverConfig.port(), + equalTo(8080)); + assertThat(new String(serverConfig.pswd()), + equalTo("pwd1")); + assertThat(serverConfig.toString(), + startsWith("TestServerConfig")); + assertThat(serverConfig.cipherSuites(), + contains("a", "b", "c")); + assertThat(serverConfig.toString(), + endsWith("(name=server, port=8080, cipherSuites=[a, b, c], pswd=not-null, description=Optional[test])")); + + TestClientConfig clientConfig = DefaultTestClientConfig.toBuilder(cfg).build(); + assertThat(clientConfig.name(), + equalTo("server")); + assertThat(clientConfig.port(), + equalTo(8080)); + assertThat(new String(clientConfig.pswd()), + equalTo("pwd1")); + assertThat(clientConfig.toString(), + startsWith("TestClientConfig")); + assertThat(clientConfig.cipherSuites(), + contains("a", "b", "c")); + assertThat(clientConfig.headers(), + hasEntry("headers.0", "header1")); + assertThat(clientConfig.headers(), + hasEntry("headers.1", "header2")); + assertThat(clientConfig.toString(), + endsWith("(name=server, port=8080, cipherSuites=[a, b, c], pswd=not-null, " + + "serverPort=0, headers={headers.1=header2, headers.0=header1})")); + } + + @Test + void emptyConfig() { + Config cfg = Config.create(); + TestServerConfig serverConfig = DefaultTestServerConfig.toBuilder(cfg).build(); + assertThat(serverConfig.description(), + optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(0)); + } + + /** + * Callers can conceptually use config beans as just plain old vanilla builders, void of any config usage. + */ + @Test + void noConfig() { + TestServerConfig serverConfig = DefaultTestServerConfig.builder().build(); + assertThat(serverConfig.description(), optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(0)); + assertThat(serverConfig.cipherSuites(), + equalTo(List.of())); + + serverConfig = DefaultTestServerConfig.toBuilder(serverConfig).port(123).build(); + assertThat(serverConfig.description(), + optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(123)); + assertThat(serverConfig.cipherSuites(), + equalTo(List.of())); + + TestClientConfig clientConfig = DefaultTestClientConfig.builder().build(); + assertThat(clientConfig.name(), + equalTo("default")); + assertThat(clientConfig.port(), + equalTo(0)); + assertThat(clientConfig.headers(), + equalTo(Map.of())); + assertThat(clientConfig.cipherSuites(), + equalTo(List.of())); + + clientConfig = DefaultTestClientConfig.toBuilder(clientConfig).port(123).build(); + assertThat(clientConfig.name(), + equalTo("default")); + assertThat(clientConfig.port(), + equalTo(123)); + assertThat(clientConfig.headers(), + equalTo(Map.of())); + assertThat(clientConfig.cipherSuites(), + equalTo(List.of())); + } + +} diff --git a/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/NestedConfigBeanTest.java b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/NestedConfigBeanTest.java new file mode 100644 index 00000000000..4a8499e6f5f --- /dev/null +++ b/builder/tests/configbean/src/test/java/io/helidon/builder/config/test/NestedConfigBeanTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.builder.config.test; + +import java.util.Objects; + +import io.helidon.builder.config.testsubjects.fakes.DefaultFakeServerConfig; +import io.helidon.builder.config.testsubjects.fakes.DefaultFakeSocketConfig; +import io.helidon.builder.config.testsubjects.fakes.FakeSocketConfig; +import io.helidon.builder.config.testsubjects.fakes.FakeWebServerTlsConfig; +import io.helidon.common.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.yaml.YamlConfigParser; + +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; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasEntry; + +class NestedConfigBeanTest extends AbstractConfigBeanTest { + + @Test + void rootServerConfigPlusOneSocket() { + Config cfg = io.helidon.config.Config.builder(createRootPlusOneSocketTestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + DefaultFakeServerConfig serverConfig = DefaultFakeServerConfig + .toBuilder(cfg.get(FAKE_SERVER_CONFIG)).build(); + + assertThat(serverConfig.name(), + equalTo("root")); + assertThat(serverConfig.port(), + equalTo(8080)); + + // validate the map + assertThat(serverConfig.sockets(), + hasEntry("1", + DefaultFakeSocketConfig.builder() + .name("first") + .port(8081) + .build())); + assertThat(serverConfig.sockets().get("1").tls(), + optionalEmpty()); + } + + @Test + void nestedServerConfigPlusOneSocketAndOneTls() { + Config cfg = io.helidon.config.Config.builder(createNestedPlusOneSocketAndOneTlsTestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + DefaultFakeServerConfig serverConfig = DefaultFakeServerConfig + .toBuilder(cfg.get(NESTED + "." + FAKE_SERVER_CONFIG)).build(); + + assertThat(serverConfig.name(), + equalTo("nested")); + assertThat(serverConfig.port(), + equalTo(8080)); + + // validate the map + FakeWebServerTlsConfig tls = serverConfig.sockets().get("1").tls().orElseThrow(); + assertThat(tls.enabled(), + is(true)); + assertThat(tls.cipherSuite(), + containsInAnyOrder("cipher-1")); + assertThat(tls.enabledTlsProtocols(), + containsInAnyOrder(FakeWebServerTlsConfig.PROTOCOL)); + } + + @Test + void fakeServerConfigFromUnnamedYaml() { + Config cfg = io.helidon.config.Config.builder() + .sources(ConfigSources.classpath("io/helidon/builder/config/test/FakeServerConfigPlusTwoUnnamedSockets.yaml")) + .addParser(YamlConfigParser.create()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + DefaultFakeServerConfig serverConfig = DefaultFakeServerConfig + .toBuilder(cfg.get(FAKE_SERVER_CONFIG)).build(); + + assertThat(serverConfig.name(), + equalTo("@default")); + + // validate the map + FakeSocketConfig zero = Objects.requireNonNull(serverConfig.namedSocket("0").orElse(null), + serverConfig.sockets().toString()); + assertThat(zero.bindAddress(), + equalTo("127.0.0.1")); + assertThat(zero.port(), + equalTo(8086)); + assertThat(zero.tls(), + optionalEmpty()); + FakeSocketConfig one = Objects.requireNonNull(serverConfig.sockets().get("1"), + serverConfig.sockets().toString()); + assertThat(one.bindAddress(), + equalTo("localhost")); + assertThat(one.port(), + equalTo(8087)); + FakeWebServerTlsConfig tls = one.tls().orElseThrow(); + assertThat(tls.enabled(), + is(true)); + assertThat(tls.cipherSuite(), + containsInAnyOrder("cipher-1")); + assertThat(tls.enabledTlsProtocols(), + containsInAnyOrder(FakeWebServerTlsConfig.PROTOCOL)); + + // validate the list + assertThat(serverConfig.socketList(), + contains(DefaultFakeSocketConfig.builder() + .bindAddress("127.0.0.1") + .port(8086) + .build(), + DefaultFakeSocketConfig.builder() + .bindAddress("localhost") + .port(8087) + .tls(tls) + .build())); + + // validate the set + assertThat(serverConfig.socketSet(), + containsInAnyOrder(DefaultFakeSocketConfig.builder() + .bindAddress("127.0.0.1") + .port(8086) + .build(), + DefaultFakeSocketConfig.builder() + .bindAddress("localhost") + .port(8087) + .tls(tls) + .build())); + } + + @Test + void fakeServerConfigFromNamedYaml() { + Config cfg = io.helidon.config.Config.builder() + .sources(ConfigSources.classpath("io/helidon/builder/config/test/FakeServerConfigPlusTwoNamedSockets.yaml")) + .addParser(YamlConfigParser.create()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + DefaultFakeServerConfig serverConfig = DefaultFakeServerConfig + .toBuilder(cfg.get(FAKE_SERVER_CONFIG)).build(); + + // validate the map + assertThat(serverConfig.name(), + equalTo("@default")); + FakeSocketConfig admin = serverConfig.namedSocket("admin").orElseThrow(); + assertThat(admin.port(), + equalTo(8086)); + assertThat(admin.name(), + equalTo("admin")); + + FakeSocketConfig secure = serverConfig.namedSocket("secure").orElseThrow(); + assertThat(secure.port(), + equalTo(8087)); + assertThat(secure.name(), + equalTo("obscure")); + FakeWebServerTlsConfig tls = secure.tls().orElseThrow(); + assertThat(tls.enabled(), + is(true)); + assertThat(tls.cipherSuite(), + containsInAnyOrder("cipher-1")); + assertThat(tls.enabledTlsProtocols(), + containsInAnyOrder(FakeWebServerTlsConfig.PROTOCOL)); + } + +} diff --git a/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoNamedSockets.yaml b/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoNamedSockets.yaml new file mode 100644 index 00000000000..dfced8379bb --- /dev/null +++ b/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoNamedSockets.yaml @@ -0,0 +1,30 @@ +# +# Copyright (c) 2023 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. +# + +fake-server: + sockets: + admin: + name: "admin" + bind-address: "127.0.0.1" + port: 8086 + secure: + name: "obscure" + bind-address: "localhost" + port: 8087 + tls: + enabled: true + cipher: "cipher-1" + enabled-tls-protocols: "TLS" diff --git a/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoUnnamedSockets.yaml b/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoUnnamedSockets.yaml new file mode 100644 index 00000000000..8d3147c7f3f --- /dev/null +++ b/builder/tests/configbean/src/test/resources/io/helidon/builder/config/test/FakeServerConfigPlusTwoUnnamedSockets.yaml @@ -0,0 +1,26 @@ +# +# Copyright (c) 2023 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. +# + +fake-server: + sockets: + - bind-address: "127.0.0.1" + port: 8086 + - bind-address: "localhost" + port: 8087 + tls: + enabled: true + cipher: "cipher-1" + enabled-tls-protocols: "TLS" diff --git a/builder/tests/nodeps/pom.xml b/builder/tests/nodeps/pom.xml index 168f33db2b7..be347a4c45b 100644 --- a/builder/tests/nodeps/pom.xml +++ b/builder/tests/nodeps/pom.xml @@ -1,4 +1,5 @@ + + + + 4.0.0 + + io.helidon.common + helidon-common-project + 4.0.0-SNAPSHOT + + + helidon-common-types + Helidon Common Types + + Abstraction of language types, that can be used during annotation processing and at runtime instead of reflection. + + + + + 11 + + + + + io.helidon.common + helidon-common + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.hamcrest + hamcrest-all + test + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + + + + + + diff --git a/pico/types/src/main/java/io/helidon/pico/types/AnnotationAndValue.java b/common/types/src/main/java/io/helidon/common/types/AnnotationAndValue.java similarity index 76% rename from pico/types/src/main/java/io/helidon/pico/types/AnnotationAndValue.java rename to common/types/src/main/java/io/helidon/common/types/AnnotationAndValue.java index 37217871324..6b657ee9165 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/AnnotationAndValue.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationAndValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.util.Map; import java.util.Optional; @@ -53,14 +53,4 @@ public interface AnnotationAndValue { */ Map values(); - /** - * Determines whether the {@link #value()} is present and a non-blank String (see {@link String#isBlank()}. - * - * @return true if our value is present and non-blank - */ - default boolean hasNonBlankValue() { - Optional val = value(); - return val.isPresent() && !val.get().isBlank(); - } - } diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java b/common/types/src/main/java/io/helidon/common/types/DefaultAnnotationAndValue.java similarity index 82% rename from pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java rename to common/types/src/main/java/io/helidon/common/types/DefaultAnnotationAndValue.java index f663230d034..618f4860eec 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java +++ b/common/types/src/main/java/io/helidon/common/types/DefaultAnnotationAndValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.lang.annotation.Annotation; import java.util.Collection; @@ -28,7 +28,6 @@ */ public class DefaultAnnotationAndValue implements AnnotationAndValue, Comparable { private final TypeName typeName; - private final String value; private final Map values; /** @@ -38,67 +37,13 @@ public class DefaultAnnotationAndValue implements AnnotationAndValue, Comparable * @see #builder() */ protected DefaultAnnotationAndValue(Builder b) { - this.typeName = b.typeName; - this.value = b.value; - this.values = Map.copyOf(b.values); - } - - @Override - public String toString() { - String result = getClass().getSimpleName() + "(typeName=" + typeName(); - if (values != null && !values.isEmpty()) { - result += ", values=" + values(); - } else if (value != null) { - result += ", value=" + value().orElse(null); - } - return result + ")"; - } - - @Override - public int hashCode() { - return Objects.hashCode(typeName()); - } - - @Override - public boolean equals(Object another) { - if (!(another instanceof AnnotationAndValue)) { - return false; - } - if (!Objects.equals(typeName(), ((AnnotationAndValue) another).typeName())) { - return false; - } - if (Objects.nonNull(values) && (another instanceof DefaultAnnotationAndValue) - && Objects.equals(values(), ((DefaultAnnotationAndValue) another).values())) { - return true; - } else if (Objects.nonNull(values) && values.size() > 1) { - return false; - } - String thisValue = value().orElse(""); - String anotherValue = ((AnnotationAndValue) another).value().orElse(""); - return thisValue.equals(anotherValue); - } - - @Override - public TypeName typeName() { - return typeName; - } - - @Override - public Optional value() { - if (Objects.nonNull(value)) { - return Optional.of(value); + LinkedHashMap map = new LinkedHashMap<>(b.values); + if (b.value != null) { + String prev = map.put("value", b.value); + assert (prev == null || prev.equals(b.value)); } - return value("value"); - } - - @Override - public Optional value(String name) { - return Objects.isNull(values) ? Optional.empty() : Optional.ofNullable(values.get(name)); - } - - @Override - public Map values() { - return values; + this.typeName = b.typeName; + this.values = Map.copyOf(map); } /** @@ -132,7 +77,8 @@ public static DefaultAnnotationAndValue create(TypeName annoType) { * @param value the annotation value * @return the new instance */ - public static DefaultAnnotationAndValue create(Class annoType, String value) { + public static DefaultAnnotationAndValue create(Class annoType, + String value) { return create(DefaultTypeName.create(annoType), value); } @@ -143,7 +89,8 @@ public static DefaultAnnotationAndValue create(Class annoT * @param values the annotation values * @return the new instance */ - public static DefaultAnnotationAndValue create(Class annoType, Map values) { + public static DefaultAnnotationAndValue create(Class annoType, + Map values) { return create(DefaultTypeName.create(annoType), values); } @@ -154,7 +101,8 @@ public static DefaultAnnotationAndValue create(Class annoT * @param value the annotation value * @return the new instance */ - public static DefaultAnnotationAndValue create(TypeName annoTypeName, String value) { + public static DefaultAnnotationAndValue create(TypeName annoTypeName, + String value) { return DefaultAnnotationAndValue.builder().typeName(annoTypeName).value(value).build(); } @@ -165,7 +113,8 @@ public static DefaultAnnotationAndValue create(TypeName annoTypeName, String val * @param values the annotation values * @return the new instance */ - public static DefaultAnnotationAndValue create(TypeName annoTypeName, Map values) { + public static DefaultAnnotationAndValue create(TypeName annoTypeName, + Map values) { return DefaultAnnotationAndValue.builder().typeName(annoTypeName).values(values).build(); } @@ -177,21 +126,15 @@ public static DefaultAnnotationAndValue create(TypeName annoTypeName, Map findFirst(String annoTypeName, - Collection coll) { + Collection coll) { assert (!annoTypeName.isBlank()); return coll.stream() .filter(it -> it.typeName().name().equals(annoTypeName)) .findFirst(); } - @Override - public int compareTo(AnnotationAndValue other) { - return typeName().compareTo(other.typeName()); - } - - /** - * Creates a builder for {@link io.helidon.pico.types.AnnotationAndValue}. + * Creates a builder for {@link AnnotationAndValue}. * * @return a fluent builder */ @@ -199,9 +142,64 @@ public static Builder builder() { return new Builder(); } + @Override + public String toString() { + String result = getClass().getSimpleName() + "(typeName=" + typeName(); + Optional value = value(); + if (!values.isEmpty() && value.isEmpty()) { + result += ", values=" + values(); + } else if (value.isPresent()) { + result += ", value=" + value.orElse(null); + } + return result + ")"; + } + + @Override + public int hashCode() { + return Objects.hash(typeName(), values); + } + + @Override + public boolean equals(Object another) { + if (!(another instanceof AnnotationAndValue)) { + return false; + } + if (!Objects.equals(typeName(), ((AnnotationAndValue) another).typeName())) { + return false; + } + if (!Objects.equals(values, ((AnnotationAndValue) another).values())) { + return false; + } + return true; + } + + @Override + public TypeName typeName() { + return typeName; + } + + @Override + public Optional value() { + return value("value"); + } + + @Override + public Optional value(String name) { + return Optional.ofNullable(values.get(name)); + } + + @Override + public Map values() { + return values; + } + + @Override + public int compareTo(AnnotationAndValue other) { + return typeName().compareTo(other.typeName()); + } /** - * Fluent API builder for {@link io.helidon.pico.types.DefaultAnnotationAndValue}. + * Fluent API builder for {@link DefaultAnnotationAndValue}. */ public static class Builder implements io.helidon.common.Builder { private final Map values = new LinkedHashMap<>(); @@ -268,4 +266,5 @@ public Builder type(Class annoType) { return typeName(DefaultTypeName.create(annoType)); } } + } diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java b/common/types/src/main/java/io/helidon/common/types/DefaultTypeInfo.java similarity index 76% rename from builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java rename to common/types/src/main/java/io/helidon/common/types/DefaultTypeInfo.java index eb9a5bb5ebe..97c117d64ad 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/DefaultTypeInfo.java @@ -14,20 +14,18 @@ * limitations under the License. */ -package io.helidon.builder.processor.spi; +package io.helidon.common.types; import java.util.ArrayList; import java.util.Collection; +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.Set; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; - /** * Default implementation for {@link TypeInfo}. */ @@ -37,6 +35,7 @@ public class DefaultTypeInfo implements TypeInfo { private final List annotations; private final List elementInfo; private final List otherElementInfo; + private final Map> referencedTypeNamesToAnnotations; private final TypeInfo superTypeInfo; private final Set modifierNames; @@ -54,6 +53,7 @@ protected DefaultTypeInfo(Builder b) { this.otherElementInfo = List.copyOf(b.otherElementInfo); this.superTypeInfo = b.superTypeInfo; this.modifierNames = Set.copyOf(b.modifierNames); + this.referencedTypeNamesToAnnotations = Map.copyOf(b.referencedTypeNamesToAnnotations); } /** @@ -90,6 +90,11 @@ public List otherElementInfo() { return otherElementInfo; } + @Override + public Map> referencedTypeNamesToAnnotations() { + return referencedTypeNamesToAnnotations; + } + @Override public Optional superTypeInfo() { return Optional.ofNullable(superTypeInfo); @@ -125,6 +130,7 @@ public static class Builder implements io.helidon.common.Builder annotations = new ArrayList<>(); private final List elementInfo = new ArrayList<>(); private final List otherElementInfo = new ArrayList<>(); + private final Map> referencedTypeNamesToAnnotations = new LinkedHashMap<>(); private final Set modifierNames = new LinkedHashSet<>(); private TypeName typeName; private String typeKind; @@ -155,7 +161,7 @@ public DefaultTypeInfo build() { */ public Builder typeName(TypeName val) { this.typeName = val; - return this; + return identity(); } /** @@ -166,7 +172,7 @@ public Builder typeName(TypeName val) { */ public Builder typeKind(String val) { this.typeKind = val; - return this; + return identity(); } /** @@ -179,7 +185,7 @@ public Builder annotations(Collection val) { Objects.requireNonNull(val); this.annotations.clear(); this.annotations.addAll(val); - return this; + return identity(); } /** @@ -191,7 +197,7 @@ public Builder annotations(Collection val) { public Builder addAnnotation(AnnotationAndValue val) { Objects.requireNonNull(val); annotations.add(Objects.requireNonNull(val)); - return this; + return identity(); } /** @@ -204,7 +210,7 @@ public Builder elementInfo(Collection val) { Objects.requireNonNull(val); this.elementInfo.clear(); this.elementInfo.addAll(val); - return this; + return identity(); } /** @@ -216,7 +222,7 @@ public Builder elementInfo(Collection val) { public Builder addElementInfo(TypedElementName val) { Objects.requireNonNull(val); elementInfo.add(val); - return this; + return identity(); } /** @@ -229,7 +235,7 @@ public Builder otherElementInfo(Collection val) { Objects.requireNonNull(val); this.otherElementInfo.clear(); this.otherElementInfo.addAll(val); - return this; + return identity(); } /** @@ -241,9 +247,52 @@ public Builder otherElementInfo(Collection val) { public Builder addOtherElementInfo(TypedElementName val) { Objects.requireNonNull(val); otherElementInfo.add(val); + return identity(); + } + + /** + * Sets the referencedTypeNamesToAnnotations to val. + * + * @param val the value + * @return this fluent builder + */ + public Builder referencedTypeNamesToAnnotations(Map> val) { + Objects.requireNonNull(val); + this.referencedTypeNamesToAnnotations.clear(); + this.referencedTypeNamesToAnnotations.putAll(val); return this; } + /** + * Adds a single referencedTypeNamesToAnnotations val. + * + * @param key the key + * @param val the value + * @return this fluent builder + */ + public Builder addReferencedTypeNamesToAnnotations(TypeName key, AnnotationAndValue val) { + return addReferencedTypeNamesToAnnotations(key, List.of(val)); + } + + /** + * Adds a collection of referencedTypeNamesToAnnotations values. + * + * @param key the key + * @param vals the values + * @return this fluent builder + */ + public Builder addReferencedTypeNamesToAnnotations(TypeName key, Collection vals) { + Objects.requireNonNull(vals); + referencedTypeNamesToAnnotations.compute(key, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.addAll(vals); + return v; + }); + return identity(); + } + /** * Sets the modifiers to val. * @@ -254,7 +303,7 @@ public Builder modifierNames(Collection val) { Objects.requireNonNull(val); this.modifierNames.clear(); this.modifierNames.addAll(val); - return this; + return identity(); } /** @@ -266,7 +315,7 @@ public Builder modifierNames(Collection val) { public Builder addModifierName(String val) { Objects.requireNonNull(val); modifierNames.add(val); - return this; + return identity(); } /** @@ -278,7 +327,7 @@ public Builder addModifierName(String val) { public Builder superTypeInfo(TypeInfo val) { Objects.requireNonNull(val); this.superTypeInfo = val; - return this; + return identity(); } } diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java b/common/types/src/main/java/io/helidon/common/types/DefaultTypeName.java similarity index 95% rename from pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java rename to common/types/src/main/java/io/helidon/common/types/DefaultTypeName.java index 049d3bbedf5..06d97c4e2d9 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java +++ b/common/types/src/main/java/io/helidon/common/types/DefaultTypeName.java @@ -14,17 +14,16 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Objects; /** - * Default implementation for {@link io.helidon.pico.types.TypeName}. + * Default implementation for {@link TypeName}. */ public class DefaultTypeName implements TypeName { private final String packageName; @@ -61,7 +60,7 @@ public String toString() { @Override public int hashCode() { - return Objects.hashCode(name()); + return Objects.hash(name(), primitive, array); } @Override @@ -77,7 +76,16 @@ public boolean equals(Object o) { @Override public int compareTo(TypeName o) { - return name().compareTo(o.name()); + int diff = name().compareTo(o.name()); + if (diff != 0) { + // different name + return diff; + } + diff = Boolean.compare(primitive, o.primitive()); + if (diff != 0) { + return diff; + } + return Boolean.compare(array, o.array()); } /** @@ -139,7 +147,7 @@ public static DefaultTypeName createFromTypeName(String typeName) { // a.b.c.SomeClass // a.b.c.SomeClass.InnerClass.Builder String className = typeName; - List packageElements = new LinkedList<>(); + List packageElements = new ArrayList<>(); while (true) { if (Character.isUpperCase(className.charAt(0))) { @@ -180,7 +188,7 @@ public static TypeName createExtendsTypeName(TypeName 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 + * @throws IllegalStateException if the name is invalid */ public static void ensureIsFQN(TypeName name) { if (!isFQN(name)) { @@ -294,7 +302,7 @@ protected String calcFQName() { /** - * Creates a builder for {@link io.helidon.pico.types.TypeName}. + * Creates a builder for {@link TypeName}. * * @return a fluent builder */ diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java b/common/types/src/main/java/io/helidon/common/types/DefaultTypedElementName.java similarity index 92% rename from pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java rename to common/types/src/main/java/io/helidon/common/types/DefaultTypedElementName.java index 39b5fed7e26..0216db7590f 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java +++ b/common/types/src/main/java/io/helidon/common/types/DefaultTypedElementName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.util.ArrayList; import java.util.Collection; @@ -25,7 +25,7 @@ import java.util.Set; /** - * Default implementation for {@link io.helidon.pico.types.TypedElementName}. + * Default implementation for {@link io.helidon.common.types.TypedElementName}. */ public class DefaultTypedElementName implements TypedElementName { private final TypeName typeName; @@ -65,7 +65,7 @@ public String elementName() { } @Override - public String elementKind() { + public String elementTypeKind() { return elementKind; } @@ -96,7 +96,7 @@ public Set modifierNames() { @Override public int hashCode() { - return System.identityHashCode(typeName()); + return Objects.hash(typeName(), elementName(), elementTypeKind(), annotations()); } @Override @@ -108,7 +108,7 @@ public boolean equals(Object another) { TypedElementName other = (TypedElementName) another; return Objects.equals(typeName(), other.typeName()) && Objects.equals(elementName(), other.elementName()) - && Objects.equals(elementKind(), other.elementKind()) + && Objects.equals(elementTypeKind(), other.elementTypeKind()) && Objects.equals(annotations(), other.annotations()); } @@ -128,7 +128,7 @@ public String toDeclaration() { /** - * Creates a builder for {@link io.helidon.pico.types.TypedElementName}. + * Creates a builder for {@link io.helidon.common.types.TypedElementName}. * * @return a fluent builder */ @@ -164,6 +164,7 @@ protected Builder() { * @return this fluent builder */ public Builder typeName(TypeName val) { + Objects.requireNonNull(val); this.typeName = val; return this; } @@ -198,6 +199,7 @@ public Builder componentTypeNames(List val) { * @return this fluent builder */ public Builder elementName(String val) { + Objects.requireNonNull(val); this.elementName = val; return this; } @@ -209,6 +211,7 @@ public Builder elementName(String val) { * @return this fluent builder */ public Builder elementKind(String val) { + Objects.requireNonNull(val); this.elementKind = val; return this; } @@ -220,6 +223,7 @@ public Builder elementKind(String val) { * @return this fluent builder */ public Builder defaultValue(String val) { + Objects.requireNonNull(val); this.defaultValue = val; return this; } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java new file mode 100644 index 00000000000..f0daf685835 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2022, 2023 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.common.types; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Represents the model object for a type. + */ +public interface TypeInfo { + /** + * The {@code public} modifier. + */ + String MODIFIER_PUBLIC = "PUBLIC"; + /** + * The {@code protected} modifier. + */ + String MODIFIER_PROTECTED = "PROTECTED"; + /** + * The {@code private} modifier. + */ + String MODIFIER_PRIVATE = "PRIVATE"; + /** + * The {@code abstract} modifier. + */ + String MODIFIER_ABSTRACT = "ABSTRACT"; + /** + * The {@code default} modifier. + */ + String MODIFIER_DEFAULT = "DEFAULT"; + /** + * The {@code static} modifier. + */ + String MODIFIER_STATIC = "STATIC"; + /** + * The {@code sealed} modifier. + */ + String MODIFIER_SEALED = "SEALED"; + /** + * The {@code final} modifier. + */ + String MODIFIER_FINAL = "FINAL"; + + + /** + * Field element type kind. + * See javax.lang.model.element.ElementKind#FIELD + */ + String KIND_FIELD = "FIELD"; + + /** + * Method element type kind. + * See javax.lang.model.element.ElementKind#METHOD + */ + String KIND_METHOD = "METHOD"; + + /** + * Constructor element type kind. + * See javax.lang.model.element.ElementKind#CONSTRUCTOR + */ + String KIND_CONSTRUCTOR = "CONSTRUCTOR"; + + /** + * Parameter element type kind. + * See javax.lang.model.element.ElementKind#PARAMETER + */ + String KIND_PARAMETER = "PARAMETER"; + + /** + * Interface element type kind. + * See javax.lang.model.element.ElementKind#INTERFACE + */ + String KIND_INTERFACE = "INTERFACE"; + + /** + * Interface element type kind. + * See javax.lang.model.element.ElementKind#CLASS + */ + String KIND_CLASS = "CLASS"; + + /** + * Enum element type kind. + * See javax.lang.model.element.ElementKind#ENUM + */ + String KIND_ENUM = "ENUM"; + + /** + * Annotation element type kind. + * See javax.lang.model.element.ElementKind#ANNOTATION_TYPE + */ + String KIND_ANNOTATION_TYPE = "ANNOTATION_TYPE"; + + /** + * Package element type kind. + * See javax.lang.model.element.ElementKind#PACKAGE + */ + String KIND_PACKAGE = "PACKAGE"; + + /** + * Record element type kind (since Java 16). + * See javax.lang.model.element.ElementKind#RECORD + */ + String KIND_RECORD = "RECORD"; + + /** + * The type name. + * + * @return the type name + */ + TypeName typeName(); + + /** + * The type element kind. + * + * @return the type element kind (e.g., "{@value #KIND_INTERFACE}", "{@value #KIND_ANNOTATION_TYPE}", etc.) + * @see #KIND_CLASS and other constants on this class prefixed with {@code TYPE} + */ + String typeKind(); + + /** + * The annotations on the type. + * + * @return the annotations on the type + */ + List annotations(); + + /** + * The elements that make up the type that are relevant for processing. + * + * @return the elements that make up the type that are relevant for processing + */ + List elementInfo(); + + /** + * The elements that make up this type that are considered "other", or being skipped because they are irrelevant to + * processing. + * + * @return the elements that still make up the type, but are otherwise deemed irrelevant for processing + */ + List otherElementInfo(); + + /** + * Any Map, List, Set, or method that has {@link TypeName#typeArguments()} will be analyzed and any type arguments will have + * its annotations added here. Note that this only applies to non-built-in types. + * + * @return all referenced types + */ + Map> referencedTypeNamesToAnnotations(); + + /** + * The parent/super class for this type info. + * + * @return the super type + */ + Optional superTypeInfo(); + + /** + * Element modifiers. + * + * @return element modifiers + * @see #MODIFIER_PUBLIC and other constants prefixed with {@code MODIFIER} + */ + Set modifierNames(); + +} diff --git a/pico/types/src/main/java/io/helidon/pico/types/TypeName.java b/common/types/src/main/java/io/helidon/common/types/TypeName.java similarity index 94% rename from pico/types/src/main/java/io/helidon/pico/types/TypeName.java rename to common/types/src/main/java/io/helidon/common/types/TypeName.java index c45f002b470..59a748634cf 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/TypeName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.util.List; @@ -22,11 +22,11 @@ * TypeName is similar to {@link java.lang.reflect.Type} in its most basic use case. The {@link #name()} returns the package + * class name tuple for the given type (i.e., the canonical type name). *

      - * This class also provides a number of methods that are typically found in {@link java.lang.Class} that can be used to avoid + * This class also provides a number of methods that are typically found in {@link Class} that can be used to avoid * classloading resolution: *

        *
      • {@link #packageName()} and {@link #className()} - access to the package and simple class names.
      • - *
      • {@link #primitive()} and {@link #array()} - access to flags that is typically found in {@link java.lang.Class}.
      • + *
      • {@link #primitive()} and {@link #array()} - access to flags that is typically found in {@link Class}.
      • *
      * Additionally, this class offers a number of additional methods that are useful for handling generics: *
        @@ -134,7 +134,8 @@ default boolean isOptional() { String declaredName(); /** - * The fully qualified type name. This will include the generic portion of the declaration, as well as any array declaration, etc. + * The fully qualified type name. This will include the generic portion of the declaration, as well as any array + * declaration, etc. * * @return the fully qualified name which includes the use of generics/parameterized types, arrays, etc. */ diff --git a/pico/types/src/main/java/io/helidon/pico/types/TypedElementName.java b/common/types/src/main/java/io/helidon/common/types/TypedElementName.java similarity index 85% rename from pico/types/src/main/java/io/helidon/pico/types/TypedElementName.java rename to common/types/src/main/java/io/helidon/common/types/TypedElementName.java index 95df3697c3e..9720dce9c40 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/TypedElementName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.types; +package io.helidon.common.types; import java.util.List; import java.util.Optional; @@ -24,9 +24,9 @@ * Provides a way to describe method, field, or annotation attribute. */ public interface TypedElementName { - /** - * The type name for the element (e.g., java.util.List). + * The type name for the element (e.g., java.util.List). If the element is a method, then this is the return type of + * the method. * * @return the type name of the element */ @@ -43,8 +43,9 @@ public interface TypedElementName { * The kind of element (e.g., method, field, etc). * * @return the element kind + * @see io.helidon.common.types.TypeInfo */ - String elementKind(); + String elementTypeKind(); /** * The default value assigned to the element, represented as a string. @@ -79,6 +80,7 @@ public interface TypedElementName { * Element modifiers. * * @return element modifiers + * @see io.helidon.common.types.TypeInfo */ Set modifierNames(); diff --git a/pico/types/src/main/java/io/helidon/pico/types/package-info.java b/common/types/src/main/java/io/helidon/common/types/package-info.java similarity index 73% rename from pico/types/src/main/java/io/helidon/pico/types/package-info.java rename to common/types/src/main/java/io/helidon/common/types/package-info.java index 1d313b87557..b67a9300628 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/package-info.java +++ b/common/types/src/main/java/io/helidon/common/types/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,7 +15,7 @@ */ /** - * Subset of Pico's SPI types that are useful for runtime. Used in the Pico Config builder, etc., that require a minimal set of + * Subset of Builder's SPI types that are useful for runtime. Used in the ConfigBean builder, etc., that require a minimal set of * types present at runtime. */ -package io.helidon.pico.types; +package io.helidon.common.types; diff --git a/common/types/src/main/java/module-info.java b/common/types/src/main/java/module-info.java new file mode 100644 index 00000000000..5ad3598384c --- /dev/null +++ b/common/types/src/main/java/module-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022, 2023 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. + */ + +/** + * Builder (minimal) types support. + */ +module io.helidon.common.types { + requires transitive io.helidon.common; + + exports io.helidon.common.types; +} diff --git a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultAnnotationAndValueTest.java b/common/types/src/test/java/io/helidon/common/types/DefaultAnnotationAndValueTest.java similarity index 73% rename from pico/types/src/test/java/io/helidon/pico/types/test/DefaultAnnotationAndValueTest.java rename to common/types/src/test/java/io/helidon/common/types/DefaultAnnotationAndValueTest.java index 7f2459d0cd8..8051155e24a 100644 --- a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultAnnotationAndValueTest.java +++ b/common/types/src/test/java/io/helidon/common/types/DefaultAnnotationAndValueTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,15 +14,14 @@ * limitations under the License. */ -package io.helidon.pico.types.test; +package io.helidon.common.types; +import java.lang.annotation.Target; import java.util.Map; -import io.helidon.pico.types.DefaultAnnotationAndValue; - -import jakarta.inject.Named; import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -34,19 +33,19 @@ void sanity() { DefaultAnnotationAndValue val1 = DefaultAnnotationAndValue.create(Test.class); assertThat(val1.typeName().toString(), equalTo(Test.class.getName())); assertThat(val1.toString(), - equalTo("DefaultAnnotationAndValue(typeName=" + Test.class.getName() + ")")); + containsString("typeName=" + Test.class.getName())); DefaultAnnotationAndValue val2 = DefaultAnnotationAndValue.create(Test.class); assertThat(val2, equalTo(val1)); assertThat(val2.compareTo(val1), is(0)); - DefaultAnnotationAndValue val3 = DefaultAnnotationAndValue.create(Named.class, "name"); + DefaultAnnotationAndValue val3 = DefaultAnnotationAndValue.create(Target.class, "name"); assertThat(val3.toString(), - equalTo("DefaultAnnotationAndValue(typeName=jakarta.inject.Named, value=name)")); + containsString("typeName=java.lang.annotation.Target, value=name")); DefaultAnnotationAndValue val4 = DefaultAnnotationAndValue.create(Test.class, Map.of("a", "1")); assertThat(val4.toString(), - equalTo("DefaultAnnotationAndValue(typeName=" + Test.class.getName() + ", values={a=1})")); + containsString("typeName=" + Test.class.getName() + ", values={a=1}")); } } diff --git a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypeNameTest.java b/common/types/src/test/java/io/helidon/common/types/DefaultTypeNameTest.java similarity index 65% rename from pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypeNameTest.java rename to common/types/src/test/java/io/helidon/common/types/DefaultTypeNameTest.java index 9188b1c8ae4..84bb0204afc 100644 --- a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypeNameTest.java +++ b/common/types/src/test/java/io/helidon/common/types/DefaultTypeNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,25 +14,29 @@ * limitations under the License. */ -package io.helidon.pico.types.test; +package io.helidon.common.types; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; - -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; +import java.util.Set; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import static io.helidon.pico.types.DefaultTypeName.create; -import static io.helidon.pico.types.DefaultTypeName.createFromTypeName; +import static io.helidon.common.types.DefaultTypeName.builder; +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.common.types.DefaultTypeName.createFromTypeName; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; class DefaultTypeNameTest { @@ -152,7 +156,7 @@ void primitiveArrayTypes() { } @Test - public void nonPrimitiveUsages() { + void nonPrimitiveUsages() { assertThat(create(Boolean.class).toString(), is("java.lang.Boolean")); assertThat(create(Long.class).toString(), is("java.lang.Long")); assertThat(create(Object.class).toString(), is("java.lang.Object")); @@ -193,7 +197,7 @@ public void nonPrimitiveUsages() { } @Test - public void typeArguments() { + void typeArguments() { DefaultTypeName typeName = DefaultTypeName.create(List.class) .toBuilder() .typeArguments(Collections.singletonList(DefaultTypeName.create(String.class))) @@ -201,29 +205,29 @@ public void typeArguments() { assertThat(typeName.fqName(), is("java.util.List")); assertThat(typeName.toString(), - is("java.util.List")); + equalTo(typeName.fqName())); assertThat(typeName.name(), - is("java.util.List")); + is("java.util.List")); typeName = DefaultTypeName.createFromTypeName("? extends pkg.Something"); assertThat(typeName.wildcard(), is(true)); assertThat(typeName.fqName(), is("? extends pkg.Something")); assertThat(typeName.toString(), - is("? extends pkg.Something")); + is("? extends pkg.Something")); assertThat(typeName.name(), - is("pkg.Something")); + is("pkg.Something")); assertThat(typeName.packageName(), - is("pkg")); + is("pkg")); assertThat(typeName.className(), - is("Something")); + is("Something")); typeName = DefaultTypeName.createFromTypeName("?"); assertThat(typeName.wildcard(), is(true)); assertThat(typeName.fqName(), is("?")); assertThat(typeName.toString(), - is("?")); + equalTo(typeName.fqName())); assertThat(typeName.name(), is(Object.class.getName())); assertThat(typeName.packageName(), @@ -236,28 +240,28 @@ public void typeArguments() { .typeArguments(Collections.singletonList(DefaultTypeName.createFromTypeName("? extends pkg.Something"))) .build(); assertThat(typeName.fqName(), - is("java.util.List")); + is("java.util.List")); assertThat(typeName.toString(), - is("java.util.List")); + equalTo(typeName.fqName())); assertThat(typeName.name(), - is("java.util.List")); + is("java.util.List")); } @SuppressWarnings("unchecked") @Test - public void declaredName() { - List> list = new LinkedList<>(); + void declaredName() { + List> list = new ArrayList<>(); List>[] arrayOfLists = new List[] {}; assertThat(DefaultTypeName.create(char.class).declaredName(), equalTo("char")); assertThat(DefaultTypeName.create(char[].class).declaredName(), equalTo("char[]")); - assertThat(DefaultTypeName.create(list.getClass()).declaredName(), equalTo("java.util.LinkedList")); + assertThat(DefaultTypeName.create(list.getClass()).declaredName(), equalTo("java.util.ArrayList")); assertThat(DefaultTypeName.create(arrayOfLists.getClass()).declaredName(), equalTo("java.util.List[]")); assertThat(DefaultTypeName.create(List[].class).declaredName(), equalTo("java.util.List[]")); } @Test - public void genericDecl() { + void genericDecl() { DefaultTypeName genericTypeName = DefaultTypeName.createFromGenericDeclaration("CB"); assertThat(genericTypeName.name(), equalTo("CB")); assertThat(genericTypeName.fqName(), equalTo("CB")); @@ -283,13 +287,13 @@ public void genericDecl() { .build(); assertThat(typeName.name(), equalTo("java.util.Map")); assertThat(typeName.fqName(), equalTo("java.util.Map")); - assertThat(typeName.toString(), equalTo("java.util.Map")); + assertThat(typeName.toString(), equalTo(typeName.fqName())); // note: in the future was can always add getBoundedTypeName() genericTypeName = DefaultTypeName.createFromGenericDeclaration("CB extends MyClass"); assertThat(genericTypeName.name(), equalTo("CB extends MyClass")); assertThat(genericTypeName.fqName(), equalTo("CB extends MyClass")); - assertThat(genericTypeName.toString(), equalTo("CB extends MyClass")); + assertThat(genericTypeName.toString(), equalTo(genericTypeName.fqName())); assertThat(genericTypeName.generic(), is(true)); assertThat(genericTypeName.wildcard(), is(false)); } @@ -325,4 +329,119 @@ void extendsTypeName() { assertThat(extendsName.name(), equalTo("java.util.Map")); } + @Test + void testDefaultMethods() { + TypeName typeName = DefaultTypeName.create(Optional.class); + assertThat("isOptional() for: " + typeName.name(), typeName.isOptional(), is(true)); + assertThat("isList() for: " + typeName.name(), typeName.isList(), is(false)); + assertThat("isMap() for: " + typeName.name(), typeName.isMap(), is(false)); + assertThat("isSet() for: " + typeName.name(), typeName.isSet(), is(false)); + + typeName = DefaultTypeName.create(Set.class); + assertThat("isOptional() for: " + typeName.name(), typeName.isOptional(), is(false)); + assertThat("isList() for: " + typeName.name(), typeName.isList(), is(false)); + assertThat("isMap() for: " + typeName.name(), typeName.isMap(), is(false)); + assertThat("isSet() for: " + typeName.name(), typeName.isSet(), is(true)); + + typeName = DefaultTypeName.create(List.class); + assertThat("isOptional() for: " + typeName.name(), typeName.isOptional(), is(false)); + assertThat("isList() for: " + typeName.name(), typeName.isList(), is(true)); + assertThat("isMap() for: " + typeName.name(), typeName.isMap(), is(false)); + assertThat("isSet() for: " + typeName.name(), typeName.isSet(), is(false)); + + typeName = DefaultTypeName.create(Map.class); + assertThat("isOptional() for: " + typeName.name(), typeName.isOptional(), is(false)); + assertThat("isList() for: " + typeName.name(), typeName.isList(), is(false)); + assertThat("isMap() for: " + typeName.name(), typeName.isMap(), is(true)); + assertThat("isSet() for: " + typeName.name(), typeName.isSet(), is(false)); + + typeName = DefaultTypeName.create(String.class); + assertThat("isOptional() for: " + typeName.name(), typeName.isOptional(), is(false)); + assertThat("isList() for: " + typeName.name(), typeName.isList(), is(false)); + assertThat("isMap() for: " + typeName.name(), typeName.isMap(), is(false)); + assertThat("isSet() for: " + typeName.name(), typeName.isSet(), is(false)); + } + + @ParameterizedTest + @MethodSource("equalsAndCompareSource") + @SuppressWarnings("unchecked") + void hashEqualsAndCompare(EqualsData data) { + if (data.equal) { + assertThat("equals", data.first, equalTo(data.second)); + assertThat("equals", data.second, equalTo(data.first)); + assertThat("has", data.first.hashCode(), is(data.second.hashCode())); + if (data.canCompare) { + assertThat("compare", data.first.compareTo(data.second), is(0)); + assertThat("compare", data.second.compareTo(data.first), is(0)); + } + } else { + assertThat("equals", data.first, not(equalTo(data.second))); + assertThat("equals", data.second, not(equalTo(data.first))); + assertThat("has", data.first.hashCode(), not(data.second.hashCode())); + if (data.canCompare) { + int compareOne = data.first.compareTo(data.second); + int compareTwo = data.second.compareTo(data.first); + assertThat("compare", compareOne, not(0)); + assertThat("compare", compareTwo, not(0)); + assertThat("compare", compareOne, not(compareTwo)); + // also make sure one is negative and one positive + assertThat("compare has negative and positive", (compareOne * compareTwo), lessThan(0)); + } + } + } + + private static Stream equalsAndCompareSource() { + return Stream.of( + new EqualsData(create(DefaultTypeNameTest.class), create(DefaultTypeNameTest.class), true), + new EqualsData(create(DefaultTypeNameTest.class), + builder().type(DefaultTypeNameTest.class).array(true).build(), + false), + new EqualsData(create(DefaultTypeNameTest.class), + builder().type(DefaultTypeNameTest.class).primitive(true).build(), + false), + new EqualsData(create(DefaultTypeNameTest.class), + builder().type(DefaultTypeNameTest.class).primitive(true).array(true).build(), + false), + new EqualsData(builder().type(DefaultTypeNameTest.class).array(true).build(), + builder().type(DefaultTypeNameTest.class).array(true).build(), + true), + new EqualsData(builder().type(DefaultTypeNameTest.class).array(true).build(), + builder().type(DefaultTypeNameTest.class).array(true).primitive(true).build(), + false), + new EqualsData(builder().type(DefaultTypeNameTest.class).primitive(true).build(), + builder().type(DefaultTypeNameTest.class).primitive(true).build(), + true), + new EqualsData(builder().type(DefaultTypeNameTest.class).primitive(true).build(), + builder().type(DefaultTypeNameTest.class).array(true).primitive(true).build(), + false), + new EqualsData(create(long.class), + builder().className("long").primitive(false).build(), + false), + new EqualsData(create(DefaultTypeNameTest.class), "Some string", false, false) + ); + } + + @SuppressWarnings("rawtypes") + private final static class EqualsData { + private final Comparable first; + private final Comparable second; + private final boolean equal; + private final boolean canCompare; + + private EqualsData(Comparable first, Comparable second, boolean equal) { + this(first, second, equal, true); + } + + private EqualsData(Comparable first, Comparable second, boolean equal, boolean canCompare) { + this.first = first; + this.second = second; + this.equal = equal; + this.canCompare = canCompare; + } + + @Override + public String toString() { + return first + ", " + second + ", equals: " + equal; + } + } } diff --git a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypedElementNameTest.java b/common/types/src/test/java/io/helidon/common/types/DefaultTypedElementNameTest.java similarity index 93% rename from pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypedElementNameTest.java rename to common/types/src/test/java/io/helidon/common/types/DefaultTypedElementNameTest.java index 87c18ffd82a..6552662f6f4 100644 --- a/pico/types/src/test/java/io/helidon/pico/types/test/DefaultTypedElementNameTest.java +++ b/common/types/src/test/java/io/helidon/common/types/DefaultTypedElementNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,13 +14,11 @@ * limitations under the License. */ -package io.helidon.pico.types.test; - -import io.helidon.pico.types.DefaultTypedElementName; +package io.helidon.common.types; import org.junit.jupiter.api.Test; -import static io.helidon.pico.types.DefaultTypeName.create; +import static io.helidon.common.types.DefaultTypeName.create; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/etc/checkstyle-suppressions.xml b/etc/checkstyle-suppressions.xml index 1ec36407da1..1f2c11c3f36 100644 --- a/etc/checkstyle-suppressions.xml +++ b/etc/checkstyle-suppressions.xml @@ -90,17 +90,21 @@ checks=".*"/> - - - - - + + + + + + diff --git a/etc/copyright-exclude.txt b/etc/copyright-exclude.txt index c72f400ab06..50bf4f6698c 100644 --- a/etc/copyright-exclude.txt +++ b/etc/copyright-exclude.txt @@ -67,3 +67,4 @@ persistence_3_0.xsd # excluded as this is a test file and we need to validate its content src/test/resources/static/classpath/index.html ._java_ +._pico_ diff --git a/examples/nima/pico/pom.xml b/examples/nima/pico/pom.xml new file mode 100644 index 00000000000..50b0007e6a6 --- /dev/null +++ b/examples/nima/pico/pom.xml @@ -0,0 +1,158 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.0.0-SNAPSHOT + ../../../applications/se/pom.xml + + + io.helidon.examples.nima + helidon-examples-nima-pico + Helidon Níma Pico Example + + + io.helidon.examples.nima.pico.PicoMain + + + + + io.helidon.nima + helidon-nima + + + io.helidon.nima.webserver + helidon-nima-webserver + + + io.helidon.nima.webclient + helidon-nima-webclient + + + io.helidon.common.features + helidon-common-features + + + io.helidon.pico + helidon-pico-api + + + io.helidon.pico + helidon-pico-services + + + io.helidon.pico.configdriven + helidon-pico-configdriven-services + + + io.helidon.config + helidon-config-yaml + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + io.helidon.nima.http + helidon-nima-http-processor + ${helidon.version} + + + + -Aio.helidon.pico.autoAddNonContractInterfaces=true + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + diff --git a/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/ConfigService.java b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/ConfigService.java new file mode 100644 index 00000000000..bc9b188b7ce --- /dev/null +++ b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/ConfigService.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.examples.nima.pico; + +import io.helidon.config.Config; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * This service will be part of Níma on Pico module. + * It may use pico to get config sources exposed through pico. + */ +@Singleton +public class ConfigService implements Provider { + @Override + public Config get() { + return Config.create(); + } +} diff --git a/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/GreetEndpoint.java b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/GreetEndpoint.java new file mode 100644 index 00000000000..faefddb7092 --- /dev/null +++ b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/GreetEndpoint.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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.examples.nima.pico; + +import io.helidon.common.http.Entity; +import io.helidon.common.http.GET; +import io.helidon.common.http.HeaderParam; +import io.helidon.common.http.Http; +import io.helidon.common.http.POST; +import io.helidon.common.http.Path; +import io.helidon.common.http.PathParam; +import io.helidon.common.http.QueryParam; + +import jakarta.inject.Singleton; + +@Singleton +@Path("/greet") +class GreetEndpoint { + private String greeting = "Hello"; + + @GET + String greet() { + return greeting + " World!"; + } + + @GET + @Path("/{name}") + String greetNamed(@PathParam("name") String name, + @HeaderParam(Http.Header.HOST_STRING) String hostHeader, + @QueryParam("required") String requiredQuery, + @QueryParam(value = "optional", defaultValue = "defaultValue") String optionalQuery) { + return greeting + " " + name + "! Requested host: " + hostHeader + ", required query: " + requiredQuery + + ", optionalQuery: " + optionalQuery; + } + + @POST + void post(@Entity String message) { + greeting = message; + } +} diff --git a/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PicoMain.java b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PicoMain.java new file mode 100644 index 00000000000..cd85207af34 --- /dev/null +++ b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PicoMain.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.examples.nima.pico; + +import java.util.Optional; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.logging.common.LogConfig; +import io.helidon.pico.Bootstrap; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.PicoServices; + +/** + * As simple as possible with a fixed port. + */ +public final class PicoMain { + private PicoMain() { + } + + /** + * Start the example. + * + * @param args ignored + */ + public static void main(String[] args) { + // todo move to a Níma on Pico module + LogConfig.configureRuntime(); + + Optional existingBootstrap = PicoServices.globalBootstrap(); + if (existingBootstrap.isEmpty()) { + Config config = Config.builder() + .addSource(ConfigSources.classpath("application.yaml")) + .disableSystemPropertiesSource() + .disableEnvironmentVariablesSource() + .build(); + Bootstrap bootstrap = DefaultBootstrap.builder() + .config(config) + .build(); + PicoServices.globalBootstrap(bootstrap); + } + + PicoServices picoServices = PicoServices.picoServices().get(); + // this line is needed! + picoServices.services(); + } +} diff --git a/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PlaintextEndpoint.java b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PlaintextEndpoint.java new file mode 100644 index 00000000000..f4d91258e62 --- /dev/null +++ b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/PlaintextEndpoint.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.examples.nima.pico; + +import java.nio.charset.StandardCharsets; + +import io.helidon.common.http.GET; +import io.helidon.common.http.Http; +import io.helidon.common.http.Path; +import io.helidon.nima.webserver.http.ServerResponse; + +import jakarta.inject.Singleton; + +@Singleton +@Path("/plaintext") +class PlaintextEndpoint { + static final Http.HeaderValue CONTENT_TYPE = Http.Header.createCached(Http.Header.CONTENT_TYPE, + "text/plain; charset=UTF-8"); + static final Http.HeaderValue CONTENT_LENGTH = Http.Header.createCached(Http.Header.CONTENT_LENGTH, "13"); + static final Http.HeaderValue SERVER = Http.Header.createCached(Http.Header.SERVER, "Nima"); + + private static final byte[] RESPONSE_BYTES = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + @GET + void plaintext(ServerResponse res) { + res.header(CONTENT_LENGTH); + res.header(CONTENT_TYPE); + res.header(SERVER); + res.send(RESPONSE_BYTES); + } +} diff --git a/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/package-info.java b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/package-info.java new file mode 100644 index 00000000000..876594cb2ea --- /dev/null +++ b/examples/nima/pico/src/main/java/io/helidon/examples/nima/pico/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Example showing Níma on Pico. + */ +package io.helidon.examples.nima.pico; diff --git a/examples/nima/pico/src/main/resources/application.yaml b/examples/nima/pico/src/main/resources/application.yaml new file mode 100644 index 00000000000..6089c766f78 --- /dev/null +++ b/examples/nima/pico/src/main/resources/application.yaml @@ -0,0 +1,24 @@ +# +# Copyright (c) 2023 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. +# + +server: + host: "localhost" + port: 8080 +# sockets: - not yet +# - name: "https" +# port: 8081 +# - name: "admin" +# port: 8082 diff --git a/examples/nima/pico/src/main/resources/logging.properties b/examples/nima/pico/src/main/resources/logging.properties new file mode 100644 index 00000000000..9d15362275a --- /dev/null +++ b/examples/nima/pico/src/main/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.nima.level=INFO diff --git a/examples/nima/pom.xml b/examples/nima/pom.xml index 5fbf6209ff5..40053039bb2 100644 --- a/examples/nima/pom.xml +++ b/examples/nima/pom.xml @@ -40,6 +40,7 @@ tracing observe static-content + pico diff --git a/examples/pico/README.md b/examples/pico/README.md new file mode 100644 index 00000000000..70204793fb6 --- /dev/null +++ b/examples/pico/README.md @@ -0,0 +1,7 @@ +# examples-pico + +Helidon Pico was inspired by several DI frameworks (e.g., Hk2, Guice and Dagger, etc.). These examples will serve as a way to learn about the capabilities of Pico by comparing Pico to each of these frameworks. It is recommended to review each example to understand the capabilities of Pico and to see it in action. + +* [book](./book/README.md) - compares Pico to Hk2. +* [car](./car/README.md) - compares Pico to Dagger2. +* [logger](./logger/README.md) - compares Pico to Guice. diff --git a/examples/pico/book/README.md b/examples/pico/book/README.md new file mode 100644 index 00000000000..395441309df --- /dev/null +++ b/examples/pico/book/README.md @@ -0,0 +1,152 @@ +# pico-examples-car + +This example compares Pico to Jakarta's Hk2. The example is constructed around a virtual library with bookcases, books, and a color wheel on display. This example is different from the other examples in that it combines Hk2 and Pico into the same application. A best practice is to only have one injection framework being used, so this combination is generally not a very realistic example for how a developer would construct their application. Nevertheless, this will be done here in order to convey how the frameworks are similar in some ways while different in other ways. + +Take a momemt to review the code. Note that the generated source code is found under +"./target/generated-sources" for Pico. Generate META-INF is found under "./target/classes". Similarities and differences are listed below. + +# Building and Running +``` +> mvn clean install +> ./run.sh +``` + +# Notable + +1. Both Pico and Hk2 supports jakarta.inject and use annotation processors (see [pom.xml](pom.xml)) to process the injection model in compliance to jsr-330 specifications. But the way they work is very different. Hk2 uses compile-time to determine the set of services in the model, and then at runtime will reflectively analyze those classes for resolving injection point dependencies. Pico, on the other hand, relies 100% on compile-time processing that generates source. In Pico the entire application can be analyzed and verified for completeness (i.e., no missing non-optional dependencies) at compile-time instead of Hk2's approach to perform this at runtime lazily during a service activation - and that validation only happens if the service being looked up is missing its dependencies - which might not happen unless your testing and runtime goes down the path of activating a service that is missing its dependencies. In Pico, when the application is created it is completely bound and verified safeguarding against this possibility. This technique of code generation and binding also leads to better performance of Pico as compared to Hk2, and additionally helps ensure deterministic behavior. + +2. The API programming model between Hk2 and Pico is very similar (see the application). + +* Declaring contracts. Contracts (usually interface types) are a means to lookup or inject into other classes. In the example, there is a contract called BookHolder. Implementation classes of this include: BookCase, EmptyRedBookCase, GreenBookCase, and Library. + +```java +import io.helidon.pico.api.Contract; + +@Contract +@org.jvnet.hk2.annotations.Contract +public interface BookHolder { + Collection getBooks(); +} +``` + +In Hk2's annotation processing the use of Contract or ExternalContract is required to be present. In Pico this is optional when using -Aio.helidon.pico.autoAddNonContractInterfaces=true (see pom). + +* Declaring services. Services (usually concrete class types implementing zero or more contracts) can have zero or more injection points using the standard @Inject annotation. In the below example we see how Library is annotated as a Service for Hk2, while not being necessary for Pico. Pico will naturally resolve any standard Singleton scoped (or ApplicationScoped w/ another -A flag) type, or any service type that contains injection points even without having a Scope annotation. + +``` +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@ToString +public class Library implements BookHolder { + ... +} +``` + +* Injection points. If we look more closely at the Library class we will see it uses constructor injection. Constructor, setter, and field injection are all supported in both Hk2 and Pico. Note, however, that Pico can only handle public, protected, or package-privates (i.e., no pure private) types and the only for types that are also non-static. This is due to the way Activators are code generated to work with your main service classes. + +``` +@io.helidon.pico.Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +public class Library implements BookHolder { + private List> books; + private List bookHolders; + private ColorWheel colorWheel; + + @Inject + public Library(List> books, List bookHolders, ColorWheel colorWheel) { + this.books = books; + this.bookHolders = bookHolders; + this.colorWheel = colorWheel; + } + + ... +} +``` + +* Pico can handle injection of T, Provider, Optional, and List> while Hk2 can handle T, Provider, Optional, and IterableProvider. Notice how Hk2 does not handle List and yet the annotation processor accepted this form of injection. It is not until runtime that the problem is observed. This type of issue would be found at compile time in Pico. + + +``` + public static void main(String[] args) { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator(); + + try { + ServiceHandle librarySh = locator.getServiceHandle(Library.class); + System.out.println("found a library handle: " + librarySh.getActiveDescriptor()); + Library library = librarySh.getService(); + System.out.println("found a library: " + library); + } catch (Exception e) { + // list injection is not supported in Hk2 - must switch to use IterableProvider instead. + // see https://javaee.github.io/hk2/apidocs/org/glassfish/hk2/api/IterableProvider.html + // and see https://javaee.github.io/hk2/introduction.html + System.out.println("error: " + e.getMessage()); + } + +``` + +* Output from MainHk2 at runtime: + +``` +RUN 1: (HK2) +found a library handle: SystemDescriptor( + implementation=io.helidon.examples.examples.book.Library + contracts={io.helidon.examples.pico.book.Library,io.helidon.examples.pico.book.BookHolder} + scope=jakarta.inject.Singleton + qualifiers={} + descriptorType=CLASS + descriptorVisibility=NORMAL + metadata= + rank=0 + loader=null + proxiable=null + proxyForSameScope=null + analysisName=null + id=12 + locatorId=0 + identityHashCode=99347477 + reified=true) +... +error: A MultiException has 4 exceptions. They are: +1. org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available in __HK2_Generated_0 for injection at SystemInjecteeImpl(requiredType=List>,parent=Library,qualifiers={},position=0,optional=false,self=false,unqualified=null,940060004) +2. org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available in __HK2_Generated_0 for injection at SystemInjecteeImpl(requiredType=List,parent=Library,qualifiers={},position=1,optional=false,self=false,unqualified=null,1121172875) +3. java.lang.IllegalArgumentException: While attempting to resolve the dependencies of io.helidon.examples.pico.book.Library errors were found +4. java.lang.IllegalStateException: Unable to perform operation: resolve on io.helidon.examples.pico.book.Library +``` + +* Output from MainPico at runtime: + +``` +RUN 1: (PICO)) +found a library provider: Library$$picoActivator:io.helidon.examples.pico.book.Library:INIT:[io.helidon.examples.pico.book.BookHolder] +... +library is open: Library(books=[MobyDickInBlue$$picoActivator@50134894:io.helidon.examples.pico.book.MobyDickInBlue@0 : INIT : [io.helidon.examples.pico.book.Book], ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]], bookHolders=[BookCase(allBooks=[MobyDickInBlue$$picoActivator@50134894:io.helidon.pico.examples.book.MobyDickInBlue@0 : INIT : [io.helidon.pico.examples.book.Book], ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]]), EmptyRedBookCase(books=[Optional.empty]), GreenBookCase(greenBooks=[ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]])], colorWheel=ColorWheel(preferredOptionalRedThing=Optional[EmptyRedBookCase(books=[Optional.empty])], preferredOptionalGreenThing=Optional[GreenColor()], preferredOptionalBlueThing=Optional[BlueColor()], preferredProviderRedThing=EmptyRedBookCase$$picoActivator@619a5dff:io.helidon.pico.examples.book.EmptyRedBookCase@16b4a017 : ACTIVE : [io.helidon.pico.examples.book.BookHolder, io.helidon.pico.examples.book.Color, io.helidon.pico.examples.book.RedColor], preferredProviderGreenThing=GreenColor$$picoActivator@7506e922:io.helidon.pico.examples.book.GreenColor@8807e25 : ACTIVE : [io.helidon.pico.examples.book.Color], preferredProviderBlueThing=BlueColor$$picoActivator@25f38edc:io.helidon.pico.examples.book.BlueColor@2a3046da : ACTIVE : [io.helidon.pico.examples.book.Color])) +library: Library(books=[MobyDickInBlue$$picoActivator@50134894:io.helidon.pico.examples.book.MobyDickInBlue@0 : INIT : [io.helidon.pico.examples.book.Book], ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]], bookHolders=[BookCase(allBooks=[MobyDickInBlue$$picoActivator@50134894:io.helidon.pico.examples.book.MobyDickInBlue@0 : INIT : [io.helidon.pico.examples.book.Book], ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]]), EmptyRedBookCase(books=[Optional.empty]), GreenBookCase(greenBooks=[ParadiseLostInGreen$$picoActivator@28ba21f3:io.helidon.pico.examples.book.ParadiseLostInGreen@0 : INIT : [io.helidon.pico.examples.book.Book], UlyssesInGreen$$picoActivator@2530c12:io.helidon.pico.examples.book.UlyssesInGreen@0 : INIT : [io.helidon.pico.examples.book.Book]])], colorWheel=ColorWheel(preferredOptionalRedThing=Optional[EmptyRedBookCase(books=[Optional.empty])], preferredOptionalGreenThing=Optional[GreenColor()], preferredOptionalBlueThing=Optional[BlueColor()], preferredProviderRedThing=EmptyRedBookCase$$picoActivator@619a5dff:io.helidon.pico.examples.book.EmptyRedBookCase@16b4a017 : ACTIVE : [io.helidon.pico.examples.book.BookHolder, io.helidon.pico.examples.book.Color, io.helidon.pico.examples.book.RedColor], preferredProviderGreenThing=GreenColor$$picoActivator@7506e922:io.helidon.pico.examples.book.GreenColor@8807e25 : ACTIVE : [io.helidon.pico.examples.book.Color], preferredProviderBlueThing=BlueColor$$picoActivator@25f38edc:io.helidon.pico.examples.book.BlueColor@2a3046da : ACTIVE : [io.helidon.pico.examples.book.Color])) + +``` + +* The output for Pico (see "library is open:") reveals how lifecycle is supported via the use of the standard PostConstruct and PreDestroy annotations. Pico will also display services that have not yet been lazily initialized (see "INIT") vs those services that have been (see "ACTIVE"). Hk2 and Pico behave in similar ways - it will only activate a service when the provider (or ServiceHandle in Hk2) is resolved. Until that time no service will be activated. It is a best practice, therefore, to use Provider<> type injection as much as possible as it will avoid chains of activation at runtime (i.e., every non-provided type injection point will be recursively activated). + +3. Pico generates a suggested module-info.java based upon analysis of your injection/dependency model (see ./target/pico/classes/module-info.java.pico). Hk2 does not have this feature. + +``` +// @Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +module io.helidon.examples.pico.book { + exports io.helidon.examples.pico.book; + // pico module - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + provides io.helidon.pico.Module with io.helidon.examples.pico.book.Pico$$Module; + // pico services - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires transitive io.helidon.pico; +} +``` + +4. As previously mentioned, both Hk2 and Pico supports PostConstruct and PreDestroy annotations. Additionally, both frameworks offers a notion of RunLevel where RunLevel(value==0) typically represents a "startup" like service. Check the javadoc for details. + +5. Pico can optionally generate the activators (i.e., the DI supporting classes) on an external jar module. See the [logger](../logger) example for details. Hk2 has a similar mechanism called inhabitants generator - see references below. + +# References +* https://javaee.github.io/hk2/introduction.html +* https://javaee.github.io/hk2/getting-started.html diff --git a/examples/pico/book/pom.xml b/examples/pico/book/pom.xml new file mode 100644 index 00000000000..9f39df6a3f5 --- /dev/null +++ b/examples/pico/book/pom.xml @@ -0,0 +1,138 @@ + + + + + + io.helidon.examples.pico + helidon-examples-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.examples.pico.book + helidon-examples-pico-book + Helidon Pico Examples - Book (Hk2 and Pico) + + + + 3.0.3 + 5.1.0 + + io.helidon.examples.pico.book.MainHk2 + io.helidon.examples.pico.book.MainPico + + + + + jakarta.annotation + jakarta.annotation-api + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + + org.glassfish.hk2 + hk2-locator + ${version.lib.hk2} + + + + io.helidon.pico + helidon-pico-services + + + io.helidon.builder + helidon-builder + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + -Apico.autoAddNonContractInterfaces=false + + + + + org.glassfish.hk2 + hk2-metadata-generator + ${version.lib.hk2} + + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + + ALL + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + diff --git a/examples/pico/book/run.sh b/examples/pico/book/run.sh new file mode 100755 index 00000000000..8d2f870392b --- /dev/null +++ b/examples/pico/book/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e +# +# Copyright (c) 2023 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. +# + +# mvn clean install +clear +echo "RUN 1: (HK2)" +java -cp target/helidon-examples-pico-book-4.0.0-SNAPSHOT-jar-with-dependencies.jar io.helidon.examples.pico.book.MainHk2 +echo "RUN 2: (HK2)" +java -cp target/helidon-examples-pico-book-4.0.0-SNAPSHOT-jar-with-dependencies.jar io.helidon.examples.pico.book.MainHk2 +echo "========================" +echo "RUN 1: (PICO)" +java -cp target/helidon-examples-pico-book-4.0.0-SNAPSHOT-jar-with-dependencies.jar io.helidon.examples.pico.book.MainPico +echo "RUN 2: (PICO)" +java -cp target/helidon-examples-pico-book-4.0.0-SNAPSHOT-jar-with-dependencies.jar io.helidon.examples.pico.book.MainPico diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Blue.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Blue.java new file mode 100644 index 00000000000..438012ac4dd --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Blue.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({TYPE, METHOD, FIELD, PARAMETER}) +public @interface Blue { + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BlueColor.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BlueColor.java new file mode 100644 index 00000000000..ca28f727bcb --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BlueColor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Blue +public class BlueColor implements Color { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Book.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Book.java new file mode 100644 index 00000000000..aa1ba73a188 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Book.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Contract +public interface Book { + + String getName(); + + Class getColor(); + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookCase.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookCase.java new file mode 100644 index 00000000000..f89301c4de6 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookCase.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.Collection; +import java.util.List; + +import io.helidon.pico.Contract; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +/** + * Demonstrates a field type injection point. + */ +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +public class BookCase implements BookHolder { + + @Inject + List> allBooks; + + @Override + public Collection getBooks() { + return allBooks; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookHolder.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookHolder.java new file mode 100644 index 00000000000..48a95485d43 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/BookHolder.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.Collection; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Contract +public interface BookHolder { + + Collection getBooks(); + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Color.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Color.java new file mode 100644 index 00000000000..1240b34bfb8 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Color.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Contract +public interface Color { + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ColorWheel.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ColorWheel.java new file mode 100644 index 00000000000..057dd8fe81f --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ColorWheel.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.List; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +import static io.helidon.pico.services.ServiceUtils.toDescription; +import static io.helidon.pico.services.ServiceUtils.toDescriptions; + +/** + * Demonstrates setter type injection points w/ qualifiers & optionals. + */ +@org.jvnet.hk2.annotations.Service +public class ColorWheel { + Optional preferredOptionalRedThing; + Optional preferredOptionalGreenThing; + Optional preferredOptionalBlueThing; + + Provider preferredProviderRedThing; + Provider preferredProviderGreenThing; + Provider preferredProviderBlueThing; + + @Inject + void setPreferredOptionalRedThing(@org.jvnet.hk2.annotations.Optional @Red Optional thing) { + System.out.println("setting optional color wheel red to " + thing); + preferredOptionalRedThing = thing; + } + + @Inject + void setPreferredOptionalGreenThing(@org.jvnet.hk2.annotations.Optional @Green Optional thing) { + System.out.println("setting optional color wheel green to " + thing); + preferredOptionalGreenThing = thing; + } + + @Inject + void setPreferredOptionalBlueThing(@org.jvnet.hk2.annotations.Optional @Blue Optional thing) { + System.out.println("setting optional color wheel blue to " + thing); + preferredOptionalBlueThing = thing; + } + + @Inject + void setPreferredProviderRedThing(@org.jvnet.hk2.annotations.Optional @Red Provider thing) { + System.out.println("setting provider color wheel red to " + toDescription(thing)); + preferredProviderRedThing = thing; + } + + @Inject + void setPreferredProviderGreenThing(@org.jvnet.hk2.annotations.Optional @Green Provider thing) { + System.out.println("setting provider wheel green to " + toDescription(thing)); + preferredProviderGreenThing = thing; + } + + @Inject + void setPreferredBlueThing(@org.jvnet.hk2.annotations.Optional @Blue Provider thing) { + System.out.println("setting provider wheel blue to " + toDescription(thing)); + preferredProviderBlueThing = thing; + } + + @Inject + void setListProviderRedThings(@org.jvnet.hk2.annotations.Optional @Red List> things) { + System.out.println("setting providerList color wheel red to " + (things == null ? "null" : toDescriptions(things))); + } + + @Inject + void setListProviderGreenThings(@org.jvnet.hk2.annotations.Optional @Green List> things) { + System.out.println("setting providerList wheel green to " + (things == null ? "null" : toDescriptions(things))); + } + + @Inject + void setListProviderBlueThings(@org.jvnet.hk2.annotations.Optional @Blue List> things) { + System.out.println("setting providerList wheel blue to " + (things == null ? "null" : toDescriptions(things))); + } + + // not supported by Pico... +// @Inject +// void setIterableProviderRedThings(@org.jvnet.hk2.annotations.Optional @Red IterableProvider things) { +// System.out.println("setting iterableList color wheel red to " + things); +// } +// +// @Inject +// void setIterableProviderGreenThings(@org.jvnet.hk2.annotations.Optional @Green IterableProvider things) { +// System.out.println("setting iterableList wheel green to " + things); +// } +// +// @Inject +// void setIterableProviderBlueThings(@org.jvnet.hk2.annotations.Optional @Blue IterableProvider things) { +// System.out.println("setting iterableList wheel blue to " + things); +// } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/EmptyRedBookCase.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/EmptyRedBookCase.java new file mode 100644 index 00000000000..ae2dad45b9b --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/EmptyRedBookCase.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import io.helidon.pico.Contract; + +import jakarta.inject.Inject; + +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@Red +public class EmptyRedBookCase extends RedColor implements BookHolder { + + private Collection books; + + @Override + public Collection getBooks() { + return books; + } + + @Inject + public EmptyRedBookCase(@Red Optional preferrredRedBook) { + this.books = Collections.singleton(preferrredRedBook); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(books=" + getBooks() + ")"; + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Green.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Green.java new file mode 100644 index 00000000000..51a1596a5b9 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Green.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({TYPE, METHOD, FIELD, PARAMETER}) +public @interface Green { + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenBookCase.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenBookCase.java new file mode 100644 index 00000000000..9247f2efa89 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenBookCase.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.Collection; +import java.util.List; + +import io.helidon.pico.Contract; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +/** + * Demonstrates ctor injection with qualifiers and lists of providers. + */ +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +public class GreenBookCase implements BookHolder { + + private final List> greenBooks; + + @Override + public Collection getBooks() { + return greenBooks; + } + + @Inject + GreenBookCase(@Green List> greenBooks) { + this.greenBooks = greenBooks; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(books=" + getBooks() + ")"; + } +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenColor.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenColor.java new file mode 100644 index 00000000000..83a3dabda56 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/GreenColor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Green +public class GreenColor implements Color { + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Library.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Library.java new file mode 100644 index 00000000000..48ff6b58c22 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Library.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.util.Collection; +import java.util.List; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +/** + * Demonstrates constructor injection of various types. + */ +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +public class Library implements BookHolder { + + private List> books; + private List bookHolders; + private ColorWheel colorWheel; + + @Inject + public Library( + List> books, + List bookHolders, + ColorWheel colorWheel) { + this.books = books; + this.bookHolders = bookHolders; + this.colorWheel = colorWheel; + } + + @Override + public Collection getBooks() { + return books; + } + + public Collection getBookHolders() { + return bookHolders; + } + + public ColorWheel getColorWheel() { + return colorWheel; + } + + @PostConstruct + public void postConstruct() { + System.out.println("library is open: " + this); + } + + @PreDestroy + public void preDestroy() { + System.out.println("library is closed: " + this); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(books=" + getBooks() + ")"; + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainHk2.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainHk2.java new file mode 100644 index 00000000000..d8a8ebb9c83 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainHk2.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import org.glassfish.hk2.api.ServiceHandle; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.utilities.ServiceLocatorUtilities; + +public class MainHk2 { + + public static void main(String[] args) { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + ServiceLocator locator = ServiceLocatorUtilities.createAndPopulateServiceLocator(); + + try { + ServiceHandle librarySh = locator.getServiceHandle(Library.class); + System.out.println("found a library handle: " + librarySh.getActiveDescriptor()); + Library library = librarySh.getService(); + System.out.println("found a library: " + library); + } catch (Exception e) { + // list injection is not supported in Hk2 - must switch to use IterableProvider instead. + // see https://javaee.github.io/hk2/apidocs/org/glassfish/hk2/api/IterableProvider.html + // and see https://javaee.github.io/hk2/introduction.html + System.out.println("error: " + e.getMessage()); + } + + ServiceHandle colorWheelSh = locator.getServiceHandle(ColorWheel.class); + System.out.println("found a color wheel handle: " + colorWheelSh.getActiveDescriptor()); + ColorWheel library = colorWheelSh.getService(); + System.out.println("color wheel: " + library); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Hk2 Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Hk2 Main elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainPico.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainPico.java new file mode 100644 index 00000000000..fa9cd841b2a --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MainPico.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; + +public class MainPico { + + public static void main(String[] args) { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + Services services = PicoServices.realizedServices(); + + ServiceProvider librarySp = services.lookupFirst(Library.class); + System.out.println("found a library provider: " + librarySp.description()); + Library library = librarySp.get(); + System.out.println("library: " + library); + + ServiceProvider colorWheelSp = services.lookupFirst(ColorWheel.class); + System.out.println("found a color wheel provider: " + colorWheelSp.description()); + ColorWheel colorWheel = colorWheelSp.get(); + System.out.println("color wheel: " + colorWheel); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Pico Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Pico Main elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MobyDickInBlue.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MobyDickInBlue.java new file mode 100644 index 00000000000..0633d24c34c --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/MobyDickInBlue.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Blue +public class MobyDickInBlue implements Book { + + @Override + public String getName() { + return "Moby Dick"; + } + + @Override + public Class getColor() { + return BlueColor.class; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ParadiseLostInGreen.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ParadiseLostInGreen.java new file mode 100644 index 00000000000..806c8946f4f --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/ParadiseLostInGreen.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Green +public class ParadiseLostInGreen implements Book { + + @Override + public String getName() { + return "Paradise Lost"; + } + + @Override + public Class getColor() { + return null; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Red.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Red.java new file mode 100644 index 00000000000..24b6f5ba99c --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/Red.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({TYPE, METHOD, FIELD, PARAMETER}) +public @interface Red { + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/RedColor.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/RedColor.java new file mode 100644 index 00000000000..d2749e53616 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/RedColor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import io.helidon.pico.Contract; + +@Contract +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Red +public class RedColor implements Color { + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/UlyssesInGreen.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/UlyssesInGreen.java new file mode 100644 index 00000000000..4d484779282 --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/UlyssesInGreen.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +@org.jvnet.hk2.annotations.Service +@jakarta.inject.Singleton +@jakarta.inject.Named +@Green +public class UlyssesInGreen implements Book { + + @Override + public String getName() { + return "Ulysses"; + } + + @Override + public Class getColor() { + return GreenColor.class; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/book/src/main/java/io/helidon/examples/pico/book/package-info.java b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/package-info.java new file mode 100644 index 00000000000..4c2a7178c5a --- /dev/null +++ b/examples/pico/book/src/main/java/io/helidon/examples/pico/book/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Book Example. + */ +package io.helidon.examples.pico.book; diff --git a/examples/pico/book/src/main/resources/logging.properties b/examples/pico/book/src/main/resources/logging.properties new file mode 100644 index 00000000000..b3e81c5cb71 --- /dev/null +++ b/examples/pico/book/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2023 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. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.nima.level=INFO diff --git a/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainHk2Test.java b/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainHk2Test.java new file mode 100644 index 00000000000..ab356fca811 --- /dev/null +++ b/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainHk2Test.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import org.junit.jupiter.api.Test; + +public class MainHk2Test { + + @Test + public void testMain() { + final long start = System.currentTimeMillis(); + + MainHk2.main(null); + + final long finish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Hk2 Junit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainPicoTest.java b/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainPicoTest.java new file mode 100644 index 00000000000..668472596ec --- /dev/null +++ b/examples/pico/book/src/test/java/io/helidon/examples/pico/book/MainPicoTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.examples.pico.book; + +import org.junit.jupiter.api.Test; + +public class MainPicoTest { + + @Test + public void testMain() { + final long start = System.currentTimeMillis(); + + MainPico.main(null); + + final long finish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Pico Junit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/car/README.md b/examples/pico/car/README.md new file mode 100644 index 00000000000..317efd2d857 --- /dev/null +++ b/examples/pico/car/README.md @@ -0,0 +1,220 @@ +# pico-examples-car + +This example compares Pico to Google's Dagger 2. It was taken from an example found on the internet (see references below). The example is fairly trivial, but it is sufficient to compare the similarities between the two. + +[dagger2](dagger2) contains the Dagger2 application module. +[pico](pico) contains the Pico application module. + +Unlike the [logger](../logger) example, this example replicates the entire set of classes for each application module. This was done for a few reasons. The primary reason is that while Pico offers the ability to generate the DI supporting module on an external jar (as the logger example demonstrates), Dagger does not provide such an option - the DI module for dagger +only provides for an annotation processor mechanism, thereby requiring the developer to inject the Dagger annotation processor +into the build of the project to generate the DI module at compilation time. The other reason why this was done was to demonstrate +a few variations in the way developers can use Pico for more complicated use cases. + +Review the code to see the similarities and differences. Note that the generated source code is found under +"./target/generated-sources" for each sub application module. Summaries of similarities and differences are listed below. + +# Building and Running +``` +> mvn clean install +> ./run.sh +``` + +# Notable + +1. Pico supports jakarta.inject as well as javax.inject packaging. Dagger 2 (at the time of this writing - see https://github.com/google/dagger/issues/2058) only supports javax.inject. This example uses javax.inject for the Dagger app, and uses jakarta.inject for the Pico app. Functionally, however, both are the same. + +2. The API programming model between Pico and Dagger is fairly different. Pico strives to closely follow the jsr-330 specification and relies heavily on annotation processing to generate *all* of the supporting DI classes, and those classes are hidden as an implementation detail not directly exposed to the (typical) developer. This can be seen by observing Dagger's [VehiclesComponent.java](./dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesComponent.java) and [VehiclesModule.java](./dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesModule.java) classes - notice the imports as well as the code the developer is expected to write. + +On the Pico side, you will notice that the only #import of Pico is found on the [Vehicle.java](./pico/src/main/java/io/helidon/examples/pico/car/pico/Vehicle.java) class. The @Contract annotation is used to demarcate the Vehcile iterface that Car implements/advertises. All the other imports are using standard javax/jakarta annotations. Pico actually offers an option to advertise all interfaces as contracts (see the pom.xml snippet below). Turning on this switch will allow the @Contract to be removed as well, thereby using 100% standard javax/jakarta types. + +``` + + + + +``` + +There are a few other "forced" examples under Pico which demonstrate additional options available. For example, the [BrandProvider.java](./pico/src/main/java/io/helidon/examples/pico/car/pico/BrandProvider.java) class is the standard means to produce an instance of a Brand. The implementation can choose the cardinality of the instances created. At times, it might be convenient to "know" the injection point consumer requesting the Brand instance, in order to change the cardinality or somehow make it dependent in scope. This option is demonstrated in the [BrandProvider.java](./pico/src/main/java/io/helidon/examples/pico/car/pico/EngineProvider.java) class. + +3. Both Dagger and Pico are using compile-time to generate the DI supporting classes as mentioned above. The code generated between the two, however is a little different. Let's have a closer look: + +The Dagger generated Car_Factory: +```java +@ScopeMetadata +@QualifierMetadata +@DaggerGenerated +@Generated( + value = "dagger.internal.codegen.ComponentProcessor", + comments = "https://dagger.dev" +) +@SuppressWarnings({ + "unchecked", + "rawtypes" +}) +public final class Car_Factory implements Factory { + private final Provider engineProvider; + + private final Provider brandProvider; + + public Car_Factory(Provider engineProvider, Provider brandProvider) { + this.engineProvider = engineProvider; + this.brandProvider = brandProvider; + } + + @Override + public Car get() { + return newInstance(engineProvider.get(), brandProvider.get()); + } + + public static Car_Factory create(Provider engineProvider, Provider brandProvider) { + return new Car_Factory(engineProvider, brandProvider); + } + + public static Car newInstance(Engine engine, Brand brand) { + return new Car(engine, brand); + } +} +``` + +The Pico generated Car$$picoActivator: +```java +/** + * Activator for {@link io.helidon.examples.pico.car.pico.Car}. + */ +// @Singleton +@SuppressWarnings("unchecked") +@Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +public class Car$$picoActivator + extends io.helidon.pico.services.AbstractServiceProvider { + private static final DefaultServiceInfo serviceInfo = + DefaultServiceInfo.builder() + .serviceTypeName(io.helidon.examples.pico.car.pico.Car.class.getName()) + .addContractsImplemented(io.helidon.examples.pico.car.pico.Vehicle.class.getName()) + .activatorTypeName(Car$$picoActivator.class.getName()) + .addScopeTypeName(jakarta.inject.Singleton.class.getName()) + .build(); + + /** + * The global singleton instance for this service provider activator. + */ + public static final Car$$picoActivator INSTANCE = new Car$$picoActivator(); + + /** + * Default activator constructor. + */ + protected Car$$picoActivator() { + serviceInfo(serviceInfo); + } + + /** + * The service type of the managed service. + * + * @return the service type of the managed service + */ + public Class serviceType() { + return io.helidon.examples.pico.car.pico.Car.class; + } + + @Override + public DependenciesInfo dependencies() { + DependenciesInfo deps = Dependencies.builder(io.helidon.examples.pico.car.pico.Car.class.getName()) + .add(CONSTRUCTOR, io.helidon.examples.pico.car.pico.Engine.class, ElementKind.CONSTRUCTOR, 2, Access.PUBLIC).elemOffset(1) + .add(CONSTRUCTOR, io.helidon.examples.pico.car.pico.Brand.class, ElementKind.CONSTRUCTOR, 2, Access.PUBLIC).elemOffset(2) + .build(); + return Dependencies.combine(super.dependencies(), deps); + } + + @Override + protected Car createServiceProvider(Map deps) { + io.helidon.examples.pico.car.pico.Engine c1 = (io.helidon.examples.pico.car.pico.Engine) get(deps, "io.helidon.examples.pico.car.pico.|2(1)"); + io.helidon.examples.pico.car.pico.Brand c2 = (io.helidon.examples.pico.car.pico.Brand) get(deps, "io.helidon.examples.pico.car.pico.|2(2)"); + return new io.helidon.examples.pico.car.pico.Car(c1, c2); + } + +} +``` + +Here is the main difference: + +* Pico attempts to model each service in terms of the contracts/interfaces each service offers, as well as the dependencies (other contracts/interfaces) that it requires. A more elaborate dependency model would additionally include the qualifiers (such as @Named), whether the services are optional, list-based, etc. This model extends down to mention each element (for methods or constructors for example). All of this is generated at compile-time. In this way the Pico Services registry has knowledge of every available service and what it offers and what it requires. These services are left to be lazily activated on-demand. + +4. Pico provides the ability (as demonstrated in the [pom.xml](./pico/pom.xml)) to take the injection model, analyze and validate it, and ultimately bind to the final injection model at assembly time. Using this option provides several key benefits including deterministic behavior, speed & performance enhancements, and helps to ensure the completeness & validity of the entire application's dependency graph at compile time. When this option is applied the Pico$$Application is generated. Here is what it looks like for this example: + +```java +@Generated(value = "io.helidon.pico.maven.plugin.ApplicationCreatorMojo", comments = "version=1") +@Singleton @Named(Pico$$Application.NAME) +public class Pico$$Application implements Application { + static final String NAME = "io.helidon.examples.pico.car.pico"; + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + ":" + getClass().getName(); + } + + @Override + public void configure(ServiceInjectionPlanBinder binder) { + /** + * In module name "io.helidon.examples.pico.car.pico". + * @see {@link io.helidon.examples.pico.car.pico.BrandProvider } + */ + binder.bindTo(io.helidon.examples.pico.car.pico.BrandProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "io.helidon.examples.pico.car.pico". + * @see {@link io.helidon.examples.pico.car.pico.Car } + */ + binder.bindTo(io.helidon.examples.pico.car.pico.Car$$picoActivator.INSTANCE) + .bind("io.helidon.examples.pico.car.pico.|2(1)", + io.helidon.examples.pico.car.pico.EngineProvider$$picoActivator.INSTANCE) + .bind("io.helidon.examples.pico.car.pico.|2(2)", + io.helidon.examples.pico.car.pico.BrandProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "io.helidon.examples.pico.car.pico". + * @see {@link io.helidon.examples.pico.car.pico.EngineProvider } + */ + binder.bindTo(io.helidon.examples.pico.car.pico.EngineProvider$$picoActivator.INSTANCE) + .commit(); + + } +} +``` + +At initialization time (and using the default configuration) Pico will use the service loader to attempt to find the Application instance and use that instead of resolving the dependency graph at runtime. Generating the application is optional but recommended for production scenarios. + +5. The Dagger application is considerably smaller in terms of disk and memory footprints. This makes sense considering that the primary driver for Dagger 2 is to be consumed by Android developers who naturally require a liter footprint. Pico, while still small and lite compared to many other options on the i-net, offers more features including: interceptor/interception capabilities, flexible service registry search & resolution semantics, service meta information as described above, lazy activation, circular dependency detection, extensibility, configuration, etc. The default configuration assumes a production use case where the services registry is assumed to be non-dynamic/static in nature. + +On the topic of extensibility - Pico is centered around extensibility of its tooling. The templates use replaceable [handlebars](https://github.com/jknack/handlebars.java), and the generators use service loader. Anyone can either override Pico's reference implementation, or else write an entirely new implementation based upon the API & SPI that Pico provides. + +6. Pico requires less coding as compared to Dagger. In this example the BrandProvider and EngineProvider were contrived in order to demonstrate a nuances of the approach. Generally speaking most of the time the @Singleton annotation (or lack thereof) is all that is needed, depending upon the injection scope required. + +7. Pico offers lifecycle support (see jakarta.annotation.@PostConstruct, jakarta.annotation.@PreDestroy, Pico's @RunLevel + annotations and PicoServices#shutdown()). + +8. Pico generates a suggested module-info.java based upon analysis of your injection/dependency model (see ./target/classes/module-info.java.pico). + +```./target/classes/module-info.java.pico +// @Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +module io.helidon.examples.pico.car.pico { + exports io.helidon.examples.pico.car.pico; + // pico module - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + provides io.helidon.pico.Module with io.helidon.examples.pico.car.pico.Pico$$Module; + // pico external contract usage - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + uses jakarta.inject.Provider; + uses io.helidon.pico.InjectionPointProvider; + // pico services - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires transitive io.helidon.pico; +} +``` + +9. Pico can optionally generate the activators (i.e., the DI supporting classes) on an external jar module. See the [logger](../logger) example for details. + +# References +* https://www.baeldung.com/dagger-2 diff --git a/examples/pico/car/dagger2/pom.xml b/examples/pico/car/dagger2/pom.xml new file mode 100644 index 00000000000..27785c5c81b --- /dev/null +++ b/examples/pico/car/dagger2/pom.xml @@ -0,0 +1,129 @@ + + + + + + io.helidon.examples.pico.car + helidon-examples-pico-car-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-examples-pico-car-dagger2 + Helidon Pico Examples - Car - Dagger2 + + + io.helidon.examples.pico.car.dagger2.Main + + + + + com.google.dagger + dagger + ${version.lib.dagger} + + + io.helidon.builder + helidon-builder + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + com.google.dagger + dagger-compiler + ${version.lib.dagger} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + ${mainClass} + + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + + + diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Brand.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Brand.java new file mode 100644 index 00000000000..a5958f0c86e --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Brand.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +import io.helidon.builder.Builder; + +@Builder(requireLibraryDependencies = false) +public interface Brand { + + String name(); + +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Car.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Car.java new file mode 100644 index 00000000000..c876f655200 --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Car.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +import javax.inject.Inject; + +public class Car implements Vehicle { + + private Engine engine; + private Brand brand; + + @Inject + public Car( + Engine engine, + Brand brand) { + this.engine = engine; + this.brand = brand; + } + + @Override + public String toString() { + return "Car(engine=" + engine() + ",brand=" + brand() + ")"; + } + + @Override + public Engine engine() { + return engine; + } + + public void engine(Engine engine) { + this.engine = engine; + } + + @Override + public Brand brand() { + return brand; + } + + public void brand(Brand brand) { + this.brand = brand; + } + +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Engine.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Engine.java new file mode 100644 index 00000000000..1fb3165b765 --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Engine.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +public class Engine { + + public void start() { + System.out.println("Engine started"); + } + + public void stop() { + System.out.println("Engine stopped"); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Main.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Main.java new file mode 100644 index 00000000000..5449dfb45a9 --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Main.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +public class Main { + + public static void main(String[] args) { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + if (args.length > 0) { + VehiclesModule.brandName = args[0]; + } + VehiclesComponent component = DaggerVehiclesComponent.create(); + System.out.println("found a car component: " + component); + Car car = component.buildCar(); + System.out.println("found a car: " + car); + car.engine().start(); + car.engine().stop(); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Dagger2 Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Dagger2 Main elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Vehicle.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Vehicle.java new file mode 100644 index 00000000000..3a03d858afc --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/Vehicle.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +public interface Vehicle { + Engine engine(); + Brand brand(); +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesComponent.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesComponent.java new file mode 100644 index 00000000000..7a1c86d7395 --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesComponent.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = VehiclesModule.class) +public interface VehiclesComponent { + Car buildCar(); +} diff --git a/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesModule.java b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesModule.java new file mode 100644 index 00000000000..32db99b5753 --- /dev/null +++ b/examples/pico/car/dagger2/src/main/java/io/helidon/examples/pico/car/dagger2/VehiclesModule.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class VehiclesModule { + + static String brandName; + + @Provides + public Engine provideEngine() { + return new Engine(); + } + + @Provides + @Singleton + public Brand provideBrand() { + return DefaultBrand.builder().name(brandName).build(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/car/dagger2/src/test/java/io/helidon/examples/pico/car/dagger2/Dagger2Test.java b/examples/pico/car/dagger2/src/test/java/io/helidon/examples/pico/car/dagger2/Dagger2Test.java new file mode 100644 index 00000000000..5c9101da70c --- /dev/null +++ b/examples/pico/car/dagger2/src/test/java/io/helidon/examples/pico/car/dagger2/Dagger2Test.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.dagger2; + +import org.junit.jupiter.api.Test; + +public class Dagger2Test { + + @Test + public void testMain() { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + Main.main(new String[] {"Dagger2"}); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Dagger2 JUnit memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Dagger2 JUnit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/car/pico/pom.xml b/examples/pico/car/pico/pom.xml new file mode 100644 index 00000000000..e3d1f779d00 --- /dev/null +++ b/examples/pico/car/pico/pom.xml @@ -0,0 +1,154 @@ + + + + + + io.helidon.examples.pico.car + helidon-examples-pico-car-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-examples-pico-car-pico + Helidon Pico Examples - Car - Pico + + + io.helidon.examples.pico.car.pico.Main + + + + + io.helidon.pico + helidon-pico-services + + + io.helidon.builder + helidon-builder + + + org.junit.jupiter + junit-jupiter-api + test + + + jakarta.inject + jakarta.inject-api + + + + jakarta.annotation + jakarta.annotation-api + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + ${mainClass} + + + + + + make-assembly + package + + single + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + + ALL + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + + + diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Brand.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Brand.java new file mode 100644 index 00000000000..53bba32116e --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Brand.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import io.helidon.builder.Builder; + +@Builder +public interface Brand { + + String name(); + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/BrandProvider.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/BrandProvider.java new file mode 100644 index 00000000000..0b70241823f --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/BrandProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +public class BrandProvider implements Provider { + static String brandName; + + @Override + public Brand get() { + return DefaultBrand.builder().name(brandName).build(); + } + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Car.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Car.java new file mode 100644 index 00000000000..72f6ade9841 --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Car.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +public class Car implements Vehicle { + + private Engine engine; + private Brand brand; + + @Inject + public Car(Engine engine, Brand brand) { + this.engine = engine; + this.brand = brand; + } + + @Override + public String toString() { + return "Car(engine=" + engine() + ",brand=" + brand() + ")"; + } + + @Override + public Engine engine() { + return engine; + } + + public void engine(Engine engine) { + this.engine = engine; + } + + @Override + public Brand brand() { + return brand; + } + + public void brand(Brand brand) { + this.brand = brand; + } + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Engine.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Engine.java new file mode 100644 index 00000000000..fd77d120508 --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Engine.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +public class Engine { + + public void start() { + System.out.println("Engine started"); + } + + public void stop() { + System.out.println("Engine stopped"); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/EngineProvider.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/EngineProvider.java new file mode 100644 index 00000000000..dfdca52d282 --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/EngineProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import java.util.Optional; + +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.InjectionPointProvider; + +import jakarta.inject.Singleton; + +@Singleton +public class EngineProvider implements InjectionPointProvider { + + @Override + public Optional first(ContextualServiceQuery query) { + return Optional.of(new Engine()); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Main.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Main.java new file mode 100644 index 00000000000..59de93251f3 --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Main.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; + +public class Main { + + public static void main(String[] args) { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + if (args.length > 0) { + BrandProvider.brandName = args[0]; + } + ServiceProvider carSp = PicoServices.realizedServices().lookupFirst(Car.class); + System.out.println("found a car provider: " + carSp.description()); + Car car = carSp.get(); + System.out.println("found a car: " + car); + car.engine().start(); + car.engine().stop(); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Pico Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Pico Main elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Vehicle.java b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Vehicle.java new file mode 100644 index 00000000000..793ba69d7ec --- /dev/null +++ b/examples/pico/car/pico/src/main/java/io/helidon/examples/pico/car/pico/Vehicle.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import io.helidon.pico.Contract; + +@Contract +public interface Vehicle { + Engine engine(); + Brand brand(); +} diff --git a/examples/pico/car/pico/src/test/java/io/helidon/examples/pico/car/pico/PicoTest.java b/examples/pico/car/pico/src/test/java/io/helidon/examples/pico/car/pico/PicoTest.java new file mode 100644 index 00000000000..7b8a7eb8e11 --- /dev/null +++ b/examples/pico/car/pico/src/test/java/io/helidon/examples/pico/car/pico/PicoTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.examples.pico.car.pico; + +import org.junit.jupiter.api.Test; + +public class PicoTest { + + @Test + public void testMain() { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + Main.main(new String[] {"Pico"}); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Pico JUnit memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Pico JUnit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/car/pom.xml b/examples/pico/car/pom.xml new file mode 100644 index 00000000000..335a2a3dcfb --- /dev/null +++ b/examples/pico/car/pom.xml @@ -0,0 +1,46 @@ + + + + + + io.helidon.examples.pico + helidon-examples-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.examples.pico.car + helidon-examples-pico-car-project + Helidon Pico Examples - Car + + pom + + + 2.43 + + + + dagger2 + pico + + + diff --git a/examples/pico/car/run.sh b/examples/pico/car/run.sh new file mode 100755 index 00000000000..88e71e39cf7 --- /dev/null +++ b/examples/pico/car/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e +# +# Copyright (c) 2023 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. +# + +# mvn clean install +clear +echo "RUN 1: (DAGGER2)" +java -jar dagger2/target/helidon-examples-pico-car-dagger2-4.0.0-SNAPSHOT-jar-with-dependencies.jar dagger2 +echo "RUN 2: (DAGGER2)" +java -jar dagger2/target/helidon-examples-pico-car-dagger2-4.0.0-SNAPSHOT-jar-with-dependencies.jar dagger2 +echo "========================" +echo "RUN 1: (PICO)" +java -jar pico/target/helidon-examples-pico-car-pico-4.0.0-SNAPSHOT-jar-with-dependencies.jar pico +echo "RUN 2: (PICO)" +java -jar pico/target/helidon-examples-pico-car-pico-4.0.0-SNAPSHOT-jar-with-dependencies.jar pico diff --git a/examples/pico/logger/README.md b/examples/pico/logger/README.md new file mode 100644 index 00000000000..ef02e0e3d8b --- /dev/null +++ b/examples/pico/logger/README.md @@ -0,0 +1,100 @@ +# pico-examples-logger + +## Overview +This example compares Pico to Guice. It was taken from an example found on the internet (see references below). The example is fairly trivial, but it is sufficient to compare the similarities between the two, as well as to demonstrate the performance differences between the two. + +[common](common) contains the core application logic. +[guice](guice) contains the delta for integrating to Guice. +[pico](pico) contains the delta for integrating with Pico. + +Review the code to see the similarities and differences. Note that the generated source code is found under +"./target/generated-sources". Summaries of similarities and differences are listed below. + +# Building and Running +``` +> mvn clean install +> ./run.sh +``` + +The [run.sh](./run.sh) script as shown above will spawn the Guice and Pico built applications twice (1st iteration for warmup). + +:DISCLAIMER: **Results may vary** The below measurements were captured at the time of this revision (see git log) + + +# Notable + +1. Pico supports jakarta.inject as well as javax.inject packaging. Guice (at the time of this writing) only supports javax.inject. This example therefore uses javax.inject in order to compare the two models even though it is recommended all switch to jakarta.inject if possible. + +2. Guice is based upon reflection and at runtime uses it to determine the injection points and dependency graph. Pico is based upon compile-time code generation to generate (in code) the dependency graph. Both, however, support lazy/dynamic activation of services. + +3. Pico provides the ability (as demonstrated in the [pom.xml](./pico/pom.xml)) to bind to the final injection model at assembly time. This option provides several benefits including deterministic behavior, speed & performance, and to helps ensure the completeness & validity of the entire application's dependency graph. Guice does not offer such an option. + +4. Guice is considerable larger in terms of its memory consumption footprint. + +```run.sh +... +Guice Main memory consumption = 13,194,048 bytes +... +Pico Main memory consumption = 7,930,352 bytes +... +``` + +5. Both applications are packaged with all of its transitive compile-time dependencies to showcase the differences in disk size. Pico is considerably smaller in terms of its disk consumption footprint: +``` +> find . | grep dependencies | grep jar | xargs ls -l +-rw-r--r-- 1 jtrent staff 3975690 Feb 21 20:43 ./guice/target/helidon-examples-pico-logger-guice-4.0.0-SNAPSHOT-jar-with-dependencies.jar +-rw-r--r-- 1 jtrent staff 403936 Feb 21 20:43 ./pico/target/helidon-examples-pico-logger-pico-4.0.0-SNAPSHOT-jar-with-dependencies.jar +``` + +6. Pico is considerably faster in terms of its end-to-end runtime. Mileage will vary. + +```run.sh +... +Guice Main elapsed time = 293 ms +... +Pico Main elapsed time = 184 ms +... +``` + +7. Pico requires less coding as compared to Guice. + +``` +jtrent@jtrent-mac logger % find guice -type f -not -path '*/.*' | grep -v target | wc + 4 4 230 +jtrent@jtrent-mac logger % find pico -type f -not -path '*/.*' | grep -v target | wc + 3 3 149 +``` + +8. Pico offers lifecycle support (see jakarta.annotation.@PostConstruct, jakarta.annotation.@PreDestroy, Pico's @RunLevel + annotations and PicoServices#shutdown()). + +9. Pico generates a suggested module-info.java based upon analysis of your injection/dependency model (see /target/classes/module-info.java.pico). + +```./target/classes/module-info.java.pico +// @Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +module helidon.examples.pico.logger.common { + exports io.helidon.examples.pico.logger.common; + // pico module - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + provides io.helidon.pico.Module with io.helidon.examples.pico.logger.common.Pico$$Module; + // pico external contract usage - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires helidon.examples.pico.logger.common; + uses io.helidon.examples.pico.logger.common.CommunicationMode; + uses io.helidon.examples.pico.logger.common.Communicator; + uses jakarta.inject.Provider; + uses javax.inject.Provider; + // pico services - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires transitive io.helidon.pico.services; +} +``` + +10. Pico can optionally generate the activators (i.e., the DI supporting classes) on an external jar module by using a maven plugin. Notice how [common](./common) is built, and then in the [pico/pom.xml](pico/pom.xml) the maven plugin uses application-create to create the supporting DI around it. That explains why there are no classes other than Main in the pico sub-module. Guice does not offer such an option, and instead requires the developer to write the modules declaring the DI module programmatically. + + +Additionally, and more philosophical in nature, Pico strives to closely adhere to standard JSR-330 constructs as compared to Guice. +To be productive only requires the use of these packages: +* [jakarta.inject](https://javadoc.io/doc/jakarta.inject/jakarta.inject-api/latest/index.html) +* Optionally, [jakarta.annotation](https://javadoc.io/doc/jakarta.annotation/jakarta.annotation-api/latest/jakarta.annotation/jakarta/annotation/package-summary.html) +* Optionally, a few [pico API](../../pico/src/main/java/io/helidon/pico) / annotations. + +# References +* https://www.baeldung.com/guice diff --git a/examples/pico/logger/common/pom.xml b/examples/pico/logger/common/pom.xml new file mode 100644 index 00000000000..7b1dc5d8a01 --- /dev/null +++ b/examples/pico/logger/common/pom.xml @@ -0,0 +1,52 @@ + + + + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-examples-pico-logger-common + Helidon Pico Examples - Logger - Common + + + + jakarta.inject + jakarta.inject-api + + + + javax.inject + javax.inject + ${javax.injection.version} + + + + javax.annotation + javax.annotation-api + ${javax.annotations.version} + provided + + + diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/AnotherCommunicationMode.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/AnotherCommunicationMode.java new file mode 100644 index 00000000000..24c87bd581f --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/AnotherCommunicationMode.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@javax.inject.Singleton +public class AnotherCommunicationMode implements CommunicationMode { + + @javax.inject.Inject + Logger logger; + + @Override + public int sendMessage(String message) { + logger.log(Level.INFO, "Sending message '" + message + "' over another (default) communication mode"); + return 1; + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communication.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communication.java new file mode 100644 index 00000000000..f8055090945 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communication.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Communication { + + Logger logger; + public Communicator communicator; + + volatile boolean didPostConstruct; + + @javax.inject.Inject + public Communication(Logger logger, Communicator communicator) { + this.logger = logger; + this.communicator = Objects.requireNonNull(communicator); + } + + @javax.annotation.PostConstruct + public void postConstruct() { + logger.log(Level.INFO, "logging is enabled for communication"); + } + + public int sendMessageViaAllModes(String message) { + if (!didPostConstruct) { + logger.log(Level.INFO, "explicitly calling postConstruct"); + postConstruct(); + } + + return communicator.sendMessage(message, "email") + + communicator.sendMessage(message, "sms") + + communicator.sendMessage(message, "im") + + communicator.sendMessage(message, "another"); + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/CommunicationMode.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/CommunicationMode.java new file mode 100644 index 00000000000..50965233e87 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/CommunicationMode.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +public interface CommunicationMode { + + int sendMessage(String message); + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communicator.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communicator.java new file mode 100644 index 00000000000..293183a8452 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/Communicator.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +public interface Communicator { + + int sendMessage(String message, String preferredMode); + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/DefaultCommunicator.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/DefaultCommunicator.java new file mode 100644 index 00000000000..ab5e5bd910c --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/DefaultCommunicator.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +public class DefaultCommunicator implements Communicator { + + @javax.inject.Inject + @javax.inject.Named("sms") + public CommunicationMode sms; + + @javax.inject.Inject + @javax.inject.Named("email") + public CommunicationMode email; + + @javax.inject.Inject + @javax.inject.Named("im") + public CommunicationMode im; + + public CommunicationMode defaultCommunication; + + @javax.inject.Inject + public DefaultCommunicator(CommunicationMode defaultCommunication) { + this.defaultCommunication = defaultCommunication; + } + + @Override + public int sendMessage(String message, String preferredMode) { + if ("sms".equals(preferredMode)) { + return sms.sendMessage(message); + } else if ("email".equals(preferredMode)) { + return email.sendMessage(message); + } else if ("im".equals(preferredMode)) { + return im.sendMessage(message); + } else { + return defaultCommunication.sendMessage(message); + } + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/EmailCommunicationMode.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/EmailCommunicationMode.java new file mode 100644 index 00000000000..737fd47b8e9 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/EmailCommunicationMode.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@javax.inject.Singleton +@javax.inject.Named("email") +public class EmailCommunicationMode implements CommunicationMode { + + @javax.inject.Inject + Logger logger; + + @Override + public int sendMessage(String message) { + logger.log(Level.INFO, "Sending message '" + message + "' over email"); + return 1; + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/ImCommunicationMode.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/ImCommunicationMode.java new file mode 100644 index 00000000000..47173b74d19 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/ImCommunicationMode.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@javax.inject.Singleton +@javax.inject.Named("im") +public class ImCommunicationMode implements CommunicationMode { + + @javax.inject.Inject + @jakarta.inject.Inject + Logger logger; + + @Override + public int sendMessage(String message) { + logger.log(Level.INFO, "Sending message '" + message + "' over IM"); + return 1; + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/LoggerProvider.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/LoggerProvider.java new file mode 100644 index 00000000000..5ade91b7ec7 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/LoggerProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.logging.Logger; + +@javax.inject.Singleton +public class LoggerProvider implements javax.inject.Provider, jakarta.inject.Provider { + + @Override + public Logger get() { + return Logger.getAnonymousLogger(); + } + +} diff --git a/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/SmsCommunicationMode.java b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/SmsCommunicationMode.java new file mode 100644 index 00000000000..baac623eca7 --- /dev/null +++ b/examples/pico/logger/common/src/main/java/io/helidon/examples/pico/logger/common/SmsCommunicationMode.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.common; + +import java.util.logging.Level; +import java.util.logging.Logger; + +@javax.inject.Singleton +@javax.inject.Named("sms") +public class SmsCommunicationMode implements CommunicationMode { + + @javax.inject.Inject + Logger logger; + + @Override + public int sendMessage(String message) { + logger.log(Level.INFO, "Sending message '" + message + "' over SMS"); + return 1; + } + +} diff --git a/examples/pico/logger/guice/pom.xml b/examples/pico/logger/guice/pom.xml new file mode 100644 index 00000000000..d12cc6451c1 --- /dev/null +++ b/examples/pico/logger/guice/pom.xml @@ -0,0 +1,106 @@ + + + + + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-examples-pico-logger-guice + Helidon Pico Examples - Logger - Guice + + + io.helidon.examples.pico.logger.guice.Main + + + + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-common + ${helidon.version} + + + com.google.inject + guice + ${version.lib.guice} + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + ${mainClass} + + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + + + diff --git a/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/BasicModule.java b/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/BasicModule.java new file mode 100644 index 00000000000..df0735ae792 --- /dev/null +++ b/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/BasicModule.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.guice; + +import io.helidon.examples.pico.logger.common.AnotherCommunicationMode; +import io.helidon.examples.pico.logger.common.Communication; +import io.helidon.examples.pico.logger.common.CommunicationMode; +import io.helidon.examples.pico.logger.common.Communicator; +import io.helidon.examples.pico.logger.common.DefaultCommunicator; +import io.helidon.examples.pico.logger.common.EmailCommunicationMode; +import io.helidon.examples.pico.logger.common.ImCommunicationMode; +import io.helidon.examples.pico.logger.common.SmsCommunicationMode; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +public class BasicModule extends AbstractModule { + + @Override + protected void configure() { + try { + bind(Communicator.class).toConstructor(DefaultCommunicator.class.getConstructor(CommunicationMode.class)); + bind(Boolean.class).toInstance(true); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + bind(CommunicationMode.class).to(AnotherCommunicationMode.class); + bind(CommunicationMode.class).annotatedWith(Names.named("default")).to(AnotherCommunicationMode.class); + bind(CommunicationMode.class).annotatedWith(Names.named("im")).to(ImCommunicationMode.class); + bind(CommunicationMode.class).annotatedWith(Names.named("im")).to(ImCommunicationMode.class); + bind(CommunicationMode.class).annotatedWith(Names.named("email")).to(EmailCommunicationMode.class); + bind(CommunicationMode.class).annotatedWith(Names.named("sms")).to(SmsCommunicationMode.class); + bind(Communication.class).asEagerSingleton(); + } + +} diff --git a/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/Main.java b/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/Main.java new file mode 100644 index 00000000000..c98f25c49ae --- /dev/null +++ b/examples/pico/logger/guice/src/main/java/io/helidon/examples/pico/logger/guice/Main.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.guice; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.examples.pico.logger.common.Communication; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class Main { + + static Injector injector; + static Communication comms; + + public static void main(String[] args){ + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + injector = Guice.createInjector(new BasicModule()); + comms = injector.getInstance(Communication.class); + + List messages = Arrays.asList(args); + if (messages.isEmpty()) { + messages = Collections.singletonList("Hello World!"); + } + + AtomicInteger sent = new AtomicInteger(); + messages.forEach((message) -> { + sent.addAndGet(comms.sendMessageViaAllModes(message)); + }); + int count = sent.get(); + System.out.println("finished sending: " + count); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Guice Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Guice Main elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/logger/guice/src/test/java/io/helidon/examples/pico/logger/guice/GuiceTest.java b/examples/pico/logger/guice/src/test/java/io/helidon/examples/pico/logger/guice/GuiceTest.java new file mode 100644 index 00000000000..3b608012118 --- /dev/null +++ b/examples/pico/logger/guice/src/test/java/io/helidon/examples/pico/logger/guice/GuiceTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.guice; + +import io.helidon.examples.pico.logger.common.AnotherCommunicationMode; +import io.helidon.examples.pico.logger.common.DefaultCommunicator; +import io.helidon.examples.pico.logger.common.EmailCommunicationMode; +import io.helidon.examples.pico.logger.common.ImCommunicationMode; +import io.helidon.examples.pico.logger.common.SmsCommunicationMode; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GuiceTest { + + @Test + public void testMain() { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + Main.main(new String[] {"Hello World!"}); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + DefaultCommunicator comms = ((DefaultCommunicator) Main.comms.communicator); + assertEquals(SmsCommunicationMode.class, comms.sms.getClass()); + assertEquals(EmailCommunicationMode.class, comms.email.getClass()); + assertEquals(ImCommunicationMode.class, comms.im.getClass()); + assertEquals(AnotherCommunicationMode.class, comms.defaultCommunication.getClass()); + System.out.println("Guice JUnit memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Guice JUnit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/logger/pico/pom.xml b/examples/pico/logger/pico/pom.xml new file mode 100644 index 00000000000..e2f90f21b18 --- /dev/null +++ b/examples/pico/logger/pico/pom.xml @@ -0,0 +1,140 @@ + + + + + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-examples-pico-logger-pico + Helidon Pico Examples - Logger - Pico + + + io.helidon.examples.pico.logger.pico.Main + + + + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-common + ${helidon.version} + + + io.helidon.pico + helidon-pico-services + + + io.helidon.builder + helidon-builder + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + ${mainClass} + + + + + + make-assembly + package + + single + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + + external-module-create + + + + compile + compile + + application-create + + + + + + io.helidon.examples.pico.logger.common + + helidon.examples.pico.logger.common + ALL + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + ${mainClass} + + + + + + diff --git a/examples/pico/logger/pico/src/main/java/io/helidon/examples/pico/logger/pico/Main.java b/examples/pico/logger/pico/src/main/java/io/helidon/examples/pico/logger/pico/Main.java new file mode 100644 index 00000000000..5f45d3cb052 --- /dev/null +++ b/examples/pico/logger/pico/src/main/java/io/helidon/examples/pico/logger/pico/Main.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.pico; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.examples.pico.logger.common.Communication; +import io.helidon.pico.PicoServices; +import io.helidon.pico.Services; + +public class Main { + + static Services services; + static Communication comms; + + public static void main(String[] args){ + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + PicoServices picoServices = PicoServices.picoServices().get(); + services = picoServices.services(); + comms = services.lookupFirst(Communication.class).get(); + + List messages = Arrays.asList(args); + if (messages.isEmpty()) { + messages = Collections.singletonList("Hello World!"); + } + + AtomicInteger sent = new AtomicInteger(); + messages.forEach((message) -> { + sent.addAndGet(comms.sendMessageViaAllModes(message)); + }); + int count = sent.get(); + System.out.println("finished sending: " + count); + + final long finish = System.currentTimeMillis(); + final long memFinish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + System.out.println("Pico Main memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Pico Main elapsed time = " + (finish - start) + " ms"); + } +} diff --git a/examples/pico/logger/pico/src/test/java/io/helidon/examples/pico/logger/pico/PicoTest.java b/examples/pico/logger/pico/src/test/java/io/helidon/examples/pico/logger/pico/PicoTest.java new file mode 100644 index 00000000000..1017c8918ec --- /dev/null +++ b/examples/pico/logger/pico/src/test/java/io/helidon/examples/pico/logger/pico/PicoTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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.examples.pico.logger.pico; + +import io.helidon.examples.pico.logger.common.AnotherCommunicationMode; +import io.helidon.examples.pico.logger.common.DefaultCommunicator; +import io.helidon.examples.pico.logger.common.EmailCommunicationMode; +import io.helidon.examples.pico.logger.common.ImCommunicationMode; +import io.helidon.examples.pico.logger.common.SmsCommunicationMode; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PicoTest { + + @Test + public void testMain() { + final long memStart = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long start = System.currentTimeMillis(); + + Main.main(new String[] {"Hello World!"}); + + final long finish = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + final long memFinish = Runtime.getRuntime().totalMemory(); + DefaultCommunicator comms = ((DefaultCommunicator) Main.comms.communicator); + assertEquals(SmsCommunicationMode.class, comms.sms.getClass()); + assertEquals(EmailCommunicationMode.class, comms.email.getClass()); + assertEquals(ImCommunicationMode.class, comms.im.getClass()); + assertEquals(AnotherCommunicationMode.class, comms.defaultCommunication.getClass()); + System.out.println("Pico Junit memory consumption = " + (memFinish - memStart) + " bytes"); + System.out.println("Pico Junit elapsed time = " + (finish - start) + " ms"); + } + +} diff --git a/examples/pico/logger/pom.xml b/examples/pico/logger/pom.xml new file mode 100644 index 00000000000..45cd71392be --- /dev/null +++ b/examples/pico/logger/pom.xml @@ -0,0 +1,51 @@ + + + + + + io.helidon.examples.pico + helidon-examples-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.examples.pico.logger + helidon-examples-pico-logger-project + Helidon Pico Examples - Logger + + pom + + + 5.1.0 + + + 1 + 1.3.2 + + + + common + guice + pico + + + diff --git a/examples/pico/logger/run.sh b/examples/pico/logger/run.sh new file mode 100755 index 00000000000..6ca82cffa03 --- /dev/null +++ b/examples/pico/logger/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e +# +# Copyright (c) 2023 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. +# + +# mvn clean install +clear +echo "RUN 1: (GUICE)" +java -jar guice/target/helidon-examples-pico-logger-guice-4.0.0-SNAPSHOT-jar-with-dependencies.jar "hello guice" +echo "RUN 2: (GUICE)" +java -jar guice/target/helidon-examples-pico-logger-guice-4.0.0-SNAPSHOT-jar-with-dependencies.jar "hello guice" +echo "========================" +echo "RUN 1: (PICO)" +java -jar pico/target/helidon-examples-pico-logger-pico-4.0.0-SNAPSHOT-jar-with-dependencies.jar "hello pico" +echo "RUN 2: (PICO)" +java -jar pico/target/helidon-examples-pico-logger-pico-4.0.0-SNAPSHOT-jar-with-dependencies.jar "hello pico" diff --git a/examples/pico/pom.xml b/examples/pico/pom.xml new file mode 100644 index 00000000000..55c65642885 --- /dev/null +++ b/examples/pico/pom.xml @@ -0,0 +1,54 @@ + + + + + + io.helidon.examples + helidon-examples-project + 4.0.0-SNAPSHOT + ../pom.xml + + + io.helidon.examples.pico + helidon-examples-pico-project + Helidon Examples Pico Project + pom + 4.0.0 + + + 11 + 11 + + + + + + + + + + + book + car + logger + + + diff --git a/examples/pom.xml b/examples/pom.xml index 532c70d3e3d..f14135d77aa 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -63,6 +63,7 @@ metrics jbatch nima + pico diff --git a/nima/http/pom.xml b/nima/http/pom.xml index df74714fbdd..8af9da5003d 100644 --- a/nima/http/pom.xml +++ b/nima/http/pom.xml @@ -38,6 +38,7 @@ encoding media + processor diff --git a/nima/http/processor/pom.xml b/nima/http/processor/pom.xml new file mode 100644 index 00000000000..abc8f95dbe8 --- /dev/null +++ b/nima/http/processor/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + io.helidon.nima.http + helidon-nima-http-project + 4.0.0-SNAPSHOT + + + helidon-nima-http-processor + Helidon Níma HTTP Annotation Processor + + + true + + + + + io.helidon.pico + helidon-pico-api + + + io.helidon.pico + helidon-pico-processor + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpEndpointCreator.java b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpEndpointCreator.java new file mode 100644 index 00000000000..428ed4837fa --- /dev/null +++ b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpEndpointCreator.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 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.nima.http.processor; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.pico.tools.CustomAnnotationTemplateRequest; +import io.helidon.pico.tools.CustomAnnotationTemplateResponse; +import io.helidon.pico.tools.DefaultGenericTemplateCreatorRequest; +import io.helidon.pico.tools.GenericTemplateCreatorRequest; +import io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; + +/** + * Annotation processor that generates a service for each class annotated with {@value #PATH_ANNOTATION} annotation. + * Service provider implementation of a {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator}. + */ +public class HttpEndpointCreator implements CustomAnnotationTemplateCreator { + private static final String PATH_ANNOTATION = "io.helidon.common.http.Path"; + + /** + * Default constructor used by the {@link java.util.ServiceLoader}. + */ + public HttpEndpointCreator() { + } + + @Override + public Set annoTypes() { + return Set.of(PATH_ANNOTATION); + } + + @Override + public Optional create(CustomAnnotationTemplateRequest request) { + TypeInfo enclosingType = request.enclosingTypeInfo(); + if (!enclosingType.typeKind().equals(TypeInfo.KIND_CLASS)) { + // we are only interested in classes, not in methods + return Optional.empty(); + } + + String classname = enclosingType.typeName().className() + "_GeneratedService"; + TypeName generatedTypeName = DefaultTypeName.create(enclosingType.typeName().packageName(), classname); + + String template = Templates.loadTemplate("nima", "http-endpoint.java.hbs"); + GenericTemplateCreatorRequest genericCreatorRequest = DefaultGenericTemplateCreatorRequest.builder() + .customAnnotationTemplateRequest(request) + .template(template) + .generatedTypeName(generatedTypeName) + .overrideProperties(addProperties(request)) + .build(); + return request.genericTemplateCreator().create(genericCreatorRequest); + } + + private Map addProperties(CustomAnnotationTemplateRequest request) { + Map response = new HashMap<>(); + + var annots = request.enclosingTypeInfo().annotations(); + for (var annot : annots) { + if (annot.typeName().name().equals(PATH_ANNOTATION)) { + response.put("http", Map.of("path", annot.value().orElse("/"))); + break; + } + } + + return response; + } +} diff --git a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java new file mode 100644 index 00000000000..b268375a9c9 --- /dev/null +++ b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2023 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.nima.http.processor; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.lang.model.element.ElementKind; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.tools.CustomAnnotationTemplateRequest; +import io.helidon.pico.tools.CustomAnnotationTemplateResponse; +import io.helidon.pico.tools.DefaultGenericTemplateCreatorRequest; +import io.helidon.pico.tools.GenericTemplateCreator; +import io.helidon.pico.tools.GenericTemplateCreatorRequest; +import io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; + +/** + * Annotation processor that generates a service for each method annotated with an HTTP method annotation. + * Service provider implementation of a {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator}. + */ +public class HttpMethodCreator implements CustomAnnotationTemplateCreator { + private static final String PATH_ANNOTATION = "io.helidon.common.http.Path"; + private static final String GET_ANNOTATION = "io.helidon.common.http.GET"; + private static final String HTTP_METHOD_ANNOTATION = "io.helidon.common.http.HttpMethod"; + private static final String POST_ANNOTATION = "io.helidon.common.http.POST"; + private static final String PATH_PARAM_ANNOTATION = "io.helidon.common.http.PathParam"; + private static final String HEADER_PARAM_ANNOTATION = "io.helidon.common.http.HeaderParam"; + private static final String QUERY_PARAM_ANNOTATION = "io.helidon.common.http.QueryParam"; + private static final String ENTITY_PARAM_ANNOTATION = "io.helidon.common.http.Entity"; + private static final String QUERY_NO_DEFAULT = "io.helidon.nima.htp.api.QueryParam_NO_DEFAULT_VALUE"; + + private static final Set PARAM_ANNOTATIONS = Set.of( + HEADER_PARAM_ANNOTATION, + QUERY_PARAM_ANNOTATION, + PATH_PARAM_ANNOTATION, + ENTITY_PARAM_ANNOTATION + ); + + /** + * Default constructor used by the {@link java.util.ServiceLoader}. + */ + public HttpMethodCreator() { + } + + @Override + public Set annoTypes() { + return Set.of(GET_ANNOTATION, + HEADER_PARAM_ANNOTATION, + HTTP_METHOD_ANNOTATION, + POST_ANNOTATION, + PATH_ANNOTATION, + QUERY_PARAM_ANNOTATION); + } + + @Override + public Optional create(CustomAnnotationTemplateRequest request) { + TypeInfo enclosingType = request.enclosingTypeInfo(); + + if (!ElementKind.METHOD.name().equals(request.targetElement().elementTypeKind())) { + // we are only interested in methods, not in classes + return Optional.empty(); + } + + String classname = enclosingType.typeName().className() + "_" + + request.annoTypeName().className() + "_" + + request.targetElement().elementName(); + TypeName generatedTypeName = DefaultTypeName.create(enclosingType.typeName().packageName(), classname); + + GenericTemplateCreator genericTemplateCreator = request.genericTemplateCreator(); + GenericTemplateCreatorRequest genericCreatorRequest = DefaultGenericTemplateCreatorRequest.builder() + .customAnnotationTemplateRequest(request) + .template(Templates.loadTemplate("nima", "http-method.java.hbs")) + .generatedTypeName(generatedTypeName) + .overrideProperties(addProperties(request)) + .build(); + return genericTemplateCreator.create(genericCreatorRequest); + } + + private Map addProperties(CustomAnnotationTemplateRequest request) { + TypedElementName targetElement = request.targetElement(); + Map response = new HashMap<>(); + + HttpDef http = new HttpDef(); + /* + Method response + http.response.type + http.response.isVoid + */ + TypeName returnType = targetElement.typeName(); + if ("void".equals(returnType.className())) { + http.response = new HttpResponse(); + } else { + http.response = new HttpResponse(returnType.name()); + } + + // http.methodName - name of the method in source code (not HTTP Method) + http.methodName = targetElement.elementName(); + + // http.params (full string) + List headerList = new LinkedList<>(); + List elementArgs = request.targetElementArgs(); + LinkedList parameters = new LinkedList<>(); + int headerCount = 1; + for (TypedElementName elementArg : elementArgs) { + String type = elementArg.typeName().name(); + + switch (type) { + case "io.helidon.nima.webserver.http.ServerRequest" -> parameters.add("req,"); + case "io.helidon.nima.webserver.http.ServerResponse" -> parameters.add("res,"); + default -> processParameter(http, parameters, headerList, type, elementArg); + } + } + + if (!parameters.isEmpty()) { + String last = parameters.removeLast(); + last = last.substring(0, last.length() - 1); + parameters.addLast(last); + } + http.params = parameters; + + /* + Headers + http.headers, field, name + */ + http.headers = headerList; + + /* + HTTP Method + http.method + */ + if (request.annoTypeName().className().equals(HTTP_METHOD_ANNOTATION)) { + http.method = findAnnotValue(targetElement.annotations(), HTTP_METHOD_ANNOTATION, null); + if (http.method == null) { + throw new IllegalStateException("HTTP method producer called without HTTP Method annotation (such as @GET)"); + } + } else { + http.method = request.annoTypeName().className(); + } + + // HTTP Path (if defined) + http.path = findAnnotValue(targetElement.annotations(), PATH_ANNOTATION, ""); + + response.put("http", http); + return response; + } + + private void processParameter(HttpDef httpDef, + LinkedList parameters, + List headerList, + String type, + TypedElementName elementArg) { + // depending on annotations + List annotations = elementArg.annotations(); + if (annotations.size() == 0) { + throw new IllegalStateException("Parameters must be annotated with one of @Entity, @PathParam, @HeaderParam, " + + "@QueryParam - parameter " + + elementArg.elementName() + " is not annotated at all."); + } + + AnnotationAndValue httpAnnotation = null; + + for (AnnotationAndValue annotation : annotations) { + if (PARAM_ANNOTATIONS.contains(annotation.typeName().name())) { + if (httpAnnotation == null) { + httpAnnotation = annotation; + } else { + throw new IllegalStateException("Parameters must be annotated with one of " + PARAM_ANNOTATIONS + + ", - parameter " + + elementArg.elementName() + " has more than one annotation."); + } + } + } + + if (httpAnnotation == null) { + throw new IllegalStateException("Parameters must be annotated with one of " + PARAM_ANNOTATIONS + + ", - parameter " + + elementArg.elementName() + " has neither of these."); + } + + // todo now we only support String for query, path and header -> add conversions + switch (httpAnnotation.typeName().className()) { + case ("PathParam") -> parameters.add("req.path().pathParameters().value(\"" + httpAnnotation.value().orElseThrow() + + "\"),"); + case ("Entity") -> parameters.add("req.content().as(" + type + ".class),"); + case ("HeaderParam") -> { + String headerName = "HEADER_" + (headerList.size() + 1); + headerList.add(new HeaderDef(headerName, httpAnnotation.value().orElseThrow())); + parameters.add("req.headers().get(" + headerName + ").value(),"); + } + case ("QueryParam") -> { + httpDef.hasQueryParams = true; + String defaultValue = httpAnnotation.value("defaultValue").orElse(null); + if (defaultValue == null || QUERY_NO_DEFAULT.equals(defaultValue)) { + defaultValue = "null"; + } else { + defaultValue = "\"" + defaultValue + "\""; + } + String queryParam = httpAnnotation.value().get(); + // TODO string is hardcoded, we need to add support for mapping + parameters.add("query(req, res, \"" + queryParam + "\", " + defaultValue + ", String.class),"); + } + default -> throw new IllegalStateException("Invalid annotation on HTTP parameter: " + elementArg.elementName()); + } + } + + private String findAnnotValue(List elementAnnotations, String name, String defaultValue) { + for (AnnotationAndValue elementAnnotation : elementAnnotations) { + if (name.equals(elementAnnotation.typeName().name())) { + return elementAnnotation.value().orElseThrow(); + } + } + return defaultValue; + } + + /** + * Needed for template processing. + * Do not use. + */ + @Deprecated(since = "1.0.0") + public static class HttpDef { + private List headers; + private LinkedList params; + private HttpResponse response; + private String methodName; + private String method; + private String path; + private boolean hasQueryParams; + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public List getHeaders() { + return headers; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public LinkedList getParams() { + return params; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public HttpResponse getResponse() { + return response; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getMethodName() { + return methodName; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getMethod() { + return method; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getPath() { + return path; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public boolean isHasQueryParams() { + return hasQueryParams; + } + } + + /** + * Needed for template processing. + * Do not use. + */ + @Deprecated(since = "1.0.0") + public static class HttpResponse { + private final String type; + private final boolean isVoid; + + HttpResponse() { + this.type = null; + this.isVoid = true; + } + + HttpResponse(String type) { + this.type = type; + this.isVoid = false; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getType() { + return type; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public boolean isVoid() { + return isVoid; + } + } + + /** + * Needed for template processing. + * Do not use. + */ + @Deprecated(since = "1.0.0") + public static class HeaderDef { + private String field; + private String name; + + HeaderDef(String field, String name) { + this.field = field; + this.name = name; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getField() { + return field; + } + + /** + * Needed for template processing. + * Do not use. + * @return do not use + */ + @Deprecated(since = "1.0.0") + public String getName() { + return name; + } + } +} diff --git a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/Templates.java b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/Templates.java new file mode 100644 index 00000000000..c5db21e4b48 --- /dev/null +++ b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/Templates.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.nima.http.processor; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +final class Templates { + private Templates() { + } + + static String loadTemplate(String templateProfile, String name) { + String path = "templates/pico/" + templateProfile + "/" + name; + try { + InputStream in = Templates.class.getClassLoader().getResourceAsStream(path); + if (in == null) { + throw new RuntimeException("Could not find template " + path + " on classpath."); + } + try (in) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/package-info.java b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/package-info.java new file mode 100644 index 00000000000..44e9b669c09 --- /dev/null +++ b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Annotation processor that generates HTTP Endpoints discoverable by Pico. + */ +package io.helidon.nima.http.processor; diff --git a/nima/http/processor/src/main/java/module-info.java b/nima/http/processor/src/main/java/module-info.java new file mode 100644 index 00000000000..19a03d18b94 --- /dev/null +++ b/nima/http/processor/src/main/java/module-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022, 2023 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. + */ + +import io.helidon.nima.http.processor.HttpEndpointCreator; +import io.helidon.nima.http.processor.HttpMethodCreator; + +/** + * Annotation processor that generates HTTP Endpoints. + */ +module io.helidon.nima.http.processor { + requires io.helidon.pico.api; + requires io.helidon.pico.tools; + requires io.helidon.pico.processor; + requires java.compiler; + requires io.helidon.builder.processor.spi; + + exports io.helidon.nima.http.processor; + + provides io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator + with HttpEndpointCreator, HttpMethodCreator; +} diff --git a/nima/http/processor/src/main/resources/templates/pico/nima/http-endpoint.java.hbs b/nima/http/processor/src/main/resources/templates/pico/nima/http-endpoint.java.hbs new file mode 100644 index 00000000000..9930b6de300 --- /dev/null +++ b/nima/http/processor/src/main/resources/templates/pico/nima/http-endpoint.java.hbs @@ -0,0 +1,57 @@ +{{! +Copyright (c) 2023 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 {{packageName}}; + +import java.util.List; + +import io.helidon.common.Weight; +import io.helidon.nima.webserver.http.GeneratedHandler; +import io.helidon.nima.webserver.http.Handler; +import io.helidon.nima.webserver.http.HttpFeature; +import io.helidon.nima.webserver.http.HttpRouting; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; + +import io.helidon.pico.ExternalContracts; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@jakarta.annotation.Generated({{{generatedSticker}}}) +@Singleton +@Named("{{enclosingClassTypeName.name}}") +@Weight({{weight}}) +@ExternalContracts(HttpFeature.class) +public class {{className}} implements HttpFeature { + private final List myMethods; + private final String path = "{{http.path}}"; + + @Inject + {{className}}(@Named("{{enclosingClassTypeName.name}}") List myMethods) { + this.myMethods = myMethods; + } + + @Override + public void setup(HttpRouting.Builder routing) { + routing.register(path, (HttpService) rules -> { + for (GeneratedHandler handler : myMethods) { + rules.route(handler.method(), handler.path(), (Handler) handler); + } + }); + } +} diff --git a/nima/http/processor/src/main/resources/templates/pico/nima/http-method.java.hbs b/nima/http/processor/src/main/resources/templates/pico/nima/http-method.java.hbs new file mode 100644 index 00000000000..1f8ade21c31 --- /dev/null +++ b/nima/http/processor/src/main/resources/templates/pico/nima/http-method.java.hbs @@ -0,0 +1,104 @@ +{{! +Copyright (c) 2023 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 {{packageName}}; + +import io.helidon.common.Weight; +import io.helidon.common.http.Http; + +{{#if http.hasQueryParams}}import io.helidon.common.http.HttpException; + +import io.helidon.common.uri.UriQuery; +{{/if}} +import io.helidon.nima.webserver.http.GeneratedHandler; +import io.helidon.nima.webserver.http.Handler; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +import io.helidon.pico.ExternalContracts; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@jakarta.annotation.Generated({{{generatedSticker}}}) +@Singleton +@Named("{{enclosingClassTypeName.name}}") +@Weight({{weight}}) +@ExternalContracts(GeneratedHandler.class) +public class {{className}} implements GeneratedHandler { +{{#each http.headers}}private static final Http.HeaderName {{this.field}} = Http.Header.create("{{this.name}}"); +{{/each}} + private static final Http.Method METHOD = Http.Method.create("{{http.method}}"); + + private final Provider<{{enclosingClassTypeName.className}}> target; + + private {{enclosingClassTypeName.className}} endpoint; + + @Inject + {{className}}(Provider<{{enclosingClassTypeName.className}}> target) { + this.target = target; + } + + @Override + public Http.Method method() { + return METHOD; + } + + @Override + public String path() { + return "{{http.path}}"; + } + + @Override + public void beforeStart() { + this.endpoint = target.get(); + } + + @Override + public void afterStop() { + this.endpoint = null; + } + + @Override + public void handle(ServerRequest req, ServerResponse res) { + {{#unless http.response.isVoid}}{{http.response.type}} response = {{/unless}}invokeMethod(req, res); + {{#if http.response.isVoid}}if (!res.isSent()) { + res.send(); + }{{else}}res.send(response);{{/if}} + } + + private {{#if http.response.isVoid}}void{{else}}{{http.response.type}}{{/if}} invokeMethod(ServerRequest req, ServerResponse res) { + {{#unless http.response.isVoid}}return {{/unless}}endpoint.{{http.methodName}}( + {{#each http.params}}{{{this}}} + {{/each}} + ); + } +{{#if http.hasQueryParams}} + private T query(ServerRequest req, ServerResponse res, String name, String defaultValue, Class type) { + UriQuery query = req.query(); + if (query.contains(name)) { + // todo hardcoded type + return (T) query.value(name); + } + if (defaultValue == null) { + throw new HttpException("Query parameter \"" + name + "\" is required", Http.Status.BAD_REQUEST_400, true); + } + // todo also hardcoded type + return (T) defaultValue; + } +{{/if}} +} diff --git a/nima/http2/webserver/pom.xml b/nima/http2/webserver/pom.xml index c9ff2b27308..5f4723e18ca 100644 --- a/nima/http2/webserver/pom.xml +++ b/nima/http2/webserver/pom.xml @@ -37,8 +37,8 @@ helidon-nima-http2 - io.helidon.pico.builder.config - helidon-pico-builder-config + io.helidon.builder + helidon-builder-config io.helidon.common.features @@ -87,8 +87,8 @@ ${helidon.version} - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.builder + helidon-builder-config-processor ${helidon.version} @@ -102,4 +102,3 @@ - diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 5498257146b..a7464b8a4bf 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -17,15 +17,15 @@ package io.helidon.nima.http2.webserver; import io.helidon.builder.Builder; +import io.helidon.builder.config.ConfigBean; import io.helidon.common.http.RequestedUriDiscoveryContext; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; /** * HTTP/2 server configuration. */ @Builder -@ConfigBean(key = "server.connection-providers.http_2") +@ConfigBean("server.connection-providers.http_2") public interface Http2Config { /** * The size of the largest frame payload that the sender is willing to receive in bytes. diff --git a/nima/http2/webserver/src/main/java/module-info.java b/nima/http2/webserver/src/main/java/module-info.java index 36e3d7e12bb..065ed60666b 100644 --- a/nima/http2/webserver/src/main/java/module-info.java +++ b/nima/http2/webserver/src/main/java/module-info.java @@ -39,8 +39,8 @@ requires transitive io.helidon.nima.http2; requires transitive io.helidon.nima.http.encoding; requires transitive io.helidon.nima.http.media; - requires io.helidon.pico.builder.config; requires io.helidon.builder; + requires io.helidon.builder.config; exports io.helidon.nima.http2.webserver; exports io.helidon.nima.http2.webserver.spi; diff --git a/nima/nima/src/main/java/io/helidon/nima/Nima.java b/nima/nima/src/main/java/io/helidon/nima/Nima.java index 879cac489fc..216985aa915 100644 --- a/nima/nima/src/main/java/io/helidon/nima/Nima.java +++ b/nima/nima/src/main/java/io/helidon/nima/Nima.java @@ -47,4 +47,13 @@ private Nima() { public static Config config() { return CONFIG_INSTANCE.get(); } + + /** + * Set global configuration. Once method {@link #config()} is invoked, the configuration cannot be modified. + * + * @param config configuration to use + */ + public static void config(Config config) { + NIMA_CONFIG.set(config); + } } diff --git a/nima/sse/webserver/pom.xml b/nima/sse/webserver/pom.xml index 23c62b9c674..c72d758eeb0 100644 --- a/nima/sse/webserver/pom.xml +++ b/nima/sse/webserver/pom.xml @@ -71,8 +71,8 @@ ${helidon.version} - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.builder + helidon-builder-config-processor ${helidon.version} @@ -86,4 +86,3 @@ - diff --git a/nima/webserver/webserver/pom.xml b/nima/webserver/webserver/pom.xml index 318c19e9daf..f248ac01ac8 100644 --- a/nima/webserver/webserver/pom.xml +++ b/nima/webserver/webserver/pom.xml @@ -69,25 +69,49 @@ helidon-nima-http-encoding - io.helidon.pico.builder.config - helidon-pico-builder-config + + io.helidon.builder + helidon-builder-config io.helidon.common.features - helidon-common-features-api - provided + helidon-common-features + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-services true - io.helidon.config - helidon-config-metadata - provided + io.helidon.pico + helidon-pico-api + + + io.helidon.pico + helidon-pico-services + true + + + io.helidon.builder + helidon-builder-config-processor + provided true - - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + provided + true + + + jakarta.inject + jakarta.inject-api + true + + + io.helidon.config + helidon-config-metadata provided true @@ -138,6 +162,11 @@ io.helidon.common.testing test + + io.helidon.pico + helidon-pico-testing + test + @@ -153,8 +182,8 @@ ${helidon.version} - io.helidon.pico.builder.config - helidon-pico-builder-config-processor + io.helidon.builder + helidon-builder-config-processor ${helidon.version} @@ -162,6 +191,11 @@ helidon-config-metadata-processor ${helidon.version} + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java index 0d71da500f7..0d2e2838414 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java @@ -39,12 +39,21 @@ import io.helidon.common.Version; import io.helidon.common.context.Context; +import io.helidon.common.features.HelidonFeatures; +import io.helidon.common.features.api.HelidonFlavor; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webserver.http.DirectHandlers; +import io.helidon.nima.webserver.http.HttpFeature; import io.helidon.nima.webserver.spi.ServerConnectionSelector; +import io.helidon.pico.configdriven.ConfiguredBy; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +@ConfiguredBy(ServerConfig.class) class LoomServer implements WebServer { private static final System.Logger LOGGER = System.getLogger(LoomServer.class.getName()); private static final String EXIT_ON_STARTED_KEY = "exit.on.started"; @@ -60,7 +69,23 @@ class LoomServer implements WebServer { private volatile List startFutures; private volatile boolean alreadyStarted = false; - LoomServer(Builder builder, DirectHandlers directHandlers) { + @Inject + LoomServer(ServerConfig serverConfig, List> features) { + this(WebServer.builder() + .routing(it -> { + features.stream() + .map(Provider::get) + .forEach(it::addFeature); + }), + serverConfig, + DirectHandlers.builder().build()); + } + + LoomServer(Builder builder, ServerConfig serverConfig, DirectHandlers directHandlers) { + // this will be modified once we move everything to config driven + builder.host(serverConfig.host()); + builder.port(serverConfig.port()); + this.registerShutdownHook = builder.shutdownHook(); this.context = builder.context(); @@ -117,8 +142,12 @@ class LoomServer implements WebServer { .factory()); } + @PostConstruct @Override public WebServer start() { + HelidonFeatures.flavor(HelidonFlavor.NIMA); + HelidonFeatures.print(HelidonFlavor.NIMA, Version.VERSION, false); + try { lifecycleLock.lockInterruptibly(); } catch (InterruptedException e) { @@ -224,12 +253,12 @@ private void startIt() { now = System.currentTimeMillis() - now; long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); - LOGGER.log(System.Logger.Level.INFO, "Helidon Níma " + Version.VERSION); LOGGER.log(System.Logger.Level.INFO, "Started all channels in " + now + " milliseconds. " + uptime + " milliseconds since JVM startup. " + "Java " + Runtime.version()); + if ("!".equals(System.getProperty(EXIT_ON_STARTED_KEY))) { LOGGER.log(System.Logger.Level.INFO, String.format("Exiting, -D%s set.", EXIT_ON_STARTED_KEY)); System.exit(0); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConfig.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConfig.java new file mode 100644 index 00000000000..12a15e4f52b --- /dev/null +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.nima.webserver; + +import io.helidon.builder.config.ConfigBean; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Server configuration bean. + * There is a generated {@link io.helidon.nima.webserver.DefaultServerConfig} implementing this type, that can be used + * to create an instance manually through a builder, or using configuration. + */ +@ConfigBean(value = "server", levelType = ConfigBean.LevelType.ROOT, drivesActivation = true) +public interface ServerConfig { + /** + * Host of the default socket. Defaults to all host addresses ({@code 0.0.0.0}). + * + * @return host address to listen on (for the default socket) + */ + @ConfiguredOption("0.0.0.0") + String host(); + + /** + * Port of the default socket. + * If configured to {@code 0} (the default), server starts on a random port. + * + * @return port to listen on (for the default socket) + */ + @ConfiguredOption("0") + int port(); +} diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java index 3f68aaf7838..3771ea4e1df 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java @@ -237,14 +237,13 @@ void start() { connectedPort, socketName)); - if (listenerConfig.writeQueueLength() <= 1) { - LOGGER.log(System.Logger.Level.INFO, "[" + serverChannelId + "] direct writes"); - } else { - LOGGER.log(System.Logger.Level.INFO, - "[" + serverChannelId + "] async writes, queue length: " + listenerConfig.writeQueueLength()); - } - if (LOGGER.isLoggable(TRACE)) { + if (listenerConfig.writeQueueLength() <= 1) { + LOGGER.log(System.Logger.Level.TRACE, "[" + serverChannelId + "] direct writes"); + } else { + LOGGER.log(System.Logger.Level.TRACE, + "[" + serverChannelId + "] async writes, queue length: " + listenerConfig.writeQueueLength()); + } if (listenerConfig.hasTls()) { debugTls(serverChannelId, listenerConfig.tls()); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java index c02683adfc2..b8a81bfebfe 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java @@ -38,10 +38,12 @@ import io.helidon.nima.webserver.http.HttpRouting; import io.helidon.nima.webserver.spi.ServerConnectionProvider; import io.helidon.nima.webserver.spi.ServerConnectionSelector; +import io.helidon.pico.Contract; /** * Server that opens server sockets and handles requests through routing. */ +@Contract public interface WebServer { /** * The default server socket configuration name. All the default server socket @@ -165,6 +167,8 @@ class Builder implements io.helidon.common.Builder, Router.R private final HelidonServiceLoader.Builder connectionProviders = HelidonServiceLoader.builder(ServiceLoader.load(ServerConnectionProvider.class)); + private final DefaultServerConfig.Builder configBuilder = DefaultServerConfig.builder(); + private Config providersConfig = Config.empty(); // MediaContext should be updated with config processing or during final build if not set. private MediaContext mediaContext; @@ -197,7 +201,7 @@ public WebServer build() { mediaContext(MediaContext.create()); } - return new LoomServer(this, directHandlers.build()); + return new LoomServer(this, configBuilder.build(), directHandlers.build()); } /** @@ -333,6 +337,7 @@ public Builder defaultSocket(Consumer socketBuild */ public Builder port(int port) { socket(DEFAULT_SOCKET_NAME).port(port); + configBuilder.port(port); return this; } @@ -344,6 +349,7 @@ public Builder port(int port) { */ public Builder host(String host) { socket(DEFAULT_SOCKET_NAME).host(host); + configBuilder.host(host); return this; } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/GeneratedHandler.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/GeneratedHandler.java new file mode 100644 index 00000000000..d9d90a23f20 --- /dev/null +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/GeneratedHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.nima.webserver.http; + +import io.helidon.common.http.Http; + +/** + * This class is only used by generated code. + * + * @deprecated please do not use directly, designed for generated code + * @see io.helidon.nima.webserver.http1.Http1Route + * @see io.helidon.nima.webserver.http.Handler + */ +@Deprecated(since = "4.0.0") +public interface GeneratedHandler extends Handler { + /** + * HTTP Method of this handler. + * + * @return method + */ + Http.Method method(); + + /** + * Path this handler should be registered at. + * + * @return path, may include path parameter (template) + */ + String path(); + + +} diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java index bfece6ce3db..7a01f0b0c75 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java @@ -19,15 +19,15 @@ import io.helidon.builder.Builder; import io.helidon.builder.Singular; +import io.helidon.builder.config.ConfigBean; import io.helidon.common.http.RequestedUriDiscoveryContext; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; /** * HTTP/1.1 server configuration. */ @Builder(interceptor = Http1BuilderInterceptor.class) -@ConfigBean(key = "server.connection-providers.http_1_1") +@ConfigBean("server.connection-providers.http_1_1") public interface Http1Config { /** * Maximal size of received HTTP prologue (GET /path HTTP/1.1). diff --git a/nima/webserver/webserver/src/main/java/module-info.java b/nima/webserver/webserver/src/main/java/module-info.java index 999f2a73b3d..d930e5d13a1 100644 --- a/nima/webserver/webserver/src/main/java/module-info.java +++ b/nima/webserver/webserver/src/main/java/module-info.java @@ -17,9 +17,6 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; import io.helidon.nima.webserver.http.spi.SinkProvider; -import io.helidon.nima.webserver.http1.Http1ConnectionProvider; -import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; -import io.helidon.nima.webserver.spi.ServerConnectionProvider; /** * Loom based WebServer. @@ -39,7 +36,9 @@ requires transitive io.helidon.common.security; requires io.helidon.logging.common; requires io.helidon.builder; - requires io.helidon.pico.builder.config; + requires io.helidon.builder.config; + requires io.helidon.common.features.api; + requires io.helidon.common.features; requires io.helidon.common.task; requires java.management; @@ -49,8 +48,13 @@ requires jakarta.annotation; requires io.helidon.common.uri; - requires static io.helidon.common.features.api; requires static io.helidon.config.metadata; + requires static io.helidon.pico.configdriven.services; + requires static jakarta.inject; + + // needed to compile pico generated classes + requires io.helidon.pico.api; + requires static io.helidon.pico.services; // provides multiple packages due to intentional cyclic dependency // we want to support HTTP/1.1 by default (we could fully separate it, but the API would be harder to use @@ -63,9 +67,10 @@ exports io.helidon.nima.webserver.http1; exports io.helidon.nima.webserver.http1.spi; - uses Http1UpgradeProvider; - uses ServerConnectionProvider; + uses io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; + uses io.helidon.nima.webserver.spi.ServerConnectionProvider; uses SinkProvider; - provides ServerConnectionProvider with Http1ConnectionProvider; -} \ No newline at end of file + provides io.helidon.nima.webserver.spi.ServerConnectionProvider with io.helidon.nima.webserver.http1.Http1ConnectionProvider; + provides io.helidon.pico.Module with io.helidon.nima.webserver.Pico$$Module; +} diff --git a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigDrivenTest.java b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigDrivenTest.java new file mode 100644 index 00000000000..56365fe2de4 --- /dev/null +++ b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigDrivenTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 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.nima.webserver; + +import io.helidon.config.Config; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.PicoTestingSupport; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +class WebServerConfigDrivenTest { + static final boolean NORMAL_PRODUCTION_PATH = false; + + @AfterEach + public void reset() { + if (!NORMAL_PRODUCTION_PATH) { + // requires 'pico.permits-dynamic=true' to be able to reset + PicoTestingSupport.resetAll(); + } + } + + @Test + void testConfigDriven() { + // This will pick up application.yaml from the classpath as default configuration file + Config config = Config.create(); + + if (NORMAL_PRODUCTION_PATH) { + // bootstrap Pico with our config tree when it initializes + PicoServices.globalBootstrap(DefaultBootstrap.builder().config(config).build()); + } + + // initialize Pico, and drive all activations based upon what has been configured + Services services; + if (NORMAL_PRODUCTION_PATH) { + services = PicoServices.realizedServices(); + } else { + PicoServices picoServices = PicoTestingSupport.testableServices(config); + services = picoServices.services(); + } + + ServiceProvider webServerSp = services.lookupFirst(WebServer.class); + assertThat(webServerSp.currentActivationPhase(), is(Phase.ACTIVE)); + } + +} diff --git a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigTest.java b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigTest.java index 88440ee24c0..32a95b51b80 100644 --- a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigTest.java +++ b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/WebServerConfigTest.java @@ -37,6 +37,8 @@ import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider; import io.helidon.nima.webserver.spi.ServerConnectionSelector; +import org.junit.jupiter.api.Test; + import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; diff --git a/nima/webserver/webserver/src/test/resources/application.yaml b/nima/webserver/webserver/src/test/resources/application.yaml index d7971c309d4..a75727823e1 100644 --- a/nima/webserver/webserver/src/test/resources/application.yaml +++ b/nima/webserver/webserver/src/test/resources/application.yaml @@ -41,3 +41,6 @@ server2: media-support: content-encoding: + +pico: + permits-dynamic: true diff --git a/pico/README.md b/pico/README.md new file mode 100644 index 00000000000..f3ebd0d300e --- /dev/null +++ b/pico/README.md @@ -0,0 +1,335 @@ +# helidon-pico + +Helidon Pico is an optional feature in Helidon. At its core it provides these main features: + +1. A service registry. The service registry holds service providers and are (or otherwise can produce) services. Each service provider in the registry advertises its meta-information for what each service provides, and what it requires in the way of dependencies. + +2. A lifecycle engine. Each service provider in the registry remains dormant until there is a "demand for activation". This "demand for activation" is also known as "lazy activation" that can come in different ways. One way is to simply "get" a service (or services) from the registry that matches the meta-information criteria you provide programmatically. If the service (or services) need other services as part of the activation then those services are chain activated, recursively. Pico provides graceful startup and shutdown at a micro service-level as well as at a macro application level for all services in the service registry. + +3. Integrations and Extensibility. More will be mentioned on this later. + +Over the foundation of these three main features of "services registry", "lifecycle", and "extensibility" there are a number of other tooling and layers that are delivered from various Pico submodules that provide the following additional features and benefits: + +1. A minimalist, compile-time generated dependency injection framework that is free from reflection, and compliant to the JSR-330 injection specification. Compile-time source code generation has a number of advantages, including: (a) pre-runtime validation of the DI model, (b) visibility into your application by providing "less magic", which in turn fosters understandability and debug-ability of your application, (c) deterministic behavior (instead of depending on reflection and classpath ordering, etc.) and (c) performance, since binding the model at compile-time is more efficient than computing it at runtime. Pico (through its tooling) provides you with a mix of declarative and programmatic ways to build your application. It doesn't have to be just one or the other like other popular frameworks in use today require. Inspiration for Pico, however, did come from many libraries and frameworks that came before it (e.g., Jakarta Hk2, Google Guice, Spring, CDI, and even OSGi). Foundationally, Pico provides a way to develop declarative code using standard (i.e., javax/jakarta, not Helidon specific) annotation types. + +2. Integrations. Blending services from different providers (e.g., Helidon services like WebServer, your application service, 3rd party service, etc.) becomes natural in the Pico framework, and enables you to build fully-integrated, feature-rich applications more easily. + +3. Extensibility. At the micro level developers can provide their own templates for things like code generation, or even provide an entirely different implementation from this reference implementation Helidon provides. At a macro level (and post the initial release), Pico will be providing a foundation to extend your Guice, Spring, Hk2, CDI, application naturally into one application runtime. The Helidon team fields a number of support questions that involve this area involving "battling DI frameworks" like CDI w/ Hk2. In time, Pico aims to smooth out this area through the integrations and extensibility features that it will be providing. + +4. Interception. Annotations are provided, that in conjunction with Pico's code-generation annotation processors, allow services in the service registry to support interception and decoration patterns - without the use of reflection at runtime, which is conducive to native image. + +*** +__Pico currently support Java 11+, using jakarta.inject or javax.inject, jakarta.annotations or javax.annotations.__ +*** + +The Helidon Team believes that the above features help developers achieve the following goals: +* More IoC options. With Pico... developers can choose to use an imperative coding style or a declarative IoC style previously only available with CDI using Helidon MP. At the initial release (Helidon 4.0), however, Pico will only be available with Helidon Nima support. +* Compile-time benefits. With Pico... developers can decide to use compile-time code generation into their build process, thereby statically and deterministically wiring their injection model while still enjoying the benefits of a declarative approach for writing their application. Added to this, all code-generated artifacts are in source form instead of bytecode thereby making your application more readable, understandable, consistent, and debuggable. Furthermore, DI model inconsistencies can be found during compile-time instead of at runtime. +* Improved performance. Pushing more into compile-time helps reduce what otherwise would need to occur (often times via reflection) to built/compile-time processing. Native code is generated that is further optimized by the compiler. Additionally, with lazy activation of services, only what is needed is activated. Anything not used may be in the classpath is available, but unless and until there is demand for those services they remain dormant. You control the lifecycle in your application code. +* Additional lifecycle options. Pico can handle micro, service-level activations for your services, as well as offer controlled shutdown if desired. + +Many DI frameworks start simple and over time become bloated with "bells and whistle" type features - the majority of which most developers don't need and will never use; especially in today's world of microservices where the application scope is the JVM process itself. + +*** +The Helidon Pico Framework is a reset back to basics, and perfect for such use cases requiring minimalism but yet still be extensible. This is why Pico intentionally chose to implement the earlier JSR-330 specification at its foundation. Application Scope == Singleton Scope in a microservices world. +*** + +Request and Session scopes are simply not made available in Pico. We believe that scoping is a recipe for undo complexity, confusion, and bugs for the many developers today. + +## Terminology +* DI - Dependency Injection. +* Inject - The assignment of a service instance to a field or method setter that has been annotated with @Inject - also referred to as an injection point. In Spring this would be referred to as 'Autowired'. +* Injection Plan - The act of determining how your application will resolve each injection point. In Pico this can optionally be performed at compile-time. But even when the injection plan is deferred to runtime it is resolved without using reflection, and is therefore conducive to native image restrictions and enhanced performance. +* Service (aka Bean) - In Spring this would be referred to as a bean with a @Service annotation; These are concrete class types in your application that represents some sort of business logic. +* Scope - This refers to the cardinality of a service instance in your application. +* Singleton - jakarta.inject.Singleton or javax.inject.Singleton - This is the default scope for services in Pico just like it is in Spring. +* Provided - jakarta.inject.Provider or javax.inject.Provider - If the scope of a service is not Singleton then it is considered to be a Provided scope - and the cardinality will be ascribed to the implementation of the Provider to determine its cardinality. The provider can optionally use the injection point context to determine the appropriate instance and/or cardinality it provides. +* Contract - These are how a service can alias itself for injection. Contracts are typically the interface or abstract base class definitions of a service implementation. Injection points must be based upon either using a contract or service that pico is aware of, usually through annotation processing at compile time. +* Qualifier - jakarta.inject.qualifier or javax.inject.qualifier - These are meta annotations that can be ascribed to other annotations. One built-in qualifier type is @Named in the same package. +* Dependency - An injection point represents what is considered to be a dependency, perhaps qualified or Optional, on another service or contract. This is just another what to describe an injection point. +* Activator (aka ServiceProvider) - This is what is code generated by Pico to lazily activate your service instance(s) in the Pico services registry, and it handles resolving all dependencies it has, along with injecting the fields, methods, etc. that are required to be satisfied as part of that activation process. +* Services (aka services registry) - This is the collection of all services that are known to the JVM/runtime in Pico. +* Module - This is where your application will "bind" services into the services registry - typically code generated, and typically with one module per jar/module in your application. +* Application - The fully realized set of modules and services/service providers that constitute your application, and code-generated using Helidon Pico Tooling. + +## Getting Started +As stated in the introduction above, the Pico framework aims to provide a minimalist API implementation. As a result, it might be surprising to learn how small the actual API is for Pico - see [pico api](./pico) and the API/annotation types at [pico api](./api/src/main/java/io/helidon/pico). If you are already familiar with [jakarta.inject](https://javadoc.io/doc/jakarta.inject/jakarta.inject-api/latest/index.html) and optionally, [jakarta.annotation](https://javadoc.io/doc/jakarta.annotation/jakarta.annotation-api/latest/jakarta.annotation/jakarta/annotation/package-summary.html) then basically you are ready to go. But if you've never used DI before then first review the basics of [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). + +The prerequisites are familiarity with dependency injection, Java 11+, and maven 3.8.5+. + +The best way to learn Helidon Pico is by looking at [the examples](../examples/pico). But if you want to immediately get started here are the basics steps: + +1. Put these in your pom.xml or gradle.build file: + Annotation processor dependency / path: +``` + io.helidon.pico + helidon-pico-processor + ${helidon.version} +``` +Compile-time dependency: +``` + + io.helidon.pico + helidon-pico-services + ${helidon.version} + +``` + +2. Write your application using w/ standard jakarta.inject.* and jakarta.annotation.* types. Again, see any of [the examples](./examples/README.md) for pointers as needed. + +3. Build and run. In a DI-based framework, the frameworks "owns" the creation of services in accordance with the Scope each service is declared as. You therefore need to get things started by creating demand for the initial service(s) instead of ever calling new directly in your application code. Generally speaking, there are two such ways to get things started at runtime: + +* If you know the class you want to create then look it up directly using the Services SPI. Here is a sample excerpt from [the book example](./examples/book/README.md): + +``` + Services services = PicoServices.realizedServices(); + // query + ServiceProvider serviceProvider = services.lookupFirst(MyService.class); + // lazily activate + MyService myLazyActivatedService = serviceProvider.get(); +``` + +* If there are a collection of services requiring activation at startup then we recommend annotating those service implementation types with RunLevel(RunLevel.STARTUP) and then use code below in main() to lazily activate those services. Note that whenever List-based injection is used in Pico all services matching the injection criteria will be in the injected (and immutable) list. The list will always be in order according to the Weight annotation value, ranking from the highest weight to the lowest weight. If services are not weighted explicitly, then a default weight is assigned. If the weight is the same for two services, then the secondary ordering will be based on the FN class name of the service types. While Weight determines list order, the RunLevel annotation is used to rank the startup ordering, from the lowest value to the highest value, where RunLevel.STARTUP == 0. The developer is expected to activate these directly using code like the following (the get() lazily creates & activates the underlying service type): + +``` + List> startupServices = services + .lookup(DefaultServiceInfoCriteria.builder().runLevel(RunLevel.STARTUP).build()); + startupServices.stream().forEach(ServiceProvider::get); +``` + +* If the ordering of the list of services is important, remember to use the Weight and/or RunLevel annotations to establish the priority / weighted ordering, and startup ordering. + +## More Advanced Features + +* Pico provides a means to generate "Activators" (the DI supporting types) for externally built modules as well as supporting javax annotated types. See [the logger example](../examples/pico/logger) for use of these features. + +* Pico offers services the ability to be intercepted. If your service contains any annotation that itself is annotated with InterceptorTrigger then the code generated for that service will support interception. The Helidon Nima project provides these types of examples. + +* Pico provides meta-information for each service in its service registry, including such information as what contracts are provided by each service as well as describing its dependencies. + +* Java Module System support / generation. Pico generates a proposed module-info.java.pico file for your module (look for module-info.java.pico under ./target/pico). + +* Pico provides a maven-plugin that allows the injection graph to be (a) validated for completeness, and (b) deterministically bound to the service implementation - at compile-time. This is demonstrated in each of the examples, the result of which leads to early detection of issues at compile-time instead of at runtime as well as a marked performance enhancement. + +* Testability. The [testing](./testing) module offers a set of types in order to facility for creating fake/mock services for various testing scenarios. + +* Extensibility. The entire Pico Framework is designed to by extended either at a micro level (developers can override mustache/handlebar templates) to the macro level (developers can provide their own implementation of any SPI). Another example of internal extensibility is via our [config-driven](./configdriven) services. + +* Determinism. Pico strives to keep your application as deterministic as possible. Dynamically adding services post-initialization will not be allowed, and will even result in a runtime exception (configurable). Any service that is "a provider" that dynamically creates a service in runtime code will issue build failures or warnings (configurable). All services are always ordered first according to Weight and secondarily according to type name (instead of relying on classpath ordering). Essentially, all areas of Pico attempts to keep your application as deterministic as possible at production runtime. + +## Modules + +* [api](./api) - the Pico API and SPI; depends on jakarta-inject and jakarta-annotations. Required as a maven compile-time dependency for runtime consumption. +* [services](./services) - contains the default runtime implementation of the Pico API/SPI; depends on the pico api module above. Requires as a maven compile-time dependency for runtime consumption. +* [config-driven](./configdriven) - Extensions to Pico to integrate directly with the [Helidon Config](../config) subsystem. +* [tools](./tools) - contains the libraries and template-based codegen mustache resources as well as model validation tooling; depends on runtime services. Only required at build time and is not required for Pico at runtime. +* [processor](./processor) - contains the libraries for annotation processing; depends on tools. Only required at build time and is not required for Pico at runtime. +* [maven-plugin](./maven-plugin) - provides code generation Mojo wrappers for maven; depends on tools. Only required at build time and is not required for Pico at runtime. This is what would be used to create your Application. +* [testing](./testing) - provides testing types useful for Pico unit & integration testing. +* [tests](./tests) - used internally for testing Pico. +* [examples](../examples/pico) - providing examples for how to use Pico as well as side-by-side comparisons for Pico compared to Guice, Dagger2, Hk2, etc. + +## How Pico Works + +* The Pico annotation [processor](./processor) will look for standard jakarta/javax inject and jakarta/javax annotation types. When these types are found in a class that is being compiled by javac, Pico will trigger the creation of an Activator for that service class/type. For example, if you have a FooImpl class implementing Foo interface, and the FooImpl either contains "@Inject" or "@Singleton" then the presence of either of these annotations will trigger the creation of a FooImpl$$picoActivator to be created. The Activator is used to (a) describe the service in terms of what service contracts (i.e., interfaces) are advertised by FooImpl - in this case Foo (if Foo is annotated with @Contract or if "-Aio.helidon.pico.autoAddNonContractInterfaces=true" is used at compile-time), (b) lifecycle of services including creation, calling injection-based setters, and any PostConstruct or PreDestroy methods. + +* If one or more activators are created at compile-time, then a Pico$$Module is also created to aggregate the services for the given module. Below is an example if a picoModule from [examples/logger](./examples/logger). At initialization time of Pico, all Modules will be located using the ServiceLocator and each service will be binded into the Pico service registry. + +```java +@Generated(value = "io.helidon.pico.tools.creator.impl.DefaultActivatorCreator", comments = "version = 1") +@Singleton @Named(Pico$$Module.NAME) +public class Pico$$Module implements Module { + static final String NAME = "pico.examples.logger.common"; + + @Override + public Optional getName() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + ":" + getClass().getName(); + } + + @Override + public void configure(ServiceBinder binder) { + binder.bind(io.helidon.pico.examples.logger.common.AnotherCommunicationMode$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.Communication$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.DefaultCommunicator$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.EmailCommunicationMode$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.ImCommunicationMode$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE); + binder.bind(io.helidon.pico.examples.logger.common.SmsCommunicationMode$$picoActivator.INSTANCE); + } +} +``` + +And just randomly taking one of the generated Activators: +```java +@Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +public class SmsCommunicationMode$$Pico$$Activator + extends io.helidon.pico.services.AbstractServiceProvider { + private static final DefaultServiceInfo serviceInfo = + DefaultServiceInfo.builder() + .serviceTypeName(io.helidon.examples.pico.logger.common.SmsCommunicationMode.class.getName()) + .addExternalContractsImplemented(io.helidon.examples.pico.logger.common.CommunicationMode.class.getName()) + .activatorTypeName(SmsCommunicationMode$$Pico$$Activator.class.getName()) + .addScopeTypeName(jakarta.inject.Singleton.class.getName()) + .addQualifier(io.helidon.pico.DefaultQualifierAndValue.create(jakarta.inject.Named.class, "sms")) + .build(); + + /** + * The global singleton instance for this service provider activator. + */ + public static final SmsCommunicationMode$$Pico$$Activator INSTANCE = new SmsCommunicationMode$$Pico$$Activator(); + + /** + * Default activator constructor. + */ + protected SmsCommunicationMode$$Pico$$Activator() { + serviceInfo(serviceInfo); + } + + /** + * The service type of the managed service. + * + * @return the service type of the managed service + */ + public Class serviceType() { + return io.helidon.examples.pico.logger.common.SmsCommunicationMode.class; + } + + @Override + public DependenciesInfo dependencies() { + DependenciesInfo deps = Dependencies.builder(io.helidon.examples.pico.logger.common.SmsCommunicationMode.class.getName()) + .add("logger", java.util.logging.Logger.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .build(); + return Dependencies.combine(super.dependencies(), deps); + } + + @Override + protected SmsCommunicationMode createServiceProvider(Map deps) { + return new io.helidon.examples.pico.logger.common.SmsCommunicationMode(); + } + + @Override + protected void doInjectingFields(Object t, Map deps, Set injections, String forServiceType) { + super.doInjectingFields(t, deps, injections, forServiceType); + SmsCommunicationMode target = (SmsCommunicationMode) t; + target.logger = (java.util.logging.Logger) get(deps, "io.helidon.examples.pico.logger.common.logger"); + } + +} +``` + +* As you can see from above example, the Activators are effectively managing the lifecycle and injection of your classes. These generated Activator types are placed in the same package as your class(es). Since Pico is avoiding reflection, however, it means that only public, protected, and package private injection points are supported. private and static injection points are not supported by the framework. + +* If an annotation in your service is meta-annotated with InterceptedTrigger, then an extra service type is created that will trigger interceptor service code generation. For example, if FooImpl was found to have one such annotation then FooImpl$$Pico$$Interceptor would also be created along with an activator for that interceptor. The interceptor would be created with a higher weight than your FooImpl, and would therefore be "preferred" when a single @Inject is used for Foo or FooImpl. If a list is injected then it would appear towards the head of the list. Once again, all reflection is avoided in these generated classes. Any calls to Foo/FooImpl will be interceptable for any Interceptor that is @Named to handle that type name. Search the test code and Nima code for such examples as this is an advanced feature. + +* The [maven-plugin](./maven-plugin) can optionally be used to avoid Pico lookup resolutions at runtime within each service activation. At startup Pico will attempt to first use the Application to avoid lookups. The best practice is to apply the maven-plugin to create-application on your maven assembly - this is usually your "final" application module that depends upon every other service / module in your entire deployed application. Here is the Pico$$Application from [examples/logger](../examples/pico/logger): + +```java +@Generated({"generator=io.helidon.pico.maven.plugin.ApplicationCreatorMojo", "ver=1"}) +@Singleton @Named(Pico$$Application.NAME) +public class Pico$$Application implements Application { + static final String NAME = "unnamed"; + + @Override + public Optional getName() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + ":" + getClass().getName(); + } + + @Override + public void configure(ServiceInjectionPlanBinder binder) { + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.AnotherCommunicationMode } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.AnotherCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.logger", + io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.Communication } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.Communication$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.|2(1)", + io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.|2(2)", + io.helidon.pico.examples.logger.common.DefaultCommunicator$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.DefaultCommunicator } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.DefaultCommunicator$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.sms", + io.helidon.pico.examples.logger.common.SmsCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.email", + io.helidon.pico.examples.logger.common.EmailCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.im", + io.helidon.pico.examples.logger.common.ImCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.|1(1)", + io.helidon.pico.examples.logger.common.AnotherCommunicationMode$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.EmailCommunicationMode } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.EmailCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.logger", + io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.ImCommunicationMode } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.ImCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.logger", + io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.LoggerProvider } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .commit(); + + /** + * In module name "pico.examples.logger.common". + * @see {@link io.helidon.pico.examples.logger.common.SmsCommunicationMode } + */ + binder.bindTo(io.helidon.pico.examples.logger.common.SmsCommunicationMode$$picoActivator.INSTANCE) + .bind("io.helidon.pico.examples.logger.common.logger", + io.helidon.pico.examples.logger.common.LoggerProvider$$picoActivator.INSTANCE) + .commit(); + } +} +``` + +* The maven-plugin can additionally be used to create the Pico DI supporting types (Activators, Modules, Interceptors, Applications, etc.) from introspecting an external jar - see the [examples](../examples/pico) for details. + +That is basically all there is to know to get started and become productive using Pico. + +## Special Notes to Providers & Contributors +Pico aims to provide an extensible, SPI-based mechanism. There are many ways Pico can be overridden, extended, or even replaced with a different implementation than what is provided out of the built-in reference implementation modules included. Of course, you can also contribute directly by becoming a committer. However, if you are looking to fork the implementation then you are strongly encouraged to honor the "spirit of this framework" and follow this as a high-level guide: + +* In order to be a Pico provider implementation, the provider must supply an implementation for PicoServices discoverable by the + ServiceLoader with a higher-than-default Weight. +* All SPI class definitions from the io.helidon.pico.spi package are considered primordial and therefore should not participate in injection or conventionally be considered injectable. +* All service classes that are not targets for injection should be represented under + /META-INF/services/ to be found by the standard ServiceLocator. +* Providers are encouraged to fail-fast during compile time - this implies a sophisticated set of tooling that can and should be applied to create and validate the integrity of the dependency graph at compile time instead of at runtime. +* Providers are encouraged to avoid reflection completely at runtime. +* Providers are encouraged to advertise capabilities and configuration using PicoServicesConfig. diff --git a/pico/api/README.md b/pico/api/README.md new file mode 100644 index 00000000000..22fd2ed6605 --- /dev/null +++ b/pico/api/README.md @@ -0,0 +1,112 @@ +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) +* [@ExteralContracts](src/main/java/io/helidon/pico/ExternalContracts.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](../services) module for the default reference implementation for this API / SPI. + +## Declaring a Service + +### Singleton +In this example the service is declared to be one-per JVM. Also note that ApplicationScoped is effectively the same as Singleton scoped services in a (micro)services framework such as Helidon. + +```java +@jakarta.inject.Singleton +class MySingletonService implements ServiceContract { +} + +``` + +Also note that in the above example ServiceContract is typically the Contract or ExternalContract definition - which is a way to signify lookup capabilities within the Services registry. + +### Provider +Provider extends the Singleton to delegate dynamic behavior to service creation. In other frameworks this would typically be called a Factory, Producer, or PerLookup. + +```java +@jakarta.inject.Singleton +class MySingletonProvider implements jakarta.inject.Provider { + @Override + ServiceContract get() { + ... + } +} +``` + +Pico delegates the cardinality of to the provider implementation for which instance to return to the caller. However, note that the instances returned are not "owned" by Pico - unless those instances are looked up out of the Services registry. + +### InjectionPointProvider +Here the standard jakarta.inject.Provider<> from above is extended to support contextual knowledge of "who is asking" to be injected with the service. In this way the provider implementation can provide the "right" instance based upon the caller's context. + +```java +@Singleton +@Named("*") +public class BladeProvider implements InjectionPointProvider { + @Override + public Optional first( + ContextualServiceQuery query) { + ServiceInfoCriteria criteria = query.serviceInfoCriteria(); + + AbstractBlade blade; + if (criteria.qualifiers().contains(all) || criteria.qualifiers().contains(coarse)) { + blade = new CoarseBlade(); + } else if (criteria.qualifiers().contains(fine)) { + blade = new FineBlade(); + } else { + assert (criteria.qualifiers().isEmpty()); + blade = new DullBlade(); + } + + return Optional.of(blade); + } +} +``` + +## Injectable Constructs +Any service can declare field, method, or constructor injection points. The only caveat is that these injectable elements must either be public or package private. Generally speaking, it is considered a best practice to (a) use only an injectable constructor, and (b) only inject Provider instances. Here is an example for best practice depicting all possible usages for injection types supported by Pico. + +```java +@Singleton +public class MainToolBox implements ToolBox { + + // generally not recommended + @Inject + Provider anyHammerProvider; + + // the best practice is to generally to use only constructor injection with Provider-wrapped types + @Inject + MainToolBox( + @Preferred Screwdriver screwdriver, // the qualifier restricts to the "preferred" screwdriver + List> allTools, // all tools in proper weighted/ranked order + @Named("big") Provider bigHammerProvider, // only the hammer provider qualified with name "big" + List> allHammers, // all hammers in proper weighted/ranked order + Optional maybeAChisel) // optionally a Chisel, activated + { + } + + // generally not recommended + @Inject + void setScrewdriver(Screwdriver screwdriver) { + } + + // called after construction by Pico's lifecycle startup management + @PostConstruct + void postConstruct() { + } + + // called before shutdown by Pico's lifecycle shutdown management + @PreDestroy + void preDestroy() { + } +} + +``` diff --git a/pico/pico/pom.xml b/pico/api/pom.xml similarity index 90% rename from pico/pico/pom.xml rename to pico/api/pom.xml index 755f625f61a..b2c712c47bf 100644 --- a/pico/pico/pom.xml +++ b/pico/api/pom.xml @@ -1,6 +1,5 @@ 4.0.0 - helidon-pico + helidon-pico-api Helidon Pico API / SPI @@ -38,8 +36,8 @@ - io.helidon.pico - helidon-pico-types + io.helidon.common + helidon-common-types io.helidon.common @@ -49,6 +47,11 @@ io.helidon.common helidon-common-config + + io.helidon.config + helidon-config + test + jakarta.inject jakarta.inject-api @@ -107,13 +110,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - - true - - diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationLog.java b/pico/api/src/main/java/io/helidon/pico/ActivationLog.java similarity index 91% rename from pico/pico/src/main/java/io/helidon/pico/ActivationLog.java rename to pico/api/src/main/java/io/helidon/pico/ActivationLog.java index 0ae772183a5..8eb69a8c4f9 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationLog.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationLog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -33,7 +33,7 @@ public interface ActivationLog { * @param entry the log entry to record * @return the (perhaps decorated) activation log entry */ - ActivationLogEntry recordActivationEvent(ActivationLogEntry entry); + ActivationLogEntry record(ActivationLogEntry entry); /** * Optionally provide a means to query the activation log, if query is possible. If query is not possible then an empty diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java b/pico/api/src/main/java/io/helidon/pico/ActivationLogEntry.java similarity index 51% rename from pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java rename to pico/api/src/main/java/io/helidon/pico/ActivationLogEntry.java index ec0ee0ed6b8..9ba609222d7 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationLogEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,6 +20,7 @@ import java.util.Optional; import io.helidon.builder.Builder; +import io.helidon.builder.BuilderInterceptor; /** * Log entry for lifecycle related events (i.e., activation startup and deactivation shutdown). @@ -27,32 +28,9 @@ * @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(); +@Builder(interceptor = ActivationLogEntry.Interceptor.class) +public interface ActivationLogEntry { /** * The event. @@ -62,32 +40,32 @@ enum Event { Event event(); /** - * The starting activation phase. + * Optionally, any special message being logged. * - * @return the starting activation phase + * @return the message */ - ActivationPhase startingActivationPhase(); + Optional message(); /** - * The eventual/desired/target activation phase. + * Optionally, when this log entry pertains to a service provider activation. * - * @return the eventual/desired/target activation phase + * @return the activation result */ - ActivationPhase targetActivationPhase(); + Optional activationResult(); /** - * The finishing phase at the time of this event's log entry. + * Optionally, the managing service provider the event pertains to. * - * @return the actual finishing phase + * @return the managing service provider */ - ActivationPhase finishingActivationPhase(); + Optional> serviceProvider(); /** - * The finishing activation status at the time of this event's log entry. + * Optionally, the injection point that the event pertains to. * - * @return the activation status + * @return the injection point */ - ActivationStatus finishingStatus(); + Optional injectionPoint(); /** * The time this event was generated. @@ -110,4 +88,31 @@ enum Event { */ long threadId(); + + /** + * Ensures that the non-nullable fields are populated with default values. + */ + class Interceptor implements BuilderInterceptor { + + Interceptor() { + } + + @Override + public DefaultActivationLogEntry.Builder intercept(DefaultActivationLogEntry.Builder b) { + if (b.time() == null) { + b.time(Instant.now()); + } + + if (b.threadId() == 0) { + b.threadId(Thread.currentThread().getId()); + } + + if (b.event() == null) { + b.event(Event.FINISHED); + } + + return b; + } + } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java b/pico/api/src/main/java/io/helidon/pico/ActivationLogQuery.java similarity index 53% rename from pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java rename to pico/api/src/main/java/io/helidon/pico/ActivationLogQuery.java index 88899a271f4..f776f5b67e2 100644 --- a/pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationLogQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,26 +17,28 @@ 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. + * Provide a means to query the activation log. + * + * @see ActivationLog */ -public interface DependenciesInfo { +public interface ActivationLogQuery extends Resettable { /** - * Represents the set of dependencies for each {@link ServiceInfo}. + * Clears the activation log. * - * @return map from the service info to its dependencies + * @param deep ignored + * @return true if the log was cleared, false if the log was previously empty */ - Map> serviceInfoDependencies(); + @Override + boolean reset(boolean deep); /** - * Represents a flattened list of all dependencies. + * The full transcript of all services phase transitions being managed. * - * @return the flattened list of all dependencies + * @return the activation log if log capture is enabled */ - List allDependencies(); + List fullActivationLog(); } diff --git a/pico/pico/src/main/java/io/helidon/pico/EventReceiver.java b/pico/api/src/main/java/io/helidon/pico/ActivationPhaseReceiver.java similarity index 51% rename from pico/pico/src/main/java/io/helidon/pico/EventReceiver.java rename to pico/api/src/main/java/io/helidon/pico/ActivationPhaseReceiver.java index 4516ea1be83..f59624dda2d 100644 --- a/pico/pico/src/main/java/io/helidon/pico/EventReceiver.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationPhaseReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,42 +17,22 @@ package io.helidon.pico; /** - * A receiver of events from the {@link Services} registry. + * A receiver of events from the {@link Services} registry and providers held by the service 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 { +public interface ActivationPhaseReceiver { /** - * 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. + * Called when there is an event transition within the service registry. * * @param event the event + * @param phase the phase */ - void onEvent(Event event); + void onPhaseEvent(Event event, + Phase phase); } diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java b/pico/api/src/main/java/io/helidon/pico/ActivationRequest.java similarity index 61% rename from pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java rename to pico/api/src/main/java/io/helidon/pico/ActivationRequest.java index 8d426dcb6da..6cb57543eaf 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -23,32 +23,31 @@ /** * Request to activate a service. - * - * @param service type */ @Builder -public interface ActivationRequest { +public interface ActivationRequest { /** - * Target service provider. + * Optionally, the injection point context information. * - * @return service provider + * @return injection point info */ - ServiceProvider serviceProvider(); + Optional injectionPoint(); /** - * Injection point context information. + * The phase to start activation. Typically, this should be left as the default (i.e., PENDING). * - * @return injection point info + * @return phase to start */ - Optional injectionPoint(); + Optional startingPhase(); /** * Ultimate target phase for activation. * * @return phase to target */ - ActivationPhase targetPhase(); + @ConfiguredOption("ACTIVE") + Phase targetPhase(); /** * Whether to throw an exception on failure to activate, or return an error activation result on activation. @@ -56,6 +55,18 @@ public interface ActivationRequest { * @return whether to throw on failure */ @ConfiguredOption("true") - boolean throwOnFailure(); + boolean throwIfError(); + + /** + * Creates a new activation request. + * + * @param targetPhase the target phase + * @return the activation request + */ + static ActivationRequest create(Phase targetPhase) { + return DefaultActivationRequest.builder() + .targetPhase(targetPhase) + .build(); + } } diff --git a/pico/api/src/main/java/io/helidon/pico/ActivationResult.java b/pico/api/src/main/java/io/helidon/pico/ActivationResult.java new file mode 100644 index 00000000000..4fa3600e182 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/ActivationResult.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022, 2023 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; +import java.util.concurrent.Future; + +import io.helidon.builder.Builder; +import io.helidon.pico.spi.InjectionPlan; + +/** + * Represents the result of a service activation or deactivation. + * + * @see Activator + * @see DeActivator + **/ +@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 + */ + Phase startingActivationPhase(); + + /** + * The activation phase that was requested at the onset of the phase transition. + * + * @return the target, desired, ultimate phase requested + */ + Phase targetActivationPhase(); + + /** + * The activation phase we finished successfully on, or are otherwise currently in if not yet finished. + * + * @return the finishing phase + */ + Phase finishingActivationPhase(); + + /** + * How did the activation finish. + * Will only be populated if the lifecycle event has completed - see {@link #finishedActivationResult()}. + * + * @return the finishing status + */ + Optional finishingStatus(); + + /** + * The injection plan that was found or determined, key'ed by each element's {@link ServiceProvider#id()}. + * + * @return the resolved injection plan map + */ + Map injectionPlans(); + + /** + * The dependencies that were resolved or loaded, key'ed by each element's {@link ServiceProvider#id()}. + * + * @return the resolved dependency map + */ + Map resolvedDependencies(); + + /** + * Set to true if the injection plan in {@link #resolvedDependencies()} has been resolved and can be "trusted" as being + * complete and accurate. + * + * @return true if was resolved + */ + boolean wasResolved(); + + /** + * Any throwable/exceptions that were observed during activation. + * + * @return any captured error + */ + Optional error(); + + /** + * Returns true if this result is finished. + * + * @return true if finished + */ + default boolean finished() { + Future f = finishedActivationResult().orElse(null); + return (f == null || f.isDone()); + } + + /** + * Returns true if this result was successful. + * + * @return true if successful + */ + default boolean success() { + return finishingStatus().orElse(null) != ActivationStatus.FAILURE; + } + + /** + * Returns true if this result was unsuccessful. + * + * @return true if unsuccessful + */ + default boolean failure() { + return !success(); + } + + /** + * Creates a successful result. + * + * @param serviceProvider the service provider + * @return the result + */ + static ActivationResult createSuccess(ServiceProvider serviceProvider) { + Phase phase = serviceProvider.currentActivationPhase(); + return DefaultActivationResult.builder() + .serviceProvider(serviceProvider) + .startingActivationPhase(phase) + .finishingActivationPhase(phase) + .targetActivationPhase(phase) + .finishingStatus(ActivationStatus.SUCCESS) + .build(); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java b/pico/api/src/main/java/io/helidon/pico/ActivationStatus.java similarity index 95% rename from pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java rename to pico/api/src/main/java/io/helidon/pico/ActivationStatus.java index b1e12fb21b7..bce05ad9d7f 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java +++ b/pico/api/src/main/java/io/helidon/pico/ActivationStatus.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/pico/pico/src/main/java/io/helidon/pico/Activator.java b/pico/api/src/main/java/io/helidon/pico/Activator.java similarity index 80% rename from pico/pico/src/main/java/io/helidon/pico/Activator.java rename to pico/api/src/main/java/io/helidon/pico/Activator.java index 4d2c5129641..a7553480179 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Activator.java +++ b/pico/api/src/main/java/io/helidon/pico/Activator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,10 +18,10 @@ /** * 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 ServiceProvider}'s manage service instance from {@link Phase#PENDING} + * through {@link Phase#POST_CONSTRUCTING} (i.e., including any * {@link PostConstructMethod} invocations, etc.), and finally into the - * {@link ActivationPhase#ACTIVE} phase. + * {@link Phase#ACTIVE} phase. *

        * Assumption: *

          @@ -32,18 +32,16 @@ *
        * Activation includes: *
          - *
        1. Management of the service's {@link ActivationPhase}.
        2. + *
        3. Management of the service's {@link Phase}.
        4. *
        5. Control over creation (i.e., invoke the constructor non-reflectively).
        6. *
        7. Control over gathering the service requisite dependencies (ctor, field, setters) and optional activation of those.
        8. *
        9. Invocation of any {@link PostConstructMethod}.
        10. *
        11. Responsible to logging to the {@link ActivationLog} - see {@link PicoServices#activationLog()}.
        12. *
        * - * @param the managed service type being activated * @see DeActivator */ -@Contract -public interface Activator { +public interface Activator { /** * Activate a managed service/provider. @@ -51,5 +49,6 @@ public interface Activator { * @param activationRequest activation request * @return the result of the activation */ - ActivationResult activate(ActivationRequest activationRequest); + ActivationResult activate(ActivationRequest activationRequest); + } diff --git a/pico/pico/src/main/java/io/helidon/pico/Application.java b/pico/api/src/main/java/io/helidon/pico/Application.java similarity index 91% rename from pico/pico/src/main/java/io/helidon/pico/Application.java rename to pico/api/src/main/java/io/helidon/pico/Application.java index ada9c3d3819..0361ebda5d3 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Application.java +++ b/pico/api/src/main/java/io/helidon/pico/Application.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -28,7 +28,7 @@ * @see Module */ @Contract -public interface Application extends Named { +public interface Application extends OptionallyNamed { /** * Called by the provider implementation at bootstrapping time to bind all injection plans to each and every service provider. diff --git a/pico/pico/src/main/java/io/helidon/pico/Bootstrap.java b/pico/api/src/main/java/io/helidon/pico/Bootstrap.java similarity index 56% rename from pico/pico/src/main/java/io/helidon/pico/Bootstrap.java rename to pico/api/src/main/java/io/helidon/pico/Bootstrap.java index b62912256cc..83dc4f37766 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Bootstrap.java +++ b/pico/api/src/main/java/io/helidon/pico/Bootstrap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -25,17 +25,27 @@ * This is the bootstrap needed to provide to {@code Pico} initialization. * * @see io.helidon.pico.spi.PicoServicesProvider + * @see io.helidon.pico.PicoServices#globalBootstrap() */ @Builder public interface Bootstrap { /** * Provides the base primordial bootstrap configuration to the {@link io.helidon.pico.spi.PicoServicesProvider}. - * The provider will then bootstrap its {@link io.helidon.pico.PicoServicesConfig} to any provided bootstrap - * configuration instance provided, etc. + * The provider will then bootstrap {@link io.helidon.pico.PicoServices} using this bootstrap instance. + * then default values will be used accordingly. * - * @return the bootstrap configuration + * @return the bootstrap helidon configuration */ Optional config(); + /** + * In certain conditions Pico services should be initialized but not started (i.e., avoiding calls to {@code PostConstruct} + * etc.). This can be used in special cases where the normal Pico startup should limit lifecycle up to a given phase. Normally + * one should not use this feature - it is mainly used in Pico tooling (e.g., the pico-maven-plugin). + * + * @return the phase to stop at during lifecycle + */ + Optional limitRuntimePhase(); + } diff --git a/pico/api/src/main/java/io/helidon/pico/CallingContext.java b/pico/api/src/main/java/io/helidon/pico/CallingContext.java new file mode 100644 index 00000000000..e3e6de172b0 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/CallingContext.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 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.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.builder.Builder; + +import static io.helidon.pico.PicoServicesConfig.TAG_DEBUG; + +/** + * For internal use only to Helidon. Applicable when {@link io.helidon.pico.PicoServicesConfig#TAG_DEBUG} is enabled. + */ +@Builder(interceptor = CallingContext.BuilderInterceptor.class) +public abstract class CallingContext { + + /** + * Helpful hint to give developers needing to see more debug info. + */ + public static final String DEBUG_HINT = "use the (-D and/or -A) tag '" + TAG_DEBUG + "=true' to see full trace output."; + + private static CallingContext defaultCallingContext; + + /** + * This needs to be private since a generated builder will be extending this. + */ + protected CallingContext() { + } + + @Override + public String toString() { + String prettyPrintStackTrace = String.join("\n", stackTraceOf(trace())); + return "module name: " + moduleName() + "; thread name: " + threadName() + + "; trace:\n" + prettyPrintStackTrace; + } + + /** + * Only populated when {@link io.helidon.pico.PicoServicesConfig#TAG_DEBUG} is set. + * + * @return the stack trace for who initialized + */ + public abstract StackTraceElement[] trace(); + + /** + * Only populated when {@link io.helidon.pico.PicoServicesConfig#TAG_MODULE_NAME} is set. + * + * @return the module name + */ + public abstract Optional moduleName(); + + /** + * The thread that created the calling context. + * + * @return thread creating the calling context + */ + public abstract String threadName(); + + /** + * Returns a stack trace as a list of strings. + * + * @param trace the trace + * @return the list of strings for the stack trace + */ + static List stackTraceOf(StackTraceElement[] trace) { + List result = new ArrayList<>(); + for (StackTraceElement e : trace) { + result.add(e.toString()); + } + return result; + } + + /** + * Sets the default global calling context. + * + * @param callingContext the default global context + * @param throwIfAlreadySet should an exception be thrown if the global calling context was already set + * @throws java.lang.IllegalStateException if context was already set and the throwIfAlreadySet is active + */ + public static void globalCallingContext(CallingContext callingContext, + boolean throwIfAlreadySet) { + Objects.requireNonNull(callingContext); + + CallingContext global = defaultCallingContext; + if (global != null && throwIfAlreadySet) { + CallingContext currentCallingContext = CallingContextFactory.create(true).orElseThrow(); + throw new IllegalStateException("Expected to be the owner of the calling context. This context is: " + + currentCallingContext + "\n Context previously set was: " + global); + } + + CallingContext.defaultCallingContext = callingContext; + } + + /** + * Convenience method for producing an error message that may involve advising the user to apply a debug mode. + * + * @param callingContext the calling context (caller can be using a custom calling context, which is why we accept it here + * instead of using the global one) + * @param msg the base message to display + * @return the message appropriate for any exception being thrown + */ + public static String toErrorMessage(CallingContext callingContext, + String msg) { + return msg + " - previous calling context: " + callingContext; + } + + /** + * Convenience method for producing an error message that may involve advising the user to apply a debug mode. Use + * {@link #toErrorMessage(CallingContext, String)} iinstead f a calling context is available. + * + * @param msg the base message to display + * @return the message appropriate for any exception being thrown + * @see #toErrorMessage(CallingContext, String) + */ + public static String toErrorMessage(String msg) { + return msg + " - " + DEBUG_HINT; + } + + + static class BuilderInterceptor implements io.helidon.builder.BuilderInterceptor { + @Override + public DefaultCallingContext.Builder intercept(DefaultCallingContext.Builder target) { + if (target.threadName() == null) { + target.threadName(Thread.currentThread().getName()); + } + return target; + } + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/CallingContextFactory.java b/pico/api/src/main/java/io/helidon/pico/CallingContextFactory.java new file mode 100644 index 00000000000..3c4e08109ed --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/CallingContextFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 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; + +/** + * Factory for creating {@link CallingContext} and builders for the calling context. + * After a calling context builder is created, it should be amended with as much contextual information as possible, and then + * optionally set globally using {@link CallingContext#globalCallingContext(CallingContext, boolean)}. + */ +public class CallingContextFactory { + + private CallingContextFactory() { + } + + /** + * Creates a new calling context instance. Normally this method will return a context optionally only when debug is + * enabled. This behavior can be overridden by passing the {@code force=true} flag. + * + * @param force forces the creation of the calling context even when debug is disabled + * @return a new calling context if there is an indication that debug mode is enabled, or if the force flag is set + * @see io.helidon.pico.PicoServices#isDebugEnabled() + */ + public static Optional create(boolean force) { + Optional optBuilder = createBuilder(force); + return optBuilder.map(DefaultCallingContext.Builder::build); + + } + + /** + * Creates a new calling context builder instance. Normally this method will return a context builder optionally only when + * debug is enabled. This behavior can be overridden by passing the {@code force=true} flag. + * + * @param force forces the creation of the calling context even when debug is disabled + * @return a new calling context builder if there is an indication that debug mode is enabled, or if the force flag is set + * @see io.helidon.pico.PicoServices#isDebugEnabled() + */ + public static Optional createBuilder(boolean force) { + boolean createIt = (force || PicoServices.isDebugEnabled()); + if (!createIt) { + return Optional.empty(); + } + + return Optional.of(DefaultCallingContext.builder() + .trace(new RuntimeException().getStackTrace())); + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/CommonQualifiers.java b/pico/api/src/main/java/io/helidon/pico/CommonQualifiers.java new file mode 100644 index 00000000000..362a894a606 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/CommonQualifiers.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 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.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; + +import jakarta.inject.Named; + +/** + * Commonly used {@link QualifierAndValue} types. + */ +public final class CommonQualifiers { + + /** + * 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("*"); + + private CommonQualifiers() { + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java b/pico/api/src/main/java/io/helidon/pico/ContextualServiceQuery.java similarity index 50% rename from pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java rename to pico/api/src/main/java/io/helidon/pico/ContextualServiceQuery.java index 63b23da1150..cc5553d1b41 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java +++ b/pico/api/src/main/java/io/helidon/pico/ContextualServiceQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,15 +16,16 @@ package io.helidon.pico; +import java.util.Objects; import java.util.Optional; import io.helidon.builder.Builder; /** - * Combines the {@link io.helidon.pico.ServiceInfo} criteria along with the {@link io.helidon.pico.InjectionPointInfo} context + * Combines the {@link ServiceInfo} criteria along with the {@link InjectionPointInfo} context * that the query applies to. * - * @see io.helidon.pico.InjectionPointProvider + * @see InjectionPointProvider */ @Builder public interface ContextualServiceQuery { @@ -34,14 +35,14 @@ public interface ContextualServiceQuery { * * @return the service info criteria */ - ServiceInfoCriteria serviceInfo(); + ServiceInfoCriteria serviceInfoCriteria(); /** - * The injection point context this search applies to. + * Optionally, the injection point context this search applies to. * - * @return the injection point context info + * @return the optional injection point context info */ - Optional ipInfo(); + Optional injectionPointInfo(); /** * Set to true if there is an expectation that there is at least one match result from the search. @@ -50,4 +51,21 @@ public interface ContextualServiceQuery { */ boolean expected(); + /** + * Creates a contextual service query given the injection point info. + * + * @param ipInfo the injection point info + * @param expected true if the query is expected to at least have a single match + * @return the query + */ + static ContextualServiceQuery create(InjectionPointInfo ipInfo, + boolean expected) { + Objects.requireNonNull(ipInfo); + return DefaultContextualServiceQuery.builder() + .expected(expected) + .injectionPointInfo(ipInfo) + .serviceInfoCriteria(ipInfo.dependencyToServiceInfo()) + .build(); + } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/Contract.java b/pico/api/src/main/java/io/helidon/pico/Contract.java similarity index 69% rename from pico/pico/src/main/java/io/helidon/pico/Contract.java rename to pico/api/src/main/java/io/helidon/pico/Contract.java index 70c10b499f2..ef3e0902cf8 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Contract.java +++ b/pico/api/src/main/java/io/helidon/pico/Contract.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -22,11 +22,11 @@ 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. + * The {@code Contract} annotation is used to relay significance to the type that it annotates. 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 placed on interface types, it can also be placed on abstract and concrete class as well. The main point is that + * a {@code 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 @@ -36,7 +36,7 @@ * @see io.helidon.pico.ServiceInfo#externalContractsImplemented() */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Target(java.lang.annotation.ElementType.TYPE) public @interface Contract { diff --git a/pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java b/pico/api/src/main/java/io/helidon/pico/DeActivationRequest.java similarity index 53% rename from pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java rename to pico/api/src/main/java/io/helidon/pico/DeActivationRequest.java index 67facfacbe2..9cc72e0dcd3 100644 --- a/pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java +++ b/pico/api/src/main/java/io/helidon/pico/DeActivationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,41 +17,39 @@ package io.helidon.pico; import io.helidon.builder.Builder; +import io.helidon.common.LazyValue; import io.helidon.config.metadata.ConfiguredOption; /** - * Request to {@link io.helidon.pico.DeActivator#deactivate(DeActivationRequest)}. - * - * @param type to deactivate + * Request to deactivate a {@link io.helidon.pico.ServiceProvider}. */ @Builder -public interface DeActivationRequest { +public abstract class 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(); + DeActivationRequest() { } /** - * Service provider responsible for invoking deactivate. + * Whether to throw an exception on failure, or return it as part of the result. * - * @return service provider + * @return throw on failure */ - ServiceProvider serviceProvider(); + @ConfiguredOption("true") + public abstract boolean throwIfError(); /** - * Whether to throw an exception on failure, or return it as part of the result. + * A standard/default deactivation request, without any additional options placed on the request. * - * @return throw on failure + * @return a standard/default deactivation request. */ - @ConfiguredOption("true") - boolean throwOnFailure(); + public static DeActivationRequest defaultDeactivationRequest() { + return Init.DEFAULT.get(); + } + + + static class Init { + static final LazyValue DEFAULT = + LazyValue.create(() -> DefaultDeActivationRequest.builder().build()); + } } diff --git a/pico/pico/src/main/java/io/helidon/pico/DeActivator.java b/pico/api/src/main/java/io/helidon/pico/DeActivator.java similarity index 73% rename from pico/pico/src/main/java/io/helidon/pico/DeActivator.java rename to pico/api/src/main/java/io/helidon/pico/DeActivator.java index 6978ef3ccae..57841f117d5 100644 --- a/pico/pico/src/main/java/io/helidon/pico/DeActivator.java +++ b/pico/api/src/main/java/io/helidon/pico/DeActivator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,15 +18,13 @@ /** * DeActivators are responsible for lifecycle, transitioning a {@link ServiceProvider} through its - * {@link io.helidon.pico.ActivationPhase}'s, notably including any + * {@link Phase}'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}. + * {@link Phase#DESTROYED} phase. These phase transitions are the inverse of {@link Activator}. * - * @param the type to deactivate * @see Activator */ -@Contract -public interface DeActivator { +public interface DeActivator { /** * Deactivate a managed service. This will trigger any {@link jakarta.annotation.PreDestroy} method on the @@ -35,5 +33,6 @@ public interface DeActivator { * @param request deactivation request * @return the result */ - ActivationResult deactivate(DeActivationRequest request); + ActivationResult deactivate(DeActivationRequest request); + } diff --git a/pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java b/pico/api/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java similarity index 84% rename from pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java rename to pico/api/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java index b4c22725b25..ea0a659f3d1 100644 --- a/pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java +++ b/pico/api/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,12 +17,13 @@ package io.helidon.pico; import java.lang.annotation.Annotation; +import java.util.Map; 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; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; /** * Describes a {@link jakarta.inject.Qualifier} type annotation associated with a service being provided or dependant upon. @@ -31,16 +32,6 @@ 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. * @@ -59,7 +50,7 @@ protected DefaultQualifierAndValue(Builder b) { */ public static DefaultQualifierAndValue createNamed(String name) { Objects.requireNonNull(name); - return (DefaultQualifierAndValue) builder().typeName(NAMED).value(name).build(); + return (DefaultQualifierAndValue) builder().typeName(CommonQualifiers.NAMED).value(name).build(); } /** @@ -114,7 +105,21 @@ public static DefaultQualifierAndValue create(TypeName qualifierType, String val } /** - * Converts from an {@link io.helidon.pico.types.AnnotationAndValue} to a {@link QualifierAndValue}. + * Creates a qualifier. + * + * @param qualifierType the qualifier + * @param vals the values + * @return qualifier + */ + public static DefaultQualifierAndValue create(TypeName qualifierType, Map vals) { + return (DefaultQualifierAndValue) builder() + .typeName(qualifierType) + .values(vals) + .build(); + } + + /** + * Converts from an {@link AnnotationAndValue} to a {@link QualifierAndValue}. * * @param annotationAndValue the annotation and value * @return the qualifier and value equivalent diff --git a/pico/api/src/main/java/io/helidon/pico/DependenciesInfo.java b/pico/api/src/main/java/io/helidon/pico/DependenciesInfo.java new file mode 100644 index 00000000000..17c0ce353ff --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/DependenciesInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, 2023 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.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; + +/** + * Represents a per {@link ServiceInfo} mapping of {@link DependencyInfo}'s. These are typically assigned to a + * {@link ServiceProvider} via compile-time code generation within the Pico framework. + */ +@Builder +public interface DependenciesInfo { + + /** + * Represents the set of dependencies for each {@link ServiceInfo}. + * + * @return map from the service info to its dependencies + */ + @Singular("serviceInfoDependency") + Map> serviceInfoDependencies(); + + /** + * Optionally, the service type name aggregating {@link #allDependencies()}. + * + * @return the optional service type name for which these dependencies belong + */ + Optional fromServiceTypeName(); + + /** + * Represents a flattened set of all dependencies. + * + * @return the flattened set of all dependencies + */ + default Set allDependencies() { + Set all = new TreeSet<>(comparator()); + serviceInfoDependencies().values() + .forEach(all::addAll); + return all; + } + + /** + * Represents the list of all dependencies for a given injection point element name ordered by the element position. + * + * @param elemName the element name of the injection point + * @return the list of all dependencies got a given element name of a given injection point + */ + default List allDependenciesFor(String elemName) { + Objects.requireNonNull(elemName); + return allDependencies().stream() + .flatMap(dep -> dep.injectionPointDependencies().stream() + .filter(ipi -> elemName.equals(ipi.elementName())) + .map(ipi -> DefaultDependencyInfo.toBuilder(dep) + .injectionPointDependencies(Set.of(ipi)) + .build())) + .sorted(comparator()) + .collect(Collectors.toList()); + } + + /** + * Comparator appropriate for {@link DependencyInfo}. + */ + class Comparator implements java.util.Comparator, Serializable { + private Comparator() { + } + + @Override + public int compare(DependencyInfo o1, + DependencyInfo o2) { + InjectionPointInfo ipi1 = o1.injectionPointDependencies().iterator().next(); + InjectionPointInfo ipi2 = o2.injectionPointDependencies().iterator().next(); + + java.util.Comparator idComp = java.util.Comparator.comparing(InjectionPointInfo::baseIdentity); + java.util.Comparator posComp = java.util.Comparator.comparing(Comparator::elementOffsetOf); + + return idComp.thenComparing(posComp).compare(ipi1, ipi2); + } + + private static int elementOffsetOf(InjectionPointInfo ipi) { + return ipi.elementOffset().orElse(0); + } + } + + /** + * Provides a comparator appropriate for {@link io.helidon.pico.DependencyInfo}. + * + * @return a comparator for dependency info + */ + static java.util.Comparator comparator() { + return new Comparator(); + } + + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java b/pico/api/src/main/java/io/helidon/pico/DependencyInfo.java similarity index 68% rename from pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java rename to pico/api/src/main/java/io/helidon/pico/DependencyInfo.java index 9e7b417a50f..65c75bb965e 100644 --- a/pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java +++ b/pico/api/src/main/java/io/helidon/pico/DependencyInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,9 +16,11 @@ package io.helidon.pico; +import java.util.Optional; import java.util.Set; import io.helidon.builder.Builder; +import io.helidon.builder.Singular; /** * Aggregates the set of {@link InjectionPointInfo}'s that are dependent upon a specific and common @@ -32,13 +34,22 @@ public interface DependencyInfo { * * @return the service info dependency */ - ServiceInfo dependencyTo(); + ServiceInfoCriteria dependencyTo(); /** * The set of injection points that depends upon {@link #dependencyTo()}. * * @return the set of dependencies */ + @Singular("injectionPointDependency") Set injectionPointDependencies(); + /** + * The {@link io.helidon.pico.ServiceProvider} that this dependency is optional resolved and bound to. All dependencies + * from {@link #injectionPointDependencies()} will be bound to this resolution. + * + * @return the optional resolved and bounded service provider + */ + Optional> resolvedTo(); + } diff --git a/pico/pico/src/main/java/io/helidon/pico/ElementInfo.java b/pico/api/src/main/java/io/helidon/pico/ElementInfo.java similarity index 83% rename from pico/pico/src/main/java/io/helidon/pico/ElementInfo.java rename to pico/api/src/main/java/io/helidon/pico/ElementInfo.java index 40df5a34650..4646174eb61 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ElementInfo.java +++ b/pico/api/src/main/java/io/helidon/pico/ElementInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,7 +20,8 @@ import java.util.Set; import io.helidon.builder.Builder; -import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.builder.Singular; +import io.helidon.common.types.AnnotationAndValue; /** * Abstractly describes method or field elements of a managed service type (i.e., fields, constructors, injectable methods, etc.). @@ -113,6 +114,13 @@ enum Access { */ Optional elementOffset(); + /** + * If the element is a method or constructor then this is the total argument count for that method. + * + * @return total argument count + */ + Optional elementArgs(); + /** * True if the injection point is static. * @@ -132,6 +140,15 @@ enum Access { * * @return the annotations on this element */ + @Singular Set annotations(); + /** + * The qualifier type annotations on this element. + * + * @return the qualifier type annotations on this element + */ + @Singular + Set qualifiers(); + } diff --git a/pico/api/src/main/java/io/helidon/pico/Event.java b/pico/api/src/main/java/io/helidon/pico/Event.java new file mode 100644 index 00000000000..48bfee6fbf5 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/Event.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 lifecycle activation event. + */ +public enum Event { + + /** + * Starting. + */ + STARTING, + + /** + * Finished. + */ + FINISHED, + + /** + * Other, informational. + */ + OTHER + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java b/pico/api/src/main/java/io/helidon/pico/ExternalContracts.java similarity index 94% rename from pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java rename to pico/api/src/main/java/io/helidon/pico/ExternalContracts.java index 9001c25243a..5041d788b5d 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java +++ b/pico/api/src/main/java/io/helidon/pico/ExternalContracts.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -28,7 +28,7 @@ * from a 3rd party library provider. */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Target(java.lang.annotation.ElementType.TYPE) public @interface ExternalContracts { diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionException.java b/pico/api/src/main/java/io/helidon/pico/InjectionException.java similarity index 75% rename from pico/pico/src/main/java/io/helidon/pico/InjectionException.java rename to pico/api/src/main/java/io/helidon/pico/InjectionException.java index 21c07c1f206..02162885f9c 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InjectionException.java +++ b/pico/api/src/main/java/io/helidon/pico/InjectionException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,6 +16,7 @@ package io.helidon.pico; +import java.util.Objects; import java.util.Optional; /** @@ -27,9 +28,9 @@ public class InjectionException extends PicoServiceProviderException { /** * The optional activation log (configure to enabled). * - * @see io.helidon.pico.PicoServicesConfig + * @see PicoServicesConfig#activationLogs() */ - private final ActivationLog activationLog; + private ActivationLog activationLog; /** * Injection, or a required service lookup related exception. @@ -38,7 +39,6 @@ public class InjectionException extends PicoServiceProviderException { */ public InjectionException(String msg) { super(msg); - this.activationLog = null; } /** @@ -48,25 +48,21 @@ public InjectionException(String msg) { * @param cause the root cause * @param serviceProvider the service provider */ - public InjectionException(String msg, Throwable cause, ServiceProvider serviceProvider) { + 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; + ServiceProvider serviceProvider) { + super(msg, serviceProvider); } /** @@ -78,4 +74,15 @@ public Optional activationLog() { return Optional.ofNullable(activationLog); } + /** + * Sets the activation log on this exception instance. + * + * @param log the activation log + * @return this exception instance + */ + public InjectionException activationLog(ActivationLog log) { + this.activationLog = Objects.requireNonNull(log); + return this; + } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java b/pico/api/src/main/java/io/helidon/pico/InjectionPointInfo.java similarity index 78% rename from pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java rename to pico/api/src/main/java/io/helidon/pico/InjectionPointInfo.java index 5df97f2b3d8..cf3c9fe41a0 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java +++ b/pico/api/src/main/java/io/helidon/pico/InjectionPointInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,8 +16,6 @@ package io.helidon.pico; -import java.util.Set; - import io.helidon.builder.Builder; /** @@ -27,14 +25,14 @@ 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. + * The identity (aka id) for this injection point. The id 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(); + String id(); /** * The base identifying name for this injection point. If the element represents a function, then the function arguments @@ -44,13 +42,6 @@ public interface InjectionPointInfo extends ElementInfo { */ 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}. * @@ -73,9 +64,10 @@ public interface InjectionPointInfo extends ElementInfo { boolean providerWrapped(); /** - * The dependency this is dependent upon. + * The service info criteria/dependency this is dependent upon. * - * @return The service info we are dependent upon. + * @return the service info criteria we are dependent upon */ - ServiceInfo dependencyToServiceInfo(); + ServiceInfoCriteria dependencyToServiceInfo(); + } diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java b/pico/api/src/main/java/io/helidon/pico/InjectionPointProvider.java similarity index 85% rename from pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java rename to pico/api/src/main/java/io/helidon/pico/InjectionPointProvider.java index 09b2945dd30..d35303902fd 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java +++ b/pico/api/src/main/java/io/helidon/pico/InjectionPointProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -33,6 +33,7 @@ * @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. * @@ -42,7 +43,7 @@ public interface InjectionPointProvider extends Provider { @Override default T get() { return first(PicoServices.SERVICE_QUERY_REQUIRED) - .orElseThrow(() -> new PicoException("Could not resolve a match for " + this)); + .orElseThrow(() -> couldNotFindMatch()); } /** @@ -66,4 +67,12 @@ default List list(ContextualServiceQuery query) { return first(query).map(List::of).orElseGet(List::of); } + @SuppressWarnings("rawtypes") + private PicoException couldNotFindMatch() { + if (this instanceof ServiceProvider) { + return new PicoServiceProviderException("expected to find a match", (ServiceProvider) this); + } + return new PicoException("expected to find a match for " + this); + } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/Injector.java b/pico/api/src/main/java/io/helidon/pico/Injector.java similarity index 63% rename from pico/pico/src/main/java/io/helidon/pico/Injector.java rename to pico/api/src/main/java/io/helidon/pico/Injector.java index e6636d6b1a8..c19b81adf74 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Injector.java +++ b/pico/api/src/main/java/io/helidon/pico/Injector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -24,10 +24,6 @@ * 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 @@ -55,39 +51,31 @@ enum Strategy { /** * 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. + * {@link Phase#ACTIVE}. * * @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 + * @param opts the injector options + * @param the managed service 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; + ActivationResult activateInject(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException; /** - * Called to deactivate a managed service or service provider, putting it into {@link ActivationPhase#DESTROYED}. + * Called to deactivate a managed service or service provider, putting it into {@link Phase#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 + * @param opts the injector options + * @param the managed service 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; + ActivationResult deactivate(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException; } diff --git a/pico/api/src/main/java/io/helidon/pico/InjectorOptions.java b/pico/api/src/main/java/io/helidon/pico/InjectorOptions.java new file mode 100644 index 00000000000..6badfe72e40 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/InjectorOptions.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022, 2023 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.builder.Builder; +import io.helidon.builder.BuilderInterceptor; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Provides optional, contextual tunings to the {@link Injector}. + * + * @see Injector + */ +@Builder(interceptor = InjectorOptions.Interceptor.class) +public abstract class InjectorOptions { + + InjectorOptions() { + } + + /** + * The strategy the injector should apply. The default is {@link Injector.Strategy#ANY}. + * + * @return the injector strategy to use + */ + @ConfiguredOption("ANY") + public abstract Injector.Strategy strategy(); + + /** + * Optionally, customized activator options to use for the {@link io.helidon.pico.Activator}. + * + * @return activator options, or leave blank to use defaults + */ + public abstract ActivationRequest activationRequest(); + + + /** + * This will ensure that the activation request is populated. + */ + static class Interceptor implements BuilderInterceptor { + Interceptor() { + } + + @Override + public DefaultInjectorOptions.Builder intercept(DefaultInjectorOptions.Builder target) { + if (target.activationRequest() == null) { + target.activationRequest(PicoServices.createDefaultActivationRequest()); + } + return target; + } + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Intercepted.java b/pico/api/src/main/java/io/helidon/pico/Intercepted.java similarity index 92% rename from pico/pico/src/main/java/io/helidon/pico/Intercepted.java rename to pico/api/src/main/java/io/helidon/pico/Intercepted.java index eb984cf7b39..9578a262588 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Intercepted.java +++ b/pico/api/src/main/java/io/helidon/pico/Intercepted.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -30,7 +30,7 @@ * @see io.helidon.pico.Interceptor */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Inherited @Qualifier @Target(java.lang.annotation.ElementType.TYPE) diff --git a/pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java b/pico/api/src/main/java/io/helidon/pico/InterceptedTrigger.java similarity index 85% rename from pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java rename to pico/api/src/main/java/io/helidon/pico/InterceptedTrigger.java index 60fa62a87dc..4e549bd7bc2 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java +++ b/pico/api/src/main/java/io/helidon/pico/InterceptedTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -24,9 +24,12 @@ /** * Meta-annotation for an annotation that will trigger services annotated with it to become intercepted. + * + * @see io.helidon.pico.Interceptor + * @see io.helidon.pico.Intercepted */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Target(ElementType.ANNOTATION_TYPE) public @interface InterceptedTrigger { diff --git a/pico/pico/src/main/java/io/helidon/pico/Interceptor.java b/pico/api/src/main/java/io/helidon/pico/Interceptor.java similarity index 81% rename from pico/pico/src/main/java/io/helidon/pico/Interceptor.java rename to pico/api/src/main/java/io/helidon/pico/Interceptor.java index 103f78ac081..26f5f48bfcf 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Interceptor.java +++ b/pico/api/src/main/java/io/helidon/pico/Interceptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -25,14 +25,15 @@ public interface Interceptor { /** * Called during interception of the target V. The implementation typically should finish with the call to - * {@link Interceptor.Chain#proceed()}. + * {@link Interceptor.Chain#proceed}. * * @param ctx the invocation context * @param chain the chain to call proceed on + * @param args the arguments to the call * @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); + V proceed(InvocationContext ctx, Chain chain, Object... args); /** @@ -44,9 +45,10 @@ interface Chain { /** * Call the next interceptor in line, or finishing with the call to the service provider. * - * @return the result of the call. + * @param args the arguments pass + * @return the result of the call */ - V proceed(); + V proceed(Object[] args); } } diff --git a/pico/api/src/main/java/io/helidon/pico/InternalBootstrap.java b/pico/api/src/main/java/io/helidon/pico/InternalBootstrap.java new file mode 100644 index 00000000000..72cb603ce69 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/InternalBootstrap.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 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.builder.Builder; + +/** + * Internal bootstrap is what we store when {@link io.helidon.pico.PicoServices#globalBootstrap(Bootstrap)} is used. + */ +@Builder +abstract class InternalBootstrap { + + /** + * The user established bootstrap. + * + * @return user establised bootstrap + */ + abstract Bootstrap bootStrap(); + + /** + * Only populated when {@link io.helidon.pico.PicoServicesConfig#TAG_DEBUG} is set. + * + * @return the calling context + */ + abstract Optional callingContext(); + + /** + * Creates an internal bootstrap. + * See the notes in {@link CallingContextFactory#create(boolean)}. + * + * @param bootstrap Optionally, the user-defined bootstrap - one will be created if passed as null + * @param callingContext Optionally, the calling context if known + * @return a newly created internal bootstrap instance + */ + static InternalBootstrap create(Bootstrap bootstrap, + CallingContext callingContext) { + if (callingContext == null) { + callingContext = CallingContextFactory.create(false).orElse(null); + } + return DefaultInternalBootstrap.builder() + .bootStrap((bootstrap == null) ? DefaultBootstrap.builder().build() : bootstrap) + .callingContext(Optional.ofNullable(callingContext)) + .build(); + } + + /** + * Creates a calling context when nothing is known from the caller's perspective. + * + * @return a newly created internal bootstrap instance + */ + static InternalBootstrap create() { + return create(null, null); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InvocationContext.java b/pico/api/src/main/java/io/helidon/pico/InvocationContext.java similarity index 71% rename from pico/pico/src/main/java/io/helidon/pico/InvocationContext.java rename to pico/api/src/main/java/io/helidon/pico/InvocationContext.java index c037f9dc6d4..c178ad4b9af 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InvocationContext.java +++ b/pico/api/src/main/java/io/helidon/pico/InvocationContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,22 +18,27 @@ import java.util.List; import java.util.Map; +import java.util.Optional; -import io.helidon.pico.types.AnnotationAndValue; -import io.helidon.pico.types.TypeName; -import io.helidon.pico.types.TypedElementName; +import io.helidon.builder.Builder; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; + +import jakarta.inject.Provider; /** * Used by {@link Interceptor}. */ +@Builder public interface InvocationContext { /** - * The root service provider being intercepted. + * The service provider being intercepted. * - * @return the root service provider being intercepted + * @return the service provider being intercepted */ - ServiceProvider rootServiceProvider(); + ServiceProvider serviceProvider(); /** * The service type name for the root service provider. @@ -59,16 +64,16 @@ public interface InvocationContext { /** * The method/element argument info. * - * @return the method/element argument info. + * @return the method/element argument info */ - TypedElementName[] elementArgInfo(); + Optional elementArgInfo(); /** - * The arguments to the method. + * The interceptor chain. * - * @return the read/write method/element arguments + * @return the interceptor chain */ - Object[] elementArgs(); + List> interceptors(); /** * The contextual info that can be shared between interceptors. diff --git a/pico/pico/src/main/java/io/helidon/pico/InvocationException.java b/pico/api/src/main/java/io/helidon/pico/InvocationException.java similarity index 95% rename from pico/pico/src/main/java/io/helidon/pico/InvocationException.java rename to pico/api/src/main/java/io/helidon/pico/InvocationException.java index 99a6538852a..c26f5bbeba5 100644 --- a/pico/pico/src/main/java/io/helidon/pico/InvocationException.java +++ b/pico/api/src/main/java/io/helidon/pico/InvocationException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/pico/api/src/main/java/io/helidon/pico/Metrics.java b/pico/api/src/main/java/io/helidon/pico/Metrics.java new file mode 100644 index 00000000000..198c71f2698 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/Metrics.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, 2023 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.builder.Builder; + +/** + * Pico Metrics. + */ +@Builder +public interface Metrics { + + /** + * The total service count in the registry. + * + * @return total service count + */ + Optional serviceCount(); + + /** + * The total number of {@code Services::lookup()} calls since jvm start, or since last reset. + * + * @return lookup count + */ + Optional lookupCount(); + + /** + * The total number of {@code Services::lookup()} calls that were attempted against the lookup cache. This will be empty + * if caching is disabled. + * + * @see io.helidon.pico.PicoServicesConfig + * @return cache lookup count + */ + Optional cacheLookupCount(); + + /** + * The total number of {@code Services:lookup()} calls that were successfully resolved via cache. This will be a value less + * than or equal to {@link #cacheLookupCount()}. + * + * @return cache hit count + */ + Optional cacheHitCount(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Module.java b/pico/api/src/main/java/io/helidon/pico/Module.java similarity index 84% rename from pico/pico/src/main/java/io/helidon/pico/Module.java rename to pico/api/src/main/java/io/helidon/pico/Module.java index 906efb40574..d35f3dad139 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Module.java +++ b/pico/api/src/main/java/io/helidon/pico/Module.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,6 +16,8 @@ package io.helidon.pico; +import java.util.Optional; + /** * Provides aggregation of services to the "containing" (jar) module. *

        @@ -27,7 +29,7 @@ * @see Application */ @Contract -public interface Module extends Named { +public interface Module extends OptionallyNamed { /** * Called by the provider implementation at bootstrapping time to bind all services / service providers to the @@ -37,4 +39,9 @@ public interface Module extends Named { */ void configure(ServiceBinder binder); + @Override + default Optional named() { + return Optional.empty(); + } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/Named.java b/pico/api/src/main/java/io/helidon/pico/OptionallyNamed.java similarity index 76% rename from pico/pico/src/main/java/io/helidon/pico/Named.java rename to pico/api/src/main/java/io/helidon/pico/OptionallyNamed.java index 2e50c17a44f..0994ab3dfb8 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Named.java +++ b/pico/api/src/main/java/io/helidon/pico/OptionallyNamed.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,19 +19,17 @@ import java.util.Optional; /** - * Provides a means to identify if the instance is named. + * Provides a means to identify if the instance is optionally named. * * @see jakarta.inject.Named */ -public interface Named { +public interface OptionallyNamed { /** * The optional name for this instance. * - * @return the name associated with this instance or empty if not available or known. + * @return the name associated with this instance or empty if not available or known */ - default Optional name() { - return Optional.empty(); - } + Optional named(); } diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java b/pico/api/src/main/java/io/helidon/pico/Phase.java similarity index 78% rename from pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java rename to pico/api/src/main/java/io/helidon/pico/Phase.java index 325eef96edd..7c5a3d299a6 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java +++ b/pico/api/src/main/java/io/helidon/pico/Phase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,7 +19,7 @@ /** * Forms a progression of full activation and deactivation. */ -public enum ActivationPhase { +public enum Phase { /** * Starting state before anything happens activation-wise. @@ -66,10 +66,25 @@ public enum ActivationPhase { */ ACTIVE(true), + /** + * Called after all modules and services loaded into the service registry. + */ + POST_BIND_ALL_MODULES(true), + + /** + * Called after {@link #POST_BIND_ALL_MODULES} to resolve any latent bindings, prior to {@link #SERVICES_READY}. + */ + FINAL_RESOLVE(true), + + /** + * The service registry is fully populated and ready. + */ + SERVICES_READY(true), + /** * About to call pre-destroy. */ - PRE_DESTROYING(false), + PRE_DESTROYING(true), /** * Destroyed (after calling any pre-destroy). @@ -84,15 +99,15 @@ public enum ActivationPhase { /** * 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. - * + * @return true if this phase is eligible to be included in shutdown processing * @see PicoServices#shutdown() */ public boolean eligibleForDeactivation() { return eligibleForDeactivation; } - ActivationPhase(boolean eligibleForDeactivation) { + Phase(boolean eligibleForDeactivation) { this.eligibleForDeactivation = eligibleForDeactivation; } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoException.java b/pico/api/src/main/java/io/helidon/pico/PicoException.java similarity index 90% rename from pico/pico/src/main/java/io/helidon/pico/PicoException.java rename to pico/api/src/main/java/io/helidon/pico/PicoException.java index 6e5d44f3df4..9c72dd4ac2f 100644 --- a/pico/pico/src/main/java/io/helidon/pico/PicoException.java +++ b/pico/api/src/main/java/io/helidon/pico/PicoException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,7 +17,7 @@ package io.helidon.pico; /** - * A general purpose exception. + * A general exception indicating that something failed related to Pico. * * @see PicoServiceProviderException * @see InjectionException diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java b/pico/api/src/main/java/io/helidon/pico/PicoServiceProviderException.java similarity index 76% rename from pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java rename to pico/api/src/main/java/io/helidon/pico/PicoServiceProviderException.java index 2f66fb07f8d..8038416dffd 100644 --- a/pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java +++ b/pico/api/src/main/java/io/helidon/pico/PicoServiceProviderException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -20,7 +20,7 @@ import java.util.Optional; /** - * A general purpose exception from Pico. + * An exception relative to a {@link io.helidon.pico.ServiceProvider}. */ public class PicoServiceProviderException extends PicoException { @@ -49,11 +49,24 @@ public PicoServiceProviderException(String msg, Throwable cause) { super(msg, cause); - if (cause instanceof PicoServiceProviderException) { + if (cause instanceof PicoServiceProviderException) { this.serviceProvider = ((PicoServiceProviderException) cause).serviceProvider().orElse(null); - } else { + } else { this.serviceProvider = null; - } + } + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param serviceProvider the service provider + */ + public PicoServiceProviderException(String msg, + ServiceProvider serviceProvider) { + super(msg); + Objects.requireNonNull(serviceProvider); + this.serviceProvider = serviceProvider; } /** @@ -83,8 +96,7 @@ public Optional> serviceProvider() { @Override public String getMessage() { return super.getMessage() - + (Objects.isNull(serviceProvider) - ? "" : (": service provider: " + serviceProvider)); + + (serviceProvider == null ? "" : (": service provider: " + serviceProvider)); } } diff --git a/pico/api/src/main/java/io/helidon/pico/PicoServices.java b/pico/api/src/main/java/io/helidon/pico/PicoServices.java new file mode 100644 index 00000000000..da0b2544533 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/PicoServices.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2022, 2023 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.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** + * 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()}. + *

        + * The global singleton instance is accessed via {@link #picoServices()}. Note that optionally one can provide a + * primordial bootstrap configuration to the {@code Pico} services provider. One must establish any bootstrap instance + * prior to the first call to {@link #picoServices()} as it will use a default configuration if not explicitly set. Once + * the bootstrap has been set it cannot be changed for the lifespan of the JVM. + */ +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() + .serviceInfoCriteria(EMPTY_CRITERIA) + .expected(true) + .build(); + + /** + * Returns the {@link io.helidon.pico.Bootstrap} configuration instance that was used to initialize this instance. + * + * @return the bootstrap configuration instance + */ + Bootstrap bootstrap(); + + /** + * Returns true if debugging is enabled. + * + * @return true if debugging is enabled + * @see io.helidon.pico.PicoServicesConfig#TAG_DEBUG + */ + // Note that here in Pico at this level we don't have much access to information. + //

          + //
        • we don't have access to full config, just common config - so no config sources from system properties, etc.
        • + //
        • we don't have access to annotation processing options that may be passed
        • + //
        + static boolean isDebugEnabled() { + Supplier lastResortSupplier = () -> Boolean.getBoolean(PicoServicesConfig.TAG_DEBUG); + Optional bootstrap = globalBootstrap(); + if (bootstrap.isPresent()) { + return PicoServicesConfig.asBoolean(PicoServicesConfig.TAG_DEBUG, lastResortSupplier); + } else { + // last resort + return lastResortSupplier.get(); + } + } + + /** + * Retrieves any primordial bootstrap configuration that previously set. + * + * @return the bootstrap primordial configuration already assigned + * @see #globalBootstrap(Bootstrap) + */ + static Optional globalBootstrap() { + return PicoServicesHolder.bootstrap(false); + } + + /** + * First attempts to locate and return the {@link #globalBootstrap()} and if not found will create a new bootstrap instance. + * + * @return a bootstrap + */ + static Bootstrap realizedGlobalBootStrap() { + Optional bootstrap = globalBootstrap(); + return bootstrap.orElseGet(() -> PicoServicesHolder.bootstrap(true).orElseThrow()); + } + + /** + * Sets the primordial bootstrap configuration that will supply {@link #picoServices()} during global + * singleton initialization. + * + * @param bootstrap the primordial global bootstrap configuration + * @see #globalBootstrap() + */ + static void globalBootstrap(Bootstrap bootstrap) { + Objects.requireNonNull(bootstrap); + PicoServicesHolder.bootstrap(bootstrap); + } + + /** + * Get {@link PicoServices} instance if available. The highest {@link io.helidon.common.Weighted} service will be loaded + * and returned. Remember to optionally configure any primordial {@link Bootstrap} configuration prior to the + * first call to get {@code PicoServices}. + * + * @return the Pico services instance + */ + static Optional picoServices() { + return PicoServicesHolder.picoServices(); + } + + /** + * Short-cut for the following code block. During the first invocation the {@link io.helidon.pico.Services} registry + * will be initialized. + * + *
        +     * {@code
        +     *   return picoServices().orElseThrow().services();
        +     * }
        +     * 
        + * + * @return the services instance + */ + static Services realizedServices() { + return picoServices().orElseThrow().services(); + } + + /** + * Similar to {@link #services()}, but here if Pico is not available or the services registry has not yet been initialized + * then this method will return {@code Optional.empty()}. This is convenience for users who conditionally want to use Pico's + * service registry if it is currently available and in active use, but if not do alternative processing or allocations + * directly, etc. + * + * @return the services instance if it has already been activated and initialized, empty otherwise + */ + @SuppressWarnings("unchecked") + static Optional unrealizedServices() { + PicoServices picoServices = picoServices().orElse(null); + if (picoServices == null) { + return Optional.empty(); + } + + return (Optional) picoServices.services(false); + } + + /** + * The service registry. The first call typically loads and initializes the service registry. To avoid automatic loading + * and initialization on any first request then consider using {@link #unrealizedServices()} or {@link #services(boolean)}. + * + * @return the services registry + */ + default Services services() { + return services(true).orElseThrow(); + } + + /** + * The service registry. The first call typically loads and initializes the service registry. + * + * @param initialize true to allow initialization applicable for the 1st request, false to prevent 1st call initialization + * @return the services registry if it is available and already has been initialized, empty if not yet initialized + */ + Optional services(boolean initialize); + + /** + * The governing configuration. + * + * @return the config + */ + PicoServicesConfig config(); + + /** + * Optionally, the injector. + * + * @return the injector, or empty if not available + */ + Optional injector(); + + /** + * 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 achieve shutdown. + * + * @return a map of all managed service types deactivated to results of deactivation, or empty if shutdown is not supported + */ + Optional> shutdown(); + + /** + * Optionally, the service provider activation log. + * + * @return the injector, or empty if not available + */ + Optional activationLog(); + + /** + * Optionally, the metrics that are exposed by the provider implementation. + * + * @return the metrics, or empty if not available + */ + Optional metrics(); + + /** + * Optionally, the set of {@link io.helidon.pico.Services} lookup criteria that were recorded. This is only available if + * {@link PicoServicesConfig#serviceLookupCaching()} is enabled. + * + * @return the lookup criteria recorded, or empty if not available + */ + Optional> lookups(); + + /** + * Will create an activation request either to {@link io.helidon.pico.Phase#ACTIVE} or limited to any + * {@link io.helidon.pico.Bootstrap#limitRuntimePhase()} specified. + * + * @return the activation request + */ + static ActivationRequest createDefaultActivationRequest() { + return DefaultActivationRequest.builder().targetPhase(terminalActivationPhase()).build(); + } + + /** + * The terminal phase for activation that we should not cross. + * + * @return the terminal phase for activation + */ + static Phase terminalActivationPhase() { + Optional globalBootstrap = PicoServices.globalBootstrap(); + if (globalBootstrap.isPresent()) { + Optional limitPhase = globalBootstrap.get().limitRuntimePhase(); + return limitPhase.orElse(Phase.ACTIVE); + } + return Phase.ACTIVE; + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/PicoServicesConfig.java b/pico/api/src/main/java/io/helidon/pico/PicoServicesConfig.java new file mode 100644 index 00000000000..07c460c8069 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/PicoServicesConfig.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2022, 2023 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.function.Supplier; + +import io.helidon.builder.Builder; +import io.helidon.common.config.Config; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * This is the configuration that the Pico service provider uses internally. + *

        + * If left as-is a default configuration instance will be used with default values provided herein. Callers can + * optionally configure values by providing a {@link Bootstrap#config()} prior to Pico startup. The configuration provided + * will be used, and tunable configuration must be located under the key {@link #NAME} within the provided configuration + * element. + */ +@Builder +public abstract class PicoServicesConfig { + + /** + * The short name for pico. + */ + public static final String NAME = "pico"; + + /** + * The fully qualified name for pico. + */ + public static final String FQN = "io.helidon." + NAME; + + /** + * The key association with the name of the provider implementation. + */ + public static final String KEY_PROVIDER_NAME = "provider-name"; + + /** + * Tag for putting Pico tooling, processing, and runtime into debug mode. + * @see PicoServices#isDebugEnabled() + */ + public static final String TAG_DEBUG = NAME + ".debug"; + + /** + * Identify the module name being processed or the desired target module name. + */ + public static final String TAG_MODULE_NAME = /* do not use #NAME here */ "modulename"; + + /** + * Default Constructor. + */ + protected PicoServicesConfig() { + } + + /** + * The provider implementation name. + * + * @return the provider implementation name + */ + @ConfiguredOption(key = KEY_PROVIDER_NAME) + public abstract String providerName(); + + + /** + * The key association with the version of the provider implementation. + */ + public static final String KEY_PROVIDER_VERSION = "provider-version"; + + /** + * The provider implementation version. + * + * @return the provider implementation version + */ + @ConfiguredOption(key = KEY_PROVIDER_VERSION) + public abstract String providerVersion(); + + + /** + * Applicable during activation, this is the key that controls the timeout before deadlock detection exceptions are thrown. + *

        + * Deadlock can occur in situations where there are cyclic, non-{@code Provider<>} type dependencies between two services, e.g., + * A -> B and B -> A. Obviously this example is the simplest of cases. More often cyclic dependencies are nested N levels deep. + *

        + * Pico may attempt to resolve cyclic dependencies, but this timeout will govern how long Pico will wait before giving up + * and instead will result in an exception being thrown. + *

        + * There are two best practices recommended: + *

          + *
        1. Use {@code Provider<>} as often as possible. If a service activation does not a dependency during + * {@code PostConstruct} then there is really no need to have a direct dependency to that service. + *
        2. Use compile-time {@link Application} generation. See the Pico maven-plugin module for details. Use of this + * feature will detect all cyclic dependencies at compile-time and will result in faster startup times. + *
        + */ + public static final String KEY_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS = "activation-deadlock-timeout-millis"; + /** + * The default deadlock detection timeout in millis. + */ + public static final String DEFAULT_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS = "10000"; + + /** + * The deadlock detection timeout in millis. + * + * @return the deadlock detection timeout in mills + */ + @ConfiguredOption(key = KEY_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS, value = DEFAULT_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS) + public long activationDeadlockDetectionTimeoutMillis() { + return asLong(KEY_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS, + () -> Long.valueOf(DEFAULT_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS)); + } + + + /** + * Applicable for enabling the capture of activation logs at Pico startup. + */ + public static final String KEY_ACTIVATION_LOGS = "activation-logs"; + /** + * The default value for this is false, meaning that the activation logs will not be captured or recorded or logged. + */ + public static final String DEFAULT_ACTIVATION_LOGS = "false"; + + /** + * Flag indicating whether activation logs are captured, recorded, and retained. + * + * @return the flag indicating whether activation logs are captured and retained + */ + @ConfiguredOption(key = KEY_ACTIVATION_LOGS, value = DEFAULT_ACTIVATION_LOGS) + public boolean activationLogs() { + return asBoolean(KEY_ACTIVATION_LOGS, () -> Boolean.valueOf(DEFAULT_ACTIVATION_LOGS)); + } + + + /** + * Applicable for enabling service lookup caching. + */ + public static final String KEY_SERVICE_LOOKUP_CACHING = "service-lookup-caching"; + /** + * The default value for this is false, meaning that no caching will occur. + */ + public static final String DEFAULT_SERVICE_LOOKUP_CACHING = "false"; + + /** + * Flag indicating whether service lookups (i.e., via {@link Services#lookup}) are cached. + * + * @return the flag indicating whether service lookups are cached + */ + @ConfiguredOption(key = KEY_SERVICE_LOOKUP_CACHING, value = DEFAULT_SERVICE_LOOKUP_CACHING) + public boolean serviceLookupCaching() { + return asBoolean(KEY_SERVICE_LOOKUP_CACHING, () -> Boolean.valueOf(DEFAULT_SERVICE_LOOKUP_CACHING)); + } + + + /** + * The key that controls whether the {@link Services} registry is permitted to expand or be dynamically altered after JVM + * startup. + */ + public static final String KEY_PERMITS_DYNAMIC = "permits-dynamic"; + /** + * The default value for this is false, meaning that the services registry can be changed during runtime post Pico startup. + */ + public static final String DEFAULT_PERMITS_DYNAMIC = "false"; + + /** + * Flag indicating whether the services registry permits dynamic behavior (key is {@link #KEY_PERMITS_DYNAMIC}). The default + * implementation of Pico supports dynamic (see {@link #supportsDynamic()}), but does not permit it by default. + * + * @return the flag indicating whether the services registry supports dynamic updates of the service registry + */ + @ConfiguredOption(key = KEY_PERMITS_DYNAMIC, value = DEFAULT_PERMITS_DYNAMIC) + public boolean permitsDynamic() { + return asBoolean(KEY_PERMITS_DYNAMIC, () -> Boolean.valueOf(DEFAULT_PERMITS_DYNAMIC)); + } + + + /** + * The key that indicates whether the {@link Services} registry is capable of expanding or being dynamically altered after JVM + * startup. This is referred to as the service registry being dynamic in nature. + */ + public static final String KEY_SUPPORTS_DYNAMIC = "supports-dynamic"; + /** + * The default value for this is false, meaning that the services registry supports dynamic behavior post Pico startup. + */ + public static final String DEFAULT_SUPPORTS_DYNAMIC = "true"; + + /** + * Flag indicating whether the services registry supports dynamic behavior (key is {@link #KEY_SUPPORTS_DYNAMIC}). Note that + * if the provider does not support this flag then permitting it via {@link #permitsDynamic()} will have no affect. The default + * implementation of Pico supports dynamic, but does not permit it by default. + * + * @return the flag indicating whether the services registry supports dynamic updates of the service registry post Pico startup + */ + @ConfiguredOption(key = KEY_SUPPORTS_DYNAMIC, value = DEFAULT_SUPPORTS_DYNAMIC) + public abstract boolean supportsDynamic(); + + + /** + * The key that controls whether reflection is permitted to be used during Pico runtime operations. The default implementation + * of Pico does not support runtime reflection usage; it is only supported via the Pico maven-plugin and + * supporting tooling, which typically occurs during compile-time operations but not during normal runtime operations. + */ + public static final String KEY_PERMITS_REFLECTION = "permits-reflection"; + /** + * The default value for this is false, meaning that the Pico will make not attempt to use reflection during runtime operations. + */ + public static final String DEFAULT_PERMITS_REFLECTION = "false"; + + /** + * Flag indicating whether reflection is permitted (key is {@link #KEY_PERMITS_DYNAMIC}). The default implementation of Pico + * supports reflection at compile-time only, and is not controlled by this flag directly. + * + * @return the flag indicating whether the provider is permitted to use reflection for normal runtime usage + */ + @ConfiguredOption(key = KEY_PERMITS_REFLECTION, value = DEFAULT_PERMITS_REFLECTION) + public boolean permitsReflection() { + return asBoolean(KEY_PERMITS_REFLECTION, () -> Boolean.valueOf(DEFAULT_PERMITS_REFLECTION)); + } + + + /** + * The key that indicates whether the reflection is supported in normal runtime behavior. + */ + public static final String KEY_SUPPORTS_REFLECTION = "supports-reflection"; + /** + * The default value for this is false, meaning that the reflection is not supported by the provider. + */ + public static final String DEFAULT_SUPPORTS_REFLECTION = "false"; + + /** + * Flag indicating whether the reflection is supported. Note that if the provider does not support this + * flag then permitting it via {@link #permitsReflection()} will have no affect. The default implementation of Pico supports + * reflection only during compile-time operations using the Pico maven-plugin. + * + * @return the flag indicating whether reflection is supported during runtime operations + */ + @ConfiguredOption(key = KEY_SUPPORTS_REFLECTION, value = DEFAULT_SUPPORTS_REFLECTION) + public abstract boolean supportsReflection(); + + + /** + * The key that controls whether any {@link Application}'s + * (typically produced at compile-time by Pico tooling) can be discovered and is used during Pico startup processing. It is + * strongly suggested for developers to adopt a compile-time strategy for producing the dependency/injection model as it will + * lead to faster startup times as well as be deterministic and validated during compile-time instead of at runtime. + */ + public static final String KEY_USES_COMPILE_TIME_APPLICATIONS = "uses-compile-time-applications"; + /** + * The default value for this is true, meaning that the Pico will attempt to find and use {@link Application} code generated + * during compile-time (see Pico's APT processor and maven-plugin modules for usage). + */ + public static final String DEFAULT_USES_COMPILE_TIME_APPLICATIONS = "true"; + + /** + * Flag indicating whether compile-time generated {@link Application}'s should be used at Pico's startup initialization. Setting + * this value to false will have no affect if the underlying provider does not support compile-time generation via + * {@link #supportsCompileTime()}. + * + * @return the flag indicating whether the provider is permitted to use Application generated code from compile-time + * @see io.helidon.pico.Application + * @see io.helidon.pico.Activator + */ + @ConfiguredOption(key = KEY_USES_COMPILE_TIME_APPLICATIONS, value = DEFAULT_USES_COMPILE_TIME_APPLICATIONS) + public boolean usesCompileTimeApplications() { + return asBoolean(KEY_USES_COMPILE_TIME_APPLICATIONS, () -> Boolean.valueOf(DEFAULT_USES_COMPILE_TIME_APPLICATIONS)); + } + + + /** + * The key that controls whether any {@link io.helidon.pico.Module}'s + * (typically produced at compile-time by Pico tooling) can be discovered and is used during Pico startup processing. It is + * strongly suggested for developers to adopt a compile-time strategy for producing the dependency/injection model as it will + * lead to faster startup times as well as be deterministic and validated during compile-time instead of at runtime. + */ + public static final String KEY_USES_COMPILE_TIME_MODULES = "uses-compile-time-modules"; + /** + * The default value for this is true, meaning that the Pico will attempt to find and use {@link io.helidon.pico.Module} code + * generated during compile-time (see Pico's APT processor and maven-plugin modules for usage). + */ + public static final String DEFAULT_USES_COMPILE_TIME_MODULES = "true"; + + /** + * Flag indicating whether compile-time generated {@link io.helidon.pico.Module}'s should be used at Pico's startup + * initialization. Setting this value to false will have no affect if the underlying provider does not support compile-time + * generation via {@link #supportsCompileTime()}. + * + * @return the flag indicating whether the provider is permitted to use Application generated code from compile-time + * @see io.helidon.pico.Module + * @see io.helidon.pico.Activator + */ + @ConfiguredOption(key = KEY_USES_COMPILE_TIME_MODULES, value = DEFAULT_USES_COMPILE_TIME_MODULES) + public boolean usesCompileTimeModules() { + return asBoolean(KEY_USES_COMPILE_TIME_MODULES, () -> Boolean.valueOf(DEFAULT_USES_COMPILE_TIME_MODULES)); + } + + + /** + * The key that represents whether the provider supports compile-time code generation of DI artifacts. + * + * @see io.helidon.pico.Application + * @see io.helidon.pico.Module + * @see io.helidon.pico.Activator + * @see #usesCompileTimeApplications() + * @see #usesCompileTimeModules() + */ + public static final String KEY_SUPPORTS_COMPILE_TIME = "supports-compile-time"; + /** + * The default value is true, meaning that the provider supports compile-time code generation of DI artifacts. + */ + public static final String DEFAULT_SUPPORTS_COMPILE_TIME = "true"; + + /** + * Flag indicating whether the dependency injection model for the {@link Application} and + * {@link Activator} is capable for being produced at compile-time, and therefore used/loaded during runtime operations. + * + * @return the flag indicating whether the provider supports compile-time code generation of DI artifacts + */ + @ConfiguredOption(key = KEY_SUPPORTS_COMPILE_TIME, value = DEFAULT_SUPPORTS_COMPILE_TIME) + public abstract boolean supportsCompileTime(); + + + /** + * The key that controls whether strict jsr330 specification interpretation is used. See the README for additional details. + */ + public static final String KEY_USES_JSR330 = "uses-jsr330"; + /** + * The default value for this is false, meaning that the Pico implementation will not follow a strict jsr330 interpretation + * of the specification. See the README for additional details. + */ + public static final String DEFAULT_USES_JSR330 = "false"; + + /** + * Flag indicating whether jsr330 specification will be used and enforced. + * + * @return the flag indicating whether strict jsr330 specification will be enforced + */ + @ConfiguredOption(key = KEY_USES_JSR330, value = DEFAULT_USES_JSR330) + public boolean usesJsr330() { + return asBoolean(KEY_USES_JSR330, () -> Boolean.valueOf(DEFAULT_USES_JSR330)); + } + + + /** + * The key to represent whether the provider supports the jsr330 specification. + */ + public static final String KEY_SUPPORTS_JSR330 = "supports-jsr330"; + /** + * The default value is true, meaning that the default Pico implementation supports the jsr330 specification (i.e., + * one that passes the jsr330 TCK). + */ + public static final String DEFAULT_SUPPORTS_JSR330 = "true"; + + /** + * Flag indicating whether jsr330 is supported by the provider implementation. + * + * @return the flag indicating whether the provider supports the jsr330 specification + */ + @ConfiguredOption(key = KEY_SUPPORTS_JSR330, value = DEFAULT_SUPPORTS_JSR330) + public abstract boolean supportsJsr330(); + + + /** + * Key indicating support for static injection points. Note: this is optional in jsr330. + */ + public static final String KEY_SUPPORTS_JSR330_STATICS = KEY_SUPPORTS_JSR330 + "-statics"; + /** + * The default value is false, meaning that the default provider implementation does not support static injection points. + */ + public static final String DEFAULT_SUPPORTS_JSR330_STATICS = "false"; + + /** + * Flag indicating whether jsr330 is supported by the provider implementation for the use on static injection points. + * + * @return the flag indicating whether the provider supports the jsr330 specification for the use of static injection points + */ + @ConfiguredOption(key = KEY_SUPPORTS_JSR330_STATICS, value = DEFAULT_SUPPORTS_JSR330_STATICS) + public abstract boolean supportsJsr330Statics(); + + + /** + * Key indicating support for private injection points. Note: this is optional in jsr330. + */ + public static final String KEY_SUPPORTS_JSR330_PRIVATES = KEY_SUPPORTS_JSR330 + "-privates"; + /** + * The default value is false, meaning that the default provider implementation does not support private injection points. + */ + public static final String DEFAULT_SUPPORTS_JSR330_PRIVATES = "false"; + + /** + * Flag indicating whether jsr330 is supported by the provider implementation for the use on private injection points. + * + * @return the flag indicating whether the provider supports the jsr330 specification for the use of private injection points + */ + @ConfiguredOption(key = KEY_SUPPORTS_JSR330_PRIVATES, value = DEFAULT_SUPPORTS_JSR330_PRIVATES) + public abstract boolean supportsJsr330Privates(); + + + /** + * Key indicating support for contextual lookup via {@link Services#contextualServices(InjectionPointInfo)}. + */ + public static final String KEY_SUPPORTS_CONTEXTUAL_LOOKUP = "supports-contextual-lookup"; + /** + * The default value is false, meaning that contextual lookup is not supported. + */ + public static final String DEFAULT_SUPPORTS_CONTEXTUAL_LOOKUP = "false"; + + /** + * Flag indicating whether contextual lookup is supported via {@link Services#contextualServices(InjectionPointInfo)}. + * + * @return the flag indicating whether the provider supports contextual lookup + */ + @ConfiguredOption(key = KEY_SUPPORTS_CONTEXTUAL_LOOKUP, value = DEFAULT_SUPPORTS_CONTEXTUAL_LOOKUP) + public abstract boolean supportsContextualLookup(); + + /** + * Shortcut method to obtain a Boolean with a default value supplier. + * + * @param key configuration key + * @param defaultValueSupplier supplier of default value + * @return value + */ + static Boolean asBoolean(String key, + Supplier defaultValueSupplier) { + Optional cfg = get(key); + if (cfg.isEmpty() + || !cfg.get().hasValue()) { + return defaultValueSupplier.get(); + } + return cfg.get().asBoolean().orElseGet(defaultValueSupplier); + } + + /** + * Shortcut method to obtain a Long with a default value supplier. + * + * @param key configuration key + * @param defaultValueSupplier supplier of default value + * @return value + */ + static Long asLong(String key, + Supplier defaultValueSupplier) { + Optional cfg = get(key); + if (cfg.isEmpty() + || !cfg.get().hasValue()) { + return defaultValueSupplier.get(); + } + return cfg.get().asLong().orElseGet(defaultValueSupplier); + } + + /** + * Retrieves an arbitrary {@link Config} given a key. The physical configuration will be based upon any + * {@link Bootstrap#config()} that was previously established using {@link PicoServices#globalBootstrap()}. If the bootstrap + * configuration has not been established then empty is returned. + * + * @param key the config key relative to the parent global bootstrap configuration + * @return the configuration for the key + */ + static Optional get(String key) { + Optional bootstrap = PicoServicesHolder.bootstrap(false); + if (bootstrap.isEmpty() || bootstrap.get().config().isEmpty()) { + return Optional.empty(); + } + return Optional.of(bootstrap.get().config().get().get(NAME).get(key)); + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/PicoServicesHolder.java b/pico/api/src/main/java/io/helidon/pico/PicoServicesHolder.java new file mode 100644 index 00000000000..d788072309c --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/PicoServicesHolder.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2022, 2023 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.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.pico.spi.PicoServicesProvider; + +import static io.helidon.pico.CallingContext.DEBUG_HINT; + +/** + * The holder for the globally active {@link PicoServices} singleton instance, as well as its associated + * {@link io.helidon.pico.Bootstrap} primordial configuration. + */ +// exposed in the testing module as non deprecated +public abstract class PicoServicesHolder { + private static final AtomicReference BOOTSTRAP = new AtomicReference<>(); + private static final AtomicReference INSTANCE = new AtomicReference<>(); + + /** + * Default Constructor. + * + * @deprecated use {@link PicoServices#picoServices()} or {@link PicoServices#globalBootstrap()}. + */ + // exposed in the testing module as non deprecated + @Deprecated + protected PicoServicesHolder() { + } + + /** + * Returns the global Pico services instance. The returned service instance will be initialized with any bootstrap + * configuration that was previously established. + * + * @return the loaded global pico services instance + */ + static Optional picoServices() { + if (INSTANCE.get() == null) { + INSTANCE.compareAndSet(null, new ProviderAndServicesTuple(load())); + if (INSTANCE.get().picoServices == null) { + System.getLogger(PicoServices.class.getName()) + .log(System.Logger.Level.WARNING, + DefaultPicoServicesConfig.NAME + " runtime services not detected on the classpath"); + } + } + return Optional.ofNullable(INSTANCE.get().picoServices); + } + + /** + * Resets the bootstrap state. + */ + protected static void reset() { + ProviderAndServicesTuple instance = INSTANCE.get(); + if (instance != null) { + instance.reset(); + } + INSTANCE.set(null); + BOOTSTRAP.set(null); + } + + static void bootstrap(Bootstrap bootstrap) { + Objects.requireNonNull(bootstrap); + InternalBootstrap iBootstrap = InternalBootstrap.create(bootstrap, null); + if (!BOOTSTRAP.compareAndSet(null, iBootstrap)) { + InternalBootstrap existing = BOOTSTRAP.get(); + CallingContext callingContext = (existing == null) ? null : existing.callingContext().orElse(null); + StackTraceElement[] trace = (callingContext == null) ? new StackTraceElement[] {} : callingContext.trace(); + if (trace != null && trace.length > 0) { + throw new IllegalStateException( + "bootstrap was previously set from this code path:\n" + prettyPrintStackTraceOf(trace) + + "; module name is '" + callingContext.moduleName().orElse("undefined") + "'"); + } + throw new IllegalStateException("bootstrap already set - " + DEBUG_HINT); + } + } + + static Optional bootstrap(boolean assignIfNeeded) { + if (assignIfNeeded) { + InternalBootstrap iBootstrap = InternalBootstrap.create(); + BOOTSTRAP.compareAndSet(null, iBootstrap); + } + + InternalBootstrap iBootstrap = BOOTSTRAP.get(); + return Optional.ofNullable((iBootstrap != null) ? iBootstrap.bootStrap() : null); + } + + private static Optional load() { + return HelidonServiceLoader.create(ServiceLoader.load(PicoServicesProvider.class, + PicoServicesProvider.class.getClassLoader())) + .asList() + .stream() + .findFirst(); + } + + // we need to keep the provider and the instance the provider creates together as one entity + private static class ProviderAndServicesTuple { + private final PicoServicesProvider provider; + private final PicoServices picoServices; + + private ProviderAndServicesTuple(Optional provider) { + this.provider = provider.orElse(null); + this.picoServices = (provider.isPresent()) + ? this.provider.services(bootstrap(true).orElseThrow()) : null; + } + + private void reset() { + if (provider instanceof Resettable) { + ((Resettable) provider).reset(true); + } else if (picoServices instanceof Resettable) { + ((Resettable) picoServices).reset(true); + } + } + } + + /** + * Returns a stack trace as a list of strings. + * + * @param trace the trace + * @return the list of strings for the stack trace + */ + static List stackTraceOf(StackTraceElement[] trace) { + List result = new ArrayList<>(); + for (StackTraceElement e : trace) { + result.add(e.toString()); + } + return result; + } + + /** + * Returns a stack trace as a CRLF joined string. + * + * @param trace the trace + * @return the stringified stack trace + */ + static String prettyPrintStackTraceOf(StackTraceElement[] trace) { + return String.join("\n", stackTraceOf(trace)); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java b/pico/api/src/main/java/io/helidon/pico/PostConstructMethod.java similarity index 93% rename from pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java rename to pico/api/src/main/java/io/helidon/pico/PostConstructMethod.java index 16f4bfea2df..c396e260db3 100644 --- a/pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java +++ b/pico/api/src/main/java/io/helidon/pico/PostConstructMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java b/pico/api/src/main/java/io/helidon/pico/PreDestroyMethod.java similarity index 93% rename from pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java rename to pico/api/src/main/java/io/helidon/pico/PreDestroyMethod.java index a8af213ef75..8f05497f5a3 100644 --- a/pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java +++ b/pico/api/src/main/java/io/helidon/pico/PreDestroyMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java b/pico/api/src/main/java/io/helidon/pico/QualifierAndValue.java similarity index 87% rename from pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java rename to pico/api/src/main/java/io/helidon/pico/QualifierAndValue.java index d9b4d4f48e3..7d7e78c9b6b 100644 --- a/pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java +++ b/pico/api/src/main/java/io/helidon/pico/QualifierAndValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,12 +16,13 @@ package io.helidon.pico; -import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.common.types.AnnotationAndValue; /** * Represents a tuple of the Qualifier and optionally any value. * * @see jakarta.inject.Qualifier + * @see CommonQualifiers */ public interface QualifierAndValue extends AnnotationAndValue { diff --git a/pico/api/src/main/java/io/helidon/pico/Resettable.java b/pico/api/src/main/java/io/helidon/pico/Resettable.java new file mode 100644 index 00000000000..3131dc8e73f --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/Resettable.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022, 2023 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 are capable of resetting the state of itself (i.e., clears cache, log entries, etc.). + */ +@FunctionalInterface +public interface Resettable { + + /** + * Resets the state of this object. + * + * @param deep true to iterate over any contained objects, to reflect the reset into the retained object + * @return returns true if the state was changed + */ + boolean reset(boolean deep); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/RunLevel.java b/pico/api/src/main/java/io/helidon/pico/RunLevel.java similarity index 64% rename from pico/pico/src/main/java/io/helidon/pico/RunLevel.java rename to pico/api/src/main/java/io/helidon/pico/RunLevel.java index 40f60af0418..dbc332248d6 100644 --- a/pico/pico/src/main/java/io/helidon/pico/RunLevel.java +++ b/pico/api/src/main/java/io/helidon/pico/RunLevel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -28,19 +28,27 @@ * Indicates the desired startup sequence for a service class. */ @Documented -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.CLASS) @Target(TYPE) @Inherited public @interface RunLevel { /** - * Represents an eager singleton. + * Represents an eager singleton that should be started at "startup". Note, however, that callers control the actual + * activation for these services, not the framework itself, as shown below: + *
        +     * {@code
        +     * List> startupServices = services
        +     *               .lookup(DefaultServiceInfoCriteria.builder().runLevel(RunLevel.STARTUP).build());
        +     *       startupServices.stream().forEach(ServiceProvider::get);
        +     * }
        +     * 
        */ - int STARTUP = 0; + int STARTUP = 10; /** - * Anything > 0 is left to the underlying provider implementation's discretion for meaning; this is just a default for something - * that is deemed "other than startup". + * Anything > 0 is left to the underlying provider implementation's discretion for meaning; this is just a default for + * something that is deemed "other than startup". */ int NORMAL = 100; diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceBinder.java b/pico/api/src/main/java/io/helidon/pico/ServiceBinder.java similarity index 63% rename from pico/pico/src/main/java/io/helidon/pico/ServiceBinder.java rename to pico/api/src/main/java/io/helidon/pico/ServiceBinder.java index 11982299c99..5567d3780eb 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceBinder.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceBinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,9 +16,6 @@ package io.helidon.pico; -import java.util.LinkedHashSet; -import java.util.Set; - /** * Responsible for binding service providers to the service registry. */ @@ -31,19 +28,4 @@ public interface ServiceBinder { */ 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/api/src/main/java/io/helidon/pico/ServiceInfo.java b/pico/api/src/main/java/io/helidon/pico/ServiceInfo.java new file mode 100644 index 00000000000..d3d249ba5df --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInfo.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2022, 2023 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.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.AnnotationAndValue; + +/** + * Describes a managed service or injection point. + * + * @see Services + * @see ServiceInfoCriteria + */ +@Builder(interceptor = ServiceInfoBuildInterceptor.class) +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 + */ + @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(); + + /** + * Determines whether this service info matches the criteria for injection. + * Matches is a looser form of equality check than {@code equals()}. 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 + */ + // internal note: it is unfortunate that we have a matches() here as well as in ServiceInfo. This is what happened + // when we split ServiceInfo into ServiceInfoCriteria. Sometimes we need ServiceInfo.matches(criteria), and other times + // ServiceInfoCriteria.matches(criteria). + default boolean matches(ServiceInfoCriteria criteria) { + if (criteria == PicoServices.EMPTY_CRITERIA) { + return true; + } + + boolean matches = matches(serviceTypeName(), criteria.serviceTypeName()); + if (matches && criteria.serviceTypeName().isEmpty()) { + matches = contractsImplemented().containsAll(criteria.contractsImplemented()) + || criteria.contractsImplemented().contains(serviceTypeName()); + } + return matches + && scopeTypeNames().containsAll(criteria.scopeTypeNames()) + && matchesQualifiers(qualifiers(), criteria.qualifiers()) + && matches(activatorTypeName(), criteria.activatorTypeName()) + && matchesWeight(this, criteria) + && matches(realizedRunLevel(), criteria.runLevel()) + && matches(moduleName(), criteria.moduleName()); + } + + /** + * 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 + */ + static boolean matchesQualifiers(Collection src, + Collection criteria) { + if (criteria.isEmpty()) { + return true; + } + + if (src.isEmpty()) { + return false; + } + + if (src.contains(CommonQualifiers.WILDCARD_NAMED)) { + return true; + } + + for (QualifierAndValue criteriaQualifier : criteria) { + if (src.contains(criteriaQualifier)) { + // NOP; + continue; + } else if (criteriaQualifier.typeName().equals(CommonQualifiers.NAMED)) { + if (criteriaQualifier.equals(CommonQualifiers.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(CommonQualifiers.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 Objects.equals(src, criteria.get()); + } + + /** + * 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 + */ + private static boolean matchesWeight(ServiceInfoBasics src, + ServiceInfoCriteria criteria) { + if (criteria.weight().isEmpty()) { + return true; + } + + Double srcWeight = src.realizedWeight(); + return (srcWeight.compareTo(criteria.weight().get()) <= 0); + } + + /** + * Creates a builder from a {@link io.helidon.pico.ServiceInfoBasics} instance. + * + * @param val the instance to copy + * @return the fluent builder + */ + static DefaultServiceInfo.Builder toBuilder(ServiceInfoBasics val) { + if (val instanceof ServiceInfo) { + return DefaultServiceInfo.toBuilder((ServiceInfo) val); + } + + DefaultServiceInfo.Builder result = DefaultServiceInfo.builder(); + result.serviceTypeName(val.serviceTypeName()); + result.scopeTypeNames(val.scopeTypeNames()); + result.qualifiers(val.qualifiers()); + result.contractsImplemented(val.contractsImplemented()); + result.declaredRunLevel(val.declaredRunLevel()); + result.declaredWeight(val.declaredWeight()); + return result; + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java b/pico/api/src/main/java/io/helidon/pico/ServiceInfoBasics.java similarity index 63% rename from pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java rename to pico/api/src/main/java/io/helidon/pico/ServiceInfoBasics.java index 8a20304ed11..0ddc36b103f 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInfoBasics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,18 +16,26 @@ package io.helidon.pico; -import java.util.Collections; import java.util.Optional; import java.util.Set; +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; import io.helidon.common.Weighted; -import jakarta.inject.Singleton; - /** * Basic service info that describes a service provider type. + * + * @see ServiceInfo */ -public interface ServiceInfoBasics extends Weighted { +@Builder +public interface ServiceInfoBasics { + + /** + * Default weight for any weighted component (whether it implements this interface + * or uses {@link io.helidon.common.Weight} annotation). + */ + double DEFAULT_WEIGHT = Weighted.DEFAULT_WEIGHT; /** * The managed service implementation {@link Class}. @@ -41,18 +49,16 @@ public interface ServiceInfoBasics extends Weighted { * * @return the service scope type name */ - default Set scopeTypeNames() { - return Collections.singleton(Singleton.class.getName()); - } + @Singular + Set scopeTypeNames(); /** * The managed service assigned Qualifier's. * * @return the service qualifiers */ - default Set qualifiers() { - return Set.of(); - } + @Singular + Set qualifiers(); /** * The managed services advertised types (i.e., typically its interfaces). @@ -60,17 +66,25 @@ default Set qualifiers() { * @see io.helidon.pico.ExternalContracts * @return the service contracts implemented */ - default Set contractsImplemented() { - return Set.of(); - } + @Singular + Set contractsImplemented(); /** * The optional {@link RunLevel} ascribed to the service. * * @return the service's run level + * @see #realizedRunLevel() */ - default int runLevel() { - return RunLevel.NORMAL; + Optional declaredRunLevel(); + + /** + * The realized run level will use the default run level if no run level was specified directly. + * + * @return the realized run level + * @see #declaredRunLevel() + */ + default int realizedRunLevel() { + return declaredRunLevel().orElse(RunLevel.NORMAL); } /** @@ -79,27 +93,16 @@ default int runLevel() { * @return the declared weight * @see #realizedWeight() */ - default Optional declaredWeight() { - return Optional.of(weight()); - } + Optional declaredWeight(); /** * The realized weight will use the default weight if no weight was specified directly. * * @return the realized weight - * @see #weight() + * @see #declaredWeight() */ default double realizedWeight() { - return declaredWeight().orElse(weight()); + return declaredWeight().orElse(DEFAULT_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/api/src/main/java/io/helidon/pico/ServiceInfoBuildInterceptor.java b/pico/api/src/main/java/io/helidon/pico/ServiceInfoBuildInterceptor.java new file mode 100644 index 00000000000..9927a1ef9cb --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInfoBuildInterceptor.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.builder.BuilderInterceptor; + +/** + * Ensures that all external contracts are also treated as normal contracts, etc. + */ +class ServiceInfoBuildInterceptor implements BuilderInterceptor { + + @Override + public DefaultServiceInfo.Builder intercept(DefaultServiceInfo.Builder target) { + target.addContractsImplemented(target.externalContractsImplemented()); + return target; + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java b/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java new file mode 100644 index 00000000000..fca1af3b8f4 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInfoCriteria.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2022, 2023 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.Set; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; + +/** + * Criteria to discover services. + * + * @see Services + * @see ServiceInfo + */ +@Builder +public interface ServiceInfoCriteria { + + /** + * The managed service implementation type name. + * + * @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("contractImplemented") + 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("externalContractImplemented") + 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(); + + /** + * Determines whether this service info matches the criteria for injection. + * Matches is a looser form of equality check than {@code equals()}. 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 + */ + // internal note: it is unfortunate that we have a matches() here as well as in ServiceInfo. This is what happened + // when we split ServiceInfo into ServiceInfoCriteria. Sometimes we need ServiceInfo.matches(criteria), and other times + // ServiceInfoCriteria.matches(criteria). + default boolean matches(ServiceInfoCriteria criteria) { + return matchesContracts(criteria) + && scopeTypeNames().containsAll(criteria.scopeTypeNames()) + && ServiceInfo.matchesQualifiers(qualifiers(), criteria.qualifiers()) + && matches(activatorTypeName(), criteria.activatorTypeName()) + && matches(runLevel(), criteria.runLevel()) +// && matchesWeight(this, criteria) -- intentionally not checking weight here! + && matches(moduleName(), criteria.moduleName()); + } + + /** + * Determines whether the provided criteria match just the contracts portion of the provided criteria. Note that + * it is expected any external contracts have been consolidated into the regular contract section. + * + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance from only the contracts point of view + */ + default boolean matchesContracts(ServiceInfoCriteria criteria) { + if (criteria == PicoServices.EMPTY_CRITERIA) { + return true; + } + + boolean matches = matches(serviceTypeName(), criteria.serviceTypeName()); + if (matches && criteria.serviceTypeName().isEmpty()) { + matches = contractsImplemented().containsAll(criteria.contractsImplemented()); + } + return matches; + } + + private static boolean matches(Object src, + Optional criteria) { + if (criteria.isEmpty()) { + return true; + } + + return Objects.equals(src, criteria.get()); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java b/pico/api/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java similarity index 74% rename from pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java rename to pico/api/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java index 75ecfefaca6..f1040631db8 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -38,44 +38,44 @@ public interface ServiceInjectionPlanBinder { interface Binder { /** - * Binds a single service provider to the injection point identified by {@link InjectionPointInfo#identity()}. + * Binds a single service provider to the injection point identified by {@link InjectionPointInfo#id()}. * 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 id 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); + Binder bind(String id, + ServiceProvider serviceProvider); /** - * Binds a list of service providers to the injection point identified by {@link InjectionPointInfo#identity()}. + * Binds a list of service providers to the injection point identified by {@link InjectionPointInfo#id()}. * 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. + * @param id the injection point identity + * @param serviceProviders the list of service providers to bind to this identity. * @return the binder builder */ - Binder bindMany(String ipIdentity, + Binder bindMany(String id, ServiceProvider... serviceProviders); /** * Represents a void / null bind, only applicable for an Optional injection point. * - * @param ipIdentity the injection point identity + * @param id the injection point identity * @return the binder builder */ - Binder bindVoid(String ipIdentity); + Binder bindVoid(String id); /** * 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 + * @param id the injection point identity + * @param serviceType the service type needing to be resolved * @return the binder builder */ - Binder resolvedBind(String ipIdentity, + Binder resolvedBind(String id, Class serviceType); /** diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java b/pico/api/src/main/java/io/helidon/pico/ServiceProvider.java similarity index 81% rename from pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java rename to pico/api/src/main/java/io/helidon/pico/ServiceProvider.java index f9938ab9a25..47277049f6a 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -30,29 +30,19 @@ public interface ServiceProvider extends InjectionPointProvider, Weighted { /** - * Describe the service provider physically and (globally) uniquely. + * Identifies the service provider physically and uniquely. * - * @return the unique identity description + * @return the unique identity of the service provider */ - String identity(); + String id(); /** - * Describe the service provider conceptually. + * Describe the service provider. This will change based upon activation state. * - * @return the logical description + * @return the logical and immutable 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 @@ -65,7 +55,7 @@ default boolean isSingletonScope() { * {@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. + * @return true if the service provider provides per-request instances for each caller */ boolean isProvider(); @@ -89,7 +79,7 @@ default boolean isSingletonScope() { * * @return the activation phase */ - ActivationPhase currentActivationPhase(); + Phase currentActivationPhase(); /** * The agent responsible for activation - this will be non-null for build-time activators. If not present then @@ -97,7 +87,7 @@ default boolean isSingletonScope() { * * @return the activator */ - Activator activator(); + Optional activator(); /** * The agent responsible for deactivation - this will be non-null for build-time activators. If not present then @@ -105,7 +95,7 @@ default boolean isSingletonScope() { * * @return the deactivator to use or null if the service is not interested in deactivation */ - DeActivator deActivator(); + Optional deActivator(); /** * The optional method handling PreDestroy. @@ -122,12 +112,13 @@ default boolean isSingletonScope() { Optional preDestroyMethod(); /** - * The agent/instance to be used for binding this service provider to the pico application that is class code generated. + * The agent/instance to be used for binding this service provider to the pico application that was code generated. * - * @return the service provider that should be used for binding + * @return the service provider that should be used for binding, or empty if this provider does not support binding * @see Module * @see ServiceBinder * @see ServiceProviderBindable */ - ServiceProvider serviceProviderBindable(); + Optional> serviceProviderBindable(); + } diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java b/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java similarity index 90% rename from pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java rename to pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java index ff27012f9a6..a8759166bd6 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceProviderBindable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -19,7 +19,7 @@ import java.util.Optional; /** - * An extension to {@link ServiceProvider} that allows for startup binding from a picoApplication, + * An extension to {@link ServiceProvider} that allows for startup binding from a {@code Pico$$Application}, * and thereby works in conjunction with the {@link ServiceBinder} during pico service registry * initialization. *

        @@ -83,7 +83,7 @@ default Optional> rootProvider() { /** * Returns true if this provider is the root provider. * - * @return indicates whether this provider is a root provider - the default is true. + * @return indicates whether this provider is a root provider - the default is true */ default boolean isRootProvider() { return rootProvider().isEmpty(); @@ -100,14 +100,12 @@ default void rootProvider(ServiceProvider rootProvider) { } /** - * The instance of services this provider is bound to. A service provider can be associated with 0..1 services instance. + * Assigns the services instance 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 + * @param picoServices the pico services instance, or empty to clear any active binding */ - default void picoServices(PicoServices picoServices) { - // NOP; intended to be overridden if applicable - } + void picoServices(Optional picoServices); /** * The binder can be provided by the service provider to deterministically set the injection plan at compile-time, and @@ -118,4 +116,5 @@ default void picoServices(PicoServices picoServices) { default Optional injectionPlanBinder() { return Optional.empty(); } + } diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java b/pico/api/src/main/java/io/helidon/pico/ServiceProviderProvider.java similarity index 83% rename from pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java rename to pico/api/src/main/java/io/helidon/pico/ServiceProviderProvider.java index f961836797f..ce1f94b8e74 100644 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java +++ b/pico/api/src/main/java/io/helidon/pico/ServiceProviderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -33,15 +33,17 @@ public interface ServiceProviderProvider { * matched using the standard service info matching checks * @return the list of service providers matching */ - List> serviceProviders(ServiceInfo criteria, boolean wantThis, boolean thisAlreadyMatches); + List> serviceProviders(ServiceInfoCriteria 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)}. + * {@link #serviceProviders(ServiceInfoCriteria, 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); + Map> managedServiceProviders(ServiceInfoCriteria criteria); } diff --git a/pico/pico/src/main/java/io/helidon/pico/Services.java b/pico/api/src/main/java/io/helidon/pico/Services.java similarity index 69% rename from pico/pico/src/main/java/io/helidon/pico/Services.java rename to pico/api/src/main/java/io/helidon/pico/Services.java index 68e8e3f5fed..137eb5dec2c 100644 --- a/pico/pico/src/main/java/io/helidon/pico/Services.java +++ b/pico/api/src/main/java/io/helidon/pico/Services.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -23,7 +23,7 @@ * 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 @@ -58,61 +58,65 @@ public interface Services { * @throws io.helidon.pico.PicoException if resolution fails to resolve a match */ default ServiceProvider lookup(Class type) { - return lookupFirst(type, true).get(); + return lookupFirst(type, true) + .orElseThrow(() -> new PicoException("There are no service providers for service of type " + type.getName())); } /** * 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 type the type criteria 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(); + default ServiceProvider lookup(Class type, + String name) { + return lookupFirst(type, name, true) + .orElseThrow(() -> new PicoException("There are no service providers for service of type " + type.getName())); } /** * 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 type the type 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); - } + Optional> lookupFirst(Class type, + boolean 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 type the type 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); + 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 criteria 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(); + default ServiceProvider lookup(ServiceInfoCriteria criteria) { + return (ServiceProvider) lookupFirst(criteria, true).orElseThrow(); } /** @@ -124,13 +128,49 @@ default ServiceProvider lookup(ServiceInfo serviceInfo) { * @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); + Optional> lookupFirst(ServiceInfoCriteria criteria, + boolean expected); /** - * Retrieve all services that implement a given contract type. + * Retrieves the first match based upon the passed service info criteria. + *

        + * This is the same as calling the following: + *

        +     *     lookupFirst(criteria, true).orElseThrow();
        +     * 
        + * + * @param criteria the criteria 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 + */ + @SuppressWarnings("unchecked") + default ServiceProvider lookupFirst(ServiceInfoCriteria criteria) { + return (ServiceProvider) lookupFirst(criteria, true).orElseThrow(); + } + + /** + * Retrieves the first match based upon the passed service info criteria. + *

        + * This is the same as calling the following: + *

        +     *     lookupFirst(criteria, true).orElseThrow();
        +     * 
        * - * @param type the criteria to find + * @param type the type criteria 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 lookupFirst(Class type) { + return lookupFirst(type, true).orElseThrow(); + } + + /** + * Retrieve all services that implement a given contract type. + * + * @param type the type criteria to find + * @param the type of the service being managed * @return the list of service providers matching criteria */ List> lookupAll(Class type); @@ -142,24 +182,29 @@ default ServiceProvider lookup(ServiceInfo serviceInfo) { * @param the type of the service * @return the list of service providers matching criteria */ - List> lookupAll(ServiceInfo criteria); + @SuppressWarnings({"unchecked", "rawtypes"}) + default List> lookupAll(ServiceInfoCriteria criteria) { + return (List) lookupAll(criteria, false); + } /** * 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); + List> lookupAll(ServiceInfoCriteria 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. + * point context to apply for sub-lookup* operations. If the provider does not support contextual lookup then the same + * services instance as this will be returned. * * @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 + * @see PicoServicesConfig#supportsContextualLookup() */ default Services contextualServices(InjectionPointInfo ctx) { return this; diff --git a/pico/pico/src/main/java/io/helidon/pico/package-info.java b/pico/api/src/main/java/io/helidon/pico/package-info.java similarity index 97% rename from pico/pico/src/main/java/io/helidon/pico/package-info.java rename to pico/api/src/main/java/io/helidon/pico/package-info.java index d19da58e74d..9b571288758 100644 --- a/pico/pico/src/main/java/io/helidon/pico/package-info.java +++ b/pico/api/src/main/java/io/helidon/pico/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. diff --git a/pico/api/src/main/java/io/helidon/pico/spi/InjectionPlan.java b/pico/api/src/main/java/io/helidon/pico/spi/InjectionPlan.java new file mode 100644 index 00000000000..afff1d22d77 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/spi/InjectionPlan.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 java.util.List; +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.ServiceProvider; + +/** + * Represents the injection plan targeting a given {@link io.helidon.pico.ServiceProvider}. + */ +@Builder +public interface InjectionPlan { + + /** + * The service provider this plan pertains to. + * + * @return the service provider this plan pertains to + */ + ServiceProvider serviceProvider(); + + /** + * The injection point info for this element, which will also include its identity information. + * + * @return the injection point info for this element + */ + InjectionPointInfo injectionPointInfo(); + + /** + * The list of service providers that are qualified to satisfy the given injection point for this service provider. + * + * @return the qualified service providers for this injection point + */ + @Singular + List> injectionPointQualifiedServiceProviders(); + + /** + * Flag indicating whether resolution occurred. + * + * @return true if resolution occurred + */ + boolean wasResolved(); + + /** + * The resolved value, set only if {@link #wasResolved()}. + * + * @return any resolved value + */ + Optional resolved(); + +} diff --git a/pico/api/src/main/java/io/helidon/pico/spi/InjectionResolver.java b/pico/api/src/main/java/io/helidon/pico/spi/InjectionResolver.java new file mode 100644 index 00000000000..440c0216c72 --- /dev/null +++ b/pico/api/src/main/java/io/helidon/pico/spi/InjectionResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022, 2023 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 java.util.Optional; + +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; + +/** + * Implementors of this contract can assist with resolving injection points. + */ +public interface InjectionResolver { + + /** + * Attempts to resolve the injection point info for a given service provider. + *

        + * There are two modes that injection resolvers run through. + * Phase 1 (resolveIps=false) is during the time when the injection plan is being formulated. This is the time we need + * to identify which {@link ServiceProvider} instances qualify. + * Phase 2 (resolveIps=true) is during actual resolution, and typically comes during the service activation lifecycle. + * + * @param ipInfo the injection point being resolved + * @param picoServices the pico services + * @param serviceProvider the service provider this pertains to + * @param resolveIps flag indicating whether injection points should be resolved + * @return the resolution for the plan or the injection point, or empty if unable to resolve the injection point context + */ + Optional resolve(InjectionPointInfo ipInfo, + PicoServices picoServices, + ServiceProvider serviceProvider, + boolean resolveIps); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java b/pico/api/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java similarity index 79% rename from pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java rename to pico/api/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java index a28c5b39ef7..dbdf2c6d98b 100644 --- a/pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java +++ b/pico/api/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -25,11 +25,11 @@ public interface PicoServicesProvider { /** - * Provide the {@code Pico} Services implementation, using the provided primordial {@link io.helidon.pico.Bootstrap} + * Provide the {@code Pico} Services implementation given the provided primordial {@link io.helidon.pico.Bootstrap} * configuration instance. * * @param bootstrap the primordial bootstrap configuration - * @return Pico services + * @return pico services instance configured with the provided bootstrap instance */ PicoServices services(Bootstrap bootstrap); diff --git a/pico/pico/src/main/java/io/helidon/pico/spi/package-info.java b/pico/api/src/main/java/io/helidon/pico/spi/package-info.java similarity index 86% rename from pico/pico/src/main/java/io/helidon/pico/spi/package-info.java rename to pico/api/src/main/java/io/helidon/pico/spi/package-info.java index 7af31ae5716..4b49b7ae31f 100644 --- a/pico/pico/src/main/java/io/helidon/pico/spi/package-info.java +++ b/pico/api/src/main/java/io/helidon/pico/spi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -15,6 +15,6 @@ */ /** - * SPI for Pico, to load the implementation. + * Pico SPI. */ package io.helidon.pico.spi; diff --git a/pico/pico/src/main/java/module-info.java b/pico/api/src/main/java/module-info.java similarity index 88% rename from pico/pico/src/main/java/module-info.java rename to pico/api/src/main/java/module-info.java index 3c7fcb3fed2..34597d5c947 100644 --- a/pico/pico/src/main/java/module-info.java +++ b/pico/api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -17,15 +17,14 @@ /** * Pico API module. */ -module io.helidon.pico { +module io.helidon.pico.api { + requires jakarta.inject; + requires io.helidon.common.types; requires io.helidon.common; requires io.helidon.common.config; - requires io.helidon.pico.types; requires static io.helidon.builder; requires static io.helidon.config.metadata; - requires static jakarta.annotation; - requires jakarta.inject; exports io.helidon.pico; exports io.helidon.pico.spi; diff --git a/pico/api/src/test/java/io/helidon/pico/DefaultPicoConfigTest.java b/pico/api/src/test/java/io/helidon/pico/DefaultPicoConfigTest.java new file mode 100644 index 00000000000..81bcab4973f --- /dev/null +++ b/pico/api/src/test/java/io/helidon/pico/DefaultPicoConfigTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022, 2023 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 io.helidon.common.config.Config; +import io.helidon.config.ConfigSources; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isEmptyOrNullString; + +class DefaultPicoConfigTest { + + @AfterEach + void reset() { + PicoServicesHolder.reset(); + } + + /** + * This tests the default pico configuration. + */ + @Test + void withOutBootstrap() { + DefaultPicoServicesConfig cfg = DefaultPicoServicesConfig.builder().build(); + assertThat(cfg.serviceLookupCaching(), equalTo(Boolean.FALSE)); + assertThat(cfg.activationLogs(), equalTo(Boolean.FALSE)); + assertThat(cfg.activationDeadlockDetectionTimeoutMillis(), equalTo(10000L)); + assertThat(cfg.permitsDynamic(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsDynamic(), equalTo(Boolean.TRUE)); + assertThat(cfg.permitsReflection(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsReflection(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsJsr330(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsJsr330Statics(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsJsr330Privates(), equalTo(Boolean.FALSE)); + assertThat(cfg.usesJsr330(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsCompileTime(), equalTo(Boolean.TRUE)); + assertThat(cfg.usesCompileTimeApplications(), equalTo(Boolean.TRUE)); + assertThat(cfg.usesCompileTimeModules(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsContextualLookup(), equalTo(Boolean.FALSE)); + assertThat(cfg.providerName(), isEmptyOrNullString()); + assertThat(cfg.providerVersion(), isEmptyOrNullString()); + } + + @Test + void withBootstrapWithoutConfig() { + DefaultBootstrap bootstrap = DefaultBootstrap.builder().build(); + PicoServicesHolder.bootstrap(bootstrap); + assertThat(PicoServicesHolder.bootstrap(false), optionalPresent()); + + // should be the same as if we had no bootstrap + withOutBootstrap(); + } + + @Test + void withBootStrapConfig() { + Config config = io.helidon.config.Config.builder( + ConfigSources.create( + Map.of(PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PROVIDER_NAME, "fake", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PROVIDER_VERSION, "fake", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_SERVICE_LOOKUP_CACHING, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_ACTIVATION_LOGS, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_ACTIVATION_DEADLOCK_TIMEOUT_IN_MILLIS, "111", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_REFLECTION, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_USES_JSR330, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_USES_COMPILE_TIME_APPLICATIONS, "false", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_USES_COMPILE_TIME_MODULES, "false" + ), "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + DefaultBootstrap bootstrap = DefaultBootstrap.builder() + .config(config) + .build(); + PicoServicesHolder.bootstrap(bootstrap); + assertThat(PicoServicesHolder.bootstrap(false), optionalPresent()); + + DefaultPicoServicesConfig cfg = DefaultPicoServicesConfig.builder().build(); + assertThat(cfg.serviceLookupCaching(), equalTo(Boolean.TRUE)); + assertThat(cfg.activationLogs(), equalTo(Boolean.TRUE)); + assertThat(cfg.activationDeadlockDetectionTimeoutMillis(), equalTo(111L)); + assertThat(cfg.permitsDynamic(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsDynamic(), equalTo(Boolean.TRUE)); + assertThat(cfg.permitsReflection(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsReflection(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsJsr330(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsJsr330Statics(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsJsr330Privates(), equalTo(Boolean.FALSE)); + assertThat(cfg.usesJsr330(), equalTo(Boolean.TRUE)); + assertThat(cfg.supportsCompileTime(), equalTo(Boolean.TRUE)); + assertThat(cfg.usesCompileTimeApplications(), equalTo(Boolean.FALSE)); + assertThat(cfg.usesCompileTimeModules(), equalTo(Boolean.FALSE)); + assertThat(cfg.supportsContextualLookup(), equalTo(Boolean.FALSE)); + assertThat(cfg.providerName(), isEmptyOrNullString()); + assertThat(cfg.providerVersion(), isEmptyOrNullString()); + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/PicoServicesTest.java b/pico/api/src/test/java/io/helidon/pico/PicoServicesTest.java similarity index 71% rename from pico/pico/src/test/java/io/helidon/pico/test/PicoServicesTest.java rename to pico/api/src/test/java/io/helidon/pico/PicoServicesTest.java index dc56a21e048..a9a13791dc2 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/PicoServicesTest.java +++ b/pico/api/src/test/java/io/helidon/pico/PicoServicesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,13 +14,12 @@ * limitations under the License. */ -package io.helidon.pico.test; +package io.helidon.pico; -import io.helidon.pico.Bootstrap; -import io.helidon.pico.DefaultBootstrap; -import io.helidon.pico.PicoServices; -import io.helidon.pico.test.testsubjects.PicoServices2; +import io.helidon.pico.testsubjects.PicoServices2; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; @@ -36,6 +35,12 @@ */ class PicoServicesTest { + @BeforeEach + @AfterEach + void reset() { + PicoServicesHolder.reset(); + } + /** * Test basic loader. */ @@ -44,16 +49,23 @@ void testGetPicoServices() { assertThat(PicoServices.globalBootstrap(), optionalEmpty()); Bootstrap bootstrap = DefaultBootstrap.builder().build(); PicoServices.globalBootstrap(bootstrap); + assertThat(PicoServices.globalBootstrap().orElseThrow(), sameInstance(bootstrap)); IllegalStateException e = assertThrows(IllegalStateException.class, () -> PicoServices.globalBootstrap(bootstrap)); - assertThat(e.getMessage(), equalTo("bootstrap already set")); + assertThat(e.getMessage(), + equalTo("bootstrap already set - use the (-D and/or -A) tag 'pico.debug=true' to see full trace output.")); - PicoServices picoServices = PicoServices.picoServices().get(); + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); assertThat(picoServices, notNullValue()); assertThat(picoServices, instanceOf(PicoServices2.class)); - assertThat(picoServices, sameInstance(PicoServices.picoServices().get())); + assertThat(picoServices, sameInstance(PicoServices.picoServices().orElseThrow())); assertThat(picoServices.bootstrap(), sameInstance(bootstrap)); } + @Test + void unrealizedServices() { + assertThat(PicoServices.unrealizedServices(), optionalEmpty()); + } + } diff --git a/pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java b/pico/api/src/test/java/io/helidon/pico/PriorityAndServiceTypeComparatorTest.java similarity index 93% rename from pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java rename to pico/api/src/test/java/io/helidon/pico/PriorityAndServiceTypeComparatorTest.java index 711d0d82ac1..4e5dc9de76c 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java +++ b/pico/api/src/test/java/io/helidon/pico/PriorityAndServiceTypeComparatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.test; +package io.helidon.pico; import java.util.ArrayList; import java.util.Comparator; @@ -22,9 +22,9 @@ 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 io.helidon.pico.testsubjects.PicoServices1Provider; +import io.helidon.pico.testsubjects.PicoServices2Provider; +import io.helidon.pico.testsubjects.PicoServices3Provider; import org.junit.jupiter.api.Test; diff --git a/pico/api/src/test/java/io/helidon/pico/testsubjects/AbstractPicoServices.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/AbstractPicoServices.java new file mode 100644 index 00000000000..9b70b00a69e --- /dev/null +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/AbstractPicoServices.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022, 2023 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.testsubjects; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.ActivationLog; +import io.helidon.pico.ActivationResult; +import io.helidon.pico.Injector; +import io.helidon.pico.Metrics; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.Services; + +abstract class AbstractPicoServices implements PicoServices { + + @Override + public PicoServicesConfig config() { + return null; + } + + @Override + public Optional services(boolean initialize) { + return Optional.empty(); + } + + @Override + public Services services() { + return null; + } + + @Override + public Optional injector() { + return Optional.empty(); + } + + @Override + public Optional> shutdown() { + return Optional.empty(); + } + + @Override + public Optional activationLog() { + return Optional.empty(); + } + + @Override + public Optional metrics() { + return Optional.empty(); + } + + @Override + public Optional> lookups() { + return Optional.empty(); + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1.java similarity index 71% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1.java index 89a8cc4c4e2..6a7ffe74555 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,32 +14,28 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import java.util.Objects; import io.helidon.pico.Bootstrap; -import io.helidon.pico.PicoServices; -import io.helidon.pico.Services; +import io.helidon.pico.PicoServicesConfig; -import jakarta.inject.Singleton; - -@Singleton -public class PicoServices1 implements PicoServices { +class PicoServices1 extends AbstractPicoServices { private final Bootstrap bootstrap; - public PicoServices1(Bootstrap bootstrap) { + PicoServices1(Bootstrap bootstrap) { this.bootstrap = Objects.requireNonNull(bootstrap); } @Override - public Services services() { - return null; + public Bootstrap bootstrap() { + return bootstrap; } @Override - public Bootstrap bootstrap() { - return bootstrap; + public PicoServicesConfig config() { + return null; } } diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1Provider.java similarity index 90% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1Provider.java index 99cb78693a2..40ed022aeb5 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices1Provider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import io.helidon.common.Weight; import io.helidon.pico.Bootstrap; diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2.java similarity index 67% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2.java index 75845b9d80e..3c84f9c22ed 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,29 +14,19 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import java.util.Objects; import io.helidon.pico.Bootstrap; -import io.helidon.pico.PicoServices; -import io.helidon.pico.Services; -import jakarta.inject.Singleton; - -@Singleton -public class PicoServices2 implements PicoServices { +public class PicoServices2 extends AbstractPicoServices { private final Bootstrap bootstrap; - public PicoServices2(Bootstrap bootstrap) { + PicoServices2(Bootstrap bootstrap) { this.bootstrap = Objects.requireNonNull(bootstrap); } - @Override - public Services services() { - return null; - } - @Override public Bootstrap bootstrap() { return bootstrap; diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2Provider.java similarity index 90% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2Provider.java index dc14ded4e16..07ba30a569a 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices2Provider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import io.helidon.common.Weight; import io.helidon.pico.Bootstrap; diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3.java similarity index 67% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3.java index 1ea459ec723..f8cef0eaaf7 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,29 +14,19 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import java.util.Objects; import io.helidon.pico.Bootstrap; -import io.helidon.pico.PicoServices; -import io.helidon.pico.Services; -import jakarta.inject.Singleton; - -@Singleton -public class PicoServices3 implements PicoServices { +class PicoServices3 extends AbstractPicoServices { private final Bootstrap bootstrap; - public PicoServices3(Bootstrap bootstrap) { + PicoServices3(Bootstrap bootstrap) { this.bootstrap = Objects.requireNonNull(bootstrap); } - @Override - public Services services() { - return null; - } - @Override public Bootstrap bootstrap() { return bootstrap; diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3Provider.java similarity index 90% rename from pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java rename to pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3Provider.java index b0a04387e44..c3a0ffd9b51 100644 --- a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java +++ b/pico/api/src/test/java/io/helidon/pico/testsubjects/PicoServices3Provider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.test.testsubjects; +package io.helidon.pico.testsubjects; import io.helidon.common.Weight; import io.helidon.pico.Bootstrap; diff --git a/pico/api/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServicesProvider b/pico/api/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServicesProvider new file mode 100644 index 00000000000..ac5eaff7079 --- /dev/null +++ b/pico/api/src/test/resources/META-INF/services/io.helidon.pico.spi.PicoServicesProvider @@ -0,0 +1,18 @@ +# +# 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. +# +io.helidon.pico.testsubjects.PicoServices1Provider +io.helidon.pico.testsubjects.PicoServices2Provider +io.helidon.pico.testsubjects.PicoServices3Provider diff --git a/pico/builder-config/README.md b/pico/builder-config/README.md deleted file mode 100644 index eb5083914e0..00000000000 --- a/pico/builder-config/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# pico-builder-config - -This is a specialization of the [builder](../builder) that extends the builder to support additional integration with Helidon's configuration sub-system. It adds support for the [@ConfigBean](builder-config/src/main/java/io/helidon/pico/builder/config/ConfigBean.java) annotation. When applied to a target interface it will map that interface to configuration via a new toBuilder method generated on the implementation as follows: - -```java - ... - - public static Builder toBuilder(io.helidon.common.config.Config cfg) { - ... - } - - ... -``` - -There are a few additional caveats to understand about ConfigBean and its supporting infrastructure. - -* @Builder can be used in conjunction with @ConfigBean. All attributed will be honored exception one... -* Builder.requireLibraryDependencies is not supported. All generated configuration beans and builders will minimally require a compile-time and runtime dependency on Helidon's common-config module. But for full fidelity support of Helidon's config one should instead use the full config module. - -## Modules -* [builder-config](builder-config) - annotations and other SPI types. -* [processor](processor) - the annotation processor that should be used when using ConfigBeans. -* [tests](tests) - tests that can also serve as examples for usage. diff --git a/pico/builder-config/builder-config/README.md b/pico/builder-config/builder-config/README.md deleted file mode 100644 index 0d1b5cf22a9..00000000000 --- a/pico/builder-config/builder-config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# pico-builder-config - -This module can be used at compile time and runtime. diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/ConfigBean.java b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/ConfigBean.java deleted file mode 100644 index 14a5ce61d88..00000000000 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/ConfigBean.java +++ /dev/null @@ -1,99 +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. - */ - -package io.helidon.pico.builder.config; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import io.helidon.builder.BuilderTrigger; - -/** - * A {@code ConfigBean} is another {@link io.helidon.builder.BuilderTrigger} which extends the - * {@link io.helidon.builder.Builder} concept in support of integration to Helidon's configuration sub-system. It provides - * everything that {@link io.helidon.builder.Builder} provides. However, unlike the base - * {@link io.helidon.builder.Builder} generated classes which can handle any object type, the types used within your target - * {@code ConfigBean}-annotated interface must have all of its attribute getter method types resolvable by Helidon's configuration - * sub-system. - *

        - * One should write a {@code ConfigBean}-annotated interface in such a way as to group the collection of configurable elements - * that logically belong together to then be delivered (and perhaps trigger an activation of) one or more java service types that - * are said to be {@code ConfiguredBy} the given {@link ConfigBean} instance. - *

        - * The {@code pico-builder-config-processor} module is required to be on the APT classpath to code-generate the implementation - * classes for the {@code ConfigBean}. - *

        - * Example: - *

        {@code
        - * @ConfigBean
        - * public interface MyConfigBean {
        - *     String getName();
        - *     int getPort();
        - * }
        - * }
        - *

        - * When {@code Pico} services and config-service modules are incorporated into the application lifecycle, the configuration - * sub-system is scanned at startup and {@code ConfigBean} instances are created and fed into the {@code ConfigBeanRegistry}. - * This mapping occurs based upon the {@link io.helidon.config.metadata.ConfiguredOption#key()} applied on the {@code ConfigBean} - * interface type. If no such declaration is found, then the type name is used as the key (e.g., MyConfigBean would map to - * "my-config-bean"). - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(java.lang.annotation.ElementType.TYPE) -@BuilderTrigger -public @interface ConfigBean { - - /** - * Determines whether an instance of this config bean in the bean registry will result in the backing service - * {@code ConfiguredBy} this bean to be activated. - * - * @return true if this config bean should drive {@code ConfiguredBy} service activation - */ - boolean drivesActivation() default true; - - /** - * An instance of this bean will be created if there are no instances discovered by the configuration provider(s) post - * startup, and will use all default values annotated using {@code ConfiguredOptions} from the bean interface methods. - * - * @return the default config bean instance using defaults - */ - boolean atLeastOne() default false; - - /** - * Determines whether there can be more than one bean instance of this type. - *

        - * If false then only 0..1 behavior will be permissible for active beans in the config registry. If true then {@code > 1} - * instances will be permitted. The default values is {@code true}. - *

        - * Note: this attribute is dynamic in nature, and therefore cannot be validated at compile time. All violations found to this - * policy will be observed during PicoServices activation. - * - * @return true if repeatable - */ - boolean repeatable() default true; - - /** - * The overridden key to use. If not set this will default to use the expanded name from the config subsystem, - * (e.g. MyConfig -> "my-config"). - * - * @return the overriding key to use - */ - String key() default ""; - -} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanInfo.java b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanInfo.java deleted file mode 100644 index 8a9c6482a90..00000000000 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanInfo.java +++ /dev/null @@ -1,81 +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. - */ - -package io.helidon.pico.builder.config.spi; - -import java.util.Objects; - -import io.helidon.builder.Builder; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * Represents all the attributes belonging to {@link io.helidon.pico.builder.config.ConfigBean} available in a - * {@link io.helidon.builder.Builder} style usage pattern. - */ -@Builder(implPrefix = "Meta") -public interface ConfigBeanInfo extends ConfigBean { - - /** - * Builds meta information appropriate for config integration from a - * {@link io.helidon.pico.builder.config.ConfigBean} instance. This will use the key if {@link #key()} is present, and - * if not present will default to the simple class name of the bean type. - * - * @param val the config bean instance - * @param cfgBeanType the config bean type - * @return the meta information for the config bean - */ - static MetaConfigBeanInfo toMetaConfigBeanInfo(ConfigBean val, - Class cfgBeanType) { - Objects.requireNonNull(cfgBeanType); - MetaConfigBeanInfo.Builder builder = MetaConfigBeanInfo.toBuilder(Objects.requireNonNull(val)); - String key = val.key(); - if (!key.isBlank()) { - builder.key(toConfigKey(cfgBeanType.getSimpleName())); - } - return builder.build(); - } - - /** - * Converts the name (i.e., simple class name or method name) into a config key. - *

        - * Method name is camel case (such as maxInitialLineLength) - * result is dash separated and lower cased (such as max-initial-line-length). - * - * @param name the input name - * @return the config key - */ - // note: this method is also found in ConfigMetadataHandler. - static String toConfigKey(String name) { - StringBuilder result = new StringBuilder(name.length() + 5); - - char[] chars = name.toCharArray(); - for (char aChar : chars) { - if (Character.isUpperCase(aChar)) { - if (result.length() == 0) { - result.append(Character.toLowerCase(aChar)); - } else { - result.append('-') - .append(Character.toLowerCase(aChar)); - } - } else { - result.append(aChar); - } - } - - return result.toString(); - } - -} diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java deleted file mode 100644 index f9cbaa80c1d..00000000000 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java +++ /dev/null @@ -1,120 +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. - */ - -package io.helidon.pico.builder.config.spi; - -import java.util.Collection; -import java.util.Map; -import java.util.Optional; - -import io.helidon.common.Weight; -import io.helidon.common.Weighted; -import io.helidon.common.config.Config; -import io.helidon.common.config.ConfigValue; - -import jakarta.inject.Singleton; - -/** - * The default implementation of {@link ConfigResolver} simply resolves against {@link io.helidon.common.config.Config} directly. - */ -@Singleton -@Weight(Weighted.DEFAULT_WEIGHT - 1) // allow all other creators to take precedence over us... -public class DefaultConfigResolver implements ConfigResolver, ConfigResolverProvider { - - /** - * Tag that represents meta information about the attribute. Used in the maps for various methods herein. - */ - public static final String TAG_META = "__meta"; - - /** - * Default constructor, service loader invoked. - */ - @Deprecated - public DefaultConfigResolver() { - } - - @Override - public ConfigResolver configResolver() { - return this; - } - - @Override - public Optional of(ResolutionContext ctx, - Map> meta, - ConfigResolverRequest request) { - Config attrCfg = ctx.config().get(request.configKey()); - return attrCfg.exists() - ? optionalWrappedConfig(attrCfg, meta, request) : Optional.empty(); - } - - @Override - @SuppressWarnings("unchecked") - public Optional> ofCollection(ResolutionContext ctx, - Map> meta, - ConfigResolverRequest request) { - Config attrCfg = ctx.config().get(request.configKey()); - return attrCfg.exists() - ? (Optional>) optionalWrappedConfig(attrCfg, meta, request) : Optional.empty(); - } - - @Override - @SuppressWarnings("unchecked") - public Optional> ofMap(ResolutionContext ctx, - Map> meta, - ConfigResolverMapRequest request) { - Config attrCfg = ctx.config().get(request.configKey()); - return attrCfg.exists() - ? (Optional>) optionalWrappedConfig(attrCfg, meta, request) : Optional.empty(); - } - - @SuppressWarnings("unchecked") - private Optional optionalWrappedConfig(Config attrCfg, - Map> meta, - ConfigResolverRequest request) { - Class componentType = request.valueComponentType().orElse(null); - Class type = request.valueType(); - final boolean isOptional = Optional.class.equals(type); - if (isOptional) { - type = request.valueComponentType().orElseThrow(); - } - final boolean isCharArray = (type.isArray() && char.class == type.getComponentType()); - if (isCharArray) { - type = String.class; - } - - try { - ConfigValue attrVal = attrCfg.as(type); - Object val = attrVal.get(); - if (isCharArray) { - val = ((String) val).toCharArray(); - } - - return Optional.of(isOptional ? (T) Optional.of(val) : (T) val); - } catch (Exception e) { - String typeName = toTypeNameDescription(request.valueType(), componentType); - String configKey = attrCfg.key().toString(); - throw new IllegalStateException("Failed to convert " + typeName - + " for attribute: " + request.attributeName() - + " and config key: " + configKey, e); - } - } - - private static String toTypeNameDescription(Class type, - Class componentType) { - return type.getTypeName() + "<" + componentType.getTypeName() + ">"; - } - -} diff --git a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/MetaConfigBeanInfoTest.java b/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/MetaConfigBeanInfoTest.java deleted file mode 100644 index e473fb1602f..00000000000 --- a/pico/builder-config/builder-config/src/test/java/io/helidon/pico/builder/config/spi/test/MetaConfigBeanInfoTest.java +++ /dev/null @@ -1,55 +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. - */ - -package io.helidon.pico.builder.config.spi.test; - -import io.helidon.pico.builder.config.ConfigBean; -import io.helidon.pico.builder.config.spi.ConfigBeanInfo; -import io.helidon.pico.builder.config.spi.MetaConfigBeanInfo; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@ConfigBean() -class MetaConfigBeanInfoTest { - - @Test - void testToMetaConfigBeanInfo() { - ConfigBean cfg = getClass().getAnnotation(ConfigBean.class); - assertNotNull(cfg); - MetaConfigBeanInfo metaCfg = ConfigBeanInfo.toMetaConfigBeanInfo(cfg, ConfigBean.class); - assertThat(metaCfg.annotationType(), sameInstance(ConfigBean.class)); - assertThat(metaCfg.repeatable(), is(true)); - assertThat(metaCfg.drivesActivation(), is(true)); - assertThat(metaCfg.atLeastOne(), is(false)); - assertThat(metaCfg.key(), is("")); - } - - @Test - void testToConfigKey() { - assertAll( - () -> assertThat(ConfigBeanInfo.toConfigKey("maxInitialLineLength"), is("max-initial-line-length")), - () -> assertThat(ConfigBeanInfo.toConfigKey("port"), is("port")), - () -> assertThat(ConfigBeanInfo.toConfigKey("listenAddress"), is("listen-address")) - ); - } - -} diff --git a/pico/builder-config/processor/src/test/java/io/helidon/pico/builder/config/tools/tests/ConfigBeanBuilderCreatorTest.java b/pico/builder-config/processor/src/test/java/io/helidon/pico/builder/config/tools/tests/ConfigBeanBuilderCreatorTest.java deleted file mode 100644 index 1bcb0756c10..00000000000 --- a/pico/builder-config/processor/src/test/java/io/helidon/pico/builder/config/tools/tests/ConfigBeanBuilderCreatorTest.java +++ /dev/null @@ -1,37 +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. - */ - -package io.helidon.pico.builder.config.tools.tests; - -import io.helidon.pico.builder.config.ConfigBean; -import io.helidon.pico.builder.config.processor.ConfigBeanBuilderCreator; -import io.helidon.builder.processor.spi.BuilderCreatorProvider; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; - -class ConfigBeanBuilderCreatorTest { - - @Test - void supportedAnnotationTypes() { - BuilderCreatorProvider creator = new ConfigBeanBuilderCreator(); - assertEquals(1, creator.supportedAnnotationTypes().size()); - assertSame(ConfigBean.class, creator.supportedAnnotationTypes().iterator().next()); - } - -} diff --git a/pico/builder-config/tests/configbean/README.md b/pico/builder-config/tests/configbean/README.md deleted file mode 100644 index a9c613736c9..00000000000 --- a/pico/builder-config/tests/configbean/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# pico-builder-config-test-config -Tests for ConfigBean-generated builder types. diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeComponentTracingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeComponentTracingConfig.java deleted file mode 100644 index f0f889ace61..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeComponentTracingConfig.java +++ /dev/null @@ -1,232 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.Map; - -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka ComponentTracing. - * - * A component is a single "layer" of the application that can trace. - * Component examples: - *

          - *
        • web-server: webServer adds the root tracing span + two additional spans (content-read and content-write)
        • - *
        • security: security adds the overall request security span, a span for authentication ("security:atn"), a span for - * authorization "security:atz", and a span for response processing ("security:response")
        • - *
        • jax-rs: JAX-RS integration adds spans for overall resource invocation
        • - *
        - */ -@ConfigBean -public interface FakeComponentTracingConfig extends FakeTraceableConfig { -// /** -// * Disabled component - all subsequent calls return disabled spans and logs. -// */ -// public static final ComponentTracingConfig DISABLED = ComponentTracingConfig.builder("disabled").enabled(false).build(); -// /** -// * Enabled component - all subsequent calls return enabled spans and logs. -// */ -// public static final ComponentTracingConfig ENABLED = ComponentTracingConfig.builder("enabled").build(); - -// /** -// * A new named component. -// * -// * @param name name of the component -// */ -// protected ComponentTracingConfig(String name) { -// super(name); -// } -// - -// /** -// * Merge configuration of two traced components. This enabled hierarchical configuration -// * with common, default configuration in one traced component and override in another. -// * -// * @param older the older configuration with "defaults" -// * @param newer the newer configuration to override defaults in older -// * @return merged component -// */ -// static ComponentTracingConfig merge(ComponentTracingConfig older, ComponentTracingConfig newer) { -// return new ComponentTracingConfig(newer.name()) { -// @Override -// public Optional getSpan(String spanName) { -// if (!enabled()) { -// return Optional.of(SpanTracingConfig.DISABLED); -// } -// -// Optional newSpan = newer.getSpan(spanName); -// Optional oldSpan = older.getSpan(spanName); -// -// // both configured -// if (newSpan.isPresent() && oldSpan.isPresent()) { -// return Optional.of(SpanTracingConfig.merge(oldSpan.get(), newSpan.get())); -// } -// -// // only newer -// if (newSpan.isPresent()) { -// return newSpan; -// } -// -// return oldSpan; -// } -// -// @Override -// public Optional isEnabled() { -// return newer.isEnabled() -// .or(older::isEnabled); -// } -// }; -// } -// -// /** -// * Get a traced span configuration for a named span. -// * -// * @param spanName name of the span in this component -// * @return configuration of that span if present -// */ -// protected abstract Optional getSpan(String spanName); -// -// /** -// * Get a traced span configuration for a named span. -// * -// * @param spanName name of a span in this component -// * @return configuration of the span, or enabled configuration if not configured -// * @see #span(String, boolean) -// */ -// public SpanTracingConfig span(String spanName) { -// return span(spanName, true); -// } - - @Singular("span") // Builder::addSpan(String span, FakeSpanLogTracingConfigBean val), Impl::getSpan(String span), etc. - Map spanLogMap(); - -// -// /** -// * Get a traced span configuration for a named span. -// * -// * @param spanName name of a span in this component -// * @param enabledByDefault whether the result is enabled if a configuration is not present -// * @return configuration of the span, or a span configuration enabled or disabled depending on {@code enabledByDefault} if -// * not configured -// */ -// public SpanTracingConfig span(String spanName, boolean enabledByDefault) { -// if (enabled()) { -// return getSpan(spanName).orElseGet(() -> enabledByDefault ? SpanTracingConfig.ENABLED : SpanTracingConfig.DISABLED); -// } -// -// return SpanTracingConfig.DISABLED; -// } -// -// /** -// * Fluent API builder for traced component. -// * -// * @param name the name of the component -// * @return a new builder instance -// */ -// public static Builder builder(String name) { -// return new Builder(name); -// } -// -// /** -// * Create a new traced component configuration from {@link Config}. -// * -// * @param name name of the component -// * @param config config for a new component -// * @return a new traced component configuration -// */ -// public static ComponentTracingConfig create(String name, Config config) { -// return builder(name) -// .config(config) -// .build(); -// } -// -// /** -// * Fluent API builder for {@link ComponentTracingConfig}. -// */ -// public static final class Builder implements io.helidon.common.Builder { -// private final Map tracedSpans = new HashMap<>(); -// private Optional enabled = Optional.empty(); -// private final String name; -// -// private Builder(String name) { -// this.name = name; -// } -// -// @Override -// public ComponentTracingConfig build() { -// // immutability -// final Optional finalEnabled = enabled; -// final Map finalSpans = new HashMap<>(tracedSpans); -// return new ComponentTracingConfig(name) { -// @Override -// public Optional getSpan(String spanName) { -// if (enabled.orElse(true)) { -// return Optional.ofNullable(finalSpans.get(spanName)); -// } else { -// return Optional.of(SpanTracingConfig.DISABLED); -// } -// } -// -// @Override -// public Optional isEnabled() { -// return finalEnabled; -// } -// }; -// } -// -// /** -// * Update this builder from {@link io.helidon.config.Config}. -// * -// * @param config configuration of a traced component -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// config.get("spans").asNodeList().ifPresent(spanConfigList -> { -// spanConfigList.forEach(spanConfig -> { -// // span name is mandatory -// addSpan(SpanTracingConfig.create(spanConfig.get("name").asString().get(), spanConfig)); -// }); -// }); -// return this; -// } -// -// /** -// * Add a new traced span configuration. -// * -// * @param span configuration of a traced span -// * @return updated builder instance -// */ -// public Builder addSpan(SpanTracingConfig span) { -// this.tracedSpans.put(span.name(), span); -// return this; -// } -// -// /** -// * Configure whether this component is enabled or disabled. -// * -// * @param enabled if disabled, all spans and logs will be disabled -// * @return updated builder instance -// */ -// public Builder enabled(boolean enabled) { -// this.enabled = Optional.of(enabled); -// return this; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeyConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeyConfig.java deleted file mode 100644 index 9c4381e9054..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeyConfig.java +++ /dev/null @@ -1,842 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; - -import io.helidon.builder.Builder; - -/** - * aka KeyConfig. - * - */ -@Builder -public interface FakeKeyConfig { - - // /*private static final*/ String DEFAULT_PRIVATE_KEY_ALIAS = "1"; -// /*private static final*/ char[] EMPTY_CHARS = new char[0]; - -// private static final Logger LOGGER = Logger.getLogger(FakeKeyConfigBean.class.getName()); -// -// private final PrivateKey privateKey; -// private final PublicKey publicKey; -// private final X509Certificate publicCert; -// private final List certChain = new LinkedList<>(); -// private final List certificates = new LinkedList<>(); -// -// private FakeKeyConfigBean(PrivateKey privateKey, -// PublicKey publicKey, -// X509Certificate publicCert, -// Collection certChain, -// Collection certificates) { -// -// this.privateKey = privateKey; -// this.publicKey = publicKey; -// this.publicCert = publicCert; -// this.certChain.addAll(certChain); -// this.certificates.addAll(certificates); -// } -// -// /** -// * Load key config from config. -// * -// * @param config config instance located at keys configuration (expects "keystore-path" child) -// * @return KeyConfig loaded from config -// * @throws PkiException when keys or certificates fail to load from keystore or when misconfigured -// */ -// public static FakeKeyConfigBean create(Config config) throws PkiException { -// try { -// return fullBuilder().config(config).build(); -// } catch (ResourceException e) { -// throw new PkiException("Failed to load from config", e); -// } -// } -// -// /** -// * Creates a new builder to configure instance. -// * -// * @return builder instance -// */ -// public static Builder fullBuilder() { -// return new Builder(); -// } -// -// /** -// * Build this instance from PEM files (usually a pair of private key and certificate chain). -// * Call {@link PemBuilder#build()} to build the instance. -// * If you need to add additional information to {@link FakeKeyConfigBean}, use {@link PemBuilder#toFullBuilder()}. -// * -// * @return builder for PEM files -// */ -// public static PemBuilder pemBuilder() { -// return new PemBuilder(); -// } -// -// /** -// * Build this instance from a java keystore (such as PKCS12 keystore). -// * Call {@link KeystoreBuilder#build()} to build the instance. -// * If you need to add additional information to {@link FakeKeyConfigBean}, use {@link PemBuilder#toFullBuilder()}. -// * -// * @return builder for Keystore -// */ -// public static KeystoreBuilder keystoreBuilder() { -// return new KeystoreBuilder(); -// } -// - /** - * The public key of this config if configured. - * - * @return the public key of this config or empty if not configured - */ - /*public*/ Optional publicKey(); -// { -// return Optional.ofNullable(publicKey); -// } - - /** - * The private key of this config if configured. - * - * @return the private key of this config or empty if not configured - */ - /*public*/ Optional privateKey(); -// { -// return Optional.ofNullable(privateKey); -// } - - /** - * The public X.509 Certificate if configured. - * - * @return the public certificate of this config or empty if not configured - */ - /*public*/ Optional publicCert(); -// { -// return Optional.ofNullable(publicCert); -// } - - /** - * The X.509 Certificate Chain. - * - * @return the certificate chain or empty list if not configured - */ - /*public*/ List certChain(); -// { -// return Collections.unmodifiableList(certChain); -// } - - /** - * The X.509 Certificates. - * - * @return the certificates configured or empty list if none configured - */ - /*public*/ List certs(); -// { -// return Collections.unmodifiableList(certificates); -// } - - - /** - * @see FakeKeystoreConfig - */ -// public DefaultFakeConfigBean.Builder config(Config config) { -// Config keystoreConfig = config.get("keystore"); -// -// // the actual resource (file, classpath) with the bytes of the keystore -// keystoreConfig.get("resource").as(Resource::create).ifPresent(this::keystore); -// -// // type of keystore -// keystoreConfig.get("type") -// .asString() -// .ifPresent(this::keystoreType); -// // password of the keystore -// keystoreConfig.get("passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keystorePassphrase); -// // private key alias -// keystoreConfig.get("key.alias") -// .asString() -// .ifPresent(this::keyAlias); -// // private key password -// keystoreConfig.get("key.passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keyPassphrase); -// keystoreConfig.get("cert.alias") -// .asString() -// .ifPresent(this::certAlias); -// keystoreConfig.get("cert-chain.alias") -// .asString() -// .ifPresent(this::certChainAlias); -// // whether this is a keystore (with a private key) or a trust store (just trusted public keys/certificates) -// keystoreConfig.get("trust-store") -// .asBoolean() -// .ifPresent(this::trustStore); -// -// return this; -// } - - -// /** -// * Fluent API builder for {@link FakeKeyConfigBean}. -// * Call {@link #build()} to create an instance. -// * -// * The keys may be loaded from multiple possible sources. -// * -// * @see FakeKeyConfigBean#keystoreBuilder() -// * @see FakeKeyConfigBean#pemBuilder() -// * @see FakeKeyConfigBean#fullBuilder() -// */ -// @Configured -// public static class Builder implements io.helidon.common.Builder { -// private PrivateKey explicitPrivateKey; -// private PublicKey explicitPublicKey; -// private X509Certificate explicitPublicCert; -// private final List explicitCertChain = new LinkedList<>(); -// private final List explicitCertificates = new LinkedList<>(); -// -// /** -// * Build a new instance of the configuration based on this builder. -// * -// * @return instance from this builder -// * @throws PkiException when keys or certificates fail to load from keystore or when misconfigured -// */ -// @Override -// public FakeKeyConfigBean build() throws PkiException { -// PrivateKey privateKey = this.explicitPrivateKey; -// PublicKey publicKey = this.explicitPublicKey; -// X509Certificate publicCert = this.explicitPublicCert; -// List certChain = new LinkedList<>(explicitCertChain); -// List certificates = new LinkedList<>(explicitCertificates); -// -// // fix public key if cert is provided -// if (null == publicKey && null != publicCert) { -// publicKey = publicCert.getPublicKey(); -// } -// -// return new FakeKeyConfigBean(privateKey, publicKey, publicCert, certChain, certificates); -// } -// -// /** -// * Configure a private key instance (rather then keystore and alias). -// * -// * @param privateKey private key instance -// * @return updated builder instance -// */ -// public Builder privateKey(PrivateKey privateKey) { -// this.explicitPrivateKey = privateKey; -// return this; -// } -// -// /** -// * Configure a public key instance (rather then keystore and certificate alias). -// * -// * @param publicKey private key instance -// * @return updated builder instance -// */ -// public Builder publicKey(PublicKey publicKey) { -// this.explicitPublicKey = publicKey; -// return this; -// } -// -// /** -// * Configure an X.509 certificate instance for public key certificate. -// * -// * @param certificate certificate instance -// * @return updated builder instance -// */ -// public Builder publicKeyCert(X509Certificate certificate) { -// this.explicitPublicCert = certificate; -// return this; -// } -// -// /** -// * Add an X.509 certificate instance to the end of certification chain. -// * -// * @param certificate certificate to add to certification path -// * @return updated builder instance -// */ -// public Builder addCertChain(X509Certificate certificate) { -// this.explicitCertChain.add(certificate); -// return this; -// } -// -// /** -// * Add a certificate to the list of certificates, used e.g. in a trust store. -// * -// * @param certificate X.509 certificate to trust -// * @return updated builder instance -// */ -// public Builder addCert(X509Certificate certificate) { -// this.explicitCertificates.add(certificate); -// return this; -// } -// -// /** -// * Update this builder with information from a pem builder. -// * -// * @param builder builder obtained from {@link FakeKeyConfigBean#pemBuilder()} -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "pem") -// public Builder updateWith(PemBuilder builder) { -// builder.updateBuilder(this); -// return this; -// } -// -// /** -// * Update this builder with information from a keystore builder. -// * -// * @param builder builder obtained from {@link FakeKeyConfigBean#keystoreBuilder()} ()} -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "keystore") -// public Builder updateWith(KeystoreBuilder builder) { -// builder.updateBuilder(this); -// return this; -// } -// -// /** -// * Updated this builder instance from configuration. -// * Keys configured will override existing fields in this builder, others will be left intact. -// * If certification path is already defined, configuration based cert-path will be added. -// * -// * @param config configuration to update this builder from -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// updateWith(pemBuilder().config(config)); -// updateWith(keystoreBuilder().config(config)); -// -// return this; -// } -// } -// -// /** -// * Builder for resources from a java keystore (PKCS12, JKS etc.). Obtain an instance through {@link -// * FakeKeyConfigBean#keystoreBuilder()}. -// */ -// @Configured(ignoreBuildMethod = true) -// public static final class KeystoreBuilder implements io.helidon.common.Builder { -// private static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; -// -// private String keystoreType = DEFAULT_KEYSTORE_TYPE; -// private char[] keystorePassphrase = EMPTY_CHARS; -// private char[] keyPassphrase = null; -// private String keyAlias; -// private String certAlias; -// private String certChainAlias; -// private boolean addAllCertificates; -// private final List certificateAliases = new LinkedList<>(); -// private final StreamHolder keystoreStream = new StreamHolder("keystore"); -// -// private KeystoreBuilder() { -// } -// -// /** -// * If you want to build a trust store, call this method to add all -// * certificates present in the keystore to certificate list. -// * -// * @return updated builder instance -// */ -// @ConfiguredOption(type = Boolean.class, value = "false") -// public KeystoreBuilder trustStore() { -// return trustStore(true); -// } -// -// private KeystoreBuilder trustStore(boolean isTrustStore) { -// this.addAllCertificates = isTrustStore; -// return this; -// } -// -// /** -// * Add an alias to list of aliases used to generate a trusted set of certificates. -// * -// * @param alias alias of a certificate -// * @return updated builder instance -// */ -// public KeystoreBuilder addCertAlias(String alias) { -// certificateAliases.add(alias); -// return this; -// } -// -// /** -// * Keystore resource definition. -// * -// * @param keystore keystore resource, from file path, classpath, URL etc. -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "resource", required = true) -// public KeystoreBuilder keystore(Resource keystore) { -// this.keystoreStream.stream(keystore); -// return this; -// } -// -// /** -// * Set type of keystore. -// * Defaults to "PKCS12", expected are other keystore types supported by java then can store keys under aliases. -// * -// * @param keystoreType keystore type to load the key -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "type", value = "PKCS12") -// public KeystoreBuilder keystoreType(String keystoreType) { -// this.keystoreType = keystoreType; -// return this; -// } -// -// /** -// * Pass-phrase of the keystore (supported with JKS and PKCS12 keystores). -// * -// * @param keystorePassphrase keystore pass-phrase -// * @return updated builder instance -// */ -// public KeystoreBuilder keystorePassphrase(char[] keystorePassphrase) { -// this.keystorePassphrase = Arrays.copyOf(keystorePassphrase, keystorePassphrase.length); -// -// return this; -// } -// -// /** -// * Pass-phrase of the keystore (supported with JKS and PKCS12 keystores). -// * -// * @param keystorePassword keystore password to use, calls {@link #keystorePassphrase(char[])} -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "passphrase") -// public KeystoreBuilder keystorePassphrase(String keystorePassword) { -// return keystorePassphrase(keystorePassword.toCharArray()); -// } -// -// /** -// * Alias of the private key in the keystore. -// * -// * @param keyAlias alias of the key in the keystore -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "key.alias", value = "1") -// public KeystoreBuilder keyAlias(String keyAlias) { -// this.keyAlias = keyAlias; -// return this; -// } -// -// /** -// * Alias of X.509 certificate of public key. -// * Used to load both the certificate and public key. -// * -// * @param alias alias under which the certificate is stored in the keystore -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "cert.alias") -// public KeystoreBuilder certAlias(String alias) { -// this.certAlias = alias; -// return this; -// } -// -// /** -// * Alias of an X.509 chain. -// * -// * @param alias alias of certificate chain in the keystore -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "cert-chain.alias") -// public KeystoreBuilder certChainAlias(String alias) { -// this.certChainAlias = alias; -// return this; -// } -// -// /** -// * Pass-phrase of the key in the keystore (used for private keys). -// * This is (by default) the same as keystore passphrase - only configure -// * if it differs from keystore passphrase. -// * -// * @param privateKeyPassphrase pass-phrase of the key -// * @return updated builder instance -// */ -// public KeystoreBuilder keyPassphrase(char[] privateKeyPassphrase) { -// this.keyPassphrase = Arrays.copyOf(privateKeyPassphrase, privateKeyPassphrase.length); -// -// return this; -// } -// -// /** -// * Pass-phrase of the key in the keystore (used for private keys). -// * This is (by default) the same as keystore passphrase - only configure -// * if it differs from keystore passphrase. -// * -// * @param privateKeyPassphrase pass-phrase of the key -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "key.passphrase") -// public KeystoreBuilder keyPassphrase(String privateKeyPassphrase) { -// return keyPassphrase(privateKeyPassphrase.toCharArray()); -// } -// -// /** -// * Create an instance of {@link FakeKeyConfigBean} based on this builder. -// * -// * @return new key config based on a keystore -// */ -// @Override -// public FakeKeyConfigBean build() { -// return toFullBuilder().build(); -// } -// -// /** -// * Create a builder for {@link FakeKeyConfigBean} from this keystore builder. This allows you to enhance the config -// * with additional (explicit) fields. -// * -// * @return builder of {@link FakeKeyConfigBean} -// */ -// public Builder toFullBuilder() { -// return updateBuilder(FakeKeyConfigBean.fullBuilder()); -// } -// - -// private Builder updateBuilder(Builder builder) { -// if (keystoreStream.isSet()) { -// if (null == keyPassphrase) { -// keyPassphrase = keystorePassphrase; -// } -// KeyStore keyStore; -// -// try { -// keyStore = PkiUtil.loadKeystore(keystoreType, -// keystoreStream.stream(), -// keystorePassphrase, -// keystoreStream.message()); -// } finally { -// keystoreStream.closeStream(); -// } -// -// // attempt to read private key -// boolean guessing = false; -// if (null == keyAlias) { -// keyAlias = DEFAULT_PRIVATE_KEY_ALIAS; -// guessing = true; -// } -// try { -// builder.privateKey(PkiUtil.loadPrivateKey(keyStore, keyAlias, keyPassphrase)); -// } catch (Exception e) { -// if (guessing) { -// LOGGER.log(Level.FINEST, "Failed to read private key from default alias", e); -// } else { -// throw e; -// } -// } -// -// List certChain = null; -// if (null == certChainAlias) { -// guessing = true; -// // by default, cert chain uses the same alias as private key -// certChainAlias = keyAlias; -// } else { -// guessing = false; -// } -// -// if (null != certChainAlias) { -// try { -// certChain = PkiUtil.loadCertChain(keyStore, certChainAlias); -// certChain.forEach(builder::addCertChain); -// } catch (Exception e) { -// if (guessing) { -// LOGGER.log(Level.FINEST, "Failed to certificate chain from alias \"" + certChainAlias + "\"", e); -// } else { -// throw e; -// } -// } -// } -// -// if (null == certAlias) { -// // no explicit public key certificate, just load it from cert chain if present -// if (null != certChain && !certChain.isEmpty()) { -// builder.publicKeyCert(certChain.get(0)); -// } -// } else { -// builder.publicKeyCert(PkiUtil.loadCertificate(keyStore, certAlias)); -// } -// -// if (addAllCertificates) { -// PkiUtil.loadCertificates(keyStore).forEach(builder::addCert); -// } else { -// certificateAliases.forEach(it -> builder.addCert(PkiUtil.loadCertificate(keyStore, it))); -// } -// } -// return builder; -// } -// -// /** -// * Update this builder from configuration. -// * The following keys are expected under key {@code keystore}: -// *
          -// *
        • {@code resource}: resource configuration as understood by {@link io.helidon.common.configurable.Resource}
        • -// *
        • {@code type}: type of keystore (defaults to PKCS12)
        • -// *
        • {@code passphrase}: passphrase of keystore, if required
        • -// *
        • {@code key.alias}: alias of private key, if wanted (defaults to "1")
        • -// *
        • {@code key.passphrase}: passphrase of private key if differs from keystore passphrase
        • -// *
        • {@code cert.alias}: alias of public certificate (to obtain public key)
        • -// *
        • {@code cert-chain.alias}: alias of certificate chain
        • -// *
        • {@code trust-store}: true if this is a trust store (and we should load all certificates from it), defaults to false
        • -// *
        -// * -// * @param config configuration instance -// * @return updated builder instance -// */ -// public KeystoreBuilder config(Config config) { -// Config keystoreConfig = config.get("keystore"); -// -// // the actual resource (file, classpath) with the bytes of the keystore -// keystoreConfig.get("resource").as(Resource::create).ifPresent(this::keystore); -// -// // type of keystore -// keystoreConfig.get("type") -// .asString() -// .ifPresent(this::keystoreType); -// // password of the keystore -// keystoreConfig.get("passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keystorePassphrase); -// // private key alias -// keystoreConfig.get("key.alias") -// .asString() -// .ifPresent(this::keyAlias); -// // private key password -// keystoreConfig.get("key.passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keyPassphrase); -// keystoreConfig.get("cert.alias") -// .asString() -// .ifPresent(this::certAlias); -// keystoreConfig.get("cert-chain.alias") -// .asString() -// .ifPresent(this::certChainAlias); -// // whether this is a keystore (with a private key) or a trust store (just trusted public keys/certificates) -// keystoreConfig.get("trust-store") -// .asBoolean() -// .ifPresent(this::trustStore); -// -// return this; -// } -// } -// - -// /** -// * Builder for PEM files - accepts private key and certificate chain. Obtain an instance through {@link -// * FakeKeyConfigBean#pemBuilder()}. -// * -// * If you have "standard" linux/unix private key, you must run " -// * {@code openssl pkcs8 -topk8 -in ./id_rsa -out ./id_rsa.p8}" on it to work with this builder for password protected -// * file; or "{@code openssl pkcs8 -topk8 -in ./id_rsa -out ./id_rsa_nocrypt.p8 -nocrypt}" for unprotected file. -// * -// * The only supported format is PKCS#8. If you have a different format, you must to transform it to PKCS8 PEM format (to -// * use this builder), or to PKCS#12 keystore format (and use {@link KeystoreBuilder}). -// */ -// @Configured(ignoreBuildMethod = true) -// public static final class PemBuilder implements io.helidon.common.Builder { -// private final StreamHolder privateKeyStream = new StreamHolder("privateKey"); -// private final StreamHolder publicKeyStream = new StreamHolder("publicKey"); -// private final StreamHolder certChainStream = new StreamHolder("certChain"); -// private final StreamHolder certificateStream = new StreamHolder("certificate"); -// private char[] pemKeyPassphrase; -// -// private PemBuilder() { -// } -// -// /** -// * Read a private key from PEM format from a resource definition. -// * -// * @param resource key resource (file, classpath, URL etc.) -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "key.resource") -// public PemBuilder key(Resource resource) { -// privateKeyStream.stream(resource); -// return this; -// } -// -// /** -// * Read a public key from PEM format from a resource definition. -// * -// * @param resource key resource (file, classpath, URL etc.) -// * @return updated builder instance -// */ -// public PemBuilder publicKey(Resource resource) { -// publicKeyStream.stream(resource); -// return this; -// } -// -// /** -// * Passphrase for private key. If the key is encrypted (and in PEM PKCS#8 format), this passphrase will be used to -// * decrypt it. -// * -// * @param passphrase passphrase used to encrypt the private key -// * @return updated builder instance -// */ -// public PemBuilder keyPassphrase(char[] passphrase) { -// this.pemKeyPassphrase = Arrays.copyOf(passphrase, passphrase.length); -// -// return this; -// } -// -// /** -// * Passphrase for private key. If the key is encrypted (and in PEM PKCS#8 format), this passphrase will be used to -// * decrypt it. -// * -// * @param passphrase passphrase used to encrypt the private key -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "key.passphrase") -// public PemBuilder keyPassphrase(String passphrase) { -// return keyPassphrase(passphrase.toCharArray()); -// } -// -// /** -// * Load certificate chain from PEM resource. -// * -// * @param resource resource (e.g. classpath, file path, URL etc.) -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "cert-chain.resource") -// public PemBuilder certChain(Resource resource) { -// certChainStream.stream(resource); -// return this; -// } -// -// /** -// * Read one or more certificates in PEM format from a resource definition. Used eg: in a trust store. -// * -// * @param resource key resource (file, classpath, URL etc.) -// * @return updated builder instance -// */ -// public PemBuilder certificates(Resource resource) { -// certificateStream.stream(resource); -// return this; -// } -// -// /** -// * Build {@link FakeKeyConfigBean} based on information from PEM files only. -// * -// * @return new instance configured from this builder -// */ -// @Override -// public FakeKeyConfigBean build() { -// return toFullBuilder().build(); -// } -// -// /** -// * Get a builder filled from this builder to add additional information (such as public key from certificate etc.). -// * -// * @return builder for {@link FakeKeyConfigBean} -// */ -// public Builder toFullBuilder() { -// return updateBuilder(FakeKeyConfigBean.fullBuilder()); -// } -// -// private Builder updateBuilder(Builder builder) { -// if (privateKeyStream.isSet()) { -// builder.privateKey(PemReader.readPrivateKey(privateKeyStream.stream(), pemKeyPassphrase)); -// } -// if (publicKeyStream.isSet()) { -// builder.publicKey(PemReader.readPublicKey(publicKeyStream.stream())); -// } -// -// if (certChainStream.isSet()) { -// List chain = PemReader.readCertificates(certChainStream.stream()); -// chain.forEach(builder::addCertChain); -// if (!chain.isEmpty()) { -// builder.publicKeyCert(chain.get(0)); -// } -// } -// -// if (certificateStream.isSet()) { -// PemReader.readCertificates(certificateStream.stream()).forEach(builder::addCert); -// } -// -// return builder; -// } -// -// /** -// * Update this builder from configuration. -// * Expected keys: -// *
          -// *
        • pem-key-path - path to PEM private key file (PKCS#8 format)
        • -// *
        • pem-key-resource-path - path to resource on classpath
        • -// *
        • pem-key-passphrase - passphrase of private key if encrypted
        • -// *
        • pem-cert-chain-path - path to certificate chain PEM file
        • -// *
        • pem-cert-chain-resource-path - path to resource on classpath
        • -// *
        -// * -// * @param config configuration to update builder from -// * @return updated builder instance -// */ -// public PemBuilder config(Config config) { -// Config pemConfig = config.get("pem"); -// pemConfig.get("key.resource").as(Resource::create).ifPresent(this::key); -// pemConfig.get("key.passphrase").asString().map(String::toCharArray).ifPresent(this::keyPassphrase); -// pemConfig.get("cert-chain.resource").as(Resource::create).ifPresent(this::certChain); -// pemConfig.get("certificates.resource").as(Resource::create).ifPresent(this::certificates); -// return this; -// } -// } -// -// private static final class StreamHolder { -// private final String baseMessage; -// private InputStream inputStream; -// private String message; -// -// private StreamHolder(String message) { -// this.baseMessage = message; -// this.message = message; -// } -// -// private boolean isSet() { -// return inputStream != null; -// } -// -// private void stream(Resource resource) { -// closeStream(); -// Objects.requireNonNull(resource, "Resource for \"" + message + "\" must not be null"); -// -// this.inputStream = resource.stream(); -// this.message = message + ":" + resource.sourceType() + ":" + resource.location(); -// } -// -// private InputStream stream() { -// return inputStream; -// } -// -// private String message() { -// return message; -// } -// -// private void closeStream() { -// if (null != inputStream) { -// try { -// inputStream.close(); -// } catch (IOException e) { -// LOGGER.log(Level.WARNING, "Failed to close input stream: " + message, e); -// } -// } -// message = baseMessage; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeystoreConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeystoreConfig.java deleted file mode 100644 index a9a9122ab52..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeKeystoreConfig.java +++ /dev/null @@ -1,134 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.List; - -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka KeyConfig.Keystore.Builder - * - * This is a ConfigBean since it marries up to the backing config. - */ -@ConfigBean -public interface FakeKeystoreConfig { - - String DEFAULT_KEYSTORE_TYPE = "PKCS12"; - -// private final StreamHolder keystoreStream = new StreamHolder("keystore"); - -// default FakeKeystoreConfigBean trustStore() { -// return trustStore(true); -// } - - @ConfiguredOption(key = "trust-store") - boolean trustStore(); - -// /** -// * Keystore resource definition. -// * -// * @param keystore keystore resource, from file path, classpath, URL etc. -// * @return updated builder instance -// */ -// @ConfiguredOption(key = "resource", required = true) -// public KeystoreBuilder keystore(Resource keystore) { -// this.keystoreStream.stream(keystore); -// return this; -// } -// default DefaultFakeKeystoreConfigBean.Builder keystore(Resource keystore) { -// -// } - - @ConfiguredOption(key = "type", value = DEFAULT_KEYSTORE_TYPE) - String keystoreType(); - - @ConfiguredOption(key = "passphrase") - char[] keystorePassphrase(); - - @ConfiguredOption(key = "key.alias", value = "1") - String keyAlias(); - - @ConfiguredOption(key = "key.passphrase") - char[] keyPassphrase(); - - @ConfiguredOption(key = "cert.alias") - @Singular("certAlias") - List certAliases(); - - @ConfiguredOption(key = "cert-chain.alias") - String certChainAlias(); - - -// /** -// * Update this builder from configuration. -// * The following keys are expected under key {@code keystore}: -// *
          -// *
        • {@code resource}: resource configuration as understood by {@link io.helidon.common.configurable.Resource}
        • -// *
        • {@code type}: type of keystore (defaults to PKCS12)
        • -// *
        • {@code passphrase}: passphrase of keystore, if required
        • -// *
        • {@code key.alias}: alias of private key, if wanted (defaults to "1")
        • -// *
        • {@code key.passphrase}: passphrase of private key if differs from keystore passphrase
        • -// *
        • {@code cert.alias}: alias of public certificate (to obtain public key)
        • -// *
        • {@code cert-chain.alias}: alias of certificate chain
        • -// *
        • {@code trust-store}: true if this is a trust store (and we should load all certificates from it), defaults to false
        • -// *
        -// * -// * @param config configuration instance -// * @return updated builder instance -// */ -// public KeystoreBuilder config(Config config) { -// Config keystoreConfig = config.get("keystore"); -// -// // the actual resource (file, classpath) with the bytes of the keystore -// keystoreConfig.get("resource").as(Resource::create).ifPresent(this::keystore); -// -// // type of keystore -// keystoreConfig.get("type") -// .asString() -// .ifPresent(this::keystoreType); -// // password of the keystore -// keystoreConfig.get("passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keystorePassphrase); -// // private key alias -// keystoreConfig.get("key.alias") -// .asString() -// .ifPresent(this::keyAlias); -// // private key password -// keystoreConfig.get("key.passphrase") -// .asString() -// .map(String::toCharArray) -// .ifPresent(this::keyPassphrase); -// keystoreConfig.get("cert.alias") -// .asString() -// .ifPresent(this::certAlias); -// keystoreConfig.get("cert-chain.alias") -// .asString() -// .ifPresent(this::certChainAlias); -// // whether this is a keystore (with a private key) or a trust store (just trusted public keys/certificates) -// keystoreConfig.get("trust-store") -// .asBoolean() -// .ifPresent(this::trustStore); -// -// return this; -// } - -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakePathTracingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakePathTracingConfig.java deleted file mode 100644 index 5380490a25a..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakePathTracingConfig.java +++ /dev/null @@ -1,179 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.List; - -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka PathTracing. - * - * Traced system configuration for web server for a specific path. - */ -@ConfigBean -public interface FakePathTracingConfig { -// /** -// * Create a new traced path configuration from {@link io.helidon.config.Config}. -// * @param config config of a path -// * @return traced path configuration -// */ -// static FakePathTracingConfigBean create(Config config) { -// return builder().config(config).build(); -// } -// -// /** -// * Create a new builder to configure traced path configuration. -// * -// * @return a new builder instance -// */ -// static Builder builder() { -// return new Builder(); -// } - - /** - * Path this configuration should configure. - * - * @return path on the web server - */ - String path(); - - /** - * Method(s) this configuration should be valid for. This can be used to restrict the configuration - * only to specific HTTP methods (such as {@code GET} or {@code POST}). - * - * @return list of methods, if empty, this configuration is valid for any method - */ - @Singular("method") // Builder::addMethod(String method); - List methods(); - -// /** -// * Associated configuration of tracing valid for the configured path and (possibly) methods. -// * -// * @return traced system configuration -// */ - @ConfiguredOption(required = true) - FakeTracingConfig tracedConfig(); - -// Optional tracedConfig(); - - -// /** -// * Fluent API builder for {@link FakePathTracingConfigBean}. -// */ -// final class Builder implements io.helidon.common.Builder { -// private final List methods = new LinkedList<>(); -// private String path; -// private TracingConfig tracedConfig; -// -// private Builder() { -// } -// -// @Override -// public FakePathTracingConfigBean build() { -// // immutable -// final String finalPath = path; -// final List finalMethods = new LinkedList<>(methods); -// final TracingConfig finalTracingConfig = tracedConfig; -// -// return new FakePathTracingConfigBean() { -// @Override -// public String path() { -// return finalPath; -// } -// -// @Override -// public List methods() { -// return finalMethods; -// } -// -// @Override -// public TracingConfig tracedConfig() { -// return finalTracingConfig; -// } -// -// @Override -// public String toString() { -// return path + "(" + finalMethods + "): " + finalTracingConfig; -// } -// }; -// } -// -// /** -// * Update this builder from provided {@link io.helidon.config.Config}. -// * -// * @param config config to update this builder from -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// path(config.get("path").asString().get()); -// List methods = config.get("methods").asList(String.class).orElse(null); -// if (null != methods) { -// methods(methods); -// } -// tracingConfig(TracingConfig.create(config)); -// -// return this; -// } -// -// /** -// * Path to register the traced configuration on. -// * -// * @param path path as understood by {@link io.helidon.webserver.Routing.Builder} of web server -// * @return updated builder instance -// */ -// public Builder path(String path) { -// this.path = path; -// return this; -// } -// -// /** -// * HTTP methods to restrict registration of this configuration on web server. -// * @param methods list of methods to use, empty means all methods -// * @return updated builder instance -// */ -// public Builder methods(List methods) { -// this.methods.clear(); -// this.methods.addAll(methods); -// return this; -// } -// -// /** -// * Add a new HTTP method to restrict this configuration for. -// * -// * @param method method to add to the list of supported methods -// * @return updated builder instance -// */ -// public Builder addMethod(String method) { -// this.methods.add(method); -// return this; -// } -// -// /** -// * Configuration of a traced system to use on this path and possibly method(s). -// * -// * @param tracedConfig configuration of components, spans and span logs -// * @return updated builder instance -// */ -// public Builder tracingConfig(TracingConfig tracedConfig) { -// this.tracedConfig = tracedConfig; -// return this; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeRoutingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeRoutingConfig.java deleted file mode 100644 index df7086be20e..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeRoutingConfig.java +++ /dev/null @@ -1,694 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -/* - * 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. - */ - -/** - * aka Routing. - */ -public interface FakeRoutingConfig extends FakeServerLifecycle { - -// void route(BareRequest bareRequest, BareResponse bareResponse); -// -// /** -// * Creates new instance of {@link Builder routing builder}. -// * -// * @return a new instance -// */ -// static Builder builder() { -// return new Builder(); -// } -// -// /** -// * An API to define HTTP request routing rules. -// * -// * @see Builder -// */ -// interface Rules { -// /** -// * Configuration of tracing for this routing. -// * The configuration may control whether to log specific components, -// * spans and span logs, either globally, or for a specific path and method combinations. -// * -// * @param webTracingConfig WebServer tracing configuration -// * @return Updated routing configuration -// */ -// Rules register(WebTracingConfig webTracingConfig); -// -// /** -// * Registers builder consumer. It enables to separate complex routing definitions to dedicated classes. -// * -// * @param services services to register -// * @return Updated routing configuration -// */ -// Rules register(Service... services); -// -// /** -// * Registers builder consumer. It enables to separate complex routing definitions to dedicated classes. -// * -// * @param serviceBuilders service builder to register; they will be built as a first step of this -// * method execution -// * @return Updated routing configuration -// */ -// @SuppressWarnings("unchecked") -// Rules register(Supplier... serviceBuilders); -// -// /** -// * Registers builder consumer. It enables to separate complex routing definitions to dedicated classes. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param services services to register -// * @return Updated routing configuration -// */ -// Rules register(String pathPattern, Service... services); -// -// /** -// * Registers builder consumer. It enables to separate complex routing definitions to dedicated classes. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param serviceBuilders service builder to register; they will be built as a first step of this -// * method execution -// * @return an updated routing configuration -// */ -// @SuppressWarnings("unchecked") -// Rules register(String pathPattern, Supplier... serviceBuilders); -// -// /** -// * Routes all GET requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules get(Handler... requestHandlers); -// -// /** -// * Routes GET requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules get(String pathPattern, Handler... requestHandlers); -// -// /** -// * Add a route. This allows also protocol version specific routing. -// * -// * @param route route to add -// * @return updated rules -// */ -// Rules route(HttpRoute route); -// -// /** -// * Routes GET requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules get(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all PUT requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules put(Handler... requestHandlers); -// -// /** -// * Routes PUT requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules put(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes PUT requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for a registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules put(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all POST requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules post(Handler... requestHandlers); -// -// /** -// * Routes POST requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules post(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes POST requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules post(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all RFC 5789 PATCH requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules patch(Handler... requestHandlers); -// -// /** -// * Routes RFC 5789 PATCH requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules patch(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes RFC 5789 PATCH requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules patch(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all DELETE requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules delete(Handler... requestHandlers); -// -// /** -// * Routes DELETE requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules delete(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes DELETE requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules delete(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all OPTIONS requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules options(Handler... requestHandlers); -// -// /** -// * Routes OPTIONS requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules options(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes OPTIONS requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules options(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all HEAD requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules head(Handler... requestHandlers); -// -// /** -// * Routes HEAD requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules head(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes HEAD requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules head(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all TRACE requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules trace(Handler... requestHandlers); -// -// /** -// * Routes TRACE requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules trace(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes TRACE requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules trace(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes all requests to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules any(Handler... requestHandlers); -// -// /** -// * Routes all requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules any(String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes all requests with corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules any(PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Routes requests any specified method to provided handler(s). Request handler can call {@link ServerRequest#next()} -// * to continue processing on the next registered handler. -// * -// * @param methods HTTP methods -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules anyOf(Iterable methods, Handler... requestHandlers); -// -// /** -// * Routes requests with any specified method and corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param methods HTTP methods -// * @param pathPattern a URI path pattern. See {@link PathMatcher} for pattern syntax reference. -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules anyOf(Iterable methods, String pathPattern, Handler... requestHandlers); -// -// /** -// * Routes requests with any specified method and corresponding path to provided handler(s). Request handler can call -// * {@link ServerRequest#next()} to continue processing on the next registered handler. -// * -// * @param methods HTTP methods -// * @param pathMatcher define path for registered router -// * @param requestHandlers handlers to process HTTP request -// * @return an updated routing configuration -// */ -// Rules anyOf(Iterable methods, PathMatcher pathMatcher, Handler... requestHandlers); -// -// /** -// * Registers callback on created new {@link WebServer} instance with this routing. -// * -// * @param webServerConsumer a WebServer creation callback -// * @return updated routing configuration -// */ -// Rules onNewWebServer(Consumer webServerConsumer); -// } -// -// /** -// * A {@link Routing} builder. -// */ -// class Builder implements Rules, io.helidon.common.Builder { -// -// private final RouteListRoutingRules delegate = new RouteListRoutingRules(); -// private final List> errorHandlerRecords = new ArrayList<>(); -// private boolean tracingRegistered; -// -// /** -// * Creates new instance. -// */ -// private Builder() { -// } -// -// // --------------- ROUTING API -// -// @Override -// public Builder register(WebTracingConfig webTracingConfig) { -// this.tracingRegistered = true; -// delegate.register(webTracingConfig); -// return this; -// } -// -// @Override -// @SuppressWarnings("unchecked") -// public Builder register(Supplier... serviceBuilders) { -// delegate.register(serviceBuilders); -// return this; -// } -// -// @Override -// public Builder register(Service... services) { -// delegate.register(services); -// return this; -// } -// -// @Override -// public Builder register(String pathPattern, Service... services) { -// delegate.register(pathPattern, services); -// return this; -// } -// -// @Override -// @SuppressWarnings("unchecked") -// public Builder register(String pathPattern, Supplier... serviceBuilders) { -// delegate.register(pathPattern, serviceBuilders); -// return this; -// } -// -// @Override -// public Builder get(Handler... requestHandlers) { -// delegate.get(requestHandlers); -// return this; -// } -// -// @Override -// public Builder get(String pathPattern, Handler... requestHandlers) { -// delegate.get(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder route(HttpRoute route) { -// delegate.register(route); -// return this; -// } -// -// @Override -// public Builder get(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.get(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder put(Handler... requestHandlers) { -// delegate.put(requestHandlers); -// return this; -// } -// -// @Override -// public Builder put(String pathPattern, Handler... requestHandlers) { -// delegate.put(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder put(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.put(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder post(Handler... requestHandlers) { -// delegate.post(requestHandlers); -// return this; -// } -// -// @Override -// public Builder post(String pathPattern, Handler... requestHandlers) { -// delegate.post(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder post(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.post(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder patch(Handler... requestHandlers) { -// delegate.patch(requestHandlers); -// return this; -// } -// -// @Override -// public Builder patch(String pathPattern, Handler... requestHandlers) { -// delegate.patch(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder patch(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.patch(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder delete(Handler... requestHandlers) { -// delegate.delete(requestHandlers); -// return this; -// } -// -// @Override -// public Builder delete(String pathPattern, Handler... requestHandlers) { -// delegate.delete(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder delete(PathMatcher pathMatcher, -// Handler... requestHandlers) { -// delegate.delete(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder options(Handler... requestHandlers) { -// delegate.options(requestHandlers); -// return this; -// } -// -// @Override -// public Builder options(String pathPattern, Handler... requestHandlers) { -// delegate.options(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder options(PathMatcher pathMatcher, -// Handler... requestHandlers) { -// delegate.options(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder head(Handler... requestHandlers) { -// delegate.head(requestHandlers); -// return this; -// } -// -// @Override -// public Builder head(String pathPattern, Handler... requestHandlers) { -// delegate.head(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder head(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.head(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder trace(Handler... requestHandlers) { -// delegate.trace(requestHandlers); -// return this; -// } -// -// @Override -// public Builder trace(String pathPattern, Handler... requestHandlers) { -// delegate.trace(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder trace(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.trace(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder any(Handler... requestHandlers) { -// delegate.any(requestHandlers); -// return this; -// } -// -// @Override -// public Builder any(String pathPattern, Handler... requestHandlers) { -// delegate.any(pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder any(PathMatcher pathMatcher, Handler... requestHandlers) { -// delegate.any(pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder anyOf(Iterable methods, Handler... requestHandlers) { -// delegate.anyOf(methods, requestHandlers); -// return this; -// } -// -// @Override -// public Builder anyOf(Iterable methods, String pathPattern, Handler... requestHandlers) { -// delegate.anyOf(methods, pathPattern, requestHandlers); -// return this; -// } -// -// @Override -// public Builder anyOf(Iterable methods, -// PathMatcher pathMatcher, -// Handler... requestHandlers) { -// delegate.anyOf(methods, pathMatcher, requestHandlers); -// return this; -// } -// -// @Override -// public Builder onNewWebServer(Consumer webServerConsumer) { -// delegate.onNewWebServer(webServerConsumer); -// return this; -// } -// // --------------- ERROR API -// -// /** -// * Registers an error handler that handles the given type of exceptions. -// * -// * @param exceptionClass the type of exception to handle by this handler -// * @param errorHandler the error handler -// * @param an error handler type -// * @return an updated builder -// */ -// public Builder error(Class exceptionClass, ErrorHandler errorHandler) { -// if (errorHandler == null) { -// return this; -// } -// errorHandlerRecords.add(RequestRouting.ErrorHandlerRecord.of(exceptionClass, errorHandler)); -// -// return this; -// } -// -// // --------------- BUILD API -// -// /** -// * Builds a new routing instance. -// * -// * @return a new instance -// */ -// public Routing build() { -// if (!tracingRegistered) { -// register(WebTracingConfig.create()); -// } -// RouteListRoutingRules.Aggregation aggregate = delegate.aggregate(); -// return new RequestRouting(aggregate.routeList(), errorHandlerRecords, aggregate.newWebServerCallbacks()); -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerConfig.java deleted file mode 100644 index 4fe9a780c2d..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerConfig.java +++ /dev/null @@ -1,763 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.time.Duration; -import java.util.Map; -import java.util.Optional; - -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka ServerConfiguration. - */ -@ConfigBean -public interface FakeServerConfig extends FakeSocketConfig { - - /** - * Returns the count of threads in the pool used to process HTTP requests. - *

        - * Default value is {@link Runtime#availableProcessors()}. - * - * @return a workers count - */ - int workersCount(); - - /** - * Returns a server port to listen on with the default server socket. If port is - * {@code 0} then any available ephemeral port will be used. - *

        - * Additional named server socket configuration is accessible through - * the {@link #socket(String)} and {@link #sockets()} methods. - * - * @return the server port of the default server socket - */ - @Override - int port(); - -// /** -// * Returns local address where the server listens on with the default server socket. -// * If {@code null} then listens an all local addresses. -// *

        -// * Additional named server socket configuration is accessible through -// * the {@link #socket(String)} and {@link #sockets()} methods. -// * -// * @return an address to bind with the default server socket; {@code null} for all local addresses -// */ -// @Override -// InetAddress bindAddress(); - - /** - * Returns a maximum length of the queue of incoming connections on the default server - * socket. - *

        - * Default value is {@link FakeSocketConfig#DEFAULT_BACKLOG_SIZE}. - *

        - * Additional named server socket configuration is accessible through - * the {@link #socket(String)} and {@link #sockets()} methods. - * - * @return a maximum length of the queue of incoming connections - */ - @Override - int backlog(); - - /** - * Returns a default server socket timeout in milliseconds or {@code 0} for an infinite timeout. - *

        - * Additional named server socket configuration is accessible through - * the {@link #socket(String)} and {@link #sockets()} methods. - * - * @return a default server socket timeout in milliseconds or {@code 0} - */ - @Override - int timeoutMillis(); - - /** - * Returns proposed value of the TCP receive window that is advertised to the remote peer on the - * default server socket. - *

        - * If {@code 0} then use implementation default. - *

        - * Additional named server socket configuration is accessible through - * the {@link #socket(String)} and {@link #sockets()} methods. - * - * @return a buffer size in bytes of the default server socket or {@code 0} - */ - @Override - int receiveBufferSize(); - - /** - * A socket configuration of an additional named server socket. - *

        - * An additional named server socket may have a dedicated {@link FakeRoutingConfig} configured - * - * @param name the name of the additional server socket - * @return an additional named server socket configuration or {@code null} if there is no such - * named server socket - * @deprecated since 2.0.0, please use {@link #namedSocket(String)} instead - */ - @Deprecated - default FakeSocketConfig socket(String name) { - return namedSocket(name).orElse(null); - } - - /** - * A socket configuration of an additional named server socket. - *

        - * An additional named server socket may have a dedicated {@link FakeRoutingConfig} configured - * - * @param name the name of the additional server socket - * @return an additional named server socket configuration or {@code empty} if there is no such - * named server socket configured - */ - default Optional namedSocket(String name) { - return Optional.ofNullable(sockets().get(name)); - } - - /** - * A map of all the configured server sockets; that is the default server socket - * which is identified by the key {@link io.helidon.pico.config.fake.helidon.WebServer#DEFAULT_SOCKET_NAME} and also all the additional - * named server socket configurations. - *

        - * An additional named server socket may have a dedicated {@link FakeRoutingConfig} configured - * - * @return a map of all the configured server sockets, never null - */ - @Singular("socket") - Map sockets(); - - /** - * The maximum amount of time that the server will wait to shut - * down regardless of the value of any additionally requested - * quiet period. - * - *

        The default implementation of this method returns {@link - * java.time.Duration#ofSeconds(long) Duration.ofSeconds(10L)}.

        - * - * @return the {@link java.time.Duration} to use - */ -// TODO: @DefaultValue (Duration translation) - @ConfiguredOption(key = "whatever") - default Duration maxShutdownTimeout() { - return Duration.ofSeconds(10L); - } - - /** - * The quiet period during which the webserver will wait for new - * incoming connections after it has been told to shut down. - * - *

        The webserver will wait no longer than the duration returned - * by the {@link #maxShutdownTimeout()} method.

        - * - *

        The default implementation of this method returns {@link - * java.time.Duration#ofSeconds(long) Duration.ofSeconds(0L)}, indicating - * that there will be no quiet period.

        - * - * @return the {@link java.time.Duration} to use - */ - default Duration shutdownQuietPeriod() { - return Duration.ofSeconds(0L); - } - -// /** -// * Returns a Tracer. -// * -// * @return a tracer to use - never {@code null} -// */ -// Tracer tracer(); - -// /** -// * The top level {@link io.helidon.common.context.Context} to be used by this webserver. -// * @return a context instance with registered application scoped instances -// */ -// Context context(); -// -// /** -// * Returns an optional {@link Transport}. -// * -// * @return an optional {@link Transport} -// */ -// default Optional transport() { -// return Optional.ofNullable(null); -// } - - /** - * Whether to print details of HelidonFeatures. - * - * @return whether to print details - */ - boolean printFeatureDetails(); - -// /** -// * Creates new instance with defaults from external configuration source. -// * -// * @param config the externalized configuration -// * @return a new instance -// */ -// static FakeServerConfigBean create(Config config) { -// return builder(config).build(); -// } -// -// /** -// * Creates new instance of a {@link Builder server configuration builder}. -// * -// * @return a new builder instance -// * -// * @deprecated since 2.0.0 - please use {@link io.helidon.webserver.WebServer#builder()} instead -// */ -// @Deprecated -// static Builder builder() { -// return new Builder(); -// } -// -// /** -// * Creates new instance of a {@link Builder server configuration builder} with defaults from external configuration source. -// * -// * @param config the externalized configuration -// * @return a new builder instance -// * @deprecated since 2.0.0 - please use {@link io.helidon.webserver.WebServer#builder()}, then -// * {@link WebServer.Builder#config(io.helidon.config.Config)}, or -// * {@link io.helidon.webserver.WebServer#create(Routing, io.helidon.config.Config)} -// */ -// @Deprecated -// static Builder builder(Config config) { -// return new Builder().config(config); -// } -// -// /** -// * A {@link FakeServerConfigBean} builder. -// * -// * @deprecated since 2.0.0 - use {@link io.helidon.webserver.WebServer.Builder} instead -// */ -// @Deprecated -// final class Builder implements FakeSocketConfigBean.SocketConfigurationBuilder, -// io.helidon.common.Builder { -// -// private static final AtomicInteger WEBSERVER_COUNTER = new AtomicInteger(1); -// private final Map socketBuilders = new HashMap<>(); -// private final Map socketsConfigs = new HashMap<>(); -// private int workers; -// private Tracer tracer; -// private Duration maxShutdownTimeout; -// private Duration shutdownQuietPeriod; -// private Optional transport; -// private Context context; -// private boolean printFeatureDetails; -// -// private Builder() { -// transport = Optional.ofNullable(null); -// maxShutdownTimeout = Duration.ofSeconds(10L); -// shutdownQuietPeriod = Duration.ofSeconds(0L); -// } -// -// /** -// * Sets {@link SSLContext} to to use with the server. If not {@code null} then server enforce SSL communication. -// * -// * @param sslContext ssl context -// * @return an updated builder -// */ -// public Builder ssl(SSLContext sslContext) { -// defaultSocketBuilder().ssl(sslContext); -// return this; -// } -// -// /** -// * Sets {@link SSLContext} to to use with the server. If not {@code null} then server enforce SSL communication. -// * -// * @param sslContextBuilder ssl context builder; will be built as a first step of this method execution -// * @return an updated builder -// */ -// public Builder ssl(Supplier sslContextBuilder) { -// defaultSocketBuilder().ssl(sslContextBuilder); -// return this; -// } -// -// /** -// * Sets server port. If port is {@code 0} or less then any available ephemeral port will be used. -// *

        -// * Configuration key: {@code port} -// * -// * @param port the server port -// * @return an updated builder -// */ -// public Builder port(int port) { -// defaultSocketBuilder().port(port); -// return this; -// } -// -// /** -// * Sets a local address for server to bind. If {@code null} then listens an all local addresses. -// *

        -// * Configuration key: {@code bind-address} -// * -// * @param bindAddress the address to bind the server or {@code null} for all local addresses -// * @return an updated builder -// */ -// public Builder bindAddress(InetAddress bindAddress) { -// defaultSocketBuilder().bindAddress(bindAddress); -// return this; -// } -// -// /** -// * Sets a maximum length of the queue of incoming connections. Default value is {@code 1024}. -// *

        -// * Configuration key: {@code backlog} -// * -// * @param size the maximum length of the queue of incoming connections -// * @return an updated builder -// */ -// public Builder backlog(int size) { -// defaultSocketBuilder().backlog(size); -// return this; -// } -// -// /** -// * Sets a socket timeout in milliseconds or {@code 0} for infinite timeout. -// *

        -// * Configuration key: {@code timeout} -// * -// * @param milliseconds a socket timeout in milliseconds or {@code 0} -// * @return an updated builder -// */ -// public Builder timeout(int milliseconds) { -// defaultSocketBuilder().timeoutMillis(milliseconds); -// return this; -// } -// -// /** -// * Propose value of the TCP receive window that is advertised to the remote peer. -// * If {@code 0} then implementation default is used. -// *

        -// * Configuration key: {@code receive-buffer} -// * -// * @param bytes a buffer size in bytes or {@code 0} -// * @return an updated builder -// */ -// public Builder receiveBufferSize(int bytes) { -// defaultSocketBuilder().receiveBufferSize(bytes); -// return this; -// } -// -// @Override -// public Builder maxHeaderSize(int size) { -// defaultSocketBuilder().maxHeaderSize(size); -// return this; -// } -// -// @Override -// public Builder maxInitialLineLength(int length) { -// defaultSocketBuilder().maxInitialLineLength(length); -// return this; -// } -// -// /** -// * Adds an additional named server socket configuration. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param name the name of the additional server socket configuration -// * @param port the port to bind; if {@code 0} or less, any available ephemeral port will be used -// * @param bindAddress the address to bind; if {@code null}, all local addresses will be bound -// * @return an updated builder -// * -// * @deprecated since 2.0.0, please use {@link #addSocket(String, FakeSocketConfigBean)} instead -// */ -// @Deprecated -// public Builder addSocket(String name, int port, InetAddress bindAddress) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// return addSocket(name, FakeSocketConfigBean.builder() -// .port(port) -// .bindAddress(bindAddress)); -// } -// -// /** -// * Adds an additional named server socket configuration. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param name the name of the additional server socket configuration -// * @param socketConfiguration the additional named server socket configuration -// * @return an updated builder -// */ -// public Builder addSocket(String name, FakeSocketConfigBean socketConfiguration) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// this.socketsConfigs.put(name, socketConfiguration); -// return this; -// } -// -// /** -// * Adds an additional named server socket configuration. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param name the name of the additional server socket configuration -// * @param socketConfiguration the additional named server socket configuration builder -// * @return an updated builder -// */ -// public Builder addSocket(String name, FakeSocketConfigBean.Builder socketConfiguration) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// this.socketBuilders.put(name, socketConfiguration); -// return this; -// } -// -// /** -// * Adds an additional named server socket configuration builder. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param name the name of the additional server socket configuration -// * @param socketConfigurationBuilder the additional named server socket configuration builder; will be built as -// * a first step of this method execution -// * @return an updated builder -// */ -// public Builder addSocket(String name, Supplier socketConfigurationBuilder) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// -// return addSocket(name, socketConfigurationBuilder != null ? socketConfigurationBuilder.get() : null); -// } -// -// /** -// * Sets a count of threads in pool used to process HTTP requests. -// * Default value is {@code CPU_COUNT * 2}. -// *

        -// * Configuration key: {@code workers} -// * -// * @param workers a workers count -// * @return an updated builder -// */ -// public Builder workersCount(int workers) { -// this.workers = workers; -// return this; -// } -// -// /** -// * Sets a tracer. -// * -// * @param tracer a tracer to set -// * @return an updated builder -// */ -// public Builder tracer(Tracer tracer) { -// this.tracer = tracer; -// return this; -// } -// -// /** -// * Sets a tracer. -// * -// * @param tracerBuilder a tracer builder to set; will be built as a first step of this method execution -// * @return updated builder -// */ -// public Builder tracer(Supplier tracerBuilder) { -// return tracer(tracerBuilder.get()); -// } -// -// /** -// * Configures the SSL protocols to enable with the default server socket. -// * @param protocols protocols to enable, if {@code null} enables the -// * default protocols -// * @return an updated builder -// */ -// public Builder enabledSSlProtocols(String... protocols) { -// defaultSocketBuilder().enabledSSlProtocols(protocols); -// return this; -// } -// -// /** -// * Configures the SSL protocols to enable with the default server socket. -// * @param protocols protocols to enable, if {@code null} or empty enables -// * the default protocols -// * @return an updated builder -// */ -// public Builder enabledSSlProtocols(List protocols) { -// defaultSocketBuilder().enabledSSlProtocols(protocols); -// return this; -// } -// -// /** -// * Configure maximum client payload size. -// * @param size maximum payload size -// * @return an updated builder -// */ -// @Override -// public Builder maxPayloadSize(long size) { -// defaultSocketBuilder().maxPayloadSize(size); -// return this; -// } -// -// /** -// * Set a maximum length of the content of an upgrade request. -// *

        -// * Default is {@code 64*1024} -// * -// * @param size Maximum length of the content of an upgrade request -// * @return this builder -// */ -// @Override -// public Builder maxUpgradeContentLength(int size) { -// defaultSocketBuilder().maxUpgradeContentLength(size); -// return this; -// } -// -// /** -// * Configure the maximum amount of time that the server will wait to shut -// * down regardless of the value of any additionally requested -// * quiet period. -// * @param maxShutdownTimeout the {@link Duration} to use -// * @return an updated builder -// */ -// public Builder maxShutdownTimeout(Duration maxShutdownTimeout) { -// this.maxShutdownTimeout = -// Objects.requireNonNull(maxShutdownTimeout, "Parameter 'maxShutdownTimeout' must not be null!"); -// return this; -// } -// -// /** -// * Configure the quiet period during which the webserver will wait for new -// * incoming connections after it has been told to shut down. -// * @param shutdownQuietPeriod the {@link Duration} to use -// * @return an updated builder -// */ -// public Builder shutdownQuietPeriod(Duration shutdownQuietPeriod) { -// this.shutdownQuietPeriod = -// Objects.requireNonNull(shutdownQuietPeriod, "Parameter 'shutdownQuietPeriod' must not be null!"); -// return this; -// } -// -// /** -// * Configure transport. -// * @param transport a {@link Transport} -// * @return an updated builder -// */ -// public Builder transport(Transport transport) { -// this.transport = Optional.of(transport); -// return this; -// } -// -// /** -// * Set to {@code true} to print detailed feature information on startup. -// * -// * @param print whether to print details or not -// * @return updated builder instance -// * @see io.helidon.common.HelidonFeatures -// */ -// public Builder printFeatureDetails(boolean print) { -// this.printFeatureDetails = print; -// return this; -// } -// -// /** -// * Configure the application scoped context to be used as a parent for webserver request contexts. -// * @param context top level context -// * @return an updated builder -// */ -// public Builder context(Context context) { -// this.context = context; -// -// return this; -// } -// -// private InetAddress string2InetAddress(String address) { -// try { -// return InetAddress.getByName(address); -// } catch (UnknownHostException e) { -// throw new ConfigException("Illegal value of 'bind-address' configuration key. Expecting host or ip address!", e); -// } -// } -// -// /** -// * Sets configuration values included in provided {@link Config} parameter. -// *

        -// * It can be used for configuration externalisation. -// *

        -// * All parameters sets before this method call can be seen as defaults and all parameters sets after can be seen -// * as forced. -// * -// * @param config the configuration to use -// * @return an updated builder -// */ -// public Builder config(Config config) { -// if (config == null) { -// return this; -// } -// -// defaultSocketBuilder().config(config); -// -// config.get("host").asString().ifPresent(defaultSocketBuilder()::host); -// -// DeprecatedConfig.get(config, "worker-count", "workers") -// .asInt() -// .ifPresent(this::workersCount); -// -// config.get("features.print-details").asBoolean().ifPresent(this::printFeatureDetails); -// -// // shutdown timeouts -// config.get("max-shutdown-timeout-seconds").asLong().ifPresent(it -> maxShutdownTimeout(Duration.ofSeconds(it))); -// config.get("shutdown-quiet-period-seconds").asLong().ifPresent(it -> shutdownQuietPeriod(Duration.ofSeconds(it))); -// -// // sockets -// Config socketsConfig = config.get("sockets"); -// if (socketsConfig.exists()) { -// List socketConfigs = socketsConfig.asNodeList().orElse(List.of()); -// for (Config socketConfig : socketConfigs) { -// // the whole section checking the socket name can be removed -// // when we remove deprecated methods with socket name on server builder -// String socketName; -// -// String nodeName = socketConfig.name(); -// Optional maybeSocketName = socketConfig.get("name").asString().asOptional(); -// -// socketName = maybeSocketName.orElse(nodeName); -// -// // log warning for deprecated config -// try { -// Integer.parseInt(nodeName); -// if (socketName.equals(nodeName) && maybeSocketName.isEmpty()) { -// throw new ConfigException("Cannot find \"name\" key for socket configuration " + socketConfig.key()); -// } -// } catch (NumberFormatException e) { -// // this is old approach -// Logger.getLogger(SocketConfigurationBuilder.class.getName()) -// .warning("Socket configuration at " + socketConfig.key() + " is deprecated. Please use an array " -// + "with \"name\" key to define the socket name."); -// } -// -// FakeSocketConfigBean.Builder socket = FakeSocketConfigBean.builder() -// .name(socketName) -// .config(socketConfig); -// -// socketBuilders.put(socket.name(), socket); -// } -// } -// -// return this; -// } -// -// /** -// * Builds a new configuration instance. -// * -// * @return a new instance -// */ -// @Override -// public FakeServerConfigBean build() { -// if (null == context) { -// // I do not expect "unlimited" number of webservers -// // in case somebody spins a huge number up, the counter will cycle to negative numbers once -// // Integer.MAX_VALUE is reached. -// context = Context.builder() -// .id("web-" + WEBSERVER_COUNTER.getAndIncrement()) -// .build(); -// } -// -// Optional maybeTracer = context.get(Tracer.class); -// -// if (null == this.tracer) { -// this.tracer = maybeTracer.orElseGet(Tracer::global); -// } -// -// if (maybeTracer.isEmpty()) { -// context.register(this.tracer); -// } -// -// if (workers <= 0) { -// workers = Runtime.getRuntime().availableProcessors(); -// } -// -// return new ServerBasicConfig(this); -// } -// -// FakeSocketConfigBean.Builder defaultSocketBuilder() { -// return socketBuilder(WebServer.DEFAULT_SOCKET_NAME); -// } -// -// FakeSocketConfigBean.Builder socketBuilder(String socketName) { -// return socketBuilders.computeIfAbsent(socketName, k -> FakeSocketConfigBean.builder().name(socketName)); -// } -// -// Map sockets() { -// Set builtSocketConfigsKeys = socketsConfigs.keySet(); -// Map result = -// new HashMap<>(this.socketBuilders.size() + this.socketsConfigs.size()); -// for (Map.Entry e : this.socketBuilders.entrySet()) { -// String key = e.getKey(); -// if (builtSocketConfigsKeys.contains(key)) { -// throw new IllegalStateException("Both mutable and immutable socket configuration provided for named socket " -// + key); -// } -// result.put(key, e.getValue().build()); -// } -// -// result.putAll(this.socketsConfigs); -// return result; -// } -// -// int workers() { -// return workers; -// } -// -// Tracer tracer() { -// return tracer; -// } -// -// Duration maxShutdownTimeout() { -// return maxShutdownTimeout; -// } -// -// Duration shutdownQuietPeriod() { -// return shutdownQuietPeriod; -// } -// -// Optional transport() { -// return transport; -// } -// -// Context context() { -// return context; -// } -// -// boolean printFeatureDetails() { -// return printFeatureDetails; -// } -// -// @Override -// public Builder timeout(long amount, TimeUnit unit) { -// defaultSocketBuilder().timeout(amount, unit); -// return this; -// } -// -// @Override -// public Builder tls(FakeWebServerTlsConfigBean webServerTls) { -// defaultSocketBuilder().tls(webServerTls); -// return this; -// } -// -// @Override -// public Builder enableCompression(boolean value) { -// defaultSocketBuilder().enableCompression(value); -// return this; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSocketConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSocketConfig.java deleted file mode 100644 index 9b7dcb3ba93..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSocketConfig.java +++ /dev/null @@ -1,847 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -/* - * 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. - */ - -import java.util.Optional; -import java.util.Set; - -import javax.net.ssl.SSLContext; - -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka ServerConfiguration. - * - * The SocketConfiguration configures a port to listen on and its associated server socket parameters. - */ -@ConfigBean -public interface FakeSocketConfig { - - /** - * The default backlog size to configure the server sockets with if no other value - * is provided. - */ - int DEFAULT_BACKLOG_SIZE = 1024; - - /** - * Name of this socket. - * Default to WebServer#DEFAULT_SOCKET_NAME for the main and - * default server socket. All other sockets must be named. - * - * @return name of this socket - */ -// default String name() { -// return WebServer.DEFAULT_SOCKET_NAME; -// } - @ConfiguredOption(WebServer.DEFAULT_SOCKET_NAME) - String name(); - - /** - * Returns a server port to listen on with the server socket. If port is - * {@code 0} then any available ephemeral port will be used. - * - * @return the server port of the server socket - */ - int port(); - -// /** -// * Returns local address where the server listens on with the server socket. -// * If {@code null} then listens an all local addresses. -// * -// * @return an address to bind with the server socket; {@code null} for all local addresses -// */ -// InetAddress bindAddress(); - @ConfiguredOption(key = "bind-address") - String bindAddress(); - - /** - * Returns a maximum length of the queue of incoming connections on the server - * socket. - *

        - * Default value is {@link #DEFAULT_BACKLOG_SIZE}. - * - * @return a maximum length of the queue of incoming connections - */ - @ConfiguredOption("1024") - int backlog(); - - /** - * Returns a server socket timeout in milliseconds or {@code 0} for an infinite timeout. - * - * @return a server socket timeout in milliseconds or {@code 0} - */ - @ConfiguredOption(key = "timeout-millis") - int timeoutMillis(); - - /** - * Returns proposed value of the TCP receive window that is advertised to the remote peer on the - * server socket. - *

        - * If {@code 0} then use implementation default. - * - * @return a buffer size in bytes of the server socket or {@code 0} - */ - int receiveBufferSize(); - - /** - * Return a {@link FakeWebServerTlsConfig} containing server TLS configuration. When empty {@link java.util.Optional} is returned - * no TLS should be configured. - * - * @return web server tls configuration - */ - Optional tls(); - - /** - * Returns a {@link javax.net.ssl.SSLContext} to use with the server socket. If not {@code null} then - * the server enforces an SSL communication. - * - * @deprecated use {@code tls().sslContext()} instead. This method will be removed at 3.0.0 version. - * @return a SSL context to use - */ - @Deprecated(since = "2.3.1", forRemoval = true) - SSLContext ssl(); - - /** - * Returns the SSL protocols to enable, or {@code null} to enable the default - * protocols. - * @deprecated use {@code tls().enabledTlsProtocols()} instead. This method will be removed at 3.0.0 version. - * @return the SSL protocols to enable - */ - @Deprecated(since = "2.3.1", forRemoval = true) - Set enabledSslProtocols(); - - /** - * Return the allowed cipher suite of the TLS. If empty set is returned, the default cipher suite is used. - * - * @deprecated use {@code tls().cipherSuite()} instead. This method will be removed at 3.0.0 version. - * @return the allowed cipher suite - */ - @Deprecated(since = "2.3.1", forRemoval = true) - Set allowedCipherSuite(); - - /** - * Whether to require client authentication or not. - * - * @deprecated use {@code tls().clientAuth()} instead. This method will be removed at 3.0.0 version. - * @return client authentication - */ - @Deprecated(since = "2.3.1", forRemoval = true) - FakeNettyClientAuth clientAuth(); - - /** - * Whether this socket is enabled (and will be opened on server startup), or disabled - * (and ignored on server startup). - * - * @return {@code true} for enabled socket, {@code false} for socket that should not be opened - */ -// default boolean enabled() { -// return true; -// } - @ConfiguredOption("true") - boolean enabled(); - - /** - * Maximal size of all headers combined. - * - * @return size in bytes - */ - @ConfiguredOption(key = "max-header-size", value = "8192") - int maxHeaderSize(); - - /** - * Maximal length of the initial HTTP line. - * - * @return length - */ - @ConfiguredOption("4096") - int maxInitialLineLength(); - - /** - * Maximal size of a single chunk of received data. - * - * @return chunk size - */ - int maxChunkSize(); - - /** - * Whether to validate HTTP header names. - * When set to {@code true}, we make sure the header name is a valid string - * - * @return {@code true} if headers should be validated - */ - boolean validateHeaders(); - - /** - * Whether to allow negotiation for a gzip/deflate content encoding. Supporting - * HTTP compression may interfere with application that use streaming and other - * similar features. Thus, it defaults to {@code false}. - * - * @return compression flag - */ -// default boolean enableCompression() { -// return false; -// } - boolean enableCompression(); - - /** - * Maximum size allowed for an HTTP payload in a client request. A negative - * value indicates that there is no maximum set. - * - * @return maximum payload size - */ -// default long maxPayloadSize() { -// return -1L; -// } - @ConfiguredOption("-1") - long maxPayloadSize(); - - /** - * Initial size of the buffer used to parse HTTP line and headers. - * - * @return initial size of the buffer - */ - int initialBufferSize(); - - /** - * Maximum length of the content of an upgrade request. - * - * @return maximum length of the content of an upgrade request - */ -// default int maxUpgradeContentLength() { -// return 64 * 1024; -// } - @ConfiguredOption("65536") - int maxUpgradeContentLength(); - -// /** -// * Creates a builder of {@link FakeSocketConfigBean} class. -// * -// * @return a builder -// */ -// static Builder builder() { -// return new Builder(); -// } -// -// /** -// * Create a default named configuration. -// * -// * @param name name of the socket -// * @return a new socket configuration with defaults -// */ -// static FakeSocketConfigBean create(String name) { -// return builder() -// .name(name) -// .build(); -// } -// -// /** -// * Socket configuration builder API, used by {@link io.helidon.webserver.SocketConfiguration.Builder} -// * to configure additional sockets, and by {@link io.helidon.webserver.WebServer.Builder} to -// * configure the default socket. -// * -// * @param type of the subclass of this class to provide correct fluent API -// */ -// @Configured -// interface SocketConfigurationBuilder> { -// /** -// * Configures a server port to listen on with the server socket. If port is -// * {@code 0} then any available ephemeral port will be used. -// * -// * @param port the server port of the server socket -// * @return this builder -// */ -// @ConfiguredOption("0") -// B port(int port); -// -// /** -// * Configures local address where the server listens on with the server socket. -// * If not configured, then listens an all local addresses. -// * -// * @param address an address to bind with the server socket -// * @return this builder -// * @throws java.lang.NullPointerException in case the bind address is null -// * @throws io.helidon.config.ConfigException in case the address provided is not a valid host address -// */ -// @ConfiguredOption(deprecated = true) -// default B bindAddress(String address) { -// try { -// return bindAddress(InetAddress.getByName(address)); -// } catch (UnknownHostException e) { -// throw new ConfigException("Illegal value of 'bind-address' configuration key. Expecting host or ip address!", e); -// } -// } -// -// /** -// * A helper method that just calls {@link #bindAddress(String)}. -// * -// * @param address host to listen on -// * @return this builder -// */ -// @ConfiguredOption -// default B host(String address) { -// return bindAddress(address); -// } -// -// /** -// * Configures local address where the server listens on with the server socket. -// * If not configured, then listens an all local addresses. -// * -// * @param bindAddress an address to bind with the server socket -// * @return this builder -// * @throws java.lang.NullPointerException in case the bind address is null -// */ -// B bindAddress(InetAddress bindAddress); -// -// /** -// * Configures a maximum length of the queue of incoming connections on the server -// * socket. -// *

        -// * Default value is {@link #DEFAULT_BACKLOG_SIZE}. -// * -// * @param backlog a maximum length of the queue of incoming connections -// * @return this builder -// */ -// @ConfiguredOption("1024") -// B backlog(int backlog); -// -// /** -// * Configures a server socket timeout. -// * -// * @param amount an amount of time to configure the timeout, use {@code 0} for infinite timeout -// * @param unit time unit to use with the configured amount -// * @return this builder -// */ -// @ConfiguredOption(key = "timeout-millis", type = Long.class, value = "0", -// description = "Socket timeout in milliseconds") -// B timeout(long amount, TimeUnit unit); -// -// /** -// * Configures proposed value of the TCP receive window that is advertised to the remote peer on the -// * server socket. -// *

        -// * If {@code 0} then use implementation default. -// * -// * @param receiveBufferSize a buffer size in bytes of the server socket or {@code 0} -// * @return this builder -// */ -// @ConfiguredOption -// B receiveBufferSize(int receiveBufferSize); -// -// /** -// * Configures SSL for this socket. When configured, the server enforces SSL -// * configuration. -// * If this method is called, any other method except for {@link #tls(java.util.function.Supplier)}¨ -// * and repeated invocation of this method would be ignored. -// *

        -// * If this method is called again, the previous configuration would be ignored. -// * -// * @param webServerTls ssl configuration to use with this socket -// * @return this builder -// */ -// @ConfiguredOption -// B tls(FakeWebServerTlsConfigBean webServerTls); -// -// /** -// * Configures SSL for this socket. When configured, the server enforces SSL -// * configuration. -// * -// * @param tlsConfig supplier ssl configuration to use with this socket -// * @return this builder -// */ -// default B tls(Supplier tlsConfig) { -// return tls(tlsConfig.get()); -// } -// -// /** -// * Maximal number of bytes of all header values combined. When a bigger value is received, a -// * {@link io.helidon.common.http.Http.Status#BAD_REQUEST_400} -// * is returned. -// *

        -// * Default is {@code 8192} -// * -// * @param size maximal number of bytes of combined header values -// * @return this builder -// */ -// @ConfiguredOption("8192") -// B maxHeaderSize(int size); -// -// /** -// * Maximal number of characters in the initial HTTP line. -// *

        -// * Default is {@code 4096} -// * -// * @param length maximal number of characters -// * @return this builder -// */ -// @ConfiguredOption("4096") -// B maxInitialLineLength(int length); -// -// /** -// * Enable negotiation for gzip/deflate content encodings. Clients can -// * request compression using the "Accept-Encoding" header. -// *

        -// * Default is {@code false} -// * -// * @param value compression flag -// * @return this builder -// */ -// @ConfiguredOption("false") -// B enableCompression(boolean value); -// -// /** -// * Set a maximum payload size for a client request. Can prevent DoS -// * attacks. -// * -// * @param size maximum payload size -// * @return this builder -// */ -// @ConfiguredOption -// B maxPayloadSize(long size); -// -// /** -// * Set a maximum length of the content of an upgrade request. -// *

        -// * Default is {@code 64*1024} -// * -// * @param size Maximum length of the content of an upgrade request -// * @return this builder -// */ -// @ConfiguredOption("65536") -// B maxUpgradeContentLength(int size); -// -// /** -// * Update this socket configuration from a {@link io.helidon.config.Config}. -// * -// * @param config configuration on the node of a socket -// * @return updated builder instance -// */ -// @SuppressWarnings("unchecked") -// default B config(Config config) { -// config.get("port").asInt().ifPresent(this::port); -// config.get("bind-address").asString().ifPresent(this::host); -// config.get("backlog").asInt().ifPresent(this::backlog); -// config.get("max-header-size").asInt().ifPresent(this::maxHeaderSize); -// config.get("max-initial-line-length").asInt().ifPresent(this::maxInitialLineLength); -// config.get("max-payload-size").asInt().ifPresent(this::maxPayloadSize); -// -// DeprecatedConfig.get(config, "timeout-millis", "timeout") -// .asInt() -// .ifPresent(it -> this.timeout(it, TimeUnit.MILLISECONDS)); -// DeprecatedConfig.get(config, "receive-buffer-size", "receive-buffer") -// .asInt() -// .ifPresent(this::receiveBufferSize); -// -// Optional> enabledProtocols = DeprecatedConfig.get(config, "ssl.protocols", "ssl-protocols") -// .asList(String.class) -// .asOptional(); -// -// // tls -// Config sslConfig = DeprecatedConfig.get(config, "tls", "ssl"); -// if (sslConfig.exists()) { -// try { -// FakeWebServerTlsConfigBean.Builder builder = FakeWebServerTlsConfigBean.builder(); -// enabledProtocols.ifPresent(builder::enabledProtocols); -// builder.config(sslConfig); -// -// this.tls(builder.build()); -// } catch (IllegalStateException e) { -// throw new ConfigException("Cannot load SSL configuration.", e); -// } -// } -// -// // compression -// config.get("enable-compression").asBoolean().ifPresent(this::enableCompression); -// return (B) this; -// } -// } -// -// /** -// * The {@link io.helidon.webserver.SocketConfiguration} builder class. -// */ -// @Configured -// final class Builder implements SocketConfigurationBuilder, io.helidon.common.Builder { -// /** -// * @deprecated remove once WebServer.Builder.addSocket(name, socket) methods are removed -// */ -// @Deprecated -// static final String UNCONFIGURED_NAME = "io.helidon.webserver.SocketConfiguration.UNCONFIGURED"; -// private final FakeWebServerTlsConfigBean.Builder tlsConfigBuilder = FakeWebServerTlsConfigBean.builder(); -// -// private int port = 0; -// private InetAddress bindAddress = null; -// private int backlog = DEFAULT_BACKLOG_SIZE; -// private int timeoutMillis = 0; -// private int receiveBufferSize = 0; -// private FakeWebServerTlsConfigBean webServerTls; -// // this is for backward compatibility, should be initialized to null once the -// // methods with `name` are removed from server builder (for adding sockets) -// private String name = UNCONFIGURED_NAME; -// private boolean enabled = true; -// // these values are as defined in Netty implementation -// private int maxHeaderSize = 8192; -// private int maxInitialLineLength = 4096; -// private int maxChunkSize = 8192; -// private boolean validateHeaders = true; -// private int initialBufferSize = 128; -// private boolean enableCompression = false; -// private long maxPayloadSize = -1; -// private int maxUpgradeContentLength = 64 * 1024; -// -// private Builder() { -// } -// -// @Override -// public FakeSocketConfigBean build() { -// if (null == webServerTls) { -// webServerTls = tlsConfigBuilder.build(); -// } -// -// if (null == name) { -// throw new ConfigException("Socket name must be configured for each socket"); -// } -// -// return new ServerBasicConfig.SocketConfig(this); -// } -// -// @Override -// public Builder port(int port) { -// this.port = port; -// return this; -// } -// -// @Override -// public Builder bindAddress(InetAddress bindAddress) { -// this.bindAddress = bindAddress; -// return this; -// } -// -// /** -// * Configures a maximum length of the queue of incoming connections on the server -// * socket. -// *

        -// * Default value is {@link #DEFAULT_BACKLOG_SIZE}. -// * -// * @param backlog a maximum length of the queue of incoming connections -// * @return this builder -// */ -// public Builder backlog(int backlog) { -// this.backlog = backlog; -// return this; -// } -// -// /** -// * Configures a server socket timeout in milliseconds or {@code 0} for an infinite timeout. -// * -// * @param timeoutMillis a server socket timeout in milliseconds or {@code 0} -// * @return this builder -// * -// * @deprecated since 2.0.0 please use {@link #timeout(long, java.util.concurrent.TimeUnit)} instead -// */ -// @Deprecated -// public Builder timeoutMillis(int timeoutMillis) { -// this.timeoutMillis = timeoutMillis; -// return this; -// } -// -// /** -// * Configures proposed value of the TCP receive window that is advertised to the remote peer on the -// * server socket. -// *

        -// * If {@code 0} then use implementation default. -// * -// * @param receiveBufferSize a buffer size in bytes of the server socket or {@code 0} -// * @return this builder -// */ -// @Override -// public Builder receiveBufferSize(int receiveBufferSize) { -// this.receiveBufferSize = receiveBufferSize; -// return this; -// } -// -// /** -// * Configures a {@link SSLContext} to use with the server socket. If not {@code null} then -// * the server enforces an SSL communication. -// * -// * @param sslContext a SSL context to use -// * @return this builder -// * -// * @deprecated since 2.0.0, please use {@link #tls(FakeWebServerTlsConfigBean)} instead -// */ -// @Deprecated -// public Builder ssl(SSLContext sslContext) { -// if (null != sslContext) { -// this.tlsConfigBuilder.sslContext(sslContext); -// } -// return this; -// } -// -// /** -// * Configures a {@link SSLContext} to use with the server socket. If not {@code null} then -// * the server enforces an SSL communication. -// * -// * @param sslContextBuilder a SSL context builder to use; will be built as a first step of this -// * method execution -// * @return this builder -// * @deprecated since 2.0.0, please use {@link #tls(Supplier)} instead -// */ -// @Deprecated -// public Builder ssl(Supplier sslContextBuilder) { -// return ssl(sslContextBuilder != null ? sslContextBuilder.get() : null); -// } -// -// /** -// * Configures the SSL protocols to enable with the server socket. -// * @param protocols protocols to enable, if {@code null} enables the -// * default protocols -// * @return this builder -// * -// * @deprecated since 2.0.0, please use {@link FakeWebServerTlsConfigBean.Builder#enabledProtocols(String...)} -// * instead -// */ -// @Deprecated -// public Builder enabledSSlProtocols(String... protocols) { -// if (null == protocols) { -// enabledSSlProtocols(List.of()); -// } else { -// enabledSSlProtocols(Arrays.asList(protocols)); -// } -// return this; -// } -// -// /** -// * Configures the SSL protocols to enable with the server socket. -// * @param protocols protocols to enable, if {@code null} or empty enables -// * the default protocols -// * @return this builder -// */ -// @Deprecated -// public Builder enabledSSlProtocols(List protocols) { -// if (null == protocols) { -// this.tlsConfigBuilder.enabledProtocols(List.of()); -// } else { -// this.tlsConfigBuilder.enabledProtocols(protocols); -// } -// return this; -// } -// -// @Override -// public Builder timeout(long amount, TimeUnit unit) { -// long timeout = unit.toMillis(amount); -// if (timeout > Integer.MAX_VALUE) { -// this.timeoutMillis = 0; -// } else { -// this.timeoutMillis = (int) timeout; -// } -// return this; -// } -// -// @Override -// public Builder tls(FakeWebServerTlsConfigBean webServerTls) { -// this.webServerTls = webServerTls; -// return this; -// } -// -// @Override -// public Builder maxHeaderSize(int size) { -// this.maxHeaderSize = size; -// return this; -// } -// -// @Override -// public Builder maxInitialLineLength(int length) { -// this.maxInitialLineLength = length; -// return this; -// } -// -// @Override -// public Builder maxPayloadSize(long size) { -// this.maxPayloadSize = size; -// return this; -// } -// -// @Override -// public Builder maxUpgradeContentLength(int size) { -// this.maxUpgradeContentLength = size; -// return this; -// } -// -// /** -// * Configure a socket name, to bind named routings to. -// * -// * @param name name of the socket -// * @return updated builder instance -// */ -// @ConfiguredOption(required = true) -// public Builder name(String name) { -// this.name = name; -// return this; -// } -// -// /** -// * Set this socket builder to enabled or disabled. -// * -// * @param enabled when set to {@code false}, the socket is not going to be opened by the server -// * @return updated builder instance -// */ -// public Builder enabled(boolean enabled) { -// this.enabled = enabled; -// return this; -// } -// -// /** -// * Configure maximal size of a chunk to be read from incoming requests. -// * Defaults to {@code 8192}. -// * -// * @param size maximal chunk size -// * @return updated builder instance -// */ -// public Builder maxChunkSize(int size) { -// this.maxChunkSize = size; -// return this; -// } -// -// /** -// * Configure whether to validate header names. -// * Defaults to {@code true} to make sure header names are valid strings. -// * -// * @param validate set to {@code false} to ignore header validation -// * @return updated builder instance -// */ -// public Builder validateHeaders(boolean validate) { -// this.validateHeaders = validate; -// return this; -// } -// -// /** -// * Configure initial size of the buffer used to parse HTTP line and headers. -// * Defaults to {@code 128}. -// * -// * @param size initial buffer size -// * @return updated builder instance -// */ -// public Builder initialBufferSize(int size) { -// this.initialBufferSize = size; -// return this; -// } -// -// /** -// * Configure whether to enable content negotiation for compression. -// * -// * @param value compression flag -// * @return updated builder instance -// */ -// public Builder enableCompression(boolean value) { -// this.enableCompression = value; -// return this; -// } -// -// @Override -// public Builder config(Config config) { -// SocketConfigurationBuilder.super.config(config); -// -// config.get("name").asString().ifPresent(this::name); -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// config.get("max-chunk-size").asInt().ifPresent(this::maxChunkSize); -// config.get("validate-headers").asBoolean().ifPresent(this::validateHeaders); -// config.get("initial-buffer-size").asInt().ifPresent(this::initialBufferSize); -// config.get("enable-compression").asBoolean().ifPresent(this::enableCompression); -// -// return this; -// } -// -// int port() { -// return port; -// } -// -// Optional bindAddress() { -// return Optional.ofNullable(bindAddress); -// } -// -// int backlog() { -// return backlog; -// } -// -// int timeoutMillis() { -// return timeoutMillis; -// } -// -// int receiveBufferSize() { -// return receiveBufferSize; -// } -// -// FakeWebServerTlsConfigBean tlsConfig() { -// return webServerTls; -// } -// -// String name() { -// return name; -// } -// -// boolean enabled() { -// return enabled; -// } -// -// int maxHeaderSize() { -// return maxHeaderSize; -// } -// -// int maxInitialLineLength() { -// return maxInitialLineLength; -// } -// -// int maxChunkSize() { -// return maxChunkSize; -// } -// -// boolean validateHeaders() { -// return validateHeaders; -// } -// -// int initialBufferSize() { -// return initialBufferSize; -// } -// -// boolean enableCompression() { -// return enableCompression; -// } -// -// long maxPayloadSize() { -// return maxPayloadSize; -// } -// -// int maxUpgradeContentLength() { -// return maxUpgradeContentLength; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanLogTracingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanLogTracingConfig.java deleted file mode 100644 index 398d44c44c6..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanLogTracingConfig.java +++ /dev/null @@ -1,129 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka SpanLogTracingConfig. - * Configuration of a single log event in a traced span. - */ -@ConfigBean -public interface FakeSpanLogTracingConfig extends FakeTraceableConfig { -// /** -// * Disabled traced span log. -// */ -// public static final FakeSpanLogTracingConfigBean DISABLED = FakeSpanLogTracingConfigBean.builder("disabled").enabled(false).build(); -// /** -// * Enabled traced span log. -// */ -// public static final FakeSpanLogTracingConfigBean ENABLED = FakeSpanLogTracingConfigBean.builder("enabled").build(); -// -// /** -// * A new span log. -// * @param name name of the span log -// */ -// protected FakeSpanLogTracingConfigBean(String name) { -// super(name); -// } -// - -// /** -// * Merge two traced span log configurations. -// * -// * @param older original configuration with default values -// * @param newer new configuration to override the older -// * @return a new traced span log mergint the older and newer -// */ -// static FakeSpanLogTracingConfigBean merge(FakeSpanLogTracingConfigBean older, FakeSpanLogTracingConfigBean newer) { -// return new FakeSpanLogTracingConfigBean(newer.name()) { -// @Override -// public Optional isEnabled() { -// return newer.isEnabled() -// .or(older::isEnabled); -// } -// }; -// } -// -// /** -// * Fluent API builder to create a new traced span log configuration. -// * -// * @param name name of the span log -// * @return a new builder instance -// */ -// public static Builder builder(String name) { -// return new Builder(name); -// } -// -// /** -// * Create a new traced span log configuration from {@link io.helidon.config.Config}. -// * -// * @param name name of the span log -// * @param config config for a traced span log -// * @return a new traced span log configuration -// */ -// public static FakeSpanLogTracingConfigBean create(String name, Config config) { -// return builder(name).config(config).build(); -// } -// -// /** -// * A fluent API builder for {@link FakeSpanLogTracingConfigBean}. -// */ -// public static final class Builder implements io.helidon.common.Builder { -// private final String name; -// private Optional enabled = Optional.empty(); -// -// private Builder(String name) { -// this.name = name; -// } -// -// @Override -// public FakeSpanLogTracingConfigBean build() { -// final Optional finalEnabled = enabled; -// return new FakeSpanLogTracingConfigBean(name) { -// @Override -// public Optional isEnabled() { -// return finalEnabled; -// } -// }; -// } -// -// /** -// * Configure whether this traced span log is enabled or disabled. -// * -// * @param enabled if disabled, this span and all logs will be disabled -// * @return updated builder instance -// */ -// public Builder enabled(boolean enabled) { -// this.enabled = Optional.of(enabled); -// return this; -// } -// -// /** -// * Update this builder from {@link io.helidon.config.Config}. -// * -// * @param config config of a traced span log -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// -// return this; -// } -// } -// -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanTracingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanTracingConfig.java deleted file mode 100644 index d71487a43a1..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeSpanTracingConfig.java +++ /dev/null @@ -1,258 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.Map; -import java.util.Optional; - -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka SpanTracingConfig. - * - * Configuration of a single traced span. - */ -@ConfigBean -public interface FakeSpanTracingConfig extends FakeTraceableConfig { - -// /** -// * A traced span that is disabled and all logs on it are disabled as well. -// */ -// public static final SpanTracingConfig DISABLED = SpanTracingConfig.builder("disabled").enabled(false).build(); -// /** -// * A traced span that is inabled and all logs on it are enabled as well. -// */ -// public static final SpanTracingConfig ENABLED = SpanTracingConfig.builder("enabled").build(); - -// /** -// * A new traceable span. -// * -// * @param name name of this span -// */ -// protected SpanTracingConfig(String name) { -// super(name); -// } -// -// @Override -// public String toString() { -// return "SpanTracingConfig(" + name() + ")"; -// } -// -// /** -// * Merge configuration of two traced spans. -// * -// * @param older older span with default values -// * @param newer newer span overriding values in older -// * @return a new merged traced span configuration -// */ -// static SpanTracingConfig merge(SpanTracingConfig older, SpanTracingConfig newer) { -// return new SpanTracingConfig(newer.name()) { -// @Override -// public Optional newName() { -// return newer.newName() -// .or(older::newName); -// } -// -// @Override -// public Optional isEnabled() { -// return newer.isEnabled() -// .or(older::isEnabled); -// } -// -// @Override -// public Optional getSpanLog(String name) { -// Optional newLog = newer.getSpanLog(name); -// Optional oldLog = older.getSpanLog(name); -// -// if (newLog.isPresent() && oldLog.isPresent()) { -// return Optional.of(SpanLogTracingConfig.merge(oldLog.get(), newLog.get())); -// } -// -// if (newLog.isPresent()) { -// return newLog; -// } -// -// return oldLog; -// } -// }; -// } - - /** - * When rename is desired, returns the new name. - * - * @return new name for this span or empty when rename is not desired - */ - Optional newName(); - -// /** -// * Configuration of a traced span log. -// * -// * @param name name of the log event -// * @return configuration of the log event, or empty if not explicitly configured (used when merging) -// */ -// protected abstract Optional getSpanLog(String name); - - @Singular("spanLog") // B addSpanLog(String, FakeSpanLogTracingConfigBean); - Map spanLogMap(); - -// /** -// * Configuration of a traceable span log. -// * If this span is disabled, the log is always disabled. -// * -// * @param name name of the log event -// * @return configuration of the log event -// */ -// public final SpanLogTracingConfig spanLog(String name) { -// if (enabled()) { -// return getSpanLog(name).orElse(SpanLogTracingConfig.ENABLED); -// } else { -// return SpanLogTracingConfig.DISABLED; -// } -// } -// -// /** -// * Whether a log event should be logged on the span with a default value. -// * -// * @param logName name of the log event -// * @param defaultValue to use in case the log event is not configured in this span's configuration -// * @return whether to log ({@code true}) the event or not ({@code false}), uses the default value for unconfigured logs -// */ -// public boolean logEnabled(String logName, boolean defaultValue) { -// if (enabled()) { -// return getSpanLog(logName).map(Traceable::enabled).orElse(defaultValue); -// } -// return false; -// } -// -// /** -// * A fluent API builder to create traced span configuration. -// * -// * @param name name of the span -// * @return a new builder instance -// */ -// public static Builder builder(String name) { -// return new Builder(name); -// } -// -// /** -// * Create traced span configuration from a {@link io.helidon.config.Config}. -// * -// * @param name name of the span -// * @param config config to load span configuration from -// * @return a new traced span configuration -// */ -// public static SpanTracingConfig create(String name, Config config) { -// return builder(name).config(config).build(); -// } -// -// /** -// * A fluent API builder for {@link SpanTracingConfig}. -// */ -// public static final class Builder implements io.helidon.common.Builder { -// private final Map spanLogMap = new HashMap<>(); -// private final String name; -// private Optional enabled = Optional.empty(); -// private String newName; -// -// private Builder(String name) { -// this.name = name; -// } -// -// @Override -// public SpanTracingConfig build() { -// final Map finalSpanLogMap = new HashMap<>(spanLogMap); -// final Optional finalNewName = Optional.ofNullable(newName); -// final Optional finalEnabled = enabled; -// -// return new SpanTracingConfig(name) { -// @Override -// public Optional newName() { -// return finalNewName; -// } -// -// @Override -// public Optional isEnabled() { -// return finalEnabled; -// } -// -// @Override -// protected Optional getSpanLog(String name) { -// if (enabled.orElse(true)) { -// return Optional.ofNullable(finalSpanLogMap.get(name)); -// } -// return Optional.of(SpanLogTracingConfig.DISABLED); -// } -// }; -// } -// -// /** -// * Configure whether this traced span is enabled or disabled. -// * -// * @param enabled if disabled, this span and all logs will be disabled -// * @return updated builder instance -// */ -// public Builder enabled(boolean enabled) { -// this.enabled = Optional.of(enabled); -// return this; -// } -// -// /** -// * Configure a new name of this span. -// * -// * @param newName new name to use when reporting this span -// * @return updated builder instance -// */ -// public Builder newName(String newName) { -// this.newName = newName; -// return this; -// } -// -// /** -// * Add configuration of a traced span log. -// * -// * @param spanLogTracingConfig configuration of the traced span log -// * @return updated builder instance -// */ -// public Builder addSpanLog(SpanLogTracingConfig spanLogTracingConfig) { -// this.spanLogMap.put(spanLogTracingConfig.name(), spanLogTracingConfig); -// return this; -// } -// -// /** -// * Update this builder from {@link io.helidon.config.Config}. -// * -// * @param config configuration of this span -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// config.get("new-name").asString().ifPresent(this::newName); -// config.get("logs") -// .asNodeList() -// .ifPresent(nodes -> { -// nodes.forEach(node -> { -// // name is mandatory -// addSpanLog(SpanLogTracingConfig.create(node.get("name").asString().get(), node)); -// }); -// }); -// -// return this; -// } -// } -// -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracer.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracer.java deleted file mode 100644 index a431c9d2801..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracer.java +++ /dev/null @@ -1,103 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -/** - * Tracer abstraction. - * Tracer is the central point that collects tracing spans, and (probably) pushes them to backend. - */ -public interface FakeTracer { -// /** -// * Create a no-op tracer. All spans created from this tracer are not doing anything. -// * -// * @return no-op tracer -// */ -// static Tracer noOp() { -// return NoOpTracer.instance(); -// } -// -// /** -// * Get the currently registered global tracer. -// * -// * @return global tracer -// */ -// static Tracer global() { -// return TracerProviderHelper.global(); -// } -// -// /** -// * Register a global tracer, behavior depends on implementation. -// * -// * @param tracer tracer to use as a global tracer -// */ -// -// static void global(Tracer tracer) { -// TracerProviderHelper.global(tracer); -// } -// -// /** -// * Whether this tracer is enabled or not. -// * A no op tracer is disabled. -// * -// * @return {@code true} if this tracer is enabled -// */ -// boolean enabled(); -// -// /** -// * A new span builder to construct {@link io.helidon.tracing.Span}. -// * -// * @param name name of the operation -// * @return a new span builder -// */ -// Span.Builder spanBuilder(String name); -// -// /** -// * Extract parent span context from inbound request, such as from HTTP headers. -// * -// * @param headersProvider provider of headers -// * @return span context of inbound parent span, or empty optional if no span context can be found -// */ -// Optional extract(HeaderProvider headersProvider); -// -// /** -// * Inject current span as a parent for outbound request, such as when invoking HTTP request from a client. -// * -// * @param spanContext current span context -// * @param inboundHeadersProvider provider of inbound headers, may be {@link HeaderProvider#empty()} or headers from original -// * request (if any) -// * @param outboundHeadersConsumer consumer of headers that should be propagated to remote endpoint -// */ -// void inject(SpanContext spanContext, HeaderProvider inboundHeadersProvider, HeaderConsumer outboundHeadersConsumer); -// -// /** -// * Access the underlying tracer by specific type. -// * This is a dangerous operation that will succeed only if the tracer is of expected type. This practically -// * removes abstraction capabilities of this API. -// * -// * @param tracerClass type to access -// * @return instance of the tracer -// * @param type of the tracer -// * @throws java.lang.IllegalArgumentException in case the tracer cannot provide the expected type -// */ -// default T unwrap(Class tracerClass) { -// try { -// return tracerClass.cast(this); -// } catch (ClassCastException e) { -// throw new IllegalArgumentException("This tracer is not compatible with " + tracerClass.getName()); -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracingConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracingConfig.java deleted file mode 100644 index 00bc2569286..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeTracingConfig.java +++ /dev/null @@ -1,245 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.Map; - -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka TracingConfig. - * - * Tracing configuration that contains traced components (such as WebServer, Security) and their traced spans and span logs. - * Spans can be renamed through configuration, components, spans and span logs may be disabled through this configuration. - */ -@ConfigBean(key = "tracing") -public interface FakeTracingConfig extends FakeTraceableConfig { - -// /** -// * Traced config that is enabled for all components, spans and logs. -// */ -// FakeTracingConfig ENABLED = FakeTracingConfig.builder().build(); -// /** -// * Traced conifg that is disabled for all components, spans and logs. -// */ -// FakeTracingConfig DISABLED = FakeTracingConfig.builder().enabled(false).build(); - -// /** -// * A new traced configuration. -// * -// * @param name name of this configuration, when created using {@link FakeTracingConfig.Builder}, -// * the name is {@code helidon} -// */ -// protected FakeTracingConfig(String name) { -// super(name); -// } -// -// /** -// * Configuration of a traced component. -// * -// * @param componentName name of the component -// * @return component tracing configuration or empty if defaults should be used -// */ -// protected abstract Optional getComponent(String componentName); -// -// /** -// * Configuration of a traced component. -// * -// * @param componentName name of the component -// * @return component tracing configuration if configured, or an enabled component configuration -// */ -// public ComponentTracingConfig component(String componentName) { -// return component(componentName, true); -// } - - @Singular("component") // Builder::addComponent(String component); Impl::getComponent(String component); - Map components(); - -// /** -// * Configuration of a traced component. -// * -// * @param componentName name of the component -// * @param enabledByDefault whether the component should be enabled or disabled in case it is not configured -// * @return component tracing configuration if configured, or an enabled/disabled component configuration depending on -// * {@code enabledByDefault} -// */ -// public ComponentTracingConfig component(String componentName, boolean enabledByDefault) { -// if (enabled()) { -// return getComponent(componentName) -// .orElseGet(() -> enabledByDefault ? ComponentTracingConfig.ENABLED : ComponentTracingConfig.DISABLED); -// } -// -// return ComponentTracingConfig.DISABLED; -// } -// -// @Override -// public String toString() { -// return "TracingConfig(" + name() + ")"; -// } -// -// /** -// * Create new tracing configuration based on the provided config. -// * -// * @param config configuration of tracing -// * @return tracing configuration -// */ -// public static FakeTracingConfig create(Config config) { -// return builder().config(config).build(); -// } -// -// /** -// * A fluent API builder for tracing configuration. -// * @return a new builder instance -// */ -// public static Builder builder() { -// return new Builder(); -// } -// -// /** -// * Merge two configurations together. -// * The result will combine configuration from both configurations. In case -// * of conflicts, the {@code newer} wins. -// * -// * @param older older instance to merge -// * @param newer newer (more significant) instance to merge -// * @return a new configuration combining odler and newer -// */ -// public static FakeTracingConfig merge(FakeTracingConfig older, FakeTracingConfig newer) { -// return new FakeTracingConfig(newer.name()) { -// @Override -// public Optional getComponent(String componentName) { -// Optional newerComponent = newer.getComponent(componentName); -// Optional olderComponent = older.getComponent(componentName); -// -// // both configured -// if (newerComponent.isPresent() && olderComponent.isPresent()) { -// return Optional.of(ComponentTracingConfig.merge(olderComponent.get(), newerComponent.get())); -// } -// -// // only newer configured -// if (newerComponent.isPresent()) { -// return newerComponent; -// } -// -// // only older configured -// return olderComponent; -// } -// -// @Override -// public Optional isEnabled() { -// return newer.isEnabled() -// .or(older::isEnabled); -// } -// }; -// } -// -// /** -// * Return configuration of a specific span. -// * This is a shortcut method to {@link #component(String)} and -// * {@link ComponentTracingConfig#span(String)}. -// * -// * @param component component, such as "web-server", "security" -// * @param spanName name of the span, such as "HTTP Request", "security:atn" -// * @return configuration of the span if present in this traced system configuration -// */ -// public SpanTracingConfig spanConfig(String component, String spanName) { -// return component(component).span(spanName); -// } - -// /** -// * Fluent API builder for {@link FakeTracingConfig}. -// */ -// public static final class Builder implements io.helidon.common.Builder { -// private final Map components = new HashMap<>(); -// private Optional enabled = Optional.empty(); -// -// private Builder() { -// } -// -// @Override -// public FakeTracingConfig build() { -// return new RootTracingConfig("helidon", new HashMap<>(components), enabled); -// } -// -// /** -// * Update this builder from configuration. -// * -// * @param config Config with tracing configuration -// * @return updated builder instance -// */ -// public Builder config(Config config) { -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// Config compConfig = config.get("components"); -// compConfig.asNodeList() -// .ifPresent(compList -> { -// compList.forEach(componentConfig -> addComponent(ComponentTracingConfig.create(componentConfig.name(), -// componentConfig))); -// }); -// -// return this; -// } -// -// /** -// * Add a traced component configuration. -// * -// * @param component configuration of this component's tracing -// * @return updated builder instance -// */ -// public Builder addComponent(ComponentTracingConfig component) { -// components.put(component.name(), component); -// return this; -// } -// -// /** -// * Whether overall tracing is enabled. -// * If tracing is disabled on this level, all traced components and spans are disabled - even if explicitly configured -// * as enabled. -// * -// * @param enabled set to {@code false} to disable tracing for any component and span -// * @return updated builder instance -// */ -// public Builder enabled(boolean enabled) { -// this.enabled = Optional.of(enabled); -// return this; -// } -// } -// -// static final class RootTracingConfig extends FakeTracingConfig { -// private final Map components; -// private final Optional enabled; -// -// RootTracingConfig(String name, -// Map components, -// Optional enabled) { -// super(name); -// this.components = components; -// this.enabled = enabled; -// } -// -// @Override -// public Optional getComponent(String componentName) { -// return Optional.ofNullable(components.get(componentName)); -// } -// -// @Override -// public Optional isEnabled() { -// return enabled; -// } -// -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServerTlsConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServerTlsConfig.java deleted file mode 100644 index 6b7e05e4798..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServerTlsConfig.java +++ /dev/null @@ -1,437 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.security.SecureRandom; -import java.util.Collection; -import java.util.Random; -import java.util.Set; - -import javax.net.ssl.SSLContext; - -import io.helidon.common.LazyValue; -import io.helidon.builder.Singular; -import io.helidon.pico.builder.config.ConfigBean; - -/** - * aka WebServerTls. - * - * A class wrapping transport layer security (TLS) configuration for - * WebServer sockets. - */ -@ConfigBean -public interface FakeWebServerTlsConfig { - String PROTOCOL = "TLS"; - // secure random cannot be stored in native image, it must be initialized at runtime - LazyValue RANDOM = LazyValue.create(SecureRandom::new); - - /** - * This constant is a context classifier for the x509 client certificate if it is present. Callers may use this - * constant to lookup the client certificate associated with the current request context. - */ - String CLIENT_X509_CERTIFICATE = FakeWebServerTlsConfig.class.getName() + ".client-x509-certificate"; - -// private final Set enabledTlsProtocols; -// private final Set cipherSuite; -// private final SSLContext sslContext; -// private final boolean enabled; -// private final ClientAuthentication clientAuth; -// -// private FakeWebServerTlsConfigBean(Builder builder) { -// this.enabledTlsProtocols = Set.copyOf(builder.enabledTlsProtocols); -// this.cipherSuite = builder.cipherSuite; -// this.sslContext = builder.sslContext; -// this.enabled = (null != sslContext); -// this.clientAuth = builder.clientAuth; -// } -// -// /** -// * A fluent API builder for {@link FakeWebServerTlsConfigBean}. -// * -// * @return a new builder instance -// */ -// public static Builder builder() { -// return new Builder(); -// } -// -// /** -// * Create TLS configuration from config. -// * -// * @param config located on the node of the tls configuration (usually this is {@code ssl}) -// * @return a new TLS configuration -// */ -// public static FakeWebServerTlsConfigBean create(Config config) { -// return builder().config(config).build(); -// } -// - Collection enabledTlsProtocols(); -// { -// return enabledTlsProtocols; -// } -// - - SSLContext sslContext(); -// { -// return sslContext; -// } - -// ClientAuthentication clientAuth() { -// return clientAuth; -// } - - @Singular("cipher") - Set cipherSuite(); -// { -// return cipherSuite; -// } - - /** - * Whether this TLS config has security enabled (and the socket is going to be - * protected by one of the TLS protocols), or no (and the socket is going to be plain). - * - * @return {@code true} if this configuration represents a TLS configuration, {@code false} for plain configuration - */ - boolean enabled(); -// { -// return enabled; -// } -// -// /** -// * Fluent API builder for {@link FakeWebServerTlsConfigBean}. -// */ -// @Configured -// public static class Builder implements io.helidon.common.Builder { -// private final Set enabledTlsProtocols = new HashSet<>(); -// -// private SSLContext sslContext; -// private FakeKeyConfig privateKeyConfig; -// private FakeKeyConfig trustConfig; -// private long sessionCacheSize; -// private long sessionTimeoutSeconds; -// -// private boolean enabled; -// private Boolean explicitEnabled; -// private ClientAuthentication clientAuth; -// private Set cipherSuite = Set.of(); -// -// private Builder() { -// clientAuth = ClientAuthentication.NONE; -// } -// -// @Override -// public FakeWebServerTlsConfigBean build() { -// boolean enabled; -// -// if (null == explicitEnabled) { -// enabled = this.enabled; -// } else { -// enabled = explicitEnabled; -// } -// -// if (!enabled) { -// this.sslContext = null; -// // ssl is disabled -// return new FakeWebServerTlsConfigBean(this); -// } -// -// if (null == sslContext) { -// // no explicit ssl context, build it using private key and trust store -// sslContext = newSSLContext(); -// } -// -// return new FakeWebServerTlsConfigBean(this); -// } -// -// /** -// * Update this builder from configuration. -// * -// * @param config config on the node of SSL configuration -// * @return this builder -// */ -// public Builder config(Config config) { -// config.get("enabled").asBoolean().ifPresent(this::enabled); -// -// if (explicitEnabled != null && !explicitEnabled) { -// return this; -// } -// -// config.get("client-auth").asString().ifPresent(this::clientAuth); -// config.get("private-key") -// .ifExists(it -> privateKey(FakeKeyConfig.create(it))); -// -// config.get("trust") -// .ifExists(it -> trust(FakeKeyConfig.create(it))); -// -// config.get("protocols").asList(String.class).ifPresent(this::enabledProtocols); -// config.get("session-cache-size").asLong().ifPresent(this::sessionCacheSize); -// config.get("cipher-suite").asList(String.class).ifPresent(this::allowedCipherSuite); -// DeprecatedConfig.get(config, "session-timeout-seconds", "session-timeout") -// .asLong() -// .ifPresent(this::sessionTimeoutSeconds); -// -// return this; -// } -// -// private void clientAuth(String it) { -// clientAuth(ClientAuthentication.valueOf(it.toUpperCase())); -// } -// -// /** -// * Configures whether client authentication will be required or not. -// * -// * @param clientAuth client authentication -// * @return this builder -// */ -// @ConfiguredOption("none") -// public Builder clientAuth(ClientAuthentication clientAuth) { -// this.clientAuth = Objects.requireNonNull(clientAuth); -// return this; -// } -// -// /** -// * Configures a {@link SSLContext} to use with the server socket. If not {@code null} then -// * the server enforces an SSL communication. -// * -// * @param context a SSL context to use -// * @return this builder -// */ -// public Builder sslContext(SSLContext context) { -// this.enabled = true; -// this.sslContext = context; -// return this; -// } -// -// /** -// * Configures the TLS protocols to enable with the server socket. -// * @param protocols protocols to enable, if empty, enables defaults -// * -// * @return this builder -// * @throws java.lang.NullPointerException in case the protocols is null -// */ -// public Builder enabledProtocols(String... protocols) { -// return enabledProtocols(Arrays.asList(Objects.requireNonNull(protocols))); -// } -// -// /** -// * Configures the TLS protocols to enable with the server socket. -// * -// * @param protocols protocols to enable, if empty enables -// * the default protocols -// * @return this builder -// * @throws java.lang.NullPointerException in case the protocols is null -// */ -// public Builder enabledProtocols(Collection protocols) { -// Objects.requireNonNull(protocols); -// -// this.enabledTlsProtocols.clear(); -// this.enabledTlsProtocols.addAll(protocols); -// return this; -// } -// -// /** -// * Configure private key to use for SSL context. -// * -// * @param privateKeyConfig the required private key configuration parameter -// * @return this builder -// */ -// @ConfiguredOption(required = true) -// public Builder privateKey(FakeKeyConfig privateKeyConfig) { -// // setting private key, need to reset ssl context -// this.enabled = true; -// this.sslContext = null; -// this.privateKeyConfig = Objects.requireNonNull(privateKeyConfig); -// return this; -// } -// -// /** -// * Configure private key to use for SSL context. -// * -// * @param privateKeyConfigBuilder the required private key configuration parameter -// * @return this builder -// */ -// public Builder privateKey(Supplier privateKeyConfigBuilder) { -// return privateKey(privateKeyConfigBuilder.get()); -// } -// -// /** -// * Set the trust key configuration to be used to validate certificates. -// * -// * @param trustConfig the trust configuration -// * @return this builder -// */ -// @ConfiguredOption -// public Builder trust(FakeKeyConfig trustConfig) { -// // setting explicit trust, need to reset ssl context -// this.enabled = true; -// this.sslContext = null; -// this.trustConfig = Objects.requireNonNull(trustConfig); -// return this; -// } -// -// /** -// * Set the trust key configuration to be used to validate certificates. -// * -// * @param trustConfigBuilder the trust configuration builder -// * @return this builder -// */ -// public Builder trust(Supplier trustConfigBuilder) { -// return trust(trustConfigBuilder.get()); -// } -// -// /** -// * Set the size of the cache used for storing SSL session objects. {@code 0} to use the -// * default value. -// * -// * @param sessionCacheSize the session cache size -// * @return this builder -// */ -// @ConfiguredOption -// public Builder sessionCacheSize(long sessionCacheSize) { -// this.sessionCacheSize = sessionCacheSize; -// return this; -// } -// -// /** -// * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the -// * default value. -// * -// * @param sessionTimeout the session timeout -// * @return this builder -// */ -// @ConfiguredOption -// public Builder sessionTimeoutSeconds(long sessionTimeout) { -// this.sessionTimeoutSeconds = sessionTimeout; -// return this; -// } -// -// /** -// * Set the timeout for the cached SSL session objects. {@code 0} to use the -// * default value. -// * -// * @param timeout the session timeout amount -// * @param unit the session timeout time unit -// * @return this builder -// */ -// public Builder sessionTimeout(long timeout, TimeUnit unit) { -// this.sessionTimeoutSeconds = unit.toSeconds(timeout); -// return this; -// } -// -// /** -// * Set allowed cipher suite. If an empty collection is set, an exception is thrown since -// * it is required to support at least some ciphers. -// * -// * @param cipherSuite allowed cipher suite -// * @return an updated builder -// */ -// @ConfiguredOption(key = "cipher-suite") -// public Builder allowedCipherSuite(List cipherSuite) { -// Objects.requireNonNull(cipherSuite); -// if (cipherSuite.isEmpty()) { -// throw new IllegalStateException("Allowed cipher suite has to have at least one cipher specified"); -// } -// this.cipherSuite = Set.copyOf(cipherSuite); -// return this; -// } -// -// /** -// * Whether the TLS config should be enabled or not. -// * -// * @param enabled configure to {@code false} to disable SSL context (and SSL support on the server) -// * @return this builder -// */ -// @ConfiguredOption(description = "Can be used to disable TLS even if keys are configured.", value = "true") -// public Builder enabled(boolean enabled) { -// this.enabled = enabled; -// this.explicitEnabled = enabled; -// return this; -// } -// -// private SSLContext newSSLContext() { -// try { -// if (null == privateKeyConfig) { -// throw new IllegalStateException("Private key must be configured when SSL is enabled."); -// } -// KeyManagerFactory kmf = buildKmf(this.privateKeyConfig); -// TrustManagerFactory tmf = buildTmf(this.trustConfig); -// -// // Initialize the SSLContext to work with our key managers. -// SSLContext ctx = SSLContext.getInstance(PROTOCOL); -// ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); -// -// SSLSessionContext sessCtx = ctx.getServerSessionContext(); -// if (sessionCacheSize > 0) { -// sessCtx.setSessionCacheSize((int) Math.min(sessionCacheSize, Integer.MAX_VALUE)); -// } -// if (this.sessionTimeoutSeconds > 0) { -// sessCtx.setSessionTimeout((int) Math.min(sessionTimeoutSeconds, Integer.MAX_VALUE)); -// } -// return ctx; -// } catch (IOException | GeneralSecurityException e) { -// throw new IllegalStateException("Failed to build server SSL Context!", e); -// } -// } -// -// private static KeyManagerFactory buildKmf(FakeKeyConfig privateKeyConfig) throws IOException, GeneralSecurityException { -// String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); -// if (algorithm == null) { -// algorithm = "SunX509"; -// } -// -// byte[] passwordBytes = new byte[64]; -// RANDOM.get().nextBytes(passwordBytes); -// char[] password = Base64.getEncoder().encodeToString(passwordBytes).toCharArray(); -// -// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); -// ks.load(null, null); -// ks.setKeyEntry("key", -// privateKeyConfig.privateKey().orElseThrow(() -> new RuntimeException("Private key not available")), -// password, -// privateKeyConfig.certChain().toArray(new Certificate[0])); -// -// KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); -// kmf.init(ks, password); -// -// return kmf; -// } -// -// private static TrustManagerFactory buildTmf(FakeKeyConfig trustConfig) -// throws IOException, GeneralSecurityException { -// List certs; -// -// if (trustConfig == null) { -// certs = List.of(); -// } else { -// certs = trustConfig.certs(); -// } -// -// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); -// ks.load(null, null); -// -// int i = 1; -// for (X509Certificate cert : certs) { -// ks.setCertificateEntry(String.valueOf(i), cert); -// i++; -// } -// -// TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); -// tmf.init(ks); -// return tmf; -// } -// } -// -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/SSLContextConfig.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/SSLContextConfig.java deleted file mode 100644 index 353d7af35a6..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/SSLContextConfig.java +++ /dev/null @@ -1,204 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.Random; - -import io.helidon.builder.Builder; - -/** - * aka SSLContextBuilder. - * Note that this is just a normal builder, and will not be integrated with Config. - * Builder for configuring a new SslContext for creation. - */ -@Builder -public interface SSLContextConfig { - - String PROTOCOL = "TLS"; - Random RANDOM = new Random(); - -// private FakeKeyConfig privateKeyConfig; -// private FakeKeyConfig trustConfig; -// private long sessionCacheSize; -// private long sessionTimeout; -// -// private SSLContextConfig() { -// } -// - -// /** -// * Creates a builder of the {@link javax.net.ssl.SSLContext}. -// * -// * @param privateKeyConfig the required private key configuration parameter -// * @return this builder -// */ -// public static SSLContextConfig create(FakeKeyConfig privateKeyConfig) { -// return new SSLContextConfig().privateKeyConfig(privateKeyConfig); -// } - -// /** -// * Creates {@link javax.net.ssl.SSLContext} from the provided configuration. -// * -// * @param sslConfig the ssl configuration -// * @return a built {@link javax.net.ssl.SSLContext} -// * @throws IllegalStateException in case of a problem; will wrap either an instance of {@link java.io.IOException} or -// * a {@link java.security.GeneralSecurityException} -// */ -// static SSLContext create(Config sslConfig) { -// return new SSLContextConfig().privateKeyConfig(FakeKeyConfig.create(sslConfig.get("private-key"))) -// .sessionCacheSize(sslConfig.get("session-cache-size").asInt().orElse(0)) -// .sessionTimeout(sslConfig.get("session-timeout").asInt().orElse(0)) -// .trustConfig(FakeKeyConfig.create(sslConfig.get("trust"))) -// .build(); -// } -// -// private SSLContextConfig privateKeyConfig(FakeKeyConfig privateKeyConfig) { -// this.privateKeyConfig = privateKeyConfig; -// return this; -// } - - FakeKeyConfig privateKeyConfig(); - -// -// /** -// * Set the trust key configuration to be used to validate certificates. -// * -// * @param trustConfig the trust configuration -// * @return an updated builder -// */ -// public SSLContextConfig trustConfig(FakeKeyConfig trustConfig) { -// this.trustConfig = trustConfig; -// return this; -// } - - FakeKeyConfig trustConfig(); - -// /** -// * Set the size of the cache used for storing SSL session objects. {@code 0} to use the -// * default value. -// * -// * @param sessionCacheSize the session cache size -// * @return an updated builder -// */ -// public SSLContextConfig sessionCacheSize(long sessionCacheSize) { -// this.sessionCacheSize = sessionCacheSize; -// return this; -// } - - long sessionCacheSize(); - -// /** -// * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the -// * default value. -// * -// * @param sessionTimeout the session timeout -// * @return an updated builder -// */ -// public SSLContextConfig sessionTimeout(long sessionTimeout) { -// this.sessionTimeout = sessionTimeout; -// return this; -// } - - long sessionTimeout(); - -// /** -// * Create new {@code {@link javax.net.ssl.SSLContext}} instance with configured settings. -// * -// * @return the SSL Context built instance -// * @throws IllegalStateException in case of a problem; will wrap either an instance of {@link java.io.IOException} or -// * a {@link java.security.GeneralSecurityException} -// */ -// public SSLContext build() { -// Objects.requireNonNull(privateKeyConfig, "The private key config must be set!"); -// -// try { -// return newSSLContext(privateKeyConfig, trustConfig, sessionCacheSize, sessionTimeout); -// } catch (IOException | GeneralSecurityException e) { -// throw new IllegalStateException("Building of the SSLContext of unsuccessful!", e); -// } -// } - - // re-enable this. -// private static SSLContext newSSLContext(FakeKeyConfig privateKeyConfig, -// FakeKeyConfig trustConfig, -// long sessionCacheSize, -// long sessionTimeout) -// throws IOException, GeneralSecurityException { -// KeyManagerFactory kmf = buildKmf(privateKeyConfig); -// TrustManagerFactory tmf = buildTmf(trustConfig); -// -// // Initialize the SSLContext to work with our key managers. -// SSLContext ctx = SSLContext.getInstance(PROTOCOL); -// ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); -// -// SSLSessionContext sessCtx = ctx.getServerSessionContext(); -// if (sessionCacheSize > 0) { -// sessCtx.setSessionCacheSize((int) Math.min(sessionCacheSize, Integer.MAX_VALUE)); -// } -// if (sessionTimeout > 0) { -// sessCtx.setSessionTimeout((int) Math.min(sessionTimeout, Integer.MAX_VALUE)); -// } -// return ctx; -// } -// -// private static KeyManagerFactory buildKmf(FakeKeyConfig privateKeyConfig) throws IOException, GeneralSecurityException { -// String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); -// if (algorithm == null) { -// algorithm = "SunX509"; -// } -// -// byte[] passwordBytes = new byte[64]; -// RANDOM.nextBytes(passwordBytes); -// char[] password = Base64.getEncoder().encodeToString(passwordBytes).toCharArray(); -// -// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); -// ks.load(null, null); -// ks.setKeyEntry("key", -// privateKeyConfig.privateKey().orElseThrow(() -> new RuntimeException("Private key not available")), -// password, -// privateKeyConfig.certChain().toArray(new Certificate[0])); -// -// KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); -// kmf.init(ks, password); -// -// return kmf; -// } -// -// private static TrustManagerFactory buildTmf(FakeKeyConfig trustConfig) -// throws IOException, GeneralSecurityException { -// List certs; -// -// if (trustConfig == null) { -// certs = List.of(); -// } else { -// certs = trustConfig.certs(); -// } -// -// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); -// ks.load(null, null); -// -// int i = 1; -// for (X509Certificate cert : certs) { -// ks.setCertificateEntry(String.valueOf(i), cert); -// i++; -// } -// -// TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); -// tmf.init(ks); -// return tmf; -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/WebServer.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/WebServer.java deleted file mode 100644 index 0da8c238531..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/WebServer.java +++ /dev/null @@ -1,773 +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. - */ - -package io.helidon.pico.builder.config.fakes; - -import java.util.Objects; - -import io.helidon.pico.Contract; - -@Contract -public interface WebServer { - - String DEFAULT_SOCKET_NAME = "@default"; - - /** - * Gets effective server configuration. - * - * @return Server configuration - */ - FakeServerConfig configuration(); - -// /** -// * Starts the server. Has no effect if server is running. -// * The start will fail on a server that is shut down, or that failed to start. -// * In such cases, create a new instance of Web Server. -// * -// * @return a single to react on startup process -// */ -// Single start(); -// -// /** -// * Completion stage is completed when server is shut down. -// * -// * @return a completion stage of the server -// */ -// Single whenShutdown(); -// -// /** -// * Attempt to gracefully shutdown server. It is possible to use returned {@link io.helidon.common.reactive.Single} to react. -// *

        -// * RequestMethod can be called periodically. -// * -// * @return a single to react on finished shutdown process -// * @see #start() -// */ -// Single shutdown(); - - /** - * Returns {@code true} if the server is currently running. Running server in stopping phase returns {@code true} until it - * is not fully stopped. - * - * @return {@code true} if server is running - */ - boolean isRunning(); - -// /** -// * Gets a {@link WebServer} context. -// * -// * @return a server context -// */ -// Context context(); -// -// /** -// * Get the parent {@link MessageBodyReaderContext} context. -// * -// * @return media body reader context -// */ -// MessageBodyReaderContext readerContext(); -// -// /** -// * Get the parent {@link MessageBodyWriterContext} context. -// * -// * @return media body writer context -// */ -// MessageBodyWriterContext writerContext(); - - /** - * Returns a port number the default server socket is bound to and is listening on; - * or {@code -1} if unknown or not active. - *

        - * It is supported only when server is running. - * - * @return a listen port; or {@code -1} if unknown or the default server socket is not active - */ -// default int port() { -// return port(WebServer.DEFAULT_SOCKET_NAME); -// } - - /** - * Returns a port number an additional named server socket is bound to and is listening on; - * or {@code -1} if unknown or not active. - * - * @param socketName the name of an additional named server socket - * @return a listen port; or {@code -1} if socket name is unknown or the server socket is not active - */ - // based on runtime -// default int port(String socketName) { -// if (!isRunning()) { -// return -1; -// } -// -// FakeSocketConfig cfg = configuration().sockets().get(socketName); -// return (Objects.isNull(cfg)) ? -1 : cfg.port(); -// } - - /** - * Returns {@code true} if TLS is configured for the default socket. - * - * @return whether TLS is enabled for the default socket - */ - default boolean hasTls() { - return hasTls(WebServer.DEFAULT_SOCKET_NAME); - } - - /** - * Returns {@code true} if TLS is configured for the named socket. - * - * @param socketName the name of a socket - * @return whether TLS is enabled for the socket, returns {@code false} if the socket does not exists - */ - default boolean hasTls(String socketName) { - FakeSocketConfig cfg = configuration().sockets().get(socketName); - return !Objects.isNull(cfg) && cfg.tls().isPresent(); - } - -// /** -// * Update the TLS configuration of the default socket {@link WebServer#DEFAULT_SOCKET_NAME}. -// * -// * @param tls new TLS configuration -// * @throws IllegalStateException if {@link WebServerTls#enabled()} returns {@code false} or -// * if {@code SocketConfiguration.tls().sslContext()} returns {@code null} -// */ -// void updateTls(WebServerTls tls); -// -// /** -// * Update the TLS configuration of the named socket. -// * -// * @param tls new TLS configuration -// * @param socketName specific named socket name -// * @throws IllegalStateException if {@link WebServerTls#enabled()} returns {@code false} or -// * if {@code SocketConfiguration.tls().sslContext()} returns {@code null} -// */ -// void updateTls(WebServerTls tls, String socketName); -// -// /** -// * Creates new instance from provided routing and default configuration. -// * -// * @param routing a routing instance -// * @return a new web server instance -// * @throws IllegalStateException if none SPI implementation found -// * @throws NullPointerException if 'routing' parameter is {@code null} -// */ -// static WebServer create(Routing routing) { -// return builder(routing).build(); -// } -// -// /** -// * Creates new instance from provided configuration and routing. -// * -// * @param routing a routing instance -// * @param config configuration located on server configuration node -// * @return a new web server instance -// * @throws NullPointerException if 'routing' parameter is {@code null} -// * -// * @since 2.0.0 -// */ -// static WebServer create(Routing routing, Config config) { -// return builder(routing) -// .config(config) -// .build(); -// } -// -// /** -// * Creates new instance from provided configuration and routing. -// * -// * @param routingBuilder a supplier of routing (such as {@link Routing.Builder} -// * @param config configuration located on server configuration node -// * @return a new web server instance -// * @throws NullPointerException if 'routing' parameter is {@code null} -// * -// * @since 2.0.0 -// */ -// static WebServer create(Supplier routingBuilder, Config config) { -// return builder(routingBuilder.get()) -// .config(config) -// .build(); -// } -// -// /** -// * Creates new instance from provided routing and default configuration. -// * -// * @param routingBuilder a routing builder instance that will be built as a first step -// * of this method execution -// * @return a new web server instance -// * @throws IllegalStateException if none SPI implementation found -// * @throws NullPointerException if 'routing' parameter is {@code null} -// */ -// static WebServer create(Supplier routingBuilder) { -// Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); -// return create(routingBuilder.get()); -// } - -// /** -// * Creates a builder of the {@link WebServer}. -// * -// * @param routingBuilder the routing builder; must not be {@code null} -// * @return the builder -// */ -// static Builder builder(Supplier routingBuilder) { -// Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); -// return builder(routingBuilder.get()); -// } -// -// /** -// * Creates a fluent API builder of the {@link io.helidon.webserver.WebServer}. -// * Before calling the {@link io.helidon.webserver.WebServer.Builder#build()} method, you should -// * configure the default routing. If none is configured, all requests will end in {@code 404}. -// * -// * @return a new builder -// */ -// static Builder builder() { -// return new Builder(); -// } -// -// /** -// * Creates a builder of the {@link WebServer}. -// * -// * @param routing the routing to use for the default port; must not be {@code null} -// * @return the builder -// * @see #builder() -// */ -// static Builder builder(Routing routing) { -// return builder().addRouting(routing); -// } -// -// /** -// * WebServer builder class provides a convenient way to set up WebServer with multiple server -// * sockets and optional multiple routings. -// */ -// @Configured(root = true, prefix = "server", description = "Configuration of the HTTP server.") -// final class Builder implements io.helidon.common.Builder, -// FakeSocketConfigBean.SocketConfigurationBuilder, -// ParentingMediaContextBuilder, -// MediaContextBuilder { -// -// private static final Logger LOGGER = Logger.getLogger(Builder.class.getName()); -// private static final MediaContext DEFAULT_MEDIA_SUPPORT = MediaContext.create(); -// private final Map routingBuilders = new HashMap<>(); -// private final DirectHandlers.Builder directHandlers = DirectHandlers.builder(); -// // internal use - we may keep this even after we remove the public access to ServerConfiguration -// @SuppressWarnings("deprecation") -// private final FakeServerConfigBean.Builder configurationBuilder = FakeServerConfigBean.builder(); -// // for backward compatibility -// @SuppressWarnings("deprecation") -// private FakeServerConfigBean explicitConfig; -// private MessageBodyReaderContext readerContext; -// private MessageBodyWriterContext writerContext; -// -// private Builder() { -// readerContext = MessageBodyReaderContext.create(DEFAULT_MEDIA_SUPPORT.readerContext()); -// writerContext = MessageBodyWriterContext.create(DEFAULT_MEDIA_SUPPORT.writerContext()); -// } -// -// /** -// * Builds the {@link WebServer} instance as configured by this builder and its parameters. -// * -// * @return a ready to use {@link WebServer} -// * @throws IllegalStateException if there are unpaired named routings (as described -// * at {@link #addNamedRouting(String, Routing)}) -// */ -// @Override -// public WebServer build() { -// if (routingBuilders.get(WebServer.DEFAULT_SOCKET_NAME) == null) { -// LOGGER.warning("Creating a web server with no default routing configured."); -// routingBuilders.put(WebServer.DEFAULT_SOCKET_NAME, RouterImpl.builder()); -// } -// if (explicitConfig == null) { -// explicitConfig = configurationBuilder.build(); -// } -// -// Map routers = routingBuilders.entrySet().stream() -// .collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().build())); -// -// String unpairedRoutings = -// routingBuilders.keySet() -// .stream() -// .filter(routingName -> explicitConfig.namedSocket(routingName).isEmpty()) -// .collect(Collectors.joining(", ")); -// -// if (!unpairedRoutings.isEmpty()) { -// throw new IllegalStateException("No server socket configuration found for named routings: " + unpairedRoutings); -// } -// -// routers.values().forEach(Router::beforeStart); -// -// WebServer result = new NettyWebServer(explicitConfig, -// routers, -// writerContext, -// readerContext, -// directHandlers.build()); -// -// RequestRouting defaultRouting = routers.get(WebServer.DEFAULT_SOCKET_NAME).routing(RequestRouting.class, null); -// -// if (defaultRouting != null) { -// defaultRouting.fireNewWebServer(result); -// } -// return result; -// } -// -// /** -// * Configure listener for the default socket. -// * -// * @param socket socket configuration builder consumer -// * @return updated builder -// */ -// public Builder defaultSocket(Consumer socket){ -// socket.accept(this.configurationBuilder.defaultSocketBuilder()); -// return this; -// } -// -// /** -// * Configure the transport to be used by this server. -// * -// * @param transport transport to use -// * @return updated builder instance -// */ -// public Builder transport(Transport transport) { -// configurationBuilder.transport(transport); -// return this; -// } -// -// /** -// * Configure the default routing of this WebServer. Default routing is the one -// * available on the default listen {@link #bindAddress(String) host} and {@link #port() port} of the WebServer -// * -// * @param defaultRouting new default routing; if already configured, this instance would replace the existing instance -// * @return updated builder instance -// * @deprecated Use {@link WebServer.Builder#addRouting(Routing)} -// * or {@link WebServer.Builder#addRouting(Routing)} instead. -// */ -// @Deprecated(since = "3.0.0", forRemoval = true) -// public Builder routing(Routing defaultRouting) { -// addRouting(defaultRouting); -// return this; -// } -// -// /** -// * Configure the default routing of this WebServer. Default routing is the one -// * available on the default listen {@link #bindAddress(String) host} and {@link #port() port} of the WebServer -// * -// * @param routing new default routing; if already configured, this instance would replace the existing instance -// * @return updated builder instance -// */ -// public Builder addRouting(Routing routing) { -// Objects.requireNonNull(routing); -// routingBuilders.computeIfAbsent(WebServer.DEFAULT_SOCKET_NAME, s -> RouterImpl.builder()) -// .addRouting(routing); -// return this; -// } -// -// /** -// * Configure the default routing of this WebServer. Default routing is the one -// * available on the default listen {@link #bindAddress(String) host} and {@link #port() port} of the WebServer -// * -// * @param routingSupplier new default routing; if already configured, this instance would replace the existing instance -// * @return updated builder instance -// */ -// public Builder addRouting(Supplier routingSupplier) { -// Objects.requireNonNull(routingSupplier); -// addRouting(routingSupplier.get()); -// return this; -// } -// -// /** -// * Configure the default routing of this WebServer. Default routing is the one -// * available on the default listen {@link #bindAddress(String) host} and {@link #port() port} of the WebServer -// * -// * @param defaultRouting new default routing; if already configured, this instance would replace the existing instance -// * @return updated builder instance -// */ -// public Builder routing(Supplier defaultRouting) { -// addRouting(Objects.requireNonNull(defaultRouting).get()); -// return this; -// } -// -// /** -// * Configure the default routing of this WebServer. Default routing is the one -// * available on the default listen {@link #bindAddress(String) host} and {@link #port() port} of the WebServer -// * -// * @param routing new default routing; if already configured, this instance would replace the existing instance -// * @return updated builder instance -// */ -// public Builder routing(Consumer routing) { -// Routing.Builder builder = Routing.builder(); -// Objects.requireNonNull(routing).accept(builder); -// routingBuilders.computeIfAbsent(WebServer.DEFAULT_SOCKET_NAME, s -> RouterImpl.builder()) -// .addRoutingBuilder(RequestRouting.class, builder); -// return this; -// } -// -// /** -// * Update this server configuration from the config provided. -// * -// * @param config config located on server node -// * @return an updated builder -// * @since 2.0.0 -// */ -// public Builder config(Config config) { -// this.configurationBuilder.config(config); -// return this; -// } -// -// /** -// * Associates a dedicated routing with an additional server socket configuration. -// *

        -// * The additional server socket configuration must be set as per -// * {@link io.helidon.pico.config.fake.helidon.config.FakeServerConfigBean.Builder#addSocket(String, io.helidon.pico.config.fake.helidon.config.FakeSocketConfigBean)}. If there is no such -// * named server socket configuration, a {@link IllegalStateException} is thrown by the -// * {@link #build()} method. -// * -// * @param name the named server socket configuration to associate the provided routing with -// * @param routing the routing to associate with the provided name of a named server socket -// * configuration -// * @return an updated builder -// */ -// public Builder addNamedRouting(String name, Routing routing) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// Objects.requireNonNull(routing, "Parameter 'routing' must not be null!"); -// routingBuilders.computeIfAbsent(name, s -> RouterImpl.builder()) -// .addRouting(routing); -// return this; -// } -// -// /** -// * Associates a dedicated routing with an additional server socket configuration. -// *

        -// * The additional server socket configuration must be set as per -// * {@link io.helidon.pico.config.fake.helidon.config.FakeServerConfigBean.Builder#addSocket(String, io.helidon.pico.config.fake.helidon.config.FakeSocketConfigBean)}. If there is no such -// * named server socket configuration, a {@link IllegalStateException} is thrown by the -// * {@link #build()} method. -// * -// * @param name the named server socket configuration to associate the provided routing with -// * @param routingBuilder the routing builder to associate with the provided name of a named server socket -// * configuration; will be built as a first step of this method execution -// * @return an updated builder -// */ -// public Builder addNamedRouting(String name, Supplier routingBuilder) { -// Objects.requireNonNull(name, "Parameter 'name' must not be null!"); -// Objects.requireNonNull(routingBuilder, "Parameter 'routingBuilder' must not be null!"); -// -// return addNamedRouting(name, routingBuilder.get()); -// } -// -// @Override -// public Builder mediaContext(MediaContext mediaContext) { -// Objects.requireNonNull(mediaContext); -// this.readerContext = MessageBodyReaderContext.create(mediaContext.readerContext()); -// this.writerContext = MessageBodyWriterContext.create(mediaContext.writerContext()); -// return this; -// } -// -// @Override -// public Builder addMediaSupport(MediaSupport mediaSupport) { -// Objects.requireNonNull(mediaSupport); -// mediaSupport.register(readerContext, writerContext); -// return this; -// } -// -// @Override -// public Builder addReader(MessageBodyReader reader) { -// readerContext.registerReader(reader); -// return this; -// } -// -// @Override -// public Builder addStreamReader(MessageBodyStreamReader streamReader) { -// readerContext.registerReader(streamReader); -// return this; -// } -// -// @Override -// public Builder addWriter(MessageBodyWriter writer) { -// writerContext.registerWriter(writer); -// return this; -// } -// -// @Override -// public Builder addStreamWriter(MessageBodyStreamWriter streamWriter) { -// writerContext.registerWriter(streamWriter); -// return this; -// } -// -// @Override -// public Builder port(int port) { -// configurationBuilder.port(port); -// return this; -// } -// -// @Override -// public Builder bindAddress(InetAddress bindAddress) { -// configurationBuilder.bindAddress(bindAddress); -// return this; -// } -// -// @Override -// public Builder backlog(int backlog) { -// configurationBuilder.backlog(backlog); -// return this; -// } -// -// @Override -// public Builder timeout(long amount, TimeUnit unit) { -// configurationBuilder.timeout(amount, unit); -// return this; -// } -// -// @Override -// public Builder receiveBufferSize(int receiveBufferSize) { -// configurationBuilder.receiveBufferSize(receiveBufferSize); -// return this; -// } -// -// @Override -// public Builder tls(WebServerTls webServerTls) { -// configurationBuilder.tls(webServerTls); -// return this; -// } -// -// @Override -// public Builder maxHeaderSize(int size) { -// configurationBuilder.maxHeaderSize(size); -// return this; -// } -// -// @Override -// public Builder maxInitialLineLength(int length) { -// configurationBuilder.maxInitialLineLength(length); -// return this; -// } -// -// @Override -// public Builder enableCompression(boolean value) { -// configurationBuilder.enableCompression(value); -// return this; -// } -// -// @Override -// public Builder maxPayloadSize(long size) { -// configurationBuilder.maxPayloadSize(size); -// return this; -// } -// -// @Override -// public Builder maxUpgradeContentLength(int size) { -// configurationBuilder.maxUpgradeContentLength(size); -// return this; -// } -// -// /** -// * A helper method to support fluentAPI when invoking another method. -// *

        -// * Example: -// *

        -//         *     WebServer.Builder builder = WebServer.builder();
        -//         *     updateBuilder(builder);
        -//         *     return builder.build();
        -//         * 
        -// * Can be changed to: -// *
        -//         *     return WebServer.builder()
        -//         *              .update(this::updateBuilder)
        -//         *              .build();
        -//         * 
        -// * -// * -// * @param updateFunction function to update this builder -// * @return an updated builder -// */ -// public Builder update(Consumer updateFunction) { -// updateFunction.accept(this); -// return this; -// } -// -// /** -// * Adds an additional named server socket configuration. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param config the additional named server socket configuration, never null -// * @return an updated builder -// */ -// @ConfiguredOption(key = "sockets", kind = ConfiguredOption.Kind.LIST) -// public Builder addSocket(FakeSocketConfigBean config) { -// configurationBuilder.addSocket(config.name(), config); -// return this; -// } -// -// /** -// * Adds or augment existing named server socket configuration. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param name socket name -// * @param socket new or existing configuration builder -// * @return an updated builder -// */ -// public Builder socket(String name, Consumer socket) { -// socket.accept(configurationBuilder.socketBuilder(name)); -// return this; -// } -// -// /** -// * Adds or augment existing named server socket configuration. -// *

        -// * An additional named server socket have a dedicated {@link Routing} configured -// * through supplied routing builder. -// * -// * @param socketName socket name -// * @param builders consumer with socket configuration and dedicated routing builders -// * @return an updated builder -// */ -// public Builder socket(String socketName, BiConsumer builders) { -// builders.accept( -// this.configurationBuilder.socketBuilder(socketName), -// routingBuilders.computeIfAbsent(socketName, s -> RouterImpl.builder()) -// ); -// return this; -// } -// -// /** -// * Adds an additional named server socket configuration builder. As a result, the server will listen -// * on multiple ports. -// *

        -// * An additional named server socket may have a dedicated {@link Routing} configured -// * through {@link io.helidon.webserver.WebServer.Builder#addNamedRouting(String, Routing)}. -// * -// * @param socketConfigurationBuilder the additional named server socket configuration builder; will be built as -// * a first step of this method execution -// * @return an updated builder -// */ -// public Builder addSocket(Supplier socketConfigurationBuilder) { -// FakeSocketConfigBean socketConfiguration = socketConfigurationBuilder.get(); -// -// configurationBuilder.addSocket(socketConfiguration.name(), socketConfiguration); -// return this; -// } -// -// /** -// * Add a named socket and routing. -// * -// * @param socketConfiguration named configuration of the socket -// * @param routing routing to use for this socket -// * -// * @return an updated builder -// */ -// public Builder addSocket(FakeSocketConfigBean socketConfiguration, Routing routing) { -// addSocket(socketConfiguration); -// addNamedRouting(socketConfiguration.name(), routing); -// return this; -// } -// -// -// /** -// * Sets a tracer. -// * -// * @param tracer a tracer to set -// * @return an updated builder -// */ -// public Builder tracer(Tracer tracer) { -// configurationBuilder.tracer(tracer); -// return this; -// } -// -// /** -// * Sets a tracer. -// * -// * @param tracerBuilder a tracer builder to set; will be built as a first step of this method execution -// * @return updated builder -// */ -// public Builder tracer(Supplier tracerBuilder) { -// configurationBuilder.tracer(tracerBuilder); -// return this; -// } -// -// /** -// * A method to validate a named socket configuration exists in this builder. -// * -// * @param socketName name of the socket, using {@link io.helidon.webserver.WebServer#DEFAULT_SOCKET_NAME} -// * will always return {@code true} -// * @return {@code true} in case the named socket is configured in this builder -// */ -// public boolean hasSocket(String socketName) { -// return DEFAULT_SOCKET_NAME.equals(socketName) -// || configurationBuilder.sockets().containsKey(socketName); -// } -// -// /** -// * Configure the application scoped context to be used as a parent for webserver request contexts. -// * @param context top level context -// * @return an updated builder -// */ -// public Builder context(Context context) { -// configurationBuilder.context(context); -// return this; -// } -// -// /** -// * Sets a count of threads in pool used to process HTTP requests. -// * Default value is {@code CPU_COUNT * 2}. -// *

        -// * Configuration key: {@code workers} -// * -// * @param workers a workers count -// * @return an updated builder -// */ -// @ConfiguredOption(key = "worker-count") -// public Builder workersCount(int workers) { -// configurationBuilder.workersCount(workers); -// return this; -// } -// -// /** -// * Set to {@code true} to print detailed feature information on startup. -// * -// * @param shouldPrint whether to print details or not -// * @return updated builder instance -// * @see io.helidon.common.HelidonFeatures -// */ -// @ConfiguredOption(key = "features.print-details", value = "false") -// public Builder printFeatureDetails(boolean shouldPrint) { -// configurationBuilder.printFeatureDetails(shouldPrint); -// return this; -// } -// -// /** -// * Provide a custom handler for events that bypass routing. -// * The handler can customize status, headers and message. -// *

        -// * Examples of bad request ({@link DirectHandler.EventType#BAD_REQUEST}: -// *

          -// *
        • Invalid character in path
        • -// *
        • Content-Length header set to a non-integer value
        • -// *
        • Invalid first line of the HTTP request
        • -// *
        -// * @param handler direct handler to use -// * @param types event types to handle with the provided handler -// * @return updated builder -// */ -// public Builder directHandler(DirectHandler handler, DirectHandler.EventType... types) { -// for (DirectHandler.EventType type : types) { -// directHandlers.addHandler(type, handler); -// } -// -// return this; -// } -// } -} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/test/BasicConfigBeanTest.java b/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/test/BasicConfigBeanTest.java deleted file mode 100644 index cfdbe87cadd..00000000000 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/test/BasicConfigBeanTest.java +++ /dev/null @@ -1,104 +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. - */ - -package io.helidon.pico.builder.config.test; - -import java.util.List; -import java.util.Map; - -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.pico.builder.config.testsubjects.ClientConfig; -import io.helidon.pico.builder.config.testsubjects.DefaultClientConfig; -import io.helidon.pico.builder.config.testsubjects.DefaultServerConfig; -import io.helidon.pico.builder.config.testsubjects.ServerConfig; - -import org.junit.jupiter.api.Test; - -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; -import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; - -class BasicConfigBeanTest { - - @Test - void acceptConfig() { - Config cfg = Config.create( - ConfigSources.create( - Map.of("name", "server", - "port", "8080", - "description", "test", - "pwd", "pwd1" -// , "cipher-suites", "a,b,c" // no List mapper available --- discuss w/ tlanger - ), - "my-simple-config-1")); - ServerConfig serverConfig = DefaultServerConfig.toBuilder(cfg).build(); - - assertThat(serverConfig.description(), optionalValue(equalTo("test"))); - assertThat(serverConfig.name(), equalTo("server")); - assertThat(serverConfig.port(), equalTo(8080)); -// assertThat(serverConfig.cipherSuites(), hasSize(3)); -// assertThat(serverConfig.cipherSuites(), contains("a", "b", "c")); - assertThat(new String(serverConfig.pwd()), equalTo("pwd1")); - assertThat(serverConfig.toString(), - startsWith("ServerConfig")); - assertThat(serverConfig.toString(), - endsWith("(name=server, port=8080, cipherSuites=[], pwd=not-null, description=Optional[test])")); - } - - @Test - void emptyConfig() { - Config cfg = Config.create(); - ServerConfig serverConfig = DefaultServerConfig.toBuilder(cfg).build(); - assertThat(serverConfig.description(), optionalEmpty()); - assertThat(serverConfig.name(), equalTo("default")); - assertThat(serverConfig.port(), equalTo(0)); - } - - /** - * Callers can conceptually use config beans as just plain old vanilla builders, void of any config usage. - */ - @Test - void noConfig() { - ServerConfig serverConfig = DefaultServerConfig.builder().build(); - assertThat(serverConfig.description(), optionalEmpty()); - assertThat(serverConfig.name(), equalTo("default")); - assertThat(serverConfig.port(), equalTo(0)); - assertThat(serverConfig.cipherSuites(), equalTo(List.of())); - - serverConfig = DefaultServerConfig.toBuilder(serverConfig).port(123).build(); - assertThat(serverConfig.description(), optionalEmpty()); - assertThat(serverConfig.name(), equalTo("default")); - assertThat(serverConfig.port(), equalTo(123)); - assertThat(serverConfig.cipherSuites(), equalTo(List.of())); - - ClientConfig clientConfig = DefaultClientConfig.builder().build(); - assertThat(clientConfig.name(), equalTo("default")); - assertThat(clientConfig.port(), equalTo(0)); - assertThat(clientConfig.headers(), equalTo(Map.of())); - assertThat(clientConfig.cipherSuites(), equalTo(List.of())); - - clientConfig = DefaultClientConfig.toBuilder(clientConfig).port(123).build(); - assertThat(clientConfig.name(), equalTo("default")); - assertThat(clientConfig.port(), equalTo(123)); - assertThat(clientConfig.headers(), equalTo(Map.of())); - assertThat(clientConfig.cipherSuites(), equalTo(List.of())); - } - -} diff --git a/pico/configdriven/README.md b/pico/configdriven/README.md new file mode 100644 index 00000000000..d3e95d732b5 --- /dev/null +++ b/pico/configdriven/README.md @@ -0,0 +1,49 @@ +# pico-configdriven + +This is a specialization of the [Pico](../)'s that is based upon Helidon's [configuration](../../config) subsystem, and adds support for something called config-driven services using the [@ConfiguredBy](./api/src/main/java/io/helidon/pico/configdriven/ConfiguredBy.java) annotation. When applied to a target service interface it will allow developers to use a higher level aggregation for their application configuration, and then allow the configuration to drive activation of services in the Pico Framework. + +There are a few additional caveats to understand about ConfiguredBy and its supporting infrastructure. + +* [@ConfigBean Builder](../../builder/builder-config) is used to aggregate configuration attributes to this higher-level, application-centric configuration beans. +* The Pico Framework needs to be started w/ supporting configdriven modules in order for configuration to drive service activation. + +See the user documentation for more information. + +## Modules +* [api](api) - the config-driven API & SPI. +* [processor](processor) - the annotation processor extensions that should be used when using ConfiguredBy. +* [services](services) - the runtime support for config-driven services. +* [tests](tests) - tests that can also serve as examples for usage. + +## Usage Example +1. Follow the basics instructions for [using Pico](../pico/README.md). + +2. Write your [ConfigBean](../../builder/builder-config). + +```java +@ConfigBean("server") +public interface ServerConfig { + @ConfiguredOption("0.0.0.0") + String host(); + + @ConfiguredOption("0") + int port(); +} +``` + +3. Write your [ConfiguredBy](./api/src/main/java/io/helidon/pico/configdriven/ConfiguredBy.java) service. + +```java +@ConfiguredBy(ServerConfig.class) +class LoomServer implements WebServer { + @Inject + LoomServer(ServerConfig serverConfig) { + ... + } +} +``` + +4. Provide your configuration, build, and run. + +## How It Works +At Pico startup initialization, and if configdriven/services is in the runtime classpath, then the Helidon's configuration tree will be scanned for "ConfigBean eligible" instances. And when a configuration matches then the config bean instance is built and fed into a ConfigBeanRegistry. If the ConfiguredBy services is declared to be "driven" (the default value), then the server (in this example the LoomServer) will be automatically started. In this way, the presence of configuration drives demand for a service implicitly starting that service (or services) that are declared to be configured by that config bean (in this example ServerConfig). diff --git a/pico/configdriven/api/pom.xml b/pico/configdriven/api/pom.xml new file mode 100644 index 00000000000..1711e718164 --- /dev/null +++ b/pico/configdriven/api/pom.xml @@ -0,0 +1,71 @@ + + + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-configdriven-api + Helidon Pico Config-Driven ConfiguredBy + + + + io.helidon.builder + helidon-builder-config + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.config + helidon-config-metadata + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + diff --git a/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/ConfiguredBy.java b/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/ConfiguredBy.java new file mode 100644 index 00000000000..a9c160b2dfa --- /dev/null +++ b/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/ConfiguredBy.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 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.configdriven; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +/** + * In support of config-driven services. Place this on a service type interface where {@link jakarta.inject.Singleton} would + * typically go, for example as follows: + *
        + * {@code
        + * @ConfiguredBy(MyConfigBean.class)
        + * public class MyConfiguredService {
        + *     @Inject
        + *     public MyConfiguredService(MyConfigBean cfg) {
        + *         ...
        + *     }
        + *     ...
        + * }}
        + * 
        + * + * @see io.helidon.builder.config.ConfigBean + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(java.lang.annotation.ElementType.TYPE) +@Qualifier +public @interface ConfiguredBy { + + /** + * The {@link io.helidon.builder.config.ConfigBean}-annotated type. + * + * @return the {@code ConfigBean}-annotated type + */ + Class value(); + + /** + * Required to be set to true if {@link #drivesActivation()} is + * applied, thereby overriding the defaults in {@link io.helidon.builder.config.ConfigBean} annotation. + * The default value is {@code false}. + * + * @return true to override the config bean attributes, false to defer to the bean + */ + boolean overrideBean() default false; + + /** + * Determines whether an instance of this config bean in the registry will cause the backing service to be activated. + * The default value is {@code true}. + * Note, however, that {@link #overrideBean()} must be set to true for this to be applied. + * + * @return true if the presence of the config bean has an activation affect (aka, "config-driven services") + */ + boolean drivesActivation() default true; + +} diff --git a/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/package-info.java b/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/package-info.java new file mode 100644 index 00000000000..b0ac2f09945 --- /dev/null +++ b/pico/configdriven/api/src/main/java/io/helidon/pico/configdriven/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Config-Driven API. + */ +package io.helidon.pico.configdriven; diff --git a/pico/configdriven/api/src/main/java/module-info.java b/pico/configdriven/api/src/main/java/module-info.java new file mode 100644 index 00000000000..bf3ec4113cb --- /dev/null +++ b/pico/configdriven/api/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Helidon Pico Config-Driven ConfiguredBy API Module. + */ +module io.helidon.pico.configdriven.api { + requires static jakarta.inject; + requires static io.helidon.builder.config; + + exports io.helidon.pico.configdriven; +} diff --git a/pico/builder-config/pom.xml b/pico/configdriven/pom.xml similarity index 81% rename from pico/builder-config/pom.xml rename to pico/configdriven/pom.xml index a2d1d75fbca..c555049514b 100644 --- a/pico/builder-config/pom.xml +++ b/pico/configdriven/pom.xml @@ -28,16 +28,15 @@ 4.0.0 - io.helidon.pico.builder.config - helidon-pico-builder-config-project - Helidon Pico Builder Config Project + io.helidon.pico.configdriven + helidon-pico-configdriven-project + Helidon Pico Config-Driven Project pom - builder-config + api processor - - + services tests diff --git a/pico/builder-config/processor/README.md b/pico/configdriven/processor/README.md similarity index 88% rename from pico/builder-config/processor/README.md rename to pico/configdriven/processor/README.md index 6d743bf762d..750b7f151bc 100644 --- a/pico/builder-config/processor/README.md +++ b/pico/configdriven/processor/README.md @@ -1,3 +1,3 @@ -# pico-builder-config-processor +# pico-configdriven-processor This module should typically should be used at compile-time on the APt classpath. Also note that even though it is called processor it is not technically a processor, but rather a BuilderCreator extension to the base Builder processor. diff --git a/pico/configdriven/processor/pom.xml b/pico/configdriven/processor/pom.xml new file mode 100644 index 00000000000..7061c3db4ce --- /dev/null +++ b/pico/configdriven/processor/pom.xml @@ -0,0 +1,89 @@ + + + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-configdriven-processor + Helidon Pico Config-Driven Processor + + + + io.helidon.builder + helidon-builder-config + + + io.helidon.pico.configdriven + helidon-pico-configdriven-api + + + io.helidon.pico + helidon-pico-api + + + io.helidon.pico.configdriven + helidon-pico-configdriven-services + + + io.helidon.pico + helidon-pico-processor + + + io.helidon.builder + helidon-builder-processor + + + io.helidon.common + helidon-common-types + + + io.helidon.common + helidon-common-config + + + jakarta.inject + jakarta.inject-api + compile + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java new file mode 100644 index 00000000000..6ab075b652f --- /dev/null +++ b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByProcessor.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2023 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.configdriven.processor; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; + +import io.helidon.builder.AttributeVisitor; +import io.helidon.builder.Builder; +import io.helidon.builder.config.ConfigBean; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.common.config.Config; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.TypeName; +import io.helidon.pico.configdriven.ConfiguredBy; +import io.helidon.pico.configdriven.services.AbstractConfiguredServiceProvider; +import io.helidon.pico.processor.ServiceAnnotationProcessor; +import io.helidon.pico.tools.ActivatorCreatorProvider; +import io.helidon.pico.tools.ServicesToProcess; +import io.helidon.pico.tools.ToolsException; +import io.helidon.pico.tools.TypeTools; + +import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement; +import static io.helidon.builder.processor.tools.BuilderTypeTools.extractValues; +import static io.helidon.builder.processor.tools.BuilderTypeTools.findAnnotationMirror; +import static io.helidon.builder.processor.tools.BuilderTypeTools.toTypeElement; +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.common.types.DefaultTypeName.createFromGenericDeclaration; +import static io.helidon.common.types.DefaultTypeName.createFromTypeName; +import static io.helidon.common.types.DefaultTypeName.toBuilder; + +/** + * Processor for @{@link io.helidon.pico.configdriven.ConfiguredBy} type annotations. + */ +public class ConfiguredByProcessor extends ServiceAnnotationProcessor { + private final System.Logger logger = System.getLogger(getClass().getName()); + private final LinkedHashSet elementsProcessed = new LinkedHashSet<>(); + + static final String TAG_OVERRIDE_BEAN = "overrideBean"; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ConfiguredByProcessor() { + } + + @Override + public Set getSupportedAnnotationTypes() { + Set supported = new LinkedHashSet<>(super.getSupportedAnnotationTypes()); + supported.add(ConfiguredBy.class.getName()); + return supported; + } + + @Override + public Set contraAnnotations() { + return Set.of(); + } + + @Override + public boolean process(Set annotations, + RoundEnvironment roundEnv) { + super.process(annotations, roundEnv); + + if (roundEnv.processingOver()) { + elementsProcessed.clear(); + // we claim this annotation! + return true; + } + + try { + Set typesToProcess = roundEnv.getElementsAnnotatedWith(ConfiguredBy.class); + for (Element element : typesToProcess) { + if (!elementsProcessed.add(element)) { + continue; + } + + try { + process(element); + } catch (Throwable e) { + throw new ToolsException("Failed while processing " + element + "; " + e.getMessage(), e); + } + } + } catch (Throwable e) { + logger.log(System.Logger.Level.ERROR, e.getMessage(), e); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage()); + throw new RuntimeException(e.getMessage(), e); + } + + // we need to claim this annotation! + return true; + } + + void process(Element element) { + if (!(element instanceof TypeElement)) { + throw new ToolsException("Expected " + element + " to be processed as a TypeElement"); + } + + AnnotationMirror am = findAnnotationMirror(ConfiguredBy.class.getName(), element.getAnnotationMirrors()).orElseThrow(); + Map configuredByAttributes = extractValues(am, processingEnv.getElementUtils()); + TypeName configBeanType = createFromTypeName(configuredByAttributes.get("value")); + TypeName serviceTypeName = createTypeNameFromElement(element).orElseThrow(); + TypeElement parent = toTypeElement(((TypeElement) element).getSuperclass()).orElse(null); + TypeName parentServiceTypeName = (parent == null) ? null : createTypeNameFromElement(parent).orElseThrow(); + TypeName activatorImplTypeName = ActivatorCreatorProvider.instance().toActivatorImplTypeName(serviceTypeName); + TypeName genericCB = createFromGenericDeclaration("CB"); + TypeName genericExtendsCB = createFromGenericDeclaration("CB extends " + configBeanType.name()); + boolean hasParent = (parentServiceTypeName != null); + if (hasParent) { + // we already know our parent, but we need to morph it with our activator and new CB reference + TypeName parentActivatorImplTypeName = ActivatorCreatorProvider.instance() + .toActivatorImplTypeName(parentServiceTypeName); + parentServiceTypeName = toBuilder(parentActivatorImplTypeName) + .typeArguments(List.of(genericCB)) + .build(); + } else { + List typeArgs = List.of(serviceTypeName, genericCB); + parentServiceTypeName = create(AbstractConfiguredServiceProvider.class).toBuilder() + .typeArguments(typeArgs) + .build(); + } + + validate((TypeElement) element, configBeanType, serviceTypeName, parentServiceTypeName); + + List extraCodeGen = createExtraCodeGen(activatorImplTypeName, configBeanType, hasParent, configuredByAttributes); + + ServicesToProcess servicesToProcess = ServicesToProcess.servicesInstance(); + boolean accepted = servicesToProcess.addParentServiceType(serviceTypeName, parentServiceTypeName, Optional.of(true)); + assert (accepted); + servicesToProcess.addActivatorGenericDecl(serviceTypeName, "<" + genericExtendsCB.fqName() + ">"); + extraCodeGen.forEach(fn -> servicesToProcess.addExtraCodeGen(serviceTypeName, fn)); + + List extraActivatorClassComments = createExtraActivatorClassComments(); + extraActivatorClassComments.forEach(fn -> servicesToProcess.addExtraActivatorClassComments(serviceTypeName, fn)); + + processServiceType(serviceTypeName, (TypeElement) element); + } + + void validate(TypeElement element, + TypeName configBeanType, + TypeName serviceTypeName, + TypeName parentServiceTypeName) { + assertNoAnnotation(create(jakarta.inject.Singleton.class), element); + validateBeanType(configBeanType); + validateServiceType(serviceTypeName, parentServiceTypeName); + } + + void assertNoAnnotation(TypeName annoType, + TypeElement element) { + Set annos = TypeTools.createAnnotationAndValueSet(element); + Optional anno = DefaultAnnotationAndValue.findFirst(annoType.name(), annos); + if (anno.isPresent()) { + throw new IllegalStateException(annoType + " cannot be used in conjunction with " + + ConfiguredBy.class + " on " + element); + } + } + + void validateBeanType(TypeName configBeanType) { + TypeElement typeElement = (configBeanType == null) + ? null : processingEnv.getElementUtils().getTypeElement(configBeanType.name()); + if (typeElement == null) { + throw new ToolsException("unknown type: " + configBeanType); + } + + if (typeElement.getKind() != ElementKind.INTERFACE) { + throw new ToolsException("The config bean must be an interface: " + typeElement); + } + + Optional cfgBean = BuilderTypeTools + .findAnnotationMirror(ConfigBean.class.getName(), typeElement.getAnnotationMirrors()); + if (cfgBean.isEmpty()) { + throw new ToolsException("The config bean must be annotated with @" + ConfigBean.class.getSimpleName() + + ": " + configBeanType); + } + } + + void validateServiceType(TypeName serviceTypeName, + TypeName ignoredParentServiceTypeName) { + TypeElement typeElement = (serviceTypeName == null) + ? null : processingEnv.getElementUtils().getTypeElement(serviceTypeName.name()); + if (typeElement == null) { + throw new ToolsException("unknown type: " + serviceTypeName); + } + + if (typeElement.getKind() != ElementKind.CLASS) { + throw new ToolsException("The configured service must be a concrete class: " + typeElement); + } + } + + List createExtraCodeGen(TypeName activatorImplTypeName, + TypeName configBeanType, + boolean hasParent, + Map configuredByAttributes) { + List result = new ArrayList<>(); + TypeName configBeanImplName = toDefaultImpl(configBeanType); + + String comment = "\n\t/**\n" + + "\t * Config-driven service constructor.\n" + + "\t * \n" + + "\t * @param configBean config bean\n" + + "\t */"; + if (hasParent) { + result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" + + "\t\tsuper(configBean);\n" + + "\t\tserviceInfo(serviceInfo);\n" + + "\t}\n"); + } else { + result.add("\n\tprivate " + configBeanType + " configBean;\n"); + result.add(comment + "\n\tprotected " + activatorImplTypeName.className() + "(" + configBeanType + " configBean) {\n" + + "\t\tthis.configBean = Objects.requireNonNull(configBean);\n" + + "\t\tassertIsRootProvider(false, true);\n" + + "\t\tserviceInfo(serviceInfo);\n" + + "\t}\n"); + } + + comment = "\n\t/**\n" + + "\t * Creates an instance given a config bean.\n" + + "\t * \n" + + "\t * @param configBean config bean\n" + + "\t */\n"; + result.add(comment + "\t@Override\n" + + "\tprotected " + activatorImplTypeName + " createInstance(Object configBean) {\n" + + "\t\treturn new " + activatorImplTypeName.className() + "((" + configBeanType + ") configBean);\n" + + "\t}\n"); + + if (!hasParent) { + result.add("\t@Override\n" + + "\tpublic Optional configBean() {\n" + + "\t\treturn Optional.ofNullable((CB) configBean);\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic Optional<" + Config.class.getName() + "> rawConfig() {\n" + + "\t\tif (configBean == null) {\n" + + "\t\t\treturn Optional.empty();\n" + + "\t\t}\n" + + "\t\treturn ((" + configBeanImplName + ") configBean).__config();\n" + + "\t}\n"); + } + + result.add("\t@Override\n" + + "\tpublic Class configBeanType() {\n" + + "\t\treturn " + configBeanType + ".class;\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic Map> configBeanAttributes() {\n" + + "\t\treturn " + configBeanImplName + ".__metaAttributes();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic void visitAttributes(CB configBean, " + AttributeVisitor.class.getName() + + " visitor, R userDefinedCtx) {\n" + + "\t\t" + AttributeVisitor.class.getName() + " beanVisitor = visitor::visit;\n" + + "\t\t((" + configBeanImplName + ") configBean).visitAttributes(beanVisitor, userDefinedCtx);\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic CB toConfigBean(" + Config.class.getName() + " config) {\n" + + "\t\treturn (CB) " + configBeanImplName + "\n\t\t\t.toBuilder(config)\n\t\t\t.build();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic " + configBeanImplName + ".Builder " + + "toConfigBeanBuilder(" + Config.class.getName() + " config) {\n" + + "\t\treturn " + configBeanImplName + ".toBuilder(config);\n" + + "\t}\n"); + + if (!hasParent) { + result.add("\t@Override\n" + + "\tprotected CB acceptConfig(io.helidon.common.config.Config config) {\n" + + "\t\tthis.configBean = (CB) super.acceptConfig(config);\n" + + "\t\treturn (CB) this.configBean;\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic String toConfigBeanInstanceId(CB configBean) {\n" + + "\t\treturn ((" + configBeanImplName + + ") configBean).__instanceId();\n" + + "\t}\n"); + result.add("\t@Override\n" + + "\tpublic void configBeanInstanceId(CB configBean, String val) {\n" + + "\t\t((" + configBeanImplName + ") configBean).__instanceId(val);\n" + + "\t}\n"); + } + + String overridesEnabledStr = configuredByAttributes.get(TAG_OVERRIDE_BEAN); + if (Boolean.parseBoolean(overridesEnabledStr)) { + String drivesActivationStr = configuredByAttributes.get(ConfigBeanInfo.TAG_DRIVES_ACTIVATION); + result.add("\t@Override\n" + + "\tprotected boolean drivesActivation() {\n" + + "\t\treturn " + Boolean.parseBoolean(drivesActivationStr) + ";\n" + + "\t}\n"); + } + + return result; + } + + List createExtraActivatorClassComments() { + return List.of("@param the config bean type"); + } + + TypeName toDefaultImpl(TypeName configBeanType) { + return create(configBeanType.packageName(), + Builder.DEFAULT_IMPL_PREFIX + configBeanType.className() + Builder.DEFAULT_SUFFIX); + } + +} diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/package-info.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/package-info.java new file mode 100644 index 00000000000..1eab772221e --- /dev/null +++ b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Config-Driven processor. + */ +package io.helidon.pico.configdriven.processor; diff --git a/pico/configdriven/processor/src/main/java/module-info.java b/pico/configdriven/processor/src/main/java/module-info.java new file mode 100644 index 00000000000..6605681cc0c --- /dev/null +++ b/pico/configdriven/processor/src/main/java/module-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022, 2023 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. + */ + +/** + * Helidon Pico ConfiguredBy Processor module. + */ +module io.helidon.pico.configdriven.processor { + requires java.compiler; + requires jakarta.inject; + requires io.helidon.common; + requires io.helidon.common.config; + requires io.helidon.config.metadata; + requires io.helidon.builder.processor.tools; + requires io.helidon.common.types; + requires io.helidon.pico.api; + requires io.helidon.pico.configdriven.api; + requires io.helidon.pico.configdriven.services; + requires transitive io.helidon.builder.config; + requires transitive io.helidon.builder.processor; + requires transitive io.helidon.builder.processor.spi; + requires transitive io.helidon.pico.processor; + + exports io.helidon.pico.configdriven.processor; + + provides javax.annotation.processing.Processor with + io.helidon.pico.configdriven.processor.ConfiguredByProcessor; +} diff --git a/pico/configdriven/services/README.md b/pico/configdriven/services/README.md new file mode 100644 index 00000000000..efe44b0be00 --- /dev/null +++ b/pico/configdriven/services/README.md @@ -0,0 +1,3 @@ +# pico-configdriven-services + +This module should typically be used by anyone needing Pico's config driven services at runtime. diff --git a/pico/configdriven/services/pom.xml b/pico/configdriven/services/pom.xml new file mode 100644 index 00000000000..049f1bbdb60 --- /dev/null +++ b/pico/configdriven/services/pom.xml @@ -0,0 +1,113 @@ + + + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-configdriven-services + Helidon Pico Config-Driven Services + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-api + + + io.helidon.config + helidon-config + + + io.helidon.pico + helidon-pico-services + + + jakarta.inject + jakarta.inject-api + + + io.helidon.config + helidon-config-metadata + true + + + io.helidon.builder + helidon-builder-config-processor + provided + true + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.pico + helidon-pico-testing + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + + + true + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + + + + + + + + diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/AbstractConfiguredServiceProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/AbstractConfiguredServiceProvider.java new file mode 100644 index 00000000000..b67d20ec7e5 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/AbstractConfiguredServiceProvider.java @@ -0,0 +1,836 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import io.helidon.builder.config.ConfigBean; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.builder.config.spi.HelidonConfigBeanRegistry; +import io.helidon.builder.config.spi.HelidonConfigResolver; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.common.LazyValue; +import io.helidon.common.config.Config; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.pico.CallingContext; +import io.helidon.pico.CallingContextFactory; +import io.helidon.pico.CommonQualifiers; +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DefaultContextualServiceQuery; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Event; +import io.helidon.pico.InjectionException; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.InjectionPointProvider; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServiceProviderException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.ServiceProviderProvider; +import io.helidon.pico.configdriven.ConfiguredBy; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.spi.InjectionResolver; + +import static io.helidon.pico.CallingContext.toErrorMessage; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.hasValue; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.isBlank; + +/** + * Abstract base for any config-driven-service. + * + * @param the type of the service this provider manages + * @param the type of config beans that this service is configured by + */ +// special note: many of these methods are referenced in code generated code! +public abstract class AbstractConfiguredServiceProvider extends AbstractServiceProvider + implements ConfiguredServiceProvider, + ServiceProviderProvider, + InjectionPointProvider, + InjectionResolver { + private static final QualifierAndValue EMPTY_CONFIGURED_BY = DefaultQualifierAndValue.create(ConfiguredBy.class); + private static final CBInstanceComparator BEAN_INSTANCE_ID_COMPARATOR = new CBInstanceComparator(); + private static final System.Logger LOGGER = System.getLogger(AbstractConfiguredServiceProvider.class.getName()); + private static final LazyValue CONFIG_BEAN_REGISTRY + = LazyValue.create(AbstractConfiguredServiceProvider::resolveConfigBeanRegistry); + + private final AtomicReference isRootProvider = new AtomicReference<>(); + private final AtomicReference> rootProvider = new AtomicReference<>(); + private final AtomicBoolean initialized = new AtomicBoolean(); + private final AtomicReference initializationCallingContext + = new AtomicReference<>(); // used only when we are in pico.debug mode + private final Map configBeanMap + = new TreeMap<>(BEAN_INSTANCE_ID_COMPARATOR); + private final Map>> managedConfiguredServicesMap + = new ConcurrentHashMap<>(); + + /** + * The default constructor. + */ + protected AbstractConfiguredServiceProvider() { + } + + /** + * The special comparator for ordering config bean instance ids. + * + * @return the special comparator for ordering config bean instance ids + */ + static Comparator configBeanComparator() { + return BEAN_INSTANCE_ID_COMPARATOR; + } + + static BindableConfigBeanRegistry resolveConfigBeanRegistry() { + HelidonConfigBeanRegistry cbr = ConfigBeanRegistryHolder.configBeanRegistry().orElse(null); + if (cbr == null) { + LOGGER.log(System.Logger.Level.INFO, "Config-Driven Services disabled (config bean registry not found"); + return null; + } + + if (!(cbr instanceof BindableConfigBeanRegistry)) { + Optional callingContext = CallingContextFactory.create(false); + String desc = "Config-Driven Services disabled (unsupported implementation): " + cbr; + String msg = (callingContext.isEmpty()) ? toErrorMessage(desc) : toErrorMessage(callingContext.get(), desc); + throw new PicoException(msg); + } + + return (BindableConfigBeanRegistry) cbr; + } + + /** + * The map of bean instance id's to config bean instances. + * + * @return map of bean instance id's to config bean instances + */ + public Map configBeanMap() { + return Map.copyOf(configBeanMap); + } + + /** + * The map of bean instance id's to managed configured service providers that this instance managed directly. + * + * @return map of bean instance id to managed configured service providers + */ + public Map>> configuredServicesMap() { + return Collections.unmodifiableMap(managedConfiguredServicesMap); + } + + /** + * Called during initialization to register a loaded config bean. + * + * @param instanceId the config bean instance id + * @param configBean the config bean + */ + public void registerConfigBean(String instanceId, + Object configBean) { + Objects.requireNonNull(instanceId); + Objects.requireNonNull(configBean); + assertIsInitializing(); + + Object prev = configBeanMap.put(instanceId, configBean); + assert (prev == null); + + prev = managedConfiguredServicesMap.put(instanceId, Optional.empty()); + assert (prev == null); + } + + @Override + public boolean reset(boolean deep) { + super.reset(deep); + configBeanMap.clear(); + managedConfiguredServicesMap.clear(); + isRootProvider.set(null); + rootProvider.set(null); + initialized.set(false); + initializationCallingContext.set(null); + return true; + } + + @Override + public boolean isRootProvider() { + Boolean root = isRootProvider.get(); + return (root != null && root && rootProvider.get() == null); + } + + @Override + public Optional> rootProvider() { + return Optional.ofNullable(rootProvider.get()); + } + + @Override + @SuppressWarnings("unchecked") + public void rootProvider(ServiceProvider root) { + assertIsRootProvider(false, false); + assert (!isRootProvider() && rootProvider.get() == null && this != root); + boolean set = rootProvider.compareAndSet(null, + (AbstractConfiguredServiceProvider) Objects.requireNonNull(root)); + assert (set); + } + + @Override + public void picoServices(Optional picoServices) { + assertIsInitializing(); + assertIsRootProvider(true, false); + + super.picoServices(picoServices); + + if (isRootProvider()) { + // override out service info to account for any named lookup + ServiceInfo serviceInfo = Objects.requireNonNull(serviceInfo()); + if (!serviceInfo.qualifiers().contains(CommonQualifiers.WILDCARD_NAMED)) { + serviceInfo = DefaultServiceInfo.toBuilder(serviceInfo) + .addQualifier(CommonQualifiers.WILDCARD_NAMED) + .build(); + serviceInfo(serviceInfo); + } + + // bind to the config bean registry ... but, don't yet resolve! + BindableConfigBeanRegistry cbr = CONFIG_BEAN_REGISTRY.get(); + if (cbr != null) { + Optional configuredByQualifier = serviceInfo.qualifiers().stream() + .filter(q -> q.typeName().name().equals(ConfiguredBy.class.getName())) + .findFirst(); + assert (configuredByQualifier.isPresent()); + cbr.bind(this, configuredByQualifier.get(), metaConfigBeanInfo()); + } + } + } + + @Override + public void onPhaseEvent(Event event, + Phase phase) { + if (phase == Phase.POST_BIND_ALL_MODULES) { + assertIsInitializing(); + PicoServices picoServices = picoServices(); + assert (picoServices != null); + + if (Phase.INIT == currentActivationPhase()) { + LogEntryAndResult logEntryAndResult = createLogEntryAndResult(Phase.PENDING); + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.PENDING); + } + + // one of the configured services need to "tickle" the bean registry to initialize + BindableConfigBeanRegistry cbr = CONFIG_BEAN_REGISTRY.get(); + if (cbr != null) { + cbr.initialize(picoServices); + + // pre-initialize ourselves + if (isRootProvider()) { + // pre-activate our managed services + configBeanMap.forEach(this::preActivateManagedService); + } + } + } else if (phase == Phase.FINAL_RESOLVE) { + // post-initialize ourselves + if (isRootProvider()) { + if (drivesActivation()) { + ContextualServiceQuery query = DefaultContextualServiceQuery + .builder().serviceInfoCriteria(PicoServices.EMPTY_CRITERIA) + .build(); + maybeActivate(query); + } + } + + assertInitialized(true); + resolveConfigDrivenServices(); + } else if (phase == Phase.SERVICES_READY) { + assertIsInitialized(); + activateConfigDrivenServices(); + } + } + + @Override + // note: it is expected that the generated services override this method - which will override the getAnnotation() call. + public Class configBeanType() { + Class serviceType = serviceType(); + ConfiguredBy configuredBy = + Objects.requireNonNull(serviceType.getAnnotation(ConfiguredBy.class), String.valueOf(serviceType)); + return Objects.requireNonNull(configuredBy, String.valueOf(serviceType)).value(); + } + + @Override + public MetaConfigBeanInfo metaConfigBeanInfo() { + Map meta = configBeanAttributes().get(HelidonConfigResolver.TAG_META); + if (meta != null) { + ConfigBeanInfo cbi = (ConfigBeanInfo) meta.get(ConfigBeanInfo.class.getName()); + if (cbi != null) { + // normal path + return MetaConfigBeanInfo.toBuilder(cbi).build(); + } + + return ConfigBeanInfo.toMetaConfigBeanInfo(meta); + } + + LOGGER.log(System.Logger.Level.WARNING, "Unusual to find config bean without meta attributes: " + this); + Class configBeanType = configBeanType(); + ConfigBean configBean = + Objects.requireNonNull(configBeanType.getAnnotation(ConfigBean.class), String.valueOf(serviceType())); + return ConfigBeanInfo.toMetaConfigBeanInfo(configBean, configBeanType); + } + + @Override + public Map> configBeanAttributes() { + return Map.of(); + } + + // note that all responsibilities to resolve is delegated to the root provider + @Override + @SuppressWarnings("unchecked") + public Optional resolve(InjectionPointInfo ipInfo, + PicoServices picoServices, + ServiceProvider serviceProvider, + boolean resolveIps) { + if (resolveIps) { + assert (isRootProvider()); + // too early to resolve... + return Optional.empty(); + } + + ServiceInfoCriteria dep = ipInfo.dependencyToServiceInfo(); + DefaultServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(configBeanType().getName()) + .build(); + if (!dep.matchesContracts(criteria)) { + return Optional.empty(); + } + + // if we are here then we are asking for a config bean for ourselves, or a slave/managed instance + if (!dep.qualifiers().isEmpty()) { + throw new InjectionException("cannot use qualifiers while injecting config beans for self", this); + } + + if (isRootProvider()) { + return Optional.of(configBeanType()); + } + + return (Optional) configBean(); + } + + /** + * Here we are only looking for service providers, not service instances. What we need to do here is to determine + * whether to (a) include root providers, (b) include slave providers, or (c) include both. + *

        + * The result depends on the type of this provider instance. + * Here is the heuristic: + *

          + *
        • if this is a slave then simply use the standard matching behavior. + *

          + * If, however, we are the root provider then the additional heuristic is applied: + *

        • if the request mentions the {@link ConfiguredBy} qualifier w/ no value specified + * then the caller is only interested in the root provider. + *
        • if the request mentions the {@link ConfiguredBy} qualifier w/ a value specified + * then the caller is only interested in the slave providers. + *
        • if the request is completely empty then they are interested in everything - the root + * provider as well as the slave providers. + *
        • if there is no slaves under management then they must be interested in the root provider. + *
        • the fallback is to use standard matching using the criteria provided and only include the slaves. + *
        + * + * @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 matching service providers based upon the context and criteria provided + */ + @Override + public List> serviceProviders(ServiceInfoCriteria criteria, + boolean wantThis, + boolean thisAlreadyMatches) { + if (isRootProvider()) { + Set qualifiers = criteria.qualifiers(); + Optional configuredByQualifier = DefaultQualifierAndValue + .findFirst(EMPTY_CONFIGURED_BY.typeName().name(), qualifiers); + boolean hasValue = configuredByQualifier.isPresent() + && hasValue(configuredByQualifier.get().value().orElse(null)); + boolean blankCriteria = qualifiers.isEmpty() && isBlank(criteria); + boolean slavesQualify = !managedConfiguredServicesMap.isEmpty() + && (blankCriteria || hasValue || configuredByQualifier.isEmpty()); + boolean rootQualifies = wantThis + && (blankCriteria + || managedConfiguredServicesMap.isEmpty() + || (!hasValue && configuredByQualifier.isPresent())); + + if (slavesQualify) { + List> slaves = managedServiceProviders(criteria) + .entrySet().stream() + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + + if (rootQualifies) { + List> result = new ArrayList<>(); + if (thisAlreadyMatches || serviceInfo().matches(criteria)) { + result.add(this); + } + result.addAll(slaves); + // no need to sort using the comparator here since we should already be in the proper order... + return result; + } else { + return slaves; + } + } else if (rootQualifies + && (thisAlreadyMatches || serviceInfo().matches(criteria))) { + if (!hasValue && managedConfiguredServicesMap.isEmpty()) { + return List.of(new UnconfiguredServiceProvider<>(this)); + } + return List.of(this); + } + } else { // this is a slave instance ... + if (thisAlreadyMatches || serviceInfo().matches(criteria)) { + return List.of(this); + } + } + + return List.of(); + } + + @Override + public Map> managedServiceProviders(ServiceInfoCriteria criteria) { + if (!isRootProvider()) { + assert (managedConfiguredServicesMap.isEmpty()); + return Map.of(); + } + + Map> map = managedConfiguredServicesMap.entrySet().stream() + .filter(e -> e.getValue().isPresent()) + .filter(e -> e.getValue().get().serviceInfo().matches(criteria)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + if (map.size() <= 1) { + return map; + } + + Map> result = new TreeMap<>(configBeanComparator()); + result.putAll(map); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public Optional first(ContextualServiceQuery query) { + if (!isRootProvider()) { + Optional serviceOrProvider = maybeActivate(query); + return serviceOrProvider; + } + + // we are root provider + if (Phase.ACTIVE != currentActivationPhase()) { + LogEntryAndResult logEntryAndResult = createLogEntryAndResult(Phase.ACTIVE); + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVE); + } + + ServiceInfoCriteria criteria = query.serviceInfoCriteria(); + List> qualifiedProviders = serviceProviders(criteria, false, true); + for (ServiceProvider qualifiedProvider : qualifiedProviders) { + assert (this != qualifiedProvider); + Optional serviceOrProvider = qualifiedProvider.first(query); + if (serviceOrProvider.isPresent()) { + return (Optional) serviceOrProvider; + } + } + + if (query.expected()) { + throw expectedQualifiedServiceError(query); + } + + return Optional.empty(); + } + + @Override + @SuppressWarnings("unchecked") + public List list(ContextualServiceQuery query) { + if (!isRootProvider()) { + Optional serviceOrProvider = maybeActivate(query); + if (query.expected() && serviceOrProvider.isEmpty()) { + throw expectedQualifiedServiceError(query); + } + return serviceOrProvider.map(List::of).orElseGet(List::of); + } + + // we are root + Map> matching = managedServiceProviders(query.serviceInfoCriteria()); + if (!matching.isEmpty()) { + List result = matching.values().stream() + .map(it -> it.first(query)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (!result.isEmpty()) { + return (List) result; + } + } + + if (!query.expected()) { + return List.of(); + } + + throw expectedQualifiedServiceError(query); + } + + /** + * Configurable services by their very nature are not compile-time bindable during application creation. + * + * @return empty, signaling that we are not bindable + */ + @Override + public Optional> serviceProviderBindable() { + return Optional.empty(); + } + + @Override + @SuppressWarnings("unchecked") + public Optional toConfigBean(C config, + Class configBeanType) { + CB configBean = (CB) toConfigBean(config); + assert (configBeanType.isInstance(configBean)) : configBean; + return Optional.of(configBean); + } + + @Override + public abstract String toConfigBeanInstanceId(CB configBean); + + /** + * Brokers the set of the instance id for the given config bean. + * + * @param configBean the config bean to set + * @param val the instance id to associate it with + */ + public abstract void configBeanInstanceId(CB configBean, + String val); + + /** + * The backing config of this configured service instance. + * + * @return the backing config of this configured service instance + */ + protected abstract Optional rawConfig(); + + /** + * Creates a new instance of this type of configured service provider, along with the configuration bean + * associated with the service. + * + * @param configBean the config bean + * @return the created instance injected with the provided config bean + */ + protected abstract AbstractConfiguredServiceProvider createInstance(Object configBean); + + /** + * After the gathering dependency phase, we will short circuit directly to the finish line. + */ + @Override + protected void doConstructing(LogEntryAndResult logEntryAndResult) { + if (isRootProvider()) { + boolean shouldBeActive = (drivesActivation() && !managedConfiguredServicesMap.isEmpty()); + Phase setPhase = (shouldBeActive) ? Phase.ACTIVE : Phase.PENDING; + startTransitionCurrentActivationPhase(logEntryAndResult, setPhase); + onFinished(logEntryAndResult); + return; + } + + super.doConstructing(logEntryAndResult); + } + + @Override + protected String identitySuffix() { + if (isRootProvider()) { + return "{root}"; + } + + Optional configBean = configBean(); + String instanceId = toConfigBeanInstanceId(configBean.orElse(null)); + return "{" + instanceId + "}"; + } + + @Override + protected void serviceInfo(ServiceInfo serviceInfo) { + // this might appear strange, but since activators can inherit from one another this is in place to trigger + // only when the most derived activator ctor is setting its serviceInfo. + boolean isThisOurServiceInfo = serviceType().getName().equals(serviceInfo.serviceTypeName()); + if (isThisOurServiceInfo) { + assertIsInitializing(); + assertIsRootProvider(true, false); + + // override our service info to account for any named lookup + if (isRootProvider() && !serviceInfo.qualifiers().contains(CommonQualifiers.WILDCARD_NAMED)) { + serviceInfo = DefaultServiceInfo.toBuilder(serviceInfo) + .addQualifier(CommonQualifiers.WILDCARD_NAMED) + .build(); + } + } + + super.serviceInfo(serviceInfo); + } + + @Override + protected System.Logger logger() { + return LOGGER; + } + + @Override + protected void doPreDestroying(LogEntryAndResult logEntryAndResult) { + if (isRootProvider()) { + managedConfiguredServicesMap.values().stream() + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(csp -> { + LogEntryAndResult cspLogEntryAndResult = csp.createLogEntryAndResult(Phase.DESTROYED); + csp.doPreDestroying(cspLogEntryAndResult); + }); + } + super.doPreDestroying(logEntryAndResult); + } + + @Override + protected void doDestroying(LogEntryAndResult logEntryAndResult) { + super.doDestroying(logEntryAndResult); + } + + @Override + protected void onFinalShutdown() { + if (isRootProvider()) { + managedConfiguredServicesMap.values().stream() + .filter(Optional::isPresent) + .map(Optional::get) + .filter(csp -> csp.currentActivationPhase().eligibleForDeactivation()) + .forEach(AbstractConfiguredServiceProvider::onFinalShutdown); + } + + this.initialized.set(false); + this.managedConfiguredServicesMap.clear(); + this.configBeanMap.clear(); + + super.onFinalShutdown(); + } + + /** + * Maybe transition into being a root provider if we are the first to claim it. Otherwise, we are a slave being managed. + * + * @param isRootProvider true if an asserting is being made to claim root or claim managed slave + * @param expectSet true if this is a strong assertion, and if not claimed an exception will be thrown + */ + // special note: this is referred to in code generated code! + protected void assertIsRootProvider(boolean isRootProvider, + boolean expectSet) { + boolean set = this.isRootProvider.compareAndSet(null, isRootProvider); + if (!set && expectSet) { + throw new PicoServiceProviderException(description() + " was already initialized", null, this); + } + assert (!isRootProvider || rootProvider.get() == null); + } + + /** + * Return true if this service is driven to activation during startup (and provided it has some config). + * See {@link io.helidon.pico.configdriven.ConfiguredBy#drivesActivation()} and + * see {@link io.helidon.builder.config.ConfigBean#drivesActivation()} for more. + * + * @return true if this service is driven to activation during startup + */ + // note: overridden by the service if disabled at the ConfiguredBy service level + protected boolean drivesActivation() { + return metaConfigBeanInfo().drivesActivation(); + } + + /** + * Called to accept the new config bean instance initialized from the appropriate configuration tree location. + * + * @param config the configuration + * @return the new config bean + */ + // expected that the generated configured service overrides this to set its new config bean value + protected CB acceptConfig(Config config) { + return Objects.requireNonNull(toConfigBean(config)); + } + + /** + * Transition into an initialized state. + */ + void assertInitialized(boolean initialized) { + assertIsInitializing(); + assert (!drivesActivation() + || isAlreadyAtTargetPhase(PicoServices.terminalActivationPhase()) + || managedConfiguredServicesMap.isEmpty()); + this.initialized.set(initialized); + } + + void assertIsInitializing() { + if (initialized.get()) { + CallingContext callingContext = initializationCallingContext.get(); + String desc = description() + " was previously initialized"; + String msg = (callingContext == null) ? toErrorMessage(desc) : toErrorMessage(callingContext, desc); + throw new PicoServiceProviderException(msg, this); + } + } + + void assertIsInitialized() { + if (!initialized.get()) { + throw new PicoServiceProviderException(description() + " was expected to be initialized", this); + } + } + + void resolveConfigDrivenServices() { + assertIsInitialized(); + assert (isRootProvider()); + assert (managedConfiguredServicesMap.size() == configBeanMap.size()) : description(); + + if (managedConfiguredServicesMap.isEmpty()) { + if (logger().isLoggable(System.Logger.Level.DEBUG)) { + logger().log(System.Logger.Level.DEBUG, "no configured services for: " + description()); + } + return; + } + + // accept and resolve config + managedConfiguredServicesMap.values().forEach(opt -> { + assert (opt.isPresent()); + + AbstractConfiguredServiceProvider csp = opt.get(); + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Resolving config for " + csp); + } + + LogEntryAndResult logEntryAndResult = createLogEntryAndResult(Phase.PENDING); + try { + csp.startTransitionCurrentActivationPhase(logEntryAndResult, Phase.PENDING); + io.helidon.common.config.Config commonConfig = PicoServices.realizedGlobalBootStrap().config() + .orElseThrow(this::expectedConfigurationSetGlobally); + csp.acceptConfig(commonConfig); + } catch (Throwable t) { + csp.onFailedFinish(logEntryAndResult, t, true); + } + }); + } + + private PicoException expectedConfigurationSetGlobally() { + return new PicoException("expected to have configuration set globally - see PicoServices.globalBootstrap()"); + } + + private void activateConfigDrivenServices() { + assertIsInitialized(); + assert (isRootProvider()); + assert (managedConfiguredServicesMap.size() == configBeanMap.size()) : description(); + + if (configBeanMap.isEmpty()) { + return; + } + + if (!drivesActivation()) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "drivesActivation disabled for: " + description()); + } + return; + } + + configBeanMap.forEach(this::activateManagedService); + } + + private AbstractConfiguredServiceProvider activateManagedService(String instanceId, + Object configBean) { + return managedConfiguredServicesMap.compute(instanceId, (id, existing) -> { + if (existing == null || existing.isEmpty()) { + existing = innerPreActivateManagedService(instanceId, configBean); + } + + AbstractConfiguredServiceProvider csp = existing.orElseThrow(); + if (Phase.ACTIVE != csp.currentActivationPhase()) { + csp.innerActivate(); + } + return existing; + }).get(); + } + + private void innerActivate() { + // this may go into a wait state if other threads are trying to also initialize at the same time - expected behavior + ContextualServiceQuery query = DefaultContextualServiceQuery + .builder().serviceInfoCriteria(PicoServices.EMPTY_CRITERIA) + .build(); + Optional service = maybeActivate(query); // triggers the post-construct + if (service.isPresent() && LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "finished activating: " + service); + } + } + + private AbstractConfiguredServiceProvider preActivateManagedService(String instanceId, + Object configBean) { + return managedConfiguredServicesMap.compute(instanceId, (id, existing) -> { + if (existing != null && existing.isPresent()) { + return existing; + } + return innerPreActivateManagedService(instanceId, configBean); + }).orElseThrow(); + } + + private Optional> innerPreActivateManagedService(String instanceId, + Object configBean) { + Objects.requireNonNull(instanceId); + Objects.requireNonNull(configBean); + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "creating: " + serviceType() + " with config instance id: " + instanceId); + } + + AbstractConfiguredServiceProvider instance = createInstance(configBean); + assert (instance != this); + + DefaultServiceInfo newServiceInfo = DefaultServiceInfo.toBuilder(instance.serviceInfo()) + .addQualifier(DefaultQualifierAndValue.createNamed(instanceId)) + .build(); + + // override our service info + instance.serviceInfo(newServiceInfo); + instance.picoServices(Optional.of(picoServices())); + instance.rootProvider(this); + + if (logger().isLoggable(System.Logger.Level.DEBUG)) { + logger().log(System.Logger.Level.DEBUG, "config instance successfully initialized: " + + id() + ":" + newServiceInfo.qualifiers()); + } + + return Optional.of(instance); + } + + /** + * See {@link #configBeanComparator()}. + */ + static class CBInstanceComparator implements Comparator, Serializable { + @Override + public int compare(String str1, + String str2) { + if (DefaultPicoConfigBeanRegistry.DEFAULT_INSTANCE_ID.equals(str1)) { + return -1 * Integer.MAX_VALUE; + } else if (DefaultPicoConfigBeanRegistry.DEFAULT_INSTANCE_ID.equals(str2)) { + return Integer.MAX_VALUE; + } + return str1.compareTo(str2); + } + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/BindableConfigBeanRegistry.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/BindableConfigBeanRegistry.java new file mode 100644 index 00000000000..6153e071824 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/BindableConfigBeanRegistry.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.pico.PicoServices; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.Resettable; + +/** + * Provides bindable extensions to the {@link ConfigBeanRegistry}. Typically needed for internal processing only, and not expected + * to be called in normal use case scenarios. + */ +public interface BindableConfigBeanRegistry extends ConfigBeanRegistry, Resettable { + + /** + * Binds a {@link ConfiguredServiceProvider} to the + * {@link io.helidon.builder.config.spi.MetaConfigBeanInfo} annotation it is configured by. + * + * @param configuredServiceProvider the configured service provider + * @param configuredByQualifier the qualifier associated with the {@link io.helidon.builder.config.spi.ConfigBeanInfo} + * @param metaConfigBeanInfo the meta config bean info associated with this service provider + */ + void bind(ConfiguredServiceProvider configuredServiceProvider, + QualifierAndValue configuredByQualifier, + MetaConfigBeanInfo metaConfigBeanInfo); + + /** + * The first call to this initialize the bean registry, by loading all the backing configuration from the config + * subsystem. + * + * @param picoServices the pico services instance + */ + void initialize(PicoServices picoServices); + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigBeanRegistry.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigBeanRegistry.java new file mode 100644 index 00000000000..e916941c53f --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigBeanRegistry.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.HelidonConfigBeanRegistry; + +/** + * The highest ranked/weighted implementor of this contract is responsible for managing the set of + * {@link io.helidon.builder.config.ConfigBean}'s that are active, along with whether the application is configured to + * support dynamic aspects (i.e., dynamic in content, dynamic in lifecycle, etc.). + */ +public interface ConfigBeanRegistry extends HelidonConfigBeanRegistry { + + /** + * The config bean registry is initialized as part of Pico's initialization, which happens when the service registry + * is initialized and bound. + * + * @return true if the config bean registry has been initialized + */ + boolean ready(); + + /** + * These are the services that are configurable, mapping to the configuration beans each expects. + * Each entry in the returned map is the master/root for the config beans it manages. The result, therefore, is + * not associated with config beans. Use {@link #configuredServiceProviders()} for configured service instances. + * + * @return the map of configurable services to the meta config beans each expects + */ + Map, ConfigBeanInfo> configurableServiceProviders(); + + /** + * These are the managed/slave service providers that are associated with config bean instances. + * + * @return the list of configured services + */ + List> configuredServiceProviders(); + + /** + * These are the managed/slave service providers that are associated with config bean instances with the config {@code key} + * provided. + * + * @param key the config options key - note that this is a partial key - and not relative to the parent - the same + * key used by {@link io.helidon.builder.config.ConfigBean#value()}. + * @return the list of configured services + */ + List> configuredServiceProvidersConfiguredBy(String key); + + /** + * Returns all the known config beans in order of rank given the {@code key}. Callers should understand + * that this list might be incomplete until ready state is reached (see {@link #ready()}). Note also that callers should + * attempt to use {@link #configBeansByConfigKey(String)} whenever possible since it will generate more precise matches. + * + * @param key the config options key - note that this is a partial key - and not relative to the parent - the same + * key used by {@link io.helidon.builder.config.ConfigBean#value()}. + * @return the set of known config keys + */ + Set configBeansByConfigKey(String key); + + /** + * Returns all the known config beans in order of rank matching the {@code key} and {@code fullConfigKey}. Callers should + * understand that this list might be incomplete until ready state is reached (see {@link #ready()}). + * + * @param key the config options key - note that this is a partial key - and not relative to the parent - the same + * key used by {@link io.helidon.builder.config.ConfigBean#value()}. + * @param fullConfigKey the full config key + * @return the set of known config keys matching the provided criteria + */ + Set configBeansByConfigKey(String key, + String fullConfigKey); + + /** + * Similar to {@link #configBeansByConfigKey}, but instead returns all the known config beans in a + * map where the key of the map is the config key. + * + * @param key the config options key - note that this is a partial key - and not relative to the parent - the same + * key used by {@link io.helidon.builder.config.ConfigBean#value()}. + * @param fullConfigKey the full config key + * @return the map of known config keys to config beans + */ + Map configBeanMapByConfigKey(String key, + String fullConfigKey); + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigDrivenUtils.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigDrivenUtils.java new file mode 100644 index 00000000000..19846500397 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfigDrivenUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.Optional; + +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.config.Config; +import io.helidon.pico.ServiceInfoCriteria; + +final class ConfigDrivenUtils { + + private ConfigDrivenUtils() { + } + + static boolean isBlank(ServiceInfoCriteria criteria) { + assert (criteria.externalContractsImplemented().isEmpty()); + return criteria.serviceTypeName().isEmpty() + && criteria.contractsImplemented().isEmpty() + && criteria.qualifiers().isEmpty(); + } + + static boolean hasValue(String val) { + return (val != null) && !val.isBlank(); + } + + static String validatedConfigKey(String configKey) { + if (!hasValue(configKey)) { + throw new IllegalStateException("key was expected to be non-blank"); + } + return configKey; + } + + static String validatedConfigKey(MetaConfigBeanInfo metaConfigBeanInfo) { + return validatedConfigKey(metaConfigBeanInfo.value()); + } + + static Config safeDowncastOf(io.helidon.common.config.Config config) { + if (!(config instanceof Config)) { + throw new IllegalStateException(config.getClass() + " is not supported - the only type supported is " + Config.class); + } + + return (Config) config; + } + + static Optional toNumeric(String val) { + if (val == null) { + return Optional.empty(); + } + + try { + return Optional.of(Integer.parseInt(val)); + } catch (Exception e) { + return Optional.empty(); + } + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfiguredServiceProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfiguredServiceProvider.java new file mode 100644 index 00000000000..8f2c3dc0dcb --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/ConfiguredServiceProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.AttributeVisitor; +import io.helidon.builder.config.spi.ConfigBeanMapper; +import io.helidon.builder.config.spi.GeneratedConfigBeanBuilderBase; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.pico.ServiceProvider; + +/** + * An extension to {@link io.helidon.pico.ServiceProvider} that represents a config-driven service. + * + * @param the type of this service provider manages + * @param the type of config beans that this service is configured by + */ +public interface ConfiguredServiceProvider extends ServiceProvider, ConfigBeanMapper { + + /** + * The type of the service being managed. + * + * @return the service type being managed + */ + Class serviceType(); + + /** + * The {@link io.helidon.builder.config.ConfigBean} type that is used to configure this provider. + * + * @return the {@code ConfigBean} type that is used to configure this provider + */ + Class configBeanType(); + + /** + * The meta config bean information associated with this service provider's {@link io.helidon.builder.config.ConfigBean}. + * + * @return the {@code MetaConfigBeanInfo} for this config bean + */ + MetaConfigBeanInfo metaConfigBeanInfo(); + + /** + * The config bean attributes for our {@link io.helidon.builder.config.ConfigBean}. + * Generally this method is for internal use only. Most should use {@link #visitAttributes} instead of this method. + * + * @return the config bean attributes + */ + Map> configBeanAttributes(); + + /** + * Builds a config bean instance using the configuration. + * + * @param config the backing configuration + * @return the generated config bean instance + */ + CB toConfigBean(io.helidon.common.config.Config config); + + /** + * Similar to {@link #toConfigBean(io.helidon.common.config.Config)}, but instead this method builds a config bean builder + * instance using the configuration. + * + * @param config the backing configuration + * @return the generated config bean instance + */ + GeneratedConfigBeanBuilderBase toConfigBeanBuilder(io.helidon.common.config.Config config); + + /** + * Visit the attributes of the config bean, calling the visitor for each attribute in the hierarchy. + * + * @param configBean the config bean to visit + * @param visitor the attribute visitor + * @param userDefinedContext the optional user define context + * @param the type of the user defined context + */ + void visitAttributes(CB configBean, + AttributeVisitor visitor, + R userDefinedContext); + + /** + * Gets the internal config bean instance id for the provided config bean. + * + * @param configBean the config bean + * @return the config bean instance id + */ + String toConfigBeanInstanceId(CB configBean); + + /** + * Returns the config bean associated with this managed service provider. + * + * @return the config bean associated with this managed service provider + */ + Optional configBean(); + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanBuilderValidatorProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanBuilderValidatorProvider.java new file mode 100644 index 00000000000..7a294d5422c --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanBuilderValidatorProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import io.helidon.builder.config.spi.ConfigBeanBuilderValidator; +import io.helidon.builder.config.spi.ConfigBeanBuilderValidatorProvider; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +/** + * Service-loaded provider for {@link io.helidon.builder.config.spi.ConfigResolverProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultConfigBeanBuilderValidatorProvider implements ConfigBeanBuilderValidatorProvider { + static final LazyValue> INSTANCE = LazyValue.create(DefaultConfigBuilderValidator::new); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultConfigBeanBuilderValidatorProvider() { + } + + @Override + public ConfigBeanBuilderValidator configBeanBuilderValidator() { + return INSTANCE.get(); + } +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanRegistryProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanRegistryProvider.java new file mode 100644 index 00000000000..dab78df326c --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBeanRegistryProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import io.helidon.builder.config.spi.ConfigBeanRegistryProvider; +import io.helidon.builder.config.spi.HelidonConfigBeanRegistry; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +/** + * Service-loaded provider for {@link ConfigBeanRegistry}. + */ +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultConfigBeanRegistryProvider implements ConfigBeanRegistryProvider { + static final LazyValue INSTANCE = LazyValue.create(DefaultPicoConfigBeanRegistry::new); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultConfigBeanRegistryProvider() { + } + + @Override + public HelidonConfigBeanRegistry configBeanRegistry() { + return INSTANCE.get(); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBuilderValidator.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBuilderValidator.java new file mode 100644 index 00000000000..c2969489290 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigBuilderValidator.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.config.spi.ConfigBeanBuilderValidator; +import io.helidon.pico.PicoException; + +/** + * The default implementation for {@link ConfigBeanBuilderValidator}. + */ +class DefaultConfigBuilderValidator implements ConfigBeanBuilderValidator { + + DefaultConfigBuilderValidator() { + } + + /** + * Creates a validation round for all the config bean attributes of the provided config bean. + * + * @param builder the config builder instance + * @param configBeanBuilderType the config bean type + * @return the validation round that can be used for attribute level validation + */ + @Override + public ValidationRound createValidationRound(CBB builder, + Class configBeanBuilderType) { + assert (builder != null); + assert (configBeanBuilderType != null); + + return new DefaultValidation(configBeanBuilderType); + } + + static class DefaultValidation implements ValidationRound { + private final Class configBeanType; + private final List issues = new ArrayList<>(); + private boolean finished; + + DefaultValidation(Class configBeanType) { + this.configBeanType = Objects.requireNonNull(configBeanType); + } + + @Override + public List issues() { + return List.copyOf(issues); + } + + @Override + public boolean isCompleted() { + return finished; + } + + @Override + public ValidationRound validate(String attributeName, + Supplier valueSupplier, + Class cbType, + Map meta) { + if (cbType.isPrimitive() || cbType.getName().startsWith("java.lang.")) { + return this; + } + + Object val = valueSupplier.get(); + if (val == null) { + return this; + } + + return this; + } + + @SuppressWarnings("rawtypes") + Collection extractValues(Object rawVal, + Class cbType) { + if (rawVal == null) { + return List.of(); + } + + if (Optional.class.equals(rawVal.getClass())) { + Optional val = (Optional) rawVal; + if (val.isEmpty()) { + return List.of(); + } + + rawVal = val.get(); + } + + if (rawVal instanceof Collection) { + return (Collection) rawVal; + } + + if (rawVal instanceof Map) { + return extractValues(((Map) rawVal).values(), null); + } + + if (cbType != null && cbType.isArray()) { + if (cbType.getComponentType() != null && !cbType.getComponentType().isPrimitive()) { + return List.of((Object[]) rawVal); + } + } + + return List.of(rawVal); + } + + @Override + public ValidationRound finish(boolean throwIfErrors) { + assert (!finished) : "already finished"; + finished = true; + + logIssues(); + if (throwIfErrors && hasErrors()) { + throw new PicoException("Validation failed for config bean of type " + + configBeanType.getName() + ":\n" + toDescription(issues)); + } + return this; + } + + void logIssues() { + if (!hasIssues()) { + return; + } + + System.Logger logger = System.getLogger(DefaultConfigBuilderValidator.class.getName()); + issues.forEach(issue -> logger.log(issue.severity() == Severity.ERROR + ? System.Logger.Level.ERROR : System.Logger.Level.WARNING, issue)); + } + + static String toDescription(List issues) { + StringBuilder builder = new StringBuilder(); + issues.forEach(issue -> { + builder.append("* ").append(issue.severity()).append(": ").append(issue.attributeName()); + builder.append(": ").append(issue.message()); + }); + + return builder.toString(); + } + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolver.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolver.java new file mode 100644 index 00000000000..96c4e852cab --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolver.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.builder.config.spi.ConfigResolverMapRequest; +import io.helidon.builder.config.spi.ConfigResolverRequest; +import io.helidon.builder.config.spi.HelidonConfigResolver; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.builder.config.spi.ResolutionContext; +import io.helidon.builder.config.spi.StringValueParser; +import io.helidon.builder.config.spi.StringValueParserHolder; +import io.helidon.config.Config; +import io.helidon.config.ConfigException; +import io.helidon.config.metadata.ConfiguredOption; + +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.hasValue; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.safeDowncastOf; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.validatedConfigKey; + +/** + * Handles "full" config system presence. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +class DefaultConfigResolver extends HelidonConfigResolver { + + DefaultConfigResolver() { + } + + /** + * Extracts the component type from the meta attributes provided for a particular bean attribute name. + * + * @param request the request + * @param meta the meta attributes + * @param the attribute value type being resolved in the request + * @return the component type + */ + static Optional> toComponentType(Map> meta, + ConfigResolverRequest request) { + Map thisMeta = meta.get(request.attributeName()); + Class componentType = request.valueComponentType().orElse(null); + componentType = (Class) (componentType == null && thisMeta != null ? thisMeta.get(TAG_COMPONENT_TYPE) : null); + return Optional.ofNullable(componentType); + } + + static Optional validatedDefaults(Map meta, + String attrName, + Class type, + Class componentType) { + // check the default values... + String defaultVal = (String) meta.get("value"); + if (defaultVal != null && defaultVal.equals(ConfiguredOption.UNCONFIGURED)) { + defaultVal = null; + } + Optional result = parse(defaultVal, attrName, type, componentType); + if (result.isPresent()) { + return result; + } + + // check to see if we are in policy violation... + String requiredStr = (String) meta.get("required"); + boolean required = Boolean.parseBoolean(requiredStr); + if (required) { + throw new IllegalStateException("'" + attrName + "' is a required attribute and cannot be null"); + } + + return Optional.empty(); + } + + static Class validatedTypeForConfigBeanRegistry(String attrName, + Class type, + Class cbType) { + if (type.isArray()) { + type = (Class) type.getComponentType(); + if (type == null) { + throw new ConfigException("? is not supported for " + cbType + "." + attrName); + } + } + + if (type.isPrimitive() || type.getName().startsWith("java.lang.")) { + return null; + } + + return type; + } + + static List findInConfigBeanRegistryAsList(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Class type = validatedTypeForConfigBeanRegistry(request.attributeName(), + request.valueType(), + request.valueComponentType().orElse(null)); + if (type == null) { + return List.of(); + } + + DefaultPicoConfigBeanRegistry cbr = (DefaultPicoConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry() + .orElseThrow(); + String fullConfigKey = fullConfigKeyOf(safeDowncastOf(ctx.config()), request.configKey(), meta); + Set result = cbr.configBeansByConfigKey(request.configKey(), fullConfigKey); + return new ArrayList<>(result); + } + + static Map findInConfigBeanRegistryAsMap(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Class type = validatedTypeForConfigBeanRegistry(request.attributeName(), request.valueType(), ctx.configBeanType()); + if (type == null) { + return Map.of(); + } + + DefaultPicoConfigBeanRegistry cbr = (DefaultPicoConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry() + .orElseThrow(); + String fullConfigKey = fullConfigKeyOf(safeDowncastOf(ctx.config()), request.configKey(), meta); + Map result = cbr.configBeanMapByConfigKey(request.configKey(), fullConfigKey); + return Objects.requireNonNull((Map) result); + } + + static Optional parse(String strValueToParse, + String attrName, + Class type, + Class componentType) { + if (strValueToParse == null) { + return Optional.empty(); + } + + if (type.isAssignableFrom(strValueToParse.getClass())) { + return Optional.of(strValueToParse); + } + + StringValueParser provider = StringValueParserHolder.stringValueParser().orElseThrow(); + if (componentType != null) { + // best effort here + try { + Object val = provider.parse(strValueToParse, componentType); + return Optional.ofNullable(val); + } catch (Exception e) { + if (Optional.class != type) { + throw new UnsupportedOperationException("Only Optional<> is currently supported: " + attrName); + } + } + } + + return provider.parse(strValueToParse, type); + } + + static Optional optionalWrappedBean(Object configBean, + String attrName, + Class type, + Class componentType) { + if (type.isInstance(Objects.requireNonNull(configBean)) + || ( + Optional.class.equals(type) + && (componentType != null) && componentType.isInstance(configBean))) { + if (Optional.class.equals(type)) { + return Optional.of((T) Optional.of(configBean)); + } + + return Optional.of((T) configBean); + } + + throw new UnsupportedOperationException("Cannot convert to type " + componentType + ": " + attrName); + } + + static Optional> optionalWrappedBeans(List configBeans, + String ignoredAttrName, + Class ignoredType, + Class componentType) { + assert (configBeans != null && !configBeans.isEmpty()); + + configBeans.forEach(configBean -> { + assert (componentType.isInstance(configBean)); + }); + + return Optional.of((Collection) configBeans); + } + + static Optional> optionalWrappedBeans(Map configBeans, + String attrName, + Class keyType, + Class ignoredKeyComponentType, + Class type, + Class componentType) { + assert (configBeans != null && !configBeans.isEmpty() && (type != null) && componentType != null); + if (keyType != null && String.class != keyType) { + throw new UnsupportedOperationException("Only Map with key of String is currently supported: " + attrName); + } + + configBeans.forEach((key, value) -> { + assert (componentType.isInstance(value)); + }); + + return Optional.of(configBeans); + } + + static String fullConfigKeyOf(Config config, + String configKey, + Map> metaAttributes) { + assert (hasValue(configKey)); + String parentKey; + if (config != null) { + parentKey = config.key().toString(); + } else { + parentKey = Objects.requireNonNull(configKeyOf(metaAttributes)); + } + return parentKey + "." + configKey; + } + + static MetaConfigBeanInfo configBeanInfoOf(Map> metaAttributes) { + Map meta = metaAttributes.get(TAG_META); + if (meta == null) { + return null; + } + + return (MetaConfigBeanInfo) meta.get(ConfigBeanInfo.class.getName()); + } + + static String configKeyOf(Map> metaAttributes) { + MetaConfigBeanInfo cbi = configBeanInfoOf(metaAttributes); + return (null == cbi) ? null : validatedConfigKey(cbi); + } + + @Override + public Optional of(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Objects.requireNonNull(ctx); + Objects.requireNonNull(request); + Objects.requireNonNull(meta); + Class componentType = toComponentType(meta, request).orElse(null); + + // check the config bean registry to see if we know of anything by this key + List matches = findInConfigBeanRegistryAsList(ctx, meta, request); + if (!matches.isEmpty()) { + return optionalWrappedBean(matches.get(0), request.attributeName(), request.valueType(), componentType); + } + + // check the config sub system + Optional result = super.of(ctx, meta, request); + if (result.isPresent()) { + // assume that the config sub-system did the validation + return result; + } + + Map thisMeta = meta.get(request.attributeName()); + return validatedDefaults(thisMeta, request.attributeName(), request.valueType(), componentType); + } + + @Override + public Optional> ofCollection(ResolutionContext ctx, + Map> meta, + ConfigResolverRequest request) { + Objects.requireNonNull(ctx); + Objects.requireNonNull(request); + Objects.requireNonNull(meta); + Class componentType = toComponentType(meta, request).orElse(null); + + // check the config bean registry to see if we know of anything by this key + List matches = findInConfigBeanRegistryAsList(ctx, meta, request); + if (!matches.isEmpty()) { + return optionalWrappedBeans(matches, request.attributeName(), request.valueType(), componentType); + } + + // check the config sub system + Optional> result = super.ofCollection(ctx, meta, request); + if (result.isPresent()) { + // assume that the config sub-system did the validation + return result; + } + + Map thisMeta = meta.get(request.attributeName()); + return validatedDefaults(thisMeta, request.attributeName(), request.valueType(), componentType); + } + + @Override + public Optional> ofMap(ResolutionContext ctx, + Map> meta, + ConfigResolverMapRequest request) { + Objects.requireNonNull(ctx); + Objects.requireNonNull(request); + Objects.requireNonNull(meta); + Class componentType = toComponentType(meta, request).orElse(null); + + // check the config bean registry to see if we know of anything by this key + Map matches = findInConfigBeanRegistryAsMap(ctx, meta, request); + if (!matches.isEmpty()) { + return (Optional>) (Optional) optionalWrappedBeans(matches, + request.attributeName(), + request.keyType(), + request.keyComponentType().orElse(null), + request.valueType(), + request.valueComponentType().orElse(null)); + } + + // check the config sub system + Optional> result = super.ofMap(ctx, meta, request); + if (result.isPresent()) { + // assume that the config sub-system did the validation + return result; + } + + Map thisMeta = meta.get(request.attributeName()); + return validatedDefaults(thisMeta, request.attributeName(), request.valueType(), componentType); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolverProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolverProvider.java new file mode 100644 index 00000000000..61e31ba8e7f --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultConfigResolverProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import io.helidon.builder.config.spi.ConfigResolver; +import io.helidon.builder.config.spi.ConfigResolverProvider; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +/** + * Service-loaded provider for {@link ConfigResolverProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultConfigResolverProvider implements ConfigResolverProvider { + static final LazyValue INSTANCE = LazyValue.create(DefaultConfigResolver::new); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultConfigResolverProvider() { + } + + @Override + public ConfigResolver configResolver() { + return INSTANCE.get(); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultPicoConfigBeanRegistry.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultPicoConfigBeanRegistry.java new file mode 100644 index 00000000000..2ee40aa703e --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultPicoConfigBeanRegistry.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.ArrayList; +import java.util.Collection; +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.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import io.helidon.builder.AttributeVisitor; +import io.helidon.builder.config.ConfigBean; +import io.helidon.builder.config.spi.ConfigBeanInfo; +import io.helidon.builder.config.spi.ConfigProvider; +import io.helidon.builder.config.spi.MetaConfigBeanInfo; +import io.helidon.common.config.Config; +import io.helidon.common.config.ConfigException; +import io.helidon.config.ConfigValue; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServiceProviderException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.services.ServiceProviderComparator; + +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.hasValue; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.safeDowncastOf; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.toNumeric; +import static io.helidon.pico.configdriven.services.ConfigDrivenUtils.validatedConfigKey; + +/** + * The default implementation for {@link ConfigBeanRegistry}. + */ +@SuppressWarnings("unchecked") +class DefaultPicoConfigBeanRegistry implements BindableConfigBeanRegistry { + /** + * The default config bean instance id. + */ + static final String DEFAULT_INSTANCE_ID = "@default"; + + private static final System.Logger LOGGER = System.getLogger(DefaultPicoConfigBeanRegistry.class.getName()); + + private static final boolean FORCE_VALIDATE_USING_BEAN_ATTRIBUTES = false; + private static final boolean FORCE_VALIDATE_USING_CONFIG_ATTRIBUTES = true; + + private final AtomicBoolean initializing = new AtomicBoolean(); + private final Map, ConfigBeanInfo> configuredServiceProviderMetaConfigBeanMap = + new ConcurrentHashMap<>(); + private final Map>> configuredServiceProvidersByConfigKey = + new ConcurrentHashMap<>(); + private CountDownLatch initialized = new CountDownLatch(1); + + DefaultPicoConfigBeanRegistry() { + } + + static boolean validateUsingConfigAttributes(String instanceId, + String attrName, + String attrConfigKey, + Config config, + Supplier beanBasedValueSupplier, + Set problems) { + if (config == null) { + if (!DEFAULT_INSTANCE_ID.equals(instanceId)) { + problems.add("Unable to obtain backing config for service provider for " + attrConfigKey); + } + + return false; + } else { + Config attrConfig = config.get(attrConfigKey); + if (attrConfig.exists()) { + return true; + } + + // if we have a default value from our bean, then that is the fallback verification + Object val = beanBasedValueSupplier.get(); + if (val == null) { + problems.add("'" + attrConfigKey + "' is a required configuration for attribute '" + attrName + "'"); + return true; + } + + // full through to bean validation next, just for any added checks we might do there + return false; + } + } + + static void validateUsingBeanAttributes(Supplier valueSupplier, + String attrName, + Set problems) { + Object val = valueSupplier.get(); + if (val == null) { + problems.add("'" + attrName + "' is a required attribute and cannot be null"); + } else { + if (!(val instanceof String)) { + val = val.toString(); + } + if (!hasValue((String) val)) { + problems.add("'" + attrName + "' is a required attribute and cannot be blank"); + } + } + } + + @Override + public boolean reset(boolean deep) { + System.Logger.Level level = isInitialized() ? System.Logger.Level.INFO : System.Logger.Level.DEBUG; + LOGGER.log(level, "Resetting"); + configuredServiceProviderMetaConfigBeanMap.clear(); + configuredServiceProvidersByConfigKey.clear(); + initializing.set(false); + initialized = new CountDownLatch(1); + return true; + } + + @Override + public void bind(ConfiguredServiceProvider configuredServiceProvider, + QualifierAndValue configuredByQualifier, + MetaConfigBeanInfo metaConfigBeanInfo) { + Objects.requireNonNull(configuredServiceProvider); + Objects.requireNonNull(configuredByQualifier); + Objects.requireNonNull(metaConfigBeanInfo); + + if (initializing.get()) { + throw new ConfigException("unable to bind config post initialization: " + configuredServiceProvider.description()); + } + + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Binding " + configuredServiceProvider.serviceType() + + " with " + configuredByQualifier.value()); + } + + Object prev = configuredServiceProviderMetaConfigBeanMap.put(configuredServiceProvider, metaConfigBeanInfo); + assert (prev == null) : "duplicate service provider initialization occurred"; + + String configKey = validatedConfigKey(metaConfigBeanInfo); + Class cspType = Objects.requireNonNull(configuredServiceProvider.serviceType()); + configuredServiceProvidersByConfigKey.compute(configKey, (k, cspList) -> { + if (cspList == null) { + cspList = new ArrayList<>(); + } + Optional> prevCsp = cspList.stream() + .filter(it -> cspType.equals(it.configBeanType())) + .findAny(); + assert (prevCsp.isEmpty()) : "duplicate service provider initialization occurred"; + + boolean added = cspList.add(configuredServiceProvider); + assert (added); + + return cspList; + }); + } + + @Override + public void initialize(PicoServices ignoredPicoServices) { + try { + if (initializing.getAndSet(true)) { + // all threads should wait for the leader (and the config bean registry) to have been fully initialized + initialized.await(); + return; + } + + Config config = PicoServices.realizedGlobalBootStrap().config().orElse(null); + if (config == null) { + LOGGER.log(System.Logger.Level.WARNING, + "unable to initialize - no config to read - be sure to provide or initialize " + + ConfigProvider.class.getName() + " prior to service activation."); + reset(true); + return; + } + + LOGGER.log(System.Logger.Level.DEBUG, "Initializing"); + initialize(config); + // we are now ready and initialized + initialized.countDown(); + } catch (Throwable t) { + PicoException e = new PicoServiceProviderException("Error while initializing config bean registry", t); + LOGGER.log(System.Logger.Level.ERROR, e.getMessage(), e); + reset(true); + throw e; + } + } + + @Override + public boolean ready() { + return isInitialized(); + } + + @Override + public List> configuredServiceProvidersConfiguredBy(String key) { + List> result = new ArrayList<>(); + + configuredServiceProvidersByConfigKey.forEach((k, csps) -> { + if (k.equals(key)) { + result.addAll(csps); + } + }); + + if (result.size() > 1) { + result.sort(ServiceProviderComparator.create()); + } + + return result; + } + + @Override + public Map, ConfigBeanInfo> configurableServiceProviders() { + return Map.copyOf(configuredServiceProviderMetaConfigBeanMap); + } + + @Override + @SuppressWarnings("unchecked") + public List> configuredServiceProviders() { + List> result = new ArrayList<>(); + + configuredServiceProvidersByConfigKey.values() + .forEach(cspList -> + cspList.stream() + .filter(csp -> csp instanceof AbstractConfiguredServiceProvider) + .map(AbstractConfiguredServiceProvider.class::cast) + .forEach(rootCsp -> { + rootCsp.assertIsRootProvider(true, false); + Map>> cfgBeanMap = + rootCsp.configuredServicesMap(); + cfgBeanMap.values().forEach(slaveCsp -> slaveCsp.ifPresent(result::add)); + })); + + if (result.size() > 1) { + result.sort(ServiceProviderComparator.create()); + } + + return result; + } + + @Override + public Set configBeansByConfigKey(String key) { + return configBeansByConfigKey(key, Optional.empty()); + } + + @Override + public Set configBeansByConfigKey(String key, + String fullConfigKey) { + return configBeansByConfigKey(key, Optional.of(fullConfigKey)); + } + + @Override + public Map configBeanMapByConfigKey(String key, + String fullConfigKey) { + List> cspsUsingSameKey = + configuredServiceProvidersByConfigKey.get(Objects.requireNonNull(key)); + if (cspsUsingSameKey == null) { + return Map.of(); + } + + Map result = new TreeMap<>(AbstractConfiguredServiceProvider.configBeanComparator()); + cspsUsingSameKey.stream() + .filter(csp -> csp instanceof AbstractConfiguredServiceProvider) + .map(AbstractConfiguredServiceProvider.class::cast) + .forEach(csp -> { + Map configBeans = csp.configBeanMap(); + configBeans.forEach((k, v) -> { + if (fullConfigKey.isEmpty() || fullConfigKey.equals(k)) { + Object prev = result.put(k, v); + if (prev != null && prev != v) { + throw new IllegalStateException("had two entries with the same key: " + prev + " and " + v); + } + } + }); + }); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public Map> allConfigBeans() { + Map> result = new TreeMap<>(AbstractConfiguredServiceProvider.configBeanComparator()); + + configuredServiceProvidersByConfigKey.forEach((key, value) -> value.stream() + .filter(csp -> csp instanceof AbstractConfiguredServiceProvider) + .map(AbstractConfiguredServiceProvider.class::cast) + .forEach(csp -> { + Map configBeans = csp.configBeanMap(); + configBeans.forEach((key1, value1) -> { + result.compute(key1, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.add((CB) value1); + return v; + }); + }); + })); + + return result; + } + + protected boolean isInitialized() { + return (0 == initialized.getCount()); + } + + void loadConfigBeans(io.helidon.config.Config config, + ConfiguredServiceProvider configuredServiceProvider, + ConfigBeanInfo metaConfigBeanInfo, + Map> metaAttributes) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Loading config bean(s) for " + + configuredServiceProvider.serviceType() + " with config: " + + config.key().toString()); + } + + ConfigValue> nodeList = config.asNodeList(); + Object baseConfigBean = maybeLoadBaseConfigBean(config, nodeList, configuredServiceProvider); + Map mapOfInstanceBasedConfig = maybeLoadConfigBeans(nodeList, configuredServiceProvider); + + // validate what we've loaded, to ensure it complies to the meta config info policy + if (!metaConfigBeanInfo.repeatable() && !mapOfInstanceBasedConfig.isEmpty()) { + throw new ConfigException("Expected to only have a single base, non-repeatable configuration for " + + configuredServiceProvider.serviceType() + " with config: " + + config.key().toString()); + } + + if (baseConfigBean != null) { + registerConfigBean(baseConfigBean, null, config, configuredServiceProvider, metaAttributes); + } + mapOfInstanceBasedConfig + .forEach((instanceId, configBean) -> + registerConfigBean(configBean, + config.key().toString() + "." + instanceId, + config.get(instanceId), + configuredServiceProvider, + metaAttributes)); + } + + /** + * The base config bean must be a root config, and is only available if there is a non-numeric + * key in our node list (e.g., "x.y" not "x.1.y"). + */ + CB maybeLoadBaseConfigBean(io.helidon.config.Config config, + ConfigValue> nodeList, + ConfiguredServiceProvider configuredServiceProvider) { + boolean hasAnyNonNumericNodes = nodeList.get().stream() + .anyMatch(cfg -> toNumeric(cfg.name()).isEmpty()); + if (!hasAnyNonNumericNodes) { + return null; + } + + return toConfigBean(config, configuredServiceProvider); + } + + /** + * These are any {config}.N instances, not the base w/o the N. + */ + Map maybeLoadConfigBeans(ConfigValue> nodeList, + ConfiguredServiceProvider configuredServiceProvider) { + Map result = new LinkedHashMap<>(); + + nodeList.get().stream() + .filter(cfg -> toNumeric(cfg.name()).isPresent()) + .map(ConfigDrivenUtils::safeDowncastOf) + .forEach(cfg -> { + String key = cfg.name(); + CB configBean = toConfigBean(cfg, configuredServiceProvider); + Object prev = result.put(key, configBean); + assert (prev == null) : prev + " and " + configBean; + }); + + return result; + } + + CB toConfigBean(io.helidon.config.Config config, + ConfiguredServiceProvider configuredServiceProvider) { + CB configBean = Objects.requireNonNull(configuredServiceProvider.toConfigBean(config), + "unable to create default config bean for " + configuredServiceProvider); + if (configuredServiceProvider instanceof AbstractConfiguredServiceProvider) { + AbstractConfiguredServiceProvider csp = (AbstractConfiguredServiceProvider) configuredServiceProvider; + csp.configBeanInstanceId(configBean, config.key().toString()); + } + + return configBean; + } + + /** + * Validates the config bean against the declared policy, coming by way of annotations on the + * {@code ConfiguredOption}'s. + * + * @param csp the configured service provider + * @param key the config key being validated (aka instance id) + * @param configBean the config bean itself + * @param metaAttributes the meta-attributes that captures the policy in a map like structure by attribute name + * @throws PicoServiceProviderException if the provided config bean is not validated according to policy + */ + void validate(Object configBean, + String key, + Config config, + AbstractConfiguredServiceProvider csp, + Map> metaAttributes) { + Set problems = new LinkedHashSet<>(); + String instanceId = csp.toConfigBeanInstanceId(configBean); + assert (hasValue(key)); + assert (config == null || DEFAULT_INSTANCE_ID.equals(key) || (config.key().toString().equals(key))) + : key + " and " + config.key().toString(); + + AttributeVisitor visitor = new AttributeVisitor<>() { + @Override + public void visit(String attrName, + Supplier valueSupplier, + Map meta, + Object userDefinedCtx, + Class type, + Class... typeArgument) { + Map metaAttrPolicy = metaAttributes.get(attrName); + if (metaAttrPolicy == null) { + problems.add("Unable to query policy for config key '" + key + "'"); + return; + } + + Object required = metaAttrPolicy.get("required"); + String attrConfigKey = (String) Objects.requireNonNull(metaAttrPolicy.get("key")); + + if (required == null) { + required = ConfiguredOption.DEFAULT_REQUIRED; + } else if (!(required instanceof Boolean)) { + required = Boolean.parseBoolean((String) required); + } + + if ((boolean) required) { + boolean validated = false; + + if (FORCE_VALIDATE_USING_CONFIG_ATTRIBUTES) { + validated = validateUsingConfigAttributes( + instanceId, attrName, attrConfigKey, config, valueSupplier, problems); + } + + if (!validated || FORCE_VALIDATE_USING_BEAN_ATTRIBUTES) { + validateUsingBeanAttributes(valueSupplier, attrName, problems); + } + } + + // https://github.com/helidon-io/helidon/issues/6403 : "allowed values" check needed here also! + } + }; + + csp.visitAttributes(configBean, visitor, configBean); + + if (!problems.isEmpty()) { + throw new PicoServiceProviderException("validation rules violated for " + + csp.configBeanType() + + " with config key '" + key + + "':\n" + + String.join(", ", problems).trim(), null, csp); + } + } + + @SuppressWarnings("unchecked") + void registerConfigBean(Object configBean, + String instanceId, + Config config, + ConfiguredServiceProvider configuredServiceProvider, + Map> metaAttributes) { + assert (configuredServiceProvider instanceof AbstractConfiguredServiceProvider); + AbstractConfiguredServiceProvider csp = + (AbstractConfiguredServiceProvider) configuredServiceProvider; + + if (instanceId != null) { + csp.configBeanInstanceId(configBean, instanceId); + } else { + instanceId = configuredServiceProvider.toConfigBeanInstanceId((CB) configBean); + } + + if (DEFAULT_INSTANCE_ID.equals(instanceId)) { + // default config beans should not be validated against any config, even if we have it available + config = null; + } else { + Optional beanConfig = csp.rawConfig(); + if (beanConfig.isPresent()) { + // prefer to use the bean's config over ours if it has it + config = beanConfig.get(); + } + } + + // will throw if not valid + validate(configBean, instanceId, config, csp, metaAttributes); + + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, + "Registering config bean '" + instanceId + "' with " + configuredServiceProvider.serviceType()); + } + + csp.registerConfigBean(instanceId, configBean); + } + + private Set configBeansByConfigKey(String key, + Optional optFullConfigKey) { + List> cspsUsingSameKey = + configuredServiceProvidersByConfigKey.get(Objects.requireNonNull(key)); + if (cspsUsingSameKey == null) { + return Set.of(); + } + + Set result = new LinkedHashSet<>(); + cspsUsingSameKey.stream() + .filter(csp -> csp instanceof AbstractConfiguredServiceProvider) + .map(AbstractConfiguredServiceProvider.class::cast) + .forEach(csp -> { + Map configBeans = csp.configBeanMap(); + if (optFullConfigKey.isEmpty()) { + result.addAll(configBeans.values()); + } else { + configBeans.forEach((k, v) -> { + if (optFullConfigKey.get().equals(k)) { + result.add(v); + } + }); + } + }); + return result; + } + + private void initialize(Config commonCfg) { + if (configuredServiceProvidersByConfigKey.isEmpty()) { + LOGGER.log(System.Logger.Level.DEBUG, "No config driven services found"); + return; + } + + io.helidon.config.Config cfg = safeDowncastOf(commonCfg); + + // first load all the root config beans... but defer resolve until later phase + configuredServiceProviderMetaConfigBeanMap.forEach((configuredServiceProvider, cbi) -> { + MetaConfigBeanInfo metaConfigBeanInfo = (MetaConfigBeanInfo) cbi; + processRootLevelConfigBean(cfg, configuredServiceProvider, metaConfigBeanInfo); + }); + + if (!cfg.exists() || cfg.isLeaf()) { + return; + } + + // now find all the sub root level config beans also... still deferring resolution until a later phase + visitAndInitialize(cfg.asNodeList().get(), 0); + LOGGER.log(System.Logger.Level.DEBUG, "finishing walking config tree"); + } + + private void processRootLevelConfigBean(io.helidon.config.Config cfg, + ConfiguredServiceProvider configuredServiceProvider, + MetaConfigBeanInfo metaConfigBeanInfo) { + if (metaConfigBeanInfo.levelType() != ConfigBean.LevelType.ROOT) { + return; + } + + String key = validatedConfigKey(metaConfigBeanInfo); + io.helidon.config.Config config = cfg.get(key); + Map> metaAttributes = configuredServiceProvider.configBeanAttributes(); + if (config.exists()) { + loadConfigBeans(config, configuredServiceProvider, metaConfigBeanInfo, metaAttributes); + } else if (metaConfigBeanInfo.wantDefaultConfigBean()) { + Object cfgBean = Objects.requireNonNull(configuredServiceProvider.toConfigBean(cfg), + "unable to create default config bean for " + configuredServiceProvider); + registerConfigBean(cfgBean, DEFAULT_INSTANCE_ID, config, configuredServiceProvider, metaAttributes); + } + } + + private void processNestedLevelConfigBean(io.helidon.config.Config config, + ConfiguredServiceProvider configuredServiceProvider, + ConfigBeanInfo metaConfigBeanInfo) { + if (metaConfigBeanInfo.levelType() != ConfigBean.LevelType.NESTED) { + return; + } + + Map> metaAttributes = configuredServiceProvider.configBeanAttributes(); + loadConfigBeans(config, configuredServiceProvider, metaConfigBeanInfo, metaAttributes); + } + + private void visitAndInitialize(List configs, + int depth) { + configs.forEach(config -> { + // we start nested, since we've already processed the root level config beans previously + if (depth > 0) { + String key = config.name(); + List> csps = configuredServiceProvidersByConfigKey.get(key); + if (csps != null && !csps.isEmpty()) { + csps.forEach(configuredServiceProvider -> { + ConfigBeanInfo metaConfigBeanInfo = + Objects.requireNonNull(configuredServiceProviderMetaConfigBeanMap.get(configuredServiceProvider)); + processNestedLevelConfigBean(config, configuredServiceProvider, metaConfigBeanInfo); + }); + } + } + + if (!config.isLeaf()) { + visitAndInitialize(config.asNodeList().get(), depth + 1); + } + }); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParser.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParser.java new file mode 100644 index 00000000000..26a0f269840 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParser.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.config.spi.StringValueParser; + +/** + * Default implementation of {@link StringValueParser}. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +class DefaultStringValueParser implements StringValueParser { + + private static final Map MAP = new HashMap<>(); + static { + MAP.put(int.class, IntegerParser::parse); + MAP.put(Integer.class, IntegerParser::parse); + MAP.put(long.class, LongParser::parse); + MAP.put(Long.class, LongParser::parse); + MAP.put(float.class, FloatParser::parse); + MAP.put(Float.class, FloatParser::parse); + MAP.put(double.class, DoubleParser::parse); + MAP.put(Double.class, DoubleParser::parse); + MAP.put(boolean.class, BooleanParser::parse); + MAP.put(Boolean.class, BooleanParser::parse); + MAP.put(char[].class, CharArrayParser::parse); + } + + DefaultStringValueParser() { + } + + @Override + public Optional parse(String val, + Class type) { + if (String.class == type) { + return (Optional) Optional.ofNullable(val); + } + + StringValueParser parser = MAP.get(type); + if (parser == null) { + throw new IllegalStateException("Don't know how to parse String -> " + type); + } + + return parser.parse(val, type); + } + + + static class IntegerParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : Integer.valueOf(val)); + } + } + + static class LongParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : Long.valueOf(val)); + } + } + + static class FloatParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : Float.valueOf(val)); + } + } + + static class DoubleParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : Double.valueOf(val)); + } + } + + static class BooleanParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : Boolean.valueOf(val)); + } + } + + static class CharArrayParser { + public static Optional parse(String val, + Class ignoredType) { + return Optional.ofNullable(null == val ? null : val.toCharArray()); + } + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParserProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParserProvider.java new file mode 100644 index 00000000000..edfb4959818 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/DefaultStringValueParserProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import io.helidon.builder.config.spi.StringValueParser; +import io.helidon.builder.config.spi.StringValueParserProvider; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +/** + * Service-loaded provider for {@link StringValueParserProvider}. + */ +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultStringValueParserProvider implements StringValueParserProvider { + static final LazyValue INSTANCE = LazyValue.create(DefaultStringValueParser::new); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultStringValueParserProvider() { + } + + @Override + public StringValueParser stringValueParser() { + return INSTANCE.get(); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/UnconfiguredServiceProvider.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/UnconfiguredServiceProvider.java new file mode 100644 index 00000000000..27fe6a61752 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/UnconfiguredServiceProvider.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.builder.AttributeVisitor; +import io.helidon.builder.config.spi.GeneratedConfigBeanBuilderBase; +import io.helidon.common.config.Config; +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.services.PicoInjectionPlan; + +/** + * Used by root service providers when there are no services that have been configured. + * + * @param the service type + * @param the config bean type + */ +class UnconfiguredServiceProvider extends AbstractConfiguredServiceProvider { + private final AbstractConfiguredServiceProvider delegate; + + /** + * Default Constructor. + * + * @param delegate the root delegate + */ + UnconfiguredServiceProvider(AbstractConfiguredServiceProvider delegate) { + assert (delegate != null && delegate.isRootProvider()); + this.delegate = Objects.requireNonNull(delegate); + rootProvider(delegate); + assert (rootProvider().orElseThrow() == delegate); + } + + @Override + public Optional toConfigBean(C cfg, + Class configBeanType) { + return Optional.empty(); + } + + @Override + protected Optional maybeActivate(ContextualServiceQuery query) { + return Optional.empty(); + } + + @Override + public ServiceInfo serviceInfo() { + return delegate.serviceInfo(); + } + + @Override + public Phase currentActivationPhase() { + return delegate.currentActivationPhase(); + } + + @Override + public DependenciesInfo dependencies() { + return delegate.dependencies(); + } + + @Override + public PicoServices picoServices() { + return delegate.picoServices(); + } + + @Override + protected String identitySuffix() { + return delegate.identitySuffix(); + } + + @Override + public String name(boolean simple) { + return delegate.name(simple); + } + + @Override + public Optional> serviceProviderBindable() { + return delegate.serviceProviderBindable(); + } + + @Override + public boolean isCustom() { + return delegate.isCustom(); + } + + @Override + public boolean isRootProvider() { + return false; + } + + @Override + public Optional first(ContextualServiceQuery query) { + // the entire point of this class is to really ensure that we do not resolve injection points! + return Optional.empty(); + } + + @Override + public Optional rawConfig() { + return delegate.rawConfig(); + } + + @Override + public Class serviceType() { + return delegate.serviceType(); + } + + @Override + public Map getOrCreateInjectionPlan(boolean resolveIps) { + return super.getOrCreateInjectionPlan(resolveIps); + } + + @Override + public CB toConfigBean(io.helidon.common.config.Config cfg) { + return delegate.toConfigBean(cfg); + } + + @Override + public GeneratedConfigBeanBuilderBase toConfigBeanBuilder(Config config) { + return delegate.toConfigBeanBuilder(config); + } + + @Override + public void visitAttributes(CB configBean, + AttributeVisitor visitor, + R userDefinedContext) { + delegate.visitAttributes(configBean, visitor, userDefinedContext); + } + + @Override + public String toConfigBeanInstanceId(CB configBean) { + return delegate.toConfigBeanInstanceId(configBean); + } + + @Override + public Optional configBean() { + return Optional.empty(); + } + + @Override + public void configBeanInstanceId(CB configBean, + String val) { + delegate.configBeanInstanceId(configBean, val); + } + + @Override + protected AbstractConfiguredServiceProvider createInstance(Object configBean) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean drivesActivation() { + return delegate.drivesActivation(); + } + + @Override + protected void doPreDestroying(LogEntryAndResult logEntryAndResult) { + delegate.doPreDestroying(logEntryAndResult); + } + + @Override + protected void doDestroying(LogEntryAndResult logEntryAndResult) { + delegate.doDestroying(logEntryAndResult); + } + + @Override + protected void onFinalShutdown() { + delegate.onFinalShutdown(); + } + +} diff --git a/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/package-info.java b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/package-info.java new file mode 100644 index 00000000000..bb50dc50161 --- /dev/null +++ b/pico/configdriven/services/src/main/java/io/helidon/pico/configdriven/services/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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's config-driven-service API. + */ +package io.helidon.pico.configdriven.services; diff --git a/pico/configdriven/services/src/main/java/module-info.java b/pico/configdriven/services/src/main/java/module-info.java new file mode 100644 index 00000000000..d3c32c19add --- /dev/null +++ b/pico/configdriven/services/src/main/java/module-info.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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 Config-Driven Services Module. + */ +module io.helidon.pico.configdriven.services { + requires static jakarta.inject; + requires static jakarta.annotation; + requires static io.helidon.config.metadata; + + // required for compilation of generated types + requires transitive io.helidon.pico.configdriven.api; + requires transitive io.helidon.builder; + requires transitive io.helidon.builder.config; + requires transitive io.helidon.common.types; + requires transitive io.helidon.config; + requires transitive io.helidon.pico.api; + requires transitive io.helidon.pico.services; + + exports io.helidon.pico.configdriven.services; + + uses io.helidon.builder.config.spi.ConfigBeanRegistryProvider; + uses io.helidon.builder.config.spi.StringValueParserProvider; + uses io.helidon.builder.config.spi.ConfigBeanMapperProvider; + uses io.helidon.builder.config.spi.ConfigResolverProvider; + + provides io.helidon.builder.config.spi.ConfigBeanBuilderValidatorProvider + with io.helidon.pico.configdriven.services.DefaultConfigBeanBuilderValidatorProvider; + provides io.helidon.builder.config.spi.ConfigBeanRegistryProvider + with io.helidon.pico.configdriven.services.DefaultConfigBeanRegistryProvider; + provides io.helidon.builder.config.spi.ConfigResolverProvider + with io.helidon.pico.configdriven.services.DefaultConfigResolverProvider; + provides io.helidon.builder.config.spi.StringValueParserProvider + with io.helidon.pico.configdriven.services.DefaultStringValueParserProvider; +} diff --git a/pico/configdriven/services/src/test/java/io/helidon/pico/configdriven/services/StringValueParsersTest.java b/pico/configdriven/services/src/test/java/io/helidon/pico/configdriven/services/StringValueParsersTest.java new file mode 100644 index 00000000000..29057dcc2d6 --- /dev/null +++ b/pico/configdriven/services/src/test/java/io/helidon/pico/configdriven/services/StringValueParsersTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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.configdriven.services; + +import java.util.Optional; + +import io.helidon.builder.config.spi.StringValueParser; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StringValueParsersTest { + + @Test + void testIt() { + StringValueParser parser = new DefaultStringValueParser(); + + assertThat(parser.parse("8080", int.class), optionalValue(equalTo(8080))); + assertThat(parser.parse("0", Integer.class), optionalValue(equalTo(0))); + assertThrows(RuntimeException.class, () -> parser.parse("abc", Integer.class)); + + assertThat(parser.parse("8080", long.class), optionalValue(equalTo(8080L))); + assertThat(parser.parse("1", Long.class), optionalValue(equalTo(1L))); + assertThrows(RuntimeException.class, () -> parser.parse("abc", Long.class)); + + assertThat(parser.parse("8080.1", float.class), optionalValue(equalTo(8080.1f))); + assertThat(parser.parse("1.1", Float.class), optionalValue(equalTo(1.1f))); + assertThrows(RuntimeException.class, () -> parser.parse("abc", Float.class)); + + assertThat(parser.parse("8080.1", double.class), optionalValue(equalTo(8080.1d))); + assertThat(parser.parse("1.1", Double.class), optionalValue(equalTo(1.1d))); + assertThrows(RuntimeException.class, () -> parser.parse("abc", Double.class)); + + assertThat(parser.parse("true", boolean.class), optionalValue(equalTo(true))); + assertThat(parser.parse("true", Boolean.class), optionalValue(equalTo(true))); + assertThat(parser.parse("false", boolean.class), optionalValue(equalTo(false))); + assertThat(parser.parse("false", Boolean.class), optionalValue(equalTo(false))); + assertThat(parser.parse("whatever", boolean.class), optionalValue(equalTo(false))); + assertThat(parser.parse("whatever", Boolean.class), optionalValue(equalTo(false))); + + assertEquals(Optional.empty(), parser.parse(null, String.class)); + assertThat(parser.parse("true", String.class), optionalValue(equalTo("true"))); + assertThat(parser.parse("false", String.class), optionalValue(equalTo("false"))); + + assertThat(new String(parser.parse("true", char[].class).get()), equalTo("true")); + assertThat(new String(parser.parse("false", char[].class).get()), equalTo("false")); + } + +} diff --git a/pico/configdriven/tests/configuredby-application/README.md b/pico/configdriven/tests/configuredby-application/README.md new file mode 100644 index 00000000000..ec4353b2a3d --- /dev/null +++ b/pico/configdriven/tests/configuredby-application/README.md @@ -0,0 +1,3 @@ +# pico-configdriven-test-configuredby-application + +Tests for full ConfiguredBy-generated service types, in combination with the DI model calculated at compile time using the pico-maven-plugin instead of calculated at runtime as is the case with pico-configdriven-test-configuredby. diff --git a/pico/configdriven/tests/configuredby-application/pom.xml b/pico/configdriven/tests/configuredby-application/pom.xml new file mode 100644 index 00000000000..0765f49631d --- /dev/null +++ b/pico/configdriven/tests/configuredby-application/pom.xml @@ -0,0 +1,159 @@ + + + + + + io.helidon.pico.configdriven.tests + helidon-pico-configdriven-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-configdriven-test-configuredby-application + Helidon Pico Config-Driven ConfiguredBy Appl Tests + the same tests as test-configuredby, but instead using the maven application generation in order to calc di plan at compile-time + + + true + true + true + false + true + true + true + + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-services + + + io.helidon.pico.configdriven.tests + helidon-pico-configdriven-test-configuredby + ${helidon.version} + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.pico + helidon-pico-testing + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + alphabetical + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -Apico.debug=${pico.debug} + -Apico.autoAddNonContractInterfaces=true + -Apico.allowListedInterceptorAnnotations=jakarta.inject.Named + -Apico.application.pre.create=true + -Apico.mapApplicationToSingletonScope=true + + true + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + + + + + + + + + + + -Apico.debug=${pico.debug} + -Apico.autoAddNonContractInterfaces=true + -Apico.application.pre.create=true + + + + + NAMED + + + + + + + + diff --git a/pico/configdriven/tests/configuredby-application/src/main/java/io/helidon/pico/configdriven/configuredby/application/test/ASimpleRunLevelService.java b/pico/configdriven/tests/configuredby-application/src/main/java/io/helidon/pico/configdriven/configuredby/application/test/ASimpleRunLevelService.java new file mode 100644 index 00000000000..02249d7aab6 --- /dev/null +++ b/pico/configdriven/tests/configuredby-application/src/main/java/io/helidon/pico/configdriven/configuredby/application/test/ASimpleRunLevelService.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.application.test; + +import java.util.List; +import java.util.Objects; + +import io.helidon.pico.Resettable; +import io.helidon.pico.RunLevel; +import io.helidon.pico.configdriven.configuredby.test.ASingletonServiceContract; +import io.helidon.pico.configdriven.configuredby.test.FakeWebServerContract; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +@RunLevel(RunLevel.STARTUP) +public class ASimpleRunLevelService implements Resettable { + + static int postConstructCount; + static int preDestroyCount; + private boolean running; + private ASingletonServiceContract singleton; + private List> fakeWebServers; + + @Inject // testing an empty/void ctor here + public ASimpleRunLevelService() { + } + + @Inject + void setSingleton(ASingletonServiceContract singleton) { + assert (this.singleton == null); + this.singleton = Objects.requireNonNull(singleton); + } + + @Inject + void setWebServer(List> fakeWebServers) { + assert (this.fakeWebServers == null); + assert (!Objects.requireNonNull(fakeWebServers).isEmpty()); + this.fakeWebServers = Objects.requireNonNull(fakeWebServers); + } + + @Override + public boolean reset(boolean deep) { + postConstructCount = 0; + preDestroyCount = 0; + return true; + } + + @PostConstruct + public void postConstruct() { + assert (!running); + Objects.requireNonNull(singleton); + Objects.requireNonNull(fakeWebServers); + running = true; + postConstructCount++; + } + + @PreDestroy + public void preDestroy() { + assert (running); + Objects.requireNonNull(singleton); + Objects.requireNonNull(fakeWebServers); + preDestroyCount++; + running = false; + } + + public static int getPostConstructCount() { + return postConstructCount; + } + + public static int getPreDestroyCount() { + return preDestroyCount; + } + + public boolean isRunning() { + return running; + } + +} diff --git a/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfigBeanTest.java b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfigBeanTest.java new file mode 100644 index 00000000000..df39a8598c9 --- /dev/null +++ b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfigBeanTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +/** + * Designed to re-run the same tests from base, but using the application-created DI model instead. + */ +class ApplicationConfigBeanTest extends AbstractConfigBeanTest { + +} diff --git a/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java new file mode 100644 index 00000000000..7e1a5dd4ed2 --- /dev/null +++ b/pico/configdriven/tests/configuredby-application/src/test/java/io/helidon/pico/configdriven/configuredby/test/ApplicationConfiguredByTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Metrics; +import io.helidon.pico.RunLevel; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.configdriven.configuredby.application.test.ASimpleRunLevelService; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; + +/** + * Designed to re-run the same tests from base, but using the application-created DI model instead. + */ +class ApplicationConfiguredByTest extends AbstractConfiguredByTest { + + /** + * In application mode, we should not have any lookups recorded. + */ + @Test + void verifyNoLookups() { + resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), createRootDefault8080TestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build()); + + Metrics metrics = picoServices.metrics().orElseThrow(); + Set criteriaSearchLog = picoServices.lookups().orElseThrow(); + Set contractSearchLog = criteriaSearchLog.stream().flatMap(it -> it.contractsImplemented().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + assertThat(contractSearchLog, + containsInAnyOrder( + // config beans are always looked up + "io.helidon.builder.config.testsubjects.fakes.FakeServerConfig", + // tracer doesn't really exist, so it is looked up out of best-effort (as an optional injection dep) + "io.helidon.builder.config.testsubjects.fakes.FakeTracer")); + assertThat("lookup log: " + criteriaSearchLog, + metrics.lookupCount().orElseThrow(), + is(2)); + } + + @Test + public void startupAndShutdownRunLevelServices() { + resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), createRootDefault8080TestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build()); + + Metrics metrics = picoServices.metrics().orElseThrow(); + int startingLookupCount = metrics.lookupCount().orElseThrow(); + + assertThat(ASimpleRunLevelService.getPostConstructCount(), + is(0)); + assertThat(ASimpleRunLevelService.getPreDestroyCount(), + is(0)); + + ServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .runLevel(RunLevel.STARTUP) + .build(); + List> startups = services.lookupAll(criteria); + List desc = startups.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat(desc, + contains(ASimpleRunLevelService.class.getSimpleName() + ":INIT")); + startups.forEach(ServiceProvider::get); + + metrics = picoServices.metrics().orElseThrow(); + int endingLookupCount = metrics.lookupCount().orElseThrow(); + assertThat(endingLookupCount - startingLookupCount, + is(1)); + + assertThat(ASimpleRunLevelService.getPostConstructCount(), + is(1)); + assertThat(ASimpleRunLevelService.getPreDestroyCount(), + is(0)); + + picoServices.shutdown(); + assertThat(ASimpleRunLevelService.getPostConstructCount(), + is(1)); + assertThat(ASimpleRunLevelService.getPreDestroyCount(), + is(1)); + } + +} diff --git a/pico/configdriven/tests/configuredby/README.md b/pico/configdriven/tests/configuredby/README.md new file mode 100644 index 00000000000..7491a3e36b0 --- /dev/null +++ b/pico/configdriven/tests/configuredby/README.md @@ -0,0 +1,3 @@ +# pico-configdriven-test-configuredby + +Tests for full ConfiguredBy-generated service types. diff --git a/pico/configdriven/tests/configuredby/pom.xml b/pico/configdriven/tests/configuredby/pom.xml new file mode 100644 index 00000000000..2fa9883c975 --- /dev/null +++ b/pico/configdriven/tests/configuredby/pom.xml @@ -0,0 +1,137 @@ + + + + + + io.helidon.pico.configdriven.tests + helidon-pico-configdriven-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-configdriven-test-configuredby + Helidon Pico Config-Driven ConfiguredBy Svcs Tests + tests the fuller config-driven services (i.e., full config not just common and with config-driven services) + + + true + true + true + false + true + true + true + + + + + io.helidon.builder + helidon-builder-config + + + io.helidon.builder.tests + helidon-builder-tests-test-configbean + ${helidon.version} + + + io.helidon.config + helidon-config + + + io.helidon.pico + helidon-pico-services + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + provided + true + + + io.helidon.common.testing + helidon-common-testing-junit5 + compile + + + io.helidon.pico + helidon-pico-testing + compile + + + org.hamcrest + hamcrest-all + compile + + + org.junit.jupiter + junit-jupiter-api + compile + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + alphabetical + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -Apico.autoAddNonContractInterfaces=true + + + + + true + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + + + + + + + + diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonConfigBean.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonConfigBean.java new file mode 100644 index 00000000000..1b071d2aa89 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonConfigBean.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import io.helidon.builder.config.ConfigBean; + +@ConfigBean(drivesActivation = true, atLeastOne = true, wantDefaultConfigBean = true) +public interface ASingletonConfigBean { + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonService.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonService.java new file mode 100644 index 00000000000..6cdaf8392bf --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonService.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import io.helidon.pico.configdriven.ConfiguredBy; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Named; + +@ConfiguredBy(ASingletonConfigBean.class) +@Named("jane") +class ASingletonService implements ASingletonServiceContract { + private boolean running; + + // note: initially left w/o a ctor here! + + /** + * For Testing. + */ + @PostConstruct + public void initialize() { + assert (!running); + running = true; + } + + /** + * For Testing. + */ + @PreDestroy + public void shutdown() { + running = false; + } + + /** + * For Testing. + */ + @Override + public boolean isRunning() { + return running; + } + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonServiceContract.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonServiceContract.java new file mode 100644 index 00000000000..14ef72956ba --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/ASingletonServiceContract.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +/** + * For Testing. + */ +public interface ASingletonServiceContract { + + /** + * For Testing. + */ + boolean isRunning(); + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfigBeanTest.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfigBeanTest.java new file mode 100644 index 00000000000..0094aba64ee --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfigBeanTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.List; +import java.util.Map; + +import io.helidon.builder.config.testsubjects.DefaultTestClientConfig; +import io.helidon.builder.config.testsubjects.DefaultTestServerConfig; +import io.helidon.builder.config.testsubjects.TestClientConfig; +import io.helidon.builder.config.testsubjects.TestServerConfig; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; + +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.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * See {@code BasicConfigBeanTest}, this repeats some of that with a fuller classpath with config-driven-services and full config + * enabled. This means that extra validation (e.g., required config attributes, etc.) will be tested here. + */ +public class AbstractConfigBeanTest { + + @Test + void emptyConfig() { + Config cfg = Config.create(); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> DefaultTestServerConfig.toBuilder(cfg).build()); + assertThat(e.getMessage(), + equalTo("'port' is a required attribute and cannot be null")); + } + + @Test + void minimalConfig() { + Config cfg = Config.builder( + ConfigSources.create( + Map.of("port", "8080", + "cipher-suites", "a,b,c", + "headers.0", "header1", + "headers.1", "header2"), + "my-simple-config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + TestServerConfig serverConfig = DefaultTestServerConfig.toBuilder(cfg).build(); + assertThat(serverConfig.description(), + optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(8080)); + assertThat(serverConfig.cipherSuites(), + contains("a", "b", "c")); + + TestClientConfig clientConfig = DefaultTestClientConfig.toBuilder(cfg).build(); + assertThat(clientConfig.pswd(), + nullValue()); + assertThat(clientConfig.name(), + equalTo("default")); + assertThat(clientConfig.port(), + equalTo(8080)); + assertThat(clientConfig.cipherSuites(), + contains("a", "b", "c")); + assertThat(clientConfig.headers(), + hasEntry("headers.0", "header1")); + assertThat(clientConfig.headers(), + hasEntry("headers.1", "header2")); + } + + /** + * Callers can conceptually use config beans as just plain old vanilla builders, void of any config usage. + */ + @Test + void noConfig() { + TestServerConfig serverConfig = DefaultTestServerConfig.builder().build(); + assertThat(serverConfig.description(), + optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(0)); + assertThat(serverConfig.cipherSuites(), + equalTo(List.of())); + + serverConfig = DefaultTestServerConfig.toBuilder(serverConfig).port(123).build(); + assertThat(serverConfig.description(), + optionalEmpty()); + assertThat(serverConfig.name(), + equalTo("default")); + assertThat(serverConfig.port(), + equalTo(123)); + assertThat(serverConfig.cipherSuites(), + equalTo(List.of())); + + TestClientConfig clientConfig = DefaultTestClientConfig.builder().build(); + assertThat(clientConfig.name(), + equalTo("default")); + assertThat(clientConfig.port(), + equalTo(0)); + assertThat(clientConfig.headers(), + equalTo(Map.of())); + assertThat(clientConfig.cipherSuites(), + equalTo(List.of())); + + clientConfig = DefaultTestClientConfig.toBuilder(clientConfig).port(123).build(); + assertThat(clientConfig.name(), + equalTo("default")); + assertThat(clientConfig.port(), + equalTo(123)); + assertThat(clientConfig.headers(), + equalTo(Map.of())); + assertThat(clientConfig.cipherSuites(), + equalTo(List.of())); + } + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfiguredByTest.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfiguredByTest.java new file mode 100644 index 00000000000..d2cb0e6d8f1 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/AbstractConfiguredByTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.MapConfigSource; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServiceProviderException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.configdriven.ConfiguredBy; +import io.helidon.pico.configdriven.services.ConfigBeanRegistry; +import io.helidon.pico.testing.PicoTestingSupport; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link io.helidon.pico.configdriven.ConfiguredBy}. + */ +public abstract class AbstractConfiguredByTest { + protected static final String FAKE_SOCKET_CONFIG = "sockets"; + protected static final String FAKE_SERVER_CONFIG = "fake-server"; + + protected PicoServices picoServices; + protected Services services; + + @BeforeAll + static void initialStateChecks() { + ConfigBeanRegistry cbr = (ConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry().orElseThrow(); + assertThat(cbr.ready(), is(false)); + } + + @AfterAll + static void tearDown() { + PicoTestingSupport.resetAll(); + } + + protected void resetWith(Config config) { + PicoTestingSupport.resetAll(); + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + public MapConfigSource.Builder createBasicTestingConfigSource() { + return ConfigSources.create( + Map.of( + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_ACTIVATION_LOGS, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_SERVICE_LOOKUP_CACHING, "true" + ), "config-basic"); + } + + public MapConfigSource.Builder createRootDefault8080TestingConfigSource() { + return ConfigSources.create( + Map.of( + FAKE_SERVER_CONFIG + ".name", "root", + FAKE_SERVER_CONFIG + ".port", "8080", + FAKE_SERVER_CONFIG + ".worker-count", "1" + ), "config-root-default-8080"); + } + + @Test + void testItAll() { + resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), createRootDefault8080TestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build()); + + // verify the services registry + testRegistry(); + + ServiceProvider fakeWebServer = services.lookup(FakeWebServer.class); + assertThat(fakeWebServer.currentActivationPhase(), is(Phase.ACTIVE)); + assertThat(fakeWebServer.get().isRunning(), is(true)); + + ServiceProvider singletonService = services.lookup(ASingletonService.class); + assertThat(singletonService.currentActivationPhase(), is(Phase.ACTIVE)); + assertThat(singletonService.get().isRunning(), is(true)); + + // verify the bean registry + testBeanRegistry(); + + // shutdown has to come next + testShutdown(fakeWebServer.get()); + } + + // @Test + void testRegistry() { + DefaultServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addQualifier(DefaultQualifierAndValue.create(ConfiguredBy.class)) + .build(); + List> list = services.lookupAll(criteria); + List desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat("root providers are config-driven, auto-started services unless overridden to not be driven", desc, + contains("ASingletonService{root}:ACTIVE", + "FakeTlsWSNotDrivenByCB{root}:PENDING", + "FakeWebServer{root}:ACTIVE", + "FakeWebServerNotDrivenAndHavingConfiguredByOverrides{root}:PENDING" + )); + + criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(FakeWebServerContract.class.getName()) + .build(); + list = services.lookupAll(criteria); + desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat("no root providers expected in result, but all are auto-started unless overridden", desc, + contains("FakeWebServer{3}:ACTIVE", + "FakeWebServerNotDrivenAndHavingConfiguredByOverrides{2}:PENDING")); + + criteria = DefaultServiceInfoCriteria.builder() + .serviceTypeName(FakeTlsWSNotDrivenByCB.class.getName()) + .build(); + list = services.lookupAll(criteria); + desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat("root providers expected here since we looked up by service type name", desc, + contains("FakeTlsWSNotDrivenByCB{root}:PENDING")); + + criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(FakeTlsWSNotDrivenByCB.class.getName()) + .addQualifier(DefaultQualifierAndValue.createNamed("jimmy")) + .build(); + list = services.lookupAll(criteria); + desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat("root providers expected here since no configuration for this service", desc, + contains("FakeTlsWSNotDrivenByCB{root}:PENDING")); + + ServiceProvider fakeTlsProvider = list.get(0); + PicoServiceProviderException e = assertThrows(PicoServiceProviderException.class, fakeTlsProvider::get); + assertThat("there is no configuration, so cannot activate this service", e.getMessage(), + equalTo("expected to find a match: service provider: FakeTlsWSNotDrivenByCB{root}:PENDING")); + + criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(ASingletonService.class.getName()) + .addQualifier(DefaultQualifierAndValue.createNamed("jane")) + .build(); + list = services.lookupAll(criteria); + desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat("slave providers expected here since we have default configuration for this service", desc, + contains("ASingletonService{1}:ACTIVE")); + } + + // @Test + void testShutdown(FakeWebServer fakeWebServer) { + assertThat(fakeWebServer.isRunning(), is(true)); + + picoServices.shutdown(); + + assertThat(fakeWebServer.isRunning(), is(false)); + } + + // @Test + void testBeanRegistry() { + ConfigBeanRegistry cbr = (ConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry().orElseThrow(); + assertThat(cbr.ready(), is(true)); + + Set set = cbr.allConfigBeans().keySet(); + assertThat(set, containsInAnyOrder( + "@default", + "fake-server" + )); + } + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeTlsWSNotDrivenByCB.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeTlsWSNotDrivenByCB.java new file mode 100644 index 00000000000..cdd774832e8 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeTlsWSNotDrivenByCB.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.Objects; + +import io.helidon.builder.config.testsubjects.fakes.FakeWebServerTlsConfig; +import io.helidon.pico.configdriven.ConfiguredBy; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +@ConfiguredBy(FakeWebServerTlsConfig.class) +@Named("jimmy") +public class FakeTlsWSNotDrivenByCB { + + private final FakeWebServerTlsConfig cfg; + private boolean running; + + @Inject + FakeTlsWSNotDrivenByCB(FakeWebServerTlsConfig cfg) { + this.cfg = Objects.requireNonNull(cfg); + } + + /** + * For Testing. + */ + @PostConstruct + public void initialize() { + assert (!running); + running = true; + } + + /** + * For Testing. + */ + @PreDestroy + public void shutdown() { + running = false; + } + + public FakeWebServerTlsConfig configuration() { + return cfg; + } + + public boolean isRunning() { + return running; + } + +} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServer.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServer.java similarity index 56% rename from pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServer.java rename to pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServer.java index 3b3c7cadd6f..6eecddab60b 100644 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeWebServer.java +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,36 +14,52 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.fakes; +package io.helidon.pico.configdriven.configuredby.test; import java.util.Objects; import java.util.Optional; +import io.helidon.builder.config.testsubjects.fakes.FakeServerConfig; +import io.helidon.builder.config.testsubjects.fakes.FakeTracer; +import io.helidon.pico.configdriven.ConfiguredBy; + import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; -//@ConfiguredBy(FakeServerConfig.class) -public class FakeWebServer implements WebServer { +/** + * For Testing. + */ +@ConfiguredBy(FakeServerConfig.class) +public class FakeWebServer implements FakeWebServerContract { - private FakeServerConfig cfg; + private final FakeServerConfig cfg; private boolean running; @Inject - FakeWebServer(FakeServerConfig cfg, Optional tracer) { + FakeWebServer(FakeServerConfig cfg, + Optional tracer) { this.cfg = Objects.requireNonNull(cfg); + assert (tracer.isEmpty()); } -// /** -// * The traditional approach. -// */ -// FakeWebServer(WebServer.Builder builder) { -// } - + /** + * For Testing. + */ @PostConstruct public void initialize() { + assert (!running); running = true; } + /** + * For Testing. + */ + @PreDestroy + public void shutdown() { + running = false; + } + @Override public FakeServerConfig configuration() { return cfg; @@ -53,4 +69,5 @@ public FakeServerConfig configuration() { public boolean isRunning() { return running; } + } diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerContract.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerContract.java new file mode 100644 index 00000000000..1f3f3d1bd21 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerContract.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import io.helidon.builder.config.testsubjects.fakes.FakeServerConfig; +import io.helidon.pico.Contract; + +/** + * For Testing. + */ +@Contract +public interface FakeWebServerContract { + + /** + * Gets effective server configuration. + * + * @return Server configuration + */ + FakeServerConfig configuration(); + + /** + * Returns {@code true} if the server is currently running. Running server in stopping phase returns {@code true} until it + * is not fully stopped. + * + * @return {@code true} if server is running + */ + boolean isRunning(); + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerNotDrivenAndHavingConfiguredByOverrides.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerNotDrivenAndHavingConfiguredByOverrides.java new file mode 100644 index 00000000000..ac46e50e9e1 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/FakeWebServerNotDrivenAndHavingConfiguredByOverrides.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.Optional; + +import io.helidon.builder.config.testsubjects.fakes.FakeServerConfig; +import io.helidon.builder.config.testsubjects.fakes.FakeTracer; +import io.helidon.pico.configdriven.ConfiguredBy; + +import jakarta.inject.Inject; + +@ConfiguredBy(value = FakeServerConfig.class, overrideBean = true, drivesActivation = false) +public class FakeWebServerNotDrivenAndHavingConfiguredByOverrides extends FakeWebServer { + + @Inject + FakeWebServerNotDrivenAndHavingConfiguredByOverrides(FakeServerConfig cfg, + Optional tracer) { + super(cfg, tracer); + } + +} diff --git a/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/package-info.java b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/package-info.java new file mode 100644 index 00000000000..883c4bb955a --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/main/java/io/helidon/pico/configdriven/configuredby/test/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * For Testing. + */ +package io.helidon.pico.configdriven.configuredby.test; diff --git a/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/ConfiguredByTest.java b/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/ConfiguredByTest.java new file mode 100644 index 00000000000..991ee56e319 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/ConfiguredByTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.builder.config.testsubjects.fakes.FakeServerConfig; +import io.helidon.builder.config.testsubjects.fakes.FakeSocketConfig; +import io.helidon.config.ConfigSources; +import io.helidon.config.MapConfigSource; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.configdriven.services.ConfigBeanRegistry; +import io.helidon.pico.configdriven.services.ConfiguredServiceProvider; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; + +/** + * Executes the tests from the base. + */ +class ConfiguredByTest extends AbstractConfiguredByTest { + + protected MapConfigSource.Builder createNested8080TestingConfigSource() { + return ConfigSources.create( + Map.of( + "nested." + FAKE_SERVER_CONFIG + ".0.name", "nested", + "nested." + FAKE_SERVER_CONFIG + ".0.port", "8080", + "nested." + FAKE_SERVER_CONFIG + ".0.worker-count", "1" + ), "config-nested-default-8080"); + } + + public MapConfigSource.Builder createRootPlusOneSocketTestingConfigSource() { + return ConfigSources.create( + Map.of( + FAKE_SERVER_CONFIG + ".name", "root", + FAKE_SERVER_CONFIG + ".port", "8080", + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".0.name", "first", + FAKE_SERVER_CONFIG + "." + FAKE_SOCKET_CONFIG + ".0.port", "8081" + ), "config-root-plus-one-socket"); + } + + @Test + void onlyRootConfigBeansAreCreated() { + resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), + createRootDefault8080TestingConfigSource(), + createNested8080TestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build()); + + ConfigBeanRegistry cbr = (ConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry().orElseThrow(); + assertThat(cbr.ready(), + is(true)); + + Set set = cbr.allConfigBeans().keySet(); + assertThat(set, containsInAnyOrder( + "@default", + "fake-server" + )); + } + + @Test + void serverConfigWithOneSocketConfigNested() { + resetWith(io.helidon.config.Config.builder(createBasicTestingConfigSource(), + createRootPlusOneSocketTestingConfigSource()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build()); + + ConfigBeanRegistry cbr = (ConfigBeanRegistry) ConfigBeanRegistryHolder.configBeanRegistry().orElseThrow(); + assertThat(cbr.ready(), + is(true)); + + Set set = cbr.allConfigBeans().keySet(); + assertThat(set, + containsInAnyOrder("@default", + "fake-server" + )); + + Set configBeans = cbr.configBeansByConfigKey("fake-server"); + assertThat(configBeans.toString(), configBeans.size(), + is(1)); + + List> list = cbr.configuredServiceProvidersConfiguredBy("fake-server"); + List desc = list.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat(desc, + contains("FakeWebServer{root}:ACTIVE", + "FakeWebServerNotDrivenAndHavingConfiguredByOverrides{root}:PENDING")); + + FakeServerConfig cfg = (FakeServerConfig) configBeans.iterator().next(); + Map sockets = cfg.sockets(); + assertThat(sockets.toString(), sockets.size(), + is(1)); + } + +} diff --git a/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/DefaultConfigBeanTest.java b/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/DefaultConfigBeanTest.java new file mode 100644 index 00000000000..75b71cb9c18 --- /dev/null +++ b/pico/configdriven/tests/configuredby/src/test/java/io/helidon/pico/configdriven/configuredby/test/DefaultConfigBeanTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.configdriven.configuredby.test; + +/** + * Executes the tests from the base. + */ +class DefaultConfigBeanTest extends AbstractConfigBeanTest { + +} diff --git a/pico/builder-config/tests/pom.xml b/pico/configdriven/tests/pom.xml similarity index 79% rename from pico/builder-config/tests/pom.xml rename to pico/configdriven/tests/pom.xml index cfc57cdd75d..f9c0863a035 100644 --- a/pico/builder-config/tests/pom.xml +++ b/pico/configdriven/tests/pom.xml @@ -21,8 +21,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - io.helidon.pico.builder.config - helidon-pico-builder-config-project + io.helidon.pico.configdriven + helidon-pico-configdriven-project 4.0.0-SNAPSHOT ../pom.xml @@ -37,13 +37,14 @@ true - io.helidon.pico.builder.config.tests - helidon-pico-builder-config-tests-project - Helidon Pico Builder Config Tests Project + io.helidon.pico.configdriven.tests + helidon-pico-configdriven-tests-project + Helidon Pico Config-Driven Tests Project pom - configbean + configuredby + configuredby-application diff --git a/pico/maven-plugin/README.md b/pico/maven-plugin/README.md new file mode 100644 index 00000000000..683ae16cb22 --- /dev/null +++ b/pico/maven-plugin/README.md @@ -0,0 +1,105 @@ +# pico-maven-plugin +A collection of maven plugins for Pico-based applications that provides several features including options to: + +1. Validate the entirety of dependency graph across all modules. The application-create plugin would be applied in the same pom.xml + that would otherwise assemble your application for deployment. This module would be expected to have compile-time references to + each module that contributes to your application holistically + +2. After the model has been validated, the application-create plugin will code-generate the service provider "Activators", "Modules", and "Application" into the + final assembly that will be used at runtime to satisfy every injection point for the entire application without the need for reflection at runtime. This can be thought conceptually as a "linking phase" for your native application/image. + +3. Creating Pico modules from external jars and packages using the . + +--- + +Q: Is this maven plugin required for your Pico-based application to work? + +Answer 1: No, but it is recommended. Strictly speaking the main code generation occurs using the annotation processor, and the output from that processor is fully functional at runtime. However, without the use of this plugin your application's dependency model will not be validation nor will it be linked/bound/burned into your final application image. It will still work fine and Pico will handle this case, but your application is not as optimal from a runtime performance perspective. + +Answer 2: Yes, but only if you do not have access to the service implementation types, and are unable to apply the annotation processor on those types at compile time. Note, however, that Pico can still work without the maven processor in cases where you have possession to rebuild the service classes, but get the interfaces from an external module. In this later case, the ExternalContracts interfaces can be used on the service implementation classes. + +--- + +## Usage + +The following are the maven Mojo's that are available to use. Each can be used for either the src/main or src/test. + +example usage: +```pom.xml + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + + external-module-create + + + + compile + compile + + application-create + + + + + + io.helidon.pico.examples.logger.common + + ALL + + +``` + +### application-create +This goal is used to trigger the creation of the Pico$$Application for your module (which typically is found in the final assembly jar module for your application). The usage of this also triggers the validation and integrity checks for your entire application's DI model, and will fail-fast at compilation time if any issue is detected (e.g., a non-Optional @Inject is found on a contract type having no concrete service implementations, etc.). Assuming there are no issues found during application creation - each of your service implementations will be listed inside the picoApplication, and it will include the literal DI resolution plan for each of your services. This can also be very useful for visualization and debugging purposes besides being optimal from a runtime performance perspective. + +Also note that Pico strives to ensure your application stays as deterministic as possible (as shown by the Pico$$Application/i> generated class). But when the jakarta.inject.Provider type is used within your application then some of that deterministic behavior goes away. This is due to how Provider<>'s work since the implementation for the Provider (i.e., your application logic) "owns" the behavior for what actual concrete type that are created by it, along with the scope/cardinality for those instances. These instances are then delivered (as potentially injectable services) into other dependent services. In this way Pico is simply acting as a broker and delivery mechanism between your Provider<> implementation(s) and the consumer that are using those service instances as injection points, etc. This is not meant to scare ore even dissuade you from using Provider<>, but merely to inform you that some of the deterministic behavior goes away under these circumstances using Provider instead of another Scope type like @Singleton. In many/most cases this is completely normal and acceptable. As a precaution, however, Pico chose to fail-fast at application creation time if your application is found to use jakarta.inject.Provider. You will then need to provide a strategy/configuration in your pom.xml file to permit these types of usages. There are options to allow ALL providers (as shown in the above example), or the strategy can be dictated on a case-by-case basis. See the javadoc for [AbstractApplicationCreatorMojo](./src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java) for details. + +### external-module-create +This goal is used to trigger the creation of the supporting set of Pico DI classes for an external jar/module - typically created without since it lacked hacing the Pico annotation processor during compilation. In this scenario, and to use this option then first be sure that the dependent module is a maven compile-time dependency in your module. After that then simply state the name of the package(s) to scan and produce the supporting Pico DI classes (e.g., "io.helidon.pico.examples.logger.common" in the above example) in the pom.xml and then target/generated-sources/pico should be generated accordingly. + +The example from above cover the basics for generation. There are one more advanced option that is available here that we'd like to cover. The below was taken from the [test-tck-jsr330 pom.xml](../tests/tck-jsr330/pom.xml): + +```pom.xml + + + -Apico.debug=true + -Apico.autoAddNonContractInterfaces=true + + + org.atinject.tck.auto + org.atinject.tck.auto.accessories + + true + + + org.atinject.tck.auto.accessories.SpareTire + + + jakarta.inject.Named + spare + + + + + org.atinject.tck.auto.DriversSeat + + + org.atinject.tck.auto.Drivers + + + + + +``` + +Here we can see additional DI constructs, specifically two Qualifiers, are being augmented into the DI declaration model for the 3rd party jar. We can also see the option used to treat all service type interfaces as Contracts. + +## Pico$$Application-and-Pico$$TestApplication +When the maven plugin creates an application for src/main/java sources, a Pico$$Application will be created for compile-time dependencies involved in the DI set of services. But when src/test/java sources are compiled, a Pico$$TestApplication will be created for test-type dependencies involved in the test-side DI set of services of your application. + +## Best Practices +Only one Pico$$Application should typically be in your module classpath. And in production applications there should never be any test service types or a Pico$$TestApplication, etc. diff --git a/pico/maven-plugin/etc/spotbugs/exclude.xml b/pico/maven-plugin/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..16cb0be5924 --- /dev/null +++ b/pico/maven-plugin/etc/spotbugs/exclude.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pico/maven-plugin/pom.xml b/pico/maven-plugin/pom.xml new file mode 100644 index 00000000000..0c07f9e0059 --- /dev/null +++ b/pico/maven-plugin/pom.xml @@ -0,0 +1,188 @@ + + + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-maven-plugin + Helidon Pico Maven Plugin + maven-plugin + + + 2.6.0 + 3.3.0 + 3.8.5 + 3.6.4 + 3.7.1 + 2.2.1 + + etc/spotbugs/exclude.xml + + + + + + org.apache.maven.plugins + maven-plugin-plugin + + + + report + + + + + + + + + + + org.codehaus.plexus + plexus-classworlds + ${version.plexus.classworlds} + + + org.codehaus.plexus + plexus-utils + ${version.plexus.utils} + + + org.apache.maven + maven-artifact + ${version.plugin.api} + provided + + + + + io.helidon.pico + helidon-pico-tools + + + + io.helidon.builder + helidon-builder-config + + + io.helidon.config + helidon-config + + + jakarta.inject + jakarta.inject-api + compile + + + jakarta.annotation + jakarta.annotation-api + + + com.github.jknack + handlebars + + + io.github.classgraph + classgraph + + + org.apache.maven + maven-plugin-api + ${version.plugin.api} + provided + + + org.codehaus.plexus + plexus-classworlds + + + org.codehaus.plexus + plexus-utils + + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${version.plugin.annotations} + provided + + + org.apache.maven + maven-project + ${version.plugin.project} + provided + + + org.apache.maven + maven-model + + + org.codehaus.plexus + plexus-utils + + + org.apache.maven + maven-artifact + + + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${version.plugin.plugin} + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${version.plugin.site} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -proc:none + + + + + + diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java new file mode 100644 index 00000000000..f21992da0d8 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.File; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.CallingContext; +import io.helidon.pico.CallingContextFactory; +import io.helidon.pico.DefaultCallingContext; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Module; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderProvider; +import io.helidon.pico.Services; +import io.helidon.pico.services.DefaultServiceBinder; +import io.helidon.pico.tools.AbstractFilerMessager; +import io.helidon.pico.tools.ActivatorCreatorCodeGen; +import io.helidon.pico.tools.ApplicationCreatorCodeGen; +import io.helidon.pico.tools.ApplicationCreatorConfigOptions; +import io.helidon.pico.tools.ApplicationCreatorRequest; +import io.helidon.pico.tools.ApplicationCreatorResponse; +import io.helidon.pico.tools.CodeGenFiler; +import io.helidon.pico.tools.CodeGenPaths; +import io.helidon.pico.tools.CompilerOptions; +import io.helidon.pico.tools.DefaultApplicationCreatorCodeGen; +import io.helidon.pico.tools.DefaultApplicationCreatorConfigOptions; +import io.helidon.pico.tools.DefaultApplicationCreatorRequest; +import io.helidon.pico.tools.DefaultCodeGenPaths; +import io.helidon.pico.tools.DefaultCompilerOptions; +import io.helidon.pico.tools.ModuleInfoDescriptor; +import io.helidon.pico.tools.ToolsException; +import io.helidon.pico.tools.spi.ApplicationCreator; + +import org.apache.maven.model.Build; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import static io.helidon.pico.CallingContext.globalCallingContext; +import static io.helidon.pico.CallingContext.toErrorMessage; +import static io.helidon.pico.maven.plugin.MavenPluginUtils.applicationCreator; +import static io.helidon.pico.maven.plugin.MavenPluginUtils.hasValue; +import static io.helidon.pico.maven.plugin.MavenPluginUtils.picoServices; +import static io.helidon.pico.maven.plugin.MavenPluginUtils.toDescriptions; +import static io.helidon.pico.tools.ApplicationCreatorConfigOptions.PermittedProviderType; +import static io.helidon.pico.tools.ModuleUtils.REAL_MODULE_INFO_JAVA_NAME; +import static io.helidon.pico.tools.ModuleUtils.isUnnamedModuleName; +import static io.helidon.pico.tools.ModuleUtils.toBasePath; +import static io.helidon.pico.tools.ModuleUtils.toSuggestedModuleName; +import static java.util.Optional.ofNullable; + +/** + * Abstract base for the {@code Pico maven-plugin} responsible for creating {@code Application} and Test {@code Application}'s. + * + * @see io.helidon.pico.Application + * @see io.helidon.pico.tools.ApplicationCreatorConfigOptions + */ +@SuppressWarnings({"unused", "FieldCanBeLocal"}) +public abstract class AbstractApplicationCreatorMojo extends AbstractCreatorMojo { + + /** + * The approach for handling providers. + * See {@link io.helidon.pico.tools.ApplicationCreatorConfigOptions#permittedProviderTypes()}. + */ + @Parameter(property = PicoServicesConfig.NAME + ".permitted.provider.types", readonly = true) + private String permittedProviderTypes; + private PermittedProviderType permittedProviderType; + + /** + * Sets the named types permitted for providers, assuming use of + * {@link PermittedProviderType#NAMED}. + */ + @Parameter(property = PicoServicesConfig.NAME + ".permitted.provider.type.names", readonly = true) + private List permittedProviderTypeNames; + + /** + * Sets the named qualifier types permitted for providers, assuming use of + * {@link PermittedProviderType#NAMED}. + */ + @Parameter(property = PicoServicesConfig.NAME + ".permitted.provider.qualifier.type.names", readonly = true) + private List permittedProviderQualifierTypeNames; + + /** + * Default constructor. + */ + protected AbstractApplicationCreatorMojo() { + } + + static ToolsException noModuleFoundError() { + return new ToolsException("unable to determine the name for the current module - " + + "was APT run and do you have a module-info?"); + } + + static ToolsException noModuleFoundError(String moduleName) { + return new ToolsException("no pico module named '" + moduleName + "' found in the current module - was APT run?"); + } + + String getThisModuleName() { + Build build = getProject().getBuild(); + Path basePath = toBasePath(build.getSourceDirectory()); + String moduleName = toSuggestedModuleName(basePath, Path.of(build.getSourceDirectory()), true).orElseThrow(); + if (isUnnamedModuleName(moduleName)) { + // try to recover it from a previous tooling step + String appPackageName = loadAppPackageName().orElse(null); + if (appPackageName == null) { + // throw noModuleFoundError(); + getLog().warn(noModuleFoundError().getMessage()); + } else { + moduleName = appPackageName; + } + } + return moduleName; + } + + ServiceProvider lookupThisModule(String name, + Services services) { + return services.lookupFirst(Module.class, name, false).orElseThrow(() -> noModuleFoundError(name)); + } + + String getClassPrefixName() { + return ActivatorCreatorCodeGen.DEFAULT_CLASS_PREFIX_NAME; + } + + abstract String getGeneratedClassName(); + + abstract File getOutputDirectory(); + + List getSourceRootPaths() { + return getNonTestSourceRootPaths(); + } + + List getNonTestSourceRootPaths() { + MavenProject project = getProject(); + List result = new ArrayList<>(project.getCompileSourceRoots().size()); + for (Object a : project.getCompileSourceRoots()) { + result.add(Path.of(a.toString())); + } + return result; + } + + List getTestSourceRootPaths() { + MavenProject project = getProject(); + List result = new ArrayList<>(project.getTestCompileSourceRoots().size()); + for (Object a : project.getTestCompileSourceRoots()) { + result.add(Path.of(a.toString())); + } + return result; + } + + LinkedHashSet getModulepathElements() { + return getSourceClasspathElements(); + } + + boolean hasModuleInfo() { + return getSourceRootPaths().stream() + .anyMatch(p -> new File(p.toFile(), REAL_MODULE_INFO_JAVA_NAME).exists()); + } + + /** + * Favors the 'test' module-info if available, and falls back to 'main' module-info. + * + * @param location the location for the located module-info + * @return the module-info descriptor to return or null if none is available + */ + ModuleInfoDescriptor getAnyModuleInfo(AtomicReference location) { + File file = getNonTestSourceRootPaths().stream() + .map(p -> new File(p.toFile(), REAL_MODULE_INFO_JAVA_NAME)) + .filter(File::exists) + .findFirst() + .orElse(null); + + if (file == null) { + file = getTestSourceRootPaths().stream() + .map(p -> new File(p.toFile(), REAL_MODULE_INFO_JAVA_NAME)) + .filter(File::exists) + .findFirst() + .orElse(null); + } + + if (file != null && location != null) { + location.set(file); + return ModuleInfoDescriptor.create(file.toPath()); + } + + return null; + } + + /** + * @return This represents the set of services that we already code-gen'ed + */ + Set getServiceTypeNamesForExclusion() { + getLog().info("excluding service type names: []"); + return Set.of(); + } + + @Override + protected void innerExecute() { + this.permittedProviderType = PermittedProviderType.valueOf(permittedProviderTypes.toUpperCase()); + + CallingContext callCtx = null; + Optional callingContextBuilder = + CallingContextFactory.createBuilder(false); + if (callingContextBuilder.isPresent()) { + callingContextBuilder.get().moduleName(Optional.ofNullable(getThisModuleName())); + callCtx = callingContextBuilder.get().build(); + globalCallingContext(callCtx, true); + } + + // we MUST get the exclusion list prior to building the next loader, since it will reset the service registry + Set serviceNamesForExclusion = getServiceTypeNamesForExclusion(); + boolean hasModuleInfo = hasModuleInfo(); + Set modulepath = (hasModuleInfo) ? getModulepathElements() : Collections.emptySet(); + Set classpath = getClasspathElements(); + ClassLoader prev = Thread.currentThread().getContextClassLoader(); + URLClassLoader loader = ExecutableClassLoader.create(classpath, prev); + + try { + Thread.currentThread().setContextClassLoader(loader); + + PicoServices picoServices = picoServices(false); + if (picoServices.config().usesCompileTimeApplications()) { + String desc = "should not be using 'application' bindings"; + String msg = (callCtx == null) ? toErrorMessage(desc) : toErrorMessage(callCtx, desc); + throw new IllegalStateException(msg); + } + Services services = picoServices.services(); + + // get the application creator only after pico services were initialized (we need to ignore any existing apps) + ApplicationCreator creator = applicationCreator(); + + List> allModules = services + .lookupAll(DefaultServiceInfoCriteria.builder().addContractImplemented(Module.class.getName()).build()); + getLog().info("processing modules: " + toDescriptions(allModules)); + if (allModules.isEmpty()) { + warn("no modules to process"); + } + + // retrieves all the services in the registry + List> allServices = services + .lookupAll(DefaultServiceInfoCriteria.builder().build(), false); + if (allServices.isEmpty()) { + warn("no services to process"); + return; + } + + Set serviceTypeNames = toNames(allServices); + serviceTypeNames.removeAll(serviceNamesForExclusion); + + String classPrefixName = getClassPrefixName(); + AtomicReference moduleInfoPathRef = new AtomicReference<>(); + ModuleInfoDescriptor descriptor = getAnyModuleInfo(moduleInfoPathRef); + String moduleInfoPath = (moduleInfoPathRef.get() != null) + ? moduleInfoPathRef.get().getPath() + : null; + String moduleInfoModuleName = getThisModuleName(); + ServiceProvider moduleSp = lookupThisModule(moduleInfoModuleName, services); + String packageName = determinePackageName(Optional.ofNullable(moduleSp), serviceTypeNames, descriptor, true); + + CodeGenPaths codeGenPaths = DefaultCodeGenPaths.builder() + .generatedSourcesPath(getGeneratedSourceDirectory().getPath()) + .outputPath(getOutputDirectory().getPath()) + .moduleInfoPath(ofNullable(moduleInfoPath)) + .build(); + ApplicationCreatorCodeGen applicationCodeGen = DefaultApplicationCreatorCodeGen.builder() + .packageName(packageName) + .className(getGeneratedClassName()) + .classPrefixName(classPrefixName) + .build(); + List compilerArgs = getCompilerArgs(); + CompilerOptions compilerOptions = DefaultCompilerOptions.builder() + .classpath(classpath) + .modulepath(modulepath) + .sourcepath(getSourceRootPaths()) + .source(getSource()) + .target(getTarget()) + .commandLineArguments((compilerArgs != null) ? compilerArgs : List.of()) + .build(); + ApplicationCreatorConfigOptions configOptions = DefaultApplicationCreatorConfigOptions.builder() + .permittedProviderTypes(permittedProviderType) + .permittedProviderNames(permittedProviderTypeNames) + .permittedProviderQualifierTypeNames(toTypeNames(permittedProviderQualifierTypeNames)) + .build(); + String moduleName = getModuleName(); + AbstractFilerMessager directFiler = AbstractFilerMessager.createDirectFiler(codeGenPaths, getLogger()); + CodeGenFiler codeGenFiler = CodeGenFiler.create(directFiler); + DefaultApplicationCreatorRequest.Builder reqBuilder = DefaultApplicationCreatorRequest.builder() + .codeGen(applicationCodeGen) + .messager(new Messager2LogAdapter()) + .filer(codeGenFiler) + .configOptions(configOptions) + .serviceTypeNames(serviceTypeNames) + .codeGenPaths(codeGenPaths) + .compilerOptions(compilerOptions) + .throwIfError(isFailOnError()) + .generator(getClass().getName()) + .templateName(getTemplateName()); + if (hasValue(moduleName)) { + reqBuilder.moduleName(moduleName); + } else if (!isUnnamedModuleName(moduleInfoModuleName)) { + reqBuilder.moduleName(moduleInfoModuleName); + } + ApplicationCreatorRequest req = reqBuilder.build(); + ApplicationCreatorResponse res = creator.createApplication(req); + if (res.success()) { + getLog().debug("processed service type names: " + res.serviceTypeNames()); + if (getLog().isDebugEnabled()) { + getLog().debug("response: " + res); + } + } else { + getLog().error("failed to process", res.error().orElse(null)); + } + } finally { + Thread.currentThread().setContextClassLoader(prev); + } + } + + List toTypeNames(List permittedProviderQualifierTypeNames) { + if (permittedProviderQualifierTypeNames == null || permittedProviderQualifierTypeNames.isEmpty()) { + return List.of(); + } + + return permittedProviderQualifierTypeNames.stream() + .map(DefaultTypeName::createFromTypeName) + .collect(Collectors.toList()); + } + + Set toNames(List> services) { + Map> result = new LinkedHashMap<>(); + services.forEach(sp -> { + sp = DefaultServiceBinder.toRootProvider(sp); + String serviceType = sp.serviceInfo().serviceTypeName(); + TypeName name = DefaultTypeName.createFromTypeName(serviceType); + ServiceProvider prev = result.put(name, sp); + if (prev != null) { + if (!(prev instanceof ServiceProviderProvider)) { + throw new ToolsException("there are two registrations for the same service type: " + prev + " and " + sp); + } + getLog().debug("two registrations for the same service type: " + prev + " and " + sp); + } + }); + return new TreeSet<>(result.keySet()); + } + + void warn(String msg) { + Optional optBuilder = CallingContextFactory.createBuilder(false); + CallingContext callCtx = (optBuilder.isPresent()) + ? optBuilder.get().moduleName(Optional.ofNullable(getThisModuleName())).build() : null; + String desc = "no modules to process"; + String ctxMsg = (callCtx == null) ? CallingContext.toErrorMessage(desc) : CallingContext.toErrorMessage(callCtx, desc); + ToolsException e = new ToolsException(ctxMsg); + if (PicoServices.isDebugEnabled()) { + getLog().warn(e.getMessage(), e); + } else { + getLog().warn(e.getMessage()); + } + if (isFailOnWarning()) { + throw e; + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java new file mode 100644 index 00000000000..045e78fd1f6 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.Module; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.tools.AbstractCreator; +import io.helidon.pico.tools.Messager; +import io.helidon.pico.tools.ModuleInfoDescriptor; +import io.helidon.pico.tools.ModuleUtils; +import io.helidon.pico.tools.Options; +import io.helidon.pico.tools.TemplateHelper; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import static io.helidon.pico.tools.ModuleUtils.toSuggestedGeneratedPackageName; + +/** + * Abstract base for all pico creator mojo's. + */ +@SuppressWarnings({"unused", "FieldCanBeLocal", "FieldMayBeFinal"}) +public abstract class AbstractCreatorMojo extends AbstractMojo { + private final System.Logger logger = System.getLogger(getClass().getName()); + + static final String DEFAULT_SOURCE = AbstractCreator.DEFAULT_SOURCE; + static final String DEFAULT_TARGET = AbstractCreator.DEFAULT_TARGET; + + static final TrafficCop TRAFFIC_COP = new TrafficCop(); + + /** + * Tag controlling whether we fail on error. + */ + static final String TAG_FAIL_ON_ERROR = PicoServicesConfig.NAME + ".failOnError"; + + /** + * Tag controlling whether we fail on warnings. + */ + static final String TAG_FAIL_ON_WARNING = PicoServicesConfig.NAME + ".failOnWarning"; + + /** + * The file name written to ./target/pico/ to track the last package name generated for this application. + * This application package name is what we fall back to for the application name and the module name if not otherwise + * specified directly. + */ + protected static final String APPLICATION_PACKAGE_FILE_NAME = ModuleUtils.APPLICATION_PACKAGE_FILE_NAME; + + // ---------------------------------------------------------------------- + // Pico Configurables + // ---------------------------------------------------------------------- + + /** + * The template name to use for codegen. + */ + @Parameter(property = TemplateHelper.TAG_TEMPLATE_NAME, readonly = true, defaultValue = TemplateHelper.DEFAULT_TEMPLATE_NAME) + private String templateName; + + /** + * The module name to apply. If not found the module name will be inferred. + */ + @Parameter(property = Options.TAG_MODULE_NAME, readonly = true) + private String moduleName; + + // ---------------------------------------------------------------------- + // Generic Configurables + // ---------------------------------------------------------------------- + + /** + * The current project instance. This is used for propagating generated-sources paths as + * compile/testCompile source roots. + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + /** + * Indicates whether the build will continue even if there are compilation errors. + */ + @Parameter(property = TAG_FAIL_ON_ERROR, defaultValue = "true") + private boolean failOnError = true; + + /** + * Indicates whether the build will continue even if there are any warnings. + */ + @Parameter(property = TAG_FAIL_ON_WARNING) + private boolean failOnWarning; + + /** + * The -source argument for the Java compiler. + * Note: using the same as maven-compiler for convenience and least astonishment. + */ + @Parameter(property = "maven.compiler.source", defaultValue = DEFAULT_SOURCE) + private String source; + + /** + * The -target argument for the Java compiler. + * Note: using the same as maven-compiler for convenience and least astonishment. + */ + @Parameter(property = "maven.compiler.target", defaultValue = DEFAULT_TARGET) + private String target; + + /** + * The target directory where to place output. + */ + @Parameter(defaultValue = "${project.build.directory}", readonly = true) + private String targetDir; + + /** + * The package name to apply. If not found the package name will be inferred. + */ + @Parameter(property = PicoServicesConfig.NAME + ".package.name", readonly = true) + private String packageName; + + /** + * Sets the arguments to be passed to the compiler. + *

        + * Example: + *

        +     * <compilerArgs>
        +     *   <arg>-Xmaxerrs</arg>
        +     *   <arg>1000</arg>
        +     *   <arg>-Xlint</arg>
        +     *   <arg>-J-Duser.language=en_us</arg>
        +     * </compilerArgs>
        +     * 
        + */ + @Parameter + private List compilerArgs; + + /** + * Sets the debug flag. + * See {@link io.helidon.pico.PicoServicesConfig#TAG_DEBUG}. + */ + @Parameter(property = PicoServicesConfig.TAG_DEBUG, readonly = true) + private boolean isDebugEnabled; + + /** + * Default constructor. + */ + protected AbstractCreatorMojo() { + } + + /** + * Returns true if debug is enabled. + * + * @return true if in debug mode + */ + protected boolean isDebugEnabled() { + return isDebugEnabled; + } + + /** + * The project build directory. + * + * @return the project build directory + */ + protected File getProjectBuildTargetDir() { + return new File(targetDir); + } + + /** + * The scratch directory. + * + * @return the scratch directory + */ + protected File getPicoScratchDir() { + return new File(getProjectBuildTargetDir(), PicoServicesConfig.NAME); + } + + /** + * The target package name. + * + * @return the target package name + */ + protected String getPackageName() { + return packageName; + } + + System.Logger getLogger() { + return logger; + } + + String getTemplateName() { + return templateName; + } + + String getModuleName() { + return moduleName; + } + + MavenProject getProject() { + return project; + } + + boolean isFailOnError() { + return failOnError; + } + + boolean isFailOnWarning() { + return failOnWarning; + } + + String getSource() { + return source; + } + + String getTarget() { + return target; + } + + List getCompilerArgs() { + return compilerArgs; + } + + @Override + public void execute() throws MojoExecutionException { + try (TrafficCop.GreenLight greenLight = TRAFFIC_COP.waitForGreenLight()) { + getLog().info("Started " + getClass().getName() + " for " + getProject()); + innerExecute(); + getLog().info("Finishing " + getClass().getName() + " for " + getProject()); + MavenPluginUtils.resetAll(); + } catch (Throwable t) { + MojoExecutionException me = new MojoExecutionException("creator failed", t); + getLog().error(me.getMessage(), t); + throw me; + } finally { + getLog().info("Finished " + getClass().getName() + " for " + getProject()); + } + } + + /** + * Determines the primary package name (which also typically doubles as the application name). + * + * @param optModuleSp the module service provider + * @param typeNames the type names + * @param descriptor the descriptor + * @param persistIt pass true to write it to scratch, so that we can use it in the future for this module + * @return the package name (which also typically doubles as the application name) + */ + protected String determinePackageName(Optional> optModuleSp, + Collection typeNames, + ModuleInfoDescriptor descriptor, + boolean persistIt) { + String packageName = getPackageName(); + if (packageName == null) { + // check for the existence of the file + packageName = loadAppPackageName().orElse(null); + if (packageName != null) { + return packageName; + } + + ServiceProvider moduleSp = optModuleSp.orElse(null); + if (moduleSp != null) { + packageName = DefaultTypeName.createFromTypeName(moduleSp.serviceInfo().serviceTypeName()).packageName(); + } else { + packageName = toSuggestedGeneratedPackageName(descriptor, typeNames, PicoServicesConfig.NAME); + } + } + + Objects.requireNonNull(packageName, "unable to determine package name"); + + if (persistIt) { + // record it to scratch file for later consumption (during test build for example) + saveAppPackageName(packageName); + } + + return packageName; + } + + /** + * Attempts to load the app package name from what was previously recorded. + * + * @return the app package name that was loaded + */ + protected Optional loadAppPackageName() { + return ModuleUtils.loadAppPackageName(getPicoScratchDir().toPath()); + } + + /** + * Persist the package name into scratch for later usage. + * + * @param packageName the package name + */ + protected void saveAppPackageName(String packageName) { + ModuleUtils.saveAppPackageName(getPicoScratchDir().toPath(), packageName); + } + + /** + * Gated/controlled by the {@link io.helidon.pico.maven.plugin.TrafficCop}. + * + * @throws MojoExecutionException if any mojo problems occur + */ + protected abstract void innerExecute() throws MojoExecutionException; + + LinkedHashSet getDependencies(String optionalScopeFilter) { + MavenProject project = getProject(); + LinkedHashSet result = new LinkedHashSet<>(project.getDependencyArtifacts().size()); + for (Object a : project.getDependencyArtifacts()) { + Artifact artifact = (Artifact) a; + if (optionalScopeFilter == null || optionalScopeFilter.equals(artifact.getScope())) { + result.add(((Artifact) a).getFile().toPath()); + } + } + return result; + } + + LinkedHashSet getSourceClasspathElements() { + MavenProject project = getProject(); + LinkedHashSet result = new LinkedHashSet<>(project.getCompileArtifacts().size()); + result.add(new File(project.getBuild().getOutputDirectory()).toPath()); + for (Object a : project.getCompileArtifacts()) { + result.add(((Artifact) a).getFile().toPath()); + } + return result; + } + + /** + * Provides a convenient way to handle test scope. Returns the classpath for source files (or test sources) only. + */ + LinkedHashSet getClasspathElements() { + return getSourceClasspathElements(); + } + + abstract File getGeneratedSourceDirectory(); + + + class Messager2LogAdapter implements Messager { + @Override + public void debug(String message) { + getLog().debug(message); + } + + @Override + public void debug(String message, + Throwable t) { + getLog().debug(message, t); + } + + @Override + public void log(String message) { + getLog().info(message); + } + + @Override + public void warn(String message) { + getLog().warn(message); + } + + @Override + public void warn(String message, + Throwable t) { + getLog().warn(message, t); + } + + @Override + public void error(String message, + Throwable t) { + getLog().error(message, t); + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ApplicationCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ApplicationCreatorMojo.java new file mode 100644 index 00000000000..2c69a4e5943 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ApplicationCreatorMojo.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.File; + +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.tools.DefaultApplicationCreator; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * A mojo wrapper to {@link io.helidon.pico.tools.spi.ApplicationCreator}. + */ +@Mojo(name = "application-create", defaultPhase = LifecyclePhase.COMPILE, threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE) +@SuppressWarnings("unused") +public class ApplicationCreatorMojo extends AbstractApplicationCreatorMojo { + + /** + * The classname to use for the Pico {@link io.helidon.pico.Application} class. + * If not found the classname will be inferred. + */ + @Parameter(property = PicoServicesConfig.NAME + ".application.class.name", readonly = true + // note: the default value handling doesn't work here for "$$"!! +// defaultValue = DefaultApplicationCreator.APPLICATION_NAME + ) + private String className; + + /** + * Specify where to place generated source files created by annotation processing. + */ + @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations") + private File generatedSourcesDirectory; + + /** + * The directory for compiled classes. + */ + @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) + private File outputDirectory; + + /** + * Default constructor. + */ + public ApplicationCreatorMojo() { + } + + @Override + String getGeneratedClassName() { + return (className == null) ? DefaultApplicationCreator.APPLICATION_NAME : className; + } + + @Override + File getGeneratedSourceDirectory() { + return generatedSourcesDirectory; + } + + @Override + File getOutputDirectory() { + return outputDirectory; + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecHandler.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecHandler.java new file mode 100644 index 00000000000..928230835a1 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecHandler.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URLClassLoader; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.helidon.pico.tools.ToolsException; + +/** + * Delegates a functional invocation to be run within the context of a provided ClassLoader. + */ +class ExecHandler implements Closeable { + private final boolean ownedContext; + private final IsolatedThreadGroup threadGroup; + private final URLClassLoader loader; + + /** + * Creates an instance using the provided threadGroup and loader. + * + * @param threadGroup the containing thread group to use for any/all spawned threads. + * @param loader the loader context to invoke the function in + */ + ExecHandler(boolean ownedContext, + IsolatedThreadGroup threadGroup, + URLClassLoader loader) { + this.ownedContext = ownedContext; + this.threadGroup = threadGroup; + this.loader = loader; + } + + /** + * Creates an instance using the provided threadGroup and loader. The caller is responsible + * for the lifecycle and closure of the provided context elements. + * + * @param threadGroup the containing thread group to use for any/all spawned threads + * @param loader the loader context to invoke the function + * @return the exec handler instance + */ + static ExecHandler create(IsolatedThreadGroup threadGroup, + URLClassLoader loader) { + return new ExecHandler(false, Objects.requireNonNull(threadGroup), Objects.requireNonNull(loader)); + } + + /** + * Creates a new dedicated thread for each invocation, running within the context of the provided + * isolated thread group and loader. If the request supplier returns null, then that is the signal + * to the implementation to abort the function call. + * + * @param reqSupplier the request supplier (obviously loaded in the caller's thread context/loader) + * @param fn the function to call (obviously loaded in the caller's thread/context loader) + * @param the function request type + * @param the function response type + * @return res the result (where the caller might have to use reflection to access) + */ + Res apply(Supplier reqSupplier, + Function fn) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(); + + Thread bootstrapThread = new Thread(threadGroup, () -> { + try { + Req req = reqSupplier.get(); + if (req == null) { + return; + } + result.set(fn.apply(req)); + } catch (Throwable t) { + throw new ToolsException("error in apply", t); + } finally { + latch.countDown(); + } + }); + threadGroup.preStart(bootstrapThread, loader); + bootstrapThread.start(); + + try { + latch.await(); + } catch (InterruptedException e) { + throw new ToolsException(e.getMessage(), e); + } + + threadGroup.throwAnyUncaughtErrors(); + + return result.get(); + } + + /** + * Should be closed to clean up any owned/acquired resources. + */ + @Override + public void close() throws IOException { + if (ownedContext) { + threadGroup.close(); + loader.close(); + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecutableClassLoader.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecutableClassLoader.java new file mode 100644 index 00000000000..91594fa54ec --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExecutableClassLoader.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.helidon.pico.tools.ToolsException; + +/** + * Responsible for creating a spi classlaoder using a child-first delegation strategy that can + * handle execution of callables, etc. using it. + */ + class ExecutableClassLoader { + private ExecutableClassLoader() { + } + + /** + * Creates the loader appropriate for {@link ExecHandler}. + * + * @param classPath the classpath to use + * @param parent the parent loader + * @return the loader + */ + public static URLClassLoader create(Collection classPath, + ClassLoader parent) { + List urls = new ArrayList<>(classPath.size()); + try { + for (Path dependency : classPath) { + urls.add(dependency.toUri().toURL()); + } + } catch (MalformedURLException e) { + throw new ToolsException("unable to build classpath", e); + } + + if (parent == null) { + parent = Thread.currentThread().getContextClassLoader(); + } + return new URLClassLoader(urls.toArray(new URL[0]), parent); + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExternalModuleCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExternalModuleCreatorMojo.java new file mode 100644 index 00000000000..3e841166c6c --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ExternalModuleCreatorMojo.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.File; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.tools.AbstractFilerMessager; +import io.helidon.pico.tools.ActivatorCreatorConfigOptions; +import io.helidon.pico.tools.ActivatorCreatorRequest; +import io.helidon.pico.tools.ActivatorCreatorResponse; +import io.helidon.pico.tools.CodeGenFiler; +import io.helidon.pico.tools.CodeGenPaths; +import io.helidon.pico.tools.DefaultActivatorCreatorConfigOptions; +import io.helidon.pico.tools.DefaultCodeGenPaths; +import io.helidon.pico.tools.DefaultExternalModuleCreatorRequest; +import io.helidon.pico.tools.ExternalModuleCreatorRequest; +import io.helidon.pico.tools.ExternalModuleCreatorResponse; +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.spi.ExternalModuleCreator; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import static io.helidon.pico.maven.plugin.MavenPluginUtils.activatorCreator; +import static io.helidon.pico.maven.plugin.MavenPluginUtils.externalModuleCreator; + +/** + * Responsible for creating pico {@link io.helidon.pico.Activator}'s and a {@link io.helidon.pico.Module} + * wrapping a set of packages from an external third-party jar. + */ +@Mojo(name = "external-module-create", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE) +@SuppressWarnings("unused") +public class ExternalModuleCreatorMojo extends AbstractCreatorMojo { + + /** + * Sets the packages to be passed to the creator. + *

        + * Example: + *

        +     * <packageNames>
        +     *   <org.inject.tck.auto>
        +     *   <org.inject.tck.auto.accessories>
        +     * </packageNames>
        +     * 
        + */ + @Parameter(required = true) + private List packageNames; + + /** + * Sets the qualifiers to be passed to the creator. + *

        + * Example: + *

        +     * <serviceTypeToQualifiers>
        +     *   <org.atinject.tck.auto.accessories.SpareTire>
        +     *   <qualifier>
        +     *      <qualifierTypeName>
        +     *      </qualifierTypeName>
        +     *      <value>
        +     *      </value>
        +     *   </qualifier>
        +     *   </org.atinject.tck.auto.accessories.SpareTire>
        +     * </serviceTypeToQualifiers>
        +     * 
        + */ + @Parameter(name = "serviceTypeQualifiers") + private List serviceTypeQualifiers; + + /** + * Establishes whether strict jsr-330 compliance is in effect. + */ + @Parameter(name = "supportsJsr330Strict", property = PicoServicesConfig.KEY_SUPPORTS_JSR330 + ".strict") + private boolean supportsJsr330Strict; + + /** + * Specify where to place generated source files created by annotation processing. + */ + @Parameter(defaultValue = "${project.build.directory}/generated-sources/" + PicoServicesConfig.NAME) + private File generatedSourcesDirectory; + + /** + * Default constructor. + */ + public ExternalModuleCreatorMojo() { + } + + /** + * @return the package names that should be targeted for activator creation + */ + List getPackageNames() { + return packageNames; + } + + /** + * @return the explicit qualifiers that should be setup as part of activator creation + */ + Map> getServiceTypeToQualifiers() { + if (serviceTypeQualifiers == null) { + return Map.of(); + } + + Map> result = new LinkedHashMap<>(); + serviceTypeQualifiers.forEach((serviceTypeQualifiers) -> result.putAll(serviceTypeQualifiers.toMap())); + return result; + } + + /** + * @return true if jsr-330 strict mode is in effect + */ + boolean isSupportsJsr330InStrictMode() { + return supportsJsr330Strict; + } + + /** + * @return the generated sources directory + */ + @Override + File getGeneratedSourceDirectory() { + return generatedSourcesDirectory; + } + + /** + * @return the output directory + */ + File getOutputDirectory() { + return new File(getProject().getBuild().getOutputDirectory()); + } + + @Override + protected void innerExecute() throws MojoExecutionException { + if (packageNames == null || packageNames.isEmpty()) { + throw new MojoExecutionException("packageNames are required to be specified"); + } + + ClassLoader prev = Thread.currentThread().getContextClassLoader(); + Set classpath = getDependencies("compile"); + URLClassLoader loader = ExecutableClassLoader.create(classpath, prev); + + try { + Thread.currentThread().setContextClassLoader(loader); + + ExternalModuleCreator externalModuleCreator = externalModuleCreator(); + + ActivatorCreatorConfigOptions configOptions = DefaultActivatorCreatorConfigOptions.builder() + .supportsJsr330InStrictMode(isSupportsJsr330InStrictMode()) + .build(); + String generatedSourceDir = getGeneratedSourceDirectory().getPath(); + + CodeGenPaths codeGenPaths = DefaultCodeGenPaths.builder() + .generatedSourcesPath(generatedSourceDir) + .outputPath(getOutputDirectory().getPath()) + .metaInfServicesPath(new File(getOutputDirectory(), "META-INF/services").getPath()) + .build(); + AbstractFilerMessager directFiler = AbstractFilerMessager.createDirectFiler(codeGenPaths, getLogger()); + CodeGenFiler codeGenFiler = CodeGenFiler.create(directFiler); + ExternalModuleCreatorRequest request = DefaultExternalModuleCreatorRequest.builder() + .packageNamesToScan(getPackageNames()) + .serviceTypeToQualifiersMap(getServiceTypeToQualifiers()) + .throwIfError(isFailOnWarning()) + .activatorCreatorConfigOptions(configOptions) + .codeGenPaths(codeGenPaths) + .moduleName(Optional.ofNullable(getModuleName())) + .filer(codeGenFiler) + .build(); + ExternalModuleCreatorResponse res = externalModuleCreator.prepareToCreateExternalModule(request); + if (res.success()) { + getLog().debug("processed service type names: " + res.serviceTypeNames()); + if (getLog().isDebugEnabled()) { + getLog().debug("response: " + res); + } + + // now proceed to creating the activators (we get this from the external module creation) + ActivatorCreatorRequest activatorCreatorRequest = res.activatorCreatorRequest(); + ActivatorCreator activatorCreator = activatorCreator(); + ActivatorCreatorResponse activatorCreatorResponse = + activatorCreator.createModuleActivators(activatorCreatorRequest); + if (activatorCreatorResponse.success()) { + getProject().addCompileSourceRoot(generatedSourceDir); + getLog().info("successfully processed: " + activatorCreatorResponse.serviceTypeNames()); + } else { + getLog().error("failed to process", activatorCreatorResponse.error().orElse(null)); + } + } else { + getLog().error("failed to process", res.error().orElse(null)); + } + } finally { + Thread.currentThread().setContextClassLoader(prev); + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/IsolatedThreadGroup.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/IsolatedThreadGroup.java new file mode 100644 index 00000000000..a4193b6bd0f --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/IsolatedThreadGroup.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.pico.tools.ToolsException; + +/** + * a ThreadGroup to isolate execution and collect exceptions. + */ +class IsolatedThreadGroup extends ThreadGroup implements Closeable { + static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); + + private final AtomicInteger counter = new AtomicInteger(); + private final long timeoutInMillis; + private Throwable uncaughtThrowable; + + private IsolatedThreadGroup(String name, + long timeOutInMillis) { + super(name); + this.timeoutInMillis = timeOutInMillis; + } + + /** + * Creates am isolated thread group using the default timeout of {@link #DEFAULT_TIMEOUT_MILLIS}. + * + * @param name the name of the group + * @return the instance of the isolated thread group created + */ + static IsolatedThreadGroup create(String name) { + return new IsolatedThreadGroup(name, DEFAULT_TIMEOUT_MILLIS); + } + + /** + * Creates an isolated thread group using the provided timeout of {@link #DEFAULT_TIMEOUT_MILLIS}. + * + * @param name the name of the group + * @param timeOutInMillis timeoutInMillis used during close processing + * @return the instance of the isolated thread group created + */ + static IsolatedThreadGroup create(String name, + long timeOutInMillis) { + return new IsolatedThreadGroup(name, timeOutInMillis); + } + + /** + * Adds an uncaught throwable for this thread group. + * + * @param t the throwable to add, or null to reset + */ + void setUncaughtThrowable(Throwable t) { + if (t instanceof ThreadDeath) { + return; // harmless + } + if (uncaughtThrowable != null) { + // we will only handle 0..1 errors + return; + } + this.uncaughtThrowable = t; + } + + /** + * Should be closed at completion. The implementation will attempt to join threads, + * terminate any threads after the timeout period, and throw any uncaught errors. + */ + @Override + public void close() { + try { + joinNonDaemonThreads(); + terminateThreads(); + } catch (Throwable t) { + setUncaughtThrowable(t); + } + throwAnyUncaughtErrors(); + } + + /** + * If anything was caught, will throw an error immediately. + */ + public void throwAnyUncaughtErrors() { + if (uncaughtThrowable != null) { + ToolsException e = new ToolsException("uncaught error", uncaughtThrowable); + uncaughtThrowable = null; + throw e; + } + } + + /** + * Prepares the thread for start. + * + * @param thread the thread + * @param loader the loader + */ + void preStart(Thread thread, + ClassLoader loader) { + thread.setContextClassLoader(loader); + thread.setName(getName() + "-" + counter.incrementAndGet()); + } + + private void joinNonDaemonThreads() { + boolean foundNonDaemon; + long expiry = System.currentTimeMillis() + timeoutInMillis; + do { + foundNonDaemon = false; + Collection threads = getActiveThreads(); + for (Thread thread : threads) { + if (thread.isDaemon()) { + continue; + } + // try again; maybe more threads were created while we are busy + foundNonDaemon = true; + joinThread(thread, 0); + } + } while (foundNonDaemon && System.currentTimeMillis() < expiry); + } + + private void joinThread(Thread thread, + long timeoutInMillis) { + try { + thread.join(timeoutInMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void terminateThreads() { + // these were not responsive to interruption + Set uncooperativeThreads = new CopyOnWriteArraySet<>(); + + getActiveThreads().parallelStream().forEach(thread -> { + thread.interrupt(); + if (thread.isAlive()) { + joinThread(thread, timeoutInMillis); + if (thread.isAlive()) { + uncooperativeThreads.add(thread); + } + } + }); + + if (!uncooperativeThreads.isEmpty()) { + uncaughtThrowable = new ToolsException("unable to terminate these threads: " + uncooperativeThreads); + } + } + + /** + * Returns the list of threads for this thread group. + * + * @return list of threads + */ + List getActiveThreads() { + Thread[] threads = new Thread[activeCount()]; + int numThreads = enumerate(threads); + List result = new ArrayList<>(numThreads); + for (int i = 0; i < threads.length && threads[i] != null; i++) { + result.add(threads[i]); + } + return result; + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/MavenPluginUtils.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/MavenPluginUtils.java new file mode 100644 index 00000000000..712042437b2 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/MavenPluginUtils.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.builder.config.spi.HelidonConfigBeanRegistry; +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesHolder; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.spi.ApplicationCreator; +import io.helidon.pico.tools.spi.ExternalModuleCreator; + +import static io.helidon.pico.PicoServicesConfig.KEY_PERMITS_DYNAMIC; +import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_APPLICATIONS; +import static io.helidon.pico.PicoServicesConfig.NAME; + +final class MavenPluginUtils { + private MavenPluginUtils() { + } + + /** + * Returns a {@link io.helidon.pico.Services} registry that forces application loading to be disabled. + * + * @return pico services + */ + static PicoServices picoServices(boolean wantApps) { + resetAll(); + return lazyCreate(basicConfig(wantApps)).get(); + } + + /** + * Resets all internal Pico configuration instances, JVM global singletons, service registries, etc. + */ + static void resetAll() { + Internal.reset(); + } + + static ApplicationCreator applicationCreator() { + return HelidonServiceLoader.create(ServiceLoader.load(ApplicationCreator.class)).iterator().next(); + } + + static ExternalModuleCreator externalModuleCreator() { + return HelidonServiceLoader.create(ServiceLoader.load(ExternalModuleCreator.class)).iterator().next(); + } + + static ActivatorCreator activatorCreator() { + return HelidonServiceLoader.create(ServiceLoader.load(ActivatorCreator.class)).iterator().next(); + } + + /** + * Describe the provided instance or provider. + * + * @param providerOrInstance the instance to provider + * @return the description of the instance + */ + static String toDescription(Object providerOrInstance) { + if (providerOrInstance instanceof Optional) { + providerOrInstance = ((Optional) providerOrInstance).orElse(null); + } + + if (providerOrInstance instanceof ServiceProvider) { + return ((ServiceProvider) providerOrInstance).description(); + } + return String.valueOf(providerOrInstance); + } + + /** + * Describe the provided instance or provider collection. + * + * @param coll the instance to provider collection + * @return the description of the instance + */ + static List toDescriptions(Collection coll) { + return coll.stream().map(MavenPluginUtils::toDescription).collect(Collectors.toList()); + } + + static boolean hasValue(String val) { + return (val != null && !val.isBlank()); + } + + static LazyValue lazyCreate(Config config) { + return LazyValue.create(() -> { + PicoServices.globalBootstrap(DefaultBootstrap.builder() + .config(config) + .limitRuntimePhase(Phase.GATHERING_DEPENDENCIES) + .build()); + return PicoServices.picoServices().orElseThrow(); + }); + } + + static Config basicConfig(boolean apps) { + return Config.builder(ConfigSources.create( + Map.of(NAME + "." + KEY_PERMITS_DYNAMIC, "true", + NAME + "." + KEY_USES_COMPILE_TIME_APPLICATIONS, String.valueOf(apps)), + "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + } + + private static class Internal extends PicoServicesHolder { + public static void reset() { + PicoServicesHolder.reset(); + HelidonConfigBeanRegistry cbr = ConfigBeanRegistryHolder.configBeanRegistry().orElse(null); + if (cbr instanceof Resettable) { + ((Resettable) cbr).reset(true); + } + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/Qualifier.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/Qualifier.java new file mode 100644 index 00000000000..78babc8b79d --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/Qualifier.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.QualifierAndValue; + +/** + * Used by {@link ExternalModuleCreatorMojo}, and here in this package due to maven + * requirements to be in the same package as the mojo. + * See https://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Complex_Objects + */ +public class Qualifier implements QualifierAndValue { + private String qualifierTypeName; + private String value; + + /** + * Default constructor. + */ + public Qualifier() { + } + + @Override + public String qualifierTypeName() { + return qualifierTypeName; + } + + /** + * Sets the qualifier type name. + * + * @param val the qualifier type name + */ + public void setQualifierTypeName(String val) { + this.qualifierTypeName = val; + } + + @Override + public TypeName typeName() { + return DefaultTypeName.createFromTypeName(qualifierTypeName); + } + + @Override + public Optional value() { + return Optional.ofNullable(value); + } + + @Override + public Optional value(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Map values() { + if (value == null) { + return Map.of(); + } + return Map.of("value", value); + } + + /** + * Sets the qualifier value. + * + * @param val the qualifer value + */ + @SuppressWarnings("unused") + public void setValue(String val) { + this.value = val; + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ServiceTypeQualifiers.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ServiceTypeQualifiers.java new file mode 100644 index 00000000000..7f86c2c4c80 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/ServiceTypeQualifiers.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import io.helidon.pico.QualifierAndValue; + +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Used in {@link ExternalModuleCreatorMojo}. + */ +public class ServiceTypeQualifiers { + /** + * The service type name these qualifiers apply to. + */ + @Parameter(name = "serviceTypeName") + private String serviceTypeName; + + @Parameter(name = "qualifiers") + private List qualifiers; + + /** + * Default constructor. + */ + public ServiceTypeQualifiers() { + } + + /** + * @return the map representation for this instance + */ + Map> toMap() { + return Map.of(Objects.requireNonNull(serviceTypeName), new LinkedHashSet<>(Objects.requireNonNull(qualifiers))); + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TestApplicationCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TestApplicationCreatorMojo.java new file mode 100644 index 00000000000..18b2109fc37 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TestApplicationCreatorMojo.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.io.File; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.tools.ActivatorCreatorCodeGen; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Build; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; + +import static io.helidon.pico.maven.plugin.MavenPluginUtils.picoServices; +import static io.helidon.pico.tools.DefaultApplicationCreator.APPLICATION_NAME_SUFFIX; +import static io.helidon.pico.tools.DefaultApplicationCreator.NAME_PREFIX; +import static io.helidon.pico.tools.DefaultApplicationCreator.upperFirstChar; +import static io.helidon.pico.tools.ModuleUtils.toBasePath; +import static io.helidon.pico.tools.ModuleUtils.toSuggestedModuleName; + +/** + * A mojo wrapper to {@link io.helidon.pico.tools.spi.ApplicationCreator} for test specific types. + */ +@Mojo(name = "test-application-create", defaultPhase = LifecyclePhase.TEST_COMPILE, threadSafe = true, + requiresDependencyResolution = ResolutionScope.TEST) +@SuppressWarnings("unused") +public class TestApplicationCreatorMojo extends AbstractApplicationCreatorMojo { + + /** + * The classname to use for the Pico {@link io.helidon.pico.Application} test class. + * If not found the classname will be inferred. + */ + @Parameter(property = PicoServicesConfig.FQN + ".application.class.name", readonly = true + // note: the default value handling doesn't work here for "$$"!! + // defaultValue = DefaultApplicationCreator.APPLICATION_NAME + ) + private String className; + + /** + * Specify where to place generated source files created by annotation processing. + * Only applies to JDK 1.6+ + */ + @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations") + private File generatedTestSourcesDirectory; + + /** + * The directory where compiled test classes go. + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true) + private File testOutputDirectory; + + @Override + File getGeneratedSourceDirectory() { + return generatedTestSourcesDirectory; + } + + @Override + File getOutputDirectory() { + return testOutputDirectory; + } + + @Override + List getSourceRootPaths() { + return getTestSourceRootPaths(); + } + + @Override + LinkedHashSet getClasspathElements() { + MavenProject project = getProject(); + LinkedHashSet result = new LinkedHashSet<>(project.getTestArtifacts().size()); + result.add(new File(project.getBuild().getTestOutputDirectory()).toPath()); + for (Object a : project.getTestArtifacts()) { + result.add(((Artifact) a).getFile().toPath()); + } + result.addAll(super.getClasspathElements()); + return result; + } + + @Override + LinkedHashSet getModulepathElements() { + return getClasspathElements(); + } + + @Override + String getThisModuleName() { + Build build = getProject().getBuild(); + Path basePath = toBasePath(build.getTestSourceDirectory()); + String moduleName = toSuggestedModuleName(basePath, Path.of(build.getTestOutputDirectory()), true).orElseThrow(); + return moduleName; + } + + @Override + String getGeneratedClassName() { + return (className == null) ? NAME_PREFIX + "Test" + APPLICATION_NAME_SUFFIX : className; + } + + @Override + String getClassPrefixName() { + return upperFirstChar(ActivatorCreatorCodeGen.DEFAULT_TEST_CLASS_PREFIX_NAME); + } + + /** + * Default constructor. + */ + public TestApplicationCreatorMojo() { + } + + /** + * Excludes everything from source main scope. + */ + @Override + Set getServiceTypeNamesForExclusion() { + Set classPath = getSourceClasspathElements(); + + ClassLoader prev = Thread.currentThread().getContextClassLoader(); + URLClassLoader loader = ExecutableClassLoader.create(classPath, prev); + + try { + Thread.currentThread().setContextClassLoader(loader); + + PicoServices picoServices = picoServices(false); + assert (!picoServices.config().usesCompileTimeApplications()); + Services services = picoServices.services(); + + // retrieves all the services in the registry + List> allServices = services + .lookupAll(DefaultServiceInfoCriteria.builder().build(), false); + Set serviceTypeNames = toNames(allServices); + getLog().debug("excluding service type names: " + serviceTypeNames); + return serviceTypeNames; + } finally { + Thread.currentThread().setContextClassLoader(prev); + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TrafficCop.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TrafficCop.java new file mode 100644 index 00000000000..514bd382811 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/TrafficCop.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 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.maven.plugin; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.helidon.pico.tools.ToolsException; + +/** + * maven-plugins will be called on the same JVM on different mojo instances when maven is run in parallel mode. If you didn't know + * this then you know it now. ;-) + *

        + * Since pico is designed in traditional microprofile patterns - the JVM is the container for one app - this is a problem. So to + * compensate for this, we apply a traffic cop to effectively serialize access to the mojo execute() method so that only one can + * run at a time. The code is negligible in terms of performance, and still allows for parallel builds to occur. + */ +class TrafficCop { + private final Semaphore semaphore; + + TrafficCop() { + this.semaphore = new Semaphore(1); + } + + GreenLight waitForGreenLight() { + try { + return new GreenLight(semaphore); + } catch (InterruptedException e) { + throw new ToolsException("interrupted", e); + } + } + + static class GreenLight implements AutoCloseable { + private final Semaphore semaphore; + private final AtomicBoolean closed = new AtomicBoolean(false); + + private GreenLight(Semaphore semaphore) throws InterruptedException { + this.semaphore = semaphore; + semaphore.acquire(); + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + semaphore.release(); + } + } + } + +} diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/package-info.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/package-info.java new file mode 100644 index 00000000000..41c341a11f9 --- /dev/null +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Internal tooling for the pico maven plugin. + */ +package io.helidon.pico.maven.plugin; diff --git a/pico/maven-plugin/src/main/java/module-info.java b/pico/maven-plugin/src/main/java/module-info.java new file mode 100644 index 00000000000..9c91e2fd69d --- /dev/null +++ b/pico/maven-plugin/src/main/java/module-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 maven-plugin module. + */ +module io.helidon.pico.maven.plugin { + requires maven.plugin.annotations; + requires maven.plugin.api; + requires maven.project; + requires maven.artifact; + requires maven.model; + requires io.helidon.builder.config; + requires io.helidon.common; + requires io.helidon.config; + requires transitive io.helidon.pico.tools; + + exports io.helidon.pico.maven.plugin; + + uses io.helidon.pico.tools.spi.ActivatorCreator; + uses io.helidon.pico.tools.spi.ApplicationCreator; + uses io.helidon.pico.tools.spi.ExternalModuleCreator; +} diff --git a/pico/pico/README.md b/pico/pico/README.md deleted file mode 100644 index c9d39785dca..00000000000 --- a/pico/pico/README.md +++ /dev/null @@ -1,16 +0,0 @@ -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/src/main/java/io/helidon/pico/ActivationLogQuery.java b/pico/pico/src/main/java/io/helidon/pico/ActivationLogQuery.java deleted file mode 100644 index bf43e8aad96..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationLogQuery.java +++ /dev/null @@ -1,64 +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. - */ - -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/ActivationResult.java b/pico/pico/src/main/java/io/helidon/pico/ActivationResult.java deleted file mode 100644 index 5aea248deee..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/ActivationResult.java +++ /dev/null @@ -1,133 +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. - */ - -package io.helidon.pico; - -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.Future; - -import io.helidon.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/DefaultServiceInfo.java b/pico/pico/src/main/java/io/helidon/pico/DefaultServiceInfo.java deleted file mode 100644 index b1700e54ba4..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/DefaultServiceInfo.java +++ /dev/null @@ -1,554 +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. - */ - -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/InjectorOptions.java b/pico/pico/src/main/java/io/helidon/pico/InjectorOptions.java deleted file mode 100644 index 726619363a9..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/InjectorOptions.java +++ /dev/null @@ -1,75 +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. - */ - -package io.helidon.pico; - -import java.util.Optional; - -import io.helidon.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/pico/src/main/java/io/helidon/pico/PicoServices.java b/pico/pico/src/main/java/io/helidon/pico/PicoServices.java deleted file mode 100644 index c4942cd1527..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/PicoServices.java +++ /dev/null @@ -1,153 +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. - */ - -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()}. - *

        - * The global singleton instance is accessed via {@link #picoServices()}. Note that optionally one can provide a - * primordial bootstrap configuration to the {@code Pico} services provider. One must establish any bootstrap instance - * prior to the first call to {@link #picoServices()} as it will use a default configuration if not explicitly set. Once - * the bootstrap has been set it cannot be changed for the lifespan of the JVM. - */ -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(); - - /** - * Returns the {@link io.helidon.pico.Bootstrap} configuration instance that was used to initialize this instance. - * - * @return the bootstrap configuration instance - */ - Bootstrap bootstrap(); - - /** - * Retrieves any primordial bootstrap configuration that was already assigned via - * {@link #globalBootstrap()} (Bootstrap)}. - * - * @return the bootstrap primordial configuration already assigned - */ - static Optional globalBootstrap() { - return PicoServicesHolder.bootstrap(false); - } - - /** - * Sets the primordial bootstrap configuration that will supply {@link #picoServices()} during global - * singleton initialization. - * - * @param bootstrap the primordial global bootstrap configuration - */ - static void globalBootstrap(Bootstrap bootstrap) { - PicoServicesHolder.bootstrap(bootstrap); - } - - /** - * Get {@link PicoServices} instance if available. The highest {@link io.helidon.common.Weighted} service will be loaded - * and returned. Remember to optionally configure any primordial {@link #bootstrap()} configuration prior to the - * first call to get {@code PicoServices}. - * - * @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 deleted file mode 100644 index fa0e59dfd9e..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/PicoServicesConfig.java +++ /dev/null @@ -1,168 +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. - */ - -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 deleted file mode 100644 index ec7e6a685ea..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/PicoServicesHolder.java +++ /dev/null @@ -1,65 +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. - */ - -package io.helidon.pico; - -import java.util.Objects; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.concurrent.atomic.AtomicReference; - -import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.LazyValue; -import io.helidon.pico.spi.PicoServicesProvider; - -/** - * The holder for the globally active {@link PicoServices} singleton instance, as well as its associated - * {@link io.helidon.pico.Bootstrap} primordial configuration. - */ -class PicoServicesHolder { - private static final AtomicReference BOOTSTRAP = new AtomicReference<>(); - 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(p -> p.services(bootstrap(true).orElseThrow())); - } - - static void bootstrap(Bootstrap bootstrap) { - if (!BOOTSTRAP.compareAndSet(null, Objects.requireNonNull(bootstrap))) { - throw new IllegalStateException("bootstrap already set"); - } - } - - static Optional bootstrap(boolean assignIfNeeded) { - if (assignIfNeeded) { - BOOTSTRAP.compareAndSet(null, DefaultBootstrap.builder().build()); - } - - return Optional.ofNullable(BOOTSTRAP.get()); - } - -} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java deleted file mode 100644 index ee3f6cccaab..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java +++ /dev/null @@ -1,59 +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. - */ - -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/ServiceInfoCriteria.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInfoCriteria.java deleted file mode 100644 index 3053d95fb4c..00000000000 --- a/pico/pico/src/main/java/io/helidon/pico/ServiceInfoCriteria.java +++ /dev/null @@ -1,102 +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. - */ - -package io.helidon.pico; - -import java.util.Optional; -import java.util.Set; - -import io.helidon.builder.Builder; -import io.helidon.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/test/java/module-info.java b/pico/pico/src/test/java/module-info.java deleted file mode 100644 index 31323e985ba..00000000000 --- a/pico/pico/src/test/java/module-info.java +++ /dev/null @@ -1,37 +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. - */ - -/** - * 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; - requires transitive io.helidon.common.testing.junit5; - - 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 1fb5d8d38cd..73dc49927e9 100644 --- a/pico/pom.xml +++ b/pico/pom.xml @@ -39,27 +39,20 @@ 11 - - 1 + + true - types - builder-config - pico + api + configdriven tools - - - - - - - - - - - + processor + maven-plugin + testing + services + tests diff --git a/pico/processor/README.md b/pico/processor/README.md new file mode 100644 index 00000000000..d24de5c9edd --- /dev/null +++ b/pico/processor/README.md @@ -0,0 +1,23 @@ +# pico-processor + +This module provides *compile-time only* annotation processing, and is designed to look for javax/jakarta inject type annotations to then code-generate supporting DI activator source artifacts in support for your injection points and dependency model. It leverages the [tools module](../tools/README.md) to perform the necessary code generation when Pico annotations are found. + +## Usage + +In your pom.xml, add this plugin to be run as part of the compilation phase: +```pom.xml + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + + +``` diff --git a/pico/processor/etc/spotbugs/exclude.xml b/pico/processor/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..ce4d3c2a536 --- /dev/null +++ b/pico/processor/etc/spotbugs/exclude.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/pico/types/pom.xml b/pico/processor/pom.xml similarity index 76% rename from pico/types/pom.xml rename to pico/processor/pom.xml index 3186f5926e6..1316255e2d7 100644 --- a/pico/types/pom.xml +++ b/pico/processor/pom.xml @@ -1,7 +1,7 @@ + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-services + Helidon Pico Runtime Services + + + + io.helidon.pico + helidon-pico-api + + + jakarta.inject + jakarta.inject-api + compile + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.builder + helidon-builder-processor + provided + + + io.helidon.config + helidon-config + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + + + diff --git a/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java new file mode 100644 index 00000000000..b14e4ddbd87 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/AbstractServiceProvider.java @@ -0,0 +1,1330 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Arrays; +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.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.pico.ActivationLog; +import io.helidon.pico.ActivationPhaseReceiver; +import io.helidon.pico.ActivationRequest; +import io.helidon.pico.ActivationResult; +import io.helidon.pico.ActivationStatus; +import io.helidon.pico.Activator; +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DeActivationRequest; +import io.helidon.pico.DeActivator; +import io.helidon.pico.DefaultActivationLogEntry; +import io.helidon.pico.DefaultActivationResult; +import io.helidon.pico.DefaultDependenciesInfo; +import io.helidon.pico.DefaultInjectionPointInfo; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.Event; +import io.helidon.pico.InjectionException; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.InjectionPointProvider; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServiceProviderException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.PostConstructMethod; +import io.helidon.pico.PreDestroyMethod; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceInjectionPlanBinder; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.spi.InjectionResolver; + +import jakarta.inject.Provider; + +import static io.helidon.pico.services.ServiceUtils.isQualifiedInjectionTarget; + +/** + * Abstract base implementation for {@link io.helidon.pico.ServiceProviderBindable}, which represents the basics for regular + * Singleton, ApplicationScoped, Provider, and ServiceProvider based managed services. All Pico code-generated services will + * extend from this abstract base class. + * + * @param the type of the service this provider manages + */ +public abstract class AbstractServiceProvider + implements ServiceProviderBindable, + Activator, + DeActivator, + ActivationPhaseReceiver, + Resettable { + static final DependenciesInfo NO_DEPS = DefaultDependenciesInfo.builder().build(); + private static final System.Logger LOGGER = System.getLogger(AbstractServiceProvider.class.getName()); + + private final Semaphore activationSemaphore = new Semaphore(1); + private final AtomicReference serviceRef = new AtomicReference<>(); + private Phase phase; + private long lastActivationThreadId; + private PicoServices picoServices; + private ActivationLog log; + private ServiceInfo serviceInfo; + private DependenciesInfo dependencies; + private Map injectionPlan; + private ServiceProvider interceptor; + + /** + * The default constructor. + */ + protected AbstractServiceProvider() { + this.phase = Phase.INIT; + } + + /** + * Constructor. + * + * @param instance the managed service instance + * @param phase the current phase + * @param serviceInfo the service info + * @param picoServices the pico services instance + */ + protected AbstractServiceProvider(T instance, + Phase phase, + ServiceInfo serviceInfo, + PicoServices picoServices) { + this(); + if (instance != null) { + this.serviceRef.set(instance); + this.phase = (phase != null) ? phase : Phase.ACTIVE; + } + this.serviceInfo = DefaultServiceInfo.toBuilder(serviceInfo).build(); + this.picoServices = Objects.requireNonNull(picoServices); + this.log = picoServices.activationLog().orElseThrow(); + onInitialized(); + } + + /** + * Will test and downcast the passed service provider to an instance of + * {@link io.helidon.pico.services.AbstractServiceProvider}. + * + * @param sp the service provider + * @param expected is the result expected to be present + * @param the managed service type + * @return the abstract service provider + */ + @SuppressWarnings("unchecked") + public static Optional> toAbstractServiceProvider(ServiceProvider sp, + boolean expected) { + if (!(sp instanceof AbstractServiceProvider)) { + if (expected) { + throw new IllegalStateException("expected provider to be of type " + AbstractServiceProvider.class.getName()); + } + return Optional.empty(); + } + return Optional.of((AbstractServiceProvider) sp); + } + + @Override + public Optional activator() { + return Optional.of(this); + } + + @Override + public Optional deActivator() { + return Optional.of(this); + } + + @Override + public Optional> serviceProviderBindable() { + return Optional.of(this); + } + + @Override + public boolean isProvider() { + return false; + } + + /** + * Identifies whether the implementation was custom written and not code generated. We assume by default this is part + * of code-generation, and the default is to return false. + * + * @return true if a custom, user-supplied implementation (rare) + */ + public boolean isCustom() { + return false; + } + + @Override + public ServiceInfo serviceInfo() { + return Objects.requireNonNull(serviceInfo, getClass().getName() + " should have been initialized."); + } + + @Override + public DependenciesInfo dependencies() { + return (dependencies == null) ? NO_DEPS : dependencies; + } + + @Override + public double weight() { + return serviceInfo().realizedWeight(); + } + + @Override + public Phase currentActivationPhase() { + return phase; + } + + /** + * Used to access the current pico services instance assigned to this service provider. + * + * @return the pico services assigned to this service provider + */ + public PicoServices picoServices() { + return Objects.requireNonNull(picoServices, description() + ": picoServices should have been previously set"); + } + + @Override + public void picoServices(Optional picoServices) { + if (picoServices.isPresent() + || serviceRef.get() != null) { + PicoServices current = this.picoServices; + if (picoServices.orElse(null) == current) { + return; + } + + if (current != null) { + if (current.config().permitsDynamic()) { + reset(true); + } else { + throw alreadyInitialized(); + } + } + } + + this.picoServices = picoServices.orElse(null); + this.phase = Phase.INIT; + if (this.picoServices != null) { + onInitialized(); + } + } + + @Override + public void moduleName(String moduleName) { + Objects.requireNonNull(moduleName); + ServiceInfo serviceInfo = serviceInfo(); + String moduleInfoName = serviceInfo.moduleName().orElse(null); + if (!Objects.equals(moduleInfoName, moduleName)) { + if (moduleInfoName != null) { + throw alreadyInitialized(); + } + this.serviceInfo = DefaultServiceInfo.toBuilder(serviceInfo).moduleName(moduleName).build(); + } + } + + @Override + public Optional> interceptor() { + return Optional.ofNullable(interceptor); + } + + @Override + public void interceptor(ServiceProvider interceptor) { + Objects.requireNonNull(interceptor); + if (this.interceptor != null || activationSemaphore.availablePermits() == 0 || phase != Phase.INIT) { + throw alreadyInitialized(); + } + this.interceptor = interceptor; + } + + @Override + public int hashCode() { + return System.identityHashCode(serviceInfo.serviceTypeName()); + } + + @Override + public boolean equals(Object another) { + return (another instanceof ServiceProvider) + && id().equals(((ServiceProvider) another).id()) + && serviceInfo().equals(((ServiceProvider) another).serviceInfo()); + } + + @Override + public String toString() { + return description(); + } + + @Override + public String description() { + return name(true) + identitySuffix() + ":" + currentActivationPhase(); + } + + @Override + public String id() { + return identityPrefix() + name(false) + identitySuffix(); + } + + /** + * The name assigned to this provider. Simple names are not unique. + * + * @param simple flag to indicate simple name usage + * @return this name assigned to this provider + */ + public String name(boolean simple) { + String name = serviceInfo().serviceTypeName(); + return (simple) ? DefaultTypeName.createFromTypeName(name).className() : name; + } + + @SuppressWarnings("unchecked") + @Override + public Optional first(ContextualServiceQuery ctx) { + T serviceOrProvider = maybeActivate(ctx).orElse(null); + + try { + if (isProvider()) { + T instance; + + if (serviceOrProvider instanceof InjectionPointProvider) { + instance = ((InjectionPointProvider) serviceOrProvider).first(ctx).orElse(null); + } else if (serviceOrProvider instanceof Provider) { + instance = ((Provider) serviceOrProvider).get(); + if (ctx.expected() && instance == null) { + throw expectedQualifiedServiceError(ctx); + } + } else { + instance = NonSingletonServiceProvider.createAndActivate(this); + } + + return Optional.ofNullable(instance); + } + } catch (InjectionException ie) { + throw ie; + } catch (Throwable t) { + logger().log(System.Logger.Level.ERROR, "unable to activate: " + getClass().getName(), t); + throw unableToActivate(t); + } + + return Optional.ofNullable(serviceOrProvider); + } + + @SuppressWarnings("unchecked") + @Override + public List list(ContextualServiceQuery ctx) { + T serviceProvider = maybeActivate(ctx).orElse(null); + + try { + if (isProvider()) { + List instances = null; + + if (serviceProvider instanceof InjectionPointProvider) { + instances = ((InjectionPointProvider) serviceProvider).list(ctx); + } else if (serviceProvider instanceof Provider) { + T instance = ((Provider) serviceProvider).get(); + if (ctx.expected() && instance == null) { + throw expectedQualifiedServiceError(ctx); + } + if (instance != null) { + if (instance instanceof List) { + instances = (List) instance; + } else { + instances = List.of(instance); + } + } + } else { + T instance = NonSingletonServiceProvider.createAndActivate(this); + instances = List.of(instance); + } + + return instances; + } + } catch (InjectionException ie) { + throw ie; + } catch (Throwable t) { + throw unableToActivate(t); + } + + return (serviceProvider != null) ? List.of(serviceProvider) : List.of(); + } + + @Override + public ActivationResult activate(ActivationRequest req) { + if (isAlreadyAtTargetPhase(req.targetPhase())) { + return ActivationResult.createSuccess(this); + } + + LogEntryAndResult logEntryAndResult = preambleActivate(req); + DefaultActivationResult.Builder res = logEntryAndResult.activationResult; + + // if we get here then we own the semaphore for activation... + try { + if (res.targetActivationPhase().ordinal() >= Phase.ACTIVATION_STARTING.ordinal() + && (Phase.INIT == res.finishingActivationPhase() + || Phase.PENDING == res.finishingActivationPhase() + || Phase.ACTIVATION_STARTING == res.finishingActivationPhase() + || Phase.DESTROYED == res.finishingActivationPhase())) { + doStartingLifecycle(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.GATHERING_DEPENDENCIES.ordinal() + && (Phase.ACTIVATION_STARTING == res.finishingActivationPhase())) { + doGatheringDependencies(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.CONSTRUCTING.ordinal() + && (Phase.GATHERING_DEPENDENCIES == res.finishingActivationPhase())) { + doConstructing(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.INJECTING.ordinal() + && (Phase.CONSTRUCTING == res.finishingActivationPhase())) { + doInjecting(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.POST_CONSTRUCTING.ordinal() + && (Phase.INJECTING == res.finishingActivationPhase())) { + doPostConstructing(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.ACTIVATION_FINISHING.ordinal() + && (Phase.POST_CONSTRUCTING == res.finishingActivationPhase())) { + doActivationFinishing(logEntryAndResult); + } + if (res.targetActivationPhase().ordinal() >= Phase.ACTIVE.ordinal() + && (Phase.ACTIVATION_FINISHING == res.finishingActivationPhase())) { + doActivationActive(logEntryAndResult); + } + + onFinished(logEntryAndResult); + } catch (Throwable t) { + onFailedFinish(logEntryAndResult, t, req.throwIfError()); + } finally { + this.lastActivationThreadId = 0; + activationSemaphore.release(); + } + + return logEntryAndResult.activationResult.build(); + } + + @Override + public void onPhaseEvent(Event event, + Phase phase) { + // NOP + } + + /** + * Called at startup to establish the injection plan as an alternative to gathering it dynamically. + */ + @Override + public Optional injectionPlanBinder() { + if (dependencies == null) { + dependencies(dependencies()); + + if (dependencies == null) { + // couldn't accept our suggested dependencies + return Optional.empty(); + } + } + + if (injectionPlan != null) { + logger().log(System.Logger.Level.WARNING, + "this service provider already has an injection plan (which is unusual here): " + this); + } + + ConcurrentHashMap idToIpInfo = new ConcurrentHashMap<>(); + dependencies.allDependencies() + .forEach(dep -> + dep.injectionPointDependencies().forEach(ipDep -> { + String id = ipDep.id(); + InjectionPointInfo prev = idToIpInfo.put(id, ipDep); + if (prev != null + && !prev.equals(ipDep) + && !prev.dependencyToServiceInfo().equals(ipDep.dependencyToServiceInfo())) { + logMultiDefInjectionNote(id, prev, ipDep); + } + })); + + ConcurrentHashMap injectionPlan = new ConcurrentHashMap<>(); + AbstractServiceProvider self = AbstractServiceProvider.this; + ServiceInjectionPlanBinder.Binder result = new ServiceInjectionPlanBinder.Binder() { + private InjectionPointInfo ipInfo; + + @Override + public ServiceInjectionPlanBinder.Binder bind(String id, + ServiceProvider serviceProvider) { + PicoInjectionPlan plan = createBuilder(id) + .injectionPointQualifiedServiceProviders(List.of(bind(serviceProvider))) + .build(); + Object prev = injectionPlan.put(id, plan); + assert (prev == null); + return this; + } + + @Override + public ServiceInjectionPlanBinder.Binder bindMany(String id, + ServiceProvider... serviceProviders) { + PicoInjectionPlan plan = createBuilder(id) + .injectionPointQualifiedServiceProviders(bind(Arrays.asList(serviceProviders))) + .build(); + Object prev = injectionPlan.put(id, plan); + assert (prev == null); + return this; + } + + @Override + public ServiceInjectionPlanBinder.Binder bindVoid(String id) { + return bind(id, VoidServiceProvider.INSTANCE); + } + + @Override + public ServiceInjectionPlanBinder.Binder resolvedBind(String id, + Class serviceType) { + try { + InjectionResolver resolver = (InjectionResolver) AbstractServiceProvider.this; + ServiceInfoCriteria serviceInfo = DefaultServiceInfoCriteria.builder() + .serviceTypeName(serviceType.getName()) + .build(); + InjectionPointInfo ipInfo = DefaultInjectionPointInfo.builder() + .id(id) + .dependencyToServiceInfo(serviceInfo); + // .build(); + Object resolved = Objects.requireNonNull( + resolver.resolve(ipInfo, picoServices(), AbstractServiceProvider.this, false)); + PicoInjectionPlan plan = createBuilder(id) + .unqualifiedProviders(List.of(resolved)) + .resolved(false) + .build(); + Object prev = injectionPlan.put(id, plan); + assert (prev == null); + return this; + } catch (Exception e) { + throw new PicoServiceProviderException("failed to process: " + id, e, AbstractServiceProvider.this); + } + } + + @Override + public void commit() { + if (!idToIpInfo.isEmpty()) { + throw new InjectionException("missing injection bindings for " + + idToIpInfo + " in " + + this, null, self); + } + + if ((self.injectionPlan != null) && !self.injectionPlan.equals(injectionPlan)) { + throw new InjectionException("injection plan has already been bound for " + + this, null, self); + } + self.injectionPlan = injectionPlan; + } + + private ServiceProvider bind(ServiceProvider rawSp) { + assert (!(rawSp instanceof BoundedServiceProvider)) : rawSp; + return BoundedServiceProvider.create(rawSp, ipInfo); + } + + private List> bind(List> rawList) { + return rawList.stream().map(this::bind).collect(Collectors.toList()); + } + + private InjectionPointInfo safeGetIpInfo(String id) { + InjectionPointInfo ipInfo = idToIpInfo.remove(id); + if (ipInfo == null) { + throw new InjectionException("expected to find a dependency for '" + id + "' from " + + this + " in " + idToIpInfo, null, self); + } + return ipInfo; + } + + private DefaultPicoInjectionPlan.Builder createBuilder(String id) { + ipInfo = safeGetIpInfo(id); + return DefaultPicoInjectionPlan.builder() + .injectionPointInfo(ipInfo) + .serviceProvider(self); + } + }; + + return Optional.of(result); + } + + /** + * Get or Create the injection plan. + * + * @param resolveIps true if the injection points should also be activated/resolved. + * @return the injection plan + */ + public Map getOrCreateInjectionPlan(boolean resolveIps) { + if (this.injectionPlan != null) { + return this.injectionPlan; + } + + if (this.dependencies == null) { + dependencies(dependencies()); + } + + Map plan = + DefaultInjectionPlans.createInjectionPlans(picoServices(), this, dependencies, resolveIps, logger()); + assert (this.injectionPlan == null); + this.injectionPlan = Objects.requireNonNull(plan); + + return this.injectionPlan; + } + + @Override + public boolean reset(boolean deep) { + Object service = serviceRef.get(); + boolean result = false; + boolean didAcquire = false; + try { + didAcquire = activationSemaphore.tryAcquire(1, TimeUnit.MILLISECONDS); + + if (service != null) { + logger().log(System.Logger.Level.INFO, "resetting " + this); + if (deep && service instanceof Resettable) { + try { + if (((Resettable) service).reset(deep)) { + result = true; + } + } catch (Throwable t) { + logger().log(System.Logger.Level.WARNING, "unable to reset: " + this, t); // eat it + } + } + } + + if (deep) { + injectionPlan = null; + interceptor = null; + picoServices = null; + serviceRef.set(null); + phase = Phase.INIT; + + result = true; + } + } catch (Exception e) { + if (didAcquire) { + throw new PicoServiceProviderException("unable to reset", e, this); + } else { + throw new PicoServiceProviderException("unable to reset during activation", e, this); + } + } finally { + if (didAcquire) { + activationSemaphore.release(); + } + } + + return result; + } + + @Override + public Optional postConstructMethod() { + return Optional.empty(); + } + + @Override + public Optional preDestroyMethod() { + return Optional.empty(); + } + + @Override + public ActivationResult deactivate(DeActivationRequest req) { + if (!currentActivationPhase().eligibleForDeactivation()) { + return ActivationResult.createSuccess(this); + } + + PicoServices picoServices = picoServices(); + PicoServicesConfig cfg = picoServices.config(); + + // if we are here then we are not yet at the ultimate target phase, and we either have to activate or deactivate + LogEntryAndResult logEntryAndResult = createLogEntryAndResult(Phase.DESTROYED); + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.PRE_DESTROYING); + + boolean didAcquire = false; + try { + // let's wait a bit on the semaphore until we read timeout (probably detecting a deadlock situation) + if (!activationSemaphore.tryAcquire(cfg.activationDeadlockDetectionTimeoutMillis(), TimeUnit.MILLISECONDS)) { + // if we couldn't grab the semaphore than we (or someone else) is busy activating this services, or + // we deadlocked. + InjectionException e = timedOutDeActivationInjectionError(logEntryAndResult.logEntry); + onFailedFinish(logEntryAndResult, e, req.throwIfError()); + return logEntryAndResult.activationResult.build(); + } + didAcquire = true; + + // if we made it to here then we "own" the semaphore and the subsequent activation steps + this.lastActivationThreadId = Thread.currentThread().getId(); + + doPreDestroying(logEntryAndResult); + if (Phase.PRE_DESTROYING == logEntryAndResult.activationResult.finishingActivationPhase()) { + doDestroying(logEntryAndResult); + } + onFinished(logEntryAndResult); + } catch (Throwable t) { + InjectionException e = interruptedPreActivationInjectionError(logEntryAndResult.logEntry, t); + onFailedFinish(logEntryAndResult, e, req.throwIfError()); + } finally { + if (didAcquire) { + activationSemaphore.release(); + } + onFinalShutdown(); + } + + return logEntryAndResult.activationResult.build(); + } + + /** + * Called on the final leg of the shutdown sequence. + */ + protected void onFinalShutdown() { + this.lastActivationThreadId = 0; + this.injectionPlan = null; + this.phase = Phase.DESTROYED; + this.serviceRef.set(null); + this.picoServices = null; + this.log = null; + } + + /** + * Called on a failed finish of activation. + * + * @param logEntryAndResult the log entry holding the result + * @param t the error that was observed + * @param throwOnError the flag indicating whether we should throw on error + * @see #onFinished(io.helidon.pico.services.AbstractServiceProvider.LogEntryAndResult) + */ + protected void onFailedFinish(LogEntryAndResult logEntryAndResult, + Throwable t, + boolean throwOnError) { + this.lastActivationThreadId = 0; + onFailedFinish(logEntryAndResult, t, throwOnError, activationLog()); + } + + /** + * The logger. + * + * @return the logger + */ + protected System.Logger logger() { + return LOGGER; + } + + /** + * Sets the service info that describes the managed service that is assigned. + * + * @param serviceInfo the service info + */ + protected void serviceInfo(ServiceInfo serviceInfo) { + Objects.requireNonNull(serviceInfo); + if (this.picoServices != null && this.serviceInfo != null) { + throw alreadyInitialized(); + } + this.serviceInfo = serviceInfo; + } + + /** + * Used to set the dependencies from this service provider. + * + * @param dependencies the dependencies from this service provider + */ + protected void dependencies(DependenciesInfo dependencies) { + Objects.requireNonNull(dependencies); + if (this.dependencies != null) { + throw alreadyInitialized(); + } + this.dependencies = dependencies; + } + + /** + * Returns true if the current activation phase has reached the given target phase. + * + * @param targetPhase the target phase + * @return true if the targetPhase has been reached + */ + protected boolean isAlreadyAtTargetPhase(Phase targetPhase) { + Objects.requireNonNull(targetPhase); + return (currentActivationPhase() == targetPhase); + } + + /** + * The identity prefix, or empty string if there is no prefix. + * + * @return the identity prefix + */ + protected String identityPrefix() { + return ""; + } + + /** + * The identity suffix, or empty string if there is no suffix. + * + * @return the identity suffix + */ + protected String identitySuffix() { + return ""; + } + + /** + * Returns the managed service this provider has (or is in the process of) activating. + * + * @return the service we are managing lifecycle for + */ + protected Optional serviceRef() { + return Optional.ofNullable(serviceRef.get()); + } + + /** + * Returns the activation log. + * + * @return the activation log + */ + protected Optional activationLog() { + if (log == null && picoServices != null) { + log = picoServices.activationLog().orElse(DefaultActivationLog.createUnretainedLog(logger())); + } + return Optional.ofNullable(log); + } + + /** + * Called by the generated code when it is attempting to resolve a specific injection point dependency by id. + * + * @param deps the entire map of resolved dependencies + * @param id the id of the dependency to lookup + * @param the type of the dependency + * @return the resolved object + */ + protected T get(Map deps, String id) { + return Objects.requireNonNull(deps.get(id), "'" + id + "' expected to have been found in: " + deps.keySet()); + } + + /** + * Will trigger an activation if the managed service is not yet active. + * + * @param ctx the context that triggered the activation + * @return the result of the activation + */ + protected Optional maybeActivate(ContextualServiceQuery ctx) { + Objects.requireNonNull(ctx); + + try { + T serviceOrProvider = serviceRef.get(); + + if (serviceOrProvider == null + || Phase.ACTIVE != currentActivationPhase()) { + ActivationRequest req = PicoServices.createDefaultActivationRequest(); + ActivationResult res = activate(req); + if (res.failure()) { + if (ctx.expected()) { + throw activationFailed(res); + } + return Optional.empty(); + } + + serviceOrProvider = serviceRef.get(); + } + + if (ctx.expected() + && serviceOrProvider == null) { + throw managedServiceInstanceShouldHaveBeenSetException(); + } + + return Optional.ofNullable(serviceOrProvider); + } catch (InjectionException ie) { + throw ie; + } catch (Throwable t) { + throw unableToActivate(t); + } + } + + /** + * Called on a successful finish of activation. + * + * @param logEntryAndResult the record holding the result + * @see #onFailedFinish(io.helidon.pico.services.AbstractServiceProvider.LogEntryAndResult, Throwable, boolean) + */ + protected void onFinished(LogEntryAndResult logEntryAndResult) { + // NOP + } + + /** + * Called during construction phase. + * + * @param logEntryAndResult the record that holds the results + */ + protected void doConstructing(LogEntryAndResult logEntryAndResult) { + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.CONSTRUCTING); + + Map deps = logEntryAndResult.activationResult.resolvedDependencies(); + serviceRef(createServiceProvider(deps)); + + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + + /** + * Creates the service with the supplied resolved dependencies, key'ed by each injection point id. + * + * @param resolvedDeps the resolved dependencies + * @return the newly created managed service + * @throws InjectionException since this is a base method for what is expected to be a code-generated derived + * {@link Activator} then this method will throw an exception if the derived class does not + * implement this method as it + * normally should + */ + protected T createServiceProvider(Map resolvedDeps) { + InjectionException e = new InjectionException("Don't know how to create an instance of " + serviceInfo() + + ". Was the Activator generated?", this); + activationLog().ifPresent(e::activationLog); + throw e; + } + + /** + * Used to control the order of injection. Jsr-330 is particular about this. + * + * @return the order of injection + */ + protected List serviceTypeInjectionOrder() { + return Collections.singletonList(serviceInfo.serviceTypeName()); + } + + /** + * Called during the injection of fields. + * + * @param target the target + * @param deps the dependencies + * @param injections the injections + * @param forServiceType the service type + */ + protected void doInjectingFields(Object target, + Map deps, + Set injections, + String forServiceType) { + // NOP; meant to be overridden + } + + /** + * Called during the injection of methods. + * + * @param target the target + * @param deps the dependencies + * @param injections the injections + * @param forServiceType the service type + */ + protected void doInjectingMethods(Object target, + Map deps, + Set injections, + String forServiceType) { + // NOP; meant to be overridden + } + + /** + * Called during the {@link io.helidon.pico.PostConstructMethod} process. + * + * @param logEntryAndResult the entry holding the result + */ + protected void doPostConstructing(LogEntryAndResult logEntryAndResult) { + Optional postConstruct = postConstructMethod(); + if (postConstruct.isPresent()) { + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.POST_CONSTRUCTING); + postConstruct.get().postConstruct(); + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } else { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.POST_CONSTRUCTING); + } + } + + /** + * Called during the {@link io.helidon.pico.PreDestroyMethod} process. + * + * @param logEntryAndResult the entry holding the result + */ + protected void doPreDestroying(LogEntryAndResult logEntryAndResult) { + Optional preDestroyMethod = preDestroyMethod(); + if (preDestroyMethod.isEmpty()) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.PRE_DESTROYING); + } else { + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.PRE_DESTROYING); + preDestroyMethod.get().preDestroy(); + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + } + + /** + * Called after the {@link io.helidon.pico.PreDestroyMethod} process. + * + * @param logEntryAndResult the entry holding the result + */ + protected void doDestroying(LogEntryAndResult logEntryAndResult) { + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.DESTROYED); + logEntryAndResult.activationResult.wasResolved(false); + logEntryAndResult.activationResult.resolvedDependencies(Map.of()); + serviceRef(null); + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + + /** + * Creates an injection exception appropriate when there are no matching qualified services for the context provided. + * + * @param ctx the context + * @return the injection exception + */ + protected InjectionException expectedQualifiedServiceError(ContextualServiceQuery ctx) { + InjectionException e = new InjectionException("expected to return a non-null instance for: " + ctx.injectionPointInfo() + + "; with criteria matching: " + ctx.serviceInfoCriteria(), this); + activationLog().ifPresent(e::activationLog); + return e; + } + + /** + * Creates a log entry result based upon the target phase provided. + * + * @param targetPhase the target phase + * @return a new log entry and result record + */ + protected LogEntryAndResult createLogEntryAndResult(Phase targetPhase) { + Phase currentPhase = currentActivationPhase(); + DefaultActivationResult.Builder activationResult = DefaultActivationResult.builder() + .serviceProvider(this) + .startingActivationPhase(currentPhase) + .finishingActivationPhase(currentPhase) + .targetActivationPhase(targetPhase); + DefaultActivationLogEntry.Builder logEntry = DefaultActivationLogEntry.builder() + .serviceProvider(this) + .event(Event.STARTING) + .threadId(Thread.currentThread().getId()) + .activationResult(activationResult); + return new LogEntryAndResult(logEntry, activationResult); + } + + /** + * Starts transitioning to a new phase. + * + * @param logEntryAndResult the record that will hold the state of the transition + * @param newPhase the target new phase + */ + protected void startTransitionCurrentActivationPhase(LogEntryAndResult logEntryAndResult, + Phase newPhase) { + Objects.requireNonNull(logEntryAndResult); + Objects.requireNonNull(newPhase); + logEntryAndResult.activationResult + .finishingActivationPhase(newPhase); + this.phase = newPhase; + logEntryAndResult.logEntry + .event(Event.STARTING) + .activationResult(logEntryAndResult.activationResult.build()); + activationLog().ifPresent(log -> log.record(logEntryAndResult.logEntry.build())); + onPhaseEvent(Event.STARTING, this.phase); + } + + Map resolveDependencies(Map mutablePlans) { + Map result = new LinkedHashMap<>(); + + Map.copyOf(mutablePlans).forEach((key, value) -> { + Object resolved; + if (value.wasResolved()) { + resolved = value.resolved(); + result.put(key, resolveOptional(value, resolved)); + } else { + List> serviceProviders = value.injectionPointQualifiedServiceProviders(); + serviceProviders = (serviceProviders == null) + ? List.of() + : Collections.unmodifiableList(serviceProviders); + if (serviceProviders.isEmpty() + && !value.unqualifiedProviders().isEmpty()) { + resolved = List.of(); // deferred + } else { + resolved = DefaultInjectionPlans.resolve(this, value.injectionPointInfo(), serviceProviders, logger()); + } + result.put(key, resolveOptional(value, resolved)); + } + + if (value.resolved().isEmpty()) { + // update the original plans map to properly reflect the resolved value + mutablePlans.put(key, DefaultPicoInjectionPlan.toBuilder(value) + .wasResolved(true) + .resolved(Optional.ofNullable(resolved)) + .build()); + } + }); + + return result; + } + + void serviceRef(T instance) { + serviceRef.set(instance); + } + + void onFailedFinish(LogEntryAndResult logEntryAndResult, + Throwable t, + boolean throwOnError, + Optional log) { + InjectionException e; + + DefaultActivationLogEntry.Builder res = logEntryAndResult.logEntry; + Throwable prev = res.error().orElse(null); + if (prev == null || !(t instanceof InjectionException)) { + String msg = (t != null && t.getMessage() != null) ? t.getMessage() : "failed to complete operation"; + e = new InjectionException(msg, t, this); + log.ifPresent(e::activationLog); + } else { + e = (InjectionException) t; + } + + res.error(e); + logEntryAndResult.activationResult.finishingStatus(ActivationStatus.FAILURE); + + if (throwOnError) { + throw e; + } + } + + void startAndFinishTransitionCurrentActivationPhase(LogEntryAndResult logEntryAndResult, + Phase newPhase) { + startTransitionCurrentActivationPhase(logEntryAndResult, newPhase); + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + + void finishedTransitionCurrentActivationPhase(LogEntryAndResult logEntryAndResult) { + logEntryAndResult.logEntry + .event(Event.FINISHED) + .activationResult(logEntryAndResult.activationResult.build()); + ActivationLog log = activationLog().orElse(null); + if (log != null) { + log.record(logEntryAndResult.logEntry.build()); + } + onPhaseEvent(Event.FINISHED, this.phase); + } + + // if we are here then we are not yet at the ultimate target phase, and we either have to activate or deactivate + private LogEntryAndResult preambleActivate(ActivationRequest req) { + assert (picoServices != null) : "not initialized"; + + LogEntryAndResult logEntryAndResult = createLogEntryAndResult(req.targetPhase()); + req.injectionPoint().ifPresent(logEntryAndResult.logEntry::injectionPoint); + Phase startingPhase = req.startingPhase().orElse(Phase.PENDING); + startTransitionCurrentActivationPhase(logEntryAndResult, startingPhase); + + // fail fast if we are in a recursive situation on this thread... + if (logEntryAndResult.logEntry.threadId() == lastActivationThreadId && lastActivationThreadId > 0) { + onFailedFinish(logEntryAndResult, recursiveActivationInjectionError(logEntryAndResult.logEntry), req.throwIfError()); + return logEntryAndResult; + } + + PicoServicesConfig cfg = picoServices.config(); + boolean didAcquire = false; + try { + // let's wait a bit on the semaphore until we read timeout (probably detecting a deadlock situation) + if (!activationSemaphore.tryAcquire(cfg.activationDeadlockDetectionTimeoutMillis(), TimeUnit.MILLISECONDS)) { + // if we couldn't get semaphore than we (or someone else) is busy activating this services, or we deadlocked + onFailedFinish(logEntryAndResult, + timedOutActivationInjectionError(logEntryAndResult.logEntry), + req.throwIfError()); + return logEntryAndResult; + } + didAcquire = true; + + // if we made it to here then we "own" the semaphore and the subsequent activation steps... + lastActivationThreadId = Thread.currentThread().getId(); + logEntryAndResult.logEntry.threadId(lastActivationThreadId); + + if (logEntryAndResult.activationResult.finished()) { + didAcquire = false; + activationSemaphore.release(); + } + + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } catch (Throwable t) { + this.lastActivationThreadId = 0; + if (didAcquire) { + activationSemaphore.release(); + } + + InjectionException e = interruptedPreActivationInjectionError(logEntryAndResult.logEntry, t); + onFailedFinish(logEntryAndResult, e, req.throwIfError()); + } + + return logEntryAndResult; + } + + private void onInitialized() { + if (logger().isLoggable(System.Logger.Level.DEBUG)) { + logger().log(System.Logger.Level.DEBUG, this + " initialized."); + } + } + + private void doStartingLifecycle(LogEntryAndResult logEntryAndResult) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVATION_STARTING); + } + + private void doGatheringDependencies(LogEntryAndResult logEntryAndResult) { + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.GATHERING_DEPENDENCIES); + + Map plans = Objects.requireNonNull(getOrCreateInjectionPlan(false)); + Map deps = resolveDependencies(plans); + if (!deps.isEmpty()) { + logEntryAndResult.activationResult.resolvedDependencies(deps); + } + logEntryAndResult.activationResult.injectionPlans(plans); + + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + + @SuppressWarnings("unchecked") + private Object resolveOptional(PicoInjectionPlan plan, + Object resolved) { + if (!plan.injectionPointInfo().optionalWrapped() && resolved instanceof Optional) { + return ((Optional) resolved).orElse(null); + } + return resolved; + } + + private void doInjecting(LogEntryAndResult logEntryAndResult) { + if (!isQualifiedInjectionTarget(this)) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.INJECTING); + return; + } + + Map deps = logEntryAndResult.activationResult.resolvedDependencies(); + if (deps == null || deps.isEmpty()) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.INJECTING); + return; + } + + startTransitionCurrentActivationPhase(logEntryAndResult, Phase.INJECTING); + + T target = Objects.requireNonNull(serviceRef.get()); + List serviceTypeOrdering = serviceTypeInjectionOrder(); + LinkedHashSet injections = new LinkedHashSet<>(); + serviceTypeOrdering.forEach((forServiceType) -> { + try { + doInjectingFields(target, deps, injections, forServiceType); + doInjectingMethods(target, deps, injections, forServiceType); + } catch (Throwable t) { + throw new InjectionException("failed to activate/inject: " + this + + "; dependency map was: " + deps, t, this); + } + }); + + finishedTransitionCurrentActivationPhase(logEntryAndResult); + } + + private void doActivationFinishing(LogEntryAndResult logEntryAndResult) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVATION_FINISHING); + } + + private void doActivationActive(LogEntryAndResult logEntryAndResult) { + startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVE); + } + + private InjectionException recursiveActivationInjectionError(DefaultActivationLogEntry.Builder entry) { + ServiceProvider targetServiceProvider = entry.serviceProvider().orElseThrow(); + InjectionException e = new InjectionException("circular dependency found during activation of " + targetServiceProvider, + targetServiceProvider); + activationLog().ifPresent(e::activationLog); + entry.error(e); + return e; + } + + private InjectionException timedOutActivationInjectionError(DefaultActivationLogEntry.Builder entry) { + ServiceProvider targetServiceProvider = entry.serviceProvider().orElseThrow(); + InjectionException e = new InjectionException("timed out during activation of " + targetServiceProvider, + targetServiceProvider); + activationLog().ifPresent(e::activationLog); + entry.error(e); + return e; + } + + private InjectionException timedOutDeActivationInjectionError(DefaultActivationLogEntry.Builder entry) { + ServiceProvider targetServiceProvider = entry.serviceProvider().orElseThrow(); + InjectionException e = new InjectionException("timed out during deactivation of " + targetServiceProvider, + targetServiceProvider); + activationLog().ifPresent(e::activationLog); + entry.error(e); + return e; + } + + private InjectionException interruptedPreActivationInjectionError(DefaultActivationLogEntry.Builder entry, + Throwable cause) { + ServiceProvider targetServiceProvider = entry.serviceProvider().orElseThrow(); + InjectionException e = new InjectionException("circular dependency found during activation of " + targetServiceProvider, + cause, targetServiceProvider); + activationLog().ifPresent(e::activationLog); + entry.error(e); + return e; + } + + private InjectionException managedServiceInstanceShouldHaveBeenSetException() { + InjectionException e = new InjectionException("managed service instance expected to have been set", this); + activationLog().ifPresent(e::activationLog); + return e; + } + + private InjectionException activationFailed(ActivationResult res) { + InjectionException e = new InjectionException("activation failed: " + res, this); + activationLog().ifPresent(e::activationLog); + return e; + } + + private PicoServiceProviderException unableToActivate(Throwable cause) { + return new PicoServiceProviderException("unable to activate: " + getClass().getName(), cause, this); + } + + private PicoServiceProviderException alreadyInitialized() { + throw new PicoServiceProviderException("already initialized", this); + } + + private void logMultiDefInjectionNote(String id, + Object prev, + InjectionPointInfo ipDep) { + String message = "there are two different services sharing the same injection point info id; first = " + + prev + " and the second = " + ipDep + "; both use the id '" + id + + "'; note that the second will override the first"; + if (log != null) { + log.record(DefaultActivationLogEntry.builder() + .serviceProvider(this) + .injectionPoint(ipDep) + .message(message) + .build()); + } else { + logger().log(System.Logger.Level.DEBUG, message); + } + } + + /** + * Represents a result of a phase transition. + * + * @see #createLogEntryAndResult(io.helidon.pico.Phase) + */ + // note that for one result, there may be N logEntry records we will build and write to the log + protected static class LogEntryAndResult /* implements Cloneable*/ { + private final DefaultActivationResult.Builder activationResult; + private final DefaultActivationLogEntry.Builder logEntry; + + LogEntryAndResult(DefaultActivationLogEntry.Builder logEntry, + DefaultActivationResult.Builder activationResult) { + this.logEntry = logEntry; + this.activationResult = activationResult; + } + + DefaultActivationResult.Builder activationResult() { + return activationResult; + } + + DefaultActivationLogEntry.Builder logEntry() { + return logEntry; + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/BoundedServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/BoundedServiceProvider.java new file mode 100644 index 00000000000..0fb738fedc5 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/BoundedServiceProvider.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.LazyValue; +import io.helidon.pico.Activator; +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DeActivator; +import io.helidon.pico.DefaultContextualServiceQuery; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.Phase; +import io.helidon.pico.PostConstructMethod; +import io.helidon.pico.PreDestroyMethod; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; + +/** + * A service provider that is bound to a particular injection point context. + * + * @param the type of the bound service provider + */ +class BoundedServiceProvider implements ServiceProvider { + + private final ServiceProvider binding; + private final InjectionPointInfo ipInfoCtx; + private final LazyValue instance; + private final LazyValue> instances; + + private BoundedServiceProvider(ServiceProvider binding, + InjectionPointInfo ipInfoCtx) { + this.binding = Objects.requireNonNull(binding); + this.ipInfoCtx = Objects.requireNonNull(ipInfoCtx); + ContextualServiceQuery query = DefaultContextualServiceQuery.builder() + .injectionPointInfo(ipInfoCtx) + .serviceInfoCriteria(ipInfoCtx.dependencyToServiceInfo()) + .expected(true).build(); + this.instance = LazyValue.create(() -> binding.first(query).orElse(null)); + this.instances = LazyValue.create(() -> binding.list(query)); + } + + /** + * Creates a bound service provider to a specific binding. + * + * @param binding the bound service provider + * @param ipInfoCtx the binding context + * @return the service provider created, wrapping the binding delegate provider + */ + static ServiceProvider create(ServiceProvider binding, + InjectionPointInfo ipInfoCtx) { + assert (binding != null); + assert (!(binding instanceof BoundedServiceProvider)); + if (binding instanceof AbstractServiceProvider) { + AbstractServiceProvider sp = (AbstractServiceProvider) binding; + if (!sp.isProvider()) { + return binding; + } + } + return new BoundedServiceProvider<>(binding, ipInfoCtx); + } + + @Override + public String toString() { + return binding.toString(); + } + + @Override + public int hashCode() { + return binding.hashCode(); + } + + @Override + public boolean equals(Object another) { + return (another instanceof ServiceProvider && binding.equals(another)); + } + + @Override + public Optional first(ContextualServiceQuery query) { + assert (query.injectionPointInfo().isEmpty() || ipInfoCtx.equals(query.injectionPointInfo().get())) + : query.injectionPointInfo() + " was not equal to " + this.ipInfoCtx; + assert (ipInfoCtx.dependencyToServiceInfo().matches(query.serviceInfoCriteria())) + : query.serviceInfoCriteria() + " did not match " + this.ipInfoCtx.dependencyToServiceInfo(); + return Optional.ofNullable(instance.get()); + } + + @Override + public List list(ContextualServiceQuery query) { + assert (query.injectionPointInfo().isEmpty() || ipInfoCtx.equals(query.injectionPointInfo().get())) + : query.injectionPointInfo() + " was not equal to " + this.ipInfoCtx; + assert (ipInfoCtx.dependencyToServiceInfo().matches(query.serviceInfoCriteria())) + : query.serviceInfoCriteria() + " did not match " + this.ipInfoCtx.dependencyToServiceInfo(); + return instances.get(); + } + + @Override + public String id() { + return binding.id(); + } + + @Override + public String description() { + return binding.description(); + } + + @Override + public boolean isProvider() { + return binding.isProvider(); + } + + @Override + public ServiceInfo serviceInfo() { + return binding.serviceInfo(); + } + + @Override + public DependenciesInfo dependencies() { + return binding.dependencies(); + } + + @Override + public Phase currentActivationPhase() { + return binding.currentActivationPhase(); + } + + @Override + public Optional activator() { + return binding.activator(); + } + + @Override + public Optional deActivator() { + return binding.deActivator(); + } + + @Override + public Optional postConstructMethod() { + return binding.postConstructMethod(); + } + + @Override + public Optional preDestroyMethod() { + return binding.preDestroyMethod(); + } + + @Override + public Optional> serviceProviderBindable() { + return Optional.of((ServiceProviderBindable) binding); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultActivationLog.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultActivationLog.java new file mode 100644 index 00000000000..fb4eb5238b3 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultActivationLog.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022, 2023 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.services; + +import java.lang.System.Logger; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.helidon.pico.ActivationLog; +import io.helidon.pico.ActivationLogEntry; +import io.helidon.pico.ActivationLogQuery; + +/** + * The default reference implementation of {@link io.helidon.pico.ActivationLog} and {@link io.helidon.pico.ActivationLogQuery}. + */ +class DefaultActivationLog implements ActivationLog, ActivationLogQuery { + private final List log; + private final Logger logger; + private Logger.Level level; + + private DefaultActivationLog(List log, + Logger logger, + Logger.Level level) { + this.log = log; + this.logger = logger; + this.level = level; + } + + /** + * Create a retained activation log that tee's to the provided logger. A retained log is capable of supporting + * {@link io.helidon.pico.ActivationLogQuery}. + * + * @param logger the logger to use + * @return the created activity log + */ + static DefaultActivationLog createRetainedLog(Logger logger) { + return new DefaultActivationLog(new CopyOnWriteArrayList<>(), logger, Logger.Level.INFO); + } + + /** + * Create an unretained activation log that simply logs to the provided logger. An unretained log is not capable of + * supporting {@link io.helidon.pico.ActivationLogQuery}. + * + * @param logger the logger to use + * @return the created activity log + */ + static DefaultActivationLog createUnretainedLog(Logger logger) { + return new DefaultActivationLog(null, logger, Logger.Level.DEBUG); + } + + /** + * Sets the logging level. + * + * @param level the level + */ + public void level(Logger.Level level) { + this.level = level; + } + + @Override + public ActivationLogEntry record(ActivationLogEntry entry) { + if (log != null) { + log.add(Objects.requireNonNull(entry)); + } + + if (logger != null) { + logger.log(level, entry); + } + + return entry; + } + + @Override + public Optional toQuery() { + return (log != null) ? Optional.of(this) : Optional.empty(); + } + + @Override + public boolean reset(boolean ignored) { + if (null != log) { + boolean affected = !log.isEmpty(); + log.clear(); + return affected; + } + + return false; + } + + @Override + public List fullActivationLog() { + return (null == log) ? List.of() : List.copyOf(log); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlanBinder.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlanBinder.java new file mode 100644 index 00000000000..73dcf089505 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlanBinder.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Optional; + +import io.helidon.pico.ServiceInjectionPlanBinder; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; + +class DefaultInjectionPlanBinder implements ServiceInjectionPlanBinder, ServiceInjectionPlanBinder.Binder { + + private final DefaultServices services; + + DefaultInjectionPlanBinder(DefaultServices services) { + this.services = services; + } + + @Override + public Binder bindTo(ServiceProvider untrustedSp) { + // don't trust what we get, but instead lookup the service provider that we carry in our services registry + ServiceProvider serviceProvider = services.serviceProviderFor(untrustedSp.serviceInfo().serviceTypeName()); + Optional> bindable = DefaultServiceBinder.toBindableProvider(serviceProvider); + Optional binder = (bindable.isPresent()) ? bindable.get().injectionPlanBinder() : Optional.empty(); + if (binder.isEmpty()) { + // basically this means this service will not support compile-time injection + DefaultPicoServices.LOGGER.log(System.Logger.Level.WARNING, + "service provider is not capable of being bound to injection points: " + serviceProvider); + return this; + } else { + if (DefaultPicoServices.LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.DEBUG, "binding injection plan to " + binder.get()); + } + } + + return binder.get(); + } + + @Override + public Binder bind(String id, + ServiceProvider serviceProvider) { + // NOP + return this; + } + + @Override + public Binder bindMany(String id, + ServiceProvider... serviceProviders) { + // NOP + return this; + } + + @Override + public Binder bindVoid(String ipIdentity) { + // NOP + return this; + } + + @Override + public Binder resolvedBind(String ipIdentity, + Class serviceType) { + // NOP + return this; + } + + @Override + public void commit() { + // NOP + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java new file mode 100644 index 00000000000..9ac8239f9c3 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjectionPlans.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DefaultContextualServiceQuery; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.DependencyInfo; +import io.helidon.pico.InjectionException; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.Interceptor; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.ServiceProviderProvider; +import io.helidon.pico.Services; +import io.helidon.pico.spi.InjectionResolver; + +class DefaultInjectionPlans { + + private DefaultInjectionPlans() { + } + + /** + * Converts the inputs to an injection plans for the given service provider. + * + * @param picoServices pico services + * @param self the reference to the service provider associated with this plan + * @param dependencies the dependencies + * @param resolveIps flag indicating whether injection points should be resolved + * @param logger the logger to use for any logging + * @return the injection plan per element identity belonging to the service provider + */ + static Map createInjectionPlans(PicoServices picoServices, + ServiceProvider self, + DependenciesInfo dependencies, + boolean resolveIps, + System.Logger logger) { + Map result = new LinkedHashMap<>(); + if (dependencies.allDependencies().isEmpty()) { + return result; + } + + dependencies.allDependencies() + .forEach(dep -> accumulate(dep, result, picoServices, self, resolveIps, logger)); + + return result; + } + + @SuppressWarnings("unchecked") + private static void accumulate(DependencyInfo dep, + Map result, + PicoServices picoServices, + ServiceProvider self, + boolean resolveIps, + System.Logger logger) { + ServiceInfoCriteria depTo = dep.dependencyTo(); + ServiceInfo selfInfo = self.serviceInfo(); + if (selfInfo.declaredWeight().isPresent() + && selfInfo.contractsImplemented().containsAll(depTo.contractsImplemented())) { + // if we have a weight on ourselves, and we inject an interface that we actually offer, then + // be sure to use it to get lower weighted injection points ... + depTo = DefaultServiceInfoCriteria.toBuilder(depTo) + .weight(selfInfo.declaredWeight().get()) + .build(); + } + + Services services = picoServices.services(); + PicoServicesConfig cfg = picoServices.config(); + boolean isPrivateSupported = cfg.supportsJsr330Privates(); + boolean isStaticSupported = cfg.supportsJsr330Statics(); + + if (self instanceof InjectionResolver) { + dep.injectionPointDependencies() + .stream() + .filter(ipInfo -> (isPrivateSupported || ipInfo.access() != InjectionPointInfo.Access.PRIVATE) + && (isStaticSupported || !ipInfo.staticDeclaration())) + .forEach(ipInfo -> { + String id = ipInfo.id(); + if (!result.containsKey(id)) { + Object resolved = ((InjectionResolver) self) + .resolve(ipInfo, picoServices, self, resolveIps) + .orElse(null); + Object target = (resolved instanceof Optional) + ? ((Optional) resolved).orElse(null) : resolved; + DefaultPicoInjectionPlan.Builder planBuilder = DefaultPicoInjectionPlan.builder() + .serviceProvider(self) + .injectionPointInfo(ipInfo) + .injectionPointQualifiedServiceProviders(toIpQualified(target)) + .unqualifiedProviders(toIpUnqualified(target)) + .wasResolved(resolved != null); + if (target != null) { + if (ipInfo.optionalWrapped()) { + planBuilder.resolved((target instanceof Optional && ((Optional) target).isEmpty()) + ? Optional.empty() : Optional.of(target)); + } else { + if (target instanceof Optional) { + target = ((Optional) target).orElse(null); + } + planBuilder.resolved(target); + } + } + Object prev = result.put(id, planBuilder.build()); + assert (prev == null) : ipInfo; + } + }); + } + + List> tmpServiceProviders = services.lookupAll(depTo, false); + if (tmpServiceProviders == null || tmpServiceProviders.isEmpty()) { + if (VoidServiceProvider.INSTANCE.serviceInfo().matches(depTo)) { + tmpServiceProviders = VoidServiceProvider.LIST_INSTANCE; + } + } + + // filter down the selections to not include self + List> serviceProviders = + (tmpServiceProviders != null && !tmpServiceProviders.isEmpty()) + ? tmpServiceProviders.stream() + .filter(sp -> !isSelf(self, sp)) + .collect(Collectors.toList()) + : tmpServiceProviders; + + dep.injectionPointDependencies() + .stream() + .filter(ipInfo -> + (isPrivateSupported || ipInfo.access() != InjectionPointInfo.Access.PRIVATE) + && (isStaticSupported || !ipInfo.staticDeclaration())) + .forEach(ipInfo -> { + String id = ipInfo.id(); + if (!result.containsKey(id)) { + Object resolved = (resolveIps) + ? resolve(self, ipInfo, serviceProviders, logger) : null; + if (!resolveIps && !ipInfo.optionalWrapped() + && (serviceProviders == null || serviceProviders.isEmpty()) + && !allowNullableInjectionPoint(ipInfo)) { + throw DefaultServices.resolutionBasedInjectionError( + ipInfo.dependencyToServiceInfo()); + } + DefaultPicoInjectionPlan plan = DefaultPicoInjectionPlan.builder() + .injectionPointInfo(ipInfo) + .injectionPointQualifiedServiceProviders(serviceProviders) + .serviceProvider(self) + .wasResolved(resolveIps) + .resolved((resolved instanceof Optional && ((Optional) resolved).isEmpty()) + ? Optional.empty() : Optional.ofNullable(resolved)) + .build(); + Object prev = result.put(id, plan); + assert (prev == null) : ipInfo; + } + }); + } + + /** + * Resolution comes after the plan was loaded or created. + * + * @param self the reference to the service provider associated with this plan + * @param ipInfo the injection point + * @param serviceProviders the service providers that qualify + * @param logger the logger to use for any logging + * @return the resolution (and activation) of the qualified service provider(s) in the form acceptable to the injection point + */ + @SuppressWarnings("unchecked") + static Object resolve(ServiceProvider self, + InjectionPointInfo ipInfo, + List> serviceProviders, + System.Logger logger) { + if (ipInfo.staticDeclaration()) { + throw new InjectionException(ipInfo + ": static is not supported", null, self); + } + if (ipInfo.access() == InjectionPointInfo.Access.PRIVATE) { + throw new InjectionException(ipInfo + ": private is not supported", null, self); + } + + try { + if (Void.class.getName().equals(ipInfo.serviceTypeName())) { + return null; + } + + if (ipInfo.listWrapped()) { + if (ipInfo.optionalWrapped()) { + throw new InjectionException("Optional + List injection is not supported for " + + ipInfo.serviceTypeName() + "." + ipInfo.elementName()); + } + + if (serviceProviders.isEmpty()) { + if (!allowNullableInjectionPoint(ipInfo)) { + throw new InjectionException("expected to resolve a service appropriate for " + + ipInfo.serviceTypeName() + "." + ipInfo.elementName(), + DefaultServices + .resolutionBasedInjectionError( + ipInfo.dependencyToServiceInfo()), + self); + } else { + return serviceProviders; + } + } + + if (ipInfo.providerWrapped() && !ipInfo.optionalWrapped()) { + return serviceProviders; + } + + if (ipInfo.listWrapped() && !ipInfo.optionalWrapped()) { + return toEligibleInjectionRefs(ipInfo, self, serviceProviders, true); + } + } else if (serviceProviders.isEmpty()) { + if (ipInfo.optionalWrapped()) { + return Optional.empty(); + } else { + throw new InjectionException("expected to resolve a service appropriate for " + + ipInfo.serviceTypeName() + "." + ipInfo.elementName(), + DefaultServices.resolutionBasedInjectionError(ipInfo.dependencyToServiceInfo()), + self); + } + } else { + // "standard" case + ServiceProvider serviceProvider = serviceProviders.get(0); + Optional> serviceProviderBindable = + DefaultServiceBinder.toBindableProvider(DefaultServiceBinder.toRootProvider(serviceProvider)); + if (serviceProviderBindable.isPresent() + && serviceProviderBindable.get() != serviceProvider + && serviceProviderBindable.get() instanceof ServiceProviderProvider) { + serviceProvider = serviceProviderBindable.get(); + serviceProviders = (List>) ((ServiceProviderProvider) serviceProvider) + .serviceProviders(ipInfo.dependencyToServiceInfo(), true, false); + if (!serviceProviders.isEmpty()) { + serviceProvider = serviceProviders.get(0); + } + } + + if (ipInfo.providerWrapped()) { + return ipInfo.optionalWrapped() ? Optional.of(serviceProvider) : serviceProvider; + } + + if (ipInfo.optionalWrapped()) { + Optional optVal; + try { + optVal = Objects.requireNonNull( + serviceProvider.first(ContextualServiceQuery.create(ipInfo, false))); + } catch (InjectionException e) { + logger.log(System.Logger.Level.WARNING, e.getMessage(), e); + optVal = Optional.empty(); + } + return optVal; + } + + ContextualServiceQuery query = ContextualServiceQuery.create(ipInfo, true); + Optional first = serviceProvider.first(query); + return first.orElse(null); + } + } catch (InjectionException ie) { + throw ie; + } catch (Throwable t) { + throw expectedToResolveCriteria(ipInfo, t, self); + } + + throw expectedToResolveCriteria(ipInfo, null, self); + } + + private static List> toIpQualified(Object target) { + if (target instanceof Collection) { + List> result = new ArrayList<>(); + ((Collection) target).stream() + .map(DefaultInjectionPlans::toIpQualified) + .forEach(result::addAll); + return result; + } + + return (target instanceof AbstractServiceProvider) + ? List.of((ServiceProvider) target) + : List.of(); + } + + private static List toIpUnqualified(Object target) { + if (target instanceof Collection) { + List result = new ArrayList<>(); + ((Collection) target).stream() + .map(DefaultInjectionPlans::toIpUnqualified) + .forEach(result::addAll); + return result; + } + + return (target == null || target instanceof AbstractServiceProvider) + ? List.of() + : List.of(target); + } + + private static boolean isSelf(ServiceProvider self, + Object other) { + assert (self != null); + + if (self == other) { + return true; + } + + if (self instanceof ServiceProviderBindable) { + Object selfInterceptor = ((ServiceProviderBindable) self).interceptor().orElse(null); + + if (other == selfInterceptor) { + return true; + } + } + + return false; + } + + private static boolean allowNullableInjectionPoint(InjectionPointInfo ipInfo) { + ServiceInfoCriteria missingServiceInfo = ipInfo.dependencyToServiceInfo(); + Set contractsNeeded = missingServiceInfo.contractsImplemented(); + return (1 == contractsNeeded.size() && contractsNeeded.contains(Interceptor.class.getName())); + } + + @SuppressWarnings({"unchecked", "rawTypes"}) + private static List toEligibleInjectionRefs(InjectionPointInfo ipInfo, + ServiceProvider self, + List> list, + boolean expected) { + List result = new ArrayList<>(); + + ContextualServiceQuery query = DefaultContextualServiceQuery.builder() + .injectionPointInfo(ipInfo) + .serviceInfoCriteria(ipInfo.dependencyToServiceInfo()) + .expected(expected); + for (ServiceProvider sp : list) { + Collection instances = sp.list(query); + result.addAll(instances); + } + + if (expected && result.isEmpty()) { + throw expectedToResolveCriteria(ipInfo, null, self); + } + + return result; + } + + private static InjectionException expectedToResolveCriteria(InjectionPointInfo ipInfo, + Throwable cause, + ServiceProvider self) { + String msg = (cause == null) ? "expected" : "failed"; + return new InjectionException(msg + " to resolve a service instance appropriate for '" + + ipInfo.serviceTypeName() + "." + ipInfo.elementName() + + "' with criteria = '" + ipInfo.dependencyToServiceInfo(), + cause, self); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultInjector.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjector.java new file mode 100644 index 00000000000..8b5f24b0413 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultInjector.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Objects; + +import io.helidon.pico.ActivationResult; +import io.helidon.pico.Activator; +import io.helidon.pico.DeActivationRequest; +import io.helidon.pico.DeActivator; +import io.helidon.pico.DefaultActivationResult; +import io.helidon.pico.DefaultDeActivationRequest; +import io.helidon.pico.Injector; +import io.helidon.pico.InjectorOptions; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServiceProviderException; +import io.helidon.pico.ServiceProvider; + +/** + * Default reference implementation for the {@link Injector}. + */ +class DefaultInjector implements Injector { + + @Override + @SuppressWarnings("unchecked") + public ActivationResult activateInject(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException { + Objects.requireNonNull(serviceOrServiceProvider); + Objects.requireNonNull(opts); + + DefaultActivationResult.Builder resultBuilder = DefaultActivationResult.builder(); + + if (opts.strategy() != Strategy.ANY && opts.strategy() != Strategy.ACTIVATOR) { + return handleError(resultBuilder, opts, "only " + Strategy.ACTIVATOR + " strategy is supported", null); + } + + if (!(serviceOrServiceProvider instanceof AbstractServiceProvider)) { + return handleError(resultBuilder, opts, "unsupported service type: " + serviceOrServiceProvider, null); + } + + AbstractServiceProvider instance = (AbstractServiceProvider) serviceOrServiceProvider; + resultBuilder.serviceProvider(instance); + + Activator activator = instance.activator().orElse(null); + if (activator == null) { + return handleError(resultBuilder, opts, "the service provider does not have an activator", instance); + } + + return activator.activate(opts.activationRequest()); + } + + @Override + @SuppressWarnings("unchecked") + public ActivationResult deactivate(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException { + Objects.requireNonNull(serviceOrServiceProvider); + Objects.requireNonNull(opts); + + DefaultActivationResult.Builder resultBuilder = DefaultActivationResult.builder(); + + if (opts.strategy() != Strategy.ANY && opts.strategy() != Strategy.ACTIVATOR) { + return handleError(resultBuilder, opts, "only " + Strategy.ACTIVATOR + " strategy is supported", null); + } + + if (!(serviceOrServiceProvider instanceof AbstractServiceProvider)) { + return handleError(resultBuilder, opts, "unsupported service type: " + serviceOrServiceProvider, null); + } + + AbstractServiceProvider instance = (AbstractServiceProvider) serviceOrServiceProvider; + resultBuilder.serviceProvider(instance); + + DeActivator deactivator = instance.deActivator().orElse(null); + if (deactivator == null) { + return handleError(resultBuilder, opts, "the service provider does not have a deactivator", instance); + } + + DeActivationRequest request = DefaultDeActivationRequest.builder() + .throwIfError(opts.activationRequest().throwIfError()) + .build(); + return deactivator.deactivate(request); + } + + private ActivationResult handleError(DefaultActivationResult.Builder resultBuilder, + InjectorOptions opts, + String message, + ServiceProvider serviceProvider) { + PicoException e = (serviceProvider == null) + ? new PicoException(message) : new PicoServiceProviderException(message, serviceProvider); + resultBuilder.error(e); + if (opts.activationRequest().throwIfError()) { + throw e; + } + return resultBuilder.build(); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServices.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServices.java new file mode 100644 index 00000000000..9cfae8552d7 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServices.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.ArrayList; +import java.util.Collection; +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; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import io.helidon.pico.ActivationLog; +import io.helidon.pico.ActivationLogEntry; +import io.helidon.pico.ActivationLogQuery; +import io.helidon.pico.ActivationPhaseReceiver; +import io.helidon.pico.ActivationResult; +import io.helidon.pico.ActivationStatus; +import io.helidon.pico.Application; +import io.helidon.pico.Bootstrap; +import io.helidon.pico.CallingContext; +import io.helidon.pico.CallingContextFactory; +import io.helidon.pico.DefaultActivationLogEntry; +import io.helidon.pico.DefaultActivationResult; +import io.helidon.pico.DefaultInjectorOptions; +import io.helidon.pico.DefaultMetrics; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Event; +import io.helidon.pico.Injector; +import io.helidon.pico.InjectorOptions; +import io.helidon.pico.Metrics; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; + +import static io.helidon.pico.CallingContext.toErrorMessage; + +/** + * The default implementation for {@link io.helidon.pico.PicoServices}. + */ +class DefaultPicoServices implements PicoServices, Resettable { + static final System.Logger LOGGER = System.getLogger(DefaultPicoServices.class.getName()); + + private final AtomicBoolean initializingServicesStarted = new AtomicBoolean(false); + private final AtomicBoolean initializingServicesFinished = new AtomicBoolean(false); + private final AtomicBoolean isBinding = new AtomicBoolean(false); + private final AtomicReference services = new AtomicReference<>(); + private final AtomicReference> moduleList = new AtomicReference<>(); + private final AtomicReference> applicationList = new AtomicReference<>(); + private final Bootstrap bootstrap; + private final PicoServicesConfig cfg; + private final boolean isGlobal; + private final DefaultActivationLog log; + private final State state = State.create(Phase.INIT); + private CallingContext initializationCallingContext; + + /** + * Constructor taking the bootstrap. + * + * @param bootstrap the bootstrap configuration + * @param global flag indicating whether this is the global con + */ + DefaultPicoServices(Bootstrap bootstrap, + boolean global) { + this.bootstrap = bootstrap; + this.cfg = DefaultPicoServicesConfig.createDefaultConfigBuilder().build(); + this.isGlobal = global; + this.log = cfg.activationLogs() + ? DefaultActivationLog.createRetainedLog(LOGGER) + : DefaultActivationLog.createUnretainedLog(LOGGER); + } + + @Override + public Bootstrap bootstrap() { + return bootstrap; + } + + @Override + public PicoServicesConfig config() { + return cfg; + } + + @Override + public Optional activationLog() { + return Optional.of(log); + } + + @Override + public Optional injector() { + return Optional.of(new DefaultInjector()); + } + + @Override + public Optional metrics() { + DefaultServices thisServices = services.get(); + if (thisServices == null) { + // never has been any lookup yet + return Optional.of(DefaultMetrics.builder().build()); + } + return Optional.of(thisServices.metrics()); + } + + @Override + public Optional> lookups() { + if (!cfg.serviceLookupCaching()) { + return Optional.empty(); + } + + DefaultServices thisServices = services.get(); + if (thisServices == null) { + // never has been any lookup yet + return Optional.of(Set.of()); + } + return Optional.of(thisServices.cache().keySet()); + } + + @Override + public Optional services(boolean initialize) { + if (!initialize) { + return Optional.ofNullable(services.get()); + } + + if (!initializingServicesStarted.getAndSet(true)) { + try { + initializeServices(); + } catch (Throwable t) { + state.lastError(t); + initializingServicesStarted.set(false); + if (t instanceof PicoException) { + throw (PicoException) t; + } else { + throw new PicoException("failed to initialize: " + t.getMessage(), t); + } + } finally { + state.finished(true); + initializingServicesFinished.set(true); + } + } + + DefaultServices thisServices = services.get(); + if (thisServices == null) { + throw new PicoException("must reset() after shutdown()"); + } + return Optional.of(thisServices); + } + + @Override + public Optional> shutdown() { + Map result = Map.of(); + DefaultServices current = services.get(); + if (services.compareAndSet(current, null) && current != null) { + State currentState = state.clone().currentPhase(Phase.PRE_DESTROYING); + log("started shutdown"); + result = doShutdown(current, currentState); + log("finished shutdown"); + } + return Optional.ofNullable(result); + } + + @Override + // note that this is typically only called during testing, and also in the pico-maven-plugin + public boolean reset(boolean deep) { + try { + assertNotInitializing(); + if (isInitializing() || isInitialized()) { + // we allow dynamic updates leading up to initialization - after that it should be prevented if not configured on + DefaultServices.assertPermitsDynamic(cfg); + } + boolean result = deep; + + DefaultServices prev = services.get(); + if (prev != null) { + boolean affected = prev.reset(deep); + result |= affected; + } + + boolean affected = log.reset(deep); + result |= affected; + + if (deep) { + isBinding.set(false); + moduleList.set(null); + applicationList.set(null); + if (prev != null) { + services.set(new DefaultServices(cfg)); + } + state.reset(true); + initializingServicesStarted.set(false); + initializingServicesFinished.set(false); + initializationCallingContext = null; + } + + return result; + } catch (Exception e) { + throw new PicoException("failed to reset (state=" + state + + ", isInitialized=" + isInitialized() + + ", isInitializing=" + isInitializing() + ")", e); + } + } + + /** + * Returns true if Pico is in the midst of initialization. + * + * @return true if initialization is underway + */ + public boolean isInitializing() { + return initializingServicesStarted.get() && !initializingServicesFinished.get(); + } + + /** + * Returns true if Pico was initialized. + * + * @return true if already initialized + */ + public boolean isInitialized() { + return initializingServicesStarted.get() && initializingServicesFinished.get(); + } + + private Map doShutdown(DefaultServices services, + State state) { + long start = System.currentTimeMillis(); + + ThreadFactory threadFactory = r -> { + Thread thread = new Thread(r); + thread.setDaemon(false); + thread.setPriority(Thread.MAX_PRIORITY); + thread.setName(PicoServicesConfig.NAME + "-shutdown-" + System.currentTimeMillis()); + return thread; + }; + + Shutdown shutdown = new Shutdown(services, state); + ExecutorService es = Executors.newSingleThreadExecutor(threadFactory); + long finish; + try { + return es.submit(shutdown) + // https://github.com/helidon-io/helidon/issues/6434: have an appropriate timeout config for this +// .get(cfg.activationDeadlockDetectionTimeoutMillis(), TimeUnit.MILLISECONDS); + .get(); + } catch (Throwable t) { + finish = System.currentTimeMillis(); + errorLog("error during shutdown (elapsed = " + (finish - start) + " ms)", t); + throw new PicoException("error during shutdown", t); + } finally { + es.shutdown(); + state.finished(true); + finish = System.currentTimeMillis(); + log("finished shutdown (elapsed = " + (finish - start) + " ms)"); + } + } + + private void assertNotInitializing() { + if (isBinding.get() || isInitializing()) { + CallingContext initializationCallingContext = this.initializationCallingContext; + String desc = "reset() during the initialization sequence is not supported (binding=" + + isBinding + ", initializingServicesFinished=" + + initializingServicesFinished + ")"; + String msg = (initializationCallingContext == null) + ? toErrorMessage(desc) : toErrorMessage(initializationCallingContext, desc); + throw new PicoException(msg); + } + } + + private void initializeServices() { + initializationCallingContext = CallingContextFactory.create(false).orElse(null); + + if (services.get() == null) { + services.set(new DefaultServices(cfg)); + } + + DefaultServices thisServices = services.get(); + thisServices.state(state); + state.currentPhase(Phase.ACTIVATION_STARTING); + + if (isGlobal) { + // iterate over all modules, binding to each one's set of services, but with NO activations + List modules = findModules(true); + try { + isBinding.set(true); + bindModules(thisServices, modules); + } finally { + isBinding.set(false); + } + } + + state.currentPhase(Phase.GATHERING_DEPENDENCIES); + + if (isGlobal) { + // look for the literal injection plan + // typically only be one Application in non-testing runtimes + List apps = findApplications(true); + bindApplications(thisServices, apps); + } + + state.currentPhase(Phase.POST_BIND_ALL_MODULES); + + if (isGlobal) { + // only the global services registry gets eventing (no particular reason though) + thisServices.allServiceProviders(false).stream() + .filter(sp -> sp instanceof ActivationPhaseReceiver) + .map(sp -> (ActivationPhaseReceiver) sp) + .forEach(sp -> sp.onPhaseEvent(Event.STARTING, Phase.POST_BIND_ALL_MODULES)); + } + + state.currentPhase(Phase.FINAL_RESOLVE); + + if (isGlobal || cfg.supportsCompileTime()) { + thisServices.allServiceProviders(false).stream() + .filter(sp -> sp instanceof ActivationPhaseReceiver) + .map(sp -> (ActivationPhaseReceiver) sp) + .forEach(sp -> sp.onPhaseEvent(Event.STARTING, Phase.FINAL_RESOLVE)); + } + + state.currentPhase(Phase.SERVICES_READY); + + // notify interested service providers of "readiness"... + thisServices.allServiceProviders(false).stream() + .filter(sp -> sp instanceof ActivationPhaseReceiver) + .map(sp -> (ActivationPhaseReceiver) sp) + .forEach(sp -> sp.onPhaseEvent(Event.STARTING, Phase.SERVICES_READY)); + + state.finished(true); + } + + private List findApplications(boolean load) { + List result = applicationList.get(); + if (result != null) { + return result; + } + + result = new ArrayList<>(); + if (load) { + ServiceLoader serviceLoader = ServiceLoader.load(Application.class); + for (Application app : serviceLoader) { + result.add(app); + } + + if (!cfg.permitsDynamic()) { + applicationList.compareAndSet(null, List.copyOf(result)); + result = applicationList.get(); + } + } + return result; + } + + private List findModules(boolean load) { + List result = moduleList.get(); + if (result != null) { + return result; + } + + result = new ArrayList<>(); + if (load) { + ServiceLoader serviceLoader = ServiceLoader.load(io.helidon.pico.Module.class); + for (io.helidon.pico.Module module : serviceLoader) { + result.add(module); + } + + if (!cfg.permitsDynamic()) { + moduleList.compareAndSet(null, List.copyOf(result)); + result = moduleList.get(); + } + } + return result; + } + + private void bindApplications(DefaultServices services, + Collection apps) { + if (!cfg.usesCompileTimeApplications()) { + LOGGER.log(System.Logger.Level.DEBUG, "application binding is disabled"); + return; + } + + if (apps.size() > 1) { + LOGGER.log(System.Logger.Level.WARNING, + "there is typically only 1 application instance; app instances = " + apps); + } else if (apps.isEmpty()) { + LOGGER.log(System.Logger.Level.TRACE, "no " + Application.class.getName() + " was found."); + return; + } + + DefaultInjectionPlanBinder injectionPlanBinder = new DefaultInjectionPlanBinder(services); + apps.forEach(app -> services.bind(this, injectionPlanBinder, app)); + } + + private void bindModules(DefaultServices services, + Collection modules) { + if (!cfg.usesCompileTimeModules()) { + LOGGER.log(System.Logger.Level.DEBUG, "module binding is disabled"); + return; + } + + if (modules.isEmpty()) { + LOGGER.log(System.Logger.Level.WARNING, "no " + io.helidon.pico.Module.class.getName() + " was found."); + } else { + modules.forEach(module -> services.bind(this, module, isBinding.get())); + } + } + + private void log(String message) { + ActivationLogEntry entry = DefaultActivationLogEntry.builder() + .message(message) + .build(); + log.record(entry); + } + + private void errorLog(String message, + Throwable t) { + ActivationLogEntry entry = DefaultActivationLogEntry.builder() + .message(message) + .error(t) + .build(); + log.record(entry); + } + + /** + * Will attempt to shut down in reverse order of activation, but only if activation logs are retained. + */ + private class Shutdown implements Callable> { + private final DefaultServices services; + private final State state; + private final ActivationLog log; + private final Injector injector; + private final InjectorOptions opts = DefaultInjectorOptions.builder().build(); + private final Map map = new LinkedHashMap<>(); + + Shutdown(DefaultServices services, + State state) { + this.services = Objects.requireNonNull(services); + this.state = Objects.requireNonNull(state); + this.injector = injector().orElseThrow(); + this.log = activationLog().orElseThrow(); + } + + @Override + public Map call() { + state.currentPhase(Phase.DESTROYED); + + ActivationLogQuery query = log.toQuery().orElse(null); + if (query != null) { + // we can lean on the log entries in order to shut down in reverse chronological order + List fullyActivationLog = new ArrayList<>(query.fullActivationLog()); + if (!fullyActivationLog.isEmpty()) { + LinkedHashSet> serviceProviderActivations = new LinkedHashSet<>(); + + Collections.reverse(fullyActivationLog); + fullyActivationLog.stream() + .map(ActivationLogEntry::serviceProvider) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(serviceProviderActivations::add); + + // prepare for the shutdown log event sequence + log.toQuery().ifPresent(it -> it.reset(false)); + + // shutdown using the reverse chronological ordering in the log for starters + doFinalShutdown(serviceProviderActivations); + } + } + + // next get all services that are beyond INIT state, and sort by runlevel order, and shut those down also + List> serviceProviders = services.lookupAll(DefaultServiceInfoCriteria.builder().build(), false); + serviceProviders = serviceProviders.stream() + .filter(sp -> sp.currentActivationPhase().eligibleForDeactivation()) + .collect(Collectors.toList()); + serviceProviders.sort((o1, o2) -> { + int runLevel1 = o1.serviceInfo().realizedRunLevel(); + int runLevel2 = o2.serviceInfo().realizedRunLevel(); + return Integer.compare(runLevel1, runLevel2); + }); + doFinalShutdown(serviceProviders); + + // finally, clear everything + reset(false); + + return map; + } + + private void doFinalShutdown(Collection> serviceProviders) { + for (ServiceProvider csp : serviceProviders) { + Phase startingActivationPhase = csp.currentActivationPhase(); + ActivationResult result; + try { + result = injector.deactivate(csp, opts); + } catch (Throwable t) { + errorLog("error during shutdown", t); + result = DefaultActivationResult.builder() + .serviceProvider(csp) + .startingActivationPhase(startingActivationPhase) + .targetActivationPhase(Phase.DESTROYED) + .finishingActivationPhase(csp.currentActivationPhase()) + .finishingStatus(ActivationStatus.FAILURE) + .error(t) + .build(); + } + map.put(csp.serviceInfo().serviceTypeName(), result); + } + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesConfig.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesConfig.java new file mode 100644 index 00000000000..f272d9da230 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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.services; + +/** + * The default reference implementation {@link io.helidon.pico.PicoServicesConfig}. + *

        + * It is strongly suggested that any {@link io.helidon.pico.Bootstrap} configuration is established prior to initializing + * this instance, since the results will vary once any bootstrap configuration is globally set. + */ +class DefaultPicoServicesConfig { + + static final String PROVIDER = "oracle"; + + private DefaultPicoServicesConfig() { + } + + static io.helidon.pico.DefaultPicoServicesConfig.Builder createDefaultConfigBuilder() { + return io.helidon.pico.DefaultPicoServicesConfig.builder() + .providerName(PROVIDER) + .providerVersion(Versions.CURRENT_PICO_VERSION); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesProvider.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesProvider.java new file mode 100644 index 00000000000..b7b14fc39bb --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultPicoServicesProvider.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022, 2023 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.services; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.Bootstrap; +import io.helidon.pico.PicoServices; +import io.helidon.pico.Resettable; +import io.helidon.pico.spi.PicoServicesProvider; + +import jakarta.inject.Singleton; + +/** + * The default implementation for {@link io.helidon.pico.spi.PicoServicesProvider}. + * The first instance created (or first after calling deep {@link #reset}) will be the global services instance. The global + * instance will track the set of loaded modules and applications that are loaded by this JVM. + * + * @see io.helidon.pico.PicoServices#picoServices() + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultPicoServicesProvider implements PicoServicesProvider, Resettable { + private static final AtomicReference INSTANCE = new AtomicReference<>(); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultPicoServicesProvider() { + } + + @Override + public PicoServices services(Bootstrap bootstrap) { + Objects.requireNonNull(bootstrap); + if (INSTANCE.get() == null) { + DefaultPicoServices global = new DefaultPicoServices(bootstrap, true); + INSTANCE.compareAndSet(null, global); + } + + if (INSTANCE.get().bootstrap().equals(bootstrap)) { + // the global one + return INSTANCE.get(); + } + + // not the global one + return new DefaultPicoServices(bootstrap, false); + } + + @Override + public boolean reset(boolean deep) { + DefaultPicoServices services = INSTANCE.get(); + boolean result = (services != null); + if (services != null) { + services.reset(deep); + if (deep) { + INSTANCE.set(null); + } + } + return result; + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultServiceBinder.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultServiceBinder.java new file mode 100644 index 00000000000..06b2097334c --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultServiceBinder.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Objects; +import java.util.Optional; + +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceBinder; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.Services; + +/** + * The default implementation for {@link ServiceBinder}. + */ +public class DefaultServiceBinder implements ServiceBinder { + private final PicoServices picoServices; + private final ServiceBinder serviceRegistry; + private final String moduleName; + private final boolean trusted; + + private DefaultServiceBinder(PicoServices picoServices, + String moduleName, + boolean trusted) { + this.picoServices = picoServices; + this.serviceRegistry = (ServiceBinder) picoServices.services(); + this.moduleName = moduleName; + this.trusted = trusted; + } + + /** + * Creates an instance of the default services binder. + * + * @param picoServices the pico services instance + * @param moduleName the module name + * @param trusted are we in trusted mode (typically only set during early initialization sequence) + * @return the newly created service binder + */ + public static DefaultServiceBinder create(PicoServices picoServices, + String moduleName, + boolean trusted) { + Objects.requireNonNull(picoServices); + Objects.requireNonNull(moduleName); + return new DefaultServiceBinder(picoServices, moduleName, trusted); + } + + @Override + public void bind(ServiceProvider sp) { + if (!trusted) { + DefaultServices.assertPermitsDynamic(picoServices.config()); + } + + Optional> bindableSp = toBindableProvider(sp); + + if (moduleName != null) { + bindableSp.ifPresent(it -> it.moduleName(moduleName)); + } + + Services services = picoServices.services(); + if (services instanceof DefaultServices && sp instanceof ServiceProviderBindable) { + Phase currentPhase = ((DefaultServices) services).currentPhase(); + if (currentPhase.ordinal() >= Phase.SERVICES_READY.ordinal()) { + // deferred binding (e.g., to allow PicoTestSupport to programmatically register/bind service providers + ((ServiceProviderBindable) sp).picoServices(Optional.of(picoServices)); + } + } + + serviceRegistry.bind(sp); + bindableSp.ifPresent(it -> it.picoServices(Optional.of(picoServices))); + } + + /** + * Returns the bindable service provider for what is passed if available. + * + * @param sp the service provider + * @return the bindable service provider if available, otherwise empty + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Optional> toBindableProvider(ServiceProvider sp) { + Objects.requireNonNull(sp); + if (sp instanceof ServiceProviderBindable) { + return Optional.of((ServiceProviderBindable) sp); + } + return (Optional) sp.serviceProviderBindable(); + } + + /** + * Returns the root provider of the service provider passed. + * + * @param sp the service provider + * @return the root provider of the service provider, falling back to the service provider passed + */ + public static ServiceProvider toRootProvider(ServiceProvider sp) { + Optional> bindable = toBindableProvider(sp); + if (bindable.isPresent()) { + sp = bindable.get(); + } + + ServiceProvider rootProvider = ((ServiceProviderBindable) sp).rootProvider().orElse(null); + return (rootProvider != null) ? rootProvider : sp; + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java b/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java new file mode 100644 index 00000000000..46cc8a03b45 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/DefaultServices.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2022, 2023 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.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import io.helidon.pico.Application; +import io.helidon.pico.CallingContext; +import io.helidon.pico.CallingContextFactory; +import io.helidon.pico.DefaultMetrics; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.InjectionException; +import io.helidon.pico.Intercepted; +import io.helidon.pico.Metrics; +import io.helidon.pico.Module; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceBinder; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.ServiceProviderBindable; +import io.helidon.pico.ServiceProviderProvider; +import io.helidon.pico.Services; + +import jakarta.inject.Provider; + +import static io.helidon.pico.CallingContext.toErrorMessage; + +/** + * The default reference implementation of {@link io.helidon.pico.Services}. + */ +class DefaultServices implements Services, ServiceBinder, Resettable { + private static final ServiceProviderComparator COMPARATOR = ServiceProviderComparator.create(); + + private final ConcurrentHashMap> servicesByTypeName = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> servicesByContract = new ConcurrentHashMap<>(); + private final Map>> cache = new ConcurrentHashMap<>(); + private final PicoServicesConfig cfg; + private final AtomicInteger lookupCount = new AtomicInteger(); + private final AtomicInteger cacheLookupCount = new AtomicInteger(); + private final AtomicInteger cacheHitCount = new AtomicInteger(); + private volatile State stateWatchOnly; // we are watching and not mutating this state - owned by DefaultPicoServices + + /** + * The constructor taking a configuration. + * + * @param cfg the config + */ + DefaultServices(PicoServicesConfig cfg) { + this.cfg = Objects.requireNonNull(cfg); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List explodeAndSort(Collection coll, + ServiceInfoCriteria criteria, + boolean expected) { + List result; + + if ((coll.size() > 1) + || coll.stream().anyMatch(sp -> sp instanceof ServiceProviderProvider)) { + result = new ArrayList<>(); + + coll.forEach(s -> { + if (s instanceof ServiceProviderProvider) { + List> subList = ((ServiceProviderProvider) s) + .serviceProviders(criteria, true, true); + if (subList != null && !subList.isEmpty()) { + subList.stream().filter(Objects::nonNull).forEach(result::add); + } + } else { + result.add(s); + } + }); + + if (result.size() > 1) { + result.sort(serviceProviderComparator()); + } + + return result; + } else { + result = (coll instanceof List) ? (List) coll : new ArrayList<>(coll); + } + + if (expected && result.isEmpty()) { + throw resolutionBasedInjectionError(criteria); + } + + return result; + } + + static boolean hasContracts(ServiceInfoCriteria criteria) { + return !criteria.contractsImplemented().isEmpty(); + } + + static boolean isIntercepted(ServiceProvider sp) { + return (sp instanceof ServiceProviderBindable && ((ServiceProviderBindable) sp).isIntercepted()); + } + + /** + * First use weight, then use FQN of the service type name as the secondary comparator if weights are the same. + * + * @return the pico comparator + * @see ServiceProviderComparator + */ + static Comparator> serviceProviderComparator() { + return COMPARATOR; + } + + static void assertPermitsDynamic(PicoServicesConfig cfg) { + if (!cfg.permitsDynamic()) { + String desc = "Services are configured to prevent dynamic updates.\n" + + "Set config '" + + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC + + " = true' to enable"; + Optional callCtx = CallingContextFactory.create(false); + String msg = (callCtx.isEmpty()) ? toErrorMessage(desc) : toErrorMessage(callCtx.get(), desc); + throw new IllegalStateException(msg); + } + } + + static ServiceInfo toValidatedServiceInfo(ServiceProvider serviceProvider) { + ServiceInfo info = serviceProvider.serviceInfo(); + Objects.requireNonNull(info.serviceTypeName(), () -> "service type name is required for " + serviceProvider); + return info; + } + + static InjectionException serviceProviderAlreadyBoundInjectionError(ServiceProvider previous, + ServiceProvider sp) { + return new InjectionException("service provider already bound to " + previous, null, sp); + } + + static InjectionException resolutionBasedInjectionError(ServiceInfoCriteria ctx) { + return new InjectionException("expected to resolve a service matching " + ctx); + } + + static InjectionException resolutionBasedInjectionError(String serviceTypeName) { + return resolutionBasedInjectionError(DefaultServiceInfoCriteria.builder().serviceTypeName(serviceTypeName).build()); + } + + /** + * Total size of the service registry. + * + * @return total size of the service registry + */ + public int size() { + return servicesByTypeName.size(); + } + + /** + * Performs a reset. When deep is false this will only clear the cache and metrics count. When deep is true will also + * deeply reset each service in the registry as well as clear out the registry. Dynamic must be permitted in config for + * reset to occur. + * + * @param deep set to true will iterate through every service in the registry to attempt a reset on each service as well + * @return true if reset had any affect + * @throws java.lang.IllegalStateException when dynamic is not permitted + */ + @Override + public boolean reset(boolean deep) { + if (Phase.ACTIVATION_STARTING != currentPhase()) { + assertPermitsDynamic(cfg); + } + + boolean changed = (deep || !servicesByTypeName.isEmpty() || lookupCount.get() > 0 || cacheLookupCount.get() > 0); + + if (deep) { + servicesByTypeName.values().forEach(sp -> { + if (sp instanceof Resettable) { + ((Resettable) sp).reset(true); + } + }); + servicesByTypeName.clear(); + servicesByContract.clear(); + } + + clearCacheAndMetrics(); + + return changed; + } + + @Override + public Optional> lookupFirst(Class type, + boolean expected) { + DefaultServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(type.getName()) + .build(); + return lookupFirst(criteria, expected); + } + + @Override + public Optional> lookupFirst(Class type, + String name, + boolean expected) { + DefaultServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(type.getName()) + .addQualifier(DefaultQualifierAndValue.createNamed(name)) + .build(); + return lookupFirst(criteria, expected); + } + + @Override + public Optional> lookupFirst(ServiceInfoCriteria criteria, + boolean expected) { + List> result = lookup(criteria, expected, 1); + assert (!expected || !result.isEmpty()); + return (result.isEmpty()) ? Optional.empty() : Optional.of(result.get(0)); + } + + @Override + public List> lookupAll(Class type) { + DefaultServiceInfoCriteria serviceInfo = DefaultServiceInfoCriteria.builder() + .addContractImplemented(type.getName()) + .build(); + return lookup(serviceInfo, false, Integer.MAX_VALUE); + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public List> lookupAll(ServiceInfoCriteria criteria, + boolean expected) { + List> result = (List) lookup(criteria, expected, Integer.MAX_VALUE); + assert (!expected || !result.isEmpty()); + return result; + } + + @Override + public void bind(ServiceProvider serviceProvider) { + if (currentPhase().ordinal() > Phase.GATHERING_DEPENDENCIES.ordinal()) { + assertPermitsDynamic(cfg); + } + + ServiceInfo serviceInfo = toValidatedServiceInfo(serviceProvider); + String serviceTypeName = serviceInfo.serviceTypeName(); + + ServiceProvider previous = servicesByTypeName.putIfAbsent(serviceTypeName, serviceProvider); + if (previous != null && previous != serviceProvider) { + if (cfg.permitsDynamic()) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.WARNING, + "overwriting " + previous + " with " + serviceProvider); + servicesByTypeName.put(serviceTypeName, serviceProvider); + } else { + throw serviceProviderAlreadyBoundInjectionError(previous, serviceProvider); + } + } + + // special handling in case we are an interceptor... + Set qualifiers = serviceInfo.qualifiers(); + Optional interceptedQualifier = qualifiers.stream() + .filter(q -> q.typeName().name().equals(Intercepted.class.getName())) + .findFirst(); + if (interceptedQualifier.isPresent()) { + // assumption: expected that the root service provider is registered prior to any interceptors + String interceptedServiceTypeName = Objects.requireNonNull(interceptedQualifier.get().value().orElseThrow()); + ServiceProvider interceptedSp = lookupFirst(DefaultServiceInfoCriteria.builder() + .serviceTypeName(interceptedServiceTypeName) + .build(), true).orElse(null); + if (interceptedSp instanceof ServiceProviderBindable) { + ((ServiceProviderBindable) interceptedSp).interceptor(serviceProvider); + } + } + + servicesByContract.compute(serviceTypeName, (contract, servicesSharingThisContract) -> { + if (servicesSharingThisContract == null) { + servicesSharingThisContract = new TreeSet<>(serviceProviderComparator()); + } + boolean added = servicesSharingThisContract.add(serviceProvider); + assert (added) : "expected to have added: " + serviceProvider; + return servicesSharingThisContract; + }); + for (String cn : serviceInfo.contractsImplemented()) { + servicesByContract.compute(cn, (contract, servicesSharingThisContract) -> { + if (servicesSharingThisContract == null) { + servicesSharingThisContract = new TreeSet<>(serviceProviderComparator()); + } + boolean ignored = servicesSharingThisContract.add(serviceProvider); + return servicesSharingThisContract; + }); + } + } + + void state(State state) { + this.stateWatchOnly = Objects.requireNonNull(state); + } + + Phase currentPhase() { + return (stateWatchOnly == null) ? Phase.INIT : stateWatchOnly.currentPhase(); + } + + Map>> cache() { + return Map.copyOf(cache); + } + + /** + * Clear the cache and metrics. + */ + void clearCacheAndMetrics() { + cache.clear(); + lookupCount.set(0); + cacheLookupCount.set(0); + cacheHitCount.set(0); + } + + Metrics metrics() { + return DefaultMetrics.builder() + .serviceCount(size()) + .lookupCount(lookupCount.get()) + .cacheLookupCount(cacheLookupCount.get()) + .cacheHitCount(cacheHitCount.get()) + .build(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + List> lookup(ServiceInfoCriteria criteria, + boolean expected, + int limit) { + List> result; + + lookupCount.incrementAndGet(); + + if (hasContracts(criteria)) { + String serviceTypeName = criteria.serviceTypeName().orElse(null); + boolean hasOneContractInCriteria = (1 == criteria.contractsImplemented().size()); + String theOnlyContractRequested = (hasOneContractInCriteria) + ? criteria.contractsImplemented().iterator().next() : null; + if (serviceTypeName == null + && hasOneContractInCriteria + && criteria.qualifiers().isEmpty()) { + serviceTypeName = theOnlyContractRequested; + } + if (serviceTypeName != null) { + ServiceProvider exact = servicesByTypeName.get(serviceTypeName); + if (exact != null && !isIntercepted(exact)) { + return explodeAndSort(List.of(exact), criteria, expected); + } + } + if (hasOneContractInCriteria) { + Set> subsetOfMatches = servicesByContract.get(theOnlyContractRequested); + if (subsetOfMatches != null) { + result = subsetOfMatches.stream().parallel() + .filter(sp -> sp.serviceInfo().matches(criteria)) + .limit(limit) + .collect(Collectors.toList()); + if (!result.isEmpty()) { + return explodeAndSort(result, criteria, expected); + } + } + } + } + + if (cfg.serviceLookupCaching()) { + result = cache.get(criteria); + cacheLookupCount.incrementAndGet(); + if (result != null) { + cacheHitCount.incrementAndGet(); + return (List) result; + } + } + + // table scan :-( + result = servicesByTypeName.values() + .stream().parallel() + .filter(sp -> sp.serviceInfo().matches(criteria)) + .limit(limit) + .collect(Collectors.toList()); + if (expected && result.isEmpty()) { + throw resolutionBasedInjectionError(criteria); + } + + if (!result.isEmpty()) { + result = explodeAndSort(result, criteria, expected); + } + + if (cfg.serviceLookupCaching()) { + cache.put(criteria, List.copyOf(result)); + } + + return (List) result; + } + + ServiceProvider serviceProviderFor(String serviceTypeName) { + ServiceProvider serviceProvider = servicesByTypeName.get(serviceTypeName); + if (serviceProvider == null) { + throw resolutionBasedInjectionError(serviceTypeName); + } + return serviceProvider; + } + + List> allServiceProviders(boolean explode) { + if (explode) { + return explodeAndSort(servicesByTypeName.values(), null, false); + } + + return new ArrayList<>(servicesByTypeName.values()); + } + + ServiceBinder createServiceBinder(PicoServices picoServices, + DefaultServices services, + String moduleName, + boolean trusted) { + assert (picoServices.services() == services); + return DefaultServiceBinder.create(picoServices, moduleName, trusted); + } + + void bind(PicoServices picoServices, + DefaultInjectionPlanBinder binder, + Application app) { + String appName = app.named().orElse(app.getClass().getName()); + boolean isLoggable = DefaultPicoServices.LOGGER.isLoggable(System.Logger.Level.INFO); + if (isLoggable) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.INFO, "starting binding application: " + appName); + } + try { + app.configure(binder); + bind(createServiceProvider(app, picoServices)); + if (isLoggable) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.INFO, "finished binding application: " + appName); + } + } catch (Exception e) { + throw new PicoException("failed to process: " + app, e); + } + } + + void bind(PicoServices picoServices, + Module module, + boolean initializing) { + String moduleName = module.named().orElse(module.getClass().getName()); + boolean isLoggable = DefaultPicoServices.LOGGER.isLoggable(System.Logger.Level.TRACE); + if (isLoggable) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.TRACE, "starting binding module: " + moduleName); + } + ServiceBinder moduleServiceBinder = createServiceBinder(picoServices, this, moduleName, initializing); + module.configure(moduleServiceBinder); + bind(createServiceProvider(module, moduleName, picoServices)); + if (isLoggable) { + DefaultPicoServices.LOGGER.log(System.Logger.Level.TRACE, "finished binding module: " + moduleName); + } + } + + private ServiceProvider createServiceProvider(io.helidon.pico.Module module, + String moduleName, + PicoServices picoServices) { + return new PicoModuleServiceProvider(module, moduleName, picoServices); + } + + private ServiceProvider createServiceProvider(Application app, + PicoServices picoServices) { + return new PicoApplicationServiceProvider(app, picoServices); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/Dependencies.java b/pico/services/src/main/java/io/helidon/pico/services/Dependencies.java new file mode 100644 index 00000000000..0f935f4db3c --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/Dependencies.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2023 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.services; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultDependenciesInfo; +import io.helidon.pico.DefaultDependencyInfo; +import io.helidon.pico.DefaultInjectionPointInfo; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.DependencyInfo; +import io.helidon.pico.ElementInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.ServiceInfoCriteria; + +/** + * This is the class the code-generator will target that will be used at runtime for a service provider to build up its + * dependencies expressed as {@link io.helidon.pico.DependenciesInfo}. + */ +public class Dependencies { + + private Dependencies() { + } + + /** + * Creates a builder. + * + * @param serviceTypeName the service type name + * @return the fluent builder + */ + public static BuilderContinuation builder(String serviceTypeName) { + Objects.requireNonNull(serviceTypeName); + return new BuilderContinuation(serviceTypeName); + } + + + /** + * The continuation builder. This is a specialized builder used within the generated Pico {@link io.helidon.pico.Activator}. + * It is specialized in that it validates and decorates over the normal builder, and provides a more streamlined interface. + */ + public static class BuilderContinuation { + private DefaultDependenciesInfo.Builder builder; + private DefaultInjectionPointInfo.Builder ipInfoBuilder; + + private BuilderContinuation(String serviceTypeName) { + this.builder = DefaultDependenciesInfo.builder() + .fromServiceTypeName(serviceTypeName); + } + + /** + * Adds a new dependency item. + * + * @param elemName the element name + * @param elemType the element type + * @param kind the element kind + * @param access the element access + * @return the builder + */ + // note: called from generated code + public BuilderContinuation add(String elemName, + Class elemType, + InjectionPointInfo.ElementKind kind, + InjectionPointInfo.Access access) { + if (InjectionPointInfo.ElementKind.FIELD != kind && Void.class != elemType) { + throw new IllegalStateException("should not use this method for method types"); + } + String fromServiceTypeName = builder.fromServiceTypeName().orElseThrow(); + return add(fromServiceTypeName, elemName, elemType.getName(), kind, 0, access); + } + + /** + * Adds a new dependency item. + * + * @param elemName the element name + * @param elemType the element type + * @param kind the element kind + * @param elemArgs for methods, the number of arguments the method takes + * @param access the element access + * @return the builder + */ + // note: called from generated code + public BuilderContinuation add(String elemName, + Class elemType, + InjectionPointInfo.ElementKind kind, + int elemArgs, + InjectionPointInfo.Access access) { + if (InjectionPointInfo.ElementKind.FIELD == kind && 0 != elemArgs) { + throw new IllegalStateException("should not have args for field: " + elemName); + } + String fromServiceTypeName = builder.fromServiceTypeName().orElseThrow(); + return add(fromServiceTypeName, elemName, elemType.getName(), kind, elemArgs, access); + } + + /** + * Adds a new dependency item. + * + * @param serviceType the service type + * @param elemName the element name + * @param elemType the element type + * @param kind the element kind + * @param access the element access + * @return the builder + */ + // note: called from generated code + public BuilderContinuation add(Class serviceType, + String elemName, + Class elemType, + InjectionPointInfo.ElementKind kind, + InjectionPointInfo.Access access) { + if (InjectionPointInfo.ElementKind.FIELD != kind) { + throw new IllegalStateException("should not use this for method types"); + } + return add(serviceType.getName(), elemName, elemType.getName(), kind, 0, access); + } + + /** + * Adds a new dependency item. + * + * @param serviceType the service type + * @param elemName the element name + * @param elemType the element type + * @param kind the element kind + * @param elemArgs used for methods only; the number of arguments the method accepts + * @param access the element access + * @return the builder + */ + // note: called from generated code + public BuilderContinuation add(Class serviceType, + String elemName, + Class elemType, + InjectionPointInfo.ElementKind kind, + int elemArgs, + InjectionPointInfo.Access access) { + return add(serviceType.getName(), elemName, elemType.getName(), kind, elemArgs, access); + } + + /** + * Adds a new dependency item. + * + * @param ipInfo the injection point info already built + * @return the builder + */ + public BuilderContinuation add(InjectionPointInfo ipInfo) { + commitLastDependency(); + + ipInfoBuilder = DefaultInjectionPointInfo.toBuilder(ipInfo); + return this; + } + + /** + * Sets the element offset. + * + * @param offset the offset + * @return the builder + */ + // note: called from generated code + public BuilderContinuation elemOffset(Integer offset) { + ipInfoBuilder.elementOffset(Optional.ofNullable(offset)); + return this; + } + + /** + * Sets the flag indicating the injection point is a list. + * + * @return the builder + */ + // note: called from generated code + public BuilderContinuation listWrapped() { + return listWrapped(true); + } + + /** + * Sets the flag indicating the injection point is a list. + * + * @param val true if list type + * @return the builder + */ + // note: called from generated code + public BuilderContinuation listWrapped(boolean val) { + ipInfoBuilder.listWrapped(val); + return this; + } + + /** + * Sets the flag indicating the injection point is a provider. + * + * @return the builder + */ + // note: called from generated code + public BuilderContinuation providerWrapped() { + return providerWrapped(true); + } + + /** + * Sets the flag indicating the injection point is a provider. + * + * @param val true if provider type + * @return the builder + */ + // note: called from generated code + public BuilderContinuation providerWrapped(boolean val) { + ipInfoBuilder.providerWrapped(val); + return this; + } + + /** + * Sets the flag indicating the injection point is an {@link java.util.Optional} type. + * + * @return the builder + */ + // note: called from generated code + public BuilderContinuation optionalWrapped() { + return optionalWrapped(true); + } + + /** + * Sets the flag indicating the injection point is an {@link java.util.Optional} type. + * + * @param val true if list type + * @return the builder + */ + // note: called from generated code + public BuilderContinuation optionalWrapped(boolean val) { + ipInfoBuilder.optionalWrapped(val); + return this; + } + + /** + * Sets the optional qualified name of the injection point. + * + * @param val the name + * @return the builder + */ + public BuilderContinuation named(String val) { + ipInfoBuilder.addQualifier(DefaultQualifierAndValue.createNamed(val)); + return this; + } + + /** + * Sets the optional qualifier of the injection point. + * + * @param val the qualifier + * @return the builder + */ + // note: called from generated code + public BuilderContinuation addQualifier(Class val) { + ipInfoBuilder.addQualifier(DefaultQualifierAndValue.create(val)); + return this; + } + + /** + * Sets the optional qualifier of the injection point. + * + * @param val the qualifier + * @return the builder + */ + // note: called from generated code + public BuilderContinuation addQualifier(QualifierAndValue val) { + ipInfoBuilder.addQualifier(val); + return this; + } + + /** + * Sets the optional qualifier of the injection point. + * + * @param val the qualifier + * @return the builder + */ + public BuilderContinuation qualifiers(Collection val) { + ipInfoBuilder.qualifiers(val); + return this; + } + + /** + * Sets the flag indicating that the injection point is static. + * + * @param val flag indicating if static + * @return the builder + */ + public BuilderContinuation staticDeclaration(boolean val) { + ipInfoBuilder.staticDeclaration(val); + return this; + } + + /** + * Commits the last dependency item, and prepares for the next. + * + * @return the builder + */ + public DependenciesInfo build() { + assert (builder != null); + + commitLastDependency(); + DependenciesInfo deps = builder.build(); + builder = null; + return deps; + } + + /** + * Adds a new dependency item. + * + * @param serviceTypeName the service type + * @param elemName the element name + * @param elemTypeName the element type + * @param kind the element kind + * @param elemArgs used for methods only; this is the number of arguments the method accepts + * @param access the element access + * @return the builder + */ + public BuilderContinuation add(String serviceTypeName, + String elemName, + String elemTypeName, + InjectionPointInfo.ElementKind kind, + int elemArgs, + InjectionPointInfo.Access access) { + commitLastDependency(); + + // thus begins a new builder continuation round + ipInfoBuilder = DefaultInjectionPointInfo.builder() + .serviceTypeName(serviceTypeName) + .access(access) + .elementKind(kind) + .elementTypeName(elemTypeName) + .elementName(elemName) + .elementOffset(Optional.ofNullable(ElementInfo.ElementKind.FIELD == kind ? null : 0)) + .elementArgs(elemArgs); + return this; + } + + /** + * Commits the last dependency item to complete the last builder continuation. + * + * @return any built dependencies info realized from this last commit + */ + // note: called from generated code + public Optional commitLastDependency() { + String id = null; + try { + assert (builder != null); + + if (ipInfoBuilder != null) { + id = toId(ipInfoBuilder); + ipInfoBuilder.baseIdentity(toBaseIdentity(ipInfoBuilder)); + ipInfoBuilder.id(id); + ServiceInfoCriteria criteria = DefaultServiceInfoCriteria.builder() + .addContractImplemented(ipInfoBuilder.elementTypeName()) + .qualifiers(ipInfoBuilder.qualifiers()) + .build(); + InjectionPointInfo ipInfo = ipInfoBuilder + .dependencyToServiceInfo(criteria) + .build(); + ipInfoBuilder = null; + + DependencyInfo dep = DefaultDependencyInfo.builder() + .addInjectionPointDependency(ipInfo) + .dependencyTo(ipInfo.dependencyToServiceInfo()) + .build(); + builder.addServiceInfoDependency(ipInfo.dependencyToServiceInfo(), dep); + return Optional.of(dep); + } + + return Optional.empty(); + } catch (Exception e) { + throw new IllegalStateException("failed to commit a dependency: " + id, e); + } + } + } + + + /** + * Combine the dependency info from the two sources to create a merged set of dependencies. + * + * @param parentDeps the parent set of dependencies + * @param deps the child set of dependencies + * @return the combined set + */ + public static DependenciesInfo combine(DependenciesInfo parentDeps, + DependenciesInfo deps) { + Objects.requireNonNull(parentDeps); + Objects.requireNonNull(deps); + + DefaultDependenciesInfo.Builder builder = (deps instanceof DefaultDependenciesInfo.Builder) + ? (DefaultDependenciesInfo.Builder) deps + : DefaultDependenciesInfo.toBuilder(deps); + parentDeps.serviceInfoDependencies().forEach(builder::addServiceInfoDependency); + return forceBuild(builder); + } + + /** + * Returns the non-builder version of the passed dependencies. + * + * @param deps the dependencies, but might be actually in builder form + * @return will always be the built version of the dependencies + */ + private static DependenciesInfo forceBuild(DependenciesInfo deps) { + Objects.requireNonNull(deps); + + if (deps instanceof DefaultDependenciesInfo.Builder) { + deps = ((DefaultDependenciesInfo.Builder) deps).build(); + } + + return deps; + } + + static String toBaseIdentity(InjectionPointInfo dep) { + ElementInfo.ElementKind kind = Objects.requireNonNull(dep.elementKind()); + String elemName = Objects.requireNonNull(dep.elementName()); + ElementInfo.Access access = Objects.requireNonNull(dep.access()); + Supplier packageName = toPackageName(dep.serviceTypeName()); + + String baseId; + if (ElementInfo.ElementKind.FIELD == kind) { + baseId = toFieldIdentity(elemName, packageName); + } else { + baseId = toMethodBaseIdentity(elemName, + dep.elementArgs().orElseThrow(), + access, packageName); + } + return baseId; + } + + static String toId(InjectionPointInfo dep) { + ElementInfo.ElementKind kind = Objects.requireNonNull(dep.elementKind()); + String elemName = Objects.requireNonNull(dep.elementName()); + ElementInfo.Access access = Objects.requireNonNull(dep.access()); + Supplier packageName = toPackageName(dep.serviceTypeName()); + + String id; + if (ElementInfo.ElementKind.FIELD == kind) { + id = toFieldIdentity(elemName, packageName); + } else { + id = toMethodIdentity(elemName, + dep.elementArgs().orElseThrow(), + dep.elementOffset().orElseThrow(() -> new IllegalStateException("failed on " + elemName)), + access, + packageName); + } + return id; + } + + private static Supplier toPackageName(String serviceTypeName) { + return () -> toPackageName(DefaultTypeName.createFromTypeName(serviceTypeName)); + } + + private static String toPackageName(TypeName typeName) { + return (typeName != null) ? typeName.packageName() : null; + } + + /** + * The field's identity and its base identity are the same since there is no arguments to handle. + * + * @param elemName the non-null field name + * @param packageName the package name of the owning service type containing the field + * @return the field identity (relative to the owning service type) + */ + public static String toFieldIdentity(String elemName, + Supplier packageName) { + String id = Objects.requireNonNull(elemName); + String pName = (packageName == null) ? null : packageName.get(); + if (pName != null) { + id = pName + "." + id; + } + return id; + } + + /** + * Computes the base identity given the method name and the number of arguments to the method. + * + * @param elemName the method name + * @param methodArgCount the number of arguments to the method + * @param access the method's access + * @param packageName the method's enclosing package name + * @return the base identity (relative to the owning service type) + */ + public static String toMethodBaseIdentity(String elemName, + int methodArgCount, + ElementInfo.Access access, + Supplier packageName) { + String id = Objects.requireNonNull(elemName) + "|" + methodArgCount; + if (ElementInfo.Access.PACKAGE_PRIVATE == access || elemName.equals(InjectionPointInfo.CONSTRUCTOR)) { + String pName = (packageName == null) ? null : packageName.get(); + if (pName != null) { + id = pName + "." + id; + } + } + return id; + } + + /** + * Computes the method's unique identity, taking into consideration the number of args it accepts + * plus any optionally provided specific argument offset position. + * + * @param elemName the method name + * @param methodArgCount the number of arguments to the method + * @param elemOffset the optional parameter offset + * @param access the access for the method + * @param packageName the package name of the owning service type containing the method + * @return the unique identity (relative to the owning service type) + */ + public static String toMethodIdentity(String elemName, + int methodArgCount, + Integer elemOffset, + ElementInfo.Access access, + Supplier packageName) { + String result = toMethodBaseIdentity(elemName, methodArgCount, access, packageName); + + if (elemOffset == null) { + return result; + } + + assert (elemOffset <= methodArgCount) : result; + return result + "(" + elemOffset + ")"; + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/InterceptedMethod.java b/pico/services/src/main/java/io/helidon/pico/services/InterceptedMethod.java new file mode 100644 index 00000000000..34e1de4e449 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/InterceptedMethod.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.DefaultInvocationContext; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationContext; +import io.helidon.pico.InvocationException; +import io.helidon.pico.ServiceProvider; + +import jakarta.inject.Provider; + +/** + * Base class, used in {@link io.helidon.pico.Interceptor} generated code. One of these instances will be created for each + * intercepted method. + * + * @param the intercepted type + * @param the intercepted method return type + */ +public abstract class InterceptedMethod implements Function { + private final I impl; + private final InvocationContext ctx; + + /** + * The constructor. + * + * @param interceptedImpl the intercepted instance + * @param serviceProvider the service provider for the intercepted type + * @param serviceTypeName the service type name + * @param serviceLevelAnnotations the service level annotations + * @param interceptors the interceptors for the method + * @param methodInfo the method element info + * @param methodArgInfo the method args element info + */ + protected InterceptedMethod(I interceptedImpl, + ServiceProvider serviceProvider, + TypeName serviceTypeName, + Collection serviceLevelAnnotations, + Collection> interceptors, + TypedElementName methodInfo, + TypedElementName[] methodArgInfo) { + this.impl = Objects.requireNonNull(interceptedImpl); + this.ctx = DefaultInvocationContext.builder() + .serviceProvider(serviceProvider) + .serviceTypeName(serviceTypeName) + .classAnnotations(serviceLevelAnnotations) + .interceptors(interceptors) + .elementInfo(methodInfo) + .elementArgInfo(methodArgInfo) + .build(); + } + + /** + * The constructor. + * + * @param interceptedImpl the intercepted instance + * @param serviceProvider the service provider for the intercepted type + * @param serviceTypeName the service type name + * @param serviceLevelAnnotations the service level annotations + * @param interceptors the interceptors for the method + * @param methodInfo the method element info + */ + protected InterceptedMethod(I interceptedImpl, + ServiceProvider serviceProvider, + TypeName serviceTypeName, + Collection serviceLevelAnnotations, + Collection> interceptors, + TypedElementName methodInfo) { + this.impl = Objects.requireNonNull(interceptedImpl); + this.ctx = DefaultInvocationContext.builder() + .serviceProvider(serviceProvider) + .serviceTypeName(serviceTypeName) + .classAnnotations(serviceLevelAnnotations) + .interceptors(interceptors) + .elementInfo(methodInfo) + .build(); + } + + /** + * The intercepted instance. + * + * @return the intercepted instance + */ + public I impl() { + return impl; + } + + /** + * The intercepted invocation context. + * + * @return the intercepted invocation context + */ + public InvocationContext ctx() { + return ctx; + } + + /** + * Make the invocation to an interceptor method. + * + * @param args arguments + * @return the result of the call to the intercepted method + * @throws java.lang.Throwable if there is any throwables encountered as part of the invocation + */ + public abstract V invoke(Object... args) throws Throwable; + + @Override + public V apply(Object... args) { + try { + return invoke(args); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new InvocationException(t.getMessage(), t, ctx.serviceProvider()); + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/Invocation.java b/pico/services/src/main/java/io/helidon/pico/services/Invocation.java new file mode 100644 index 00000000000..68250ee4c44 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/Invocation.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationContext; +import io.helidon.pico.ServiceProvider; + +import jakarta.inject.Provider; + +/** + * Handles the invocation of {@link Interceptor} methods. + * + * @see io.helidon.pico.InvocationContext + * @param the invocation type + */ +public class Invocation implements Interceptor.Chain { + private final InvocationContext ctx; + private final ListIterator> interceptorIterator; + private Supplier call; + + private Invocation(InvocationContext ctx, + Supplier call) { + this.ctx = ctx; + this.call = Objects.requireNonNull(call); + this.interceptorIterator = ctx.interceptors().listIterator(); + } + + /** + * Creates an instance of {@link Invocation} and invokes it in this context. + * + * @param ctx the invocation context + * @param call the call to the base service provider's method + * @param args the call arguments + * @param the type returned from the method element + * @return the invocation instance + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static V createInvokeAndSupply(InvocationContext ctx, + Supplier call, + Object... args) { + if (ctx.interceptors().isEmpty()) { + return call.get(); + } else { + return (V) new Invocation(ctx, call).proceed(args); + } + } + + /** + * Merges a variable number of lists together, where the net result is the merged set of non-null providers + * ranked in proper weight order, or else empty list. + * + * @param lists the lists to merge + * @param the type of the provider + * @return the merged result, or null instead of empty lists + */ + @SuppressWarnings("unchecked") + public static List> mergeAndCollapse(List>... lists) { + List> result = null; + + for (List> list : lists) { + if (list == null) { + continue; + } + + for (Provider p : list) { + if (p == null) { + continue; + } + + if (p instanceof ServiceProvider + && VoidServiceProvider.serviceTypeName().equals( + ((ServiceProvider) p).serviceInfo().serviceTypeName())) { + continue; + } + + if (result == null) { + result = new ArrayList<>(); + } + if (!result.contains(p)) { + result.add(p); + } + } + } + + if (result != null && result.size() > 1) { + result.sort(DefaultServices.serviceProviderComparator()); + } + + return (result != null) ? Collections.unmodifiableList(result) : List.of(); + } + + @Override + public V proceed(Object... args) { + if (!interceptorIterator.hasNext()) { + if (this.call != null) { + Supplier call = this.call; + this.call = null; + return call.get(); + } else { + throw new IllegalStateException("unknown call type: " + this); + } + } else { + return interceptorIterator.next() + .get() + .proceed(ctx, this, args); + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/NonSingletonServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/NonSingletonServiceProvider.java new file mode 100644 index 00000000000..a526cd61895 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/NonSingletonServiceProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.pico.Phase; + +/** + * A provider that represents a non-singleton service. + * + * @param the type of the service this provider manages + */ +class NonSingletonServiceProvider extends AbstractServiceProvider { + @SuppressWarnings("FieldCanBeLocal") + private final AbstractServiceProvider delegate; + + private NonSingletonServiceProvider(AbstractServiceProvider delegate) { + this.delegate = delegate; + picoServices(Optional.of(delegate.picoServices())); + serviceInfo(delegate.serviceInfo()); + dependencies(delegate.dependencies()); + } + + static T createAndActivate(AbstractServiceProvider delegate) { + NonSingletonServiceProvider serviceProvider = new NonSingletonServiceProvider<>(delegate); + + LogEntryAndResult logEntryAndResult = serviceProvider.createLogEntryAndResult(Phase.ACTIVE); + serviceProvider.startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVATION_STARTING); + + serviceProvider.startTransitionCurrentActivationPhase(logEntryAndResult, Phase.GATHERING_DEPENDENCIES); + Map plans = delegate.getOrCreateInjectionPlan(false); + logEntryAndResult.activationResult().injectionPlans(plans); + Map deps = delegate.resolveDependencies(plans); + logEntryAndResult.activationResult().resolvedDependencies(deps); + serviceProvider.finishedTransitionCurrentActivationPhase(logEntryAndResult); + + serviceProvider.startTransitionCurrentActivationPhase(logEntryAndResult, Phase.CONSTRUCTING); + T instance = delegate.createServiceProvider(deps); + serviceProvider.finishedTransitionCurrentActivationPhase(logEntryAndResult); + + if (instance != null) { + serviceProvider.startTransitionCurrentActivationPhase(logEntryAndResult, Phase.INJECTING); + List serviceTypeOrdering = Objects.requireNonNull(delegate.serviceTypeInjectionOrder()); + LinkedHashSet injections = new LinkedHashSet<>(); + serviceTypeOrdering.forEach((forServiceType) -> { + delegate.doInjectingFields(instance, deps, injections, forServiceType); + delegate.doInjectingMethods(instance, deps, injections, forServiceType); + }); + serviceProvider.finishedTransitionCurrentActivationPhase(logEntryAndResult); + + serviceProvider.startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVATION_STARTING); + + serviceProvider.startTransitionCurrentActivationPhase(logEntryAndResult, Phase.POST_CONSTRUCTING); + serviceProvider.doPostConstructing(logEntryAndResult); + serviceProvider.finishedTransitionCurrentActivationPhase(logEntryAndResult); + + serviceProvider.startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVATION_FINISHING); + } + + serviceProvider.startAndFinishTransitionCurrentActivationPhase(logEntryAndResult, Phase.ACTIVE); + + return instance; + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/PicoApplicationServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/PicoApplicationServiceProvider.java new file mode 100644 index 00000000000..57cfe0f44bf --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/PicoApplicationServiceProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.services; + +import io.helidon.pico.Application; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceInfo; + +/** + * Basic {@link io.helidon.pico.Application} implementation. A Pico application is-a service provider also. + */ +class PicoApplicationServiceProvider extends AbstractServiceProvider { + + PicoApplicationServiceProvider(Application app, PicoServices picoServices) { + super(app, Phase.ACTIVE, createServiceInfo(app), picoServices); + serviceRef(app); + } + + static ServiceInfo createServiceInfo(Application app) { + DefaultServiceInfo.Builder builder = DefaultServiceInfo.builder() + .serviceTypeName(app.getClass().getName()) + .addContractsImplemented(Application.class.getName()); + app.named().ifPresent(name -> builder.addQualifier(DefaultQualifierAndValue.createNamed(name))); + return builder.build(); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/PicoInjectionPlan.java b/pico/services/src/main/java/io/helidon/pico/services/PicoInjectionPlan.java new file mode 100644 index 00000000000..a158b4368f4 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/PicoInjectionPlan.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.List; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.pico.spi.InjectionPlan; + +/** + * The injection plan for a given service provider and element belonging to that service provider. This plan can be created during + * compile-time, and then just loaded from the {@link io.helidon.pico.Application} during Pico bootstrap initialization, or it can + * be produced during the same startup processing sequence if the Application was not found, or if it was not permitted to be + * loaded. + */ +@Builder +public interface PicoInjectionPlan extends InjectionPlan { + + /** + * The list of services/providers that are unqualified to satisfy the given injection point but were considered. + * + * @return the unqualified services/providers for this injection point + */ + @Singular + List unqualifiedProviders(); + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/PicoModuleServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/PicoModuleServiceProvider.java new file mode 100644 index 00000000000..55f97fe5ef8 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/PicoModuleServiceProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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.services; + +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceInfo; + +/** + * Basic {@link io.helidon.pico.Module} implementation. A Pico module is-a service provider also. + */ +class PicoModuleServiceProvider extends AbstractServiceProvider { + + PicoModuleServiceProvider(io.helidon.pico.Module module, + String moduleName, + PicoServices picoServices) { + super(module, PicoServices.terminalActivationPhase(), createServiceInfo(module, moduleName), picoServices); + serviceRef(module); + } + + static ServiceInfo createServiceInfo(io.helidon.pico.Module module, + String moduleName) { + DefaultServiceInfo.Builder builder = DefaultServiceInfo.builder() + .serviceTypeName(module.getClass().getName()) + .addContractsImplemented(io.helidon.pico.Module.class.getName()); + if (moduleName != null) { + builder.moduleName(moduleName) + .addQualifier(DefaultQualifierAndValue.createNamed(moduleName)); + } + return builder.build(); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/ServiceProviderComparator.java b/pico/services/src/main/java/io/helidon/pico/services/ServiceProviderComparator.java new file mode 100644 index 00000000000..efe42e92d2c --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/ServiceProviderComparator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 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.services; + +import java.io.Serializable; +import java.util.Comparator; + +import io.helidon.common.Weights; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceProvider; + +import jakarta.inject.Provider; + +/** + * A comparator appropriate for service providers, first using its {@link io.helidon.common.Weight} and then service type name + * to determine its natural ordering. + */ +public class ServiceProviderComparator implements Comparator>, Serializable { + private static final ServiceProviderComparator INSTANCE = new ServiceProviderComparator(); + + private ServiceProviderComparator() { + } + + /** + * Returns a service provider comparator. + * + * @return the service provider comparator + */ + public static ServiceProviderComparator create() { + return INSTANCE; + } + + @Override + public int compare(Provider p1, + Provider p2) { + if (p1 == p2) { + return 0; + } + + if (p1 instanceof ServiceProvider + && p2 instanceof ServiceProvider) { + ServiceProvider sp1 = (ServiceProvider) p1; + ServiceProvider sp2 = (ServiceProvider) p2; + + ServiceInfo info1 = sp1.serviceInfo(); + ServiceInfo info2 = sp2.serviceInfo(); + if (info1 == info2) { + return 0; + } + + double w1 = info1.realizedWeight(); + double w2 = info2.realizedWeight(); + int comp = Double.compare(w1, w2); + if (0 != comp) { + return -1 * comp; + } + // secondary ordering based upon its name... + String name1 = info1.serviceTypeName(); + String name2 = info2.serviceTypeName(); + comp = name2.compareTo(name1); + return -1 * comp; + } else { + return Weights.weightComparator().compare(p1, p2); + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/ServiceUtils.java b/pico/services/src/main/java/io/helidon/pico/services/ServiceUtils.java new file mode 100644 index 00000000000..3546bc0455d --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/ServiceUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.pico.Application; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceProvider; + +/** + * Public helpers around shared Pico services usages. + */ +public final class ServiceUtils { + + private ServiceUtils() { + } + + /** + * Determines if the service provider is valid to receive injections. + * + * @param sp the service provider + * @return true if the service provider can receive injection + */ + public static boolean isQualifiedInjectionTarget(ServiceProvider sp) { + ServiceInfo serviceInfo = sp.serviceInfo(); + Set contractsImplemented = serviceInfo.contractsImplemented(); + DependenciesInfo deps = sp.dependencies(); + return (deps != AbstractServiceProvider.NO_DEPS) + || (!contractsImplemented.isEmpty() + && !contractsImplemented.contains(io.helidon.pico.Module.class.getName()) + && !contractsImplemented.contains(Application.class.getName())); + } + + /** + * Provides a {@link io.helidon.pico.ServiceProvider#description()}, falling back to {@link #toString()} on the passed + * provider argument. + * + * @param provider the provider + * @return the description + */ + public static String toDescription(Object provider) { + if (provider instanceof Optional) { + provider = ((Optional) provider).orElse(null); + } + + if (provider instanceof ServiceProvider) { + return ((ServiceProvider) provider).description(); + } + return String.valueOf(provider); + } + + /** + * Provides a {@link io.helidon.pico.ServiceProvider#description()}, falling back to {@link #toString()} on the passed + * provider argument. + * + * @param coll the collection of providers + * @return the description + */ + public static List toDescriptions(Collection coll) { + return coll.stream().map(ServiceUtils::toDescription).collect(Collectors.toList()); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/State.java b/pico/services/src/main/java/io/helidon/pico/services/State.java new file mode 100644 index 00000000000..c7afeaa1619 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/State.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Objects; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import io.helidon.pico.Phase; +import io.helidon.pico.Resettable; + +class State implements Resettable, Cloneable { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private Phase currentPhase; + private boolean isFinished; + private Throwable lastError; + + private State() { + } + + static State create(Phase phase) { + return new State().currentPhase(phase); + } + + @Override + public State clone() { + ReentrantReadWriteLock.ReadLock rlock = lock.readLock(); + rlock.lock(); + try { + return create(currentPhase()).finished(finished()).lastError(lastError()); + } finally { + rlock.unlock(); + } + } + + @Override + public String toString() { + ReentrantReadWriteLock.WriteLock rlock = lock.writeLock(); + rlock.lock(); + try { + return "currentPhase=" + currentPhase + ", isFinished=" + isFinished + ", lastError=" + lastError; + } finally { + rlock.unlock(); + } + } + + @Override + public boolean reset(boolean deep) { + ReentrantReadWriteLock.WriteLock wlock = lock.writeLock(); + wlock.lock(); + try { + currentPhase(Phase.INIT).finished(false).lastError(null); + return true; + } finally { + wlock.unlock(); + } + } + + State currentPhase(Phase phase) { + ReentrantReadWriteLock.WriteLock wlock = lock.writeLock(); + wlock.lock(); + try { + Phase lastPhase = this.currentPhase; + this.currentPhase = Objects.requireNonNull(phase); + if (lastPhase != this.currentPhase) { + this.isFinished = false; + this.lastError = null; + } + return this; + } finally { + wlock.unlock(); + } + } + + Phase currentPhase() { + ReentrantReadWriteLock.ReadLock rlock = lock.readLock(); + rlock.lock(); + try { + return currentPhase; + } finally { + rlock.unlock(); + } + } + + State finished(boolean finished) { + ReentrantReadWriteLock.WriteLock wlock = lock.writeLock(); + wlock.lock(); + try { + this.isFinished = finished; + return this; + } finally { + wlock.unlock(); + } + } + + boolean finished() { + ReentrantReadWriteLock.ReadLock rlock = lock.readLock(); + rlock.lock(); + try { + return isFinished; + } finally { + rlock.unlock(); + } + } + + State lastError(Throwable t) { + ReentrantReadWriteLock.WriteLock wlock = lock.writeLock(); + wlock.lock(); + try { + this.lastError = t; + return this; + } finally { + wlock.unlock(); + } + } + + Throwable lastError() { + ReentrantReadWriteLock.ReadLock rlock = lock.readLock(); + rlock.lock(); + try { + return lastError; + } finally { + rlock.unlock(); + } + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/Versions.java b/pico/services/src/main/java/io/helidon/pico/services/Versions.java new file mode 100644 index 00000000000..5f6b3d6caae --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/Versions.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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.services; + +/** + * Keeps track of the Pico Interop Versions. + *

        + * Since Pico performs code-generation, each previously generated artifact version may need to be discoverable in order to + * determine interoperability with previous release versions. This class will only track version changes for anything that might + * affect interoperability - it will not be rev'ed for general code enhancements and fixes. + *

        + * Please note that this version is completely independent of the Helidon version and other features and modules within Helidon. + */ +public class Versions { + + /** + * Version 1 - the initial release of Pico. + */ + public static final String PICO_VERSION_1 = "1"; + + /** + * The current release is {@link #PICO_VERSION_1}. + */ + public static final String CURRENT_PICO_VERSION = PICO_VERSION_1; + + private Versions() { + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/VoidServiceProvider.java b/pico/services/src/main/java/io/helidon/pico/services/VoidServiceProvider.java new file mode 100644 index 00000000000..862b0cc6503 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/VoidServiceProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.ServiceProvider; + +import jakarta.inject.Singleton; + +/** + * A proxy service provider created internally by the framework. + */ +class VoidServiceProvider extends AbstractServiceProvider { + static final VoidServiceProvider INSTANCE = new VoidServiceProvider() {}; + static final List> LIST_INSTANCE = List.of(INSTANCE); + + private VoidServiceProvider() { + serviceInfo(DefaultServiceInfo.builder() + .serviceTypeName(serviceTypeName()) + .addContractsImplemented(serviceTypeName()) + .activatorTypeName(VoidServiceProvider.class.getName()) + .addScopeTypeName(Singleton.class.getName()) + .declaredWeight(DEFAULT_WEIGHT) + .build()); + } + + public static String serviceTypeName() { + return Void.class.getName(); + } + + @Override + protected Void createServiceProvider(Map resolvedDeps) { + // this must return null by definition + return null; + } + + @Override + public Optional first(ContextualServiceQuery query) { + return Optional.empty(); + } + +} diff --git a/pico/services/src/main/java/io/helidon/pico/services/package-info.java b/pico/services/src/main/java/io/helidon/pico/services/package-info.java new file mode 100644 index 00000000000..536b8ffa040 --- /dev/null +++ b/pico/services/src/main/java/io/helidon/pico/services/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 runtime services. + */ +package io.helidon.pico.services; diff --git a/pico/builder-config/builder-config/src/main/java/module-info.java b/pico/services/src/main/java/module-info.java similarity index 52% rename from pico/builder-config/builder-config/src/main/java/module-info.java rename to pico/services/src/main/java/module-info.java index 8eb222c048a..bf8ca89faf7 100644 --- a/pico/builder-config/builder-config/src/main/java/module-info.java +++ b/pico/services/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -15,22 +15,22 @@ */ /** - * The Helidon Pico Config Builder API / SPI + * The Pico Runtime Services Module. */ -module io.helidon.pico.builder.config { +module io.helidon.pico.services { requires static jakarta.inject; + requires static jakarta.annotation; requires io.helidon.builder; + requires io.helidon.common.types; requires io.helidon.common; requires io.helidon.common.config; + requires transitive io.helidon.pico.api; - uses io.helidon.pico.builder.config.spi.ConfigBeanMapperProvider; - uses io.helidon.pico.builder.config.spi.ConfigBeanBuilderValidatorProvider; - uses io.helidon.pico.builder.config.spi.ConfigResolverProvider; - uses io.helidon.pico.builder.config.spi.StringValueParserProvider; + exports io.helidon.pico.services; - exports io.helidon.pico.builder.config; - exports io.helidon.pico.builder.config.spi; + provides io.helidon.pico.spi.PicoServicesProvider + with io.helidon.pico.services.DefaultPicoServicesProvider; - provides io.helidon.pico.builder.config.spi.ConfigResolverProvider - with io.helidon.pico.builder.config.spi.DefaultConfigResolver; + uses io.helidon.pico.Module; + uses io.helidon.pico.Application; } diff --git a/pico/services/src/test/java/io/helidon/pico/services/DefaultActivationLogTest.java b/pico/services/src/test/java/io/helidon/pico/services/DefaultActivationLogTest.java new file mode 100644 index 00000000000..50aef404a34 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/DefaultActivationLogTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022, 2023 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.services; + +import io.helidon.pico.DefaultActivationLogEntry; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class DefaultActivationLogTest { + + private static final System.Logger LOGGER = System.getLogger(DefaultActivationLogTest.class.getName()); + + @Test + void testRetainedLog() { + DefaultActivationLog log = DefaultActivationLog.createRetainedLog(LOGGER); + log.level(System.Logger.Level.INFO); + log.record(DefaultActivationLogEntry.builder().build()); + + assertThat(log.toQuery(), optionalPresent()); + assertThat(log.toQuery().orElseThrow().fullActivationLog().size(), equalTo(1)); + assertThat(log.reset(true), equalTo(Boolean.TRUE)); + assertThat(log.reset(true), equalTo(Boolean.FALSE)); + } + + @Test + void unretainedLog() { + DefaultActivationLog log = DefaultActivationLog.createUnretainedLog(LOGGER); + log.level(System.Logger.Level.INFO); + log.record(DefaultActivationLogEntry.builder().build()); + + assertThat(log.toQuery(), optionalEmpty()); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesConfigTest.java b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesConfigTest.java new file mode 100644 index 00000000000..696005e1f1c --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesConfigTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022, 2023 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.services; + +import io.helidon.pico.PicoServicesConfig; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class DefaultPicoServicesConfigTest { + + @Test + void testIt() { + PicoServicesConfig cfg = DefaultPicoServicesConfig.createDefaultConfigBuilder(); + assertThat(cfg.providerName(), equalTo("oracle")); + assertThat(cfg.providerVersion(), equalTo("1")); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java new file mode 100644 index 00000000000..ea735e6df10 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/DefaultPicoServicesTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.Map; +import java.util.Objects; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.services.testsubjects.HelloPicoApplication; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +class DefaultPicoServicesTest { + + @BeforeEach + void setUp() { + tearDown(); + Config config = Config.builder( + ConfigSources.create( + Map.of(PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true"), "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + PicoServices.globalBootstrap(DefaultBootstrap.builder().config(config).build()); + } + + @AfterEach + void tearDown() { + HelloPicoApplication.ENABLED = true; + SimplePicoTestingSupport.resetAll(); + } + + @Test + void realizedServices() { + assertThat(PicoServices.unrealizedServices(), optionalEmpty()); + + Objects.requireNonNull(PicoServices.realizedServices()); + assertThat(PicoServices.unrealizedServices(), optionalPresent()); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java b/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java new file mode 100644 index 00000000000..75b651198c2 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/HelloPicoWorldSanityTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2023 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.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.common.Weighted; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.ActivationRequest; +import io.helidon.pico.ActivationResult; +import io.helidon.pico.Application; +import io.helidon.pico.DeActivationRequest; +import io.helidon.pico.DefaultActivationRequest; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.DefaultInjectorOptions; +import io.helidon.pico.Injector; +import io.helidon.pico.Phase; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.services.testsubjects.HelloPicoApplication; +import io.helidon.pico.services.testsubjects.HelloPicoImpl$$picoActivator; +import io.helidon.pico.services.testsubjects.HelloPicoWorld; +import io.helidon.pico.services.testsubjects.HelloPicoWorldImpl; +import io.helidon.pico.services.testsubjects.PicoWorld; +import io.helidon.pico.services.testsubjects.PicoWorldImpl; +import io.helidon.pico.services.testsubjects.PicoWorldImpl$$picoActivator; +import io.helidon.pico.spi.InjectionPlan; + +import jakarta.inject.Singleton; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +/** + * Sanity type tests only. The "real" testing is in the tests submodules. + */ +class HelloPicoWorldSanityTest { + private static final int EXPECTED_MODULES = 2; + + @BeforeEach + void setUp() { + tearDown(); + Config config = Config.builder( + ConfigSources.create( + Map.of(PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true"), "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + PicoServices.globalBootstrap(DefaultBootstrap.builder().config(config).build()); + } + + @AfterEach + void tearDown() { + HelloPicoApplication.ENABLED = true; + SimplePicoTestingSupport.resetAll(); + } + + @Test + void sanity() { + Services services = PicoServices.realizedServices(); + + List> moduleProviders = services.lookupAll(io.helidon.pico.Module.class); + assertThat(moduleProviders.size(), + equalTo(EXPECTED_MODULES)); + List descriptions = ServiceUtils.toDescriptions(moduleProviders); + assertThat(descriptions, + containsInAnyOrder("EmptyModule:ACTIVE", "HelloPicoModule:ACTIVE")); + + List> applications = services.lookupAll(Application.class); + assertThat(applications.size(), + equalTo(1)); + assertThat(ServiceUtils.toDescriptions(applications), + containsInAnyOrder("HelloPicoApplication:ACTIVE")); + } + + @Test + void standardActivationWithNoApplicationEnabled() { + HelloPicoApplication.ENABLED = false; + Optional picoServices = PicoServices.picoServices(); + ((DefaultPicoServices) picoServices.orElseThrow()).reset(true); + + standardActivation(); + } + + @Test + void standardActivation() { + Services services = PicoServices.realizedServices(); + + ServiceProvider helloProvider1 = services.lookup(HelloPicoWorld.class); + assertThat(helloProvider1, + notNullValue()); + + ServiceProvider helloProvider2 = services.lookup(HelloPicoWorldImpl.class); + assertThat(helloProvider1, + sameInstance(helloProvider2)); + assertThat(helloProvider1.id(), + equalTo(HelloPicoWorldImpl.class.getName())); + assertThat(helloProvider1.currentActivationPhase(), + equalTo(Phase.INIT)); + assertThat(helloProvider1.description(), + equalTo(HelloPicoWorldImpl.class.getSimpleName() + ":" + Phase.INIT)); + + ServiceInfo serviceInfo = helloProvider1.serviceInfo(); + assertThat(serviceInfo.serviceTypeName(), + equalTo(HelloPicoWorldImpl.class.getName())); + assertThat(serviceInfo.contractsImplemented(), + containsInAnyOrder(HelloPicoWorld.class.getName())); + assertThat(serviceInfo.externalContractsImplemented().size(), + equalTo(0)); + assertThat(serviceInfo.scopeTypeNames(), + containsInAnyOrder(Singleton.class.getName())); + assertThat(serviceInfo.qualifiers().size(), + equalTo(0)); + assertThat(serviceInfo.activatorTypeName().orElseThrow(), + equalTo(HelloPicoImpl$$picoActivator.class.getName())); + assertThat(serviceInfo.declaredRunLevel(), + optionalValue(equalTo(0))); + assertThat(serviceInfo.realizedRunLevel(), + equalTo(0)); + assertThat(serviceInfo.moduleName(), + optionalValue(equalTo("example"))); + assertThat(serviceInfo.declaredWeight(), + optionalEmpty()); + assertThat(serviceInfo.realizedWeight(), + equalTo(Weighted.DEFAULT_WEIGHT)); + + ServiceProvider worldProvider1 = services.lookup(PicoWorld.class); + assertThat(worldProvider1, notNullValue()); + assertThat(worldProvider1.description(), + equalTo("PicoWorldImpl:INIT")); + + // now activate + HelloPicoWorld hello1 = helloProvider1.get(); + assertThat(hello1.sayHello(), + equalTo("Hello pico")); + assertThat(helloProvider1.currentActivationPhase(), + equalTo(Phase.ACTIVE)); + assertThat(helloProvider1.description(), + equalTo("HelloPicoWorldImpl:ACTIVE")); + + // world should be active now too, since Hello should have implicitly activated it + assertThat(worldProvider1.description(), + equalTo("PicoWorldImpl:ACTIVE")); + + // check the post construct counts + assertThat(((HelloPicoWorldImpl) helloProvider1.get()).postConstructCallCount(), + equalTo(1)); + assertThat(((HelloPicoWorldImpl) helloProvider1.get()).preDestroyCallCount(), + equalTo(0)); + + // deactivate just the Hello service + ActivationResult result = helloProvider1.deActivator().orElseThrow() + .deactivate(DeActivationRequest.defaultDeactivationRequest()); + assertThat(result.finished(), is(true)); + assertThat(result.success(), is(true)); + assertThat(result.serviceProvider(), sameInstance(helloProvider2)); + assertThat(result.finishingActivationPhase(), + is(Phase.DESTROYED)); + assertThat(result.startingActivationPhase(), + is(Phase.ACTIVE)); + assertThat(helloProvider1.description(), + equalTo("HelloPicoWorldImpl:DESTROYED")); + assertThat(((HelloPicoWorldImpl) hello1).postConstructCallCount(), + equalTo(1)); + assertThat(((HelloPicoWorldImpl) hello1).preDestroyCallCount(), + equalTo(1)); + assertThat(worldProvider1.description(), + equalTo("PicoWorldImpl:ACTIVE")); + } + + @Test + void viaInjector() { + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); + Injector injector = picoServices.injector().orElseThrow(); + + HelloPicoImpl$$picoActivator subversiveWay = new HelloPicoImpl$$picoActivator(); + subversiveWay.picoServices(Optional.of(picoServices)); + + ActivationResult result = injector.activateInject(subversiveWay, DefaultInjectorOptions.builder().build()); + assertThat(result.finished(), is(true)); + assertThat(result.success(), is(true)); + + HelloPicoWorld hello1 = subversiveWay.serviceRef().orElseThrow(); + assertThat(hello1.sayHello(), + equalTo("Hello pico")); + assertThat(subversiveWay.currentActivationPhase(), + equalTo(Phase.ACTIVE)); + + assertThat(hello1, + sameInstance(subversiveWay.get())); + assertThat(subversiveWay, sameInstance(result.serviceProvider())); + + // the above is subversive because it is disconnected from the "real" activator + Services services = picoServices.services(); + ServiceProvider realHelloProvider = ((DefaultServices) services).serviceProviderFor(HelloPicoWorldImpl.class.getName()); + assertThat(subversiveWay, not(sameInstance(realHelloProvider))); + + assertThat(realHelloProvider.currentActivationPhase(), + equalTo(Phase.INIT)); + + result = injector.deactivate(subversiveWay, DefaultInjectorOptions.builder().build()); + assertThat(result.success(), is(true)); + } + + @Test + void injectionPlanResolved() { + HelloPicoImpl$$picoActivator activator = new HelloPicoImpl$$picoActivator(); + activator.picoServices(PicoServices.picoServices()); + + ActivationResult result = activator.activate(ActivationRequest.create(Phase.INJECTING)); + assertThat(result.success(), is(true)); + + assertThat(activator.currentActivationPhase(), is(Phase.INJECTING)); + assertThat(activator.serviceRef().orElseThrow().postConstructCallCount(), equalTo(0)); + assertThat(activator.serviceRef().orElseThrow().preDestroyCallCount(), equalTo(0)); + + Map injectionPlan = result.injectionPlans(); + assertThat(injectionPlan.keySet(), containsInAnyOrder( + "io.helidon.pico.services.testsubjects.world", + "io.helidon.pico.services.testsubjects.worldRef", + "io.helidon.pico.services.testsubjects.redWorld", + "io.helidon.pico.services.testsubjects.listOfWorlds", + "io.helidon.pico.services.testsubjects.listOfWorldRefs", + "io.helidon.pico.services.testsubjects.world|1(1)")); + InjectionPlan plan = injectionPlan.get("io.helidon.pico.services.testsubjects.world"); + assertThat(plan.wasResolved(), is(true)); + assertThat(plan.resolved().orElseThrow().getClass(), equalTo(PicoWorldImpl.class)); + plan = injectionPlan.get("io.helidon.pico.services.testsubjects.redWorld"); + assertThat(plan.wasResolved(), is(true)); + assertThat(plan.resolved().orElseThrow().getClass(), equalTo(Optional.class)); + plan = injectionPlan.get("io.helidon.pico.services.testsubjects.worldRef"); + assertThat(plan.wasResolved(), is(true)); + assertThat(plan.resolved().orElseThrow().getClass(), equalTo(PicoWorldImpl$$picoActivator.class)); + plan = injectionPlan.get("io.helidon.pico.services.testsubjects.listOfWorlds"); + assertThat(plan.wasResolved(), is(true)); + assertThat(plan.resolved().orElseThrow().getClass(), equalTo(ArrayList.class)); + + Map resolutions = result.resolvedDependencies(); + assertThat(resolutions.size(), equalTo(injectionPlan.size())); + + // now take us through activation + result = activator.activate(DefaultActivationRequest.builder() + .startingPhase(activator.currentActivationPhase()) + .build()); + assertThat(result.success(), is(true)); + assertThat(activator.currentActivationPhase(), is(Phase.ACTIVE)); + assertThat(activator.serviceRef().orElseThrow().postConstructCallCount(), equalTo(1)); + assertThat(activator.serviceRef().orElseThrow().preDestroyCallCount(), equalTo(0)); + + // these should have happened prior, so not there any longer + assertThat(result.injectionPlans(), equalTo(Map.of())); + assertThat(result.resolvedDependencies(), equalTo(Map.of())); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/SimplePicoTestingSupport.java b/pico/services/src/test/java/io/helidon/pico/services/SimplePicoTestingSupport.java new file mode 100644 index 00000000000..243e8a371f3 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/SimplePicoTestingSupport.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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.services; + +import io.helidon.pico.PicoServicesHolder; + +/** + * Supporting helper utilities unit-testing Pico services. + */ +class SimplePicoTestingSupport { + + /** + * Resets all internal Pico configuration instances, JVM global singletons, service registries, etc. + */ + static void resetAll() { + Holder.reset(); + } + + + @SuppressWarnings("deprecation") + private static class Holder extends PicoServicesHolder { + public static void reset() { + PicoServicesHolder.reset(); + } + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/EmptyModule.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/EmptyModule.java new file mode 100644 index 00000000000..d3a460090bd --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/EmptyModule.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import io.helidon.pico.ServiceBinder; + +import jakarta.inject.Singleton; + +/** + * For testing. + */ +@Singleton +public final class EmptyModule implements io.helidon.pico.Module { + + @Override + public void configure(ServiceBinder binder) { + + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java new file mode 100644 index 00000000000..e17fd480232 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoApplication.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import java.util.Optional; + +import io.helidon.pico.Application; +import io.helidon.pico.ServiceInjectionPlanBinder; + +import jakarta.annotation.Generated; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * For testing. + */ +@Generated(value = "example", comments = "API Version: n") +@Singleton +@Named(HelloPicoApplication.NAME) +public class HelloPicoApplication implements Application { + public static boolean ENABLED = true; + + static final String NAME = "HelloPicoApplication"; + + public HelloPicoApplication() { + assert(true); // for setting breakpoints in debug + } + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public void configure(ServiceInjectionPlanBinder binder) { + if (!ENABLED) { + return; + } + + binder.bindTo(HelloPicoImpl$$picoActivator.INSTANCE) + .bind(HelloPicoWorld.class.getPackageName() + ".world", PicoWorldImpl$$picoActivator.INSTANCE) + .bind(HelloPicoWorld.class.getPackageName() + ".worldRef", PicoWorldImpl$$picoActivator.INSTANCE) + .bindMany(HelloPicoWorld.class.getPackageName() + ".listOfWorldRefs", PicoWorldImpl$$picoActivator.INSTANCE) + .bindMany(HelloPicoWorld.class.getPackageName() + ".listOfWorlds", PicoWorldImpl$$picoActivator.INSTANCE) + .bindVoid(HelloPicoWorld.class.getPackageName() + ".redWorld") + .bind(HelloPicoWorld.class.getPackageName() + ".world|1(1)", PicoWorldImpl$$picoActivator.INSTANCE) + .commit(); + + binder.bindTo(PicoWorldImpl$$picoActivator.INSTANCE) + .commit(); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoImpl$$picoActivator.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoImpl$$picoActivator.java new file mode 100644 index 00000000000..34255c4db16 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoImpl$$picoActivator.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.Weight; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.PostConstructMethod; +import io.helidon.pico.PreDestroyMethod; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.services.Dependencies; + +import jakarta.annotation.Generated; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.pico.ElementInfo.Access; +import static io.helidon.pico.ElementInfo.ElementKind; + +/** + * Serves as an exemplar of what will is normally code generated. + */ +@Generated(value = "example", comments = "API Version: N") +@Singleton +@Weight(DefaultServiceInfo.DEFAULT_WEIGHT) +@SuppressWarnings({"unchecked", "checkstyle:TypeName"}) +public class HelloPicoImpl$$picoActivator extends AbstractServiceProvider { + + private static final DefaultServiceInfo serviceInfo = + DefaultServiceInfo.builder() + .serviceTypeName(getServiceTypeName()) + .activatorTypeName(HelloPicoImpl$$picoActivator.class.getName()) + .addContractsImplemented(HelloPicoWorld.class.getName()) + .addScopeTypeName(Singleton.class.getName()) + .declaredRunLevel(0) + .build(); + + public static final HelloPicoImpl$$picoActivator INSTANCE = new HelloPicoImpl$$picoActivator(); + + public HelloPicoImpl$$picoActivator() { + serviceInfo(serviceInfo); + } + + public static String getServiceTypeName() { + return HelloPicoWorldImpl.class.getName(); + } + + @Override + public DependenciesInfo dependencies() { + DependenciesInfo deps = Dependencies.builder(getServiceTypeName()) + .add("world", PicoWorld.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .add("worldRef", PicoWorld.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .providerWrapped() + .add("listOfWorlds", PicoWorld.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .listWrapped() + .add("listOfWorldRefs", PicoWorld.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .listWrapped().providerWrapped() + .add("redWorld", PicoWorld.class, ElementKind.FIELD, Access.PACKAGE_PRIVATE) + .named("red").optionalWrapped() + .add("world", PicoWorld.class, ElementKind.METHOD, 1, Access.PACKAGE_PRIVATE) + .elemOffset(1) + .build(); + return Dependencies.combine(super.dependencies(), deps); + } + + @Override + protected HelloPicoWorldImpl createServiceProvider(Map deps) { + return new HelloPicoWorldImpl(); + } + + @Override + protected void doInjectingFields(Object t, Map deps, Set injections, String forServiceType) { + super.doInjectingFields(t, deps, injections, forServiceType); + HelloPicoWorldImpl target = (HelloPicoWorldImpl) t; + target.world = Objects.requireNonNull( + (PicoWorld) deps.get(PicoWorld.class.getPackageName() + ".world"), "world"); + target.worldRef = Objects.requireNonNull( + (Provider) deps.get(PicoWorld.class.getPackageName() + ".worldRef"), "worldRef"); + target.listOfWorldRefs = Objects.requireNonNull( + (List>) deps.get(PicoWorld.class.getPackageName() + ".listOfWorldRefs"), "listOfWorldRefs"); + target.listOfWorlds = Objects.requireNonNull( + (List) deps.get(PicoWorld.class.getPackageName() + ".listOfWorlds"), "listOfWorlds"); + target.redWorld = Objects.requireNonNull( + (Optional) deps.get(PicoWorld.class.getPackageName() + ".redWorld"), "redWorld"); + } + + @Override + protected void doInjectingMethods(Object t, Map deps, Set injections, String forServiceType) { + super.doInjectingMethods(t, deps, injections, forServiceType); + HelloPicoWorldImpl target = (HelloPicoWorldImpl)t; + target.world(Objects.requireNonNull( + (PicoWorld) deps.get(PicoWorld.class.getPackageName() + ".world|1(1)"))); + } + + @Override + public Optional postConstructMethod() { + HelloPicoWorldImpl impl = serviceRef().get(); + return Optional.of(impl::postConstruct); + } + + @Override + public Optional preDestroyMethod() { + HelloPicoWorldImpl impl = serviceRef().get(); + return Optional.of(impl::preDestroy); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java new file mode 100644 index 00000000000..bb7495f58c0 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoModule.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import java.util.Optional; + +import io.helidon.pico.Module; +import io.helidon.pico.ServiceBinder; + +import jakarta.annotation.Generated; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Generated(value = "example", comments = "API Version: n") +@Singleton +@Named(HelloPicoModule.NAME) +public final class HelloPicoModule implements Module { + + public static final String NAME = "example"; + + public HelloPicoModule() { + } + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public void configure(ServiceBinder binder) { + binder.bind(HelloPicoImpl$$picoActivator.INSTANCE); + binder.bind(PicoWorldImpl$$picoActivator.INSTANCE); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorld.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorld.java new file mode 100644 index 00000000000..27a8a691534 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorld.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import io.helidon.pico.Contract; + +/** + * For testing. + */ +@Contract +public interface HelloPicoWorld { + + /** + * For testing. + * + * @return for testing + */ + String sayHello(); + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorldImpl.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorldImpl.java new file mode 100644 index 00000000000..60d74f51a77 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/HelloPicoWorldImpl.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Singleton +@RunLevel(0) +public class HelloPicoWorldImpl implements HelloPicoWorld { + + @Inject + PicoWorld world; + + @Inject + Provider worldRef; + + @Inject + List> listOfWorldRefs; + + @Inject + List listOfWorlds; + + @Inject @Named("red") + Optional redWorld; + + private PicoWorld setWorld; + + int postConstructCallCount; + int preDestroyCallCount; + + @Override + public String sayHello() { + assert(postConstructCallCount == 1); + assert(preDestroyCallCount == 0); + assert(world == worldRef.get()); + assert(world == setWorld); + assert(redWorld.isEmpty()); + + return "Hello " + world.name(); + } + + @Inject + void world(PicoWorld world) { + this.setWorld = world; + } + + @PostConstruct + public void postConstruct() { + postConstructCallCount++; + } + + @PreDestroy + public void preDestroy() { + preDestroyCallCount++; + } + + public int postConstructCallCount() { + return postConstructCallCount; + } + + public int preDestroyCallCount() { + return preDestroyCallCount; + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorld.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorld.java new file mode 100644 index 00000000000..e8594504ff4 --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorld.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +/** + * For testing. + */ +// @Contract - we will test ExternalContracts here instead +public interface PicoWorld { + + /** + * For testing. + * + * @return for testing + */ + String name(); + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl$$picoActivator.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl$$picoActivator.java new file mode 100644 index 00000000000..668b31414fd --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl$$picoActivator.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import java.util.Map; + +import io.helidon.common.Weight; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.services.Dependencies; + +import jakarta.annotation.Generated; +import jakarta.inject.Singleton; + +@Generated(value = "example", comments = "API Version: n") +@Singleton +@Weight(DefaultServiceInfo.DEFAULT_WEIGHT) +public class PicoWorldImpl$$picoActivator extends AbstractServiceProvider { + private static final DefaultServiceInfo serviceInfo = + DefaultServiceInfo.builder() + .serviceTypeName(getServiceTypeName()) + .activatorTypeName(PicoWorldImpl$$picoActivator.class.getName()) + .addExternalContractsImplemented(PicoWorld.class.getName()) + .addScopeTypeName(Singleton.class.getName()) + .declaredWeight(ServiceInfo.DEFAULT_WEIGHT) + .build(); + + public static final PicoWorldImpl$$picoActivator INSTANCE = new PicoWorldImpl$$picoActivator(); + + PicoWorldImpl$$picoActivator() { + serviceInfo(serviceInfo); + } + + public static String getServiceTypeName() { + return PicoWorldImpl.class.getName(); + } + + @Override + public DependenciesInfo dependencies() { + DependenciesInfo dependencies = Dependencies.builder(getServiceTypeName()) + .build(); + return Dependencies.combine(super.dependencies(), dependencies); + } + + @Override + protected PicoWorldImpl createServiceProvider(Map deps) { + return new PicoWorldImpl(); + } + +} diff --git a/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl.java b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl.java new file mode 100644 index 00000000000..48d566d791c --- /dev/null +++ b/pico/services/src/test/java/io/helidon/pico/services/testsubjects/PicoWorldImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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.services.testsubjects; + +import io.helidon.pico.ExternalContracts; + +@ExternalContracts(PicoWorld.class) +public class PicoWorldImpl implements PicoWorld { + private final String name; + + PicoWorldImpl() { + this("pico"); + } + + PicoWorldImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + +} diff --git a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application new file mode 100644 index 00000000000..8980ce4399a --- /dev/null +++ b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Application @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 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. +# + +io.helidon.pico.services.testsubjects.HelloPicoApplication diff --git a/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module new file mode 100644 index 00000000000..e07a24ff52e --- /dev/null +++ b/pico/services/src/test/resources/META-INF/services/io.helidon.pico.Module @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 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. +# + +io.helidon.pico.services.testsubjects.EmptyModule +io.helidon.pico.services.testsubjects.HelloPicoModule diff --git a/pico/testing/pom.xml b/pico/testing/pom.xml new file mode 100644 index 00000000000..c794236f6d4 --- /dev/null +++ b/pico/testing/pom.xml @@ -0,0 +1,65 @@ + + + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-testing + Helidon Pico Testing Support + + + + io.helidon.builder + helidon-builder-config + + + io.helidon.config + helidon-config + + + io.helidon.pico + helidon-pico-services + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + + + org.junit.jupiter + junit-jupiter-api + + + org.hamcrest + hamcrest-all + + + + diff --git a/pico/testing/src/main/java/io/helidon/pico/testing/PicoTestingSupport.java b/pico/testing/src/main/java/io/helidon/pico/testing/PicoTestingSupport.java new file mode 100644 index 00000000000..2e4bd3575c7 --- /dev/null +++ b/pico/testing/src/main/java/io/helidon/pico/testing/PicoTestingSupport.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2022, 2023 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.testing; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.helidon.builder.config.spi.ConfigBeanRegistryHolder; +import io.helidon.builder.config.spi.HelidonConfigBeanRegistry; +import io.helidon.common.LazyValue; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.DefaultBootstrap; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.PicoServicesHolder; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.DefaultServiceBinder; + +/** + * Supporting helper utilities unit-testing Pico services. + */ +public class PicoTestingSupport { + private static LazyValue instance = lazyCreate(basicTestableConfig()); + + private PicoTestingSupport() { + } + + /** + * Resets all internal Pico configuration instances, JVM global singletons, service registries, etc. + */ + public static void resetAll() { + Internal.reset(); + } + + /** + * Provides a means to bind a service provider into the {@link io.helidon.pico.Services} registry. + * + * @param picoServices the pico services instance to bind into + * @param serviceProvider the service provider to bind + * @see io.helidon.pico.ServiceBinder + */ + public static void bind(PicoServices picoServices, + ServiceProvider serviceProvider) { + DefaultServiceBinder binder = DefaultServiceBinder.create(picoServices, PicoTestingSupport.class.getSimpleName(), true); + binder.bind(serviceProvider); + } + + /** + * Creates a {@link io.helidon.pico.PicoServices} interface more conducive to unit and integration testing. + * + * @return testable services instance + */ + public static PicoServices testableServices() { + return instance.get(); + } + + /** + * Creates a {@link io.helidon.pico.PicoServices} interface more conducive to unit and integration testing. + * + * @param config the config to use + * @return testable services instance + * @see io.helidon.pico.PicoServicesConfig + */ + public static PicoServices testableServices(Config config) { + return lazyCreate(config).get(); + } + + /** + * Basic testable configuration. + * + * @return testable config + */ + public static Config basicTestableConfig() { + return Config.builder( + ConfigSources.create( + Map.of( + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_SERVICE_LOOKUP_CACHING, "true"), + "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + } + + /** + * Describe the provided instance or provider. + * + * @param providerOrInstance the instance to provider + * @return the description of the instance + */ + public static String toDescription(Object providerOrInstance) { + if (providerOrInstance instanceof Optional) { + providerOrInstance = ((Optional) providerOrInstance).orElse(null); + } + + if (providerOrInstance instanceof ServiceProvider) { + return ((ServiceProvider) providerOrInstance).description(); + } + return String.valueOf(providerOrInstance); + } + + /** + * Describe the provided instance or provider collection. + * + * @param coll the instance to provider collection + * @return the description of the instance + */ + public static List toDescriptions(Collection coll) { + return coll.stream().map(PicoTestingSupport::toDescription).collect(Collectors.toList()); + } + + private static LazyValue lazyCreate(Config config) { + return LazyValue.create(() -> { + PicoServices.globalBootstrap(DefaultBootstrap.builder().config(config).build()); + return PicoServices.picoServices().orElseThrow(); + }); + } + + @SuppressWarnings("deprecation") + private static class Internal extends PicoServicesHolder { + public static void reset() { + PicoServicesHolder.reset(); + instance = lazyCreate(basicTestableConfig()); + + HelidonConfigBeanRegistry registry = ConfigBeanRegistryHolder.configBeanRegistry().orElse(null); + if (registry instanceof Resettable) { + ((Resettable) registry).reset(true); + } + } + } + +} diff --git a/pico/testing/src/main/java/io/helidon/pico/testing/ReflectionBasedSingletonServiceProvider.java b/pico/testing/src/main/java/io/helidon/pico/testing/ReflectionBasedSingletonServiceProvider.java new file mode 100644 index 00000000000..b76cd6369f8 --- /dev/null +++ b/pico/testing/src/main/java/io/helidon/pico/testing/ReflectionBasedSingletonServiceProvider.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 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.testing; + +import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.Objects; + +import io.helidon.pico.InjectionException; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoBasics; +import io.helidon.pico.services.AbstractServiceProvider; + +/** + * Creates a simple reflection based service provider - for testing purposes only! + * + * @param the service type + */ +public class ReflectionBasedSingletonServiceProvider extends AbstractServiceProvider { + private final Class serviceType; + + private ReflectionBasedSingletonServiceProvider(Class serviceType, + ServiceInfo serviceInfo) { + this.serviceType = serviceType; + serviceInfo(serviceInfo); + } + + /** + * Generates a service provider eligible for binding into the service registry with the following proviso: + *

          + *
        • The service type will be of {@code jakarta.inject.Singleton} scope
        • + *
        • The service type will be created reflectively, and will expect to have an empty constructor
        • + *
        • The service type will not be able to provide its dependencies, nor will it be able to accept injection
        • + *
        • The service type will not be able to participate in lifecycle - + * {@link io.helidon.pico.PostConstructMethod} or {@link io.helidon.pico.PreDestroyMethod}
        • + *
        + * Note: Generally it is encouraged for users to rely on the annotation processors and other built on compile-time + * tooling to generate the appropriate service providers and modules. This method is an alternative to that + * mechanism and therefore is discouraged from production use. This method is not used in normal processing by + * the reference pico provider implementation. + * + * @param serviceType the service type + * @param siBasics the service info basic descriptor, or null to generate a default (empty) placeholder + * @param the class of the service type + * + * @return the service provider capable of being bound to the services registry + * @see io.helidon.pico.testing.PicoTestingSupport#bind(io.helidon.pico.PicoServices, io.helidon.pico.ServiceProvider) + */ + public static ReflectionBasedSingletonServiceProvider create(Class serviceType, + ServiceInfoBasics siBasics) { + Objects.requireNonNull(serviceType); + Objects.requireNonNull(siBasics); + + if (!serviceType.getName().equals(siBasics.serviceTypeName())) { + throw new IllegalArgumentException("mismatch in service types"); + } + + return new ReflectionBasedSingletonServiceProvider<>(serviceType, ServiceInfo.toBuilder(siBasics).build()); + } + + @Override + public boolean isCustom() { + return true; + } + + @Override + protected T createServiceProvider(Map deps) { + try { + Constructor ctor = serviceType.getDeclaredConstructor(); + ctor.setAccessible(true); + return ctor.newInstance(); + } catch (Exception e) { + throw new InjectionException("failed to create instance: " + this, e, this); + } + } + +} diff --git a/pico/testing/src/main/java/io/helidon/pico/testing/package-info.java b/pico/testing/src/main/java/io/helidon/pico/testing/package-info.java new file mode 100644 index 00000000000..1db9110bd6a --- /dev/null +++ b/pico/testing/src/main/java/io/helidon/pico/testing/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 testing support. + */ +package io.helidon.pico.testing; diff --git a/pico/testing/src/main/java/module-info.java b/pico/testing/src/main/java/module-info.java new file mode 100644 index 00000000000..1997bbf6014 --- /dev/null +++ b/pico/testing/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022, 2023 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 Testing Support Module. + */ +module io.helidon.pico.testing { + requires io.helidon.builder.config; + requires io.helidon.config; + requires transitive io.helidon.pico.services; + + exports io.helidon.pico.testing; +} diff --git a/pico/tests/pom.xml b/pico/tests/pom.xml new file mode 100644 index 00000000000..d21b2a422d8 --- /dev/null +++ b/pico/tests/pom.xml @@ -0,0 +1,52 @@ + + + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.pico.tests + helidon-pico-tests-project + + Helidon Pico Tests Project + pom + + + true + true + true + true + true + true + true + + + + resources-plain + resources-pico + tck-jsr330 + + + diff --git a/pico/tests/resources-pico/pom.xml b/pico/tests/resources-pico/pom.xml new file mode 100644 index 00000000000..79430604505 --- /dev/null +++ b/pico/tests/resources-pico/pom.xml @@ -0,0 +1,165 @@ + + + + + + io.helidon.pico.tests + helidon-pico-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-tests-resources-pico + Helidon Pico Test Pico Resources + a jar that offers contracts and other artifacts and is a native Pico module (e.g., uses Pico APT) + + + true + + + + + io.helidon.config + helidon-config-metadata + provided + + + io.helidon.pico + helidon-pico-processor + provided + true + + + io.helidon.pico + helidon-pico-maven-plugin + provided + true + + + io.helidon.pico.tests + helidon-pico-tests-resources-plain + ${helidon.version} + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.pico + helidon-pico-services + + + io.helidon.pico + helidon-pico-testing + test + + + jakarta.enterprise + jakarta.enterprise.cdi-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -Apico.autoAddNonContractInterfaces=true + -Apico.allowListedInterceptorAnnotations=jakarta.inject.Named + -Apico.application.pre.create=true + -Apico.mapApplicationToSingletonScope=true + -Apico.debug=${pico.debug} + + + + + true + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.lib.jakarta.cdi-api} + + + io.helidon.pico.tests + helidon-pico-tests-resources-plain + ${helidon.version} + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + testCompile + test-compile + + test-application-create + + + + + + -Apico.debug=${pico.debug} + -Apico.autoAddNonContractInterfaces=true + -Apico.application.pre.create=true + + + + + NAMED + + io.helidon.pico.tests.pico.provider.MyServices$MyConcreteClassContractPerRequestIPProvider + io.helidon.pico.tests.pico.provider.MyServices$MyConcreteClassContractPerRequestProvider + io.helidon.pico.tests.pico.ASerialProviderImpl + io.helidon.pico.tests.pico.tbox.impl.BladeProvider + + + + + + + diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/ASerialProviderImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/ASerialProviderImpl.java new file mode 100644 index 00000000000..a88fecc741f --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/ASerialProviderImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Pico Testing. + */ +@Weight(Weighted.DEFAULT_WEIGHT + 100) +@Singleton +public class ASerialProviderImpl implements Provider { + + static { + System.getLogger(ASerialProviderImpl.class.getName()).log(System.Logger.Level.DEBUG, "in static init"); + } + + private final AtomicInteger counter = new AtomicInteger(); + + @Override + public Serializable get() { + return String.valueOf(counter.incrementAndGet()); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/SomeOtherLocalNonContractInterface1.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/SomeOtherLocalNonContractInterface1.java new file mode 100644 index 00000000000..d1d504332f7 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/SomeOtherLocalNonContractInterface1.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +/** + * Pico Testing. + */ +public interface SomeOtherLocalNonContractInterface1 { + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/Verification.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/Verification.java new file mode 100644 index 00000000000..deeb7509638 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/Verification.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import jakarta.inject.Provider; + +/** + * Pico Testing. + */ +public abstract class Verification { + + /** + * Pico Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expected expected + * @param expectedType expectedType + */ + public static void verifyInjected(Optional injectee, + String tag, + Integer injectedCount, + boolean expected, + Class expectedType) { + if (expected && injectee.isEmpty()) { + throw new AssertionError(tag + " was expected to be present"); + } else if (!expected && injectee.isPresent()) { + throw new AssertionError(tag + " was not expected to be present"); + } + + if (expectedType != null && expected && !expectedType.isInstance(injectee.get())) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + injectee); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + injectedCount + " times"); + } + } + + /** + * Pico Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expectedSingleton expectedSingleton + * @param expectedType expectedType + */ + public static void verifyInjected(Provider injectee, + String tag, + Integer injectedCount, + boolean expectedSingleton, + Class expectedType) { + Objects.requireNonNull(injectee, tag + " was not injected"); + Object provided = Objects.requireNonNull(injectee.get(), tag + " was expected to be provided"); + + if (expectedType != null && !expectedType.isInstance(provided)) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + provided); + } + + Object provided2 = injectee.get(); + if (expectedSingleton && provided != provided2) { + throw new AssertionError(tag + " was expected to be a singleton provided type"); + } + if (expectedType != null && !(expectedType.isInstance(provided2))) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + provided2); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + + injectedCount + " times"); + } + } + + /** + * Pico Testing. + * + * @param injectee injectee + * @param tag tag + * @param injectedCount injectedCount + * @param expectedSize expectedSize + * @param expectedType expectedType + */ + public static void verifyInjected(List injectee, + String tag, + Integer injectedCount, + int expectedSize, + Class expectedType) { + Objects.requireNonNull(injectee, tag + " was not injected"); + + int size = injectee.size(); + if (size != expectedSize) { + throw new AssertionError(tag + " was expected to be size of " + expectedSize + + " but instead was injected with: " + injectee); + } + + if (injectedCount != null && injectedCount != 1) { + throw new AssertionError(tag + + " was was expected to be injected 1 time; it was actually injected " + + injectedCount + " times"); + } + + if (expectedType != null) { + injectee.forEach(item -> { + if (!expectedType.isInstance(item)) { + throw new AssertionError(tag + " was expected to be of type " + expectedType + " : " + item); + } + }); + } + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java new file mode 100644 index 00000000000..000f907c86c --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/interceptor/XImpl.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 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.tests.pico.interceptor; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Optional; + +import io.helidon.pico.ExternalContracts; +import io.helidon.pico.tests.plain.interceptor.IA; +import io.helidon.pico.tests.plain.interceptor.IB; +import io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("ClassX") +@ExternalContracts(value = Closeable.class, moduleNames = {"test1", "test2"}) +public class XImpl implements IA, IB, Closeable { + + XImpl() { + // this is the one that will be used by interception + } + + @Inject + public XImpl(Optional optionalIA) { + assert (optionalIA.isEmpty()); + } + + @Override + public void methodIA1() { + } + + @InterceptorBasedAnno("IA2") + @Override + public void methodIA2() { + } + + @Named("methodIB") + @InterceptorBasedAnno("IBSubAnno") + @Override + public void methodIB(@Named("arg1") String val) { + } + + @InterceptorBasedAnno + @Override + public void close() throws IOException, RuntimeException { + throw new IOException("forced"); + } + + public long methodX(String arg1, + int arg2, + boolean arg3) throws IOException, RuntimeException, AssertionError { + return 101; + } + + // test of package private + String methodY() { + return "methodY"; + } + + // test of protected + protected String methodZ() { + return "methodZ"; + } + + // test of protected + protected void throwRuntimeException() { + throw new RuntimeException("forced"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/package-info.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/package-info.java new file mode 100644 index 00000000000..b8882068d9d --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Testing. + */ +package io.helidon.pico.tests.pico; diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeConfig.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeConfig.java new file mode 100644 index 00000000000..e4b4a2a162d --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 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.tests.pico.provider; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.tests.pico.tbox.Preferred; + +import jakarta.inject.Singleton; + +public interface FakeConfig { + + interface B { + + } + + @Singleton + @Preferred("x") + @Weight(Weighted.DEFAULT_WEIGHT) + class Builder implements B { + } + + @Singleton + @Weight(Weighted.DEFAULT_WEIGHT+1) + class HigherWeightBuilder extends Builder { + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeServer.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeServer.java new file mode 100644 index 00000000000..a3141c6fd7c --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/FakeServer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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.tests.pico.provider; + +import io.helidon.pico.tests.pico.tbox.Preferred; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +public interface FakeServer { + + interface B { + + } + + @Singleton + class Builder implements B { + @Inject + Builder(@Preferred("x") FakeConfig.Builder b) { + assert (b.getClass() == FakeConfig.Builder.class) : String.valueOf(b); + } + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyConcreteClassContract.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyConcreteClassContract.java new file mode 100644 index 00000000000..f7698c91854 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyConcreteClassContract.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.tests.pico.provider; + +public class MyConcreteClassContract { + + private final String id; + + MyConcreteClassContract(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java new file mode 100644 index 00000000000..3d3cf385d2a --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/provider/MyServices.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 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.tests.pico.provider; + +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.InjectionPointProvider; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +public class MyServices { + + @Singleton + public static class MyConcreteClassContractPerRequestProvider implements Provider { + private volatile int counter; + + @Override + public MyConcreteClassContract get() { + int num = counter++; + return new MyConcreteClassContract(getClass().getSimpleName() + ":instance_" + num); + } + } + + @Singleton + @Weight(Weighted.DEFAULT_WEIGHT + 1) + public static class MyConcreteClassContractPerRequestIPProvider implements InjectionPointProvider { + private volatile int counter; + + private boolean postConstructed; + private MyConcreteClassContract injected; + + @PostConstruct + public void postConstruct() { + assert (injected != null); + postConstructed = true; + } + + @Override + public Optional first(ContextualServiceQuery query) { + assert (injected != null); + assert (postConstructed); + int num = counter++; + String id = getClass().getSimpleName() + ":instance_" + num + ", " + + query + ", " + injected; + return Optional.of(new MyConcreteClassContract(id)); + } + + @Inject + void setMyConcreteClassContract(MyConcreteClassContract injected) { + assert (this.injected == null); + this.injected = Objects.requireNonNull(injected); + } + + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/Intercepted.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/Intercepted.java new file mode 100644 index 00000000000..f1aab1bac32 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/Intercepted.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.tests.pico.stacking; + +import io.helidon.pico.Contract; + +@Contract +//@MyCompileTimeInheritableTestQualifier(value = "interface", extendedValue = "ev interface") +public interface Intercepted { + + Intercepted getInner(); + +// @MyCompileTimeInheritableTestQualifier(value = "method", extendedValue = "ev method") + String sayHello(/*@MyCompileTimeInheritableTestQualifier(value = "arg", extendedValue = "ev arg")*/ String arg); + + default void voidMethodWithNoArgs() { + } + + default void voidMethodWithAnnotatedPrimitiveIntArg(/*@MyCompileTimeInheritableTestQualifier*/ int a) { + } + + default int intMethodWithPrimitiveBooleanArg(boolean b) { + return 1; + } + + default byte[] byteArrayMethodWithAnnotatedPrimitiveCharArrayArg(/*@MyCompileTimeInheritableTestQualifier*/ char[] c) { + return null; + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/InterceptedImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/InterceptedImpl.java new file mode 100644 index 00000000000..e38d20efe45 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/InterceptedImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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.tests.pico.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.RunLevel; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 1) +//@MyCompileTimeInheritableTestQualifier(value = "InterceptedImpl") +@RunLevel(1) +@Named("InterceptedImpl") +public class InterceptedImpl implements Intercepted { + + private final Intercepted inner; + + @Inject + public InterceptedImpl(Optional inner) { + this.inner = inner.orElse(null); + } + + @Override + public Intercepted getInner() { + return inner; + } + + @Override +// @MyCompileTimeInheritableTestQualifier(value = "InterceptedImpl method", extendedValue = "ev derived method") + public String sayHello(String arg) { + return getClass().getSimpleName() + ":" + (inner != null ? inner.sayHello(arg) : arg); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/MostOuterInterceptedImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/MostOuterInterceptedImpl.java new file mode 100644 index 00000000000..8bfd1c1f0a6 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/MostOuterInterceptedImpl.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.tests.pico.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 3) +public class MostOuterInterceptedImpl extends OuterInterceptedImpl { + + @Inject + public MostOuterInterceptedImpl(Optional inner) { + super(inner); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/OuterInterceptedImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/OuterInterceptedImpl.java new file mode 100644 index 00000000000..301fe3bc73b --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/stacking/OuterInterceptedImpl.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.tests.pico.stacking; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 2) +public class OuterInterceptedImpl extends InterceptedImpl { + + @Inject + public OuterInterceptedImpl(Optional inner) { + super(inner); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractBlade.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractBlade.java new file mode 100644 index 00000000000..288903594ee --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractBlade.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import io.helidon.pico.OptionallyNamed; + +import jakarta.inject.Inject; + +public abstract class AbstractBlade implements OptionallyNamed { + + // intended to be a void injection point + @Inject + protected AbstractBlade() { + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java new file mode 100644 index 00000000000..359384b6f74 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/AbstractSaw.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.tests.pico.tbox.impl.DullBlade; +import io.helidon.pico.tests.pico.Verification; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +public abstract class AbstractSaw extends Verification implements Tool { + @Inject protected Provider fieldInjectedProtectedProviderInAbstractBase; + @Inject protected Optional fieldInjectedProtectedOptionalInAbstractBase; + @Inject protected List fieldInjectedProtectedListInAbstractBase; + @Inject protected List fieldInjectedProtectedProviderListInAbstractBase; + + @Inject Provider fieldInjectedPkgPrivateProviderInAbstractBase; + @Inject Optional fieldInjectedPkgPrivateOptionalInAbstractBase; + @Inject List fieldInjectedPkgPrivateListInAbstractBase; + @Inject List> fieldInjectedPkgPrivateProviderListInAbstractBase; + + Provider setterInjectedPkgPrivateProviderInAbstractBase; + Optional setterInjectedPkgPrivateOptionalInAbstractBase; + List setterInjectedPkgPrivateListInAbstractBase; + List> setterInjectedPkgPrivateProviderListInAbstractBase; + + int setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateListInAbstractBaseInjectedCount; + int setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount; + + @Inject + void setBladeProvider(Provider blade) { + setterInjectedPkgPrivateProviderInAbstractBase = blade; + setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount++; + } + + @Inject + void setBladeOptional(Optional blade) { + setterInjectedPkgPrivateOptionalInAbstractBase = blade; + setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount++; + } + + @Inject + void setBladeList(List blades) { + setterInjectedPkgPrivateListInAbstractBase = blades; + setterInjectedPkgPrivateListInAbstractBaseInjectedCount++; + } + + @Inject + void setBladeProviders(List> blades) { + setterInjectedPkgPrivateProviderListInAbstractBase = blades; + setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount++; + } + + public void verifyState() { + verifyInjected(fieldInjectedProtectedOptionalInAbstractBase, getClass() + + ".fieldInjectedProtectedOptionalInAbstractBase", null, true, DullBlade.class); + verifyInjected(fieldInjectedProtectedProviderInAbstractBase, getClass() + + ".fieldInjectedProtectedProviderInAbstractBase", null, true, DullBlade.class); + verifyInjected(fieldInjectedProtectedListInAbstractBase, getClass() + + ".fieldInjectedProtectedListInAbstractBase", null, 1, AbstractBlade.class); + verifyInjected(setterInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".setterInjectedPkgPrivateProviderListInAbstractBase", null, 1, ServiceProvider.class); + + verifyInjected(fieldInjectedPkgPrivateProviderInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderInAbstractBase", null, true, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateOptionalInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateOptionalInAbstractBase", null, true, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateListInAbstractBase", null, 1, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, ServiceProvider.class); + + verifyInjected(setterInjectedPkgPrivateProviderInAbstractBase, getClass() + + ".setBladeProvider(Provider blade)", + setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateOptionalInAbstractBase, getClass() + + ".setBladeOptional(Optional blade)", + setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateListInAbstractBase, getClass() + + ".setBladeList(List blades)", + setterInjectedPkgPrivateListInAbstractBaseInjectedCount, 1, DullBlade.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() + + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, ServiceProvider.class); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Awl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Awl.java new file mode 100644 index 00000000000..6a9fb751c96 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Awl.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import io.helidon.pico.Contract; + +/** + * Pico Testing. + */ +@Contract +public interface Awl extends Tool { + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Hammer.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Hammer.java new file mode 100644 index 00000000000..b295d95277e --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Hammer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import io.helidon.pico.Contract; +import io.helidon.pico.OptionallyNamed; + +/** + * Pico Testing. + */ +@Contract +public interface Hammer extends Tool, OptionallyNamed { + + @Override + default String name() { + return "hammer"; + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Lubricant.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Lubricant.java new file mode 100644 index 00000000000..0b727e6b81f --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Lubricant.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +/** + * Pico Testing. + */ +// @Singleton -- intentionally not declared to be a contract +public interface Lubricant { + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Preferred.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Preferred.java new file mode 100644 index 00000000000..c1e58b08a7a --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Preferred.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.inject.Qualifier; + +/** + * Custom qualifier. + */ +@Qualifier +@Documented +@Retention(RetentionPolicy.CLASS) +public @interface Preferred { + + /** + * Pico Testing. + * + * @return for testing + */ + String value() default ""; + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/TableSaw.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/TableSaw.java new file mode 100644 index 00000000000..ecfae4a528b --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/TableSaw.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.tests.pico.tbox.impl.CoarseBlade; +import io.helidon.pico.tests.pico.tbox.impl.DullBlade; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Intentionally in the same package as {AbstractSaw}. + */ +@Singleton +@SuppressWarnings("unused") +public class TableSaw extends AbstractSaw { + + private Optional ctorInjectedLubricantInSubClass; + private Optional setterInjectedLubricantInSubClass; + + private int setterInjectedLubricantInSubClassInjectedCount; + + @Inject @Named(CoarseBlade.NAME) Provider coarseBladeFieldInjectedPkgPrivateProviderInSubClass; + @Inject @Named(CoarseBlade.NAME) Optional coarseBladeFieldInjectedPkgPrivateOptionalInSubClass; + @Inject @Named(CoarseBlade.NAME) List coarseBladeFieldInjectedPkgPrivateListInSubClass; + @Inject @Named(CoarseBlade.NAME) List> coarseBladeFieldInjectedPkgPrivateProviderListInSubClass; + + Provider setterInjectedPkgPrivateProviderInSubClass; + Optional setterInjectedPkgPrivateOptionalInSubClass; + List setterInjectedPkgPrivateListInSubClass; + List> setterInjectedPkgPrivateProviderListInSubClass; + + int setterInjectedPkgPrivateProviderInSubClassInjectedCount; + int setterInjectedPkgPrivateOptionalInSubClassInjectedCount; + int setterInjectedPkgPrivateListInSubClassInjectedCount; + int setterInjectedPkgPrivateProviderListInSubClassInjectedCount; + + TableSaw() { + } + + @Inject + public TableSaw(Optional lubricant) { + ctorInjectedLubricantInSubClass = lubricant; + } + + @Override + public Optional named() { + return Optional.of(getClass().getSimpleName()); + } + + @Inject + protected void injectLubricant(Optional lubricant) { + setterInjectedLubricantInSubClass = lubricant; + setterInjectedLubricantInSubClassInjectedCount++; + } + + @Inject + void setBladeProviderInSubclass(Provider blade) { + setterInjectedPkgPrivateProviderInSubClass = blade; + setterInjectedPkgPrivateProviderInSubClassInjectedCount++; + } + + @Inject + void setBladeOptionalInSubclass(Optional blade) { + setterInjectedPkgPrivateOptionalInSubClass = blade; + setterInjectedPkgPrivateOptionalInSubClassInjectedCount++; + } + + @Inject + void setAllBladesInSubclass(@Named("*") List blades) { + setterInjectedPkgPrivateListInSubClass = blades; + setterInjectedPkgPrivateListInSubClassInjectedCount++; + } + + @Inject + void setBladeProviderListInSubclass(List> blades) { + setterInjectedPkgPrivateProviderListInSubClass = blades; + setterInjectedPkgPrivateProviderListInSubClassInjectedCount++; + } + + @Override + public void verifyState() { + verifyInjected(ctorInjectedLubricantInSubClass, getClass() + "." + InjectionPointInfo.CONSTRUCTOR, null, false, null); + verifyInjected(setterInjectedLubricantInSubClass, getClass() + ".injectLubricant(Optional lubricant)", setterInjectedLubricantInSubClassInjectedCount, false, null); + + verifyInjected(coarseBladeFieldInjectedPkgPrivateProviderInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateProviderInSubClass", null, true, CoarseBlade.class); + verifyInjected(coarseBladeFieldInjectedPkgPrivateOptionalInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateOptionalInSubClass", null, true, CoarseBlade.class); + verifyInjected(coarseBladeFieldInjectedPkgPrivateListInSubClass, getClass() + + ".coarseBladeFieldInjectedPkgPrivateListInSubClass", null, 1, CoarseBlade.class); + + verifyInjected(setterInjectedPkgPrivateProviderInSubClass, getClass() + + ".setBladeProvider(Provider blade)", setterInjectedPkgPrivateProviderInSubClassInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateOptionalInSubClass, getClass() + + ".setBladeOptional(Optional blade)", setterInjectedPkgPrivateOptionalInSubClassInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateListInSubClass, getClass() + + ".setAllBladesInSubclass(List blades)", setterInjectedPkgPrivateListInSubClassInjectedCount, 3, AbstractBlade.class); + + super.verifyState(); + } + +} diff --git a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerLifecycle.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Tool.java similarity index 60% rename from pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerLifecycle.java rename to pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Tool.java index e08e34b6093..d27d84fc9f4 100644 --- a/pico/builder-config/tests/configbean/src/test/java/io/helidon/pico/builder/config/fakes/FakeServerLifecycle.java +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/Tool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 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. @@ -14,24 +14,24 @@ * limitations under the License. */ -package io.helidon.pico.builder.config.fakes; +package io.helidon.pico.tests.pico.tbox; + +import io.helidon.pico.Contract; +import io.helidon.pico.OptionallyNamed; /** - * aka ServerLifecycle. - * Basic server lifecycle operations. + * Pico Testing. */ -public interface FakeServerLifecycle { +@Contract +public interface Tool extends OptionallyNamed { -// /** -// * Before server start. -// */ -// default void beforeStart() { -// } -// -// /** -// * After server stop. -// */ -// default void afterStop() { -// } + /** + * Pico Testing. + * + * @return for testing + */ + default String name() { + return named().orElseThrow(); + } } diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/ToolBox.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/ToolBox.java new file mode 100644 index 00000000000..89d6e6c23e1 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/ToolBox.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import java.util.List; + +import io.helidon.pico.Contract; + +import jakarta.inject.Provider; + +/** + * Pico Testing. + */ +@Contract +public interface ToolBox { + + /** + * Pico Testing. + * + * @return for testing + */ + List> toolsInBox(); + + /** + * Pico Testing. + * + * @return for testing + */ + Provider preferredHammer(); + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/AwlImpl.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/AwlImpl.java new file mode 100644 index 00000000000..3503e7a924c --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/AwlImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.pico.OptionallyNamed; +import io.helidon.pico.tests.pico.tbox.Awl; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +public class AwlImpl implements Awl, OptionallyNamed { + + @Inject + AwlImpl() { + } + + @Override + public Optional named() { + return Optional.of("awl"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BigHammer.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BigHammer.java new file mode 100644 index 00000000000..4078af86854 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BigHammer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.tests.pico.tbox.Hammer; +import io.helidon.pico.tests.pico.tbox.Preferred; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 1) +@Named(BigHammer.NAME) +@Preferred +public class BigHammer implements Hammer { + + public static final String NAME = "big"; + + @Override + public Optional named() { + return Optional.of(NAME + " hammer"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BladeProvider.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BladeProvider.java new file mode 100644 index 00000000000..2bebc3585d9 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/BladeProvider.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.pico.ContextualServiceQuery; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.InjectionPointProvider; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.tests.pico.tbox.AbstractBlade; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * Provides contextual injection for blades. + */ +@Singleton +@Named("*") +public class BladeProvider implements InjectionPointProvider { + + static final QualifierAndValue all = DefaultQualifierAndValue.createNamed("*"); + static final QualifierAndValue coarse = DefaultQualifierAndValue.createNamed("coarse"); + static final QualifierAndValue fine = DefaultQualifierAndValue.createNamed("fine"); + + @Override + public Optional first(ContextualServiceQuery query) { + Objects.requireNonNull(query); + ServiceInfoCriteria criteria = query.serviceInfoCriteria(); + assert (criteria.contractsImplemented().size() == 1) : criteria; + assert (criteria.contractsImplemented().contains(AbstractBlade.class.getName())) : criteria; + + AbstractBlade blade; + if (criteria.qualifiers().contains(all) || criteria.qualifiers().contains(coarse)) { + blade = new CoarseBlade(); + } else if (criteria.qualifiers().contains(fine)) { + blade = new FineBlade(); + } else { + assert (criteria.qualifiers().isEmpty()); + blade = new DullBlade(); + } + + return Optional.of(blade); + } + + @Override + public List list(ContextualServiceQuery query) { + Objects.requireNonNull(query); + assert (query.injectionPointInfo().orElseThrow().listWrapped()) : query; + ServiceInfoCriteria criteria = query.serviceInfoCriteria(); + + List result = new ArrayList<>(); + if (criteria.qualifiers().contains(all) || criteria.qualifiers().contains(coarse)) { + result.add(new CoarseBlade()); + } + + if (criteria.qualifiers().contains(all) || criteria.qualifiers().contains(fine)) { + result.add(new FineBlade()); + } + + if (criteria.qualifiers().contains(all) || criteria.qualifiers().isEmpty()) { + result.add(new DullBlade()); + } + + if (query.expected() && result.isEmpty()) { + throw new AssertionError("expected to match: " + criteria); + } + + return result; + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/CoarseBlade.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/CoarseBlade.java new file mode 100644 index 00000000000..b0e497b115f --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/CoarseBlade.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.pico.tests.pico.tbox.AbstractBlade; + +import jakarta.inject.Named; + +@Named(CoarseBlade.NAME) +public class CoarseBlade extends AbstractBlade { + + public static final String NAME = "coarse"; + + @Override + public Optional named() { + return Optional.of(NAME + " blade"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/DullBlade.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/DullBlade.java new file mode 100644 index 00000000000..37351bec29b --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/DullBlade.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.pico.tests.pico.tbox.AbstractBlade; + +/** + * When a particular blade name is not "asked for" explicitly then we give out a dull blade. + */ +public class DullBlade extends AbstractBlade { + + @Override + public Optional named() { + return Optional.of("dull blade"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/FineBlade.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/FineBlade.java new file mode 100644 index 00000000000..d8d5f55ccf5 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/FineBlade.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.pico.tests.pico.tbox.AbstractBlade; + +import jakarta.inject.Named; + +@Named(FineBlade.NAME) +public class FineBlade extends AbstractBlade { + + static final String NAME = "fine"; + + @Override + public Optional named() { + return Optional.of(NAME + " blade"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/HandSaw.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/HandSaw.java new file mode 100644 index 00000000000..a0afa5c93dd --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/HandSaw.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.DefaultContextualServiceQuery; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.InjectionPointProvider; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.tests.pico.tbox.AbstractBlade; +import io.helidon.pico.tests.pico.tbox.AbstractSaw; +import io.helidon.pico.tests.pico.tbox.Lubricant; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Kept intentionally in a different package from {@link AbstractSaw} for testing. + */ +@Singleton +@SuppressWarnings("unused") +public class HandSaw extends AbstractSaw { + + private Optional ctorInjectedLubricantInSubClass; + private Optional setterInjectedLubricantInSubClass; + + private int setterInjectedLubricantInSubClassInjectedCount; + + @Inject @Named(FineBlade.NAME) Provider fineBladeFieldInjectedPkgPrivateProviderInSubClass; + @Inject @Named(FineBlade.NAME) Optional fineBladeFieldInjectedPkgPrivateOptionalInSubClass; + @Inject @Named(FineBlade.NAME) List fineBladeFieldInjectedPkgPrivateListInSubClass; + + Provider setterInjectedPkgPrivateProviderInSubClass; + Optional setterInjectedPkgPrivateOptionalInSubClass; + List setterInjectedPkgPrivateListInSubClass; + List> setterInjectedAllProviderListInSubClass; + + int setterInjectedPkgPrivateProviderInSubClassInjectedCount; + int setterInjectedPkgPrivateOptionalInSubClassInjectedCount; + int setterInjectedPkgPrivateListInSubClassInjectedCount; + int setterInjectedAllProviderListInSubClassInjectedCount; + + HandSaw() { + } + + @Inject + public HandSaw(Optional lubricant) { + ctorInjectedLubricantInSubClass = lubricant; + } + + @Override + public Optional named() { + return Optional.of(getClass().getSimpleName()); + } + + @Inject + protected void injectLubricant(Optional lubricant) { + setterInjectedLubricantInSubClass = lubricant; + setterInjectedLubricantInSubClassInjectedCount++; + } + + @Inject + void setBladeProvider(Provider blade) { + setterInjectedPkgPrivateProviderInSubClass = blade; + setterInjectedPkgPrivateProviderInSubClassInjectedCount++; + } + + @Inject + void setBladeOptional(Optional blade) { + setterInjectedPkgPrivateOptionalInSubClass = blade; + setterInjectedPkgPrivateOptionalInSubClassInjectedCount++; + } + + @Inject + void setBladeList(List blades) { + setterInjectedPkgPrivateListInSubClass = blades; + setterInjectedPkgPrivateListInSubClassInjectedCount++; + } + + @Inject + @SuppressWarnings("unchecked") + void setAllBlades(@Named("*") List> blades) { + setterInjectedAllProviderListInSubClass = (List) blades; + setterInjectedAllProviderListInSubClassInjectedCount++; + } + + @Override + public void verifyState() { + verifyInjected(ctorInjectedLubricantInSubClass, getClass() + + "." + InjectionPointInfo.CONSTRUCTOR, null, false, null); + verifyInjected(setterInjectedLubricantInSubClass, getClass() + + ".injectLubricant(Optional lubricant)", setterInjectedLubricantInSubClassInjectedCount, false, null); + + verifyInjected(fineBladeFieldInjectedPkgPrivateProviderInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateProviderInSubClass", null, true, FineBlade.class); + verifyInjected(fineBladeFieldInjectedPkgPrivateOptionalInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateOptionalInSubClass", null, true, FineBlade.class); + verifyInjected(fineBladeFieldInjectedPkgPrivateListInSubClass, getClass() + + ".fineBladeFieldInjectedPkgPrivateListInSubClass", null, 1, FineBlade.class); + + verifyInjected(setterInjectedPkgPrivateProviderInSubClass, getClass() + + ".setBladeProvider(Provider blade)", setterInjectedPkgPrivateProviderInSubClassInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateOptionalInSubClass, getClass() + + ".setBladeOptional(Optional blade)", setterInjectedPkgPrivateOptionalInSubClassInjectedCount, true, DullBlade.class); + verifyInjected(setterInjectedPkgPrivateListInSubClass, getClass() + + ".setBladeList(List blades)", setterInjectedPkgPrivateListInSubClassInjectedCount, 1, AbstractBlade.class); + verifyInjected(setterInjectedAllProviderListInSubClass, getClass() + + ".setAllBlades(List blades)", setterInjectedAllProviderListInSubClassInjectedCount, 1, ServiceProvider.class); + List blades = setterInjectedAllProviderListInSubClass.get(0) + .list(DefaultContextualServiceQuery.builder() + .serviceInfoCriteria(DefaultServiceInfoCriteria.builder() + .addContractImplemented(AbstractBlade.class.getName()) + .build()) + .build()); + verifyInjected(blades, getClass() + + "", null, 3, AbstractBlade.class); + + super.verifyState(); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/LittleHammer.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/LittleHammer.java new file mode 100644 index 00000000000..c56cda7c58b --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/LittleHammer.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.pico.tests.pico.tbox.Hammer; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named(LittleHammer.NAME) +public class LittleHammer implements Hammer { + + public static final String NAME = "little"; + + @Override + public Optional named() { + return Optional.of(NAME + " hammer"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java new file mode 100644 index 00000000000..172e5f47ebb --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/MainToolBox.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.List; +import java.util.Objects; + +import io.helidon.pico.tests.pico.tbox.Hammer; +import io.helidon.pico.tests.pico.tbox.Preferred; +import io.helidon.pico.tests.pico.tbox.Tool; +import io.helidon.pico.tests.pico.tbox.ToolBox; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +public class MainToolBox implements ToolBox { + + private final List> allTools; + private final List> allHammers; + private final Provider bigHammer; + + @Inject + @Preferred + Provider preferredHammer; + + private final Screwdriver screwdriver; + + public int postConstructCallCount; + public int preDestroyCallCount; + public int setterCallCount; + + @Inject + MainToolBox(List> allTools, + Screwdriver screwdriver, + @Named("big") Provider bigHammer, + List> allHammers) { + this.allTools = Objects.requireNonNull(allTools); + this.screwdriver = Objects.requireNonNull(screwdriver); + this.bigHammer = bigHammer; + this.allHammers = allHammers; + } + + @Inject + void setScrewdriver(Screwdriver screwdriver) { + assert(this.screwdriver == screwdriver); + setterCallCount++; + } + + @Override + public List> toolsInBox() { + return allTools; + } + + @Override + public Provider preferredHammer() { + return preferredHammer; + } + + public List> allHammers() { + return allHammers; + } + + public Provider bigHammer() { + return bigHammer; + } + + public Screwdriver screwdriver() { + return screwdriver; + } + + @PostConstruct + void postConstruct() { + postConstructCallCount++; + } + + @PreDestroy + void preDestroy() { + preDestroyCallCount++; + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/Screwdriver.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/Screwdriver.java new file mode 100644 index 00000000000..4620c96a90f --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/Screwdriver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.io.Serializable; +import java.util.Optional; + +import io.helidon.pico.tests.pico.SomeOtherLocalNonContractInterface1; +import io.helidon.pico.tests.pico.tbox.Tool; + +import jakarta.inject.Singleton; + +@Singleton +public class Screwdriver implements Tool, SomeOtherLocalNonContractInterface1, Serializable { + + @Override + public Optional named() { + return Optional.of("screwdriver"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/SledgeHammer.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/SledgeHammer.java new file mode 100644 index 00000000000..7bf59429437 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/SledgeHammer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox.impl; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.tests.pico.tbox.Hammer; + +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 2) +//@Named(SledgeHammer.NAME) +public class SledgeHammer implements Hammer { + + public static final String NAME = "sledge"; + + @Override + public Optional named() { + return Optional.of(NAME + " hammer"); + } + +} diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/package-info.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/package-info.java new file mode 100644 index 00000000000..8c007611bea --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/impl/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Testing. + */ +package io.helidon.pico.tests.pico.tbox.impl; diff --git a/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/package-info.java b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/package-info.java new file mode 100644 index 00000000000..93c99508704 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/io/helidon/pico/tests/pico/tbox/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 Testing. + */ +package io.helidon.pico.tests.pico.tbox; diff --git a/pico/tests/resources-pico/src/main/java/module-info.java b/pico/tests/resources-pico/src/main/java/module-info.java new file mode 100644 index 00000000000..7a7b7bec0b1 --- /dev/null +++ b/pico/tests/resources-pico/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 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 Test Resources. + */ +module io.helidon.pico.tests.pico { + requires static jakarta.inject; + requires static jakarta.annotation; + + requires io.helidon.common.types; + requires io.helidon.common; + requires io.helidon.pico.api; + requires io.helidon.pico.services; + requires io.helidon.pico.tests.plain; + + exports io.helidon.pico.tests.pico; + exports io.helidon.pico.tests.pico.interceptor; + exports io.helidon.pico.tests.pico.stacking; + exports io.helidon.pico.tests.pico.tbox; + + provides io.helidon.pico.Module with io.helidon.pico.tests.pico.Pico$$Module; + provides io.helidon.pico.Application with io.helidon.pico.tests.pico.Pico$$Application; +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/AnApplicationScopedService.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/AnApplicationScopedService.java new file mode 100644 index 00000000000..a3263da7857 --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/AnApplicationScopedService.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Default; + +/** + * {@link jakarta.enterprise.context.ApplicationScoped} is normally unsupported, and as such needs a -A entry in pom.xml + * to get it to map to {@link jakarta.inject.Singleton scope.} + */ +@ApplicationScoped +@Default +public class AnApplicationScopedService { + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/JavaxTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/JavaxTest.java new file mode 100644 index 00000000000..aa562d69e1a --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/JavaxTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import io.helidon.config.Config; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Default; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.basicTestableConfig; +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; + +/** + * Javax to Jakarta related tests. + */ +class JavaxTest { + private static final Config CONFIG = basicTestableConfig(); + + private Services services; + + @BeforeEach + void setUp() { + this.services = testableServices(CONFIG).services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + /** + * Uses {@link io.helidon.pico.tools.Options#TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE}. + * This also verifies that the qualifiers were mapped over properly from javax as well. + */ + @Test + void applicationScopeToSingletonScopeTranslation() { + ServiceProvider sp = services.lookupFirst(AnApplicationScopedService.class); + assertThat(sp.toString(), + equalTo("AnApplicationScopedService:INIT")); + assertThat(sp.serviceInfo().qualifiers(), + contains(DefaultQualifierAndValue.create(Default.class))); + assertThat(sp.serviceInfo().scopeTypeNames(), + containsInAnyOrder(Singleton.class.getName(), ApplicationScoped.class.getName())); + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestUtils.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestUtils.java new file mode 100644 index 00000000000..3d60bc31598 --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import io.helidon.pico.tools.ToolsException; + +/** + * Testing utilities. + */ +public final class TestUtils { + + private TestUtils() { + } + + /** + * Load string from resource. + * + * @param resourceNamePath the resource path + * @return the loaded string + */ + // same as from CommonUtils. + public static String loadStringFromResource(String resourceNamePath) { + try { + try (InputStream in = TestUtils.class.getClassLoader().getResourceAsStream(resourceNamePath)) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).trim(); + } + } catch (Exception e) { + throw new ToolsException("failed to load: " + resourceNamePath, e); + } + } + + /** + * Loads a String from a file, wrapping any exception encountered to a {@link io.helidon.pico.tools.ToolsException}. + * + * @param fileName the file name to load + * @return the contents of the file + * @throws io.helidon.pico.tools.ToolsException if there were any exceptions encountered + */ + // same as from CommonUtils. + public static String loadStringFromFile(String fileName) { + try { + Path filePath = Path.of(fileName); + String content = Files.readString(filePath); + return content.trim(); + } catch (IOException e) { + throw new ToolsException("unable to load from file: " + fileName, e); + } + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestingSingleton.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestingSingleton.java new file mode 100644 index 00000000000..3f4a9276c40 --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/TestingSingleton.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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.tests.pico; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.pico.RunLevel; +import io.helidon.pico.Resettable; +import io.helidon.pico.tests.pico.stacking.Intercepted; +import io.helidon.pico.tests.pico.stacking.InterceptedImpl; +import io.helidon.pico.tests.pico.tbox.Awl; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@RunLevel(RunLevel.STARTUP) +@Singleton +@Named("testing") +public class TestingSingleton extends InterceptedImpl implements Resettable { + final static AtomicInteger postConstructCount = new AtomicInteger(); + final static AtomicInteger preDestroyCount = new AtomicInteger(); + + @Inject Provider awlProvider; + + @Inject + TestingSingleton(Optional inner) { + super(inner); + } + + @Override + @PostConstruct + public void voidMethodWithNoArgs() { + postConstructCount.incrementAndGet(); + } + + @PreDestroy + public void preDestroy() { + preDestroyCount.incrementAndGet(); + } + + public static int postConstructCount() { + return postConstructCount.get(); + } + + public static int preDestroyCount() { + return preDestroyCount.get(); + } + + @Override + public boolean reset(boolean deep) { + postConstructCount.set(0); + preDestroyCount.set(0); + return true; + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java new file mode 100644 index 00000000000..6254811fd9b --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/interceptor/ComplexInterceptorTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2023 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.tests.pico.interceptor; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.Interceptor; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.ReflectionBasedSingletonServiceProvider; +import io.helidon.pico.tests.plain.interceptor.IA; +import io.helidon.pico.tests.plain.interceptor.IB; +import io.helidon.pico.tests.plain.interceptor.NamedInterceptor; + +import jakarta.inject.Named; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.PicoServicesConfig.KEY_PERMITS_DYNAMIC; +import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_APPLICATIONS; +import static io.helidon.pico.PicoServicesConfig.KEY_USES_COMPILE_TIME_MODULES; +import static io.helidon.pico.PicoServicesConfig.NAME; +import static io.helidon.pico.testing.PicoTestingSupport.basicTestableConfig; +import static io.helidon.pico.testing.PicoTestingSupport.bind; +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static io.helidon.pico.testing.PicoTestingSupport.toDescriptions; +import static io.helidon.pico.tests.pico.TestUtils.loadStringFromResource; +import static io.helidon.pico.tools.TypeTools.toFilePath; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ComplexInterceptorTest { + + Config config = basicTestableConfig(); + PicoServices picoServices; + Services services; + + @BeforeEach + void setUp() { + setUp(config); + } + + void setUp(Config config) { + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + @Test + void createInterceptorSource() throws Exception { + TypeName interceptorTypeName = DefaultTypeName.create(XImpl$$Pico$$Interceptor.class); + String path = toFilePath(interceptorTypeName); + File file = new File("./target/generated-sources/annotations", path); + assertThat(file.exists(), is(true)); + String java = Files.readString(file.toPath()); + assertEquals(loadStringFromResource("expected/ximpl-interceptor._java_"), + java); + } + + @Test + void runtimeWithNoInterception() throws Exception { + List> iaProviders = services.lookupAll(IA.class); + assertThat(toDescriptions(iaProviders), + contains("XImpl$$Pico$$Interceptor:INIT", "XImpl:INIT")); + + List> ibProviders = services.lookupAll(IB.class); + assertThat(iaProviders, equalTo(ibProviders)); + + ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); + assertThat(iaProviders.get(0), is(ximplProvider)); + + XImpl x = ximplProvider.get(); + x.methodIA1(); + x.methodIA2(); + x.methodIB("test"); + long val = x.methodX("a", 2, true); + assertThat(val, equalTo(101L)); + assertThat(x.methodY(), equalTo("methodY")); + assertThat(x.methodZ(), equalTo("methodZ")); + PicoException pe = assertThrows(PicoException.class, x::close); + assertThat(pe.getMessage(), + equalTo("forced: service provider: XImpl:ACTIVE")); + RuntimeException re = assertThrows(RuntimeException.class, x::throwRuntimeException); + assertThat(re.getMessage(), equalTo("forced")); + } + + @Test + void runtimeWithInterception() throws Exception { + // disable application and modules to affectively start with an empty registry. + Config config = Config.builder( + ConfigSources.create( + Map.of(NAME + "." + KEY_PERMITS_DYNAMIC, "true", + NAME + "." + KEY_USES_COMPILE_TIME_APPLICATIONS, "false", + NAME + "." + KEY_USES_COMPILE_TIME_MODULES, "true"), + "config-1")) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + tearDown(); + setUp(config); + bind(picoServices, ReflectionBasedSingletonServiceProvider + .create(NamedInterceptor.class, + DefaultServiceInfo.builder() + .serviceTypeName(NamedInterceptor.class.getName()) + .addQualifier(DefaultQualifierAndValue.createNamed(Named.class.getName())) + .addExternalContractsImplemented(Interceptor.class.getName()) + .build())); + assertThat(NamedInterceptor.ctorCount.get(), equalTo(0)); + + List> iaProviders = picoServices.services().lookupAll(IA.class); + assertThat(toDescriptions(iaProviders), + contains("XImpl$$Pico$$Interceptor:INIT", "XImpl:INIT")); + + List> ibProviders = services.lookupAll(IB.class); + assertThat(iaProviders, equalTo(ibProviders)); + + ServiceProvider ximplProvider = services.lookupFirst(XImpl.class); + assertThat(iaProviders.get(0), is(ximplProvider)); + + assertThat(NamedInterceptor.ctorCount.get(), equalTo(0)); + XImpl xIntercepted = ximplProvider.get(); + assertThat(NamedInterceptor.ctorCount.get(), equalTo(1)); + + xIntercepted.methodIA1(); + xIntercepted.methodIA2(); + xIntercepted.methodIB("test"); + long val = xIntercepted.methodX("a", 2, true); + assertThat(val, equalTo(202L)); + assertThat(xIntercepted.methodY(), equalTo("methodY")); + assertThat(xIntercepted.methodZ(), equalTo("methodZ")); + PicoException pe = assertThrows(PicoException.class, xIntercepted::close); + assertThat(pe.getMessage(), + equalTo("forced: service provider: XImpl:ACTIVE")); + RuntimeException re = assertThrows(RuntimeException.class, xIntercepted::throwRuntimeException); + assertThat(re.getMessage(), equalTo("forced")); + + assertThat(NamedInterceptor.ctorCount.get(), equalTo(1)); + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java new file mode 100644 index 00000000000..403fb36489b --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/provider/PerRequestProviderTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 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.tests.pico.provider; + +import io.helidon.config.Config; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.PicoTestingSupport; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class PerRequestProviderTest { + + Config config = PicoTestingSupport.basicTestableConfig(); + PicoServices picoServices; + Services services; + + @BeforeEach + void setUp() { + setUp(config); + } + + void setUp(Config config) { + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + @Test + void myConcreteClassContractTest() { + ServiceProvider sp = services.lookupFirst(MyConcreteClassContract.class); + assertThat(sp.description(), + equalTo("MyServices$MyConcreteClassContractPerRequestIPProvider:INIT")); + MyConcreteClassContract instance0 = sp.get(); + assertThat(instance0.toString(), + equalTo("MyConcreteClassContractPerRequestIPProvider:instance_0, ContextualServiceQuery" + + "(serviceInfoCriteria=ServiceInfoCriteria(serviceTypeName=Optional.empty, " + + "scopeTypeNames=[], qualifiers=[], contractsImplemented=[], runLevel=Optional.empty, " + + "weight=Optional.empty, externalContractsImplemented=[], activatorTypeName=Optional.empty," + + " moduleName=Optional.empty), injectionPointInfo=Optional.empty, expected=true), " + + "MyConcreteClassContractPerRequestProvider:instance_0")); + MyConcreteClassContract instance1 = sp.get(); + assertThat(instance1.toString(), + equalTo("MyConcreteClassContractPerRequestIPProvider:instance_1, ContextualServiceQuery" + + "(serviceInfoCriteria=ServiceInfoCriteria(serviceTypeName=Optional.empty, " + + "scopeTypeNames=[], qualifiers=[], contractsImplemented=[], runLevel=Optional.empty, " + + "weight=Optional.empty, externalContractsImplemented=[], activatorTypeName=Optional.empty," + + " moduleName=Optional.empty), injectionPointInfo=Optional.empty, expected=true), " + + "MyConcreteClassContractPerRequestProvider:instance_0")); + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/stacking/InterceptorStackingTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/stacking/InterceptorStackingTest.java new file mode 100644 index 00000000000..651fbb41f33 --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/stacking/InterceptorStackingTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 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.tests.pico.stacking; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.config.Config; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.PicoTestingSupport; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +class InterceptorStackingTest { + + Config config = PicoTestingSupport.basicTestableConfig(); + PicoServices picoServices; + Services services; + + @BeforeEach + void setUp() { + setUp(config); + } + + void setUp(Config config) { + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + @Test + void interceptorStacking() { + List> allIntercepted = services.lookupAll(Intercepted.class); + List desc = allIntercepted.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // order matters here + assertThat(desc, contains( + "MostOuterInterceptedImpl:INIT", + "OuterInterceptedImpl:INIT", + "InterceptedImpl:INIT", + "TestingSingleton:INIT")); + + List injections = allIntercepted.stream().map(sp -> { + Intercepted inner = sp.get().getInner(); + return DefaultTypeName.createFromTypeName(sp.serviceInfo().serviceTypeName()).className() + " injected with " + + (inner == null ? null : inner.getClass().getSimpleName()); + }).collect(Collectors.toList()); + // order matters here + assertThat(injections, + contains("MostOuterInterceptedImpl injected with OuterInterceptedImpl", + "OuterInterceptedImpl injected with InterceptedImpl", + "InterceptedImpl injected with null", + "TestingSingleton injected with MostOuterInterceptedImpl")); + assertThat(services.lookup(Intercepted.class).get().sayHello("arg"), + equalTo("MostOuterInterceptedImpl:OuterInterceptedImpl:InterceptedImpl:arg")); + } + +} diff --git a/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java new file mode 100644 index 00000000000..5d940e15f3b --- /dev/null +++ b/pico/tests/resources-pico/src/test/java/io/helidon/pico/tests/pico/tbox/ToolBoxTest.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2023 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.tests.pico.tbox; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.helidon.config.Config; +import io.helidon.pico.ActivationResult; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.Module; +import io.helidon.pico.PicoException; +import io.helidon.pico.PicoServices; +import io.helidon.pico.RunLevel; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.testing.PicoTestingSupport; +import io.helidon.pico.tests.pico.ASerialProviderImpl; +import io.helidon.pico.tests.pico.TestingSingleton; +import io.helidon.pico.tests.pico.provider.FakeConfig; +import io.helidon.pico.tests.pico.provider.FakeServer; +import io.helidon.pico.tests.pico.stacking.Intercepted; +import io.helidon.pico.tests.pico.tbox.impl.BigHammer; +import io.helidon.pico.tests.pico.tbox.impl.MainToolBox; + +import jakarta.inject.Provider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static io.helidon.pico.tests.pico.TestUtils.loadStringFromFile; +import static io.helidon.pico.tests.pico.TestUtils.loadStringFromResource; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Expectation here is that the annotation processor ran, and we can use standard injection and pico-di registry services, etc. + */ +class ToolBoxTest { + + Config config = PicoTestingSupport.basicTestableConfig(); + PicoServices picoServices; + Services services; + + @BeforeEach + void setUp() { + setUp(config); + } + + void setUp(Config config) { + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @AfterEach + void tearDown() { + resetAll(); + } + + @Test + void sanity() { + assertNotNull(picoServices); + assertNotNull(services); + } + + @Test + void toolbox() { + List> blanks = services.lookupAll(Awl.class); + List desc = blanks.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // note that order matters here + assertThat(desc, + contains("AwlImpl:INIT")); + + List> allToolBoxes = services.lookupAll(ToolBox.class); + desc = allToolBoxes.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat(desc, + contains("MainToolBox:INIT")); + + ToolBox toolBox = allToolBoxes.get(0).get(); + assertThat(toolBox.getClass(), equalTo(MainToolBox.class)); + MainToolBox mtb = (MainToolBox) toolBox; + assertThat(mtb.postConstructCallCount, equalTo(1)); + assertThat(mtb.preDestroyCallCount, equalTo(0)); + assertThat(mtb.setterCallCount, equalTo(1)); + List> allTools = mtb.toolsInBox(); + desc = allTools.stream().map(Object::toString).collect(Collectors.toList()); + assertThat(desc, + contains("SledgeHammer:INIT", + "BigHammer:INIT", + "TableSaw:INIT", + "AwlImpl:INIT", + "HandSaw:INIT", + "LittleHammer:INIT", + "Screwdriver:ACTIVE")); + assertThat(mtb.screwdriver(), notNullValue()); + + Provider hammer = Objects.requireNonNull(toolBox.preferredHammer()); + assertThat(hammer.get(), notNullValue()); + assertThat(hammer.get(), is(hammer.get())); + assertThat(BigHammer.class, equalTo(hammer.get().getClass())); + desc = allTools.stream().map(Object::toString).collect(Collectors.toList()); + assertThat(desc, + contains("SledgeHammer:INIT", + "BigHammer:ACTIVE", + "TableSaw:INIT", + "AwlImpl:INIT", + "HandSaw:INIT", + "LittleHammer:INIT", + "Screwdriver:ACTIVE")); + + desc = (((MainToolBox) toolBox).allHammers()).stream().map(Object::toString).collect(Collectors.toList()); + assertThat(desc, + contains("SledgeHammer:INIT", + "BigHammer:ACTIVE", + "LittleHammer:INIT")); + assertThat(((ServiceProvider) ((MainToolBox) toolBox).bigHammer()).description(), + equalTo("BigHammer:ACTIVE")); + } + + @Test + void testClasses() { + assertThat(services.lookupFirst(TestingSingleton.class), + notNullValue()); + } + + /** + * This assumes {@link io.helidon.pico.tools.Options#TAG_AUTO_ADD_NON_CONTRACT_INTERFACES} has + * been enabled - see pom.xml + */ + @Test + void autoExternalContracts() { + List> allSerializable = services.lookupAll(Serializable.class); + List desc = allSerializable.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // note that order matters here + assertThat(desc, + contains("ASerialProviderImpl:INIT", "Screwdriver:INIT")); + } + + @Test + void providerTest() { + Serializable s1 = services.lookupFirst(Serializable.class).get(); + assertThat(s1, notNullValue()); + assertThat(ASerialProviderImpl.class + " is a higher weight and should have been returned for " + String.class, + String.class, equalTo(s1.getClass())); + assertThat(services.lookupFirst(Serializable.class).get(), not(s1)); + } + + @Test + void modules() { + List> allModules = services.lookupAll(Module.class); + List desc = allModules.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // note that order matters here + assertThat("ensure that Annotation Processors are enabled in the tools module meta-inf/services", + desc, contains("Pico$$Module:ACTIVE", "Pico$$TestModule:ACTIVE")); + List names = allModules.stream() + .sorted() + .map(m -> m.get().named().orElse(m.get().getClass().getSimpleName() + ":null")).collect(Collectors.toList()); + assertThat(names, + contains("io.helidon.pico.tests.pico", "io.helidon.pico.tests.pico/test")); + } + + /** + * The pico module-info that was created (by APT processing). + */ + @Test + void moduleInfo() { + assertThat(loadStringFromFile("target/pico/classes/module-info.java.pico"), + equalTo(loadStringFromResource("expected/module-info.java._pico_"))); + } + + /** + * The pico test version of module-info that was created (by APT processing). + */ + @Test + void testModuleInfo() { + assertThat(loadStringFromFile("target/pico/test-classes/module-info.java.pico"), + equalTo(loadStringFromResource("expected/tests-module-info.java._pico_"))); + } + + @Test + void innerClassesCanBeGenerated() { + FakeServer.Builder s1 = services.lookupFirst(FakeServer.Builder.class).get(); + assertThat(s1, notNullValue()); + assertThat(services.lookupFirst(FakeServer.Builder.class).get(), is(s1)); + + FakeConfig.Builder c1 = services.lookupFirst(FakeConfig.Builder.class).get(); + assertThat(c1, notNullValue()); + assertThat(services.lookupFirst(FakeConfig.Builder.class).get(), is(c1)); + } + + /** + * Targets {@link io.helidon.pico.tests.pico.tbox.AbstractSaw} with derived classes of + * {@link io.helidon.pico.tests.pico.tbox.impl.HandSaw} and {@link io.helidon.pico.tests.pico.tbox.TableSaw} found in different packages. + */ + @Test + void hierarchyOfInjections() { + List> saws = services.lookupAll(AbstractSaw.class); + List desc = saws.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // note that order matters here + assertThat(desc, + contains("TableSaw:INIT", "HandSaw:INIT")); + for (ServiceProvider saw : saws) { + saw.get().verifyState(); + } + } + + /** + * This tests the presence of module(s) + application(s) to handle all bindings, with effectively no lookups. + */ + @Test + void runlevel() { + // we start with 1 because we are looking for interceptors (which there is one here in this module) + assertThat(picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), equalTo(1)); + List> runLevelServices = services + .lookupAll(DefaultServiceInfoCriteria.builder().runLevel(RunLevel.STARTUP).build(), true); + List desc = runLevelServices.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat(desc, contains("TestingSingleton:INIT")); + + runLevelServices.forEach(sp -> Objects.requireNonNull(sp.get(), sp + " failed on get()")); + assertThat("activation should not have triggered any new lookups other than the startup we just did", + picoServices.metrics().orElseThrow().lookupCount().orElseThrow(), equalTo(2)); + desc = runLevelServices.stream().map(ServiceProvider::description).collect(Collectors.toList()); + assertThat(desc, contains("TestingSingleton:ACTIVE")); + } + + /** + * This assumes the presence of module(s) + application(s) to handle all bindings, with effectively no lookups! + */ + @Test + void noServiceActivationRequiresLookupWhenApplicationIsPresent() { + List> allServices = services + .lookupAll(DefaultServiceInfoCriteria.builder().build(), true); + allServices.stream() + .filter(sp -> !(sp instanceof Provider)) + .forEach(sp -> { + sp.get(); + assertThat("activation should not have triggered any lookups (for singletons): " + + sp + " triggered lookups", picoServices.metrics().orElseThrow().lookupCount(), + equalTo(1)); + }); + } + + @Test + void startupAndShutdownCallsPostConstructAndPreDestroy() { + assertThat(TestingSingleton.postConstructCount(), equalTo(0)); + assertThat(TestingSingleton.preDestroyCount(), equalTo(0)); + + List> allInterceptedBefore = services.lookupAll(Intercepted.class); + assertThat(allInterceptedBefore.size(), greaterThan(0)); + assertThat(TestingSingleton.postConstructCount(), equalTo(0)); + assertThat(TestingSingleton.preDestroyCount(), equalTo(0)); + + TestingSingleton testingSingletonFromLookup = picoServices.services().lookup(TestingSingleton.class).get(); + assertThat(testingSingletonFromLookup, notNullValue()); + assertThat(TestingSingleton.postConstructCount(), equalTo(1)); + assertThat(TestingSingleton.preDestroyCount(), equalTo(0)); + + Map map = picoServices.shutdown().orElseThrow(); + Map report = map.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> e.getValue().startingActivationPhase().toString() + + "->" + e.getValue().finishingActivationPhase())); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.Pico$$Application", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.Pico$$Module", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.Pico$$TestApplication", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.Pico$$TestModule", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.stacking.MostOuterInterceptedImpl", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.stacking.OuterInterceptedImpl", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.stacking.InterceptedImpl", "ACTIVE->DESTROYED")); + assertThat(report, hasEntry("io.helidon.pico.tests.pico.TestingSingleton", "ACTIVE->DESTROYED")); + assertThat(report + " : expected 8 services to be present", report.size(), equalTo(8)); + + assertThat(TestingSingleton.postConstructCount(), equalTo(1)); + assertThat(TestingSingleton.preDestroyCount(), equalTo(1)); + + assertThat(picoServices.metrics().orElseThrow().lookupCount().orElse(0), equalTo(0)); + + PicoException e = assertThrows(PicoException.class, () -> picoServices.services()); + assertThat(e.getMessage(), equalTo("must reset() after shutdown()")); + + tearDown(); + setUp(); + TestingSingleton testingSingletonFromLookup2 = picoServices.services().lookup(TestingSingleton.class).get(); + assertThat(testingSingletonFromLookup2, not(testingSingletonFromLookup)); + + map = picoServices.shutdown().orElseThrow(); + report = map.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e2 -> e2.getValue().startingActivationPhase().toString() + + "->" + e2.getValue().finishingActivationPhase())); + assertThat(report.toString(), report.size(), is(8)); + + tearDown(); + map = picoServices.shutdown().orElseThrow(); + assertThat(map.toString(), map.size(), is(0)); + } + + @Test + void knownProviders() { + List> providers = services.lookupAll( + DefaultServiceInfoCriteria.builder().addContractImplemented(Provider.class.getName()).build()); + List desc = providers.stream().map(ServiceProvider::description).collect(Collectors.toList()); + // note that order matters here + assertThat(desc, + contains("ASerialProviderImpl:INIT", + "MyServices$MyConcreteClassContractPerRequestIPProvider:INIT", + "MyServices$MyConcreteClassContractPerRequestProvider:INIT", + "BladeProvider:INIT")); + } + +} diff --git a/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ new file mode 100644 index 00000000000..a5a5206d08c --- /dev/null +++ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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 Test Resources. + */ +module io.helidon.pico.tests.pico { + requires static jakarta.inject; + requires static jakarta.annotation; + + requires io.helidon.common.types; + requires io.helidon.common; + requires io.helidon.pico.api; + requires io.helidon.pico.services; + requires io.helidon.pico.tests.plain; + + exports io.helidon.pico.tests.pico; + exports io.helidon.pico.tests.pico.interceptor; + exports io.helidon.pico.tests.pico.stacking; + exports io.helidon.pico.tests.pico.tbox; + + provides io.helidon.pico.Module with io.helidon.pico.tests.pico.Pico$$Module; + provides io.helidon.pico.Application with io.helidon.pico.tests.pico.Pico$$Application; + // pico external contract usage - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires test1; + requires test2; + uses jakarta.inject.Provider; + uses io.helidon.pico.tests.plain.interceptor.IA; + uses io.helidon.pico.tests.plain.interceptor.IB; + uses io.helidon.pico.InjectionPointProvider; + uses io.helidon.pico.OptionallyNamed; + // pico contract usage - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + exports io.helidon.pico.tests.pico.provider; +} diff --git a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ new file mode 100644 index 00000000000..d637133a4ec --- /dev/null +++ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ @@ -0,0 +1,13 @@ +// @Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") +module io.helidon.pico.tests.pico/test { + exports io.helidon.pico.tests.pico; + // pico module - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + provides io.helidon.pico.Module with io.helidon.pico.tests.pico.Pico$$TestModule; + // pico application - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + provides io.helidon.pico.Application with io.helidon.pico.tests.pico.Pico$$TestApplication; + // pico external contract usage - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + uses io.helidon.pico.Resettable; + uses io.helidon.pico.tests.pico.stacking.Intercepted; + // pico services - Generated(value = "io.helidon.pico.tools.DefaultActivatorCreator", comments = "version=1") + requires transitive io.helidon.pico.services; +} diff --git a/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ new file mode 100644 index 00000000000..c5534330498 --- /dev/null +++ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ @@ -0,0 +1,296 @@ +// This is a generated file (powered by Helidon). Do not edit or extend from this artifact as it is subject to change at any time! + +package io.helidon.pico.tests.pico.interceptor; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.DefaultInvocationContext; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationException; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.InterceptedMethod; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.pico.services.Invocation.createInvokeAndSupply; +import static io.helidon.pico.services.Invocation.mergeAndCollapse; + +/** + * Pico {@link Interceptor} manager for {@link io.helidon.pico.tests.pico.interceptor.XImpl }. + */ +@io.helidon.common.Weight(100.001) +@io.helidon.pico.Intercepted(io.helidon.pico.tests.pico.interceptor.XImpl.class) +@Singleton +@SuppressWarnings("ALL") +@jakarta.annotation.Generated(value = "io.helidon.pico.tools.DefaultInterceptorCreator", comments = "version=1") +public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interceptor.XImpl { + private static final List __serviceLevelAnnotations = List.of( + DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class), + DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX")), + DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))); + + private static final TypedElementName __ctor = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName(io.helidon.pico.ElementInfo.CONSTRUCTOR) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .build(); + private static final TypedElementName __methodIA1 = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("methodIA1") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .build(); + private static final TypedElementName __methodIA2 = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("methodIA2") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IA2"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .build(); + private static final TypedElementName __methodIB = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("methodIB") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class, Map.of("value", "IBSubAnno"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "methodIB"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .build(); + private static final TypedElementName __methodIB__p1 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("p1") + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) + .build(); + private static final TypedElementName __close = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("close") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno.class)) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .addAnnotation(DefaultAnnotationAndValue.create(java.lang.Override.class)) + .build(); + private static final TypedElementName __methodX = DefaultTypedElementName.builder() + .typeName(create(long.class)) + .elementName("methodX") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .build(); + private static final TypedElementName __methodX__p1 = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("p1") + .build(); + private static final TypedElementName __methodX__p2 = DefaultTypedElementName.builder() + .typeName(create(int.class)) + .elementName("p2") + .build(); + private static final TypedElementName __methodX__p3 = DefaultTypedElementName.builder() + .typeName(create(boolean.class)) + .elementName("p3") + .build(); + private static final TypedElementName __methodY = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("methodY") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .build(); + private static final TypedElementName __methodZ = DefaultTypedElementName.builder() + .typeName(create(java.lang.String.class)) + .elementName("methodZ") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .build(); + private static final TypedElementName __throwRuntimeException = DefaultTypedElementName.builder() + .typeName(create(void.class)) + .elementName("throwRuntimeException") + .addAnnotation(DefaultAnnotationAndValue.create(io.helidon.pico.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Named.class, Map.of("value", "ClassX"))) + .addAnnotation(DefaultAnnotationAndValue.create(jakarta.inject.Singleton.class)) + .build(); + + private final Provider __provider; + private final ServiceProvider __sp; + private final io.helidon.pico.tests.pico.interceptor.XImpl __impl; + private final TypeName __serviceTypeName; + private final List> __methodIA1__interceptors; + private final List> __methodIA2__interceptors; + private final List> __methodIB__interceptors; + private final List> __close__interceptors; + private final List> __methodX__interceptors; + private final List> __methodY__interceptors; + private final List> __methodZ__interceptors; + private final List> __throwRuntimeException__interceptors; + private final InterceptedMethod __methodIA1__call; + private final InterceptedMethod __methodIA2__call; + private final InterceptedMethod __methodIB__call; + private final InterceptedMethod __close__call; + private final InterceptedMethod __methodX__call; + private final InterceptedMethod __methodY__call; + private final InterceptedMethod __methodZ__call; + private final InterceptedMethod __throwRuntimeException__call; + + @Inject + XImpl$$Pico$$Interceptor( + @Named("io.helidon.pico.tests.plain.interceptor.InterceptorBasedAnno") List> io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, + @Named("jakarta.inject.Named") List> jakarta_inject_Named, + Provider provider) { + this.__provider = Objects.requireNonNull(provider); + this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider) __provider : null; + this.__serviceTypeName = DefaultTypeName.create(io.helidon.pico.tests.pico.interceptor.XImpl.class); + List> __ctor__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); + this.__methodIA1__interceptors = mergeAndCollapse(jakarta_inject_Named); + this.__methodIA2__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); + this.__methodIB__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); + this.__close__interceptors = mergeAndCollapse(io_helidon_pico_tests_plain_interceptor_InterceptorBasedAnno, jakarta_inject_Named); + this.__methodX__interceptors = mergeAndCollapse(jakarta_inject_Named); + this.__methodY__interceptors = mergeAndCollapse(jakarta_inject_Named); + this.__methodZ__interceptors = mergeAndCollapse(jakarta_inject_Named); + this.__throwRuntimeException__interceptors = mergeAndCollapse(jakarta_inject_Named); + + Supplier call = __provider::get; + io.helidon.pico.tests.pico.interceptor.XImpl result = createInvokeAndSupply( + DefaultInvocationContext.builder() + .serviceProvider(__sp) + .serviceTypeName(__serviceTypeName) + .classAnnotations(__serviceLevelAnnotations) + .elementInfo(__ctor) + .interceptors(__ctor__interceptors) + /*.build()*/, + call); + this.__impl = Objects.requireNonNull(result); + + this.__methodIA1__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIA1__interceptors, __methodIA1) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().methodIA1(); + return null; + } + }; + + this.__methodIA2__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIA2__interceptors, __methodIA2) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().methodIA2(); + return null; + } + }; + + this.__methodIB__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB__interceptors, __methodIB, + new TypedElementName[] {__methodIB__p1}) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().methodIB((java.lang.String) args[0]); + return null; + } + }; + + this.__close__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __close__interceptors, __close) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().close(); + return null; + } + }; + + this.__methodX__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodX__interceptors, __methodX, + new TypedElementName[] {__methodX__p1, __methodX__p2, __methodX__p3}) { + @Override + public java.lang.Long invoke(Object... args) throws Throwable { + return impl().methodX((java.lang.String) args[0], (java.lang.Integer) args[1], (java.lang.Boolean) args[2]); + } + }; + + this.__methodY__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodY__interceptors, __methodY) { + @Override + public java.lang.String invoke(Object... args) throws Throwable { + return impl().methodY(); + } + }; + + this.__methodZ__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodZ__interceptors, __methodZ) { + @Override + public java.lang.String invoke(Object... args) throws Throwable { + return impl().methodZ(); + } + }; + + this.__throwRuntimeException__call = new InterceptedMethod( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __throwRuntimeException__interceptors, __throwRuntimeException) { + @Override + public java.lang.Void invoke(Object... args) throws Throwable { + impl().throwRuntimeException(); + return null; + } + }; + } + + @Override + public void methodIA1() { + createInvokeAndSupply(__methodIA1__call.ctx(), () -> __methodIA1__call.apply()); + } + + @Override + public void methodIA2() { + createInvokeAndSupply(__methodIA2__call.ctx(), () -> __methodIA2__call.apply()); + } + + @Override + public void methodIB(java.lang.String p1) { + createInvokeAndSupply(__methodIB__call.ctx(), () -> __methodIB__call.apply(p1)); + } + + @Override + public void close() throws java.io.IOException, java.lang.RuntimeException { + createInvokeAndSupply(__close__call.ctx(), () -> __close__call.apply()); + } + + @Override + public long methodX(java.lang.String p1, int p2, boolean p3) throws java.io.IOException, java.lang.RuntimeException, java.lang.AssertionError { + return createInvokeAndSupply(__methodX__call.ctx(), () -> __methodX__call.apply(p1, p2, p3)); + } + + @Override + public java.lang.String methodY() { + return createInvokeAndSupply(__methodY__call.ctx(), () -> __methodY__call.apply()); + } + + @Override + public java.lang.String methodZ() { + return createInvokeAndSupply(__methodZ__call.ctx(), () -> __methodZ__call.apply()); + } + + @Override + public void throwRuntimeException() { + createInvokeAndSupply(__throwRuntimeException__call.ctx(), () -> __throwRuntimeException__call.apply()); + } + +} diff --git a/pico/tests/resources-plain/pom.xml b/pico/tests/resources-plain/pom.xml new file mode 100644 index 00000000000..2ad7fa44ada --- /dev/null +++ b/pico/tests/resources-plain/pom.xml @@ -0,0 +1,53 @@ + + + + + + io.helidon.pico.tests + helidon-pico-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-tests-resources-plain + Helidon Pico Test Plain Resources + a jar that offers contracts and other artifacts but is not a native Pico module (e.g., no Pico APT) + + + + + io.helidon.pico + helidon-pico-api + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + + diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/Hello.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/Hello.java new file mode 100644 index 00000000000..b8cbff3f823 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/Hello.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +import io.helidon.pico.Contract; + +@Contract +public interface Hello { + + void sayHello(); + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/HelloImpl.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/HelloImpl.java new file mode 100644 index 00000000000..1c0b7b6b23c --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/HelloImpl.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +import java.util.List; +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +public class HelloImpl implements Hello { + + @Inject + World world; + + @Inject + Provider worldRef; + + @Inject + List> listOfWorldRefs; + + @Inject + List listOfWorlds; + + @Inject @Named("red") + Optional redWorld; + + @Inject + private Optional privateWorld; + + private World setWorld; + private Optional setRedWorld; + private World ctorWorld; + + int postConstructCallCount; + int preDestroyCallCount; + + HelloImpl() { + } + + @Inject + public HelloImpl(World ctorWorld) { + this(); + this.ctorWorld = ctorWorld; + } + + @Override + public void sayHello() { + assert (postConstructCallCount == 1); + assert (preDestroyCallCount == 0); + System.getLogger(getClass().getName()).log(System.Logger.Level.INFO, "hello {0}", worldRef.get()); + assert (world == worldRef.get()) : "world != worldRef"; + assert (world == setWorld) : "world != setWorld"; + assert (ctorWorld == world) : "world != ctorWorld"; + } + + @Inject + public void world(World world) { + this.setWorld = world; + assert (world == ctorWorld); + } + + @Inject + public void setRedWorld(@Named("red") Optional redWorld) { + this.setRedWorld = redWorld; + } + + @PostConstruct + public void postConstruct() { + postConstructCallCount++; + } + + @PreDestroy + public void preDestroy() { + preDestroyCallCount++; + } + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/MyTestQualifier.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/MyTestQualifier.java new file mode 100644 index 00000000000..a55029b848b --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/MyTestQualifier.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +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; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; + +/** + * Explicitly intended to test compile time scope w/ inheritance of annotations. + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.CLASS) +@Target({TYPE, METHOD, PARAMETER, CONSTRUCTOR}) +public @interface MyTestQualifier { + + /** + * @return the name + */ + String value() default ""; + + /** + * @return just for testing + */ + String[] extendedValue() default ""; + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/SomeOtherLocalNonContractInterface2.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/SomeOtherLocalNonContractInterface2.java new file mode 100644 index 00000000000..f9930ec9c51 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/SomeOtherLocalNonContractInterface2.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +public interface SomeOtherLocalNonContractInterface2 { + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/World.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/World.java new file mode 100644 index 00000000000..b0585dbc517 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/World.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +@FunctionalInterface +public interface World { + + String name(); + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/WorldImpl.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/WorldImpl.java new file mode 100644 index 00000000000..4c06e763eee --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/hello/WorldImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.tests.plain.hello; + +import java.io.Serializable; + +import io.helidon.pico.ExternalContracts; + +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@ExternalContracts(value = World.class, moduleNames = "AnotherModule") +@Named("unknown") +@Singleton +public class WorldImpl implements World, SomeOtherLocalNonContractInterface2, Serializable { + private final String name; + + WorldImpl() { + this("unknown"); + } + + WorldImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IA.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IA.java new file mode 100644 index 00000000000..352f6ea9935 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IA.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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.tests.plain.interceptor; + +import io.helidon.pico.Contract; + +@Contract +public interface IA { + + void methodIA1(); + + void methodIA2(); + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java new file mode 100644 index 00000000000..fe1f97684d5 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/IB.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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.tests.plain.interceptor; + +@InterceptorBasedAnno("IBAnno") +public interface IB { + + void methodIB(String val); + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/InterceptorBasedAnno.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/InterceptorBasedAnno.java new file mode 100644 index 00000000000..76033df8a06 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/InterceptorBasedAnno.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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.tests.plain.interceptor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.pico.InterceptedTrigger; + +@InterceptedTrigger +@Retention(RetentionPolicy.CLASS) +public @interface InterceptorBasedAnno { + + String value() default ""; + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java new file mode 100644 index 00000000000..942a972e891 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/NamedInterceptor.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 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.tests.plain.interceptor; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationContext; + +@SuppressWarnings({"ALL", "unchecked"}) +public class NamedInterceptor implements Interceptor { + public static final AtomicInteger ctorCount = new AtomicInteger(); + + public NamedInterceptor() { + ctorCount.incrementAndGet(); + } + + @Override + public V proceed(InvocationContext ctx, + Chain chain, + Object... args) { + assert (ctx != null); + + TypedElementName methodInfo = ctx.elementInfo(); + if (methodInfo != null && methodInfo.typeName().equals(DefaultTypeName.create(long.class))) { + V result = chain.proceed(args); + long longResult = (Long) result; + Object interceptedResult = (longResult * 2); + return (V) interceptedResult; + } else { + return chain.proceed(args); + } + } + +} diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java new file mode 100644 index 00000000000..84fd4f23048 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/X.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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.tests.plain.interceptor; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +@Named("ClassX") +public class X implements IA, IB, Closeable { + + X() { + // this is the one that will be used by interception + } + + @Inject + public X(Optional optionalIA) { + assert (optionalIA.isEmpty()); + } + + @Override + public void methodIA1() { + } + + @InterceptorBasedAnno("IA2") + @Override + public void methodIA2() { + } + + @Named("methodIB") + @InterceptorBasedAnno("IBSubAnno") + @Override + public void methodIB(@Named("arg1") String val) { + } + + @InterceptorBasedAnno + @Override + public void close() throws IOException, RuntimeException { + throw new IOException("forced"); + } + + public long methodX(String arg1, int arg2, boolean arg3) throws IOException, RuntimeException, AssertionError { + return 101; + } + + // test of package private + String methodY() { + return "methodY"; + } + + // test of protected + protected String methodZ() { + return "methodZ"; + } + + // test of protected + protected void throwRuntimeException() { + throw new RuntimeException("forced"); + } + +} diff --git a/pico/tests/resources-plain/src/main/java/module-info.java b/pico/tests/resources-plain/src/main/java/module-info.java new file mode 100644 index 00000000000..53019f0d268 --- /dev/null +++ b/pico/tests/resources-plain/src/main/java/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Plain Test Resources. + */ +module io.helidon.pico.tests.plain { + requires static jakarta.inject; + requires static jakarta.annotation; + + requires io.helidon.common.types; + requires io.helidon.common; + requires io.helidon.pico.api; + + exports io.helidon.pico.tests.plain.hello; + exports io.helidon.pico.tests.plain.interceptor; +} diff --git a/pico/tests/tck-jsr330/pom.xml b/pico/tests/tck-jsr330/pom.xml new file mode 100644 index 00000000000..fb308ee66c8 --- /dev/null +++ b/pico/tests/tck-jsr330/pom.xml @@ -0,0 +1,128 @@ + + + + + + io.helidon.pico.tests + helidon-pico-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico-tests-tck-jsr330 + Helidon Pico Test JSR-330 TCK + + + true + true + + + + + jakarta.inject + jakarta.inject-tck + + + io.helidon.pico + helidon-pico-services + + + io.helidon.pico + helidon-pico-maven-plugin + provided + true + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + io.helidon.pico + helidon-pico-testing + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + + external-module-create + + + + + + -Apico.debug=${pico.debug} + -Apico.autoAddNonContractInterfaces=true + + + org.atinject.tck.auto + org.atinject.tck.auto.accessories + + true + + + org.atinject.tck.auto.accessories.SpareTire + + + jakarta.inject.Named + spare + + + + + org.atinject.tck.auto.DriversSeat + + + org.atinject.tck.auto.Drivers + + + + + + + + + + diff --git a/pico/tests/tck-jsr330/src/test/java/io/helidon/pico/tests/tck/jsr330/Jsr330TckTest.java b/pico/tests/tck-jsr330/src/test/java/io/helidon/pico/tests/tck/jsr330/Jsr330TckTest.java new file mode 100644 index 00000000000..a3d67c90b59 --- /dev/null +++ b/pico/tests/tck-jsr330/src/test/java/io/helidon/pico/tests/tck/jsr330/Jsr330TckTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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.tests.tck.jsr330; + +import java.util.Enumeration; +import java.util.Objects; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; + +import jakarta.inject.Provider; +import junit.framework.TestFailure; +import junit.framework.TestResult; +import org.atinject.tck.Tck; +import org.atinject.tck.auto.Car; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; + +/** + * Jsr-330 TCK Testing. + * This test requires the annotation processing and the maven-plugin to run - see pom.xml. + */ +class Jsr330TckTest { + + /** + * Run's the TCK tests. + */ + @Test + void testItAll() { + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); + PicoServicesConfig cfg = picoServices.config(); + Provider carProvider = picoServices.services().lookupFirst(Car.class); + Objects.requireNonNull(carProvider.get()); + assertThat("sanity", carProvider.get(), not(carProvider.get())); + junit.framework.Test jsrTest = Tck.testsFor(carProvider.get(), + cfg.supportsJsr330Statics(), + cfg.supportsJsr330Privates()); + TestResult result = new TestResult(); + jsrTest.run(result); + assertThat(result.runCount(), greaterThan(0)); + assertThat(toFailureReport(result), result.wasSuccessful(), is(true)); + } + + String toFailureReport(TestResult result) { + StringBuilder builder = new StringBuilder(); + int count = 0; + Enumeration failures = result.failures(); + while (failures.hasMoreElements()) { + TestFailure failure = failures.nextElement(); + builder.append("\nFAILURE #").append(++count).append(" : ") + .append(failure.trace()) + .append("\n"); + } + return builder.toString(); + } + +} diff --git a/pico/tools/etc/spotbugs/exclude.xml b/pico/tools/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..f9cc6b9205c --- /dev/null +++ b/pico/tools/etc/spotbugs/exclude.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pico/tools/pom.xml b/pico/tools/pom.xml index 613b1093974..21c1e9718ae 100644 --- a/pico/tools/pom.xml +++ b/pico/tools/pom.xml @@ -31,14 +31,22 @@ helidon-pico-tools Helidon Pico Tools + + etc/spotbugs/exclude.xml + + + + io.helidon.common + helidon-common-types + io.helidon.pico - helidon-pico + helidon-pico-api io.helidon.pico - helidon-pico-types + helidon-pico-services io.helidon.common @@ -57,15 +65,27 @@ com.github.jknack handlebars + + io.github.classgraph + classgraph + io.helidon.builder helidon-builder - provided io.helidon.builder helidon-builder-processor - provided + + + jakarta.inject + jakarta.inject-api + compile + + + jakarta.annotation + jakarta.annotation-api + provided io.helidon.config @@ -82,6 +102,16 @@ junit-jupiter-api test + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + jakarta.inject + jakarta.inject-tck + test + diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/AbstractCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractCreator.java new file mode 100644 index 00000000000..0924d962bbc --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractCreator.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.services.DefaultServiceBinder; + +import static io.helidon.pico.tools.CommonUtils.hasValue; +import static io.helidon.pico.tools.TypeTools.needToDeclareModuleUsage; +import static io.helidon.pico.tools.TypeTools.needToDeclarePackageUsage; + +/** + * Abstract base for any codegen creator. + */ +public abstract class AbstractCreator { + /** + * The default java source version (this can be explicitly overridden using the builder or maven plugin). + */ + public static final String DEFAULT_SOURCE = "11"; + /** + * The default java target version (this can be explicitly overridden using the builder or maven plugin). + */ + public static final String DEFAULT_TARGET = "11"; + + // no special chars since this will be used as a package and class name + static final String NAME_PREFIX = "Pico$$"; + static final String PICO_FRAMEWORK_MODULE = PicoServicesConfig.FQN + ".services"; + static final String MODULE_NAME_SUFFIX = "Module"; + + private final System.Logger logger = System.getLogger(getClass().getName()); + private final TemplateHelper templateHelper; + private final String templateName; + + AbstractCreator(String templateName) { + this.templateHelper = TemplateHelper.create(); + this.templateName = templateName; + } + + System.Logger logger() { + return logger; + } + + TemplateHelper templateHelper() { + return templateHelper; + } + + String templateName() { + return templateName; + } + + /** + * Creates a codegen filer that is not reliant on annotation processing, but still capable of creating source + * files and resources. + * + * @param paths the paths for where files should be read or written. + * @param isAnalysisOnly true if analysis only, where no code or resources will be physically written to disk + * @return the code gen filer instance to use + */ + CodeGenFiler createDirectCodeGenFiler(CodeGenPaths paths, + boolean isAnalysisOnly) { + AbstractFilerMessager filer = AbstractFilerMessager.createDirectFiler(paths, logger); + return new CodeGenFiler(filer, !isAnalysisOnly); + } + + /** + * The generated sticker string. + * + * @param req the creator request + * @return the sticker + */ + String toGeneratedSticker(GeneralCreatorRequest req) { + String generator = (null == req) ? null : req.generator().orElse(null); + return templateHelper.generatedStickerFor((generator != null) ? generator : getClass().getName()); + } + + /** + * Generates the {@link io.helidon.pico.Activator} source code for the provided service providers. Custom + * service providers (see {@link AbstractServiceProvider#isCustom()}) do not qualify to + * have activators code generated. + * + * @param sp the collection of service providers + * @return the code generated string for the service provider given + */ + static String toActivatorCodeGen(ServiceProvider sp) { + if (sp instanceof AbstractServiceProvider && ((AbstractServiceProvider) sp).isCustom()) { + return null; + } + return DefaultServiceBinder.toRootProvider(sp).activator().orElseThrow().getClass().getName() + ".INSTANCE"; + } + + /** + * Generates the {@link io.helidon.pico.Activator} source code for the provided service providers. + * + * @param coll the collection of service providers + * @return the code generated string for the collection of service providers given + */ + static String toActivatorCodeGen(Collection> coll) { + return CommonUtils.toString(coll, AbstractCreator::toActivatorCodeGen, null); + } + + /** + * Automatically adds the requirements to the module-info descriptor for what pico requires. + * + * @param moduleInfo the module info descriptor + * @param generatedAnno the generator sticker value + * @return the modified descriptor, fluent style + */ + DefaultModuleInfoDescriptor.Builder addPicoProviderRequirementsTo(DefaultModuleInfoDescriptor.Builder moduleInfo, + String generatedAnno) { + Objects.requireNonNull(generatedAnno); + // requirements on the pico services framework itself + String preComment = " // " + PicoServicesConfig.NAME + " services - Generated(" + generatedAnno + ")"; + ModuleInfoDescriptor.addIfAbsent(moduleInfo, PICO_FRAMEWORK_MODULE, DefaultModuleInfoItem.builder() + .requires(true) + .target(PICO_FRAMEWORK_MODULE) + .transitiveUsed(true) + .addPrecomment(preComment)); + return moduleInfo; + } + + ModuleInfoDescriptor createModuleInfo(ModuleInfoCreatorRequest req) { + String generatedAnno = templateHelper.generatedStickerFor(getClass().getName()); + String moduleInfoPath = req.moduleInfoPath().orElse(null); + String moduleName = req.name().orElse(null); + TypeName moduleTypeName = req.moduleTypeName(); + TypeName applicationTypeName = req.applicationTypeName().orElse(null); + String classPrefixName = req.classPrefixName(); + boolean isModuleCreated = req.moduleCreated(); + boolean isApplicationCreated = req.applicationCreated(); + Collection modulesRequired = req.modulesRequired(); + Map> contracts = req.contracts(); + Map> externalContracts = req.externalContracts(); + + DefaultModuleInfoDescriptor.Builder descriptorBuilder; + if (moduleInfoPath != null) { + descriptorBuilder = DefaultModuleInfoDescriptor + .toBuilder(ModuleInfoDescriptor.create(Paths.get(moduleInfoPath))); + if (hasValue(moduleName) && ModuleUtils.isUnnamedModuleName(descriptorBuilder.name())) { + descriptorBuilder.name(moduleName); + } + assert (descriptorBuilder.name().equals(moduleName) || (!hasValue(moduleName))) + : "bad module name: " + moduleName + " targeting " + descriptorBuilder.name(); + moduleName = descriptorBuilder.name(); + } else { + descriptorBuilder = DefaultModuleInfoDescriptor.builder().name(moduleName); + descriptorBuilder.headerComment("// @Generated(" + generatedAnno + ")"); + } + + boolean isTestModule = ModuleInfoDescriptor.DEFAULT_TEST_SUFFIX.equals(classPrefixName); + if (isTestModule) { + String baseModuleName = ModuleUtils.normalizedBaseModuleName(moduleName); + ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, baseModuleName, DefaultModuleInfoItem.builder() + .requires(true) + .target(baseModuleName) + .transitiveUsed(true)); + } + + if (isModuleCreated && (moduleTypeName != null)) { + if (!isTestModule) { + ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, moduleTypeName.packageName(), + DefaultModuleInfoItem.builder() + .exports(true) + .target(moduleTypeName.packageName())); + } + ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, TypeNames.PICO_MODULE, + DefaultModuleInfoItem.builder() + .provides(true) + .target(TypeNames.PICO_MODULE) + .addWithOrTo(moduleTypeName.name()) + .addPrecomment(" // " + + PicoServicesConfig.NAME + + " module - Generated(" + + generatedAnno + ")")); + } + if (isApplicationCreated && applicationTypeName != null) { + if (!isTestModule) { + ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, applicationTypeName.packageName(), + DefaultModuleInfoItem.builder() + .exports(true) + .target(applicationTypeName.packageName())); + } + ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, TypeNames.PICO_APPLICATION, + DefaultModuleInfoItem.builder() + .provides(true) + .target(TypeNames.PICO_APPLICATION) + .addWithOrTo(applicationTypeName.name()) + .addPrecomment(" // " + + PicoServicesConfig.NAME + + " application - Generated(" + + generatedAnno + ")")); + } + + String preComment = " // " + PicoServicesConfig.NAME + " external contract usage - Generated(" + generatedAnno + ")"; + if (modulesRequired != null) { + for (String externalModuleName : modulesRequired) { + if (!needToDeclareModuleUsage(externalModuleName)) { + continue; + } + + DefaultModuleInfoItem.Builder itemBuilder = DefaultModuleInfoItem.builder() + .requires(true) + .target(externalModuleName); + if (hasValue(preComment)) { + itemBuilder.addPrecomment(preComment); + } + + boolean added = ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, externalModuleName, itemBuilder); + if (added) { + preComment = ""; + } + } + } + + Set allExternalContracts = toAllContracts(externalContracts); + for (TypeName cn : allExternalContracts) { + String packageName = cn.packageName(); + if (!needToDeclarePackageUsage(packageName)) { + continue; + } + + DefaultModuleInfoItem.Builder itemBuilder = DefaultModuleInfoItem.builder() + .uses(true) + .target(cn.name()); + if (hasValue(preComment)) { + itemBuilder.addPrecomment(preComment); + } + + boolean added = ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, cn.name(), itemBuilder); + if (added) { + preComment = ""; + } + } + + if (!isTestModule && (contracts != null)) { + preComment = " // " + PicoServicesConfig.NAME + " contract usage - Generated(" + generatedAnno + ")"; + for (Map.Entry> e : contracts.entrySet()) { + for (TypeName contract : e.getValue()) { + if (!allExternalContracts.contains(contract)) { + String packageName = contract.packageName(); + if (!needToDeclarePackageUsage(packageName)) { + continue; + } + + DefaultModuleInfoItem.Builder itemBuilder = DefaultModuleInfoItem.builder() + .exports(true) + .target(packageName); + if (hasValue(preComment)) { + itemBuilder.addPrecomment(preComment); + } + + boolean added = ModuleInfoDescriptor.addIfAbsent(descriptorBuilder, packageName, itemBuilder); + if (added) { + preComment = ""; + } + } + } + } + } + + return addPicoProviderRequirementsTo(descriptorBuilder, generatedAnno); + } + + static Set toAllContracts(Map> servicesToContracts) { + Set result = new LinkedHashSet<>(); + servicesToContracts.forEach((serviceTypeName, cn) -> result.addAll(cn)); + return result; + } + + /** + * Creates the {@link io.helidon.pico.tools.CodeGenPaths} given the current batch of services to process. + * + * @param servicesToProcess the services to process + * @return the payload for code gen paths + */ + static CodeGenPaths createCodeGenPaths(ServicesToProcess servicesToProcess) { + Path moduleInfoFilePath = servicesToProcess.lastGeneratedModuleInfoFilePath(); + if (moduleInfoFilePath == null) { + moduleInfoFilePath = servicesToProcess.lastKnownModuleInfoFilePath(); + } + return DefaultCodeGenPaths.builder() + .moduleInfoPath(Optional.ofNullable((moduleInfoFilePath != null) ? moduleInfoFilePath.toString() : null)) + .build(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java new file mode 100644 index 00000000000..637ecca4fde --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/AbstractFilerMessager.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; + +/** + * Used to abstract processor based filer from direct filer (the latter used via maven plugin and other tooling). + */ +public abstract class AbstractFilerMessager implements Filer, Messager { + private static final System.Logger LOGGER = System.getLogger(AbstractFilerMessager.class.getName()); + + private final Filer filerDelegate; + private final Messager msgrDelegate; + private final System.Logger logger; + + AbstractFilerMessager(Filer filerDelegate, + Messager msgr) { + this.filerDelegate = filerDelegate; + this.msgrDelegate = msgr; + this.logger = LOGGER; + } + + AbstractFilerMessager(System.Logger logger) { + this.filerDelegate = null; + this.msgrDelegate = null; + this.logger = logger; + } + + /** + * Create an annotation based filer abstraction. + * + * @param processingEnv the processing env + * @param msgr the messager and error handler + * @return the filer facade + */ + public static AbstractFilerMessager createAnnotationBasedFiler(ProcessingEnvironment processingEnv, + Messager msgr) { + return new AbstractFilerMessager(Objects.requireNonNull(processingEnv.getFiler()), msgr) {}; + } + + /** + * Create a direct filer, not from annotation processing. + * + * @param paths the code paths + * @param logger the logger for messaging + * @return the filer facade + */ + public static AbstractFilerMessager createDirectFiler(CodeGenPaths paths, + System.Logger logger) { + return new DirectFilerMessager(Objects.requireNonNull(paths), logger) {}; + } + + @Override + public JavaFileObject createSourceFile(CharSequence name, + Element... originatingElements) throws IOException { + if (filerDelegate != null) { + return filerDelegate.createSourceFile(name, originatingElements); + } + throw new IllegalStateException(); + } + + @Override + public JavaFileObject createClassFile(CharSequence name, + Element... originatingElements) throws IOException { + if (filerDelegate != null) { + return filerDelegate.createClassFile(name, originatingElements); + } + throw new IllegalStateException(); + } + + @Override + public FileObject createResource(JavaFileManager.Location location, + CharSequence moduleAndPkg, + CharSequence relativeName, + Element... originatingElements) throws IOException { + if (filerDelegate != null) { + return filerDelegate.createResource(location, moduleAndPkg, relativeName, originatingElements); + } + throw new IllegalStateException(); + } + + @Override + public FileObject getResource(JavaFileManager.Location location, + CharSequence moduleAndPkg, + CharSequence relativeName) throws IOException { + if (filerDelegate != null) { + return filerDelegate.getResource(location, moduleAndPkg, relativeName); + } + throw new IllegalStateException(); + } + + @Override + public void debug(String message) { + if (msgrDelegate != null) { + msgrDelegate.debug(message); + } + if (logger != null) { + logger.log(System.Logger.Level.DEBUG, message); + } + } + + @Override + public void debug(String message, + Throwable t) { + if (msgrDelegate != null) { + msgrDelegate.debug(message, t); + } + if (logger != null) { + logger.log(System.Logger.Level.DEBUG, message, t); + } + } + + @Override + public void log(String message) { + if (msgrDelegate != null) { + msgrDelegate.log(message); + } + if (logger != null) { + logger.log(System.Logger.Level.INFO, message); + } + } + + @Override + public void warn(String message) { + if (msgrDelegate != null) { + msgrDelegate.warn(message); + } + if (logger != null) { + logger.log(System.Logger.Level.WARNING, message); + } + } + + @Override + public void warn(String message, + Throwable t) { + if (msgrDelegate != null) { + msgrDelegate.warn(message, t); + } + if (logger != null) { + logger.log(System.Logger.Level.WARNING, message, t); + } + } + + @Override + public void error(String message, + Throwable t) { + if (msgrDelegate != null) { + msgrDelegate.warn(message, t); + } + if (logger != null) { + logger.log(System.Logger.Level.ERROR, message, t); + } + } + + + static class DirectFilerMessager extends AbstractFilerMessager { + private final CodeGenPaths paths; + + DirectFilerMessager(CodeGenPaths paths, + System.Logger logger) { + super(logger); + this.paths = paths; + } + + @Override + public FileObject getResource(JavaFileManager.Location location, + CharSequence moduleAndPkg, + CharSequence relativeName) throws IOException { + return getResource(location, moduleAndPkg, relativeName, true); + } + + private FileObject getResource(JavaFileManager.Location location, + CharSequence ignoreModuleAndPkg, + CharSequence relativeName, + boolean expectedToExist) throws IOException { + if (StandardLocation.CLASS_OUTPUT != location) { + throw new IllegalStateException(location + " is not supported for: " + relativeName); + } + + File outDir = new File(paths.outputPath().orElseThrow()); + File resourceFile = new File(outDir, relativeName.toString()); + if (expectedToExist && !resourceFile.exists()) { + throw new NoSuchFileException(resourceFile.getPath()); + } + + return new DirectFileObject(resourceFile); + } + + @Override + public FileObject createResource(JavaFileManager.Location location, + CharSequence moduleAndPkg, + CharSequence relativeName, + Element... originatingElements) throws IOException { + return getResource(location, moduleAndPkg, relativeName, false); + } + + @Override + public JavaFileObject createSourceFile(CharSequence name, + Element... originatingElement) { + Path javaFilePath = Objects.requireNonNull(toSourcePath(StandardLocation.SOURCE_OUTPUT, name.toString())); + return new DirectJavaFileObject(javaFilePath.toFile()); + } + + Path toSourcePath(JavaFileManager.Location location, + String name) { + return toSourcePath(location, DefaultTypeName.createFromTypeName(name)); + } + + Path toSourcePath(JavaFileManager.Location location, + TypeName typeName) { + String sourcePath; + if (StandardLocation.SOURCE_PATH == location) { + sourcePath = paths.sourcePath().orElse(null); + } else if (StandardLocation.SOURCE_OUTPUT == location) { + sourcePath = paths.generatedSourcesPath().orElse(null); + } else { + throw new ToolsException("Unable to determine location of " + typeName + " with " + location); + } + + if (sourcePath == null) { + LOGGER.log(System.Logger.Level.DEBUG, "sourcepath is not defined in " + paths); + return null; + } + + return new File(sourcePath, TypeTools.toFilePath(typeName)).toPath(); + } + } + + + static class DirectFileObject implements FileObject { + private final File file; + + DirectFileObject(File file) { + this.file = file; + } + + @Override + public URI toUri() { + return file.toURI(); + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public InputStream openInputStream() throws IOException { + return new FileInputStream(file); + } + + @Override + public OutputStream openOutputStream() throws IOException { + Path parent = Paths.get(file.getParent()); + Files.createDirectories(parent); + return new FileOutputStream(file); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + return new InputStreamReader(openInputStream(), StandardCharsets.UTF_8); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return Files.readString(file.toPath(), StandardCharsets.UTF_8); + } + + @Override + public Writer openWriter() throws IOException { + return new OutputStreamWriter(openOutputStream(), StandardCharsets.UTF_8); + } + + @Override + public long getLastModified() { + return file.lastModified(); + } + + @Override + public boolean delete() { + return file.delete(); + } + + @Override + public String toString() { + return String.valueOf(file); + } + } + + + static class DirectJavaFileObject extends DirectFileObject implements JavaFileObject { + DirectJavaFileObject(File javaFile) { + super(javaFile); + } + + @Override + public JavaFileObject.Kind getKind() { + return JavaFileObject.Kind.SOURCE; + } + + @Override + public boolean isNameCompatible(String simpleName, + JavaFileObject.Kind kind) { + throw new IllegalStateException(); + } + + @Override + public NestingKind getNestingKind() { + throw new IllegalStateException(); + } + + @Override + public Modifier getAccessLevel() { + throw new IllegalStateException(); + } + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCodeGenDetail.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCodeGenDetail.java new file mode 100644 index 00000000000..e726172d510 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCodeGenDetail.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.ServiceInfoBasics; + +/** + * The specifics for a single {@link io.helidon.pico.ServiceProvider} that was code generated. + * + * @see ActivatorCreatorResponse#serviceTypeDetails() + */ +@Builder +public interface ActivatorCodeGenDetail extends GeneralCodeGenDetail { + + /** + * The additional meta-information describing what is offered by the generated service. + * + * @return additional meta-information describing the generated service info + */ + ServiceInfoBasics serviceInfo(); + + /** + * The additional meta-information describing what the generated service depends upon. + * + * @return additional meta-information describing what the generated service depends upon + */ + Optional dependencies(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorArgs.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorArgs.java new file mode 100644 index 00000000000..50261102357 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorArgs.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.ServiceInfoBasics; + +/** + * See {@link DefaultActivatorCreator}. + */ +@Builder +abstract class ActivatorCreatorArgs { + abstract String template(); + abstract TypeName serviceTypeName(); + abstract TypeName activatorTypeName(); + abstract Optional activatorGenericDecl(); + abstract Optional parentTypeName(); + abstract Set scopeTypeNames(); + abstract List description(); + abstract ServiceInfoBasics serviceInfo(); + abstract Optional dependencies(); + abstract Optional parentDependencies(); + abstract Collection injectionPointsSkippedInParent(); + abstract List serviceTypeInjectionOrder(); + abstract String generatedSticker(); + abstract Optional weightedPriority(); + abstract Optional runLevel(); + abstract Optional postConstructMethodName(); + abstract Optional preDestroyMethodName(); + abstract List extraCodeGen(); + abstract List extraClassComments(); + abstract boolean isConcrete(); + abstract boolean isProvider(); + abstract boolean isSupportsJsr330InStrictMode(); +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java new file mode 100644 index 00000000000..fa3d032704d --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.QualifierAndValue; + +/** + * Codegen request options applicable as part of the overall {@link ActivatorCreatorRequest}. + * + * @see ActivatorCreatorRequest + */ +@Builder +public interface ActivatorCreatorCodeGen { + + /** + * The default prefix for {@link #classPrefixName()}. + */ + String DEFAULT_CLASS_PREFIX_NAME = ""; + + /** + * The default prefix for {@link #classPrefixName()} for tests/testing. + */ + String DEFAULT_TEST_CLASS_PREFIX_NAME = ModuleInfoDescriptor.DEFAULT_TEST_SUFFIX; + + /** + * Optionally, for each service type also provide its parent (super class) service type mapping. + * + * @return the service type to parent (super class) service type mapping + */ + Map serviceTypeToParentServiceTypes(); + + /** + * The class hierarchy from Object down to and including this service type. + * + * @return the map of service type names to its class hierarchy + */ + Map> serviceTypeHierarchy(); + + /** + * Optionally, for each service, provide the generic declaration portion for the activator generic class name. + * + * @return the generic declaration portion for the activator generic class name + */ + Map serviceTypeToActivatorGenericDecl(); + + /** + * The map of service type names to access level. + * + * @return the map of service type names to each respective access level + */ + Map serviceTypeAccessLevels(); + + /** + * The map of service type names to whether they are abstract. If not found then assume concrete. + * + * @return the map of service type names to whether they are abstract + */ + Map serviceTypeIsAbstractTypes(); + + /** + * The {@link io.helidon.pico.Contract}'s associated with each service type. + * + * @return the map of service type names to {@link io.helidon.pico.Contract}'s implemented + */ + Map> serviceTypeContracts(); + + /** + * The {@link io.helidon.pico.ExternalContracts} associated with each service type. + * + * @return the map of service type names to {@link io.helidon.pico.ExternalContracts} implemented + */ + Map> serviceTypeExternalContracts(); + + /** + * The injection point dependencies for each service type. + * + * @return the map of service type names to injection point dependencies info + */ + Map serviceTypeInjectionPointDependencies(); + + /** + * The {@code PreDestroy} method name for each service type. + * + * @return the map of service type names to PreDestroy method names + * @see io.helidon.pico.PreDestroyMethod + */ + Map serviceTypePreDestroyMethodNames(); + + /** + * The {@code PostConstruct} method name for each service type. + * + * @return the map of service type names to PostConstruct method names + * @see io.helidon.pico.PostConstructMethod + */ + Map serviceTypePostConstructMethodNames(); + + /** + * The declared {@link io.helidon.common.Weighted} value for each service type. + * + * @return the map of service type names to declared weight + */ + Map serviceTypeWeights(); + + /** + * The declared {@link io.helidon.pico.RunLevel} value for each service type. + * + * @return the map of service type names to declared run level + */ + Map serviceTypeRunLevels(); + + /** + * The declared {@code Scope} value for each service type. + * + * @return the map of service type names to declared scope name + */ + Map> serviceTypeScopeNames(); + + /** + * The set of {@link jakarta.inject.Qualifier}'s for each service type. + * + * @return the map of service type names to qualifiers + */ + Map> serviceTypeQualifiers(); + + /** + * The set of type names that the service type acts as an "is provider" for (i.e., {@link jakarta.inject.Provider}). + * + * @return the map of service type names to "is provider" flag values + */ + Map> serviceTypeToProviderForTypes(); + + /** + * The service type's interception plan. + * + * @return the map of service type names to the interception plan + */ + Map serviceTypeInterceptionPlan(); + + /** + * The extra source code that needs to be appended to the implementation. + * + * @return the map of service type names to the extra source code that should be added + */ + Map> extraCodeGen(); + + /** + * The extra source code class comments that needs to be appended to the implementation. + * + * @return the map of service type names to the extra source code class comments that should be added + */ + Map> extraClassComments(); + + /** + * The set of external modules used and/or required. + * + * @return the set of external modules used and/or required + */ + Set modulesRequired(); + + /** + * Typically populated as "test" if test scoped, otherwise left blank. + * + * @return production or test scope + */ + @ConfiguredOption(DEFAULT_CLASS_PREFIX_NAME) + String classPrefixName(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorConfigOptions.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorConfigOptions.java new file mode 100644 index 00000000000..373f1d171ea --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorConfigOptions.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.PicoServicesConfig; + +/** + * These options are expected to have an affinity match to "permit" properties found within + * {@link io.helidon.pico.PicoServicesConfig}. These are used to fine tune the type of code generated. + * + * @see io.helidon.pico.tools.spi.ActivatorCreator + */ +@Builder +public interface ActivatorCreatorConfigOptions { + + /** + * This option (use -A at compile time) should be set to opt into {@link io.helidon.pico.Application} stub + * creation. + */ + String TAG_APPLICATION_PRE_CREATE = PicoServicesConfig.NAME + ".application.pre.create"; + + /** + * Should jsr-330 be followed in strict accordance. The default here is actually set to false for two reasons: + *
          + *
        1. It is usually not what people expect (i.e., losing @inject on overridden injectable setter methods), and + *
        2. The implementation will e slightly more performant (i.e., the "rules" governing jsr-330 requires that base classes + * are injected prior to derived classes. This coupled with point 1 requires special additional book-keeping to be + * managed by the activators that are generated). + *
        + * + * @return true if strict mode is in effect + */ + boolean isSupportsJsr330InStrictMode(); + + /** + * Should a {@link io.helidon.pico.Module} be created during activator creation. The default is true. + * + * @return true if the module should be created + */ + @ConfiguredOption("true") + boolean isModuleCreated(); + + /** + * Should a stub {@link io.helidon.pico.Application} be created during activator creation. The default is false. + * This feature can opt'ed in by using {@link #TAG_APPLICATION_PRE_CREATE}. Pre-req requires that this can + * only be enabled if {@link #isModuleCreated()} is also enabled. + * + * @return true if the application should be created + */ + boolean isApplicationPreCreated(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorProvider.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorProvider.java new file mode 100644 index 00000000000..82683bfefd3 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.pico.tools.spi.ActivatorCreator; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Provides access to the global singleton {@link io.helidon.pico.tools.spi.ActivatorCreator} in use. + */ +@Singleton +public class ActivatorCreatorProvider implements Provider { + private static final LazyValue INSTANCE = LazyValue.create(ActivatorCreatorProvider::load); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ActivatorCreatorProvider() { + } + + private static ActivatorCreator load() { + return HelidonServiceLoader.create(ServiceLoader.load(ActivatorCreator.class, ActivatorCreator.class.getClassLoader())) + .asList() + .stream() + .findFirst().orElseThrow(); + } + + // note that this is guaranteed to succeed since the default implementation is in this module + @Override + public ActivatorCreator get() { + return INSTANCE.get(); + } + + /** + * Returns the global instance that was service loaded. Note that this call is guaranteed to return a result since the + * default implementation is here in this module. + * + * @return the global service instance with the highest weight + */ + public static ActivatorCreator instance() { + return INSTANCE.get(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorRequest.java new file mode 100644 index 00000000000..a5f3c147eab --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.builder.Builder; + +/** + * Request used in conjunction with {@link io.helidon.pico.tools.spi.ActivatorCreator} to codegen the {@link io.helidon.pico.Activator} source artifacts. + */ +@Builder +public interface ActivatorCreatorRequest extends GeneralCreatorRequest { + + /** + * Mandatory, identifies what specifically should be generated. + * + * @return identifies what should be generated + */ + ActivatorCreatorCodeGen codeGen(); + + /** + * The configuration directives to apply during generation. + * + * @return configuration directives used for code generation + */ + ActivatorCreatorConfigOptions configOptions(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java new file mode 100644 index 00000000000..5bc1c06b027 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.TypeName; + +/** + * The result of calling {@link io.helidon.pico.tools.spi.ActivatorCreator} assuming no errors are thrown. + */ +@Builder +public interface ActivatorCreatorResponse extends GeneralCreatorResponse { + + /** + * The configuration options that were applied. + * + * @return config options + */ + ActivatorCreatorConfigOptions getConfigOptions(); + + /** + * return The interceptors that were generated. + * + * @return interceptors generated + */ + @Singular + Map serviceTypeInterceptorPlans(); + + /** + * The module-info detail, if a module was created. + * + * @return any module-info detail created + */ + Optional moduleDetail(); + + /** + * Set if the application stub was requested to have been created. + * + * @return the application name that was created + */ + Optional applicationTypeName(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorCodeGen.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorCodeGen.java new file mode 100644 index 00000000000..71d7ff5308b --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorCodeGen.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Codegen request options applicable for {@link io.helidon.pico.tools.spi.ApplicationCreator}. + * + * @see io.helidon.pico.tools.spi.ApplicationCreator + */ +@Builder +public interface ApplicationCreatorCodeGen { + + /** + * The package name to use for code generation. + * + * @return package name + */ + Optional packageName(); + + /** + * The class name to use for code generation. + * + * @return class name + */ + Optional className(); + + /** + * Typically populated as "test" if test scoped, otherwise left blank. + * + * @return production or test scope + */ + @ConfiguredOption("") + String classPrefixName(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java new file mode 100644 index 00000000000..c04bf1e05ae --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.TypeName; + +/** + * Configuration directives and options optionally provided to the {@link io.helidon.pico.tools.spi.ApplicationCreator}. + */ +@Builder +public interface ApplicationCreatorConfigOptions { + + /** + * Defines how the generator should allow the presence of {@link jakarta.inject.Provider}'s or + * {@link io.helidon.pico.InjectionPointProvider}'s. Since providers add a level of non-deterministic behavior + * to the system it is required for the application to explicitly permit whether this feature should be permitted. + */ + enum PermittedProviderType { + + /** + * No provider types are permitted. + */ + NONE, + + /** + * Each individual provider needs to be allow-listed. + */ + NAMED, + + /** + * Allows all/any provider type the system recognizes. + */ + ALL + + } + + /** + * Determines the application generator's tolerance around the usage of providers. + * + * @return provider generation permission type + */ + PermittedProviderType permittedProviderTypes(); + + /** + * Only applicable when {@link #permittedProviderTypes()} is set to + * {@link ApplicationCreatorConfigOptions.PermittedProviderType#NAMED}. This is the set of provider names that are explicitly + * permitted to be generated. + * + * @return the allow-listed named providers (which is the FQN of the underlying service type) + */ + @Singular + Set permittedProviderNames(); + + /** + * Only applicable when {@link #permittedProviderTypes()} is set to + * {@link ApplicationCreatorConfigOptions.PermittedProviderType#NAMED}. This is the set of qualifier types that are explicitly + * permitted to be generated. + * + * @return the allow-listed qualifier type names + */ + @Singular + Set permittedProviderQualifierTypeNames(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorProvider.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorProvider.java new file mode 100644 index 00000000000..7a7fa123931 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.pico.tools.spi.ApplicationCreator; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Provides access to the global singleton {@link ApplicationCreatorProvider} in use. + */ +@Singleton +public class ApplicationCreatorProvider implements Provider { + private static final LazyValue INSTANCE = LazyValue.create(ApplicationCreatorProvider::load); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ApplicationCreatorProvider() { + } + + private static ApplicationCreator load() { + return HelidonServiceLoader.create(ServiceLoader.load(ApplicationCreator.class, + ApplicationCreator.class.getClassLoader())) + .asList() + .stream() + .findFirst().orElseThrow(); + } + + // note that this is guaranteed to succeed since the default implementation is in this module + @Override + public ApplicationCreator get() { + return INSTANCE.get(); + } + + /** + * Returns the global instance that was service loaded. Note that this call is guaranteed to return a result since the + * default implementation is here in this module. + * + * @return the global service instance with the highest weight + */ + public static ApplicationCreator instance() { + return INSTANCE.get(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorRequest.java new file mode 100644 index 00000000000..75fa9860a2d --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; + +/** + * Defines the request that will be passed to the {@link io.helidon.pico.tools.spi.ApplicationCreator} in order to produce the codegen artifacts. + */ +@Builder +public interface ApplicationCreatorRequest extends GeneralCreatorRequest { + + /** + * Mandatory, qualifies what specifically should be generated. + * + * @return data specific requests for what is generated + */ + ApplicationCreatorCodeGen codeGen(); + + /** + * Optionally, the config options and directives that applies to the request. + * + * @return configuration options and directives + */ + ApplicationCreatorConfigOptions configOptions(); + + /** + * Optionally, the messager to use. + * + * @return messager + */ + Optional messager(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorResponse.java new file mode 100644 index 00000000000..039c177eea6 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.builder.Builder; + +/** + * Response from {@link io.helidon.pico.tools.spi.ApplicationCreator}. + * + * @see io.helidon.pico.tools.spi.ApplicationCreator + */ +@Builder +public interface ApplicationCreatorResponse extends GeneralCreatorResponse { + + /** + * The basic description for the {@link io.helidon.pico.Application} generated. + * + * @return describes the application generated (package and class) + */ + ApplicationCreatorCodeGen applicationCodeGen(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java new file mode 100644 index 00000000000..e36fb6761a7 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java @@ -0,0 +1,554 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +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.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.FilerException; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.PicoServicesConfig; + +import static io.helidon.pico.tools.ModuleUtils.PICO_MODULE_INFO_JAVA_NAME; +import static io.helidon.pico.tools.ModuleUtils.normalizedBaseModuleName; +import static io.helidon.pico.tools.ModuleUtils.saveAppPackageName; +import static io.helidon.pico.tools.ModuleUtils.toPath; + +/** + * This class is used to generate the source and resources originating from either annotation processing or maven-plugin + * invocation. It also provides a circuit breaker in case the filer should be disabled from actually writing out source + * and resources, and instead will use the filer's messager to report what it would have performed (applicable for apt cases). + */ +public class CodeGenFiler { + private static final boolean FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR = true; + private static final boolean FILER_WRITE_ONCE_PER_TYPE = true; + private static final Set FILER_TYPES_FILED = new LinkedHashSet<>(); + private static boolean filerWriteIsDisabled; + + private final AbstractFilerMessager filer; + private final Boolean enabled; + private final Map deferredMoves = new LinkedHashMap<>(); + private Path targetOutputPath; + private String scratchPathName; + + /** + * Constructor. + * + * @param filer the filer to use for creating resources + */ + CodeGenFiler(AbstractFilerMessager filer) { + this(filer, null); + } + + /** + * Constructor. + * + * @param filer the filer to use for creating resources + * @param enabled true if forcing enablement, false if forcing disablement, null for using defaults + */ + CodeGenFiler(AbstractFilerMessager filer, + Boolean enabled) { + this.filer = Objects.requireNonNull(filer); + this.enabled = enabled; + } + + /** + * Creates a new code gen filer. + * + * @param filer the physical filer + * @return a newly created code gen filer + */ + public static CodeGenFiler create(AbstractFilerMessager filer) { + return new CodeGenFiler(filer); + } + + /** + * Provides the ability to disable actual file writing (convenient for unit testing). The default is true for + * enabled. + * + * @param enabled if disabled, pass false + * @return the previous value of this setting + */ + static boolean filerEnabled(boolean enabled) { + boolean prev = filerWriteIsDisabled; + filerWriteIsDisabled = enabled; + return prev; + } + + boolean isFilerWriteEnabled() { + return (enabled != null) ? enabled : !filerWriteIsDisabled; + } + + AbstractFilerMessager filer() { + return filer; + } + + Messager messager() { + return filer; + } + + /** + * This map represents any move operations that were not capable at the time of code generation, that must be deferred + * until after the annotation processor has completed its round. + * + * @return map of deferred moves from source to target path locations + */ + public Map deferredMoves() { + return Map.copyOf(deferredMoves); + } + + /** + * Generate the meta-inf services given the provided map. + * + * @param paths paths to where code should be written + * @param metaInfServices the meta-inf services mapping + */ + public void codegenMetaInfServices(CodeGenPaths paths, + Map> metaInfServices) { + if (metaInfServices == null || metaInfServices.isEmpty()) { + return; + } + + Filer filer = filer(); + Messager messager = messager(); + Map> mergedMap = new LinkedHashMap<>(); + // load up any existing values, since this compilation may be partial and be run again... + for (Map.Entry> e : metaInfServices.entrySet()) { + String contract = e.getKey(); + Set mergedSet = new LinkedHashSet<>(e.getValue()); + mergedMap.put(contract, mergedSet); + String outPath = new File(paths.metaInfServicesPath() + .orElse(CodeGenPaths.DEFAULT_META_INF_SERVICES_PATH), contract).getPath(); + try { + messager.debug("Reading " + outPath); + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", outPath); + try (InputStream is = f.openInputStream(); + BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + mergedSet.add(line); + } + } + targetOutputPath(f); + } catch (FilerException | NoSuchFileException x) { + // don't show the exception in this case + messager.debug(getClass().getSimpleName() + ":" + x.getMessage()); + } catch (Exception x) { + ToolsException te = + new ToolsException("Failed to find/load existing META-INF/services file: " + x.getMessage(), x); + messager.warn(te.getMessage(), te); + } + } + + for (Map.Entry> e : mergedMap.entrySet()) { + String contract = e.getKey(); + String outPath = new File(paths.metaInfServicesPath().orElseThrow(), contract).getPath(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8))) { + for (String value : e.getValue()) { + pw.println(value); + } + } + + codegenResourceFilerOut(outPath, baos.toString(StandardCharsets.UTF_8), Optional.empty()); + } + } + + private void targetOutputPath(FileObject f) { + Path path = Path.of(f.toUri()); + Path parent = path.getParent(); + Path gparent = (parent == null) ? null : parent.getParent(); + this.targetOutputPath = gparent; + Path scratchName = (parent == null) ? null : parent.getFileName(); + this.scratchPathName = (scratchName == null) ? null : scratchName.toString(); + } + + private Path toScratchPath(boolean wantClassesOrTestClassesRelative) { + Objects.requireNonNull(targetOutputPath); + Objects.requireNonNull(scratchPathName); + Path base = targetOutputPath.resolve(PicoServicesConfig.NAME); + return (wantClassesOrTestClassesRelative) ? base.resolve(scratchPathName) : base; + } + + /** + * Code generates a resource, providing the ability to update if the resource already exists. + * + * @param outPath the path to output the resource to + * @param body the resource body + * @param optFnUpdater the optional updater of the body + * @return file path coordinates corresponding to the resource in question, or empty if not generated + */ + public Optional codegenResourceFilerOut(String outPath, + String body, + Optional> optFnUpdater) { + Messager messager = messager(); + if (!isFilerWriteEnabled()) { + messager.log("(disabled) Writing " + outPath + " with:\n" + body); + return Optional.empty(); + } + messager.debug("Writing " + outPath); + + Filer filer = filer(); + boolean contentsAlreadyVerified = false; + Function fnUpdater = optFnUpdater.orElse(null); + AtomicReference fileRef = new AtomicReference<>(); + try { + if (fnUpdater != null) { + // attempt to update it... + try { + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", outPath); + try (InputStream is = f.openInputStream()) { + String newBody = fnUpdater.apply(is); + if (newBody != null) { + body = newBody; + } + } + } catch (NoSuchFileException e) { + // no-op + } catch (Exception e) { + // messager.debug(getClass().getSimpleName() + ":" + e.getMessage()); + contentsAlreadyVerified = tryToEnsureSameContents(e, body, messager, fileRef); + } + } + + // write it + FileObject f = filer.createResource(StandardLocation.CLASS_OUTPUT, "", outPath); + try (Writer os = f.openWriter()) { + os.write(body); + } + targetOutputPath(f); + + if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR && outPath.equals(PICO_MODULE_INFO_JAVA_NAME) + && targetOutputPath != null) { + // hack: physically relocate it elsewhere + Path originalPath = Path.of(f.toUri()); + Path newPath = toScratchPath(true).resolve(PICO_MODULE_INFO_JAVA_NAME); + if (originalPath.toFile().exists()) { + Path parent = newPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.move(originalPath, newPath, StandardCopyOption.REPLACE_EXISTING); + return Optional.of(newPath); + } else { + deferredMoves.put(originalPath, newPath); + } + } + + return toPath(f.toUri()); + } catch (FilerException x) { + // messager.debug(getClass().getSimpleName() + ":" + x.getMessage(), null); + if (!contentsAlreadyVerified) { + tryToEnsureSameContents(x, body, messager, fileRef); + } + } catch (Exception x) { + ToolsException te = new ToolsException("Failed to write resource file: " + x.getMessage(), x); + messager.error(te.getMessage(), te); + } + + return Optional.of(fileRef.get().toPath()); + } + + /** + * Throws an error if the contents being written cannot be written, and the desired content is different from what + * is on disk. + * + * @param e the exception thrown by the filer + * @param expected the expected body of the resource + * @param messager the messager to handle errors and logging + * @param fileRef the reference that will be set to the coordinates of the resource + * @return true if the implementation was able to verify the contents match + */ + boolean tryToEnsureSameContents(Exception e, + String expected, + Messager messager, + AtomicReference fileRef) { + if (!(e instanceof FilerException)) { + return false; + } + + String message = e.getMessage(); + if (message == null) { + return false; + } + + int pos = message.lastIndexOf(' '); + if (pos <= 0) { + return false; + } + + String maybePath = message.substring(pos + 1); + File file = new File(maybePath); + if (!file.exists()) { + return false; + } + if (fileRef != null) { + fileRef.set(file); + } + + try { + String actual = Files.readString(file.toPath(), StandardCharsets.UTF_8); + if (!actual.equals(expected)) { + String error = "expected contents to match for file: " + file + + "\nexpected:\n" + expected + + "\nactual:\n" + actual; + ToolsException te = new ToolsException(error); + messager.error(error, te); + } + } catch (Exception x) { + messager.debug(getClass().getSimpleName() + ": unable to verify contents match: " + file + "; " + x.getMessage(), + null); + return false; + } + + return true; + } + + /** + * Code generates the {@link Module} source. + * + * @param moduleDetail the module details + */ + void codegenModuleFilerOut(ModuleDetail moduleDetail) { + if (moduleDetail.moduleBody().isEmpty()) { + return; + } + + TypeName typeName = moduleDetail.moduleTypeName(); + String body = moduleDetail.moduleBody().orElseThrow(); + codegenJavaFilerOut(typeName, body); + } + + /** + * Code generates the {@link io.helidon.pico.Application} source. + * + * @param applicationTypeName the application type + * @param body the application body of source + */ + void codegenApplicationFilerOut(TypeName applicationTypeName, + String body) { + codegenJavaFilerOut(applicationTypeName, body); + } + + /** + * Code generates the {@link io.helidon.pico.Activator} source. + * + * @param activatorDetail the activator details + */ + void codegenActivatorFilerOut(ActivatorCodeGenDetail activatorDetail) { + if (activatorDetail.body().isEmpty()) { + return; + } + + TypeName typeName = activatorDetail.serviceTypeName(); + String body = activatorDetail.body().orElseThrow(); + codegenJavaFilerOut(typeName, body); + } + + /** + * Code generate a java source file. + * + * @param typeName the source type name + * @param body the source body + * @return the new file path coordinates or empty if nothing was written + */ + public Optional codegenJavaFilerOut(TypeName typeName, + String body) { + Messager messager = messager(); + if (!isFilerWriteEnabled()) { + messager.log("(disabled) Writing " + typeName + " with:\n" + body); + return Optional.empty(); + } + + if (FILER_WRITE_ONCE_PER_TYPE && !FILER_TYPES_FILED.add(typeName)) { + messager.log(typeName + ": already processed"); + return Optional.empty(); + } + + messager.debug("Writing " + typeName); + + Filer filer = filer(); + try { + JavaFileObject javaSrc = filer.createSourceFile(typeName.name()); + try (Writer os = javaSrc.openWriter()) { + os.write(body); + } + return toPath(javaSrc.toUri()); + } catch (FilerException x) { + messager.log("Failed to write java file: " + x); + } catch (Exception x) { + messager.warn("Failed to write java file: " + x, x); + } + + return Optional.empty(); + } + + /** + * Code generate the module-info.java.pico file. + * + * @param newDeltaDescriptor the descriptor + * @param overwriteTargetIfExists should the file be overwritten if it already exists + * @return the module-info coordinates, or empty if nothing was written + */ + Optional codegenModuleInfoFilerOut(ModuleInfoDescriptor newDeltaDescriptor, + boolean overwriteTargetIfExists) { + Objects.requireNonNull(newDeltaDescriptor); + + Messager messager = messager(); + String typeName = PICO_MODULE_INFO_JAVA_NAME; + if (!isFilerWriteEnabled()) { + messager.log("(disabled) Writing " + typeName + " with:\n" + newDeltaDescriptor); + return Optional.empty(); + } + messager.debug("Writing " + typeName); + + Function moduleInfoUpdater = inputStream -> { + ModuleInfoDescriptor existingDescriptor = ModuleInfoDescriptor.create(inputStream); + ModuleInfoDescriptor newDescriptor = existingDescriptor.mergeCreate(newDeltaDescriptor); + return newDescriptor.contents(); + }; + + Optional filePath + = codegenResourceFilerOut(typeName, newDeltaDescriptor.contents(), Optional.of(moduleInfoUpdater)); + if (filePath.isPresent()) { + messager.debug("Wrote module-info: " + filePath.get()); + } else if (overwriteTargetIfExists) { + messager.warn("Expected to have written module-info, but failed to write it"); + } + + if (!newDeltaDescriptor.isUnnamed()) { + saveAppPackageName(toScratchPath(false), + normalizedBaseModuleName(newDeltaDescriptor.name())); + } + + return filePath; + } + + /** + * Reads in the module-info if it exists, or returns null if it doesn't exist. + * + * @param name the name to the module-info file + * @return the module-info descriptor, or empty if it doesn't exist + */ + Optional readModuleInfo(String name) { + Objects.requireNonNull(name); + + try { + CharSequence body = readResourceAsString(name); + return Optional.ofNullable((body == null) ? null : ModuleInfoDescriptor.create(body.toString())); + } catch (Exception e) { + throw new ToolsException("failed to read module-info: " + name, e); + } + } + + /** + * Reads in a resource from the {@link javax.tools.StandardLocation#CLASS_OUTPUT} location. + * + * @param name the name of the resource + * @return the body of the resource as a string, or null if it doesn't exist + */ + CharSequence readResourceAsString(String name) { + try { + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", name); + targetOutputPath(f); + return f.getCharContent(true); + } catch (IOException e) { + if (FORCE_MODULE_INFO_PICO_INTO_SCRATCH_DIR && name.equals(PICO_MODULE_INFO_JAVA_NAME) + && targetOutputPath != null) { + // hack: physically read it from its relocated location + File newPath = new File(targetOutputPath.toFile().getAbsolutePath(), name); + if (newPath.exists()) { + try { + return Files.readString(newPath.toPath()); + } catch (IOException e2) { + throw new ToolsException(e2.getMessage(), e2); + } + } + } + + messager().debug("unable to load resource: " + name); + return null; + } + } + + /** + * Attempts to translate the resource name to a file coordinate, or null if translation is not possible. + * + * @param name the name of the resource + * @return the file path coordinates if it can be ascertained, or empty if not possible to ascertain this information + */ + Optional toResourceLocation(String name) { + try { + FileObject f = filer.getResource(StandardLocation.CLASS_OUTPUT, "", name); + targetOutputPath(f); + return toPath(f.toUri()); + } catch (IOException e) { + messager().debug("unable to load resource: " + name); + } + return Optional.empty(); + } + + /** + * Attempts to translate the type name to a file coordinate, or empty if translation is not possible. + * + * @param name the name of the type + * @return the file coordinates if it can be ascertained, or empty if not possible to ascertain this information + * + * @see ModuleUtils#toSourcePath for annotation processing use cases + */ + Optional toSourceLocation(String name) { + if (filer instanceof AbstractFilerMessager.DirectFilerMessager) { + TypeName typeName = DefaultTypeName.createFromTypeName(name); + Optional path = Optional.ofNullable(((AbstractFilerMessager.DirectFilerMessager) filer) + .toSourcePath(StandardLocation.SOURCE_PATH, typeName)); + if (path.isPresent()) { + return path; + } + } + + messager().warn(CodeGenFiler.class.getSimpleName() + ": unable to determine source location for: " + name); + return Optional.empty(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenPaths.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenPaths.java new file mode 100644 index 00000000000..c411ca8dab9 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenPaths.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Applies only to the output paths that various {@code creators} will use (e.g., {@link io.helidon.pico.tools.spi.ActivatorCreator}). + */ +@Builder +public interface CodeGenPaths { + + /** + * The default path for {@link #metaInfServicesPath()}. + * + */ + String DEFAULT_META_INF_SERVICES_PATH = "META-INF/services"; + + /** + * Identifies where the meta-inf services should be written. + * + * @return where should meta-inf services be written + */ + @ConfiguredOption(DEFAULT_META_INF_SERVICES_PATH) + Optional metaInfServicesPath(); + + /** + * Identifies where is the source directory resides. + * + * @return the source directory + */ + Optional sourcePath(); + + /** + * Identifies where the generated sources should be written. + * + * @return where should the generated sources be written + */ + Optional generatedSourcesPath(); + + /** + * Identifies where the classes directory resides. + * + * @return the classes directory + */ + Optional outputPath(); + + /** + * Identifies where the module-info can be found. + * + * @return the module-info location + */ + Optional moduleInfoPath(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java new file mode 100644 index 00000000000..905e57e7d6c --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.pico.ElementInfo; +import io.helidon.pico.InjectionPointInfo; + +/** + * Code generation utilities. + */ +class CodeGenUtils { + + private CodeGenUtils() { + } + + static String elementNameKindRef(String elemName, + ElementInfo.ElementKind elemKind) { + if (elemKind == InjectionPointInfo.ElementKind.CONSTRUCTOR + && elemName.equals(InjectionPointInfo.CONSTRUCTOR)) { + elemName = "CONSTRUCTOR"; + } else { + elemName = "\"" + elemName + "\""; + } + return elemName; + } + + static String elementNameRef(String elemName) { + if (elemName.equals(ElementInfo.CONSTRUCTOR)) { + return ElementInfo.class.getName() + "." + "CONSTRUCTOR"; + } + return "\"" + elemName + "\""; + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CommonUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/CommonUtils.java index bfe116e8eef..7f82321326a 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CommonUtils.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CommonUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -24,8 +24,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -76,21 +78,36 @@ static String loadStringFromFile(String fileName) { * @param coll the collection * @return the concatenated, delimited string value */ - static String toPathString(Iterable coll) { - return String.join(System.getProperty("path.separator"), coll); + static String toPathString(Collection coll) { + return toString(coll, null, System.getProperty("path.separator")); } /** - * Determines the root throwable stack trace element from a chain of throwable causes. + * Converts a collection to a comma delimited string. * - * @param t the throwable - * @return the root throwable error stack trace element + * @param coll the collection + * @return the concatenated, delimited string value */ - static StackTraceElement rootStackTraceElementOf(Throwable t) { - while (Objects.nonNull(t.getCause()) && t.getCause() != t) { - t = t.getCause(); - } - return t.getStackTrace()[0]; + static String toString(Collection coll) { + return toString(coll, null, null); + } + + /** + * Provides specialization in concatenation, allowing for a function to be called for each element as well as to + * use special separators. + * + * @param coll the collection + * @param fnc the optional function to translate the collection item to a string + * @param separator the optional separator + * @param the type held by the collection + * @return the concatenated, delimited string value + */ + static String toString(Collection coll, + Function fnc, + String separator) { + Function fn = (fnc == null) ? String::valueOf : fnc; + separator = (separator == null) ? ", " : separator; + return coll.stream().map(fn::apply).collect(Collectors.joining(separator)); } /** @@ -116,6 +133,23 @@ static List toList(String str, return Arrays.stream(split).map(String::trim).collect(Collectors.toList()); } + /** + * Converts the collection of type T to a set of strings, handling the null case. + * + * @param coll the collection or null + * @param fn the mapper function + * @param the type of the items in the collection + * @return the set of mapped strings from the collection + */ + static Set toSet(Collection coll, + Function fn) { + if (coll == null) { + return Set.of(); + } + + return coll.stream().map(fn).collect(Collectors.toSet()); + } + /** * Trims each line of a multi-line string. * @@ -140,4 +174,41 @@ static String trimLines(String multiLineStr) { return builder.toString().trim(); } + /** + * Returns the first element of a collection. + * + * @param coll the collection + * @param allowEmptyCollection if true, and the collection is empty, will return null instead of throwing + * @param the type of the collection + * @return the first element, or null if empty collections are allowed + * @throws io.helidon.pico.tools.ToolsException if not allowEmptyCollection and the collection is empty + */ + static T first(Collection coll, + boolean allowEmptyCollection) { + if (coll.isEmpty()) { + if (allowEmptyCollection) { + return null; + } else { + throw new ToolsException("expected a non-empty collection"); + } + } + + return coll.iterator().next(); + } + + static boolean hasValue( + String str) { + return (str != null && !str.isBlank()); + } + + /** + * Replaces the provided string's usage of '.' with '$'. + * + * @param className the classname + * @return the converted string + */ + static String toFlatName(String className) { + return className.replace('.', '$'); + } + } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CompilerOptions.java b/pico/tools/src/main/java/io/helidon/pico/tools/CompilerOptions.java new file mode 100644 index 00000000000..43d17ae9a95 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CompilerOptions.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.nio.file.Path; +import java.util.List; + +import io.helidon.builder.Builder; + +/** + * Provides configuration to the javac compiler. + */ +@Builder +public interface CompilerOptions { + + /** + * The classpath to pass to the compiler. + * + * @return classpath + */ + List classpath(); + + /** + * The modulepath to pass to the compiler. + * + * @return the module path + */ + List modulepath(); + + /** + * The source path to pass to the compiler. + * + * @return the source path + */ + List sourcepath(); + + /** + * The command line arguments to pass to the compiler. + * + * @return arguments + */ + List commandLineArguments(); + + /** + * The compiler source version. + * + * @return source version + */ + String source(); + + /** + * The compiler target version. + * + * @return target version + */ + String target(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java new file mode 100644 index 00000000000..940ce9efd45 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.ServiceInfoBasics; + +/** + * The request will be generated internally and then passed to the appropriate + * {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator} to handle the request. + */ +@Builder +public interface CustomAnnotationTemplateRequest { + + /** + * The type of the annotation being processed. + * + * @return the type of the annotation being processed + */ + TypeName annoTypeName(); + + /** + * The target element being processed. This element is the one with the {@link #annoTypeName()} assigned to it. + * + * @return the target element being processed + */ + TypedElementName targetElement(); + + /** + * The access modifier of the element. + * + * @return the access modifier of the element + */ + InjectionPointInfo.Access targetElementAccess(); + + /** + * Only applicable for {@link javax.lang.model.element.ElementKind#METHOD} or + * {@link javax.lang.model.element.ElementKind#CONSTRUCTOR}. + * + * @return the list of typed arguments for this method or constructor + */ + List targetElementArgs(); + + /** + * Returns true if the element is declared to be static. + * + * @return returns true if the element is declared to be private + */ + boolean isElementStatic(); + + /** + * Projects the {@link #enclosingTypeInfo()} as a {@link ServiceInfoBasics} type. + * + * @return the basic service info of the element being processed + */ + ServiceInfoBasics serviceInfo(); + + /** + * The enclosing class type info of the target element being processed. + * + * @return the enclosing class type info of the target element being processed + */ + TypeInfo enclosingTypeInfo(); + + /** + * Returns true if the code should be literally generated with the provided {@code filer}. + * + * @return true if the code should be literally generated with the filer + */ + @ConfiguredOption("true") + boolean isFilerEnabled(); + + /** + * Generic template creator. + * + * @return the generic template creator + */ + GenericTemplateCreator genericTemplateCreator(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java new file mode 100644 index 00000000000..80a24bbe57f --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Map; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; + +/** + * The response from {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator#create(CustomAnnotationTemplateRequest)}. + */ +@Builder +public interface CustomAnnotationTemplateResponse { + + /** + * The request that was processed. + * + * @return the request that was processed + */ + CustomAnnotationTemplateRequest request(); + + /** + * Any source that should be code generated. + * + * @return map of generated type name to body of the source to be generated + */ + @Singular + Map generatedSourceCode(); + + /** + * Any generated resources should be generated. + * + * @return map of generated type name (which is really just a directory path under resources) to the resource to be generated + */ + @Singular + Map generatedResources(); + + /** + * Aggregates the responses given to one response. + * + * @param request the request being processed + * @param responses the responses to aggregate into one response instance + * @return the aggregated response + */ + static DefaultCustomAnnotationTemplateResponse.Builder aggregate(CustomAnnotationTemplateRequest request, + CustomAnnotationTemplateResponse... responses) { + DefaultCustomAnnotationTemplateResponse.Builder response = DefaultCustomAnnotationTemplateResponse.builder() + .request(request); + for (CustomAnnotationTemplateResponse res : responses) { + if (res == null) { + continue; + } + + response.addGeneratedSourceCode(res.generatedSourceCode()); + response.addGeneratedResources(res.generatedResources()); + } + return response; + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java new file mode 100644 index 00000000000..2251829df27 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultActivatorCreator.java @@ -0,0 +1,1239 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +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.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultInjectionPointInfo; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.DependencyInfo; +import io.helidon.pico.ElementInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.Module; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.RunLevel; +import io.helidon.pico.ServiceInfo; +import io.helidon.pico.ServiceInfoBasics; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.services.Dependencies; +import io.helidon.pico.tools.spi.ActivatorCreator; + +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodInfoList; +import io.github.classgraph.ScanResult; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.common.types.DefaultTypeName.createFromTypeName; +import static io.helidon.pico.tools.CommonUtils.first; +import static io.helidon.pico.tools.CommonUtils.hasValue; +import static io.helidon.pico.tools.CommonUtils.toFlatName; +import static io.helidon.pico.tools.CommonUtils.toSet; +import static io.helidon.pico.tools.TypeTools.componentTypeNameOf; +import static io.helidon.pico.tools.TypeTools.createTypeNameFromClassInfo; +import static io.helidon.pico.tools.TypeTools.isPackagePrivate; + +/** + * Responsible for building all pico-di related collateral for a module, including: + *
          + *
        1. The {@link io.helidon.pico.ServiceProvider} for each service type implementation passed in. + *
        2. The {@link io.helidon.pico.Activator} and {@link io.helidon.pico.DeActivator} for each service type implementation passed in. + *
        3. The {@link Module} for the aggregate service provider bindings for the same set of service type names. + *
        4. The module-info as appropriate for the above set of services (and contracts). + *
        5. The /META-INF/services entries as appropriate. + *
        + * + * This API can also be used to only produce meta-information describing the model without the codegen option - see + * {@link ActivatorCreatorRequest#codeGenPaths()} for details. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultActivatorCreator extends AbstractCreator implements ActivatorCreator, Weighted { + /** + * The suffix name for the service type activator class. + */ + static final String ACTIVATOR_NAME_SUFFIX = "Activator"; + static final String INNER_ACTIVATOR_CLASS_NAME = "$$" + NAME_PREFIX + ACTIVATOR_NAME_SUFFIX; + private static final String SERVICE_PROVIDER_ACTIVATOR_HBS = "service-provider-activator.hbs"; + private static final String SERVICE_PROVIDER_APPLICATION_STUB_HBS = "service-provider-application-stub.hbs"; + private static final String SERVICE_PROVIDER_MODULE_HBS = "service-provider-module.hbs"; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultActivatorCreator() { + super(TemplateHelper.DEFAULT_TEMPLATE_NAME); + } + + @Override + public ActivatorCreatorResponse createModuleActivators(ActivatorCreatorRequest req) throws ToolsException { + String templateName = (hasValue(req.templateName())) ? req.templateName() : templateName(); + + DefaultActivatorCreatorResponse.Builder builder = DefaultActivatorCreatorResponse.builder() + .configOptions(req.configOptions()) + .templateName(templateName); + + if (req.serviceTypeNames().isEmpty()) { + return handleError(req, new ToolsException("ServiceTypeNames is required to be passed"), builder); + } + + try { + LazyValue scan = LazyValue.create(ReflectionHandler.INSTANCE::scan); + return codegen(req, builder, scan); + } catch (ToolsException te) { + return handleError(req, te, builder); + } catch (UnsupportedOperationException e) { + throw e; + } catch (Throwable t) { + return handleError(req, new ToolsException("failed in create", t), builder); + } + } + + ActivatorCreatorResponse codegen(ActivatorCreatorRequest req, + DefaultActivatorCreatorResponse.Builder builder, + LazyValue scan) { + boolean isApplicationPreCreated = req.configOptions().isApplicationPreCreated(); + boolean isModuleCreated = req.configOptions().isModuleCreated(); + CodeGenPaths codeGenPaths = req.codeGenPaths(); + Map serviceTypeToIsAbstractType = req.codeGen().serviceTypeIsAbstractTypes(); + List activatorTypeNames = new ArrayList<>(); + List activatorTypeNamesPutInModule = new ArrayList<>(); + Map activatorDetails = new LinkedHashMap<>(); + for (TypeName serviceTypeName : req.serviceTypeNames()) { + ActivatorCodeGenDetail activatorDetail = createActivatorCodeGenDetail(req, serviceTypeName, scan); + Object prev = activatorDetails.put(serviceTypeName, activatorDetail); + assert (prev == null); + codegenActivatorFilerOut(req, activatorDetail); + TypeName activatorTypeName = toActivatorImplTypeName(serviceTypeName); + activatorTypeNames.add(activatorTypeName); + Boolean isAbstract = serviceTypeToIsAbstractType.get(serviceTypeName); + isAbstract = (isAbstract != null) && isAbstract; + if (!isAbstract) { + activatorTypeNamesPutInModule.add(activatorTypeName); + } + + InterceptionPlan interceptionPlan = req.codeGen().serviceTypeInterceptionPlan().get(serviceTypeName); + if (interceptionPlan != null) { + codegenInterceptorFilerOut(req, builder, interceptionPlan); + } + } + builder.serviceTypeNames(activatorTypeNames) + .serviceTypeDetails(activatorDetails); + + ModuleDetail moduleDetail; + TypeName applicationTypeName; + Map> metaInfServices; + TypeName moduleTypeName = toModuleTypeName(req, activatorTypeNames); + if (moduleTypeName != null) { + String className = DefaultApplicationCreator + .toApplicationClassName(req.codeGen().classPrefixName()); + applicationTypeName = create(moduleTypeName.packageName(), className); + builder.applicationTypeName(applicationTypeName); + String applicationStub = toApplicationStubBody(req, applicationTypeName, req.moduleName().orElse(null)); + if (isApplicationPreCreated && isModuleCreated) { + codegenApplicationFilerOut(req, applicationTypeName, applicationStub); + } + + moduleDetail = toModuleDetail(req, + activatorTypeNamesPutInModule, + moduleTypeName, + applicationTypeName, + isApplicationPreCreated, + isModuleCreated); + builder.moduleDetail(moduleDetail); + if (moduleDetail != null && isModuleCreated) { + codegenModuleFilerOut(req, moduleDetail); + Path outPath = codegenModuleInfoFilerOut(req, moduleDetail.descriptor().orElseThrow()); + logger().log(System.Logger.Level.DEBUG, "codegen module-info written to: " + outPath); + } + + metaInfServices = toMetaInfServices(moduleDetail, + applicationTypeName, + isApplicationPreCreated, + isModuleCreated); + builder.metaInfServices(metaInfServices); + if (!metaInfServices.isEmpty() && req.configOptions().isModuleCreated()) { + codegenMetaInfServices(req, codeGenPaths, metaInfServices); + } + } + + return builder.build(); + } + + private ModuleDetail toModuleDetail(ActivatorCreatorRequest req, + List activatorTypeNamesPutInModule, + TypeName moduleTypeName, + TypeName applicationTypeName, + boolean isApplicationCreated, + boolean isModuleCreated) { + String className = moduleTypeName.className(); + String packageName = moduleTypeName.packageName(); + String moduleName = req.moduleName().orElse(null); + + ActivatorCreatorCodeGen codeGen = req.codeGen(); + String typePrefix = codeGen.classPrefixName(); + Collection modulesRequired = codeGen.modulesRequired(); + Map> serviceTypeContracts = codeGen.serviceTypeContracts(); + Map> externalContracts = codeGen.serviceTypeExternalContracts(); + + Optional moduleInfoPath = req.codeGenPaths().moduleInfoPath(); + ModuleInfoCreatorRequest moduleCreatorRequest = DefaultModuleInfoCreatorRequest.builder() + .name(moduleName) + .moduleTypeName(moduleTypeName) + .applicationTypeName(applicationTypeName) + .modulesRequired(modulesRequired) + .contracts(serviceTypeContracts) + .externalContracts(externalContracts) + .moduleInfoPath(moduleInfoPath) + .classPrefixName(typePrefix) + .applicationCreated(isApplicationCreated) + .moduleCreated(isModuleCreated) + .build(); + ModuleInfoDescriptor moduleInfo = createModuleInfo(moduleCreatorRequest); + moduleName = moduleInfo.name(); + String moduleBody = toModuleBody(req, packageName, className, moduleName, activatorTypeNamesPutInModule); + return DefaultModuleDetail.builder() + .moduleName(moduleName) + .moduleTypeName(moduleTypeName) + .serviceProviderActivatorTypeNames(activatorTypeNamesPutInModule) + .moduleBody(moduleBody) + .moduleInfoBody(moduleInfo.contents()) + .descriptor(moduleInfo) + .build(); + } + + /** + * Applies to module-info. + */ + static TypeName toModuleTypeName(ActivatorCreatorRequest req, + List activatorTypeNames) { + String packageName; + if (hasValue(req.packageName().orElse(null))) { + packageName = req.packageName().orElseThrow(); + } else { + if (activatorTypeNames == null || activatorTypeNames.isEmpty()) { + return null; + } + packageName = activatorTypeNames.get(0).packageName() + "." + NAME_PREFIX; + } + + String className = toModuleClassName(req.codeGen().classPrefixName()); + return create(packageName, className); + } + + static String toModuleClassName(String modulePrefix) { + modulePrefix = (modulePrefix == null) ? "" : modulePrefix; + return NAME_PREFIX + modulePrefix + MODULE_NAME_SUFFIX; + } + + static Map> toMetaInfServices(ModuleDetail moduleDetail, + TypeName applicationTypeName, + boolean isApplicationCreated, + boolean isModuleCreated) { + Map> metaInfServices = new LinkedHashMap<>(); + if (isApplicationCreated && applicationTypeName != null) { + metaInfServices.put(TypeNames.PICO_APPLICATION, + List.of(applicationTypeName.name())); + } + if (isModuleCreated && moduleDetail != null) { + metaInfServices.put(TypeNames.PICO_MODULE, + List.of(moduleDetail.moduleTypeName().name())); + } + return metaInfServices; + } + + void codegenMetaInfServices(GeneralCreatorRequest req, + CodeGenPaths paths, + Map> metaInfServices) { + boolean prev = true; + if (req.analysisOnly()) { + prev = CodeGenFiler.filerEnabled(false); + } + + try { + req.filer().codegenMetaInfServices(paths, metaInfServices); + } finally { + if (req.analysisOnly()) { + CodeGenFiler.filerEnabled(prev); + } + } + } + + void codegenActivatorFilerOut(GeneralCreatorRequest req, + ActivatorCodeGenDetail activatorDetail) { + boolean prev = true; + if (req.analysisOnly()) { + prev = CodeGenFiler.filerEnabled(false); + } + + try { + req.filer().codegenActivatorFilerOut(activatorDetail); + } finally { + if (req.analysisOnly()) { + CodeGenFiler.filerEnabled(prev); + } + } + } + + void codegenModuleFilerOut(GeneralCreatorRequest req, + ModuleDetail moduleDetail) { + boolean prev = true; + if (req.analysisOnly()) { + prev = CodeGenFiler.filerEnabled(false); + } + + try { + req.filer().codegenModuleFilerOut(moduleDetail); + } finally { + if (req.analysisOnly()) { + CodeGenFiler.filerEnabled(prev); + } + } + } + + void codegenApplicationFilerOut(GeneralCreatorRequest req, + TypeName applicationTypeName, + String applicationBody) { + boolean prev = true; + if (req.analysisOnly()) { + prev = CodeGenFiler.filerEnabled(false); + } + + try { + req.filer().codegenApplicationFilerOut(applicationTypeName, applicationBody); + } finally { + if (req.analysisOnly()) { + CodeGenFiler.filerEnabled(prev); + } + } + } + + Path codegenModuleInfoFilerOut(GeneralCreatorRequest req, + ModuleInfoDescriptor descriptor) { + boolean prev = true; + if (req.analysisOnly()) { + prev = CodeGenFiler.filerEnabled(false); + } + + try { + return req.filer().codegenModuleInfoFilerOut(descriptor, true).orElse(null); + } finally { + if (req.analysisOnly()) { + CodeGenFiler.filerEnabled(prev); + } + } + } + + @Override + public InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest req, + Map interceptionPlans) { + DefaultInterceptorCreatorResponse.Builder res = DefaultInterceptorCreatorResponse.builder(); + res.interceptionPlans(interceptionPlans); + + for (Map.Entry e : interceptionPlans.entrySet()) { + try { + Path filePath = codegenInterceptorFilerOut(req, null, e.getValue()); + res.addGeneratedFile(e.getKey(), filePath); + } catch (Throwable t) { + throw new ToolsException("Failed while processing: " + e.getKey(), t); + } + } + + return res.build(); + } + + private Path codegenInterceptorFilerOut(GeneralCreatorRequest req, + DefaultActivatorCreatorResponse.Builder builder, + InterceptionPlan interceptionPlan) { + TypeName interceptorTypeName = DefaultInterceptorCreator.createInterceptorSourceTypeName(interceptionPlan); + DefaultInterceptorCreator interceptorCreator = new DefaultInterceptorCreator(); + String body = interceptorCreator.createInterceptorSourceBody(interceptionPlan); + if (builder != null) { + builder.addServiceTypeInterceptorPlan(interceptorTypeName, interceptionPlan); + } + return req.filer().codegenJavaFilerOut(interceptorTypeName, body).orElseThrow(); + } + + private ActivatorCodeGenDetail createActivatorCodeGenDetail(ActivatorCreatorRequest req, + TypeName serviceTypeName, + LazyValue scan) { + ActivatorCreatorCodeGen codeGen = req.codeGen(); + String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_ACTIVATOR_HBS); + ServiceInfoBasics serviceInfo = toServiceInfo(serviceTypeName, codeGen); + TypeName activatorTypeName = toActivatorTypeName(serviceTypeName); + TypeName parentTypeName = toParentTypeName(serviceTypeName, codeGen); + String activatorGenericDecl = toActivatorGenericDecl(serviceTypeName, codeGen); + DependenciesInfo dependencies = toDependencies(serviceTypeName, codeGen); + DependenciesInfo parentDependencies = toDependencies(parentTypeName, codeGen); + Set scopeTypeNames = toScopeTypeNames(serviceTypeName, codeGen); + String generatedSticker = toGeneratedSticker(req); + List description = toDescription(serviceTypeName); + Double weightedPriority = toWeightedPriority(serviceTypeName, codeGen); + Integer runLevel = toRunLevel(serviceTypeName, codeGen); + String postConstructMethodName = toPostConstructMethodName(serviceTypeName, codeGen); + String preDestroyMethodName = toPreDestroyMethodName(serviceTypeName, codeGen); + List serviceTypeInjectionOrder = toServiceTypeHierarchy(serviceTypeName, codeGen, scan); + List extraCodeGen = toExtraCodeGen(serviceTypeName, codeGen); + List extraClassComments = toExtraClassComments(serviceTypeName, codeGen); + boolean isProvider = toIsProvider(serviceTypeName, codeGen); + boolean isConcrete = toIsConcrete(serviceTypeName, codeGen); + boolean isSupportsJsr330InStrictMode = req.configOptions().isSupportsJsr330InStrictMode(); + Collection injectionPointsSkippedInParent = + toCodegenInjectMethodsSkippedInParent(isSupportsJsr330InStrictMode, activatorTypeName, codeGen, scan); + + ActivatorCreatorArgs args = DefaultActivatorCreatorArgs.builder() + .template(template) + .serviceTypeName(serviceTypeName) + .activatorTypeName(activatorTypeName) + .activatorGenericDecl(Optional.ofNullable(activatorGenericDecl)) + .parentTypeName(Optional.ofNullable(parentTypeName)) + .scopeTypeNames(scopeTypeNames) + .description(description) + .serviceInfo(serviceInfo) + .dependencies(Optional.ofNullable(dependencies)) + .parentDependencies(Optional.ofNullable(parentDependencies)) + .injectionPointsSkippedInParent(injectionPointsSkippedInParent) + .serviceTypeInjectionOrder(serviceTypeInjectionOrder) + .generatedSticker(generatedSticker) + .weightedPriority(Optional.ofNullable(weightedPriority)) + .runLevel(Optional.ofNullable(runLevel)) + .postConstructMethodName(Optional.ofNullable(postConstructMethodName)) + .preDestroyMethodName(Optional.ofNullable(preDestroyMethodName)) + .extraCodeGen(extraCodeGen) + .extraClassComments(extraClassComments) + .concrete(isConcrete) + .provider(isProvider) + .supportsJsr330InStrictMode(isSupportsJsr330InStrictMode) + .build(); + String activatorBody = toActivatorBody(args); + + return DefaultActivatorCodeGenDetail.builder() + .serviceInfo(serviceInfo) + .dependencies(Optional.ofNullable(dependencies)) + .serviceTypeName(toActivatorImplTypeName(activatorTypeName)) + .body(activatorBody) + .build(); + } + + /** + * Creates a payload given the batch of services to process. + * + * @param services the services to process + * @return the payload, or empty if unable or nothing to process + */ + public static Optional createActivatorCreatorCodeGen(ServicesToProcess services) { + // do not generate activators for modules or applications... + List serviceTypeNames = services.serviceTypeNames(); + if (!serviceTypeNames.isEmpty()) { + TypeName applicationTypeName = createFromTypeName(TypeNames.PICO_APPLICATION); + TypeName moduleTypeName = createFromTypeName(TypeNames.PICO_MODULE); + serviceTypeNames = serviceTypeNames.stream() + .filter(typeName -> { + Set contracts = services.contracts().get(typeName); + if (contracts == null) { + return true; + } + return !contracts.contains(applicationTypeName) && !contracts.contains(moduleTypeName); + }) + .collect(Collectors.toList()); + } + if (serviceTypeNames.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(DefaultActivatorCreatorCodeGen.builder() + .serviceTypeToParentServiceTypes(toFilteredParentServiceTypes(services)) + .serviceTypeToActivatorGenericDecl(services.activatorGenericDecls()) + .serviceTypeHierarchy(toFilteredHierarchy(services)) + .serviceTypeAccessLevels(services.accessLevels()) + .serviceTypeIsAbstractTypes(services.isAbstractMap()) + .serviceTypeContracts(toFilteredContracts(services)) + .serviceTypeExternalContracts(services.externalContracts()) + .serviceTypeInjectionPointDependencies(services.injectionPointDependencies()) + .serviceTypePostConstructMethodNames(services.postConstructMethodNames()) + .serviceTypePreDestroyMethodNames(services.preDestroyMethodNames()) + .serviceTypeWeights(services.weightedPriorities()) + .serviceTypeRunLevels(services.runLevels()) + .serviceTypeScopeNames(services.scopeTypeNames()) + .serviceTypeToProviderForTypes(services.providerForTypeNames()) + .serviceTypeQualifiers(services.qualifiers()) + .modulesRequired(services.requiredModules()) + .classPrefixName((services.lastKnownTypeSuffix() != null) + ? DefaultApplicationCreator.upperFirstChar(services.lastKnownTypeSuffix()) + : ActivatorCreatorCodeGen.DEFAULT_CLASS_PREFIX_NAME) + .serviceTypeInterceptionPlan(services.interceptorPlans()) + .extraCodeGen(services.extraCodeGen()) + .extraClassComments(services.extraActivatorClassComments()) + .build()); + } + + /** + * Create a request based upon the contents of services to processor. + * + * @param servicesToProcess the batch being processed + * @param codeGen the code gen request + * @param configOptions the config options + * @param filer the filer + * @param throwIfError fail on error? + * @return the activator request instance + */ + public static ActivatorCreatorRequest createActivatorCreatorRequest(ServicesToProcess servicesToProcess, + ActivatorCreatorCodeGen codeGen, + ActivatorCreatorConfigOptions configOptions, + CodeGenFiler filer, + boolean throwIfError) { + String packageName = servicesToProcess.determineGeneratedPackageName(); + String moduleName = servicesToProcess.determineGeneratedModuleName(); + if (ModuleInfoDescriptor.DEFAULT_MODULE_NAME.equals(moduleName)) { + // last resort is using the application name as the module name + moduleName = packageName; + } + + CodeGenPaths codeGenPaths = createCodeGenPaths(servicesToProcess); + return DefaultActivatorCreatorRequest.builder() + .serviceTypeNames(servicesToProcess.serviceTypeNames()) + .codeGen(codeGen) + .codeGenPaths(codeGenPaths) + .filer(filer) + .configOptions(configOptions) + .throwIfError(throwIfError) + .moduleName(moduleName) + .packageName(packageName) + .build(); + } + + private static Map toFilteredParentServiceTypes(ServicesToProcess services) { + Map parents = services.parentServiceTypes(); + Map filteredParents = new LinkedHashMap<>(parents); + for (Map.Entry e : parents.entrySet()) { + if (e.getValue() != null + && !services.serviceTypeNames().contains(e.getValue()) + // if the caller is declaring a parent with generics, then assume they know what they are doing + && !e.getValue().fqName().contains("<")) { + TypeName serviceTypeName = e.getKey(); + if (services.activatorGenericDecls().get(serviceTypeName) == null) { + filteredParents.put(e.getKey(), null); + } + } + } + return filteredParents; + } + + private static Map> toFilteredHierarchy(ServicesToProcess services) { + Map> hierarchy = services.serviceTypeToHierarchy(); + Map> filteredHierarchy = new LinkedHashMap<>(); + for (Map.Entry> e : hierarchy.entrySet()) { + List filtered = e.getValue().stream() + .filter((typeName) -> services.serviceTypeNames().contains(typeName)) + .collect(Collectors.toList()); +// assert (!filtered.isEmpty()) : e; + filteredHierarchy.put(e.getKey(), filtered); + } + return filteredHierarchy; + } + + private static Map> toFilteredContracts(ServicesToProcess services) { + Map> contracts = services.contracts(); + Map> filteredContracts = new LinkedHashMap<>(); + for (Map.Entry> e : contracts.entrySet()) { + Set contractsForThisService = e.getValue(); + Set externalContractsForThisService = services.externalContracts().get(e.getKey()); + if (externalContractsForThisService == null || externalContractsForThisService.isEmpty()) { + filteredContracts.put(e.getKey(), e.getValue()); + } else { + Set filteredContractsForThisService = new LinkedHashSet<>(contractsForThisService); + filteredContractsForThisService.removeAll(externalContractsForThisService); + filteredContracts.put(e.getKey(), filteredContractsForThisService); + } + } + return filteredContracts; + } + + String toApplicationStubBody(ActivatorCreatorRequest req, + TypeName applicationTypeName, + String moduleName) { + String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_APPLICATION_STUB_HBS); + + Map subst = new HashMap<>(); + subst.put("classname", applicationTypeName.className()); + subst.put("packagename", applicationTypeName.packageName()); + subst.put("description", "Generated " + PicoServicesConfig.NAME + " Application."); + subst.put("generatedanno", toGeneratedSticker(req)); + subst.put("header", BuilderTypeTools.copyrightHeaderFor(getClass().getName())); + subst.put("modulename", moduleName); + return templateHelper().applySubstitutions(template, subst, true).trim(); + } + + String toModuleBody(ActivatorCreatorRequest req, + String packageName, + String className, + String moduleName, + List activatorTypeNames) { + String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_MODULE_HBS); + + Map subst = new HashMap<>(); + subst.put("classname", className); + subst.put("packagename", packageName); + subst.put("description", "Generated " + PicoServicesConfig.NAME + " Module."); + subst.put("generatedanno", toGeneratedSticker(req)); + subst.put("header", BuilderTypeTools.copyrightHeaderFor(getClass().getName())); + subst.put("modulename", moduleName); + subst.put("activators", activatorTypeNames); + + return templateHelper().applySubstitutions(template, subst, true).trim(); + } + + @Override + public TypeName toActivatorImplTypeName(TypeName serviceTypeName) { + return create(serviceTypeName.packageName(), + toFlatName(serviceTypeName.className()) + + INNER_ACTIVATOR_CLASS_NAME); + } + + private String toActivatorBody(ActivatorCreatorArgs args) { + Map subst = new HashMap<>(); + subst.put("header", BuilderTypeTools.copyrightHeaderFor(getClass().getName())); + subst.put("activatorsuffix", INNER_ACTIVATOR_CLASS_NAME); + subst.put("classname", args.activatorTypeName().className()); + subst.put("flatclassname", toFlatName(args.activatorTypeName().className())); + subst.put("packagename", args.activatorTypeName().packageName()); + subst.put("activatorgenericdecl", args.activatorGenericDecl().orElse(null)); + subst.put("parent", toCodenParent(args.isSupportsJsr330InStrictMode(), + args.activatorTypeName(), args.parentTypeName().orElse(null))); + subst.put("scopetypenames", args.scopeTypeNames()); + subst.put("description", args.description()); + subst.put("generatedanno", args.generatedSticker()); + subst.put("isprovider", args.isProvider()); + subst.put("isconcrete", args.isConcrete()); + subst.put("contracts", args.serviceInfo().contractsImplemented()); + if (args.serviceInfo() instanceof ServiceInfo) { + ServiceInfo serviceInfo = ((ServiceInfo) args.serviceInfo()); + Set extContracts = serviceInfo.externalContractsImplemented(); + subst.put("externalcontracts", extContracts); + // there is no need to list these twice, since external contracts will implicitly back-full into contracts + subst.put("contracts", args.serviceInfo().contractsImplemented().stream() + .filter(it -> !extContracts.contains(it)).collect(Collectors.toList())); + } + subst.put("qualifiers", toCodegenQualifiers(args.serviceInfo().qualifiers())); + subst.put("dependencies", toCodegenDependencies(args.dependencies().orElse(null))); + subst.put("weight", args.weightedPriority().orElse(null)); + subst.put("isweightset", args.weightedPriority().isPresent()); + subst.put("runlevel", args.runLevel().orElse(null)); + subst.put("isrunlevelset", args.runLevel().isPresent()); + subst.put("postconstruct", args.postConstructMethodName().orElse(null)); + subst.put("predestroy", args.preDestroyMethodName().orElse(null)); + subst.put("ctorarglist", toCodegenCtorArgList(args.dependencies().orElse(null))); + subst.put("ctorargs", toCodegenInjectCtorArgs(args.dependencies().orElse(null))); + subst.put("injectedfields", toCodegenInjectFields(args.dependencies().orElse(null))); + subst.put("injectedmethods", toCodegenInjectMethods(args.activatorTypeName(), args.dependencies().orElse(null))); + subst.put("injectedmethodsskippedinparent", args.injectionPointsSkippedInParent()); + subst.put("extracodegen", args.extraCodeGen()); + subst.put("extraclasscomments", args.extraClassComments()); + subst.put("injectionorder", args.serviceTypeInjectionOrder()); + subst.put("issupportsjsr330instrictmode", args.isSupportsJsr330InStrictMode()); + + logger().log(System.Logger.Level.DEBUG, "dependencies for " + + args.serviceTypeName() + " == " + args.dependencies()); + + return templateHelper().applySubstitutions(args.template(), subst, true).trim(); + } + + String toCodenParent(boolean ignoredIsSupportsJsr330InStrictMode, + TypeName activatorTypeName, + TypeName parentTypeName) { + String result; + if (parentTypeName == null || Object.class.getName().equals(parentTypeName.name())) { + result = AbstractServiceProvider.class.getName() + "<" + activatorTypeName.className() + ">"; + } else if (parentTypeName.typeArguments() == null || parentTypeName.typeArguments().isEmpty()) { + result = parentTypeName.packageName() + + (parentTypeName.packageName() == null ? "" : ".") + + parentTypeName.className().replace(".", "$") + + INNER_ACTIVATOR_CLASS_NAME; + } else { + result = parentTypeName.fqName(); + } + + return result; + } + + List toCodegenDependencies(DependenciesInfo dependencies) { + if (dependencies == null) { + return null; + } + + List result = new ArrayList<>(); + dependencies.allDependencies() + .forEach(dep1 -> dep1.injectionPointDependencies() + .forEach(dep2 -> result.add(toCodegenDependency(dep1.dependencyTo(), dep2)))); + + return result; + } + + String toCodegenDependency(ServiceInfoCriteria dependencyTo, + InjectionPointInfo ipInfo) { + StringBuilder builder = new StringBuilder(); + //.add("world", World.class, InjectionPointInfo.ElementKind.FIELD, InjectionPointInfo.Access.PACKAGE_PRIVATE) + String elemName = CodeGenUtils.elementNameKindRef(ipInfo.elementName(), ipInfo.elementKind()); + builder.append(".add(").append(elemName).append(", "); + builder.append(Objects.requireNonNull(componentTypeNameOf(first(dependencyTo.contractsImplemented(), true)))) + .append(".class, "); + builder.append("ElementKind.").append(Objects.requireNonNull(ipInfo.elementKind())).append(", "); + if (InjectionPointInfo.ElementKind.FIELD != ipInfo.elementKind()) { + builder.append(ipInfo.elementArgs().orElseThrow()).append(", "); + } + builder.append("Access.").append(Objects.requireNonNull(ipInfo.access())).append(")"); + Integer elemPos = ipInfo.elementArgs().orElse(null); + Integer elemOffset = ipInfo.elementOffset().orElse(null); + Set qualifiers = ipInfo.qualifiers(); + if (elemPos != null) { + builder.append(".elemOffset(").append(elemOffset).append(")"); + } + if (!qualifiers.isEmpty()) { + builder.append(toCodegenQualifiers(qualifiers)); + } + if (ipInfo.listWrapped()) { + builder.append(".listWrapped()"); + } + if (ipInfo.providerWrapped()) { + builder.append(".providerWrapped()"); + } + if (ipInfo.optionalWrapped()) { + builder.append(".optionalWrapped()"); + } + if (ipInfo.staticDeclaration()) { + builder.append(".staticDeclaration()"); + } + return builder.toString(); + } + + String toCodegenQualifiers(Collection qualifiers) { + StringBuilder builder = new StringBuilder(); + for (QualifierAndValue qualifier : qualifiers) { + if (builder.length() > 0) { + builder.append("\n\t\t\t"); + } + builder.append(".addQualifier(").append(toCodegenQualifiers(qualifier)).append(")"); + } + return builder.toString(); + } + + String toCodegenQualifiers(QualifierAndValue qualifier) { + String val = toCodegenQuotedString(qualifier.value().orElse(null)); + String result = DefaultQualifierAndValue.class.getName() + ".create(" + + qualifier.qualifierTypeName() + ".class"; + if (val != null) { + result += ", " + val; + } + result += ")"; + return result; + } + + String toCodegenQuotedString(String value) { + return (value == null) ? null : "\"" + value + "\""; + } + + String toCodegenDecl(ServiceInfoCriteria dependencyTo, + InjectionPointInfo injectionPointInfo) { + String contract = first(dependencyTo.contractsImplemented(), true); + StringBuilder builder = new StringBuilder(); + if (injectionPointInfo.optionalWrapped()) { + builder.append("Optional<").append(contract).append(">"); + } else { + if (injectionPointInfo.listWrapped()) { + builder.append("List<"); + } + if (injectionPointInfo.providerWrapped()) { + builder.append("Provider<"); + } + builder.append(contract); + if (injectionPointInfo.providerWrapped()) { + builder.append(">"); + } + if (injectionPointInfo.listWrapped()) { + builder.append(">"); + } + } + PicoSupported.isSupportedInjectionPoint(logger(), + createFromTypeName(injectionPointInfo.serviceTypeName()), + injectionPointInfo, + InjectionPointInfo.Access.PRIVATE == injectionPointInfo.access(), + injectionPointInfo.staticDeclaration()); + return builder.toString(); + } + + String toCodegenCtorArgList(DependenciesInfo dependencies) { + if (dependencies == null) { + return null; + } + + AtomicInteger count = new AtomicInteger(); + AtomicReference nameRef = new AtomicReference<>(); + List args = new ArrayList<>(); + dependencies.allDependencies() + .forEach(dep1 -> dep1.injectionPointDependencies() + .stream() + .filter(dep2 -> DefaultInjectionPointInfo.CONSTRUCTOR.equals(dep2.elementName())) + .forEach(dep2 -> { + if ((nameRef.get() == null)) { + nameRef.set(dep2.baseIdentity()); + } else { + assert (nameRef.get().equals(dep2.baseIdentity())) : "only 1 ctor can be injectable"; + } + args.add("c" + count.incrementAndGet()); + }) + ); + + return (args.isEmpty()) ? null : CommonUtils.toString(args); + } + + List toCodegenInjectCtorArgs(DependenciesInfo dependencies) { + if (dependencies == null) { + return null; + } + + AtomicInteger count = new AtomicInteger(); + AtomicReference nameRef = new AtomicReference<>(); + List args = new ArrayList<>(); + List allCtorArgs = dependencies.allDependenciesFor(DefaultInjectionPointInfo.CONSTRUCTOR); + allCtorArgs.forEach(dep1 -> dep1.injectionPointDependencies() + .forEach(dep2 -> { + if (nameRef.get() == null) { + nameRef.set(dep2.baseIdentity()); + } else { + assert (nameRef.get().equals(dep2.baseIdentity())) : "only 1 constructor can be injectable"; + } + String cn = toCodegenDecl(dep1.dependencyTo(), dep2); + String argName = "c" + count.incrementAndGet(); + String id = dep2.baseIdentity() + "(" + count.get() + ")"; + String argBuilder = cn + " " + + argName + " = (" + cn + ") " + + "get(deps, \"" + id + "\");"; + args.add(argBuilder); + })); + return args; + } + + List toCodegenInjectFields(DependenciesInfo dependencies) { + if (dependencies == null) { + return null; + } + + List fields = new ArrayList<>(); + dependencies.allDependencies() + .forEach(dep1 -> dep1.injectionPointDependencies().stream() + .filter(dep2 -> InjectionPointInfo.ElementKind.FIELD + .equals(dep2.elementKind())) + .forEach(dep2 -> { + String cn = toCodegenDecl(dep1.dependencyTo(), dep2); + IdAndToString setter; + String id = dep2.id(); + if (Void.class.getName().equals(cn)) { + setter = new IdAndToString(id, dep2.elementName()); + } else { + setter = new IdAndToString(id, dep2.elementName() + + " = (" + cn + ") get(deps, \"" + + dep2.baseIdentity() + "\")"); + } + fields.add(setter); + })); + return fields; + } + + List toCodegenInjectMethods(TypeName serviceTypeName, + DependenciesInfo dependencies) { + if (dependencies == null) { + return null; + } + + List methods = new ArrayList<>(); + String lastElemName = null; + String lastId = null; + List compositeSetter = null; + List allDeps = dependencies.allDependencies().stream() + .filter(it -> it.injectionPointDependencies().iterator().next().elementKind() == ElementInfo.ElementKind.METHOD) + .collect(Collectors.toList()); + if (allDeps.size() > 1) { + allDeps.sort(DependenciesInfo.comparator()); + } + + for (DependencyInfo dep1 : allDeps) { + for (InjectionPointInfo ipInfo : dep1.injectionPointDependencies()) { + if (ipInfo.elementKind() != InjectionPointInfo.ElementKind.METHOD) { + continue; + } + + String id = toBaseIdTagName(ipInfo, serviceTypeName); + String elemName = ipInfo.elementName(); + Integer elemPos = ipInfo.elementOffset().orElse(null); + int elemArgs = ipInfo.elementArgs().orElse(0); + String cn = toCodegenDecl(dep1.dependencyTo(), ipInfo); + + if (lastId != null && !lastId.equals(id) && compositeSetter != null) { + IdAndToString setter = new IdAndToString(lastId, lastElemName + "(" + + CommonUtils.toString(compositeSetter, null, ",\n\t\t\t\t") + + ")"); + methods.add(setter); + compositeSetter = null; + } + + if (0 == elemArgs) { + assert (Void.class.getName().equals(cn)); + IdAndToString setter = new IdAndToString(id, elemName + "()"); + methods.add(setter); + } else if (1 == elemArgs) { + assert (elemArgs == elemPos); + IdAndToString setter = new IdAndToString(id, + elemName + "((" + cn + ") get(deps, \"" + id + "(1)\"))"); + methods.add(setter); + } else { + assert (elemArgs > 1); + if (compositeSetter == null) { + compositeSetter = new ArrayList<>(); + } + compositeSetter.add("(" + cn + ") get(deps, \"" + id + "(" + elemPos + ")\")"); + } + + lastId = id; + lastElemName = elemName; + } + } + + if (compositeSetter != null) { + IdAndToString setter = new IdAndToString(lastId, lastElemName + "(" + + CommonUtils.toString(compositeSetter, null, ",\n\t\t\t\t") + + ")"); + methods.add(setter); + } + + return methods; + } + + Collection toCodegenInjectMethodsSkippedInParent(boolean isSupportsJsr330InStrictMode, + TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen, + LazyValue scan) { + List hierarchy = codeGen.serviceTypeHierarchy().get(serviceTypeName); + TypeName parent = parentOf(serviceTypeName, codeGen); + if (hierarchy == null && parent != null) { + hierarchy = List.of(parent); + } + if (hierarchy == null) { + return List.of(); + } + + DependenciesInfo deps = codeGen.serviceTypeInjectionPointDependencies().get(serviceTypeName); + + Set result = new LinkedHashSet<>(); + hierarchy.stream().filter((typeName) -> !serviceTypeName.equals(typeName)) + .forEach(parentTypeName -> { + DependenciesInfo parentDeps = codeGen.serviceTypeInjectionPointDependencies().get(parentTypeName); + List skipList = toCodegenInjectMethodsSkippedInParent(isSupportsJsr330InStrictMode, + serviceTypeName, + deps, + parentTypeName, + parentDeps, + scan); + if (skipList != null) { + result.addAll(skipList); + } + }); + + return result; + } + + /** + * Called in strict Jsr330 compliance mode. If Inject anno is on parent method but not on child method + * then we should hide the inject in the parent. Crazy that inject was not inherited if you ask me! + * + * @param isSupportsJsr330InStrictMode are we in jsr-330 strict mode + * @param serviceTypeName the activator service type name + * @param dependencies the dependencies for this service type + * @param parentTypeName the parent type + * @param parentDependencies the parent dependencies + * @param scan the provider of class introspection + * @return the list of injection point identifiers that should be skipped in the parent delegation call + */ + List toCodegenInjectMethodsSkippedInParent(boolean isSupportsJsr330InStrictMode, + TypeName serviceTypeName, + DependenciesInfo dependencies, + TypeName parentTypeName, + DependenciesInfo parentDependencies, + LazyValue scan) { + if (!isSupportsJsr330InStrictMode || parentTypeName == null) { + return null; + } + + ClassInfo classInfo = toClassInfo(serviceTypeName, scan); + ClassInfo parentClassInfo = toClassInfo(parentTypeName, scan); + MethodInfoList parentMethods = parentClassInfo.getDeclaredMethodInfo(); + Map injectedParentMethods = parentMethods.stream() + .filter(m -> (m.getAnnotationInfo(TypeNames.JAKARTA_INJECT) != null)) + .filter(m -> DefaultExternalModuleCreator.isPicoSupported(parentTypeName, m, logger())) + .collect(Collectors.toMap(DefaultActivatorCreator::toBaseIdTag, Function.identity())); + if (injectedParentMethods.isEmpty()) { + return null; + } + + MethodInfoList methods = classInfo.getDeclaredMethodInfo(); + Map allSupportedMethodsOnServiceType = methods.stream() + .filter(m -> DefaultExternalModuleCreator.isPicoSupported(serviceTypeName, m, logger())) + .collect(Collectors.toMap(DefaultActivatorCreator::toBaseIdTag, Function.identity())); + + List removeList = null; + + for (Map.Entry e : injectedParentMethods.entrySet()) { + MethodInfo method = allSupportedMethodsOnServiceType.get(e.getKey()); + if (method != null) { + AnnotationInfo annotationInfo = method.getAnnotationInfo(TypeNames.JAKARTA_INJECT); + if (annotationInfo != null) { + continue; + } + if (removeList == null) { + removeList = new ArrayList<>(); + } + removeList.add(e.getKey()); + } + } + + return removeList; + } + + static ClassInfo toClassInfo(TypeName serviceTypeName, + LazyValue scan) { + ClassInfo classInfo = scan.get().getClassInfo(serviceTypeName.name()); + if (classInfo == null) { + throw new ToolsException("unable to introspect: " + serviceTypeName); + } + return classInfo; + } + + static IdAndToString toBaseIdTag(MethodInfo m) { + String packageName = m.getClassInfo().getPackageName(); + boolean isPackagePrivate = isPackagePrivate(m.getModifiers()); + InjectionPointInfo.Access access = (isPackagePrivate) + ? InjectionPointInfo.Access.PACKAGE_PRIVATE : InjectionPointInfo.Access.PUBLIC; + String idTag = toBaseIdTagName(m.getName(), m.getParameterInfo().length, access, packageName); + return new IdAndToString(idTag, m); + } + + static String toBaseIdTagName(InjectionPointInfo ipInfo, + TypeName serviceTypeName) { + String packageName = serviceTypeName.packageName(); + return toBaseIdTagName(ipInfo.elementName(), ipInfo.elementArgs().orElse(0), ipInfo.access(), packageName); + } + + static String toBaseIdTagName(String methodName, + int methodArgCount, + InjectionPointInfo.Access access, + String packageName) { + return Dependencies.toMethodBaseIdentity(methodName, methodArgCount, access, () -> packageName); + } + + Double toWeightedPriority(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Double weight = codeGen.serviceTypeWeights().get(serviceTypeName); + if (weight == null && hasParent(serviceTypeName, codeGen)) { + // we might be a child of another service, in which case we will need to override its value + weight = Weighted.DEFAULT_WEIGHT; + } + return weight; + } + + Integer toRunLevel(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Integer runLevel = codeGen.serviceTypeRunLevels().get(serviceTypeName); + if (runLevel == null && hasParent(serviceTypeName, codeGen)) { + // we might be a child of another service, in which case we will need to override its value + runLevel = RunLevel.NORMAL; + } + return runLevel; + } + + boolean hasParent(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return (parentOf(serviceTypeName, codeGen) != null); + } + + TypeName parentOf(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return codeGen.serviceTypeToParentServiceTypes().get(serviceTypeName); + } + + List toDescription(TypeName serviceTypeName) { + return List.of("Activator for {@link " + serviceTypeName + "}."); + } + + TypeName toActivatorTypeName(TypeName serviceTypeName) { + return serviceTypeName; + } + + TypeName toParentTypeName(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return codeGen.serviceTypeToParentServiceTypes().get(serviceTypeName); + } + + String toActivatorGenericDecl(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return codeGen.serviceTypeToActivatorGenericDecl().get(serviceTypeName); + } + + Set toScopeTypeNames(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Set result = codeGen.serviceTypeScopeNames().get(serviceTypeName); + return (result == null) ? Set.of() : result; + } + + /** + * One might expect that isProvider should only be set to true if the service type implements Provider<>. However, + * that alone would fail JSR-330 testing. The interpretation there is any service without a scope is inferred to be + * non-singleton, provided/dependent scope. + */ + boolean toIsProvider(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Set scopeTypeName = toScopeTypeNames(serviceTypeName, codeGen); + if ((scopeTypeName == null || scopeTypeName.isEmpty()) && toIsConcrete(serviceTypeName, codeGen)) { + return true; + } + + Set providerFor = codeGen.serviceTypeToProviderForTypes().get(serviceTypeName); + return (providerFor != null) && !providerFor.isEmpty(); + } + + boolean toIsConcrete(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Boolean isAbstract = codeGen.serviceTypeIsAbstractTypes().get(serviceTypeName); + return (isAbstract == null) || !isAbstract; + } + + /** + * Creates service info from the service type name and the activator create codegen request. + * + * @param serviceTypeName the service type name + * @param codeGen the code gen request + * @return the service info + */ + public static ServiceInfoBasics toServiceInfo(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Set contracts = codeGen.serviceTypeContracts().get(serviceTypeName); + Set externalContracts = codeGen.serviceTypeExternalContracts().get(serviceTypeName); + Set qualifiers = codeGen.serviceTypeQualifiers().get(serviceTypeName); + return DefaultServiceInfo.builder() + .serviceTypeName(serviceTypeName.name()) + .contractsImplemented(toSet(contracts, TypeName::name)) + .externalContractsImplemented(toSet(externalContracts, TypeName::name)) + .qualifiers((qualifiers == null) ? Set.of() : qualifiers) + .build(); + } + + DependenciesInfo toDependencies(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + if (serviceTypeName == null) { + return null; + } + + return codeGen.serviceTypeInjectionPointDependencies().get(serviceTypeName); + } + + String toPostConstructMethodName(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return codeGen.serviceTypePostConstructMethodNames().get(serviceTypeName); + } + + String toPreDestroyMethodName(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + return codeGen.serviceTypePreDestroyMethodNames().get(serviceTypeName); + } + + List toServiceTypeHierarchy(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen, + LazyValue scan) { + Map> map = codeGen.serviceTypeHierarchy(); + List order = (map != null) ? map.get(serviceTypeName) : null; + if (order != null) { + return (1 == order.size()) ? List.of() : order; + } + + return serviceTypeHierarchy(serviceTypeName, scan); + } + + List toExtraCodeGen(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Map> map = codeGen.extraCodeGen(); + List extraCodeGen = (map != null) ? map.get(serviceTypeName) : List.of(); + return (extraCodeGen == null) ? List.of() : extraCodeGen; + } + + List toExtraClassComments(TypeName serviceTypeName, + ActivatorCreatorCodeGen codeGen) { + Map> map = codeGen.extraClassComments(); + List extraClassComments = (map != null) ? map.get(serviceTypeName) : List.of(); + return (extraClassComments == null) ? List.of() : extraClassComments; + } + + static List serviceTypeHierarchy(TypeName serviceTypeName, + LazyValue scan) { + List order = new ArrayList<>(); + ClassInfo classInfo = toClassInfo(serviceTypeName, scan); + while (classInfo != null) { + order.add(0, createTypeNameFromClassInfo(classInfo)); + classInfo = classInfo.getSuperclass(); + } + return (1 == order.size()) ? List.of() : order; + } + + ActivatorCreatorResponse handleError(ActivatorCreatorRequest request, + ToolsException e, + DefaultActivatorCreatorResponse.Builder builder) { + if (request.throwIfError()) { + throw e; + } + + return builder + .error(e) + .success(false) + .build(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultApplicationCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultApplicationCreator.java new file mode 100644 index 00000000000..51debbbd4c7 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultApplicationCreator.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.Module; +import io.helidon.pico.PicoServices; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.services.AbstractServiceProvider; +import io.helidon.pico.services.DefaultServiceBinder; +import io.helidon.pico.services.PicoInjectionPlan; +import io.helidon.pico.tools.spi.ApplicationCreator; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.pico.services.ServiceUtils.isQualifiedInjectionTarget; + +/** + * The default implementation for {@link io.helidon.pico.tools.spi.ApplicationCreator}. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultApplicationCreator extends AbstractCreator implements ApplicationCreator { + /** + * The prefix to add before the generated "Application" class name (i.e., "Pico$$" in the "Pico$$Application"). + */ + public static final String NAME_PREFIX = /* PicoServicesConfig.NAME */ DefaultActivatorCreator.NAME_PREFIX; + + /** + * The "Application" part of the name. + */ + public static final String APPLICATION_NAME_SUFFIX = "Application"; + + /** + * The FQN "Pico$$Application" name. + */ + public static final String APPLICATION_NAME = NAME_PREFIX + APPLICATION_NAME_SUFFIX; + + static final String SERVICE_PROVIDER_APPLICATION_SERVICETYPEBINDING_HBS + = "service-provider-application-servicetypebinding.hbs"; + static final String SERVICE_PROVIDER_APPLICATION_EMPTY_SERVICETYPEBINDING_HBS + = "service-provider-application-empty-servicetypebinding.hbs"; + static final String SERVICE_PROVIDER_APPLICATION_HBS + = "service-provider-application.hbs"; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultApplicationCreator() { + super(TemplateHelper.DEFAULT_TEMPLATE_NAME); + } + + /** + * Generates the source and class file for {@link io.helidon.pico.Application} using the current classpath. + * + * @param req the request + * @return the response for application creation + */ + @Override + public ApplicationCreatorResponse createApplication(ApplicationCreatorRequest req) { + DefaultApplicationCreatorResponse.Builder builder = DefaultApplicationCreatorResponse.builder(); + + if (req.serviceTypeNames() == null) { + return handleError(req, new ToolsException("ServiceTypeNames is required to be passed"), builder); + } + + if (req.codeGen() == null) { + return handleError(req, new ToolsException("CodeGenPaths are required"), builder); + } + + List providersInUseThatAreAllowed = providersNotAllowed(req); + if (!providersInUseThatAreAllowed.isEmpty()) { + return handleError(req, + new ToolsException("There are dynamic " + Provider.class.getSimpleName() + + "s being used that are not allow-listed: " + + providersInUseThatAreAllowed + + "; see the documentation for examples of allow-listing."), builder); + } + + try { + return codegen(req, builder); + } catch (ToolsException te) { + return handleError(req, te, builder); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + return handleError(req, new ToolsException("failed in create", t), builder); + } + } + + @SuppressWarnings("rawtypes") + List providersNotAllowed(ApplicationCreatorRequest req) { + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); + Services services = picoServices.services(); + + List> providers = services.lookupAll(Provider.class); + if (providers.isEmpty()) { + return List.of(); + } + + List providersInUseThatAreNotAllowed = new ArrayList<>(); + for (TypeName typeName : req.serviceTypeNames()) { + if (!isAllowListedProviderName(req.configOptions(), typeName) + && isProvider(typeName, services) + && !isAllowListedProviderQualifierTypeName(req.configOptions(), typeName, services)) { + providersInUseThatAreNotAllowed.add(typeName); + } + } + return providersInUseThatAreNotAllowed; + } + + static boolean isAllowListedProviderName(ApplicationCreatorConfigOptions configOptions, + TypeName typeName) { + ApplicationCreatorConfigOptions.PermittedProviderType opt = configOptions.permittedProviderTypes(); + if (ApplicationCreatorConfigOptions.PermittedProviderType.ALL == opt) { + return true; + } else if (ApplicationCreatorConfigOptions.PermittedProviderType.NONE == opt) { + return false; + } else { + return configOptions.permittedProviderNames().contains(typeName.name()); + } + } + + static ServiceInfoCriteria toServiceInfoCriteria(TypeName typeName) { + return DefaultServiceInfoCriteria.builder().serviceTypeName(typeName.name()).build(); + } + + static ServiceProvider toServiceProvider(TypeName typeName, + Services services) { + return services.lookupFirst(toServiceInfoCriteria(typeName), true).orElseThrow(); + } + + static boolean isProvider(TypeName typeName, + Services services) { + ServiceProvider sp = toServiceProvider(typeName, services); + return sp.isProvider(); + } + + static boolean isAllowListedProviderQualifierTypeName(ApplicationCreatorConfigOptions configOptions, + TypeName typeName, + Services services) { + Set permittedTypeNames = configOptions.permittedProviderQualifierTypeNames(); + if (permittedTypeNames.isEmpty()) { + return false; + } + + ServiceProvider sp = toServiceProvider(typeName, services); + Set spQualifierTypeNames = sp.serviceInfo().qualifiers().stream() + .map(AnnotationAndValue::typeName) + .collect(Collectors.toSet()); + spQualifierTypeNames.retainAll(permittedTypeNames); + return !spQualifierTypeNames.isEmpty(); + } + + ApplicationCreatorResponse codegen(ApplicationCreatorRequest req, + DefaultApplicationCreatorResponse.Builder builder) { + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); + + String serviceTypeBindingTemplate = templateHelper() + .safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_APPLICATION_SERVICETYPEBINDING_HBS); + String serviceTypeBindingEmptyTemplate = templateHelper() + .safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_APPLICATION_EMPTY_SERVICETYPEBINDING_HBS); + + List serviceTypeNames = new ArrayList<>(); + List serviceTypeBindings = new ArrayList<>(); + for (TypeName serviceTypeName : req.serviceTypeNames()) { + String injectionPlan = toServiceTypeInjectionPlan(picoServices, serviceTypeName, + serviceTypeBindingTemplate, serviceTypeBindingEmptyTemplate); + if (injectionPlan == null) { + continue; + } + serviceTypeNames.add(serviceTypeName); + serviceTypeBindings.add(injectionPlan); + } + + TypeName application = toApplicationTypeName(req); + serviceTypeNames.add(application); + + String moduleName = toModuleName(req); + + Map subst = new HashMap<>(); + subst.put("classname", application.className()); + subst.put("packagename", application.packageName()); + subst.put("description", application + " - Generated " + PicoServicesConfig.NAME + " Application."); + subst.put("header", BuilderTypeTools.copyrightHeaderFor(getClass().getName())); + subst.put("generatedanno", toGeneratedSticker(req)); + subst.put("modulename", moduleName); + subst.put("servicetypebindings", serviceTypeBindings); + + String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_APPLICATION_HBS); + String body = templateHelper().applySubstitutions(template, subst, true).trim(); + + if (req.codeGenPaths().generatedSourcesPath().isPresent()) { + codegen(picoServices, req, application, body); + } + + GeneralCodeGenDetail codeGenDetail = DefaultGeneralCodeGenDetail.builder() + .serviceTypeName(application) + .body(body) + .build(); + ApplicationCreatorCodeGen codeGenResponse = DefaultApplicationCreatorCodeGen.builder() + .packageName(application.packageName()) + .className(application.className()) + .classPrefixName(req.codeGen().classPrefixName()) + .build(); + return builder + .applicationCodeGen(codeGenResponse) + .serviceTypeNames(serviceTypeNames) + .addServiceTypeDetail(application, codeGenDetail) + .templateName(req.templateName()) + .moduleName(req.moduleName()) + .build(); + } + + static String toApplicationClassName(String modulePrefix) { + modulePrefix = (modulePrefix == null) ? ActivatorCreatorCodeGen.DEFAULT_CLASS_PREFIX_NAME : modulePrefix; + return NAME_PREFIX + upperFirstChar(modulePrefix) + APPLICATION_NAME_SUFFIX; + } + + /** + * Will uppercase the first letter of the provided name. + * + * @param name the name + * @return the mame with the first letter capitalized + */ + public static String upperFirstChar(String name) { + if (name.isEmpty()) { + return name; + } + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + static TypeName toApplicationTypeName(ApplicationCreatorRequest req) { + ApplicationCreatorCodeGen codeGen = Objects.requireNonNull(req.codeGen()); + String packageName = codeGen.packageName().orElse(null); + if (packageName == null) { + packageName = PicoServicesConfig.NAME; + } + String className = Objects.requireNonNull(codeGen.className().orElse(null)); + return DefaultTypeName.create(packageName, className); + } + + static String toModuleName(ApplicationCreatorRequest req) { + return req.moduleName().orElse(ModuleInfoDescriptor.DEFAULT_MODULE_NAME); + } + + String toServiceTypeInjectionPlan(PicoServices picoServices, + TypeName serviceTypeName, + String serviceTypeBindingTemplate, + String serviceTypeBindingEmptyTemplate) { + Services services = picoServices.services(); + + ServiceInfoCriteria si = toServiceInfoCriteria(serviceTypeName); + ServiceProvider sp = services.lookupFirst(si); + String activator = toActivatorCodeGen(sp); + if (activator == null) { + return null; + } + Map subst = new HashMap<>(); + subst.put("servicetypename", serviceTypeName.name()); + subst.put("activator", activator); + subst.put("modulename", sp.serviceInfo().moduleName().orElse(null)); + if (isQualifiedInjectionTarget(sp)) { + subst.put("injectionplan", toInjectionPlanBindings(sp)); + return templateHelper().applySubstitutions(serviceTypeBindingTemplate, subst, true); + } else { + return templateHelper().applySubstitutions(serviceTypeBindingEmptyTemplate, subst, true); + } + } + + @SuppressWarnings("unchecked") + List toInjectionPlanBindings(ServiceProvider sp) { + AbstractServiceProvider asp = AbstractServiceProvider + .toAbstractServiceProvider(DefaultServiceBinder.toRootProvider(sp), true).orElseThrow(); + DependenciesInfo deps = asp.dependencies(); + if (deps.allDependencies().isEmpty()) { + return List.of(); + } + + List plan = new ArrayList<>(deps.allDependencies().size()); + Map injectionPlan = asp.getOrCreateInjectionPlan(false); + for (Map.Entry e : injectionPlan.entrySet()) { + StringBuilder line = new StringBuilder(); + InjectionPointInfo ipInfo = e.getValue().injectionPointInfo(); + List> ipQualified = e.getValue().injectionPointQualifiedServiceProviders(); + List ipUnqualified = e.getValue().unqualifiedProviders(); + boolean resolved = false; + try { + if (ipQualified.isEmpty()) { + if (!ipUnqualified.isEmpty()) { + resolved = true; + line.append(".resolvedBind("); + } else { + line.append(".bindVoid("); + } + } else if (ipInfo.listWrapped()) { + line.append(".bindMany("); + } else { + line.append(".bind("); + } + + line.append("\"").append(e.getKey()).append("\""); + + if (resolved) { + Object target = ipUnqualified.get(0); + if (!(target instanceof Class)) { + target = target.getClass(); + } + line.append(", ").append(((Class) target).getName()).append(".class"); + } else if (ipInfo.listWrapped() && !ipQualified.isEmpty()) { + line.append(", ").append(toActivatorCodeGen((Collection>) ipQualified)); + } else if (!ipQualified.isEmpty()) { + line.append(", ").append(toActivatorCodeGen(ipQualified.get(0))); + } + line.append(")"); + + plan.add(line.toString()); + } catch (Exception exc) { + throw new IllegalStateException("failed to process: " + e.getKey() + " with " + e.getValue(), exc); + } + } + + return plan; + } + + /** + * Perform the file creation and javac it. + * + * @param picoServices the pico services to use + * @param req the request + * @param applicationTypeName the application type name + * @param body the source code / body to generate + */ + void codegen(PicoServices picoServices, + ApplicationCreatorRequest req, + TypeName applicationTypeName, + String body) { + CodeGenFiler filer = createDirectCodeGenFiler(req.codeGenPaths(), req.analysisOnly()); + Path applicationJavaFilePath = filer.codegenJavaFilerOut(applicationTypeName, body).orElse(null); + + String outputDirectory = req.codeGenPaths().outputPath().orElse(null); + if (outputDirectory != null) { + File outDir = new File(outputDirectory); + + // setup meta-inf services + codegenMetaInfServices(filer, + req.codeGenPaths(), + Map.of(TypeNames.PICO_APPLICATION, List.of(applicationTypeName.name()))); + + // setup module-info + codegenModuleInfoDescriptor(filer, picoServices, req, applicationTypeName); + + // compile, but only if we generated the source file + if (applicationJavaFilePath != null) { + CompilerOptions opts = req.compilerOptions().orElse(null); + JavaC.Builder compilerBuilder = JavaC.builder() + .outputDirectory(outDir) + .logger(logger()) + .messager(req.messager().orElseThrow()); + if (opts != null) { + compilerBuilder + .classpath(opts.classpath()) + .modulepath(opts.modulepath()) + .sourcepath(opts.sourcepath()) + .source(opts.source()) + .target(opts.target()) + .commandLineArgs(opts.commandLineArguments()); + } + JavaC compiler = compilerBuilder.build(); + JavaC.Result result = compiler.compile(applicationJavaFilePath.toFile()); + ToolsException e = result.maybeGenerateError(); + if (e != null) { + throw new ToolsException("failed to compile: " + applicationJavaFilePath, e); + } + } + } + } + + static TypeName moduleServiceTypeOf(PicoServices picoServices, + String moduleName) { + Services services = picoServices.services(); + ServiceProvider serviceProvider = services.lookup(Module.class, moduleName); + return DefaultTypeName.createFromTypeName(serviceProvider.serviceInfo().serviceTypeName()); + } + + void codegenModuleInfoDescriptor(CodeGenFiler filer, + PicoServices picoServices, + ApplicationCreatorRequest req, + TypeName applicationTypeName) { + Optional picoModuleInfoPath = filer.toResourceLocation(ModuleUtils.PICO_MODULE_INFO_JAVA_NAME); + ModuleInfoDescriptor descriptor = filer.readModuleInfo(ModuleUtils.PICO_MODULE_INFO_JAVA_NAME).orElse(null); + if (descriptor != null) { + Objects.requireNonNull(picoModuleInfoPath.orElseThrow()); + String moduleName = req.moduleName().orElse(null); + if (moduleName == null || ModuleInfoDescriptor.DEFAULT_MODULE_NAME.equals(moduleName)) { + moduleName = descriptor.name(); + } + + TypeName moduleTypeName = moduleServiceTypeOf(picoServices, moduleName); + String typePrefix = req.codeGen().classPrefixName(); + ModuleInfoCreatorRequest moduleBuilderRequest = DefaultModuleInfoCreatorRequest.builder() + .name(moduleName) + .moduleTypeName(moduleTypeName) + .applicationTypeName(applicationTypeName) + .moduleInfoPath(picoModuleInfoPath.get().toAbsolutePath().toString()) + .classPrefixName(typePrefix) + .applicationCreated(true) + .moduleCreated(false) + .build(); + descriptor = createModuleInfo(moduleBuilderRequest); + filer.codegenModuleInfoFilerOut(descriptor, true); + } else { + Path realModuleInfoPath = filer.toSourceLocation(ModuleUtils.REAL_MODULE_INFO_JAVA_NAME).orElse(null); + if (realModuleInfoPath != null && !realModuleInfoPath.toFile().exists()) { + throw new ToolsException("expected to find " + realModuleInfoPath + + ". did the " + PicoServicesConfig.NAME + " annotation processor run?"); + } + } + } + void codegenMetaInfServices(CodeGenFiler filer, + CodeGenPaths paths, + Map> metaInfServices) { + filer.codegenMetaInfServices(paths, metaInfServices); + } + + ApplicationCreatorResponse handleError(ApplicationCreatorRequest request, + ToolsException e, + DefaultApplicationCreatorResponse.Builder builder) { + if (request.throwIfError()) { + throw e; + } + + return builder.error(e).success(false).build(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultExternalModuleCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultExternalModuleCreator.java new file mode 100644 index 00000000000..660430b5b5a --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultExternalModuleCreator.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.services.Dependencies; +import io.helidon.pico.tools.spi.ExternalModuleCreator; + +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.ModuleInfo; +import io.github.classgraph.PackageInfo; +import io.github.classgraph.ScanResult; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.createFromTypeName; +import static io.helidon.pico.tools.TypeTools.createInjectionPointInfo; +import static io.helidon.pico.tools.TypeTools.createQualifierAndValueSet; +import static io.helidon.pico.tools.TypeTools.createTypeNameFromClassInfo; +import static io.helidon.pico.tools.TypeTools.extractScopeTypeName; +import static io.helidon.pico.tools.TypeTools.hasAnnotation; +import static io.helidon.pico.tools.TypeTools.isAbstract; +import static io.helidon.pico.tools.TypeTools.isPrivate; +import static io.helidon.pico.tools.TypeTools.isStatic; +import static io.helidon.pico.tools.TypeTools.methodsAnnotatedWith; +import static io.helidon.pico.tools.TypeTools.providesContractType; +import static io.helidon.pico.tools.TypeTools.toAccess; + +/** + * The default implementation of {@link io.helidon.pico.tools.spi.ExternalModuleCreator}. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +public class DefaultExternalModuleCreator extends AbstractCreator implements ExternalModuleCreator { + private final LazyValue scan = LazyValue.create(ReflectionHandler.INSTANCE.scan()); + private final ServicesToProcess services = ServicesToProcess.servicesInstance(); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultExternalModuleCreator() { + super(TemplateHelper.DEFAULT_TEMPLATE_NAME); + } + + @Override + public ExternalModuleCreatorResponse prepareToCreateExternalModule(ExternalModuleCreatorRequest req) { + Objects.requireNonNull(req); + + DefaultExternalModuleCreatorResponse.Builder responseBuilder = + DefaultExternalModuleCreatorResponse.builder(); + Collection packageNames = req.packageNamesToScan(); + if (packageNames.isEmpty()) { + return handleError(req, new ToolsException("Package names to scan are required"), responseBuilder); + } + + Collection targetExternalJars = identifyExternalJars(packageNames); + if (1 != targetExternalJars.size()) { + return handleError(req, new ToolsException("the package names provided " + packageNames + + " must map to a single jar file, but instead found: " + + targetExternalJars), responseBuilder); + } + + try { + // handle the explicit qualifiers passed in + req.serviceTypeToQualifiersMap().forEach((serviceTypeName, qualifiers) -> + services.addQualifiers(createFromTypeName(serviceTypeName), qualifiers)); + + // process each found service type + scan.get().getAllStandardClasses() + .stream() + .filter((classInfo) -> packageNames.contains(classInfo.getPackageName())) + .filter((classInfo) -> !classInfo.isInterface()) + .filter((classInfo) -> !classInfo.isExternalClass()) + .filter((classInfo) -> !isPrivate(classInfo.getModifiers())) + .filter((classInfo) -> !classInfo.isInnerClass() || req.innerClassesProcessed()) + .forEach(this::processServiceType); + + ActivatorCreatorCodeGen activatorCreatorCodeGen = DefaultActivatorCreator + .createActivatorCreatorCodeGen(services).orElseThrow(); + ActivatorCreatorRequest activatorCreatorRequest = DefaultActivatorCreator + .createActivatorCreatorRequest(services, activatorCreatorCodeGen, + req.activatorCreatorConfigOptions(), + req.filer(), + req.throwIfError()); + return responseBuilder + .activatorCreatorRequest(activatorCreatorRequest) + .serviceTypeNames(services.serviceTypeNames()) + .moduleName(services.moduleName()) + .packageName(activatorCreatorRequest.packageName()) + .build(); + } catch (Throwable t) { + return handleError(req, new ToolsException("failed to analyze / prepare external module", t), responseBuilder); + } finally { + services.reset(false); + } + } + + private Collection identifyExternalJars(Collection packageNames) { + Set classpath = new LinkedHashSet<>(); + for (String packageName : packageNames) { + PackageInfo packageInfo = scan.get().getPackageInfo(packageName); + if (packageInfo != null) { + for (ClassInfo classInfo : packageInfo.getClassInfo()) { + URI uri = classInfo.getClasspathElementURI(); + Optional filePath = ModuleUtils.toPath(uri); + filePath.ifPresent(classpath::add); + } + } + } + return classpath; + } + + private void processServiceType(ClassInfo classInfo) { + logger().log(System.Logger.Level.DEBUG, "processing " + classInfo); + + TypeName serviceTypeName = createTypeNameFromClassInfo(classInfo); + + ModuleInfo moduleInfo = classInfo.getModuleInfo(); + Collection requiresModule = null; + if (moduleInfo != null) { + requiresModule = Collections.singleton(moduleInfo.getName()); + services.moduleName(moduleInfo.getName()); + } + + processTypeAndContracts(classInfo, serviceTypeName, requiresModule); + processScopeAndQualifiers(classInfo, serviceTypeName); + processPostConstructAndPreDestroy(classInfo, serviceTypeName); + processDependencies(classInfo, serviceTypeName); + } + + private void processTypeAndContracts(ClassInfo classInfo, + TypeName serviceTypeName, + Collection requiresModule) { + services.addServiceTypeName(serviceTypeName); + services.addTypeForContract(serviceTypeName, serviceTypeName, true); + services.addExternalRequiredModules(serviceTypeName, requiresModule); + services.addParentServiceType(serviceTypeName, createTypeNameFromClassInfo(classInfo.getSuperclass())); + List hierarchy = DefaultActivatorCreator.serviceTypeHierarchy(serviceTypeName, scan); + services.addServiceTypeHierarchy(serviceTypeName, hierarchy); + if (hierarchy != null) { + hierarchy.stream() + .filter((parentTypeName) -> !parentTypeName.equals(serviceTypeName)) + .forEach((parentTypeName) -> services.addTypeForContract(serviceTypeName, parentTypeName, false)); + } + services.addAccessLevel(serviceTypeName, toAccess(classInfo.getModifiers())); + services.addIsAbstract(serviceTypeName, isAbstract(classInfo.getModifiers())); + + boolean firstRound = true; + while (classInfo != null && !Object.class.getName().equals(classInfo.getName())) { + ClassInfoList list = classInfo.getInterfaces(); + for (ClassInfo contractClassInfo : list) { + String cn = contractClassInfo.getName(); + TypeName contract = createFromTypeName(cn); + services.addTypeForContract(serviceTypeName, contract, true); + } + if (firstRound) { + String cn = providesContractType(classInfo); + if (cn != null) { + TypeName contract = createFromTypeName(cn); + services.addTypeForContract(serviceTypeName, contract, true); + services.addProviderFor(serviceTypeName, Collections.singleton(contract)); + } + } + classInfo = classInfo.getSuperclass(); + firstRound = false; + } + } + + private void processScopeAndQualifiers(ClassInfo classInfo, + TypeName serviceTypeName) { + String scopeTypeName = extractScopeTypeName(classInfo); + if (scopeTypeName != null) { + services.addScopeTypeName(serviceTypeName, scopeTypeName); + } + + Set qualifiers = createQualifierAndValueSet(classInfo); + if (!qualifiers.isEmpty()) { + services.addQualifiers(serviceTypeName, qualifiers); + } + } + + private void processPostConstructAndPreDestroy(ClassInfo classInfo, + TypeName serviceTypeName) { + MethodInfo postConstructMethod = methodsAnnotatedWith(classInfo, TypeNames.JAKARTA_POST_CONSTRUCT) + .stream().findFirst().orElse(null); + if (postConstructMethod != null) { + services.addPostConstructMethod(serviceTypeName, postConstructMethod.getName()); + } + + MethodInfo preDestroyMethods = methodsAnnotatedWith(classInfo, TypeNames.JAKARTA_PRE_DESTROY) + .stream().findFirst().orElse(null); + if (preDestroyMethods != null) { + services.addPreDestroyMethod(serviceTypeName, preDestroyMethods.getName()); + } + } + + private void processDependencies(ClassInfo classInfo, + TypeName serviceTypeName) { + Dependencies.BuilderContinuation continuation = Dependencies.builder(serviceTypeName.name()); + for (FieldInfo fieldInfo : classInfo.getFieldInfo()) { + continuation = continuationProcess(serviceTypeName, continuation, fieldInfo); + } + + for (MethodInfo ctorInfo : classInfo.getDeclaredConstructorInfo()) { + continuation = continuationProcess(serviceTypeName, + continuation, InjectionPointInfo.ElementKind.CONSTRUCTOR, ctorInfo); + } + + for (MethodInfo methodInfo : classInfo.getDeclaredMethodInfo()) { + continuation = continuationProcess(serviceTypeName, + continuation, InjectionPointInfo.ElementKind.METHOD, methodInfo); + } + + DependenciesInfo dependencies = continuation.build(); + services.addDependencies(dependencies); + } + + private Dependencies.BuilderContinuation continuationProcess(TypeName serviceTypeName, + Dependencies.BuilderContinuation continuation, + FieldInfo fieldInfo) { + if (hasAnnotation(fieldInfo, TypeNames.JAKARTA_INJECT)) { + if (!PicoSupported.isSupportedInjectionPoint(logger(), + serviceTypeName, fieldInfo.toString(), + isPrivate(fieldInfo.getModifiers()), fieldInfo.isStatic())) { + return continuation; + } + + InjectionPointInfo ipInfo = createInjectionPointInfo(serviceTypeName, fieldInfo); + continuation = continuation.add(ipInfo); + } + + return continuation; + } + + private Dependencies.BuilderContinuation continuationProcess(TypeName serviceTypeName, + Dependencies.BuilderContinuation continuation, + InjectionPointInfo.ElementKind kind, + MethodInfo methodInfo) { + if (hasAnnotation(methodInfo, TypeNames.JAKARTA_INJECT)) { + if (!isPicoSupported(serviceTypeName, methodInfo, logger())) { + return continuation; + } + + MethodParameterInfo[] params = methodInfo.getParameterInfo(); + if (params.length == 0) { + continuation = continuation.add(methodInfo.getName(), Void.class, kind, + toAccess(methodInfo.getModifiers())).staticDeclaration(isStatic(methodInfo.getModifiers())); + } else { + int count = 0; + for (MethodParameterInfo ignore : params) { + count++; + InjectionPointInfo ipInfo = createInjectionPointInfo(serviceTypeName, methodInfo, count); + continuation = continuation.add(ipInfo); + } + } + } + return continuation; + } + + static boolean isPicoSupported(TypeName serviceTypeName, + MethodInfo methodInfo, + System.Logger logger) { + return PicoSupported.isSupportedInjectionPoint(logger, serviceTypeName, methodInfo.toString(), + isPrivate(methodInfo.getModifiers()), methodInfo.isStatic()); + } + + ExternalModuleCreatorResponse handleError(ExternalModuleCreatorRequest request, + ToolsException e, + DefaultExternalModuleCreatorResponse.Builder builder) { + if (request == null || request.throwIfError()) { + throw e; + } + + logger().log(System.Logger.Level.ERROR, e.getMessage(), e); + + return builder.error(e).success(false).build(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java new file mode 100644 index 00000000000..7e1153d0162 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/DefaultInterceptorCreator.java @@ -0,0 +1,952 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.common.LazyValue; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.ElementInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.InterceptedTrigger; +import io.helidon.pico.Resettable; +import io.helidon.pico.ServiceInfoBasics; +import io.helidon.pico.tools.spi.InterceptorCreator; + +import io.github.classgraph.ClassInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultAnnotationAndValue.create; +import static io.helidon.pico.tools.TypeTools.createAnnotationAndValueFromMirror; +import static io.helidon.pico.tools.TypeTools.createAnnotationAndValueListFromAnnotations; +import static io.helidon.pico.tools.TypeTools.createAnnotationAndValueSet; +import static io.helidon.pico.tools.TypeTools.createMethodElementInfo; +import static io.helidon.pico.tools.TypeTools.gatherAllAnnotationsUsedOnPublicNonStaticMethods; +import static io.helidon.pico.tools.TypeTools.toKind; +import static io.helidon.pico.tools.TypeTools.toObjectTypeName; + +/** + * The default {@link io.helidon.pico.tools.spi.InterceptorCreator} provider in use. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT) +@SuppressWarnings("unchecked") +public class DefaultInterceptorCreator extends AbstractCreator implements InterceptorCreator, Resettable { + private static final LazyValue SCAN = LazyValue.create(ReflectionHandler.INSTANCE::scan); + + private static final String INTERCEPTOR_NAME_SUFFIX = "Interceptor"; + private static final String INNER_INTERCEPTOR_CLASS_NAME = "$$" + NAME_PREFIX + INTERCEPTOR_NAME_SUFFIX; + private static final String COMPLEX_INTERCEPTOR_HBS = "complex-interceptor.hbs"; + private static final double INTERCEPTOR_PRIORITY_DELTA = 0.001; + private static final String CTOR_ALIAS = "ctor"; + private static final Set ALLOW_LIST = new LinkedHashSet<>(); + + private Set allowListedAnnoTypeNames; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public DefaultInterceptorCreator() { + super(TemplateHelper.DEFAULT_TEMPLATE_NAME); + } + + @Override + public boolean reset(boolean deep) { + allowListedAnnoTypeNames = null; + return true; + } + + /** + * Sets the allow-listed annotation types triggering interception creation for the default interceptor creator. + * + * @param allowListedAnnotationTypes the allow-listed annotation types + * @return this instance + */ + public DefaultInterceptorCreator allowListedAnnotationTypes(Set allowListedAnnotationTypes) { + this.allowListedAnnoTypeNames = allowListedAnnotationTypes; + return this; + } + + @Override + public Set allowListedAnnotationTypes() { + return (allowListedAnnoTypeNames != null) ? allowListedAnnoTypeNames : Collections.emptySet(); + } + + @Override + public Optional createInterceptorPlan(ServiceInfoBasics interceptedService, + ProcessingEnvironment processEnv, + Set annotationTypeTriggers) { + return createInterceptorProcessor(interceptedService, this, Optional.of(processEnv)) + .createInterceptorPlan(annotationTypeTriggers); + } + + /** + * Abstract base for handling the resolution of annotation types by name. + */ + abstract static class AnnotationTypeNameResolver { + /** + * Determine the all the annotations belonging to a particular annotation type name. + * + * @param annoTypeName the annotation type name + * @return the list of (meta) annotations for the given annotation + */ + abstract Collection resolve(String annoTypeName); + } + + static class ProcessorResolver extends AnnotationTypeNameResolver { + private final Elements elements; + + ProcessorResolver(Elements elements) { + this.elements = elements; + } + + @Override + public Collection resolve(String annoTypeName) { + TypeElement typeElement = elements.getTypeElement(annoTypeName); + List annotations = typeElement.getAnnotationMirrors(); + Set result = annotations.stream() + .map(it -> createAnnotationAndValueFromMirror(it, elements).orElseThrow()) + .collect(Collectors.toSet()); + return result; + } + } + + static class ReflectionResolver extends AnnotationTypeNameResolver { + private final ScanResult scan; + + ReflectionResolver(ScanResult scan) { + this.scan = scan; + } + + @Override + public Collection resolve(String annoTypeName) { + ClassInfo classInfo = scan.getClassInfo(annoTypeName); + if (classInfo == null) { + try { + Class annotationType = (Class) Class.forName(annoTypeName); + return createAnnotationAndValueListFromAnnotations(annotationType.getAnnotations()); + } catch (ClassNotFoundException e) { + throw new ToolsException(e.getMessage(), e); + } + } + return createAnnotationAndValueSet(classInfo); + } + } + + /** + * Filter will apply the appropriate strategy determine which annotation types qualify as triggers for interception. + */ + @SuppressWarnings("checkstyle:VisibilityModifier") + abstract static class TriggerFilter { + /** + * The creator. + */ + protected final InterceptorCreator creator; + + /** + * The way to convert a string to the annotation type. + */ + protected final AnnotationTypeNameResolver resolver; + + /** + * the interceptor meta-annotation. + */ + public static final AnnotationAndValue TRIGGER = create(InterceptedTrigger.class); + + protected TriggerFilter() { + this.creator = null; + this.resolver = null; + } + + protected TriggerFilter(InterceptorCreator creator) { + this.creator = Objects.requireNonNull(creator); + this.resolver = null; + } + + protected TriggerFilter(InterceptorCreator creator, + AnnotationTypeNameResolver resolver) { + this.creator = Objects.requireNonNull(creator); + this.resolver = Objects.requireNonNull(resolver); + } + + /** + * Returns true if the annotation qualifies/triggers interceptor creation. + * + * @param annotationTypeName the annotation type name + * @return true if the annotation qualifies/triggers interceptor creation + */ + public boolean isQualifyingTrigger(String annotationTypeName) { + return (creator != null) && creator.isAllowListed(annotationTypeName); + } + } + + /** + * Enforces {@link Strategy#EXPLICIT}. + */ + private static class ExplicitStrategy extends TriggerFilter { + protected ExplicitStrategy(InterceptorCreator creator, + AnnotationTypeNameResolver resolver) { + super(creator, resolver); + } + + @Override + public boolean isQualifyingTrigger(String annotationTypeName) { + return resolver.resolve(annotationTypeName).contains(TRIGGER) + || TRIGGER.typeName().name().equals(annotationTypeName); + } + } + + /** + * Enforces {@link Strategy#ALL_RUNTIME}. + */ + private static class AllRuntimeStrategy extends TriggerFilter { + protected static final AnnotationAndValue RUNTIME = create(Retention.class, RetentionPolicy.RUNTIME.name()); + protected static final AnnotationAndValue CLASS = create(Retention.class, RetentionPolicy.CLASS.name()); + + protected AllRuntimeStrategy(InterceptorCreator creator, + AnnotationTypeNameResolver resolver) { + super(creator, resolver); + } + + @Override + public boolean isQualifyingTrigger(String annotationTypeName) { + Objects.requireNonNull(resolver); + Objects.requireNonNull(annotationTypeName); + return (resolver.resolve(annotationTypeName).contains(RUNTIME) + || resolver.resolve(annotationTypeName).contains(CLASS) + || ALLOW_LIST.contains(annotationTypeName)); + } + } + + /** + * Enforces {@link Strategy#ALLOW_LISTED}. + */ + private static class AllowListedStrategy extends TriggerFilter { + private final Set allowListed; + + protected AllowListedStrategy(InterceptorCreator creator) { + super(creator); + this.allowListed = Objects.requireNonNull(creator.allowListedAnnotationTypes()); + } + + @Override + public boolean isQualifyingTrigger(String annotationTypeName) { + Objects.requireNonNull(annotationTypeName); + return allowListed.contains(annotationTypeName) || ALLOW_LIST.contains(annotationTypeName); + } + } + + /** + * Enforces {@link Strategy#CUSTOM}. + */ + private static class CustomStrategy extends TriggerFilter { + protected CustomStrategy(InterceptorCreator creator) { + super(creator); + } + + @Override + public boolean isQualifyingTrigger(String annotationTypeName) { + Objects.requireNonNull(creator); + Objects.requireNonNull(annotationTypeName); + return (creator.isAllowListed(annotationTypeName) || ALLOW_LIST.contains(annotationTypeName)); + } + } + + /** + * Enforces {@link Strategy#NONE}. + */ + private static class NoneStrategy extends TriggerFilter { + } + + /** + * Enforces {@link Strategy#BLENDED}. + */ + private static class BlendedStrategy extends ExplicitStrategy { + private final CustomStrategy customStrategy; + + protected BlendedStrategy(InterceptorCreator creator, + AnnotationTypeNameResolver resolver) { + super(creator, resolver); + this.customStrategy = new CustomStrategy(creator); + } + + @Override + public boolean isQualifyingTrigger(String annotationTypeName) { + if (super.isQualifyingTrigger(annotationTypeName)) { + return true; + } + return customStrategy.isQualifyingTrigger(annotationTypeName); + } + } + + /** + * Returns the {@link TriggerFilter} appropriate for the given {@link InterceptorCreator}. + * + * @param creator the interceptor creator + * @param resolver the resolver, used in cases where the implementation needs to research more about a given annotation type + * @return the trigger filter instance + */ + private static TriggerFilter createTriggerFilter(InterceptorCreator creator, + AnnotationTypeNameResolver resolver) { + Strategy strategy = creator.strategy(); + if (Strategy.EXPLICIT == strategy) { + return new ExplicitStrategy(creator, resolver); + } else if (Strategy.ALL_RUNTIME == strategy) { + return new AllRuntimeStrategy(creator, resolver); + } else if (Strategy.ALLOW_LISTED == strategy) { + return new AllowListedStrategy(creator); + } else if (Strategy.CUSTOM == strategy) { + return new CustomStrategy(creator); + } else if (Strategy.NONE == strategy) { + return new NoneStrategy(); + } else if (Strategy.BLENDED == strategy || strategy == null) { + return new BlendedStrategy(creator, resolver); + } else { + throw new ToolsException("unknown strategy: " + strategy); + } + } + + /** + * Able to abstractly handle processing in annotation processing mode, or in reflection mode. + */ + @SuppressWarnings("checkstyle:VisibilityModifier") + abstract static class AbstractInterceptorProcessor implements InterceptorProcessor { + /** + * The service being intercepted/processed. + */ + final ServiceInfoBasics interceptedService; + + /** + * The "real" / delegate creator. + */ + final InterceptorCreator creator; + + private final AnnotationTypeNameResolver resolver; + private final TriggerFilter triggerFilter; + private final System.Logger logger; + + protected AbstractInterceptorProcessor(ServiceInfoBasics interceptedService, + InterceptorCreator realCreator, + AnnotationTypeNameResolver resolver, + System.Logger logger) { + this.creator = realCreator; + this.interceptedService = interceptedService; + this.resolver = resolver; + this.triggerFilter = createTriggerFilter(realCreator, resolver); + this.logger = logger; + } + + String serviceTypeName() { + return interceptedService.serviceTypeName(); + } + + /** + * @return the annotation resolver in use + */ + AnnotationTypeNameResolver resolver() { + return resolver; + } + + /** + * @return the trigger filter in use + */ + TriggerFilter triggerFilter() { + return triggerFilter; + } + + /** + * The set of annotation types that are trigger interception. + * + * @return the set of annotation types that are trigger interception + */ + @Override + public Set allAnnotationTypeTriggers() { + Set allAnnotations = getAllAnnotations(); + if (allAnnotations.isEmpty()) { + return Set.of(); + } + + TriggerFilter triggerFilter = triggerFilter(); + Set annotationTypeTriggers = allAnnotations.stream() + .filter(triggerFilter::isQualifyingTrigger) + .filter(anno -> !TriggerFilter.TRIGGER.typeName().name().equals(anno)) + .collect(Collectors.toSet()); + return annotationTypeTriggers; + } + + @Override + public Optional createInterceptorPlan(Set interceptorAnnotationTriggers) { + List interceptedElements = getInterceptedElements(interceptorAnnotationTriggers); + if (interceptedElements == null || interceptedElements.isEmpty()) { + return Optional.empty(); + } + + if (!hasNoArgConstructor()) { + ToolsException te = new ToolsException("there must be a no-arg constructor for: " + serviceTypeName()); + logger.log(System.Logger.Level.WARNING, "skipping interception for: " + serviceTypeName(), te); + return Optional.empty(); + } + + Set serviceLevelAnnotations = getServiceLevelAnnotations(); + InterceptionPlan plan = DefaultInterceptionPlan.builder() + .interceptedService(interceptedService) + .serviceLevelAnnotations(serviceLevelAnnotations) + .annotationTriggerTypeNames(interceptorAnnotationTriggers) + .interceptedElements(interceptedElements) + .build(); + return Optional.of(plan); + } + + /** + * @return the cumulative annotations referenced by this type + */ + abstract Set getAllAnnotations(); + + /** + * @return only the service level annotations referenced by this type + */ + abstract Set getServiceLevelAnnotations(); + + /** + * Intercepted classes must have a no-arg constructor (current restriction). + * + * @return true if there is a no-arg constructor present + */ + abstract boolean hasNoArgConstructor(); + + abstract List getInterceptedElements(Set interceptorAnnotationTriggers); + + boolean containsAny(Set annotations, + Set annotationTypeNames) { + assert (!annotationTypeNames.isEmpty()); + for (AnnotationAndValue annotation : annotations) { + if (annotationTypeNames.contains(annotation.typeName().name())) { + return true; + } + } + return false; + } + + boolean isProcessed(InjectionPointInfo.ElementKind kind, + int methodArgCount, + Set modifiers, + Boolean isPrivate, + Boolean isStatic) { + assert (ElementInfo.ElementKind.CONSTRUCTOR == kind || InjectionPointInfo.ElementKind.METHOD == kind) + : kind + " in:" + serviceTypeName(); + + if (modifiers != null) { + if (modifiers.contains(Modifier.STATIC)) { + return false; + } + + if (modifiers.contains(Modifier.PRIVATE)) { + return false; + } + } else { + if (isPrivate) { + return false; + } + + if (isStatic) { + return false; + } + } + + if (ElementInfo.ElementKind.CONSTRUCTOR == kind && methodArgCount != 0) { + return false; + } + + return true; + } + } + + private static class ProcessorBased extends AbstractInterceptorProcessor { + private final TypeElement serviceTypeElement; + private final ProcessingEnvironment processEnv; + + ProcessorBased(ServiceInfoBasics interceptedService, + InterceptorCreator realCreator, + ProcessingEnvironment processEnv, + System.Logger logger) { + super(interceptedService, realCreator, createResolverFromProcessor(processEnv), logger); + this.serviceTypeElement = Objects + .requireNonNull(processEnv.getElementUtils().getTypeElement(serviceTypeName())); + this.processEnv = processEnv; + } + + @Override + public Set getAllAnnotations() { + Set set = gatherAllAnnotationsUsedOnPublicNonStaticMethods(serviceTypeElement, processEnv); + return set.stream().map(a -> a.typeName().name()).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public Set getServiceLevelAnnotations() { + return createAnnotationAndValueSet(serviceTypeElement); + } + + @Override + public boolean hasNoArgConstructor() { + return serviceTypeElement.getEnclosedElements().stream() + .filter(it -> it.getKind().equals(ElementKind.CONSTRUCTOR)) + .map(ExecutableElement.class::cast) + .anyMatch(it -> it.getParameters().isEmpty()); + } + + @Override + protected List getInterceptedElements(Set interceptorAnnotationTriggers) { + List result = new ArrayList<>(); + Set serviceLevelAnnos = getServiceLevelAnnotations(); + serviceTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.METHOD || e.getKind() == ElementKind.CONSTRUCTOR) + .map(ExecutableElement.class::cast) + .filter(e -> isProcessed(toKind(e), e.getParameters().size(), e.getModifiers(), null, null)) + .forEach(ee -> result.add(create(ee, serviceLevelAnnos, interceptorAnnotationTriggers))); + return result; + } + + private InterceptedElement create(ExecutableElement ee, + Set serviceLevelAnnos, + Set interceptorAnnotationTriggers) { + MethodElementInfo elementInfo = createMethodElementInfo(serviceTypeElement, ee, serviceLevelAnnos); + Set applicableTriggers = new LinkedHashSet<>(interceptorAnnotationTriggers); + applicableTriggers.retainAll(elementInfo.annotations().stream() + .map(a -> a.typeName().name()).collect(Collectors.toSet())); + return DefaultInterceptedElement.builder() + .interceptedTriggerTypeNames(applicableTriggers) + .elementInfo(elementInfo) + .build(); + } + } + + private static class ReflectionBased extends AbstractInterceptorProcessor { + private final ClassInfo classInfo; + + ReflectionBased(ServiceInfoBasics interceptedService, + InterceptorCreator realCreator, + ClassInfo classInfo, + System.Logger logger) { + super(/*serviceTypeName,*/ interceptedService, realCreator, createResolverFromReflection(), logger); + this.classInfo = classInfo; + } + + @Override + public Set getAllAnnotations() { + Set set = gatherAllAnnotationsUsedOnPublicNonStaticMethods(classInfo); + return set.stream().map(a -> a.typeName().name()).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public Set getServiceLevelAnnotations() { + return createAnnotationAndValueSet(classInfo); + } + + @Override + public boolean hasNoArgConstructor() { + return classInfo.getConstructorInfo().stream() + .filter(mi -> !mi.isPrivate()) + .anyMatch(mi -> mi.getParameterInfo().length == 0); + } + + @Override + protected List getInterceptedElements(Set interceptorAnnotationTriggers) { + List result = new ArrayList<>(); + Set serviceLevelAnnos = getServiceLevelAnnotations(); + classInfo.getMethodAndConstructorInfo() + .filter(m -> isProcessed(toKind(m), + m.getParameterInfo().length, null, m.isPrivate(), m.isStatic())) + .filter(m -> containsAny(serviceLevelAnnos, interceptorAnnotationTriggers) + || containsAny(createAnnotationAndValueSet( + m.getAnnotationInfo()), interceptorAnnotationTriggers)) + .forEach(mi -> result.add(create(mi, serviceLevelAnnos, interceptorAnnotationTriggers))); + return result; + } + + private InterceptedElement create(MethodInfo mi, + Set serviceLevelAnnos, + Set interceptorAnnotationTriggers) { + MethodElementInfo elementInfo = createMethodElementInfo(mi, serviceLevelAnnos); + Set applicableTriggers = new LinkedHashSet<>(interceptorAnnotationTriggers); + applicableTriggers.retainAll(elementInfo.annotations().stream() + .map(a -> a.typeName().name()).collect(Collectors.toSet())); + return DefaultInterceptedElement.builder() + .interceptedTriggerTypeNames(applicableTriggers) + .elementInfo(elementInfo) + .build(); + } + } + + /** + * Create an annotation resolver based on annotation processing. + * + * @param processEnv the processing env + * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AnnotationTypeNameResolver} to use + */ + static AnnotationTypeNameResolver createResolverFromProcessor(ProcessingEnvironment processEnv) { + return new ProcessorResolver(processEnv.getElementUtils()); + } + + /** + * Create an annotation resolver based on reflective processing. + * + * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AnnotationTypeNameResolver} to use + */ + static AnnotationTypeNameResolver createResolverFromReflection() { + return new ReflectionResolver(SCAN.get()); + } + + @Override + public AbstractInterceptorProcessor createInterceptorProcessor(ServiceInfoBasics interceptedService, + InterceptorCreator delegateCreator, + Optional processEnv) { + if (processEnv.isPresent()) { + return createInterceptorProcessorFromProcessor(interceptedService, delegateCreator, processEnv.get()); + } + return createInterceptorProcessorFromReflection(interceptedService, delegateCreator); + } + + + /** + * Create an interceptor processor based on annotation processing. + * + * @param interceptedService the service being processed + * @param realCreator the real/delegate creator + * @param processEnv the processing env + * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AbstractInterceptorProcessor} to use + */ + AbstractInterceptorProcessor createInterceptorProcessorFromProcessor(ServiceInfoBasics interceptedService, + InterceptorCreator realCreator, + ProcessingEnvironment processEnv) { + Options.init(processEnv); + ALLOW_LIST.addAll(Options.getOptionStringList(Options.TAG_ALLOW_LISTED_INTERCEPTOR_ANNOTATIONS)); + return new ProcessorBased(Objects.requireNonNull(interceptedService), + Objects.requireNonNull(realCreator), + Objects.requireNonNull(processEnv), + logger()); + } + + /** + * Create an interceptor processor based on reflection processing. + * + * @param interceptedService the service being processed + * @param realCreator the real/delegate creator + * @return the {@link io.helidon.pico.tools.DefaultInterceptorCreator.AbstractInterceptorProcessor} to use + */ + AbstractInterceptorProcessor createInterceptorProcessorFromReflection(ServiceInfoBasics interceptedService, + InterceptorCreator realCreator) { + return new ReflectionBased(Objects.requireNonNull(interceptedService), + Objects.requireNonNull(realCreator), + Objects.requireNonNull(SCAN.get().getClassInfo(interceptedService.serviceTypeName())), + logger()); + } + + /** + * Creates the interceptor source code type name given its plan. + * + * @param plan the plan + * @return the interceptor type name + */ + static TypeName createInterceptorSourceTypeName(InterceptionPlan plan) { + String parent = plan.interceptedService().serviceTypeName(); + return toInterceptorTypeName(parent); + } + + /** + * Creates the source code associated with an interception plan. + * + * @param plan the plan + * @return the java source code body + */ + String createInterceptorSourceBody(InterceptionPlan plan) { + String parent = plan.interceptedService().serviceTypeName(); + TypeName interceptorTypeName = toInterceptorTypeName(parent); + Map subst = new HashMap<>(); + subst.put("packageName", interceptorTypeName.packageName()); + subst.put("className", interceptorTypeName.className()); + subst.put("parent", parent); + subst.put("header", BuilderTypeTools.copyrightHeaderFor(getClass().getName())); + subst.put("generatedanno", toGeneratedSticker(null)); + subst.put("weight", interceptorWeight(plan.interceptedService().declaredWeight())); + subst.put("interceptedmethoddecls", toInterceptedMethodDecls(plan)); + subst.put("interceptedelements", IdAndToString + .toList(plan.interceptedElements(), DefaultInterceptorCreator::toBody).stream() + .filter(it -> !it.getId().equals(CTOR_ALIAS)) + .collect(Collectors.toList())); + subst.put("annotationtriggertypenames", IdAndToString + .toList(plan.annotationTriggerTypeNames(), + str -> new IdAndToString(str.replace(".", "_"), str))); + subst.put("servicelevelannotations", IdAndToString + .toList(plan.serviceLevelAnnotations(), DefaultInterceptorCreator::toDecl)); + String template = templateHelper().safeLoadTemplate(COMPLEX_INTERCEPTOR_HBS); + return templateHelper().applySubstitutions(template, subst, true).trim(); + } + + private static List toInterceptedMethodDecls(InterceptionPlan plan) { + List result = new ArrayList<>(); + for (InterceptedElement element : plan.interceptedElements()) { + IdAndToString methodTypedElement = toDecl(element); + result.add(methodTypedElement); + + if (element.elementInfo().elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) { + continue; + } + + for (ElementInfo param : element.elementInfo().parameterInfo()) { + IdAndToString paramTypedElement = new IdAndToString(element.elementInfo().elementName() + + "__" + param.elementName(), + typeNameElementNameAnnotations(param)); + result.add(paramTypedElement); + } + } + return result; + } + + private static IdAndToString toDecl(InterceptedElement method) { + MethodElementInfo mi = method.elementInfo(); + String name = (mi.elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) ? CTOR_ALIAS : mi.elementName(); + String builder = typeNameElementNameAnnotations(mi); + return new IdAndToString(name, builder); + } + + private static String typeNameElementNameAnnotations(ElementInfo ei) { + StringBuilder builder = new StringBuilder(".typeName(create(" + ei.elementTypeName() + ".class))"); + builder.append("\n\t\t\t.elementName(").append(CodeGenUtils.elementNameRef(ei.elementName())).append(")"); + TreeSet sortedAnnotations = new TreeSet<>(ei.annotations()); + for (AnnotationAndValue anno : sortedAnnotations) { + builder.append("\n\t\t\t.addAnnotation(").append(toDecl(anno)).append(")"); + } + return builder.toString(); + } + + private static IdAndToString toDecl(ElementInfo elementInfo) { + String name = elementInfo.elementName(); + return new IdAndToString(name, elementInfo.elementTypeName() + " " + name); + } + + private static IdAndToString toDecl(AnnotationAndValue anno) { + StringBuilder builder = new StringBuilder("DefaultAnnotationAndValue.create(" + anno.typeName() + ".class"); + Map map = anno.values(); + String val = anno.value().orElse(null); + if (map != null && !map.isEmpty()) { + builder.append(", Map.of("); + int count = 0; + TreeMap sortedMap = new TreeMap<>(map); + for (Map.Entry e : sortedMap.entrySet()) { + if (count++ > 0) { + builder.append(", "); + } + builder.append("\"") + .append(e.getKey()) + .append("\", \"") + .append(e.getValue()) + .append("\""); + } + builder.append(")"); + } else if (val != null) { + builder.append(", \"") + .append(val) + .append("\""); + } + builder.append(")"); + return new IdAndToString(anno.typeName().name(), builder); + } + + @SuppressWarnings("checkstyle:OperatorWrap") + private static InterceptedMethodCodeGen toBody(InterceptedElement method) { + MethodElementInfo mi = method.elementInfo(); + String name = (mi.elementKind() == ElementInfo.ElementKind.CONSTRUCTOR) ? CTOR_ALIAS : mi.elementName(); + StringBuilder builder = new StringBuilder(); + builder.append("public ").append(mi.elementTypeName()).append(" ").append(mi.elementName()).append("("); + String args = mi.parameterInfo().stream().map(ElementInfo::elementName).collect(Collectors.joining(", ")); + String argDecls = ""; + String objArrayArgs = ""; + String typedElementArgs = ""; + String untypedElementArgs = ""; + boolean hasArgs = (args.length() > 0); + if (hasArgs) { + argDecls = mi.parameterInfo().stream() + .map(DefaultInterceptorCreator::toDecl) + .map(IdAndToString::toString) + .collect(Collectors.joining(", ")); + AtomicInteger count = new AtomicInteger(); + objArrayArgs = mi.parameterInfo().stream() + .map(ElementInfo::elementTypeName) + .map(TypeTools::toObjectTypeName) + .map(typeName -> "(" + typeName + ") " + "args[" + count.getAndIncrement() + "]") + .collect(Collectors.joining(", ")); + + typedElementArgs = mi.parameterInfo().stream() + .map(ei -> "__" + mi.elementName() + "__" + ei.elementName()) + .collect(Collectors.joining(", ")); + + count.set(0); + untypedElementArgs = mi.parameterInfo().stream() + .map(ei -> "args[" + count.getAndIncrement() + "]") + .collect(Collectors.joining(", ")); + } + + boolean hasReturn = !mi.elementTypeName().equals(void.class.getName()); + builder.append(argDecls); + builder.append(")"); + if (!mi.throwableTypeNames().isEmpty()) { + builder.append(" throws ").append(CommonUtils.toString(mi.throwableTypeNames())); + } + String methodDecl = builder.toString(); + builder.append(" {\n"); + TypeName supplierType = (hasReturn) ? toObjectTypeName(mi.elementTypeName()) : DefaultTypeName.create(Void.class); + + String elementArgInfo = ""; + if (hasArgs) { + elementArgInfo = ",\n\t\t\t\tnew TypedElementName[] {" + typedElementArgs + "}"; + } + return new InterceptedMethodCodeGen(name, methodDecl, hasReturn, supplierType, elementArgInfo, args, + objArrayArgs, untypedElementArgs, + method.interceptedTriggerTypeNames(), builder); + } + + private static TypeName toInterceptorTypeName(String serviceTypeName) { + TypeName typeName = DefaultTypeName.createFromTypeName(serviceTypeName); + return DefaultTypeName.create(typeName.packageName(), typeName.className() + + INNER_INTERCEPTOR_CLASS_NAME); + } + + /** + * Assign a weight slightly higher than the service weight passed in. + * + * @param serviceWeight the service weight, where null defaults to {@link io.helidon.common.Weighted#DEFAULT_WEIGHT}. + * @return the higher weighted value appropriate for interceptors + */ + static double interceptorWeight(Optional serviceWeight) { + double val = serviceWeight.orElse(Weighted.DEFAULT_WEIGHT); + return val + INTERCEPTOR_PRIORITY_DELTA; + } + + + static class InterceptedMethodCodeGen extends IdAndToString { + private final String methodDecl; + private final boolean hasReturn; + private final TypeName elementTypeName; + private final String elementArgInfo; + private final String args; + private final String objArrayArgs; + private final String untypedElementArgs; + private final String interceptedTriggerTypeNames; + + InterceptedMethodCodeGen(String id, + String methodDecl, + boolean hasReturn, + TypeName elementTypeName, + String elementArgInfo, + String args, + String objArrayArgs, + String untypedElementArgs, + Collection interceptedTriggerTypeNames, + Object toString) { + super(id, toString); + this.methodDecl = methodDecl; + this.hasReturn = hasReturn; + this.elementTypeName = elementTypeName; + this.elementArgInfo = elementArgInfo; + this.args = args; + this.objArrayArgs = objArrayArgs; + this.untypedElementArgs = untypedElementArgs; + this.interceptedTriggerTypeNames = CommonUtils.toString(interceptedTriggerTypeNames, + (str) -> str.replace(".", "_"), null); + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getMethodDecl() { + return methodDecl; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public TypeName getElementTypeName() { + return elementTypeName; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getElementArgInfo() { + return elementArgInfo; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getArgs() { + return args; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getObjArrayArgs() { + return objArrayArgs; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getUntypedElementArgs() { + return untypedElementArgs; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public boolean getHasReturn() { + return hasReturn; + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public boolean getHasArgs() { + return !args.isEmpty(); + } + + // note: this needs to stay as a public getXXX() method to support Mustache + public String getInterceptedTriggerTypeNames() { + return interceptedTriggerTypeNames; + } + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorProvider.java b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorProvider.java new file mode 100644 index 00000000000..d1c0e2775a5 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.pico.tools.spi.ExternalModuleCreator; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Provides access to the global singleton {@link io.helidon.pico.tools.spi.ExternalModuleCreator} in use. + */ +@Singleton +public class ExternalModuleCreatorProvider implements Provider { + private static final LazyValue INSTANCE = LazyValue.create(ExternalModuleCreatorProvider::load); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ExternalModuleCreatorProvider() { + } + + private static ExternalModuleCreator load() { + return HelidonServiceLoader.create( + ServiceLoader.load(ExternalModuleCreator.class, ExternalModuleCreator.class.getClassLoader())) + .asList() + .stream() + .findFirst().orElseThrow(); + } + + // note that this is guaranteed to succeed since the default implementation is in this module + @Override + public ExternalModuleCreator get() { + return INSTANCE.get(); + } + + /** + * Returns the global instance that was service loaded. Note that this call is guaranteed to return a result since the + * default implementation is here in this module. + * + * @return the global service instance with the highest weight + */ + public static ExternalModuleCreator instance() { + return INSTANCE.get(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorRequest.java new file mode 100644 index 00000000000..01c6872374b --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorRequest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.pico.QualifierAndValue; + +/** + * The request payload that is used by {@link io.helidon.pico.tools.spi.ExternalModuleCreator}. + *

        + * Note that the thread context classloader should be setup appropriately so that service types can be resolved + * based upon the packages requested to scan. + */ +@Builder +public interface ExternalModuleCreatorRequest extends GeneralCreatorRequest { + + /** + * The set of packages to analyze and eventually generate pico activators against. + * + * @return the list of package names to analyze and target for activator creation + */ + @Singular + List packageNamesToScan(); + + /** + * Optionally, provides a means to map additional qualifiers to service types. + * + * @return any qualifiers that should be mapped into the generated services + */ + @Singular + Map> serviceTypeToQualifiersMap(); + + /** + * Config options w.r.t. planned activator creation. + * + * @return the config options for activator creation + */ + ActivatorCreatorConfigOptions activatorCreatorConfigOptions(); + + /** + * Optionally, set this to allow inner classes to be processed for potential pico activators. + * + * @return allows inner classes to be processed for potential pico activators + */ + boolean innerClassesProcessed(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorResponse.java new file mode 100644 index 00000000000..0524bb15b6f --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ExternalModuleCreatorResponse.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.builder.Builder; + +/** + * The response from {@link io.helidon.pico.tools.spi.ExternalModuleCreator}. + *

        + * The response, if successful, will contribute to the {@link ActivatorCreatorRequest} + * passed to {@link io.helidon.pico.tools.spi.ActivatorCreator} in any next phase of creation for the external Pico module. + */ +@Builder +public interface ExternalModuleCreatorResponse extends GeneralCreatorResponse { + + /** + * The activator creator request. + * + * @return the activator creator request + */ + ActivatorCreatorRequest activatorCreatorRequest(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenDetail.java b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenDetail.java new file mode 100644 index 00000000000..29580a9c96c --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenDetail.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; + +/** + * Generically describes the source code generated. + */ +@Builder +public interface GeneralCodeGenDetail { + + /** + * The FQN of the source that was generated. + * + * @return the FQN name for the code that was generated + */ + TypeName serviceTypeName(); + + /** + * Optionally, the source code generated. + * + * @return the body of the source, or empty if unknown or unavailable + */ + Optional body(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenNames.java b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenNames.java new file mode 100644 index 00000000000..0026c822cfa --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCodeGenNames.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * General code gen information. + */ +@Builder +public interface GeneralCodeGenNames { + + /** + * Optionally, the name of the template to apply, defaulting to "default". + * + * @return the template name that should be used + */ + @ConfiguredOption("default") + String templateName(); + + /** + * The module name, defaulting to "unnamed" if not specified. + * This name is used primarily to serve as the codegen name for the {@link io.helidon.pico.Module} that is generated. + * + * @return module name + */ +// @ConfiguredOption("unnamed") + Optional moduleName(); + + /** + * The package name to use for the generated {@link io.helidon.pico.Module}, {@link io.helidon.pico.Application}, etc. + * If one is not provided, one will be determined internally. + * + * @return the suggested package name, otherwise passing null will delegate package naming to the implementation heuristic + */ + Optional packageName(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java new file mode 100644 index 00000000000..e317f56b49e --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorRequest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Base interface codegen-related requests. + */ +@Builder +public interface GeneralCreatorRequest extends GeneralCodeGenNames { + + /** + * Set to true to avoid code-generating, and instead provide the plan for what would be built. + * + * @return if set to true then no codegen will occur on disk + */ + boolean analysisOnly(); + + /** + * Where codegen should be read and written. + * + * @return the code paths to use for reading and writing artifacts + */ + CodeGenPaths codeGenPaths(); + + /** + * Optionally, any compiler options to pass explicitly to the java compiler. Not applicable during annotation processing. + * + * @return explicit compiler options + */ + Optional compilerOptions(); + + /** + * The target fully qualified class name for the service implementation to be built or analyzed. + *

        + * Assumptions: + *

          + *
        • The service type is available for reflection/introspection at creator invocation time (typically at + * compile time). + *
        + * + * @return the collection of service type names to generate + */ + List serviceTypeNames(); + + /** + * Should exceptions be thrown, or else captured in the response under {@link ActivatorCreatorResponse#error()}. + * The default is true. + * + * @return true if the creator should fail, otherwise the response will show the error + */ + @ConfiguredOption("true") + boolean throwIfError(); + + /** + * Provides the generator (used to append to code generated artifacts in {@code javax.annotation.processing.Generated} + * annotations). + * + * @return the generator name + */ + Optional generator(); + + /** + * The code gen filer to use. + * + * @return the code gen filer + */ + CodeGenFiler filer(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorResponse.java new file mode 100644 index 00000000000..797e8ddd476 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GeneralCreatorResponse.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.TypeName; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * General base interface for any codegen-related create activity. + */ +@Builder +public interface GeneralCreatorResponse extends GeneralCodeGenNames { + + /** + * Flag to indicate a success or failure. + * + * @return success flag + */ + @ConfiguredOption("true") + boolean success(); + + /** + * Any error that was caught during processing. + * + * @return any error that was thrown + */ + Optional error(); + + // java source related ... + + /** + * The services that were generated. + * + * @return the services that were generated + */ + List serviceTypeNames(); + + /** + * The detailed information generated for those service type involved in code generation. + * + * @return map of service type names to generated details + */ + @Singular + Map serviceTypeDetails(); + + // resources related ... + + /** + * The META-INF services entries. + * + * @return the META-INF services entries + */ + Map> metaInfServices(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreator.java new file mode 100644 index 00000000000..2fe637a736f --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Optional; + +/** + * Tools to assist with using {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator}'s. + */ +public interface GenericTemplateCreator { + + /** + * Convenience method to help with the typical/generic case where the request + the provided generatedType + * is injected into the supplied template to produce the response. + * + * @param request the generic template creator request + * + * @return the response, or empty if the template can not be generated for this case + */ + Optional create(GenericTemplateCreatorRequest request); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreatorRequest.java new file mode 100644 index 00000000000..24bacb011cb --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/GenericTemplateCreatorRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Map; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; + +/** + * This builder represents the request arguments to pass to the {@link GenericTemplateCreator}. + */ +@Builder +public interface GenericTemplateCreatorRequest { + + /** + * The custom annotation template request. + * + * @return the custom annotation template request + */ + CustomAnnotationTemplateRequest customAnnotationTemplateRequest(); + + /** + * The type name that should will be code generated. + * + * @return the type name that will be code generated + */ + TypeName generatedTypeName(); + + /** + * The (mustache / handlebars) template. + * + * @return the (mustache / handlebars) template + */ + CharSequence template(); + + /** + * The overriding properties to apply that will supersede the default values that are specified below. + *
          + *
        • properties.put("generatedSticker", {generated-sticker}); + *
        • properties.put("generatedTypeName", req.getGeneratedTypeName().getName()); + *
        • properties.put("annoTypeName", TypeNameImpl.toName(req.getAnnoType())); + *
        • properties.put("packageName", req.getGeneratedTypeName().getPackageName()); + *
        • properties.put("className", req.getGeneratedTypeName().getClassName()); + *
        • properties.put("enclosingClassTypeName", req.getEnclosingClassType().getName()); + *
        • properties.put("enclosingClassAnnotations", req.getEnclosingClassAnnotations()); + *
        • properties.put("basicServiceInfo", req.getBasicServiceInfo()); + *
        • properties.put("weight", DefaultServiceInfo.weightOf(req.getBasicServiceInfo()); + *
        • properties.put("enclosingClassTypeName.packageName", req.getEnclosingClassType().getPackageName()); + *
        • properties.put("enclosingClassTypeName.className", req.getEnclosingClassType().getClassName()); + *
        • properties.put("elementKind", req.getElementKind()); + *
        • properties.put("elementName", req.getElementName()); + *
        • properties.put("elementAccess", req.getElementAccess()); + *
        • properties.put("elementIsStatic", req.isElementStatic()); + *
        • properties.put("elementEnclosingTypeName", req.getElementEnclosingType().getName()); + *
        • properties.put("elementEnclosingTypeName.packageName", req.getElementEnclosingType().getPackageName()); + *
        • properties.put("elementEnclosingTypeName.className", req.getElementEnclosingType().getClassName()); + *
        • properties.put("elementArgs", req.getElementArgs()); + *
        • properties.put("elementArgs-declaration", req.getElementArgs()); + *
        + * + * @return the overriding properties to apply + */ + Map overrideProperties(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/IdAndToString.java b/pico/tools/src/main/java/io/helidon/pico/tools/IdAndToString.java new file mode 100644 index 00000000000..ff51a1e95b5 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/IdAndToString.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Convenience for use with handlebars to offer {{id}} that is different from {{.}}. + */ +class IdAndToString { + + private final String id; + private final Object toString; + + /** + * Constructor. + * + * @param id the id + * @param toString the toString value + */ + IdAndToString(String id, + Object toString) { + this.id = Objects.requireNonNull(id); + this.toString = toString; + } + + /** + * Returns the id. + * + * @return the id + */ + // note that this is called from Mustache, so it needs to be bean-style named! + public String getId() { + return id; + } + + @Override + public String toString() { + return String.valueOf(toString); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public boolean equals(Object another) { + if (!(another instanceof IdAndToString)) { + return false; + } + return getId().equals(((IdAndToString) another).getId()); + } + + /** + * Creates a list of {@link IdAndToString} from a list of T's. + * + * @param list the list to convert + * @param toId the function that will create the {@link IdAndToString} + * @param the type of the list + * @return the converted list + */ + static List toList(Collection list, + Function toId) { + if (list == null) { + return null; + } + + return list.stream() + .map(toId) + .collect(Collectors.toList()); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptedElement.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptedElement.java new file mode 100644 index 00000000000..54839633680 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptedElement.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Set; + +import io.helidon.builder.Builder; + +/** + * Used in the interception model described by {@link InterceptionPlan}. An intercepted + * element typically refers to a {@link io.helidon.pico.ElementInfo.ElementKind#CONSTRUCTOR} or + * {@link io.helidon.pico.ElementInfo.ElementKind#METHOD} that qualifies for interception. If, however, + * the {@link io.helidon.pico.InterceptedTrigger} is applied on the enclosing service type then all public methods. + * Note that only public methods on pico-activated services can be intercepted. + */ +@Builder +public interface InterceptedElement { + + /** + * The set of {@link io.helidon.pico.InterceptedTrigger} types that apply to this method/element. + * + * @return the set of intercepted trigger types that apply to this method/element + */ + Set interceptedTriggerTypeNames(); + + /** + * The method element info for this intercepted method. + * + * @return the method element info for this intercepted method + */ + MethodElementInfo elementInfo(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java new file mode 100644 index 00000000000..986c1bce904 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptionPlan.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.pico.ServiceInfoBasics; + +/** + * Once a service type qualifies for interception, the interception plan will be created describing how the service type + * should be intercepted. + */ +@Builder +public interface InterceptionPlan { + + /** + * The intercepted service. + * + * @return the intercepted service + */ + ServiceInfoBasics interceptedService(); + + /** + * Annotations at the service type level. + * + * @return annotations at the service type level + */ + Set serviceLevelAnnotations(); + + /** + * All the annotation names that contributed to triggering this interceptor plan. + * + * @return all the annotation names that contributed to triggering this interceptor plan + */ + Set annotationTriggerTypeNames(); + + /** + * The list of elements that should be intercepted. + * + * @return the list of elements that should be intercepted + */ + List interceptedElements(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorProvider.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorProvider.java new file mode 100644 index 00000000000..c9a93725837 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.pico.tools.spi.InterceptorCreator; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Provides access to the global singleton {@link io.helidon.pico.tools.spi.InterceptorCreator} in use. + */ +@Singleton +public class InterceptorCreatorProvider implements Provider { + private static final LazyValue INSTANCE = LazyValue.create(InterceptorCreatorProvider::load); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public InterceptorCreatorProvider() { + } + + private static InterceptorCreator load() { + return HelidonServiceLoader.create( + ServiceLoader.load(InterceptorCreator.class, InterceptorCreator.class.getClassLoader())) + .asList() + .stream() + .findFirst().orElseThrow(); + } + + // note that this is guaranteed to succeed since the default implementation is in this module + @Override + public InterceptorCreator get() { + return INSTANCE.get(); + } + + /** + * Returns the global instance that was service loaded. Note that this call is guaranteed to return a result since the + * default implementation is here in this module. + * + * @return the global service instance with the highest weight + */ + public static InterceptorCreator instance() { + return INSTANCE.get(); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorResponse.java new file mode 100644 index 00000000000..d8e4d1aa85a --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorResponse.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.nio.file.Path; +import java.util.Map; + +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.common.types.TypeName; + +/** + * Response from interception creation. + */ +@Builder +public interface InterceptorCreatorResponse { + + /** + * The generated files. + * + * @return the generated files + */ + @Singular + Map generatedFiles(); + + /** + * The interception plans. + * + * @return the interception plans + */ + Map interceptionPlans(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/JavaC.java b/pico/tools/src/main/java/io/helidon/pico/tools/JavaC.java new file mode 100644 index 00000000000..445353b11f8 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/JavaC.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +/** + * Simple wrapper for compilation, and capturing diagnostic output. + */ +class JavaC { + private static final System.Logger LOGGER = System.getLogger(JavaC.class.getName()); + + private final List classpath = new ArrayList<>(); + private final List sourcepath = new ArrayList<>(); + private final List modulepath = new ArrayList<>(); + private final List commandLineArgs = new ArrayList<>(); + private String source = AbstractCreator.DEFAULT_SOURCE; + private String target = AbstractCreator.DEFAULT_TARGET; + private File outputDirectory; + private System.Logger logger = LOGGER; + private Messager messager; + + /** + * @return The fluent builder for eventual compilation + */ + static Builder builder() { + JavaC compiler = new JavaC(); + return compiler.new Builder(); + } + + /** + * Terminates the builder by triggering compilation. + * + * @param applicationJavaFile the java file to compile + * @return the result of the compilation + */ + Result compile(File applicationJavaFile) { + return new Result(applicationJavaFile); + } + + String toClasspath() { + if (!classpath.isEmpty()) { + return CommonUtils.toPathString(classpath); + } + return null; + } + + String toSourcepath() { + if (!sourcepath.isEmpty()) { + return CommonUtils.toPathString(sourcepath); + } + return null; + } + + String toModulePath() { + if (!modulepath.isEmpty()) { + return CommonUtils.toPathString(modulepath); + } + return null; + } + + + class Builder { + private boolean closed; + + private Builder() { + } + + Builder outputDirectory(File outputDirectory) { + assert (!closed); + JavaC.this.outputDirectory = outputDirectory; + return this; + } + + Builder classpath(List classpath) { + assert (!closed); + JavaC.this.classpath.clear(); + JavaC.this.classpath.addAll(classpath); + return this; + } + + Builder sourcepath(List sourcepath) { + assert (!closed); + JavaC.this.sourcepath.clear(); + JavaC.this.sourcepath.addAll(sourcepath); + return this; + } + + Builder modulepath(List modulepath) { + assert (!closed); + JavaC.this.modulepath.clear(); + JavaC.this.modulepath.addAll(modulepath); + return this; + } + + Builder source(String source) { + assert (!closed); + JavaC.this.source = (source == null) ? AbstractCreator.DEFAULT_SOURCE : source; + return this; + } + + Builder target(String target) { + assert (!closed); + JavaC.this.target = (target == null) ? AbstractCreator.DEFAULT_TARGET : target; + return this; + } + + Builder commandLineArgs(List commandLineArgs) { + assert (!closed); + JavaC.this.commandLineArgs.clear(); + JavaC.this.commandLineArgs.addAll(commandLineArgs); + return this; + } + + Builder logger(System.Logger logger) { + assert (!closed); + JavaC.this.logger = logger; + return this; + } + + Builder messager(Messager messager) { + assert (!closed); + JavaC.this.messager = messager; + return this; + } + + JavaC build() { + assert (outputDirectory == null || outputDirectory.exists()); + closed = true; + return JavaC.this; + } + } + + @SuppressWarnings("rawtypes") + class Result implements DiagnosticListener { + private final List> diagList = new ArrayList<>(); + private boolean isSuccessful = true; + private boolean hasWarnings = false; + + @SuppressWarnings("unchecked") + private Result(File applicationJavaFile) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(this, null, null); + + List optionList = new ArrayList<>(); + if (!classpath.isEmpty()) { + optionList.add("-classpath"); + optionList.add(toClasspath()); + } + if (!modulepath.isEmpty()) { + optionList.add("--module-path"); + optionList.add(toModulePath()); + } + if (!sourcepath.isEmpty()) { + optionList.add("--source-path"); + optionList.add(toSourcepath()); + } + if (source != null) { + optionList.add("--source"); + optionList.add(source); + } + if (target != null) { + optionList.add("--target"); + optionList.add(target); + } + optionList.addAll(commandLineArgs); + if (outputDirectory != null) { + optionList.add("-d"); + optionList.add(outputDirectory.getPath()); + } + + List filesToCompile = new ArrayList<>(); + filesToCompile.add(applicationJavaFile); + if (!modulepath.isEmpty()) { + modulepath.forEach(path -> { + File pathToPossibleModuleInfo = new File(path.toFile(), ModuleUtils.REAL_MODULE_INFO_JAVA_NAME); + if (pathToPossibleModuleInfo.exists()) { + filesToCompile.add(pathToPossibleModuleInfo); + } + }); + } + + Iterable compilationUnit = fileManager + .getJavaFileObjectsFromFiles(filesToCompile); + JavaCompiler.CompilationTask task = compiler + .getTask(null, fileManager, this, optionList, null, compilationUnit); + + if (messager != null) { + messager.debug("javac " + CommonUtils.toString(optionList, null, " ") + " " + applicationJavaFile); + } + + Boolean result = task.call(); + // we do it like this to allow for warnings to be treated as errors + if (result != null && !result) { + isSuccessful = false; + } + } + + public boolean isSuccessful() { + return isSuccessful; + } + + @SuppressWarnings("unused") + public boolean hasWarnings() { + return hasWarnings; + } + + public ToolsException maybeGenerateError() { + if (!isSuccessful()) { + return new ToolsException("creator compilation error"); + } + return null; + } + + @Override + public void report(Diagnostic diagnostic) { + System.Logger.Level level; + if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { + level = System.Logger.Level.ERROR; + isSuccessful = false; + } else if (Diagnostic.Kind.MANDATORY_WARNING == diagnostic.getKind() + || Diagnostic.Kind.WARNING == diagnostic.getKind()) { + level = System.Logger.Level.WARNING; + hasWarnings = true; + } else { + level = System.Logger.Level.INFO; + } + diagList.add(diagnostic); + + if (messager == null) { + logger.log(level, diagnostic); + return; + } + + String message = diagnostic.toString(); + if (System.Logger.Level.ERROR == level) { + messager.error(message, null); + } else if (System.Logger.Level.WARNING == level) { + messager.debug(message, null); + } else { + messager.debug(message); + } + } + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/Messager.java b/pico/tools/src/main/java/io/helidon/pico/tools/Messager.java new file mode 100644 index 00000000000..e8080c65631 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/Messager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.tools; + +/** + * Abstraction for logging messages. + */ +public interface Messager { + + /** + * Log a debug message. + * + * @param message the message + */ + void debug(String message); + + /** + * Log a debug message. + * + * @param message the message + * @param t throwable + */ + void debug(String message, + Throwable t); + + /** + * Log an info message. + * + * @param message the message + */ + void log(String message); + + /** + * Log a warning. + * + * @param message the message + */ + void warn(String message); + + /** + * Log a warning message. + * + * @param message the message + * @param t throwable + */ + void warn(String message, + Throwable t); + + /** + * Log an error message. + * + * @param message the message + * @param t any throwable + */ + void error(String message, + Throwable t); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/MethodElementInfo.java b/pico/tools/src/main/java/io/helidon/pico/tools/MethodElementInfo.java new file mode 100644 index 00000000000..fe92c0245dd --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/MethodElementInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; + +import io.helidon.builder.Builder; +import io.helidon.pico.ElementInfo; + +/** + * Describes a method element. + */ +@Builder +public interface MethodElementInfo extends ElementInfo { + + /** + * The list of "throws" that the method throws. Applies only to + * {@link io.helidon.pico.ElementInfo.ElementKind#METHOD} element types. + * + * @return the list of throwable types this method may throw + */ + List throwableTypeNames(); + + /** + * Provides information for each parameter to the method. + * + * @return parameter info + */ + List parameterInfo(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleDetail.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleDetail.java new file mode 100644 index 00000000000..9aea6ff7238 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleDetail.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Optional; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; + +/** + * The specifics for a single {@link io.helidon.pico.Module} that was codegen'ed. + * + * @see ActivatorCreatorResponse#moduleDetail + */ +@Builder +public interface ModuleDetail { + + /** + * name of the service provider activators for this module. + * + * @return name of the service provider activators for this module + */ + List serviceProviderActivatorTypeNames(); + + /** + * The name of this module. + * + * @return name of this module + */ + String moduleName(); + + /** + * The FQN of the module class name. + * + * @return The fqn of the module class name + */ + TypeName moduleTypeName(); + + /** + * The codegen body for the module. + * + * @return body for the module + */ + Optional moduleBody(); + + /** + * The Java 9+ module-info.java contents. + * + * @return contents for module-info body + */ + Optional moduleInfoBody(); + + /** + * The descriptor cooresponding to any {@link #moduleInfoBody()}. + * + * @return descriptor creator + */ + Optional descriptor(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoCreatorRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoCreatorRequest.java new file mode 100644 index 00000000000..e930556f659 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoCreatorRequest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypeName; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Used to represent the parameters that feed into the code generation of a module-info file specifically for Pico in that + * it offers easy ability to add the {@link io.helidon.pico.Module} as well as optionally the {@link io.helidon.pico.Application}. + */ +@Builder +public interface ModuleInfoCreatorRequest { + + /** + * Optionally, the module name. If not provided then an attempt will be made to calculate the suggested name. + * + * @return module name + */ + Optional name(); + + /** + * The Pico {@link io.helidon.pico.Module} type name. + * + * @return Pico module type name + */ + TypeName moduleTypeName(); + + /** + * The Pico {@link io.helidon.pico.Application} type name. + * + * @return application type name + */ + Optional applicationTypeName(); + + /** + * Set to true if the {@link io.helidon.pico.Module} should be created. + * + * @return true if the Pico Module should be created + */ + @ConfiguredOption("true") + boolean moduleCreated(); + + /** + * Set to true if the {@link io.helidon.pico.Application} should be created. + * + * @return true if the Pico Application should be created + */ + boolean applicationCreated(); + + /** + * The modules required list. + * + * @return modules required + */ + List modulesRequired(); + + /** + * The service type mapping to contracts for that service type. + * + * @return service type mapping to contracts + */ + Map> contracts(); + + /** + * The service type mapping to external contracts for that service type. + * + * @return service type mapping to external contracts + */ + Map> externalContracts(); + + /** + * Optionally, the path for where to access the module-info file. + * + * @return module info path + */ + Optional moduleInfoPath(); + + /** + * The class name prefix for the code generated class. + * + * @return class name prefix + */ + String classPrefixName(); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoDescriptor.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoDescriptor.java index 46f05c5d8fa..c1793ed36ca 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoDescriptor.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoDescriptor.java @@ -31,27 +31,23 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import io.helidon.builder.Builder; import io.helidon.builder.Singular; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.DefaultBootstrap; -import io.helidon.pico.types.DefaultTypeName; -import io.helidon.pico.types.TypeName; /** * Provides the basic formation for {@code module-info.java} creation and manipulation. * * @see java.lang.module.ModuleDescriptor */ +@SuppressWarnings("unused") @Builder public interface ModuleInfoDescriptor { - /** - * The tag used to represent the module name. - */ - String TAG_MODULE_NAME = "module_name"; - /** * The default module name (i.e., "unnamed"). */ @@ -73,11 +69,6 @@ public interface ModuleInfoDescriptor { */ String DEFAULT_MODULE_INFO_JAVA_NAME = MODULE_INFO_NAME + ".java"; - /** - * The resource providing the module-info template. - */ - String SERVICE_PROVIDER_MODULE_INFO_HBS = "module-info.hbs"; - /** * Used to declare the preferred ordering of the items in the module-info. @@ -148,14 +139,64 @@ enum Ordering { List items(); /** - * Returns true if this module info is unnamed. + * Returns true if the name currently set is the same as the {@link #DEFAULT_MODULE_NAME}. * - * @return true if this module is unnamed + * @return true if the current name is the default name */ default boolean isUnnamed() { return DEFAULT_MODULE_NAME.equals(name()); } + /** + * Provides the ability to create a new merged descriptor using this as the basis, and then combining another into it + * in order to create a new descriptor. + * + * @param another the other descriptor to merge + * @return the merged descriptor + */ + @SuppressWarnings("unchecked") + default ModuleInfoDescriptor mergeCreate(ModuleInfoDescriptor another) { + if (another == this) { + throw new IllegalArgumentException("can't merge with self"); + } + + DefaultModuleInfoDescriptor.Builder newOne = DefaultModuleInfoDescriptor.toBuilder(this); + for (ModuleInfoItem itemThere : another.items()) { + Optional itemHere = first(itemThere.target()); + if (itemHere.isPresent()) { + int index = newOne.items.indexOf(itemHere.get()); + newOne.items.remove(index); + ModuleInfoItem mergedItem = itemHere.get().mergeCreate(itemThere); + newOne.items.add(index, mergedItem); + } else { + newOne.addItem(itemThere); + } + } + + return newOne.build(); + } + + /** + * Takes a builder, and if the target does not yet exist, will add the new module info item from the supplier. + * + * @param builder the fluent builder + * @param target the target to check for existence for + * @param itemSupplier the item to add which presumably has the same target as above + * @return true if added + */ + static boolean addIfAbsent(DefaultModuleInfoDescriptor.Builder builder, + String target, + Supplier itemSupplier) { + Optional existing = builder.first(target); + if (existing.isEmpty()) { + ModuleInfoItem item = Objects.requireNonNull(itemSupplier.get()); + assert (target.equals(item.target())) : "target mismatch: " + target + " and " + item.target(); + builder.addItem(item); + return true; + } + return false; + } + /** * Loads and creates the {@code module-info} descriptor given its source file location. * @@ -245,12 +286,15 @@ static ModuleInfoDescriptor create(String moduleInfo, clean = moduleInfo.replaceAll("/\\*[^*]*(?:\\*(?!/)[^*]*)*\\*/|//.*", ""); } + // remove annotations + clean = cleanModuleAnnotations(clean); + boolean firstLine = true; Map importAliases = new LinkedHashMap<>(); + String line = null; try (BufferedReader reader = new BufferedReader(new StringReader(clean))) { - String line; while (null != (line = cleanLine(reader, comments, importAliases))) { - if (firstLine && Objects.nonNull(comments) && comments.size() > 0) { + if (firstLine && (comments != null) && comments.size() > 0) { descriptor.headerComment(String.join("\n", comments)); } firstLine = false; @@ -267,13 +311,13 @@ static ModuleInfoDescriptor create(String moduleInfo, } for (int i = start; i < split.length; i++) { descriptor.addItem(requiresModuleName(cleanLine(split[i]), isTransitive, isStatic, - Objects.nonNull(comments) ? comments : List.of())); + (comments != null) ? comments : List.of())); } } else if (line.startsWith("exports ")) { DefaultModuleInfoItem.Builder exports = DefaultModuleInfoItem.builder() .exports(true) .target(resolve(split[1], importAliases)) - .precomments(Objects.nonNull(comments) ? comments : List.of()); + .precomments((comments != null) ? comments : List.of()); for (int i = 2; i < split.length; i++) { if (!"to".equalsIgnoreCase(split[i])) { exports.addWithOrTo(resolve(cleanLine(split[i]), importAliases)); @@ -284,13 +328,16 @@ static ModuleInfoDescriptor create(String moduleInfo, DefaultModuleInfoItem.Builder uses = DefaultModuleInfoItem.builder() .uses(true) .target(resolve(split[1], importAliases)) - .precomments(Objects.nonNull(comments) ? comments : List.of()); + .precomments((comments != null) ? comments : List.of()); descriptor.addItem(uses.build()); } else if (line.startsWith("provides ")) { DefaultModuleInfoItem.Builder provides = DefaultModuleInfoItem.builder() .provides(true) .target(resolve(split[1], importAliases)) - .precomments(Objects.nonNull(comments) ? comments : List.of()); + .precomments((comments != null) ? comments : List.of()); + if (split.length < 3) { + throw new ToolsException("unable to process module-info's use of: " + line); + } if (split[2].equals("with")) { for (int i = 3; i < split.length; i++) { provides.addWithOrTo(resolve(cleanLine(split[i]), importAliases)); @@ -303,17 +350,24 @@ static ModuleInfoDescriptor create(String moduleInfo, throw new ToolsException("unable to process module-info's use of: " + line); } - if (Objects.nonNull(comments)) { + if (comments != null) { comments = new ArrayList<>(); } } - } catch (IOException e) { - throw new ToolsException("unable to load module-info", e); + } catch (ToolsException e) { + throw e; + } catch (Exception e) { + if (line != null) { + throw new ToolsException("failed on line: " + line + ";\n" + + "unable to load or parse module-info: " + moduleInfo, e); + } + throw new ToolsException("unable to load or parse module-info: " + moduleInfo, e); } return descriptor.build(); } + /** * Saves the descriptor source to the provided path. * @@ -355,7 +409,7 @@ default Optional firstUnqualifiedPackageExport() { /** * Provides the content of the description appropriate to write out. * - * @return The contents (source code body) for this descriptor. + * @return the contents (source code body) for this descriptor */ default String contents() { return contents(true); @@ -365,11 +419,10 @@ default String contents() { * Provides the content of the description appropriate to write out. * * @param wantAnnotation flag determining whether the Generated annotation comment should be present - * @return The contents (source code body) for this descriptor. + * @return the contents (source code body) for this descriptor */ default String contents(boolean wantAnnotation) { - // note to self: this will change later to be a better resolution --jtrent - TemplateHelper helper = TemplateHelper.create(DefaultBootstrap.builder().build()); + TemplateHelper helper = TemplateHelper.create(); Map subst = new HashMap<>(); subst.put("name", name()); @@ -386,12 +439,12 @@ default String contents(boolean wantAnnotation) { if (wantAnnotation) { subst.put("generatedanno", (Ordering.NATURAL_PRESERVE_COMMENTS == ordering() || headerComment().isPresent()) - ? null : helper.defaultGeneratedStickerFor(getClass().getName())); + ? null : helper.generatedStickerFor(getClass().getName())); } headerComment().ifPresent(it -> subst.put("header", it)); descriptionComment().ifPresent(it -> subst.put("description", it)); subst.put("hasdescription", descriptionComment().isPresent()); - String template = helper.safeLoadTemplate(templateName(), SERVICE_PROVIDER_MODULE_INFO_HBS); + String template = helper.safeLoadTemplate(templateName(), ModuleUtils.SERVICE_PROVIDER_MODULE_INFO_HBS); String contents = helper.applySubstitutions(template, subst, true).trim(); return CommonUtils.trimLines(contents); } @@ -426,36 +479,6 @@ static ModuleInfoItem usesExternalContract(String externalContract) { return DefaultModuleInfoItem.builder().uses(true).target(externalContract).build(); } - /** - * Creates a new item declaring a {@code provides} contract from this module descriptor. - * - * @param contract the contract definition being provided - * @return the item created - */ - static ModuleInfoItem providesContract(Class contract) { - return providesContract(contract.getName()); - } - - /** - * Creates a new item declaring a {@code provides} contract from this module descriptor. - * - * @param contract the contract definition being provided - * @return the item created - */ - static ModuleInfoItem providesContract(TypeName contract) { - return providesContract(contract.name()); - } - - /** - * Creates a new item declaring a {@code provides} contract from this module descriptor. - * - * @param contract the contract definition being provided - * @return the item created - */ - static ModuleInfoItem providesContract(String contract) { - return DefaultModuleInfoItem.builder().provides(true).target(contract).build(); - } - /** * Creates a new item declaring it to provide some contract from this module definition, along with * a {@code 'with'} declaration. @@ -503,7 +526,7 @@ static ModuleInfoItem requiresModuleName(String moduleName, } /** - * Creates a new item {@code exports} on a a package from this module descriptor. + * Creates a new item {@code exports} on a package from this module descriptor. * * @param typeName the type name exported * @return the item created @@ -513,7 +536,7 @@ static ModuleInfoItem exportsPackage(TypeName typeName) { } /** - * Creates a new item {@code exports} on a a package from this module descriptor. + * Creates a new item {@code exports} on a package from this module descriptor. * * @param pkg the package name exported * @return the item created @@ -523,7 +546,7 @@ static ModuleInfoItem exportsPackage(String pkg) { } /** - * Creates a new item {@code exports} on a a package from this module descriptor, along with + * Creates a new item {@code exports} on a package from this module descriptor, along with * a {@code 'to'} declaration. * * @param contract the contract definition being exported @@ -538,19 +561,19 @@ static ModuleInfoItem exportsPackage(String contract, private static String resolve(String name, Map importAliases) { TypeName typeName = importAliases.get(name); - return Objects.isNull(typeName) ? name : typeName.name(); + return (typeName == null) ? name : typeName.name(); } private static String cleanLine(BufferedReader reader, List preComments, Map importAliases) throws IOException { String line = reader.readLine(); - if (Objects.isNull(line)) { + if (line == null) { return null; } String trimmedline = line.trim(); - if (Objects.nonNull(preComments)) { + if (preComments != null) { boolean incomment = trimmedline.startsWith("//") || trimmedline.startsWith("/*"); boolean inempty = trimmedline.isEmpty(); while (incomment || inempty) { @@ -558,7 +581,7 @@ private static String cleanLine(BufferedReader reader, incomment = incomment && !trimmedline.endsWith("*/") && !trimmedline.startsWith("//"); line = reader.readLine(); - if (Objects.isNull(line)) { + if (line == null) { return null; } trimmedline = line.trim(); @@ -595,7 +618,7 @@ private static String cleanLine(BufferedReader reader, } private static String cleanLine(String line) { - if (Objects.isNull(line)) { + if (line == null) { return null; } @@ -610,4 +633,40 @@ private static String cleanLine(String line) { return line.trim(); } + private static String cleanModuleAnnotations(String moduleText) { + StringBuilder response = new StringBuilder(); + + try (BufferedReader br = new BufferedReader(new StringReader(moduleText))) { + boolean inModule = false; + String line; + while ((line = br.readLine()) != null) { + if (inModule) { + response.append(line).append("\n"); + continue; + } + String trimmed = line.trim(); + if (trimmed.startsWith("/*")) { + // beginning of comments + response.append(line).append("\n"); + } else if (trimmed.startsWith("*")) { + // comment line + response.append(line).append("\n"); + } else if (trimmed.startsWith("import ")) { + // import line + response.append(line).append("\n"); + } else if (trimmed.startsWith("module")) { + // now just add the rest (we do not cover annotations within module text) + inModule = true; + response.append(line).append("\n"); + } else if (trimmed.isBlank()) { + // empty line + response.append("\n"); + } + } + } catch (IOException ignored) { + // ignored, we cannot get an exception when closing string reader + } + + return response.toString(); + } } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoItem.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoItem.java index d77a6c36691..fc7faf0cf39 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoItem.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleInfoItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,7 +16,6 @@ package io.helidon.pico.tools; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -40,7 +39,7 @@ public interface ModuleInfoItem { * @return pre-comments */ @Singular - List precomments(); + Set precomments(); /** * The target class name, package name, or module name this item applies to. @@ -131,7 +130,7 @@ && ordering().get() == ModuleInfoDescriptor.Ordering.SORTED) { /** * Provides the content of the description item appropriate to write out. * - * @return The contents (source code body) for this descriptor item. + * @return the contents (source code body) for this descriptor item */ default String contents() { StringBuilder builder = new StringBuilder(); @@ -152,7 +151,7 @@ default String contents() { assert (!opens()); assert (!exports()); if (builder.length() > 0) { - builder.append(target()).append(";\n "); + builder.append(target()).append(";"); } builder.append("provides "); if (!withOrTo().isEmpty()) { @@ -197,4 +196,33 @@ default String contents() { return builder.toString(); } + /** + * Provides the ability to create a new merged descriptor item using this as the basis, and then combining another into it + * in order to create a new descriptor item. + * + * @param another the other descriptor item to merge + * @return the merged descriptor + */ + default ModuleInfoItem mergeCreate(ModuleInfoItem another) { + if (another == this) { + return this; + } + + if (!Objects.equals(target(), another.target())) { + throw new IllegalArgumentException(); + } + + DefaultModuleInfoItem.Builder newOne = DefaultModuleInfoItem.toBuilder(another); + another.precomments().forEach(newOne::addPrecomment); + newOne.requires(requires() || another.requires()); + newOne.uses(uses() || another.uses()); + newOne.transitiveUsed(isTransitiveUsed() || another.isTransitiveUsed()); + newOne.staticUsed(isStaticUsed() || another.isStaticUsed()); + newOne.exports(exports() || another.exports()); + newOne.opens(opens() || another.opens()); + newOne.provides(opens() || another.provides()); + another.withOrTo().forEach(newOne::addWithOrTo); + return newOne.build(); + } + } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java new file mode 100644 index 00000000000..721d70ff1a1 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicReference; + +import javax.lang.model.element.TypeElement; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.Application; +import io.helidon.pico.PicoServicesConfig; + +import static io.helidon.pico.tools.CommonUtils.first; +import static io.helidon.pico.tools.CommonUtils.hasValue; + +/** + * Module specific utils. + */ +public class ModuleUtils { + static final System.Logger LOGGER = System.getLogger(ModuleUtils.class.getName()); + + /** + * The "real" module-info.java file name. + */ + public static final String REAL_MODULE_INFO_JAVA_NAME = ModuleInfoDescriptor.DEFAULT_MODULE_INFO_JAVA_NAME; + + /** + * The pico generated (e.g., module-info.java.pico) file name. + */ + public static final String PICO_MODULE_INFO_JAVA_NAME = REAL_MODULE_INFO_JAVA_NAME + "." + PicoServicesConfig.NAME; + + /** + * The file name written to ./target/pico/ to track the last package name generated for this application. + * This application package name is what we fall back to for the application name and the module name if not otherwise + * specified directly. + */ + public static final String APPLICATION_PACKAGE_FILE_NAME = PicoServicesConfig.NAME + "-app-package-name.txt"; + + static final String SERVICE_PROVIDER_MODULE_INFO_HBS = "module-info.hbs"; + static final String SRC_MAIN_JAVA_DIR = "/src/main/java"; + static final String SRC_TEST_JAVA_DIR = "/src/test/java"; + + private ModuleUtils() { + } + + /** + * Returns the suggested package name to use. + * + * @param descriptor optionally, the module-info descriptor + * @param typeNames optionally, the set of types that are being codegen'ed + * @param defaultPackageName the default package name to use if all options are exhausted + * @return the suggested package name + */ + public static String toSuggestedGeneratedPackageName(ModuleInfoDescriptor descriptor, + Collection typeNames, + String defaultPackageName) { + String export = null; + + if (descriptor != null) { + Optional provides = descriptor.first(Application.class.getName()); + if (provides.isEmpty() || provides.get().withOrTo().isEmpty()) { + provides = descriptor.first(Module.class.getName()); + } + if (provides.isEmpty() || provides.get().withOrTo().isEmpty()) { + export = descriptor.firstUnqualifiedPackageExport().orElse(null); + } else { + export = DefaultTypeName + .createFromTypeName(first(provides.get().withOrTo(), false)).packageName(); + } + } + + if (export == null && typeNames != null) { + export = typeNames.stream() + .sorted() + .map(TypeName::packageName) + .findFirst().orElse(null); + } + + return (export != null) ? export : defaultPackageName; + } + + /** + * Common way for naming a module (generally for use by {@link io.helidon.pico.Application} and + * {@link io.helidon.pico.Module}). + * + * @param moduleName the module name (from module-info) + * @param typeSuffix "test" for test, or null for normal src classes + * @param defaultName the default name to return if it cannot be determined + * @return the suggested module name or defaultName if it cannot be properly determined + */ + static String toSuggestedModuleName(String moduleName, + String typeSuffix, + String defaultName) { + if (moduleName == null && typeSuffix == null) { + return defaultName; + } + if (moduleName == null) { + moduleName = (defaultName == null) ? ModuleInfoDescriptor.DEFAULT_MODULE_NAME : defaultName; + } + String suffix = normalizedModuleNameTypeSuffix(typeSuffix); + return (typeSuffix == null || moduleName.endsWith(suffix)) ? moduleName : moduleName + suffix; + } + + /** + * Returns the module's name. + * + * @param typeSuffix the type suffix. + * @return the module name suffix + */ + static String normalizedModuleNameTypeSuffix(String typeSuffix) { + if (!hasValue(typeSuffix)) { + return ""; + } + return "/" + typeSuffix; + } + + /** + * Given either a base module name or test module name, will always return the base module name. + * + * @param moduleName the module name (base or test) + * @return the base module name + */ + static String normalizedBaseModuleName(String moduleName) { + if (!hasValue(moduleName)) { + return moduleName; + } + int pos = moduleName.lastIndexOf("/"); + return (pos >= 0) ? moduleName.substring(0, pos) : moduleName; + } + + /** + * Extract the module name, first attempting the source path (test or main), and if not found using + * the base path, presumably having basePath being a parent in the sourcePath hierarchy. + * + * @param basePath the secondary path to try if module-info was not found in the source path + * @param sourcePath the source path + * @param defaultToUnnamed if true, will return the default name, otherwise empty is returned + * @return the module name suggested to use, most appropriate for the name of {@link + * io.helidon.pico.Application} or {@link io.helidon.pico.Module} + */ + public static Optional toSuggestedModuleName(Path basePath, + Path sourcePath, + boolean defaultToUnnamed) { + AtomicReference typeSuffix = new AtomicReference<>(); + ModuleInfoDescriptor descriptor = findModuleInfo(basePath, sourcePath, typeSuffix, null, null) + .orElse(null); + return Optional.ofNullable(toSuggestedModuleName((descriptor != null) ? descriptor.name() : null, typeSuffix.get(), + defaultToUnnamed ? ModuleInfoDescriptor.DEFAULT_MODULE_NAME : null)); + } + + /** + * Attempts to find the descriptor, setting meta-information that is useful for later processing. + * + * @param basePath the base path to start the search + * @param sourcePath the source path, assumed a child of the base path + * @param typeSuffix the holder that will be set with the type suffix observed + * @param moduleInfoPath the holder that will be set with the module info path + * @param srcPath the holder that will be set with the source path + * @return the descriptor, or null if one cannot be found + */ + static Optional findModuleInfo(Path basePath, + Path sourcePath, + AtomicReference typeSuffix, + AtomicReference moduleInfoPath, + AtomicReference srcPath) { + Objects.requireNonNull(basePath); + Objects.requireNonNull(sourcePath); + // if we found a module-info in the source path, then that has to be the module to use + Set moduleInfoPaths = findFile(sourcePath, REAL_MODULE_INFO_JAVA_NAME); + if (1 == moduleInfoPaths.size()) { + return finishModuleInfoDescriptor(moduleInfoPath, srcPath, moduleInfoPaths, REAL_MODULE_INFO_JAVA_NAME); + } + + // if we did not find it, then there is a chance we are in the test directory; try to infer the module name + String suffix = inferSourceOrTest(basePath, sourcePath); + if (typeSuffix != null) { + typeSuffix.set(suffix); + } + if (!basePath.equals(sourcePath)) { + Path parent = sourcePath.getParent(); + moduleInfoPaths = findFile(parent, basePath, REAL_MODULE_INFO_JAVA_NAME); + if (1 == moduleInfoPaths.size()) { + // looks to be a potential test module, get the base name from the source tree... + return finishModuleInfoDescriptor(moduleInfoPath, srcPath, moduleInfoPaths, REAL_MODULE_INFO_JAVA_NAME); + } else if (moduleInfoPaths.size() > 0) { + LOGGER.log(System.Logger.Level.WARNING, "ambiguous which module-info to select: " + moduleInfoPaths); + } + } + + // if we get to here then there was no "real" module-info file found anywhere in the target build directories + // plan b: look for the pico generated files to infer the name + Path parent = sourcePath.getParent(); + if (parent != null) { + String fileName = String.valueOf(sourcePath.getFileName()); + Path scratch = parent.resolve(PicoServicesConfig.NAME).resolve(fileName); + moduleInfoPaths = findFile(scratch, scratch, PICO_MODULE_INFO_JAVA_NAME); + if (1 == moduleInfoPaths.size()) { + // looks to be a potential test module, get the base name from the source tree... + return finishModuleInfoDescriptor(moduleInfoPath, srcPath, moduleInfoPaths, PICO_MODULE_INFO_JAVA_NAME); + } + } + + return Optional.empty(); + } + + private static Optional finishModuleInfoDescriptor(AtomicReference moduleInfoPath, + AtomicReference srcPath, + Set moduleInfoPaths, + String moduleInfoName) { + File moduleInfoFile = new File(first(moduleInfoPaths, false).toFile(), moduleInfoName); + if (moduleInfoPath != null) { + moduleInfoPath.set(moduleInfoFile); + } + if (srcPath != null) { + srcPath.set(moduleInfoFile.getParentFile()); + } + return Optional.of(ModuleInfoDescriptor.create(moduleInfoFile.toPath())); + } + + /** + * Attempts to infer 'test' or base '' given the path. + * + * @param path the path + * @return 'test' or '' (for base non-test) + */ + public static String inferSourceOrTest(Path path) { + Objects.requireNonNull(path); + Path parent = path.getParent(); + if (parent == null) { + return ""; + } + Path gparent = parent.getParent(); + if (gparent == null) { + return ""; + } + return inferSourceOrTest(gparent, path); + } + + /** + * If the relative path contains "/test/" then returns "test" else returns "". + * + * @param basePath the base path to start the search + * @param sourcePath the source path, assumed a child of the base path + * @return 'test' or '' + */ + static String inferSourceOrTest(Path basePath, + Path sourcePath) { + // create a relative path from the two paths + URI relativePath = basePath.toUri().relativize(sourcePath.toUri()); + String path = relativePath.getPath(); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (path.contains("/test/") || path.contains("/generated-test-sources/") || path.contains("/test-classes/")) { + return ActivatorCreatorCodeGen.DEFAULT_TEST_CLASS_PREFIX_NAME; + } + + return ActivatorCreatorCodeGen.DEFAULT_CLASS_PREFIX_NAME; + } + + /** + * Translates to the source path coordinate given a source file and type name. + * Only available during annotation processing. + * + * @param filePath the source file path + * @param type the type name + * @return the source path, or empty if it cannot be inferred + */ + public static Optional toSourcePath(Path filePath, + TypeElement type) { + TypeName typeName = TypeTools.createTypeNameFromElement(type).orElseThrow(); + Path typePath = Paths.get(TypeTools.toFilePath(typeName)); + if (filePath.endsWith(typePath)) { + return Optional.of(filePath.resolveSibling(typePath)); + } + return Optional.empty(); + } + + /** + * Returns the base module path given its source path. + * + * @param sourcePath the source path + * @return the base path + */ + public static Path toBasePath(String sourcePath) { + int pos = sourcePath.lastIndexOf(SRC_MAIN_JAVA_DIR); + if (pos < 0) { + pos = sourcePath.lastIndexOf(SRC_TEST_JAVA_DIR); + } + if (pos < 0) { + throw new ToolsException("invalid source path: " + sourcePath); + } + Path path = Path.of(sourcePath.substring(0, pos)); + return Objects.requireNonNull(path); + } + + /** + * Will return non-empty File if the uri represents a local file on the fs. + * + * @param uri the uri of the artifact + * @return the file instance, or empty if not local + */ + static Optional toPath(URI uri) { + if (uri.getHost() != null) { + return Optional.empty(); + } + return Optional.of(Paths.get(uri)); + } + + /** + * Returns true if the given module name is unnamed. + * + * @param moduleName the module name to check + * @return true if the provided module name is unnamed + */ + public static boolean isUnnamedModuleName(String moduleName) { + return !hasValue(moduleName) + || moduleName.equals(ModuleInfoDescriptor.DEFAULT_MODULE_NAME) + || moduleName.equals(ModuleInfoDescriptor.DEFAULT_MODULE_NAME + "/" + ModuleInfoDescriptor.DEFAULT_TEST_SUFFIX); + } + + private static Set findFile(Path startPath, + Path untilPath, + String fileToFind) { + if (startPath == null || !startPath.toString().contains(untilPath.toString())) { + return Set.of(); + } + + do { + Set set = findFile(startPath, fileToFind); + if (!set.isEmpty()) { + return set; + } + if (startPath.equals(untilPath)) { + break; + } + startPath = startPath.getParent(); + } while (startPath != null); + + return Set.of(); + } + + private static Set findFile(Path target, + String fileToFind) { + Set result = new LinkedHashSet<>(); + + Stack searchStack = new Stack<>(); + searchStack.add(target); + + while (!searchStack.isEmpty()) { + Path cwdPath = searchStack.pop(); + if (!cwdPath.toFile().exists()) { + continue; + } + + try (DirectoryStream stream = Files.newDirectoryStream(cwdPath)) { + for (Path entry : stream) { + if (Files.isDirectory(entry)) { + searchStack.add(entry); + } + + File file = new File(cwdPath.toFile(), fileToFind); + if (file.exists()) { + result.add(cwdPath); + } + } + } catch (Exception ex) { + LOGGER.log(System.Logger.Level.ERROR, "error while processing directory", ex); + } + } + + return result; + } + + /** + * Attempts to load the app package name from what was previously recorded. + * + * @param scratchPath the scratch directory path + * @return the app package name that was loaded + */ + public static Optional loadAppPackageName(Path scratchPath) { + File scratchDir = scratchPath.toFile(); + File packageFileName = new File(scratchDir, APPLICATION_PACKAGE_FILE_NAME); + if (packageFileName.exists()) { + String packageName; + try { + packageName = Files.readString(packageFileName.toPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new ToolsException("unable to load: " + packageFileName, e); + } + + if (hasValue(packageName)) { + return Optional.of(packageName); + } + } + return Optional.empty(); + } + + /** + * Persist the package name into scratch for later usage. + * + * @param scratchPath the scratch directory path + * @param packageName the package name + */ + public static void saveAppPackageName(Path scratchPath, + String packageName) { + File scratchDir = scratchPath.toFile(); + File packageFileName = new File(scratchDir, APPLICATION_PACKAGE_FILE_NAME); + try { + Files.createDirectories(packageFileName.getParentFile().toPath()); + Files.writeString(packageFileName.toPath(), packageName, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new ToolsException("unable to save: " + packageFileName, e); + } + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/Options.java b/pico/tools/src/main/java/io/helidon/pico/tools/Options.java new file mode 100644 index 00000000000..cc18ce75b6c --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/Options.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.pico.PicoServicesConfig; + +import static io.helidon.pico.tools.CommonUtils.hasValue; +import static io.helidon.pico.tools.CommonUtils.toList; + +/** + * Options that can be provided via -A (in annotation processing mode), or via system properties or env properties + * if running normally. + */ +public class Options { + + /** + * Tag for putting Pico's annotation processing into debug mode. + */ + public static final String TAG_DEBUG = PicoServicesConfig.TAG_DEBUG; + + /** + * Treat all super types as a contract for a given service type being added. + */ + public static final String TAG_AUTO_ADD_NON_CONTRACT_INTERFACES = PicoServicesConfig.NAME + ".autoAddNonContractInterfaces"; + + /** + * Pre-creates a placeholder for an {@link io.helidon.pico.Application}. + */ + public static final String TAG_APPLICATION_PRE_CREATE = ActivatorCreatorConfigOptions.TAG_APPLICATION_PRE_CREATE; + + /** + * For future use. Should the module-info.java be automatically patched to reflect the pico DI model. + */ + static final String TAG_AUTO_PATCH_MODULE_INFO = PicoServicesConfig.NAME + ".autoPatchModuleInfo"; + + /** + * Identify the module name being processed or the desired target module name. + */ + public static final String TAG_MODULE_NAME = PicoServicesConfig.TAG_MODULE_NAME; + + /** + * Identify the pico sidecar (module-info.java.pico) module file name or path. + */ + public static final String TAG_PICO_MODULE_NAME = PicoServicesConfig.NAME + "." + TAG_MODULE_NAME; + + /** + * Identify the additional annotation type names that will trigger interception. + */ + static final String TAG_ALLOW_LISTED_INTERCEPTOR_ANNOTATIONS = PicoServicesConfig.NAME + + ".allowListedInterceptorAnnotations"; + + /** + * Identify whether any application scopes (from ee) is translated to {@link jakarta.inject.Singleton}. + */ + public static final String TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE = PicoServicesConfig.NAME + + ".mapApplicationToSingletonScope"; + + /** + * Identify whether any unsupported types should trigger annotation processing to keep going (the default is to fail). + */ + public static final String TAG_IGNORE_UNSUPPORTED_ANNOTATIONS = PicoServicesConfig.NAME + + ".ignoreUnsupportedAnnotations"; + + /** + * Identify invalid usage of the {@code module-info.java} for appropriate Pico references (the default is to fail). + */ + public static final String TAG_IGNORE_MODULE_USAGE = PicoServicesConfig.NAME + + ".ignoreModuleUsage"; + + private static final Map OPTS = new HashMap<>(); + + private Options() { + } + + /** + * Initialize (applicable for annotation processing only). + * + * @param processingEnv the processing env + */ + public static void init(ProcessingEnvironment processingEnv) { + if (OPTS.isEmpty()) { + OPTS.put(TAG_DEBUG, + String.valueOf(isOptionEnabled(TAG_DEBUG, processingEnv))); + OPTS.put(TAG_AUTO_ADD_NON_CONTRACT_INTERFACES, + String.valueOf(isOptionEnabled(TAG_AUTO_ADD_NON_CONTRACT_INTERFACES, processingEnv))); + OPTS.put(TAG_APPLICATION_PRE_CREATE, + String.valueOf(isOptionEnabled(TAG_APPLICATION_PRE_CREATE, processingEnv))); + OPTS.put(TAG_AUTO_PATCH_MODULE_INFO, + String.valueOf(isOptionEnabled(TAG_AUTO_PATCH_MODULE_INFO, processingEnv))); + OPTS.put(TAG_MODULE_NAME, + getOption(TAG_MODULE_NAME, null, processingEnv)); + OPTS.put(TAG_PICO_MODULE_NAME, + getOption(TAG_PICO_MODULE_NAME, null, processingEnv)); + OPTS.put(TAG_ALLOW_LISTED_INTERCEPTOR_ANNOTATIONS, + getOption(TAG_ALLOW_LISTED_INTERCEPTOR_ANNOTATIONS, null, processingEnv)); + OPTS.put(TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE, + getOption(TAG_MAP_APPLICATION_TO_SINGLETON_SCOPE, null, processingEnv)); + OPTS.put(TAG_IGNORE_UNSUPPORTED_ANNOTATIONS, + getOption(TAG_IGNORE_UNSUPPORTED_ANNOTATIONS, null, processingEnv)); + OPTS.put(TAG_IGNORE_MODULE_USAGE, + getOption(TAG_IGNORE_MODULE_USAGE, null, processingEnv)); + } + } + + /** + * Only supports the subset of options that pico cares about, and should not be generally used for options. + * + * @param option the key (assumed to be meaningful to this class) + * @return true if the option is enabled + */ + public static boolean isOptionEnabled(String option) { + return "true".equals(getOption(option, "")); + } + + /** + * This only supports the subset of options that pico cares about, and should not be generally used for options. + * + * @param option the key (assumed to be meaningful to this class) + * @return the option value + */ + public static Optional getOption(String option) { + return Optional.ofNullable(getOption(option, null)); + } + + /** + * This only supports the subset of options that pico cares about, and should not be generally used for options. + * + * @param option the key (assumed to be meaningful to this class) + * @param defaultVal the default value used if the associated value is null. + * @return the option value + */ + private static String getOption(String option, + String defaultVal) { + assert (OPTS.containsKey(option)); + return OPTS.getOrDefault(option, defaultVal); + } + + /** + * Returns a non-null list of comma-delimited string value options. + * + * @param option the key (assumed to be meaningful to this class) + * @return the list of string values that were comma-delimited + */ + static List getOptionStringList(String option) { + String result = getOption(option, null); + if (!hasValue(result)) { + return List.of(); + } + + return toList(result); + } + + private static boolean isOptionEnabled(String option, + ProcessingEnvironment processingEnv) { + if (processingEnv != null) { + String val = processingEnv.getOptions().get(option); + if (val != null) { + return Boolean.parseBoolean(val); + } + } + + return getOption(option, "", processingEnv).equals("true"); + } + + private static String getOption(String option, + String defaultVal, + ProcessingEnvironment processingEnv) { + if (processingEnv != null) { + String val = processingEnv.getOptions().get(option); + if (val != null) { + return val; + } + } + + return defaultVal; + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/PicoSupported.java b/pico/tools/src/main/java/io/helidon/pico/tools/PicoSupported.java new file mode 100644 index 00000000000..a2af6d14e40 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/PicoSupported.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, 2023 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.tools; + +import io.helidon.common.types.TypeName; + +/** + * Centralized utility to help callers determine what is and is not supported. + */ +class PicoSupported { + + private PicoSupported() { + } + + /** + * Returns true if the targetElement can be supported within the pico model as an injection point target. + * + * @param logger the optional logger to use if not supported + * @param serviceType the enclosing service type + * @param targetElement the target element description + * @param isPrivate is the target element private + * @param isStatic is the target element static + * @return true if the target supported pico injection, false otherwise (assuming not throwIfNotSupported) + */ + static boolean isSupportedInjectionPoint(System.Logger logger, + TypeName serviceType, + Object targetElement, + boolean isPrivate, + boolean isStatic) { + boolean supported = (!isPrivate && !isStatic); + if (!supported) { + String message = "static and private injection points are not supported: " + serviceType + "." + targetElement; + if (logger != null) { + System.Logger.Level level = System.Logger.Level.WARNING; + logger.log(level, message); + } + } + return supported; + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ReflectionHandler.java b/pico/tools/src/main/java/io/helidon/pico/tools/ReflectionHandler.java new file mode 100644 index 00000000000..a283aabd7ec --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ReflectionHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 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.tools; + +import io.helidon.pico.Resettable; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; + +/** + * Handles anything involving classpath scanning and introspection. + */ +class ReflectionHandler implements Resettable { + + /** + * The shared instance. + */ + static final ReflectionHandler INSTANCE = new ReflectionHandler(); + + private ClassLoader loader; + private ScanResult scan; + + @Override + public boolean reset(boolean deep) { + if (deep) { + loader = getCurrentLoader(); + scan = new ClassGraph() + .overrideClassLoaders(loader) + .enableAllInfo() + .scan(); + + } else { + loader = null; + scan = null; + } + return true; + } + + private ClassLoader getCurrentLoader() { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = getClass().getClassLoader(); + } + return loader; + } + + /** + * Lazy scan the classpath. + * + * @return the scan result + */ + ScanResult scan() { + if (scan == null || loader != getCurrentLoader()) { + reset(true); + } + + return scan; + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java new file mode 100644 index 00000000000..8202b9b1fb3 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java @@ -0,0 +1,904 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +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.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.Application; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.Module; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.Resettable; +import io.helidon.pico.services.Dependencies; + +/** + * Tracks the services to process, and ingests them to build the codegen model. + *

        + * The basic flow: + * 'annotation processor(s)' -> ServicesToProcess -> {@link AbstractCreator} derivative. + *

        + * Note that the flow might be repeated multiple times since annotation processors by definition are recurrent in + * nature. + */ +public class ServicesToProcess implements Resettable { + private static final ServicesToProcess SERVICES = new ServicesToProcess(); + + private static final AtomicInteger RUNNING_PROCESSORS = new AtomicInteger(); + + private final Set servicesTypeNames = new LinkedHashSet<>(); + private final Set requiredModules = new TreeSet<>(); + private final Map> servicesToContracts = new LinkedHashMap<>(); + private final Map> servicesToExternalContracts = new LinkedHashMap<>(); + private final Map servicesToLockParentServiceTypeName = new LinkedHashMap<>(); + private final Map servicesToParentServiceTypeNames = new LinkedHashMap<>(); + private final Map servicesToActivatorGenericDecl = new LinkedHashMap<>(); + private final Map servicesToAccess = new LinkedHashMap<>(); + private final Map servicesToIsAbstract = new LinkedHashMap<>(); + private final Map servicesToDependencies = new LinkedHashMap<>(); + private final Map servicesToPreDestroyMethod = new LinkedHashMap<>(); + private final Map servicesToPostConstructMethod = new LinkedHashMap<>(); + private final Map servicesToWeightedPriority = new LinkedHashMap<>(); + private final Map servicesToRunLevel = new LinkedHashMap<>(); + private final Map> servicesToScopeTypeNames = new LinkedHashMap<>(); + private final Map> servicesToQualifiers = new LinkedHashMap<>(); + private final Map> servicesToProviderFor = new LinkedHashMap<>(); + private final Map interceptorPlanFor = new LinkedHashMap<>(); + private final Map> extraCodeGen = new LinkedHashMap<>(); + private final Map> extraActivatorClassComments = new LinkedHashMap<>(); + private final Map> serviceTypeHierarchy = new LinkedHashMap<>(); + + private Path lastKnownSourcePathBeingProcessed; + private String lastKnownTypeSuffix; + private String moduleName; + private String lastKnownModuleName; + private Path lastKnownModuleInfoFilePath; + private ModuleInfoDescriptor lastKnownModuleInfoDescriptor; + private Path lastGeneratedModuleInfoFilePath; + private ModuleInfoDescriptor lastGeneratedModuleInfoDescriptor; + private String lastGeneratedPackageName; + + /** + * The current services to process instance. + * + * @return the current services to process instance + */ + public static ServicesToProcess servicesInstance() { + return SERVICES; + } + + private ServicesToProcess() { + } + + @Override + public boolean reset(boolean ignoredDeep) { + servicesTypeNames.clear(); + requiredModules.clear(); + servicesToContracts.clear(); + servicesToExternalContracts.clear(); + // we intentionally except parent service type names from being cleared - we need to remember this fact! +// if (false) { +// servicesToLockParentServiceTypeName.clear(); +// servicesToParentServiceTypeNames.clear(); +// servicesToActivatorGenericDecl.clear(); +// } + servicesToAccess.clear(); + servicesToIsAbstract.clear(); + servicesToDependencies.clear(); + servicesToPreDestroyMethod.clear(); + servicesToPostConstructMethod.clear(); + servicesToWeightedPriority.clear(); + servicesToRunLevel.clear(); + servicesToScopeTypeNames.clear(); + servicesToQualifiers.clear(); + servicesToProviderFor.clear(); + interceptorPlanFor.clear(); + serviceTypeHierarchy.clear(); + extraCodeGen.clear(); + extraActivatorClassComments.clear(); + return true; + } + + /** + * Introduce a new service type name to be added. + * + * @param serviceTypeName the service type name + */ + void addServiceTypeName(TypeName serviceTypeName) { + servicesTypeNames.add(Objects.requireNonNull(serviceTypeName)); + } + + /** + * Introduce the parent superclass for a given service type name. + * + * @param serviceTypeName the service type name + * @param parentServiceTypeName the parent for this service type name + * @return flag indicating whether the parent was accepted + */ + public boolean addParentServiceType(TypeName serviceTypeName, + TypeName parentServiceTypeName) { + return addParentServiceType(serviceTypeName, parentServiceTypeName, Optional.empty()); + } + + /** + * Introduce the parent superclass for a given service type name. + * + * @param serviceTypeName the service type name + * @param parentServiceTypeName the parent for this service type name + * @param lockParent flag indicating whether the parent should be locked + * @return flag indicating whether the parent was accepted + */ + public boolean addParentServiceType(TypeName serviceTypeName, + TypeName parentServiceTypeName, + Optional lockParent) { + if (parentServiceTypeName == null) { + return false; + } + + Boolean locked = servicesToLockParentServiceTypeName.get(serviceTypeName); + if (locked != null) { + if (locked) { + TypeName lockedParentType = servicesToParentServiceTypeNames.get(serviceTypeName); + return parentServiceTypeName.equals(lockedParentType); + } else { + servicesToLockParentServiceTypeName.put(serviceTypeName, lockParent.orElse(null)); + } + } + + addServiceTypeName(serviceTypeName); + servicesToParentServiceTypeNames.put(serviceTypeName, parentServiceTypeName); + lockParent.ifPresent(aBoolean -> servicesToLockParentServiceTypeName.put(serviceTypeName, aBoolean)); + + return true; + } + + /** + * @return the map of service type names to each respective super class / parent types + */ + Map parentServiceTypes() { + return Map.copyOf(servicesToParentServiceTypeNames); + } + + /** + * Introduce the activator generic portion of the declaration (e.g., the "CB extends MySingletonConfig" portion of + * {@code MyService$$picoActivator}). + * + * @param serviceTypeName the service type name + * @param activatorGenericDecl the generics portion of the class decl + */ + public void addActivatorGenericDecl(TypeName serviceTypeName, + String activatorGenericDecl) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToActivatorGenericDecl.put(serviceTypeName, activatorGenericDecl); + assert (prev == null || Objects.equals(prev, activatorGenericDecl)); + } + + /** + * @return the map of service type names to activator generic declarations + */ + Map activatorGenericDecls() { + return Map.copyOf(servicesToActivatorGenericDecl); + } + + /** + * Introduce the parent superclass for a given service type name. + * + * @param serviceTypeName the service type name + * @param access the access level for the service type name + */ + public void addAccessLevel(TypeName serviceTypeName, + InjectionPointInfo.Access access) { + addServiceTypeName(serviceTypeName); + if (access != null) { + Object prev = servicesToAccess.put(serviceTypeName, access); + if (prev != null && !access.equals(prev)) { + throw new ToolsException("can only support one access level for " + serviceTypeName); + } + } + } + + /** + * @return the map of service type names to each respective access level + */ + Map accessLevels() { + return servicesToAccess; + } + + /** + * Introduce the flag whether the given service type name is abstract (i.e., interface or abstract) and not concrete. + * + * @param serviceTypeName the service type name + * @param isAbstract whether the service type name is abstract (i.e., interface or abstract) + */ + public void addIsAbstract(TypeName serviceTypeName, + boolean isAbstract) { + addServiceTypeName(serviceTypeName); + servicesToIsAbstract.put(serviceTypeName, isAbstract); + } + + /** + * @return the map of service type names to whether they are abstract. If not found then assume concrete + */ + Map isAbstractMap() { + return servicesToIsAbstract; + } + + /** + * @return the map of service type names to the super class hierarchy + */ + Map> serviceTypeToHierarchy() { + return serviceTypeHierarchy; + } + + /** + * Introduce the parent superclass for a given service type name. + * + * @param serviceTypeName the service type name + * @param serviceTypeHierarchy the list of superclasses (where this service type is the last in the list) + */ + public void addServiceTypeHierarchy(TypeName serviceTypeName, + List serviceTypeHierarchy) { + addServiceTypeName(serviceTypeName); + if (serviceTypeHierarchy != null) { + Object prev = this.serviceTypeHierarchy.put(serviceTypeName, serviceTypeHierarchy); + if (prev != null && !serviceTypeHierarchy.equals(prev)) { + throw new ToolsException("can only support one hierarchy for " + serviceTypeName); + } + } + } + + /** + * Checks whether the service type has an established super class hierarchy set. + * + * @param serviceTypeName the service type name + * @return true if the hierarchy is known for this service type + */ + public boolean hasHierarchyFor(TypeName serviceTypeName) { + Collection coll = serviceTypeHierarchy.get(serviceTypeName); + return (coll != null); + } + + /** + * Checks whether the service type has an established set of contracts that are known for it. + * + * @param serviceTypeName the service type name + * @return true if contracts are known about this service type + */ + public boolean hasContractsFor(TypeName serviceTypeName) { + Collection coll = servicesToContracts.get(serviceTypeName); + if (coll != null) { + return true; + } + + coll = servicesToExternalContracts.get(serviceTypeName); + if (coll != null) { + return true; + } + + coll = servicesToProviderFor.get(serviceTypeName); + return (coll != null); + } + + /** + * Checks whether the service type has been evaluated for an interceptor plan. + * + * @param serviceTypeName the service type name + * @return true if this service type has already been considered for interceptors + */ + public boolean hasVisitedInterceptorPlanFor(TypeName serviceTypeName) { + return interceptorPlanFor.containsKey(serviceTypeName); + } + + /** + * Sets the {@link InterceptionPlan} for the given service type. + * + * @param serviceTypeName the service type name + * @param plan the interceptor plan + */ + public void addInterceptorPlanFor(TypeName serviceTypeName, + Optional plan) { + Object prev = interceptorPlanFor.put(serviceTypeName, plan.orElse(null)); + if (prev != null && plan.isPresent()) { + throw new ToolsException("should only set interception plan once for: " + serviceTypeName); + } + } + + /** + * The interception plan for each service type that has a non-null interception plan. + * + * @return the interception plan for each service type + */ + public Map interceptorPlans() { + return interceptorPlanFor.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, TreeMap::new)); + } + + /** + * Clears out just the interceptor plans. + */ + public void clearInterceptorPlans() { + interceptorPlanFor.clear(); + } + + /** + * @return the extra code gen to add for each service type + */ + Map> extraCodeGen() { + return extraCodeGen; + } + + /** + * Adds extra code gen per service type. + * + * @param serviceTypeName the service type name + * @param codeGen the extra code gen to tack onto the activator implementation + */ + public void addExtraCodeGen(TypeName serviceTypeName, + String codeGen) { + Objects.requireNonNull(codeGen); + extraCodeGen.compute(serviceTypeName, (key, val) -> { + if (val == null) { + val = new ArrayList<>(); + } + val.add(codeGen); + return val; + }); + } + + /** + * @return the extra activator class level comments for code generated types + */ + Map> extraActivatorClassComments() { + return extraActivatorClassComments; + } + + /** + * Adds extra cactivator class level comments. + * + * @param serviceTypeName the service type name + * @param codeGen the extra comments tack onto the activator implementation + */ + public void addExtraActivatorClassComments(TypeName serviceTypeName, + String codeGen) { + Objects.requireNonNull(codeGen); + extraActivatorClassComments.compute(serviceTypeName, (key, val) -> { + if (val == null) { + val = new ArrayList<>(); + } + val.add(codeGen); + return val; + }); + } + + /** + * Introduces a contract associated with a service type. + * + * @param serviceTypeName the service type name + * @param contractTypeName the contract type name + * @param isExternal whether the contract is external + */ + public void addTypeForContract(TypeName serviceTypeName, + TypeName contractTypeName, + boolean isExternal) { + if (serviceTypeName.equals(contractTypeName)) { + return; + } + + addServiceTypeName(serviceTypeName); + + servicesToContracts.compute(serviceTypeName, (key, val) -> { + if (val == null) { + val = new TreeSet<>(); + } + val.add(contractTypeName); + return val; + }); + + if (isExternal) { + servicesToExternalContracts.compute(serviceTypeName, (key, val) -> { + if (val == null) { + val = new TreeSet<>(); + } + val.add(contractTypeName); + return val; + }); + } + } + + /** + * Introduces a new set of dependencies to the model. + * + * @param dependencies the dependencies + */ + public void addDependencies(DependenciesInfo dependencies) { + TypeName serviceTypeName = + DefaultTypeName.createFromTypeName(dependencies.fromServiceTypeName().orElseThrow()); + addServiceTypeName(serviceTypeName); + DependenciesInfo prevDependencies = servicesToDependencies.get(serviceTypeName); + if (prevDependencies != null) { + dependencies = Dependencies.combine(prevDependencies, dependencies); + } + servicesToDependencies.put(serviceTypeName, dependencies); + } + + /** + * Introduces a {@link jakarta.annotation.PreDestroy} method to the model for a given service type. + * + * @param serviceTypeName the service type name + * @param preDestroyMethodName the method name + */ + public void addPreDestroyMethod(TypeName serviceTypeName, + String preDestroyMethodName) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToPreDestroyMethod.put(serviceTypeName, preDestroyMethodName); + if (prev != null) { + throw new ToolsException("can only support one PreDestroy method for " + serviceTypeName); + } + } + + /** + * Introduces a {@link jakarta.annotation.PostConstruct} method to the model for a given service type. + * + * @param serviceTypeName the service type name + * @param postConstructMethodName the method name + */ + public void addPostConstructMethod(TypeName serviceTypeName, + String postConstructMethodName) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToPostConstructMethod.put(serviceTypeName, postConstructMethodName); + if (prev != null) { + throw new ToolsException("can only support one PostConstruct method for " + serviceTypeName); + } + } + + /** + * Sets the weight of a service type. + * + * @param serviceTypeName the service type name + * @param weight the weight priority + */ + public void addDeclaredWeight(TypeName serviceTypeName, + Double weight) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToWeightedPriority.put(serviceTypeName, weight); + if (prev != null) { + throw new ToolsException("can only support one weighted priority for " + serviceTypeName); + } + } + + /** + * Sets the run level for a service type name. + * + * @param serviceTypeName the service type name + * @param runLevel its run level + */ + public void addDeclaredRunLevel(TypeName serviceTypeName, + Integer runLevel) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToRunLevel.put(serviceTypeName, runLevel); + if (prev != null) { + throw new ToolsException("can only support one RunLevel for " + serviceTypeName); + } + } + + /** + * Adds a scope type name for a service type name. + * + * @param serviceTypeName the service type name + * @param scopeTypeName its scope type name + */ + public void addScopeTypeName(TypeName serviceTypeName, + String scopeTypeName) { + Objects.requireNonNull(scopeTypeName); + addServiceTypeName(serviceTypeName); + + servicesToScopeTypeNames.compute(serviceTypeName, (k, v) -> { + if (v == null) { + v = new LinkedHashSet<>(); + } + v.add(scopeTypeName); + return v; + }); + } + + /** + * Establishes the fact that a given service type is a {@link jakarta.inject.Provider} type for the given provided types. + * + * @param serviceTypeName the service type name + * @param providerFor the types that it provides + */ + public void addProviderFor(TypeName serviceTypeName, + Set providerFor) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToProviderFor.put(serviceTypeName, providerFor); + if (prev != null && !prev.equals(providerFor)) { + throw new ToolsException("can only support setting isProvider once for " + serviceTypeName); + } + } + + /** + * Sets the qualifiers associated with a service type. + * + * @param serviceTypeName the service type name + * @param qualifiers its qualifiers + */ + public void addQualifiers(TypeName serviceTypeName, + Set qualifiers) { + addServiceTypeName(serviceTypeName); + Object prev = servicesToQualifiers.put(serviceTypeName, qualifiers); + if (prev != null) { + throw new ToolsException("can only support setting qualifiers once for " + serviceTypeName + + "; prev = " + prev + " and new = " + qualifiers); + } + } + + /** + * Fetches the set of known service type names being processed in this batch. + * + * @return the list of known service type names being processed + */ + public List serviceTypeNames() { + ArrayList result = new ArrayList<>(servicesTypeNames); + Collections.sort(result); + return result; + } + + /** + * @return fetches the map of service types to their set of contracts for that service type + */ + Map> contracts() { + return new TreeMap<>(servicesToContracts); + } + + /** + * @return fetches the map of service types to their set of contracts for that service type + */ + Map> externalContracts() { + return new TreeMap<>(servicesToExternalContracts); + } + + /** + * @return fetches the map of service types to their injection point dependencies + */ + Map injectionPointDependencies() { + return new TreeMap<>(servicesToDependencies); + } + + /** + * @return fetches the map of service types to their post construct methods + */ + Map postConstructMethodNames() { + return new TreeMap<>(servicesToPostConstructMethod); + } + + /** + * @return fetches the map of service types to their pre destroy methods + */ + Map preDestroyMethodNames() { + return new TreeMap<>(servicesToPreDestroyMethod); + } + + /** + * Fetches the map of service types to their priorities. + * + * @return the map of service types to their priorities + */ + public Map weightedPriorities() { + return new TreeMap<>(servicesToWeightedPriority); + } + + /** + * Fetches the map of service types to their run levels. + * + * @return the map of service types to their run levels + */ + public Map runLevels() { + return new TreeMap<>(servicesToRunLevel); + } + + /** + * Fetches the map of service types to their scope type names. + * + * @return the map of service types to their scope type names + */ + public Map> scopeTypeNames() { + return new TreeMap<>(servicesToScopeTypeNames); + } + + /** + * @return fetches the map of service types to the set of services they provide + */ + Map> providerForTypeNames() { + return new TreeMap<>(servicesToProviderFor); + } + + /** + * @return fetches the map of service types to the set of qualifiers associated with each + */ + Map> qualifiers() { + return new TreeMap<>(servicesToQualifiers); + } + + /** + * Introduces the need for external modules. + * + * @param serviceTypeName the service type name + * @param moduleNames the required module names to support known external contracts + */ + public void addExternalRequiredModules(TypeName serviceTypeName, + Collection moduleNames) { + if (moduleNames != null) { + requiredModules.addAll(moduleNames); + } + } + + /** + * @return the set of required (external) module names + */ + Set requiredModules() { + return new TreeSet<>(requiredModules); + } + + /** + * Sets this module name. + * + * @param moduleName the module name + */ + public void moduleName(String moduleName) { + this.moduleName = moduleName; + this.lastKnownModuleName = moduleName; + } + + /** + * Clears the module name. + */ + public void clearModuleName() { + this.moduleName = null; + } + + /** + * This module name. + * + * @return this module name + */ + public String moduleName() { + return moduleName; + } + + /** + * The last known descriptor being processed. + * + * @param descriptor the descriptor + */ + public void lastKnownModuleInfoDescriptor(ModuleInfoDescriptor descriptor) { + this.lastKnownModuleInfoDescriptor = descriptor; + if (descriptor != null) { + moduleName(descriptor.name()); + } + } + + /** + * @return fetches the last known module info descriptor + */ + ModuleInfoDescriptor lastKnownModuleInfoDescriptor() { + return lastKnownModuleInfoDescriptor; + } + + /** + * The last known file path location for the module-info descriptor being processed. + * + * @param lastKnownModuleInfoFile the file path location for the descriptor + */ + public void lastKnownModuleInfoFilePath(Path lastKnownModuleInfoFile) { + this.lastKnownModuleInfoFilePath = lastKnownModuleInfoFile; + } + + /** + * @return fetches the last known module info file path + */ + Path lastKnownModuleInfoFilePath() { + return lastKnownModuleInfoFilePath; + } + + /** + * @return fetches the last generated module descriptor location + */ + Path lastGeneratedModuleInfoFilePath() { + return lastGeneratedModuleInfoFilePath; + } + + /** + * Sets the last known source path being processed. + * + * @param lastKnownSourcePathBeingProcessed the last source path being processed + */ + public void lastKnownSourcePathBeingProcessed(Path lastKnownSourcePathBeingProcessed) { + this.lastKnownSourcePathBeingProcessed = lastKnownSourcePathBeingProcessed; + } + + /** + * Sets the last known type suffix (e.g., "test"). + * + * @param typeSuffix the optional type suffix + */ + public void lastKnownTypeSuffix(String typeSuffix) { + this.lastKnownTypeSuffix = typeSuffix; + } + + /** + * @return fetches the last known type suffix + */ + String lastKnownTypeSuffix() { + return lastKnownTypeSuffix; + } + + /** + * Sets the last generated package name. + * + * @param lastGeneratedPackageName the package name + */ + public void lastGeneratedPackageName(String lastGeneratedPackageName) { + this.lastGeneratedPackageName = lastGeneratedPackageName; + } + + /** + * Fetches the last generated package name. + */ + String lastGeneratedPackageName() { + return lastGeneratedPackageName; + } + + /** + * Attempts to determine the generated module name based upon the batch of services being processed. + */ + String determineGeneratedModuleName() { + String moduleName = moduleName(); + moduleName = ModuleUtils.toSuggestedModuleName(moduleName, + lastKnownTypeSuffix(), + ModuleInfoDescriptor.DEFAULT_MODULE_NAME); + return moduleName; + } + + /** + * Attempts to determine the generated package name based upon the batch of services being processed. + */ + String determineGeneratedPackageName() { + String export = lastGeneratedPackageName(); + if (export != null) { + return export; + } + + ModuleInfoDescriptor descriptor = lastKnownModuleInfoDescriptor(); + String packageName = ModuleUtils.toSuggestedGeneratedPackageName(descriptor, serviceTypeNames(), PicoServicesConfig.NAME); + return Objects.requireNonNull(packageName); + } + + /** + * Called to signal the beginning of an annotation processing phase. + * + * @param processor the processor running + * @param annotations the annotations being processed + * @param roundEnv the round env + */ + public static void onBeginProcessing(Messager processor, + Set annotations, + RoundEnvironment roundEnv) { + boolean reallyStarted = !annotations.isEmpty(); + if (reallyStarted && !roundEnv.processingOver()) { + RUNNING_PROCESSORS.incrementAndGet(); + } + processor.debug(processor.getClass() + .getSimpleName() + " processing " + annotations + "; really-started=" + reallyStarted); + } + + /** + * Called to signal the end of an annotation processing phase. + * + * @param processor the processor running + * @param annotations the annotations being processed + * @param roundEnv the round env + */ + public static void onEndProcessing(Messager processor, + Set annotations, + RoundEnvironment roundEnv) { + boolean done = annotations.isEmpty(); + if (done && roundEnv.processingOver()) { + RUNNING_PROCESSORS.decrementAndGet(); + } + processor.debug(processor.getClass().getSimpleName() + " finished processing; done-done=" + done); + + if (done && RUNNING_PROCESSORS.get() == 0) { + // perform module analysis to ensure the proper definitions are specified for modules and applications + ServicesToProcess.servicesInstance().performModuleUsageValidation(processor); + } + } + + /** + * If we made it here we know that Pico annotation processing was used. If there is a module-info in use and services where + * defined during processing, then we should have a module created and optionally and application. If so then we should + * validate the integrity of the user's module-info.java for the {@link io.helidon.pico.Module} and + * {@link io.helidon.pico.Application} definitions - unless the user opted out of this check with the + * {@link io.helidon.pico.tools.Options#TAG_IGNORE_MODULE_USAGE} option. + */ + private void performModuleUsageValidation(Messager processor) { + if (lastKnownModuleInfoFilePath != null && lastKnownModuleInfoDescriptor == null) { + throw new ToolsException("expected to have a module-info.java"); + } + + if (lastKnownModuleInfoDescriptor == null) { + return; + } + + boolean wasModuleDefined = !servicesTypeNames.isEmpty() || contracts().values().stream() + .flatMap(Collection::stream) + .anyMatch(it -> it.name().equals(Module.class.getName())); + boolean wasApplicationDefined = contracts().values().stream() + .flatMap(Collection::stream) + .anyMatch(it -> it.name().equals(Application.class.getName())); + + boolean shouldWarnOnly = Options.isOptionEnabled(Options.TAG_IGNORE_MODULE_USAGE); + String message = ". Use -A" + Options.TAG_IGNORE_MODULE_USAGE + "=true to ignore."; + + if (wasModuleDefined) { + Optional moduleInfoItem = lastKnownModuleInfoDescriptor.first(Module.class.getName()); + if (moduleInfoItem.isEmpty() || !moduleInfoItem.get().provides()) { + ToolsException te = new ToolsException("expected to have a 'provides " + Module.class.getName() + + " with ... ' entry in " + lastKnownModuleInfoFilePath + message); + if (shouldWarnOnly) { + processor.warn(te.getMessage(), te); + } else { + processor.error(te.getMessage(), te); + } + } + } + + if (wasApplicationDefined) { + Optional moduleInfoItem = lastKnownModuleInfoDescriptor.first(Application.class.getName()); + if (moduleInfoItem.isEmpty() || !moduleInfoItem.get().provides()) { + ToolsException te = new ToolsException("expected to have a 'provides " + Application.class.getName() + + " with ... ' entry in " + lastKnownModuleInfoFilePath + message); + if (shouldWarnOnly) { + processor.warn(te.getMessage(), te); + } else { + processor.error(te.getMessage(), te); + } + } + } + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TemplateHelper.java b/pico/tools/src/main/java/io/helidon/pico/tools/TemplateHelper.java index c34b66e483b..7d76d6d0a82 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/TemplateHelper.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TemplateHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -18,56 +18,54 @@ import java.io.IOException; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; -import io.helidon.common.config.Config; -import io.helidon.common.config.ConfigValue; -import io.helidon.pico.Bootstrap; +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.pico.PicoServices; import io.helidon.pico.PicoServicesConfig; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.TagType; import com.github.jknack.handlebars.Template; +import static io.helidon.pico.tools.CommonUtils.loadStringFromResource; + /** * Helper tools for dealing with Pico-related Handlebar templates. */ -class TemplateHelper { +public class TemplateHelper { /** * The tag that us used to represent the template name to use. */ - static final String TAG_TEMPLATE_NAME = PicoServicesConfig.FQN + ".template.name"; + public static final String TAG_TEMPLATE_NAME = PicoServicesConfig.FQN + ".template.name"; /** * The default template name to use. */ - static final String DEFAULT_TEMPLATE_NAME = "default"; + public static final String DEFAULT_TEMPLATE_NAME = "default"; private static final System.Logger LOGGER = System.getLogger(TemplateHelper.class.getName()); - private final Bootstrap bootstrap; - private final String providerName; private final String versionId; - private TemplateHelper(Bootstrap bootstrap) { - this.bootstrap = bootstrap; - this.providerName = toString(bootstrap, PicoServicesConfig.KEY_PROVIDER); - this.versionId = toString(bootstrap, PicoServicesConfig.KEY_VERSION); + private TemplateHelper(PicoServicesConfig cfg) { + Objects.requireNonNull(cfg.providerName(), "provider name is required"); + this.versionId = Objects.requireNonNull(cfg.providerVersion(), "provider version is required"); } /** - * Creates a template helper utility using the provided {@link io.helidon.pico.Bootstrap} configuration. + * Creates a template helper utility using the global bootstrap configuration. * - * @param bootstrap the bootstrap configuration * @return the template helper initialized with the bootstrap configuration */ - static TemplateHelper create(Bootstrap bootstrap) { - return new TemplateHelper(bootstrap); + public static TemplateHelper create() { + PicoServicesConfig cfg = PicoServices.picoServices() + .orElseThrow(() -> new ToolsException(PicoServicesConfig.NAME + " services not found")).config(); + return new TemplateHelper(cfg); } /** @@ -76,13 +74,8 @@ static TemplateHelper create(Bootstrap bootstrap) { * @param generatorClassTypeName the generator class type name * @return the generated sticker */ - public String defaultGeneratedStickerFor(String generatorClassTypeName) { - return "{" + String.join(", ", - List.of( - "provider=" + providerName, - "generator=" + generatorClassTypeName, - "ver=" + versionId)) - + "}"; + public String generatedStickerFor(String generatorClassTypeName) { + return BuilderTypeTools.generatedStickerFor(generatorClassTypeName, versionId); } /** @@ -94,12 +87,12 @@ public String defaultGeneratedStickerFor(String generatorClassTypeName) { * * @return the new string, fully resolved with substitutions */ - public String applySubstitutions(String target, + public String applySubstitutions(CharSequence target, Map props, boolean logErr) { Set missingArgs = new LinkedHashSet<>(); try { - return applySubstitutions(target, props, logErr, true, missingArgs, null, null); + return applySubstitutions(target.toString(), props, logErr, true, missingArgs, null, null); } catch (IOException e) { throw new ToolsException("unable to apply substitutions", e); } @@ -135,9 +128,8 @@ String safeLoadTemplate(String name) { * @return the template file, without substitutions applied */ String safeLoadTemplate(String templateName, - String name) { - return Objects.requireNonNull(loadTemplate(templateName, name), - "failed to load: " + toFQN(templateName, name)); + String name) { + return Objects.requireNonNull(loadTemplate(templateName, name), "failed to load: " + toFQN(templateName, name)); } /** @@ -147,8 +139,9 @@ String safeLoadTemplate(String templateName, * @param name the template name to use * @return the template, or null if not found */ - private String loadTemplate(String templateName, String name) { - return CommonUtils.loadStringFromResource(toFQN(templateName, name)); + public String loadTemplate(String templateName, + String name) { + return loadStringFromResource(toFQN(templateName, name)); } private static String toFQN(String templateName, @@ -220,14 +213,4 @@ private static String applySubstitutions(String target, return target; } - private static String toString(Bootstrap bootstrap, String key) { - Optional cfg = bootstrap.config(); - if (cfg.isEmpty()) { - return null; - } - - ConfigValue val = cfg.get().get(key).asString(); - return val.orElse(null); - } - } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java b/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java new file mode 100644 index 00000000000..dabae317296 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TypeNames.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2023 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.tools; + +/** + * Type name constants. + *

        + * This should always be used instead of dependency on the annotation and other class types. + */ +public final class TypeNames { + /** + * Package prefix {@value}. + */ + public static final String PREFIX_JAKARTA = "jakarta."; + /** + * Package prefix {@value}. + */ + public static final String PREFIX_JAVAX = "javax."; + + /** + * Pico {@value} type. + */ + public static final String PICO_APPLICATION = "io.helidon.pico.Application"; + + /** + * Pico {@value} annotation. + */ + public static final String PICO_CONFIGURED_BY = "io.helidon.pico.configdriven.ConfiguredBy"; + /** + * Pico {@value} annotation. + */ + public static final String PICO_CONTRACT = "io.helidon.pico.Contract"; + /** + * Pico {@value} annotation. + */ + public static final String PICO_EXTERNAL_CONTRACTS = "io.helidon.pico.ExternalContracts"; + /** + * Pico {@value} annotation. + */ + public static final String PICO_INTERCEPTED = "io.helidon.pico.Intercepted"; + /** + * Pico {@value} type. + */ + public static final String PICO_MODULE = "io.helidon.pico.Module"; + + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_APPLICATION_SCOPED = "jakarta.enterprise.context.ApplicationScoped"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_INJECT = "jakarta.inject.Inject"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_MANAGED_BEAN = "jakarta.annotation.ManagedBean"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_POST_CONSTRUCT = "jakarta.annotation.PostConstruct"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_PRE_DESTROY = "jakarta.annotation.PreDestroy"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_PRIORITY = "jakarta.annotation.Priority"; + /** + * Jakarta {@value} type. + */ + public static final String JAKARTA_PROVIDER = "jakarta.inject.Provider"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_QUALIFIER = "jakarta.inject.Qualifier"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_RESOURCE = "jakarta.annotation.Resource"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_RESOURCES = "jakarta.annotation.Resources"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_SCOPE = "jakarta.inject.Scope"; + /** + * Jakarta {@value} annotation. + */ + public static final String JAKARTA_SINGLETON = "jakarta.inject.Singleton"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_ACTIVATE_REQUEST_CONTEXT = "jakarta.enterprise.context.control.ActivateRequestContext"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_ALTERNATIVE = "jakarta.enterprise.inject.Alternative"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_BEFORE_DESTROYED = "jakarta.enterprise.context.BeforeDestroyed"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_CONVERSATION_SCOPED = "jakarta.enterprise.context.ConversationScoped"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_DEPENDENT = "jakarta.enterprise.context.Dependent"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_DESTROYED = "jakarta.enterprise.context.Destroyed"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_DISPOSES = "jakarta.enterprise.inject.Disposes"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_INITIALIZED = "jakarta.enterprise.context.Initialized"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_INTERCEPTED = "jakarta.enterprise.inject.Intercepted"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_MODEL = "jakarta.enterprise.inject.Model"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_NONBINDING = "jakarta.enterprise.util.Nonbinding"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_NORMAL_SCOPE = "jakarta.enterprise.context.NormalScope"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_OBSERVES = "jakarta.enterprise.event.Observes"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_OBSERVES_ASYNC = "jakarta.enterprise.event.ObservesAsync"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_PRODUCES = "jakarta.enterprise.inject.Produces"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_REQUEST_SCOPED = "jakarta.enterprise.context.RequestScoped"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_SESSION_SCOPED = "jakarta.enterprise.context.SessionScoped"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_SPECIALIZES = "jakarta.enterprise.inject.Specializes"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_STEREOTYPE = "jakarta.enterprise.inject.Stereotype"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_TRANSIENT_REFERENCE = "jakarta.enterprise.inject.TransientReference"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_TYPED = "jakarta.enterprise.inject.Typed"; + /** + * Jakarta CDI {@value} annotation. + */ + public static final String JAKARTA_CDI_VETOED = "jakarta.enterprise.inject.Vetoed"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_APPLICATION_SCOPED = "javax.enterprise.context.ApplicationScoped"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_INJECT = "javax.inject.Inject"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_POST_CONSTRUCT = "javax.annotation.PostConstruct"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_PRE_DESTROY = "javax.annotation.PreDestroy"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_QUALIFIER = "javax.inject.Qualifier"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_PRIORITY = "javax.annotation.Priority"; + /** + * Jakarta legacy {@value} type. + */ + public static final String JAVAX_PROVIDER = "javax.inject.Provider"; + /** + * Jakarta legacy {@value} annotation. + */ + public static final String JAVAX_SINGLETON = "javax.inject.Singleton"; + + private TypeNames() { + } +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java new file mode 100644 index 00000000000..f09f893f06c --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java @@ -0,0 +1,1498 @@ +/* + * Copyright (c) 2022, 2023 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.tools; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +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.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.util.Elements; + +import io.helidon.builder.processor.tools.BuilderTypeTools; +import io.helidon.common.LazyValue; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultElementInfo; +import io.helidon.pico.DefaultInjectionPointInfo; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.ElementInfo; +import io.helidon.pico.InjectionPointInfo; +import io.helidon.pico.InjectionPointProvider; +import io.helidon.pico.PicoServicesConfig; +import io.helidon.pico.QualifierAndValue; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.services.Dependencies; + +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.AnnotationInfoList; +import io.github.classgraph.AnnotationParameterValue; +import io.github.classgraph.AnnotationParameterValueList; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassRefTypeSignature; +import io.github.classgraph.ClassTypeSignature; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.MethodInfoList; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.TypeArgument; +import io.github.classgraph.TypeSignature; +import jakarta.inject.Provider; + +import static io.helidon.pico.tools.CommonUtils.first; +import static io.helidon.pico.tools.CommonUtils.hasValue; + +/** + * Generically handles Pico generated artifact creation via APT. + */ +@SuppressWarnings("deprecation") +public final class TypeTools extends BuilderTypeTools { + private static final LazyValue> OBJ_TYPES = LazyValue.create(() -> { + Map map = new LinkedHashMap<>(); + map.put(boolean.class.getName(), DefaultTypeName.create(Boolean.class)); + map.put(byte.class.getName(), DefaultTypeName.create(Byte.class)); + map.put(char.class.getName(), DefaultTypeName.create(Character.class)); + map.put(short.class.getName(), DefaultTypeName.create(Short.class)); + map.put(int.class.getName(), DefaultTypeName.create(Integer.class)); + map.put(long.class.getName(), DefaultTypeName.create(Long.class)); + map.put(float.class.getName(), DefaultTypeName.create(Float.class)); + map.put(double.class.getName(), DefaultTypeName.create(Double.class)); + return map; + }); + + private TypeTools() { + } + + /** + * Converts the provided name to a type name path. + * + * @param typeName the type name to evaluate + * @return the file path expression where dots are translated to file separators + */ + public static String toFilePath(TypeName typeName) { + return toFilePath(typeName, ".java"); + } + + /** + * Converts the provided name to a type name path. + * + * @param typeName the type name to evaluate + * @param fileType the file type, typically ".java" + * @return the file path expression where dots are translated to file separators + */ + public static String toFilePath(TypeName typeName, + String fileType) { + String className = typeName.className(); + String packageName = typeName.packageName(); + if (!hasValue(packageName)) { + packageName = ""; + } else { + packageName = packageName.replace('.', File.separatorChar); + } + + if (!fileType.startsWith(".")) { + fileType = "." + fileType; + } + + return packageName + File.separatorChar + className + fileType; + } + + /** + * Creates a type name from a classInfo. + * + * @param classInfo the classInfo + * @return the typeName for the class info + */ + static TypeName createTypeNameFromClassInfo(ClassInfo classInfo) { + if (classInfo == null) { + return null; + } + return DefaultTypeName.create(classInfo.getPackageName(), classInfo.getSimpleName()); + } + + /** + * Will convert any primitive type name to its Object type counterpart. If not primitive, will return the value passed. + * + * @param type the type name + * @return the Object type name for the given type (e.g., "int.class" -> "Integer.class") + */ + static TypeName toObjectTypeName(String type) { + TypeName result = OBJ_TYPES.get().get(type); + return (result == null) ? DefaultTypeName.createFromTypeName(type) : result; + } + + /** + * Creates an instance for an annotation with a value. + * + * @param annotation the annotation + * @return the new instance + * @deprecated switch to using pure annotation processing wherever possible + */ + @Deprecated + static AnnotationAndValue createAnnotationAndValueFromAnnotation(Annotation annotation) { + return DefaultAnnotationAndValue.create(DefaultTypeName.create(annotation.annotationType()), extractValues(annotation)); + } + + /** + * Creates an instance from reflective access. Note that this approach will only have visibility to the + * {@link java.lang.annotation.RetentionPolicy#RUNTIME} type annotations. + * + * @param annotations the annotations on the type, method, or parameters + * @return the new instance + * @deprecated switch to use pure annotation processing instead of reflection + */ + @Deprecated + public static List createAnnotationAndValueListFromAnnotations(Annotation[] annotations) { + if (annotations == null || annotations.length <= 0) { + return List.of(); + } + + return Arrays.stream(annotations).map(TypeTools::createAnnotationAndValueFromAnnotation).collect(Collectors.toList()); + } + + /** + * Extracts values from the annotation. + * + * @param annotation the annotation + * @return the extracted value + * @deprecated switch to use pure annotation processing instead of reflection + */ + @Deprecated + static Map extractValues(Annotation annotation) { + Map result = new HashMap<>(); + + Class aClass = annotation.annotationType(); + Method[] declaredMethods = aClass.getDeclaredMethods(); + for (Method declaredMethod : declaredMethods) { + String propertyName = declaredMethod.getName(); + try { + Object value = declaredMethod.invoke(annotation); + if (!(value instanceof Annotation)) { + String stringValue; + // check if array + if (value.getClass().isArray()) { + if (value.getClass().getComponentType().equals(Annotation.class)) { + stringValue = "array of annotations"; + } else { + String[] stringArray; + if (value.getClass().getComponentType().equals(String.class)) { + stringArray = (String[]) value; + } else { + stringArray = new String[Array.getLength(value)]; + for (int i = 0; i < stringArray.length; i++) { + stringArray[i] = String.valueOf(Array.get(value, i)); + } + } + + stringValue = String.join(", ", stringArray); + } + } else { + // just convert to String + stringValue = String.valueOf(value); + } + result.put(propertyName, stringValue); + } + } catch (Throwable ignored) { + } + } + return result; + } + + /** + * Extracts values from the annotation info list. + * + * @param values the annotation values + * @return the extracted values + */ + static Map extractValues(AnnotationParameterValueList values) { + return values.asMap().entrySet().stream() + .map(e -> new AbstractMap + .SimpleEntry<>(e.getKey(), toString(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Converts "Optional" or "Provider" -> "Whatever". + * + * @param typeName the type name, that might use generics + * @return the generics component type name + */ + static String componentTypeNameOf(String typeName) { + int pos = typeName.indexOf('<'); + if (pos < 0) { + return typeName; + } + + int lastPos = typeName.indexOf('>', pos); + assert (lastPos > pos) : typeName; + return typeName.substring(pos + 1, lastPos); + } + + private static String toString(AnnotationParameterValue val) { + if (val == null) { + return null; + } + + Object v = val.getValue(); + if (v == null) { + return null; + } + + Class clazz = v.getClass(); + if (!clazz.isArray()) { + return v.toString(); + } + + Object[] arr = (Object[]) v; + return "{" + CommonUtils.toString(Arrays.asList(arr)) + "}"; + } + + /** + * Creates a set of qualifiers based upon class info introspection. + * + * @param classInfo the class info + * @return the qualifiers + */ + static Set createQualifierAndValueSet(ClassInfo classInfo) { + return createQualifierAndValueSet(classInfo.getAnnotationInfo()); + } + + /** + * Creates a set of qualifiers based upon method info introspection. + * + * @param methodInfo the method info + * @return the qualifiers + */ + static Set createQualifierAndValueSet(MethodInfo methodInfo) { + return createQualifierAndValueSet(methodInfo.getAnnotationInfo()); + } + + /** + * Creates a set of qualifiers based upon field info introspection. + * + * @param fieldInfo the field info + * @return the qualifiers + */ + static Set createQualifierAndValueSet(FieldInfo fieldInfo) { + return createQualifierAndValueSet(fieldInfo.getAnnotationInfo()); + } + + /** + * Creates a set of qualifiers given the owning element. + * + * @param annotationInfoList the list of annotations + * @return the qualifiers + */ + static Set createQualifierAndValueSet(AnnotationInfoList annotationInfoList) { + Set set = createAnnotationAndValueSetFromMetaAnnotation(annotationInfoList, + TypeNames.JAKARTA_QUALIFIER); + if (set.isEmpty()) { + return Set.of(); + } + + return set.stream().map(DefaultQualifierAndValue::convert) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates a set of qualifiers given the owning element. + + * @param type the element type (from anno processing) + * @return the set of qualifiers that the owning element has + */ + public static Set createQualifierAndValueSet(Element type) { + return createQualifierAndValueSet(type.getAnnotationMirrors()); + } + + /** + * Creates a set of qualifiers given the owning element's annotation type mirror. + + * @param annoMirrors the annotation type mirrors (from anno processing) + * @return the set of qualifiers that the owning element has + */ + public static Set createQualifierAndValueSet(List annoMirrors) { + Set result = new LinkedHashSet<>(); + + for (AnnotationMirror annoMirror : annoMirrors) { + if (findAnnotationMirror(TypeNames.JAKARTA_QUALIFIER, annoMirror.getAnnotationType() + .asElement() + .getAnnotationMirrors()) + .isPresent()) { + + String val = null; + for (Map.Entry e : annoMirror.getElementValues() + .entrySet()) { + if (e.getKey().toString().equals("value()")) { + val = String.valueOf(e.getValue().getValue()); + break; + } + } + DefaultQualifierAndValue.Builder qualifier = DefaultQualifierAndValue.builder(); + qualifier.typeName(TypeTools.createTypeNameFromDeclaredType(annoMirror.getAnnotationType()).orElseThrow()); + if (val != null) { + qualifier.value(val); + } + result.add(qualifier.build()); + } + } + + if (result.isEmpty()) { + for (AnnotationMirror annoMirror : annoMirrors) { + Optional mirror = findAnnotationMirror(TypeNames.JAVAX_QUALIFIER, + annoMirror.getAnnotationType().asElement() + .getAnnotationMirrors()); + + if (mirror.isPresent()) { + // there is an annotation meta-annotated with @javax.inject.Qualifier, let's add it to the list + Map annoValues = annoMirror.getElementValues(); + + Map values = new HashMap<>(); + annoValues.forEach((method, value) -> { + values.put(method.getSimpleName().toString(), String.valueOf(value.getValue())); + }); + + TypeName annot = DefaultTypeName.createFromTypeName(annoMirror.getAnnotationType().toString()); + result.add(DefaultQualifierAndValue.create(annot, values)); + } + } + } + + return result; + } + + /** + * Creates a set of annotations based upon class info introspection. + * + * @param classInfo the class info + * @return the annotation value set + */ + static Set createAnnotationAndValueSet(ClassInfo classInfo) { + return classInfo.getAnnotationInfo().stream() + .map(TypeTools::createAnnotationAndValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates a set of annotations based using annotation processor. + * + * @param type the enclosing/owing type element + * @return the annotation value set + */ + public static Set createAnnotationAndValueSet(Element type) { + return type.getAnnotationMirrors().stream() + .map(TypeTools::createAnnotationAndValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates a set of annotations based using annotation processor. + * + * @param annoMirrors the annotation type mirrors + * @return the annotation value set + */ + static Set createAnnotationAndValueSet(List annoMirrors) { + return annoMirrors.stream() + .map(TypeTools::createAnnotationAndValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates a set of annotations given the owning element. + * + * @param annotationInfoList the list of annotations + * @return the annotation and value set + */ + public static Set createAnnotationAndValueSet(AnnotationInfoList annotationInfoList) { + return annotationInfoList.stream() + .map(TypeTools::createAnnotationAndValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Creates an annotation and value from introspection. + * + * @param annotationInfo the introspected annotation + * @return the annotation and value + */ + static AnnotationAndValue createAnnotationAndValue(AnnotationInfo annotationInfo) { + TypeName annoTypeName = createTypeNameFromClassInfo(annotationInfo.getClassInfo()); + return DefaultAnnotationAndValue.create(annoTypeName, extractValues(annotationInfo.getParameterValues())); + } + + /** + * Creates an annotation and value from introspection. + * + * @param annotationMirror the introspected annotation + * @return the annotation and value + */ + static AnnotationAndValue createAnnotationAndValue(AnnotationMirror annotationMirror) { + TypeName annoTypeName = createTypeNameFromMirror(annotationMirror.getAnnotationType()).orElseThrow(); + return DefaultAnnotationAndValue.create(annoTypeName, extractValues(annotationMirror.getElementValues())); + } + + /** + * All annotations on every public method and the given type, including all of its methods. + * + * @param classInfo the classInfo of the enclosing class type + * @return the complete set of annotations + */ + static Set gatherAllAnnotationsUsedOnPublicNonStaticMethods(ClassInfo classInfo) { + Set result = new LinkedHashSet<>(createAnnotationAndValueSet(classInfo)); + classInfo.getMethodAndConstructorInfo() + .filter(m -> !m.isPrivate() && !m.isStatic()) + .forEach(mi -> result.addAll(createAnnotationAndValueSet(mi.getAnnotationInfo()))); + return result; + } + + /** + * All annotations on every public method and the given type, including all of its methods. + * + * @param serviceTypeElement the service type element of the enclosing class type + * @param processEnv the processing environment + * @return the complete set of annotations + */ + static Set gatherAllAnnotationsUsedOnPublicNonStaticMethods(TypeElement serviceTypeElement, + ProcessingEnvironment processEnv) { + Elements elementUtils = processEnv.getElementUtils(); + Set result = new LinkedHashSet<>(); + createAnnotationAndValueSet(serviceTypeElement).forEach(anno -> { + TypeElement typeElement = elementUtils.getTypeElement(anno.typeName().name()); + if (typeElement != null) { + typeElement.getAnnotationMirrors() + .forEach(am -> result.add(createAnnotationAndValue(am))); + } + }); + serviceTypeElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.METHOD) + .filter(e -> !e.getModifiers().contains(javax.lang.model.element.Modifier.PRIVATE)) + .filter(e -> !e.getModifiers().contains(javax.lang.model.element.Modifier.STATIC)) + .map(ExecutableElement.class::cast) + .forEach(exec -> { + exec.getAnnotationMirrors().forEach(am -> { + AnnotationAndValue anno = createAnnotationAndValue(am); + result.add(anno); + TypeElement typeElement = elementUtils.getTypeElement(anno.typeName().name()); + if (typeElement != null) { + typeElement.getAnnotationMirrors() + .forEach(am2 -> result.add(createAnnotationAndValue(am2))); + } + }); + }); + return result; + } + + /** + * Creates a set of annotation that are meta-annotated given the owning element. + * + * @param annotationInfoList the list of annotations + * @param metaAnnoType the meta-annotation type + * @return the qualifiers + */ + static Set createAnnotationAndValueSetFromMetaAnnotation(AnnotationInfoList annotationInfoList, + String metaAnnoType) { + // resolve meta annotations uses the opposite of already + AnnotationInfoList list = resolveMetaAnnotations(annotationInfoList, metaAnnoType); + if (list == null) { + return Set.of(); + } + + Set result = new LinkedHashSet<>(); + for (AnnotationInfo ai : list) { + TypeName annotationType = DefaultTypeName.createFromTypeName(translate(ai.getName())); + AnnotationParameterValueList values = ai.getParameterValues(); + if (values == null || values.isEmpty()) { + result.add(DefaultAnnotationAndValue.create(annotationType)); + } else if (values.size() > 1) { + Map strVals = extractValues(values); + result.add(DefaultAnnotationAndValue.create(annotationType, strVals)); + } else { + Object value = values.get(0).getValue(); + String strValue = (value != null) ? String.valueOf(value) : null; + AnnotationAndValue annotationAndValue = (strValue == null) + ? DefaultAnnotationAndValue.create(annotationType) + : DefaultAnnotationAndValue.create(annotationType, strValue); + result.add(annotationAndValue); + } + } + return result; + } + + /** + * Extract the scope name from the given introspected class. + * + * @param classInfo the introspected class + * @return the scope name, or null if no scope found + */ + static String extractScopeTypeName(ClassInfo classInfo) { + AnnotationInfoList list = resolveMetaAnnotations(classInfo.getAnnotationInfo(), TypeNames.JAKARTA_SCOPE); + if (list == null) { + return null; + } + + return translate(first(list, false).getName()); + } + + /** + * Returns the methods that have an annotation. + * + * @param classInfo the class info + * @param annoType the annotation + * @return the methods with the annotation + */ + static MethodInfoList methodsAnnotatedWith(ClassInfo classInfo, + String annoType) { + MethodInfoList result = new MethodInfoList(); + classInfo.getMethodInfo() + .stream() + .filter(methodInfo -> hasAnnotation(methodInfo, annoType)) + .forEach(result::add); + return result; + } + + /** + * Returns true if the method has an annotation. + * + * @param methodInfo the method info + * @param annoTypeName the annotation to check + * @return true if the method has the annotation + */ + static boolean hasAnnotation(MethodInfo methodInfo, + String annoTypeName) { + return methodInfo.hasAnnotation(annoTypeName) || methodInfo.hasAnnotation(oppositeOf(annoTypeName)); + } + + /** + * Returns true if the method has an annotation. + * + * @param fieldInfo the field info + * @param annoTypeName the annotation to check + * @return true if the method has the annotation + */ + static boolean hasAnnotation(FieldInfo fieldInfo, + String annoTypeName) { + return fieldInfo.hasAnnotation(annoTypeName) || fieldInfo.hasAnnotation(oppositeOf(annoTypeName)); + } + + /** + * Resolves meta annotations. + * + * @param annoList the complete set of annotations + * @param metaAnnoType the meta-annotation to filter on + * @return the filtered set having the meta-annotation + */ + static AnnotationInfoList resolveMetaAnnotations(AnnotationInfoList annoList, + String metaAnnoType) { + if (annoList == null) { + return null; + } + + AnnotationInfoList result = null; + String metaName2 = oppositeOf(metaAnnoType); + for (AnnotationInfo ai : annoList) { + ClassInfo aiClassInfo = ai.getClassInfo(); + if (aiClassInfo.hasAnnotation(metaAnnoType) || aiClassInfo.hasAnnotation(metaName2)) { + if (result == null) { + result = new AnnotationInfoList(); + } + result.add(ai); + } + } + return result; + } + + /** + * Determines the {@link jakarta.inject.Provider} or {@link io.helidon.pico.InjectionPointProvider} contract type. + * + * @param classInfo class info + * @return the provided type + */ + static String providesContractType(ClassInfo classInfo) { + Set candidates = new LinkedHashSet<>(); + + ClassInfo nxt = classInfo; + ToolsException firstTe = null; + while (nxt != null) { + ClassTypeSignature sig = nxt.getTypeSignature(); + List superInterfaces = (sig != null) ? sig.getSuperinterfaceSignatures() : null; + if (superInterfaces != null) { + for (ClassRefTypeSignature superInterface : superInterfaces) { + if (!isProviderType(superInterface)) { + continue; + } + + try { + candidates.add(Objects.requireNonNull( + providerTypeOf(superInterface.getTypeArguments().get(0), classInfo))); + } catch (ToolsException te) { + if (firstTe == null) { + firstTe = te; + } + } + } + } + + nxt = nxt.getSuperclass(); + } + + if (candidates.size() > 1) { + throw new ToolsException("unsupported case where provider provides more than one type: " + classInfo); + } + + if (!candidates.isEmpty()) { + return first(candidates, false); + } + + if (firstTe != null) { + throw firstTe; + } + + return null; + } + + /** + * Should only be called if the encloser of the typeArgument is known to be Provider type. + */ + private static String providerTypeOf(TypeArgument typeArgument, + Object enclosingElem) { + if (!(typeArgument.getTypeSignature() instanceof ClassRefTypeSignature)) { + throw new ToolsException("unsupported provider<> type of " + typeArgument + " in " + enclosingElem); + } + return typeArgument.toString(); + } + + /** + * Determines the {@link jakarta.inject.Provider} contract type. + * + * @param sig class type signature + * @return the provided type + */ + static String providesContractType(TypeSignature sig) { + if (sig instanceof ClassRefTypeSignature) { + ClassRefTypeSignature csig = (ClassRefTypeSignature) sig; + if (isProviderType(csig.getFullyQualifiedClassName())) { + TypeArgument typeArg = csig.getTypeArguments().get(0); + if (!(typeArg.getTypeSignature() instanceof ClassRefTypeSignature)) { + throw new ToolsException("unsupported: " + sig); + } + return typeArg.toString(); + } + } + return null; + } + + /** + * Returns the injection point info given a method element. + * + * @param elemInfo the method element info + * @return the injection point info + */ + static InjectionPointInfo createInjectionPointInfo(MethodInfo elemInfo) { + return createInjectionPointInfo(createTypeNameFromClassInfo(elemInfo.getClassInfo()), elemInfo, null); + } + + /** + * Returns the injection point info given a method element. + * + * @param serviceTypeName the enclosing service type name + * @param elemInfo the method element info + * @param elemOffset optionally, the argument position (or null for the method level) - starts at 1 not 0 + * @return the injection point info + */ + static InjectionPointInfo createInjectionPointInfo(TypeName serviceTypeName, + MethodInfo elemInfo, + Integer elemOffset) { + String elemType; + Set qualifiers; + Set annotations; + AtomicReference isProviderWrapped = new AtomicReference<>(); + AtomicReference isListWrapped = new AtomicReference<>(); + AtomicReference isOptionalWrapped = new AtomicReference<>(); + if (elemOffset != null) { + MethodParameterInfo paramInfo = elemInfo.getParameterInfo()[elemOffset - 1]; + elemType = extractInjectionPointTypeInfo(paramInfo, isProviderWrapped, isListWrapped, isOptionalWrapped); + qualifiers = createQualifierAndValueSet(paramInfo.getAnnotationInfo()); + annotations = createAnnotationAndValueSet(paramInfo.getAnnotationInfo()); + } else { + elemType = elemInfo.getTypeDescriptor().getResultType().toString(); + qualifiers = createQualifierAndValueSet(elemInfo); + annotations = createAnnotationAndValueSet(elemInfo.getAnnotationInfo()); + } + String elemName = elemInfo.isConstructor() + ? InjectionPointInfo.CONSTRUCTOR : elemInfo.getName(); + int elemArgs = elemInfo.getParameterInfo().length; + ElementInfo.Access access = toAccess(elemInfo.getModifiers()); + String packageName = serviceTypeName.packageName(); + ServiceInfoCriteria serviceInfo = DefaultServiceInfoCriteria.builder() + .serviceTypeName(elemType) + .build(); + return DefaultInjectionPointInfo.builder() + .baseIdentity(Dependencies.toMethodBaseIdentity(elemName, elemArgs, access, () -> packageName)) + .id(Dependencies.toMethodIdentity(elemName, elemArgs, elemOffset, access, () -> packageName)) + .dependencyToServiceInfo(serviceInfo) + .serviceTypeName(serviceTypeName.name()) + .elementName(elemName) + .elementKind(elemInfo.isConstructor() + ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) + .elementTypeName(elemType) + .elementArgs(elemArgs) + .elementOffset(elemOffset) + .access(access) + .staticDeclaration(isStatic(elemInfo.getModifiers())) + .qualifiers(qualifiers) + .annotations(annotations) + .optionalWrapped(isOptionalWrapped.get()) + .providerWrapped(isProviderWrapped.get()) + .listWrapped(isListWrapped.get()) + .build(); + } + + /** + * Returns the method info given a method from introspection. + * + * @param methodInfo the method element info + * @param serviceLevelAnnos the annotation at the class level that should be inherited at the method level + * @return the method info + */ + static MethodElementInfo createMethodElementInfo(MethodInfo methodInfo, + Set serviceLevelAnnos) { + TypeName serviceTypeName = createTypeNameFromClassInfo(methodInfo.getClassInfo()); + String elemType = methodInfo.getTypeDescriptor().getResultType().toString(); + Set qualifiers = createQualifierAndValueSet(methodInfo); + Set annotations = createAnnotationAndValueSet(methodInfo.getAnnotationInfo()); + if (serviceLevelAnnos != null) { + annotations.addAll(serviceLevelAnnos); + } + List throwables = extractThrowableTypeNames(methodInfo); + List parameters = createParameterInfo(serviceTypeName, methodInfo); + return DefaultMethodElementInfo.builder() + .serviceTypeName(serviceTypeName.name()) + .elementName(methodInfo.isConstructor() + ? InjectionPointInfo.CONSTRUCTOR : methodInfo.getName()) + .elementKind(methodInfo.isConstructor() + ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) + .elementTypeName(elemType) + .elementArgs(methodInfo.getParameterInfo().length) + .elementOffset(Optional.empty()) + .access(toAccess(methodInfo.getModifiers())) + .staticDeclaration(isStatic(methodInfo.getModifiers())) + .annotations(annotations) + .qualifiers(qualifiers) + .throwableTypeNames(throwables) + .parameterInfo(parameters) + .build(); + } + + /** + * Returns the method info given a method from annotation processing. + * + * @param serviceTypeElement the enclosing service type for the provided element + * @param ee the method element info + * @param serviceLevelAnnos the annotation at the class level that should be inherited at the method level + * @return the method info + */ + static MethodElementInfo createMethodElementInfo(TypeElement serviceTypeElement, + ExecutableElement ee, + Set serviceLevelAnnos) { + TypeName serviceTypeName = createTypeNameFromElement(serviceTypeElement).orElseThrow(); + String elemType = ee.getReturnType().toString(); + Set qualifiers = createQualifierAndValueSet(ee); + Set annotations = createAnnotationAndValueSet(ee); + if (serviceLevelAnnos != null) { + annotations.addAll(serviceLevelAnnos); + } + List throwables = extractThrowableTypeNames(ee); + List parameters = createParameterInfo(serviceTypeName, ee); + return DefaultMethodElementInfo.builder() + .serviceTypeName(serviceTypeName.name()) + .elementName((ee.getKind() == ElementKind.CONSTRUCTOR) + ? InjectionPointInfo.CONSTRUCTOR : ee.getSimpleName().toString()) + .elementKind((ee.getKind() == ElementKind.CONSTRUCTOR) + ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) + .elementTypeName(elemType) + .elementArgs(ee.getParameters().size()) + .elementOffset(Optional.empty()) + .access(toAccess(ee)) + .staticDeclaration(isStatic(ee)) + .qualifiers(qualifiers) + .annotations(annotations) + .throwableTypeNames(throwables) + .parameterInfo(parameters) + .build(); + } + + /** + * Returns the throwable types on a method. + * + * @param methodInfo the method info + * @return the list of throwable type names + */ + private static List extractThrowableTypeNames(MethodInfo methodInfo) { + String[] thrownExceptionNames = methodInfo.getThrownExceptionNames(); + if (thrownExceptionNames == null || thrownExceptionNames.length == 0) { + return List.of(); + } + + return Arrays.asList(thrownExceptionNames); + } + + /** + * Returns the throwable types on a method. + * + * @param methodInfo the method info + * @return the list of throwable type names + */ + private static List extractThrowableTypeNames(ExecutableElement methodInfo) { + List thrownExceptionTypes = methodInfo.getThrownTypes(); + if (thrownExceptionTypes == null) { + return List.of(); + } + return thrownExceptionTypes.stream().map(TypeMirror::toString).collect(Collectors.toList()); + } + + /** + * Returns the list of parameter info through introspection. + * + * @param serviceTypeName the enclosing service type name + * @param methodInfo the method info + * @return the list of info elements/parameters + */ + private static List createParameterInfo(TypeName serviceTypeName, + MethodInfo methodInfo) { + List result = new ArrayList<>(); + int count = 0; + for (MethodParameterInfo ignore : methodInfo.getParameterInfo()) { + count++; + result.add(createParameterInfo(serviceTypeName, methodInfo, count)); + } + return result; + } + + /** + * Returns the list of parameter info through annotation processing. + * + * @param serviceTypeName the enclosing service type name + * @param methodInfo the method info + * @return the list of info elements/parameters + */ + private static List createParameterInfo(TypeName serviceTypeName, + ExecutableElement methodInfo) { + List result = new ArrayList<>(); + int count = 0; + for (VariableElement ignore : methodInfo.getParameters()) { + count++; + result.add(createParameterInfo(serviceTypeName, methodInfo, count)); + } + return result; + } + + /** + * Returns the element info given a method element parameter. + * + * @param serviceTypeName the enclosing service type name + * @param elemInfo the method element info + * @param elemOffset the argument position - starts at 1 not 0 + * @return the element info + */ + static DefaultElementInfo createParameterInfo(TypeName serviceTypeName, + MethodInfo elemInfo, + int elemOffset) { + MethodParameterInfo paramInfo = elemInfo.getParameterInfo()[elemOffset - 1]; + String elemType = paramInfo.getTypeDescriptor().toString(); + Set annotations = createAnnotationAndValueSet(paramInfo.getAnnotationInfo()); + return DefaultElementInfo.builder() + .serviceTypeName(serviceTypeName.name()) + .elementName("p" + elemOffset) + .elementKind(elemInfo.isConstructor() + ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) + .elementTypeName(elemType) + .elementArgs(elemInfo.getParameterInfo().length) + .elementOffset(elemOffset) + .access(toAccess(elemInfo.getModifiers())) + .staticDeclaration(isStatic(elemInfo.getModifiers())) + .annotations(annotations) + .build(); + } + + /** + * Returns the element info given a method element parameter. + * + * @param serviceTypeName the enclosing service type name + * @param elemInfo the method element info + * @param elemOffset the argument position - starts at 1 not 0 + * @return the element info + */ + static DefaultElementInfo createParameterInfo(TypeName serviceTypeName, + ExecutableElement elemInfo, + int elemOffset) { + VariableElement paramInfo = elemInfo.getParameters().get(elemOffset - 1); + String elemType = paramInfo.asType().toString(); + Set annotations = createAnnotationAndValueSet(paramInfo.getAnnotationMirrors()); + return DefaultElementInfo.builder() + .serviceTypeName(serviceTypeName.name()) + .elementName("p" + elemOffset) + .elementKind(elemInfo.getKind() == ElementKind.CONSTRUCTOR + ? InjectionPointInfo.ElementKind.CONSTRUCTOR : InjectionPointInfo.ElementKind.METHOD) + .elementTypeName(elemType) + .elementArgs(elemInfo.getParameters().size()) + .elementOffset(elemOffset) + .access(toAccess(elemInfo)) + .staticDeclaration(isStatic(elemInfo)) + .annotations(annotations) + .build(); + } + + /** + * Returns the injection point info given a field element. + * + * @param serviceTypeName the enclosing service type name + * @param elemInfo the field element info + * @return the injection point info + */ + static InjectionPointInfo createInjectionPointInfo(TypeName serviceTypeName, + FieldInfo elemInfo) { + AtomicReference isProviderWrapped = new AtomicReference<>(); + AtomicReference isListWrapped = new AtomicReference<>(); + AtomicReference isOptionalWrapped = new AtomicReference<>(); + String elemType = extractInjectionPointTypeInfo(elemInfo, isProviderWrapped, isListWrapped, isOptionalWrapped); + Set qualifiers = createQualifierAndValueSet(elemInfo); + String elemName = elemInfo.getName(); + String id = Dependencies.toFieldIdentity(elemName, serviceTypeName::packageName); + ServiceInfoCriteria serviceInfo = DefaultServiceInfoCriteria.builder() + .serviceTypeName(elemType) + .build(); + return DefaultInjectionPointInfo.builder() + .baseIdentity(id) + .id(id) + .dependencyToServiceInfo(serviceInfo) + .serviceTypeName(serviceTypeName.name()) + .elementName(elemInfo.getName()) + .elementKind(InjectionPointInfo.ElementKind.FIELD) + .elementTypeName(elemType) + .access(toAccess(elemInfo.getModifiers())) + .staticDeclaration(isStatic(elemInfo.getModifiers())) + .qualifiers(qualifiers) + .optionalWrapped(isOptionalWrapped.get()) + .providerWrapped(isProviderWrapped.get()) + .listWrapped(isListWrapped.get()) + .build(); + } + + /** + * Determines the meta parts making up {@link InjectionPointInfo}. + * + * @param paramInfo the method info + * @param isProviderWrapped set to indicate that the ip is a provided type + * @param isListWrapped set to indicate that the ip is a list type + * @param isOptionalWrapped set to indicate that the ip is am optional type + * @return the return type of the parameter type + */ + static String extractInjectionPointTypeInfo(MethodParameterInfo paramInfo, + AtomicReference isProviderWrapped, + AtomicReference isListWrapped, + AtomicReference isOptionalWrapped) { + TypeSignature sig = Objects.requireNonNull(paramInfo).getTypeSignature(); + if (sig == null) { + sig = Objects.requireNonNull(paramInfo.getTypeDescriptor()); + } + return extractInjectionPointTypeInfo(sig, paramInfo.getMethodInfo(), + isProviderWrapped, isListWrapped, isOptionalWrapped); + } + + /** + * Determines the meta parts making up {@link InjectionPointInfo}. + * + * @param elemInfo the field info + * @param isProviderWrapped set to indicate that the ip is a provided type + * @param isListWrapped set to indicate that the ip is a list type + * @param isOptionalWrapped set to indicate that the ip is an optional type + * @return the return type of the injection point + */ + static String extractInjectionPointTypeInfo(FieldInfo elemInfo, + AtomicReference isProviderWrapped, + AtomicReference isListWrapped, + AtomicReference isOptionalWrapped) { + TypeSignature sig = Objects.requireNonNull(elemInfo).getTypeSignature(); + if (sig == null) { + sig = Objects.requireNonNull(elemInfo.getTypeDescriptor()); + } + return extractInjectionPointTypeInfo(sig, elemInfo.getClassInfo(), + isProviderWrapped, isListWrapped, isOptionalWrapped); + } + + private static ClassRefTypeSignature toClassRefSignature(TypeSignature sig, + Object enclosingElem) { + if (!(Objects.requireNonNull(sig) instanceof ClassRefTypeSignature)) { + throw new ToolsException("unsupported type for " + sig + " in " + enclosingElem); + } + return (ClassRefTypeSignature) sig; + } + + private static ClassRefTypeSignature toClassRefSignature( + TypeArgument arg, + Object enclosingElem) { + return toClassRefSignature(arg.getTypeSignature(), enclosingElem); + } + + /** + * Determines the meta parts making up {@link InjectionPointInfo} for reflective processing. + * + * @param sig the variable / element type + * @param isProviderWrapped set to indicate that the ip is a provided type + * @param isListWrapped set to indicate that the ip is a list type + * @param isOptionalWrapped set to indicate that the ip is an optional type + * @return the return type of the injection point + */ + static String extractInjectionPointTypeInfo(TypeSignature sig, + Object enclosingElem, + AtomicReference isProviderWrapped, + AtomicReference isListWrapped, + AtomicReference isOptionalWrapped) { + ClassRefTypeSignature csig = toClassRefSignature(sig, enclosingElem); + boolean isProvider = false; + boolean isOptional = false; + boolean isList = false; + String varTypeName = csig.toString(); + boolean handled = csig.getTypeArguments().isEmpty(); + if (1 == csig.getTypeArguments().size()) { + isProvider = isProviderType(csig); + isOptional = isOptionalType(csig); + if (isProvider || isOptional) { + ClassRefTypeSignature typeArgSig = toClassRefSignature(csig.getTypeArguments().get(0), enclosingElem); + varTypeName = typeArgSig.toString(); + handled = typeArgSig.getTypeArguments().isEmpty(); + if (!handled) { + String typeArgClassName = typeArgSig.getClassInfo().getName(); + if (isOptionalType(typeArgClassName) + || isListType(typeArgClassName) + || typeArgClassName.equals(Collections.class.getName())) { + // not handled + } else if (isProviderType(typeArgClassName)) { + isProvider = true; + varTypeName = toClassRefSignature(typeArgSig.getTypeArguments().get(0), enclosingElem).toString(); + handled = true; + } else { + // let's treat it as a supported type ... this is a bit of a gamble though. + handled = true; + } + } + } else if (isListType(csig.getClassInfo().getName())) { + isList = true; + ClassRefTypeSignature typeArgSig = toClassRefSignature(csig.getTypeArguments().get(0), enclosingElem); + varTypeName = typeArgSig.toString(); + handled = typeArgSig.getTypeArguments().isEmpty(); + if (!handled && isProviderType(typeArgSig.getClassInfo().getName())) { + isProvider = true; + varTypeName = toClassRefSignature(typeArgSig.getTypeArguments().get(0), enclosingElem).toString(); + handled = true; + } + } + } + + isProviderWrapped.set(isProvider); + isListWrapped.set(isList); + isOptionalWrapped.set(isOptional); + + if (!handled && !isOptional) { + throw new ToolsException("unsupported type for " + csig + " in " + enclosingElem); + } + + return Objects.requireNonNull(componentTypeNameOf(varTypeName)); + } + + /** + * Determines the meta parts making up {@link InjectionPointInfo} for annotation processing. + * + * @param typeElement the variable / element type + * @param isProviderWrapped set to indicate that the ip is a provided type + * @param isListWrapped set to indicate that the ip is a list type + * @param isOptionalWrapped set to indicate that the ip is an optional type + * @return the return type of the injection point + */ + public static String extractInjectionPointTypeInfo(Element typeElement, + AtomicReference isProviderWrapped, + AtomicReference isListWrapped, + AtomicReference isOptionalWrapped) { + TypeMirror typeMirror = typeElement.asType(); + if (!(typeMirror instanceof DeclaredType)) { + throw new ToolsException("unsupported type for " + typeElement.getEnclosingElement() + "." + + typeElement + " with " + typeMirror.getKind()); + } + DeclaredType declaredTypeMirror = (DeclaredType) typeMirror; + TypeElement declaredClassType = ((TypeElement) declaredTypeMirror.asElement()); + + boolean isProvider = false; + boolean isOptional = false; + boolean isList = false; + String varTypeName = declaredTypeMirror.toString(); + boolean handled = false; + if (declaredClassType != null) { + handled = declaredTypeMirror.getTypeArguments().isEmpty(); + if (1 == declaredTypeMirror.getTypeArguments().size()) { + isProvider = isProviderType(declaredClassType); + isOptional = isOptionalType(declaredClassType); + if (isProvider || isOptional) { + typeMirror = declaredTypeMirror.getTypeArguments().get(0); + if (typeMirror.getKind() == TypeKind.TYPEVAR) { + typeMirror = ((TypeVariable) typeMirror).getUpperBound(); + } + declaredTypeMirror = (DeclaredType) typeMirror; + declaredClassType = ((TypeElement) declaredTypeMirror.asElement()); + varTypeName = declaredClassType.toString(); + handled = declaredTypeMirror.getTypeArguments().isEmpty(); + if (!handled) { + if (isOptionalType(varTypeName) + || isListType(varTypeName) + || varTypeName.equals(Collections.class.getName())) { + // not handled + } else if (isProviderType(varTypeName)) { + isProvider = true; + varTypeName = declaredTypeMirror.getTypeArguments().get(0).toString(); + handled = true; + } else { + // let's treat it as a supported type ... this is a bit of a gamble though. + handled = true; + } + } + } else if (isListType(declaredClassType)) { + isList = true; + typeMirror = declaredTypeMirror.getTypeArguments().get(0); + if (typeMirror.getKind() == TypeKind.TYPEVAR) { + typeMirror = ((TypeVariable) typeMirror).getUpperBound(); + } + declaredTypeMirror = (DeclaredType) typeMirror; + declaredClassType = ((TypeElement) declaredTypeMirror.asElement()); + varTypeName = declaredClassType.toString(); + handled = declaredTypeMirror.getTypeArguments().isEmpty(); + if (!handled) { + if (isProviderType(declaredClassType)) { + isProvider = true; + typeMirror = declaredTypeMirror.getTypeArguments().get(0); + declaredTypeMirror = (DeclaredType) typeMirror; + declaredClassType = ((TypeElement) declaredTypeMirror.asElement()); + varTypeName = declaredClassType.toString(); + if (!declaredTypeMirror.getTypeArguments().isEmpty()) { + throw new ToolsException("unsupported generics usage for " + typeMirror + " in " + + typeElement.getEnclosingElement()); + } + handled = true; + } + } + } + } + } + + isProviderWrapped.set(isProvider); + isListWrapped.set(isList); + isOptionalWrapped.set(isOptional); + + if (!handled && !isOptional) { + throw new ToolsException("unsupported type for " + typeElement.getEnclosingElement() + + "." + typeElement + " with " + typeMirror.getKind()); + } + + return Objects.requireNonNull(componentTypeNameOf(varTypeName)); + } + + /** + * Determines whether the type is a {@link jakarta.inject.Provider} (or javax equiv) type. + * + * @param typeElement the type element to check + * @return true if {@link jakarta.inject.Provider} or {@link InjectionPointProvider} + */ + static boolean isProviderType(TypeElement typeElement) { + return isProviderType(typeElement.getQualifiedName().toString()); + } + + private static boolean isProviderType(ClassRefTypeSignature sig) { + return isProviderType(sig.getFullyQualifiedClassName()); + } + + /** + * Determines whether the type is a {@link jakarta.inject.Provider} (or javax equiv) type. + * + * @param typeName the type name to check + * @return true if {@link jakarta.inject.Provider} or {@link InjectionPointProvider} + */ + public static boolean isProviderType(String typeName) { + String type = translate(componentTypeNameOf(typeName)); + return (Provider.class.getName().equals(type) + || TypeNames.JAVAX_PROVIDER.equals(type) + || InjectionPointProvider.class.getName().equals(type)); + } + + /** + * Determines whether the type is an {@link java.util.Optional} type. + * + * @param typeElement the type element to check + * @return true if {@link java.util.Optional} + */ + static boolean isOptionalType(TypeElement typeElement) { + return isOptionalType(typeElement.getQualifiedName().toString()); + } + + private static boolean isOptionalType(ClassRefTypeSignature sig) { + return isOptionalType(sig.getFullyQualifiedClassName()); + } + + /** + * Determines whether the type is an {@link java.util.Optional} type. + * + * @param typeName the type name to check + * @return true if {@link java.util.Optional} + */ + static boolean isOptionalType(String typeName) { + return Optional.class.getName().equals(componentTypeNameOf(typeName)); + } + + /** + * Determines whether the type is an {@link java.util.List} type. + * + * @param typeElement the type element to check + * @return true if {@link java.util.List} + */ + static boolean isListType(TypeElement typeElement) { + return isListType(typeElement.getQualifiedName().toString()); + } + + /** + * Determines whether the type is an {@link java.util.List} type. + * + * @param typeName the type name to check + * @return true if {@link java.util.List} + */ + static boolean isListType(String typeName) { + return List.class.getName().equals(componentTypeNameOf(typeName)); + } + + /** + * Transposes {@value TypeNames#PREFIX_JAKARTA} from and/or to {@value TypeNames#PREFIX_JAVAX}. + * + * @param typeName the type name to transpose + * @return the transposed value, or the same if not able to be transposed + */ + public static String oppositeOf(String typeName) { + boolean startsWithJakarta = typeName.startsWith(TypeNames.PREFIX_JAKARTA); + boolean startsWithJavax = !startsWithJakarta && typeName.startsWith(TypeNames.PREFIX_JAVAX); + + assert (startsWithJakarta || startsWithJavax); + + if (startsWithJakarta) { + return typeName.replace(TypeNames.PREFIX_JAKARTA, TypeNames.PREFIX_JAVAX); + } else { + return typeName.replace(TypeNames.PREFIX_JAVAX, TypeNames.PREFIX_JAKARTA); + } + } + + /** + * Transpose the type name to "jakarta" (if javax is used). + * + * @param typeName the type name + * @return the normalized, transposed value or the original if it doesn't contain javax + */ + static String translate(String typeName) { + if (typeName == null || typeName.startsWith(TypeNames.PREFIX_JAKARTA)) { + return typeName; + } + + return typeName.replace(TypeNames.PREFIX_JAVAX, TypeNames.PREFIX_JAKARTA); + } + + /** + * Returns true if the modifiers indicate this is a package private element. + * + * @param modifiers the modifiers + * @return true if package private + */ + static boolean isPackagePrivate(int modifiers) { + return !Modifier.isPrivate(modifiers) && !Modifier.isProtected(modifiers) && !Modifier.isPublic(modifiers); + } + + /** + * Returns true if the modifiers indicate this is a private element. + * + * @param modifiers the modifiers + * @return true if private + */ + static boolean isPrivate(int modifiers) { + return Modifier.isPrivate(modifiers); + } + + /** + * Returns true if the modifiers indicate this is a static element. + * + * @param modifiers the modifiers + * @return true if static + */ + static boolean isStatic(int modifiers) { + return Modifier.isStatic(modifiers); + } + + /** + * Returns true if the element is static. + * + * @param element the element + * @return true if static + */ + public static boolean isStatic(Element element) { + Set modifiers = element.getModifiers(); + return (modifiers != null) && modifiers.contains(javax.lang.model.element.Modifier.STATIC); + } + + /** + * Returns true if the modifiers indicate this is an abstract element. + * + * @param modifiers the modifiers + * @return true if abstract + */ + static boolean isAbstract(int modifiers) { + return Modifier.isInterface(modifiers) || Modifier.isAbstract(modifiers); + } + + /** + * Returns true if the element is abstract. + * + * @param element the element + * @return true if abstract + */ + public static boolean isAbstract(Element element) { + Set modifiers = element.getModifiers(); + return (modifiers != null) && modifiers.contains(javax.lang.model.element.Modifier.ABSTRACT); + } + + /** + * Converts the modifiers to an {@link io.helidon.pico.ElementInfo.Access} type. + * + * @param modifiers the moifiers + * @return the access + */ + static InjectionPointInfo.Access toAccess(int modifiers) { + if (Modifier.isPublic(modifiers)) { + return InjectionPointInfo.Access.PUBLIC; + } else if (Modifier.isProtected(modifiers)) { + return InjectionPointInfo.Access.PROTECTED; + } else if (Modifier.isPrivate(modifiers)) { + return InjectionPointInfo.Access.PRIVATE; + } else { + return InjectionPointInfo.Access.PACKAGE_PRIVATE; + } + } + + /** + * Determines the access from an {@link javax.lang.model.element.Element} (from anno processing). + * + * @param element the element + * @return the access + */ + public static InjectionPointInfo.Access toAccess(Element element) { + InjectionPointInfo.Access access = InjectionPointInfo.Access.PACKAGE_PRIVATE; + Set modifiers = element.getModifiers(); + if (modifiers != null) { + for (javax.lang.model.element.Modifier modifier : modifiers) { + if (javax.lang.model.element.Modifier.PUBLIC == modifier) { + access = InjectionPointInfo.Access.PUBLIC; + break; + } else if (javax.lang.model.element.Modifier.PROTECTED == modifier) { + access = InjectionPointInfo.Access.PROTECTED; + break; + } else if (javax.lang.model.element.Modifier.PRIVATE == modifier) { + access = InjectionPointInfo.Access.PRIVATE; + break; + } + } + } + return access; + } + + /** + * Returns the kind of the method. + * + * @param methodInfo the method info + * @return the kind + */ + static ElementInfo.ElementKind toKind(MethodInfo methodInfo) { + return (methodInfo.isConstructor()) + ? ElementInfo.ElementKind.CONSTRUCTOR : ElementInfo.ElementKind.METHOD; + } + + /** + * Returns the kind of the method. + * + * @param methodInfo the method info + * @return the kind + */ + static ElementInfo.ElementKind toKind(ExecutableElement methodInfo) { + return (methodInfo.getKind() == ElementKind.CONSTRUCTOR) + ? ElementInfo.ElementKind.CONSTRUCTOR : ElementInfo.ElementKind.METHOD; + } + + /** + * Checks whether the package name need to be declared. + * + * @param packageName the package name + * @return true if the package name needs to be declared + */ + public static boolean needToDeclarePackageUsage(String packageName) { + return !(packageName.startsWith("java.") + || packageName.startsWith("sun.") + || packageName.toLowerCase().endsWith(".impl")); + } + + /** + * Checks whether the module name needs to be declared. + * + * @param moduleName the module name + * @return true if the module name needs to be declared + */ + public static boolean needToDeclareModuleUsage(String moduleName) { + return (moduleName != null) && !moduleName.equals(ModuleInfoDescriptor.DEFAULT_MODULE_NAME) + && !(moduleName.startsWith("java.") + || moduleName.startsWith("sun.") + || moduleName.startsWith("jakarta.inject") + || moduleName.startsWith(PicoServicesConfig.FQN)); + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java new file mode 100644 index 00000000000..01ef15b0777 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ActivatorCreator.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 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.tools.spi; + +import java.util.Map; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.Contract; +import io.helidon.pico.tools.ActivatorCreatorRequest; +import io.helidon.pico.tools.ActivatorCreatorResponse; +import io.helidon.pico.tools.GeneralCreatorRequest; +import io.helidon.pico.tools.InterceptionPlan; +import io.helidon.pico.tools.InterceptorCreatorResponse; + +/** + * Implementors of this contract are responsible for code-generating the Pico + * {@link io.helidon.pico.Activator}s and {@link io.helidon.pico.ServiceProvider}s for service types found in your DI-enabled + * module. + *

        + * The typical scenario will have 1-SingletonServiceType:1-GeneratedPicoActivatorClassForThatService:1-ServiceProvider + * representation in the {@link io.helidon.pico.Services} registry that can be lazily activated. + *

        + * Activators are only generated if your service is marked as a {@code jakarta.inject.Singleton} scoped service. + *

        + * All activators for your jar module are then aggregated and registered into a pico code-generated + * {@link io.helidon.pico.Module} class. + * + * @see io.helidon.pico.tools.ActivatorCreatorProvider + */ +@Contract +public interface ActivatorCreator { + + /** + * Used during annotation processing in compile time to automatically generate {@link io.helidon.pico.Activator}'s + * and optionally an aggregating {@link io.helidon.pico.Module} for those activators. + * + * @param request the request for what to generate + * @return the response result for the create operation + */ + ActivatorCreatorResponse createModuleActivators(ActivatorCreatorRequest request); + + /** + * Generates just the interceptors. + * + * @param request the request for what to generate + * @param interceptionPlans the interceptor plans + * @return the response result for the create operation + */ + InterceptorCreatorResponse codegenInterceptors(GeneralCreatorRequest request, + Map interceptionPlans); + + /** + * Generates the would-be implementation type name that will be generated if + * {@link #createModuleActivators(ActivatorCreatorRequest)} were to be called on this creator. + * + * @param activatorTypeName the service/activator type name of the developer provided service type. + * + * @return the code generated implementation type name that would be code generated + */ + TypeName toActivatorImplTypeName(TypeName activatorTypeName); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ApplicationCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ApplicationCreator.java new file mode 100644 index 00000000000..fc21da2a170 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ApplicationCreator.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 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.tools.spi; + +import io.helidon.pico.Contract; +import io.helidon.pico.tools.ApplicationCreatorRequest; +import io.helidon.pico.tools.ApplicationCreatorResponse; + +/** + * Implementors of this contract are responsible for creating the {@link io.helidon.pico.Application} instance. + * This is used by Pico's maven-plugin. + * + * @see io.helidon.pico.tools.ApplicationCreatorProvider + */ +@Contract +public interface ApplicationCreator { + + /** + * Used to create the {@link io.helidon.pico.Application} source for the entire + * application / assembly. + * + * @param request the request for what to generate + * @return the result from the create operation + */ + ApplicationCreatorResponse createApplication(ApplicationCreatorRequest request); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/CustomAnnotationTemplateCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/CustomAnnotationTemplateCreator.java new file mode 100644 index 00000000000..32b38041a29 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/CustomAnnotationTemplateCreator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 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.tools.spi; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.tools.CustomAnnotationTemplateRequest; +import io.helidon.pico.tools.CustomAnnotationTemplateResponse; + +/** + * Instances of this are found via the service loader during compilation time and called by the + * {@code io.helidon.pico.processor.CustomAnnotationProcessor}. It should be noted that this contract may be + * called multiple times since annotation processing naturally happens over multiple iterations. + */ +public interface CustomAnnotationTemplateCreator { + + /** + * These are the set of annotation types that will trigger a call this producer. + * + * @return the supported annotation types for this producer + */ + Set annoTypes(); + + /** + * The implementor should return empty if the request should not be handled. + * + * @param request the request + * @return the response that will describe what template to produce, or empty to to cause processing to skip + */ + Optional create(CustomAnnotationTemplateRequest request); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ExternalModuleCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ExternalModuleCreator.java new file mode 100644 index 00000000000..c027d249769 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ExternalModuleCreator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.tools.spi; + +import io.helidon.pico.Contract; +import io.helidon.pico.tools.ExternalModuleCreatorRequest; +import io.helidon.pico.tools.ExternalModuleCreatorResponse; + +/** + * Implementors are responsible for creating an {@link io.helidon.pico.tools.ActivatorCreatorRequest} that can be then passed to the + * {@link ActivatorCreator} based upon the scanning and reflective introspection of a set of classes found in an external + * jar module. + * This involves a two-step process of first preparing to create using + * {@link #prepareToCreateExternalModule(io.helidon.pico.tools.ExternalModuleCreatorRequest)}, followed by taking the response and proceeding + * to call {@link ActivatorCreator#createModuleActivators(io.helidon.pico.tools.ActivatorCreatorRequest)}. + * + * @see io.helidon.pico.tools.ExternalModuleCreatorProvider + */ +@Contract +public interface ExternalModuleCreator { + + /** + * Prepares the activator and module creation by reflectively scanning and analyzing the context of the request + * to build a model payload that can then be pipelined to the activator creator. + * + * @param request the request + * @return the response + */ + ExternalModuleCreatorResponse prepareToCreateExternalModule(ExternalModuleCreatorRequest request); + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java new file mode 100644 index 00000000000..576e52c9718 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/InterceptorCreator.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023 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.tools.spi; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.pico.Contract; +import io.helidon.pico.ServiceInfoBasics; +import io.helidon.pico.tools.InterceptionPlan; + +/** + * Provides the strategy used to determine which annotations cause interceptor creation. Only services that are pico- + * activated may qualify for interception. + * + * @see io.helidon.pico.tools.InterceptorCreatorProvider + */ +@Contract +public interface InterceptorCreator { + + /** + * The strategy applied for resolving annotations that trigger interception. + */ + enum Strategy { + + /** + * Meta-annotation based. Only annotations annotated with {@link io.helidon.pico.InterceptedTrigger} will + * qualify. + */ + EXPLICIT, + + /** + * All annotations marked as {@link java.lang.annotation.RetentionPolicy#RUNTIME} will qualify, which implicitly + * will also cover all usages of {@link #EXPLICIT}. + */ + ALL_RUNTIME, + + /** + * A call to {@link #allowListedAnnotationTypes()} will be used to determine which annotations qualify. The + * implementation may then cache this result internally for optimal processing. + */ + ALLOW_LISTED, + + /** + * A call to {@link #isAllowListed(String)} will be used on a case-by-case basis to check which annotation + * types qualify. + */ + CUSTOM, + + /** + * No annotations will qualify in triggering interceptor creation. + */ + NONE, + + /** + * Applies a blend of {@link #EXPLICIT} and {@link #CUSTOM} to determine which annotations qualify (i.e., if + * the annotation is not explicitly marked, then a call is still issued to {@link #isAllowListed(String)}. This + * strategy is typically the default strategy type in use. + */ + BLENDED + + } + + /** + * Determines the strategy being applied. + * + * @return the strategy being applied + */ + default Strategy strategy() { + return Strategy.BLENDED; + } + + /** + * Applicable when {@link Strategy#ALLOW_LISTED} is in use. + * + * @return the set of type names that should trigger creation + */ + default Set allowListedAnnotationTypes() { + return Set.of(); + } + + /** + * Applicable when {@link Strategy#CUSTOM} is in use. + * + * @param annotationType the annotation type name + * @return true if the annotation type should trigger interceptor creation + */ + default boolean isAllowListed(String annotationType) { + Objects.requireNonNull(annotationType); + return allowListedAnnotationTypes().contains(annotationType); + } + + /** + * After an annotation qualifies the enclosing service for interception, this method will be used to provide + * the injection plan that applies to that service type. + * + * @param interceptedService the service being intercepted + * @param processingEnvironment optionally, the processing environment (if being called by annotation processing) + * @param annotationTypeTriggers the set of annotation names that are associated with interception. + * @return the injection plan, or empty for the implementation to use the default strategy for creating a plan + */ + Optional createInterceptorPlan(ServiceInfoBasics interceptedService, + ProcessingEnvironment processingEnvironment, + Set annotationTypeTriggers); + + /** + * Returns the processor appropriate for the context revealed in the calling arguments, favoring reflection if + * the serviceTypeElement is provided. + * + * @param interceptedService the service being intercepted + * @param delegateCreator the "real" creator + * @param processEnv optionally, the processing environment (should be passed if in annotation processor) + * @return the processor to use for the given arguments + */ + InterceptorProcessor createInterceptorProcessor(ServiceInfoBasics interceptedService, + InterceptorCreator delegateCreator, + Optional processEnv); + + + /** + * Abstraction for interceptor processing. + */ + interface InterceptorProcessor { + + /** + * The set of annotation types that are trigger interception. + * + * @return the set of annotation types that are trigger interception + */ + Set allAnnotationTypeTriggers(); + + /** + * Creates the interception plan. + * + * @param interceptorAnnotationTriggers the annotation type triggering the interception creation. + * @return the plan, or empty if there is no interception needed + */ + Optional createInterceptorPlan(Set interceptorAnnotationTriggers); + + } + +} diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/package-info.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/package-info.java new file mode 100644 index 00000000000..46b1f44afb2 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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. + */ + +/** + * Tools SPI support. + */ +package io.helidon.pico.tools.spi; diff --git a/pico/tools/src/main/java/module-info.java b/pico/tools/src/main/java/module-info.java index 7baca348234..2fcb1612d89 100644 --- a/pico/tools/src/main/java/module-info.java +++ b/pico/tools/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -14,19 +14,45 @@ * limitations under the License. */ +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.spi.ApplicationCreator; +import io.helidon.pico.tools.spi.ExternalModuleCreator; +import io.helidon.pico.tools.spi.InterceptorCreator; + /** * The Pico Tools module. */ module io.helidon.pico.tools { - requires io.helidon.builder; - requires io.helidon.common.config; - requires io.helidon.pico.types; - requires transitive io.helidon.pico; - requires static io.helidon.config.metadata; + requires static jakarta.annotation; + requires java.compiler; + requires jakarta.inject; requires handlebars; + requires io.github.classgraph; + requires io.helidon.builder; requires io.helidon.common; + requires io.helidon.common.config; + requires io.helidon.builder.processor.spi; + requires transitive io.helidon.common.types; + requires transitive io.helidon.pico.services; + requires transitive io.helidon.builder.processor.tools; exports io.helidon.pico.tools; + exports io.helidon.pico.tools.spi; + + uses io.helidon.pico.tools.spi.ActivatorCreator; + uses io.helidon.pico.tools.spi.ApplicationCreator; + uses io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; + uses io.helidon.pico.tools.spi.ExternalModuleCreator; + uses io.helidon.pico.tools.spi.InterceptorCreator; + + provides ActivatorCreator + with io.helidon.pico.tools.DefaultActivatorCreator; + provides ApplicationCreator + with io.helidon.pico.tools.DefaultApplicationCreator; + provides ExternalModuleCreator + with io.helidon.pico.tools.DefaultExternalModuleCreator; + provides InterceptorCreator + with io.helidon.pico.tools.DefaultInterceptorCreator; } diff --git a/pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs b/pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs new file mode 100644 index 00000000000..96312ae8b46 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/complex-interceptor.hbs @@ -0,0 +1,105 @@ +{{! +Copyright (c) 2023 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. +}}{{#header}}{{.}} +{{/header}} +package {{packageName}}; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.DefaultTypedElementName; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementName; +import io.helidon.pico.DefaultInvocationContext; +import io.helidon.pico.Interceptor; +import io.helidon.pico.InvocationException; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.services.InterceptedMethod; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.pico.services.Invocation.createInvokeAndSupply; +import static io.helidon.pico.services.Invocation.mergeAndCollapse; + +/** + * Pico {@link Interceptor} manager for {@link {{parent}} }. + */ +@io.helidon.common.Weight({{weight}}) +@io.helidon.pico.Intercepted({{parent}}.class) +@Singleton +@SuppressWarnings("ALL") +@jakarta.annotation.Generated({{{generatedanno}}}) +public class {{className}} extends {{parent}} { + private static final List __serviceLevelAnnotations = List.of({{#servicelevelannotations}} + {{{.}}}{{#unless @last}},{{/unless}}{{/servicelevelannotations}}); +{{#interceptedmethoddecls}} + private static final TypedElementName __{{id}} = DefaultTypedElementName.builder() + {{{.}}} + .build();{{/interceptedmethoddecls}} + + private final Provider<{{parent}}> __provider; + private final ServiceProvider<{{parent}}> __sp; + private final {{parent}} __impl; + private final TypeName __serviceTypeName;{{#interceptedelements}} + private final List> __{{id}}__interceptors;{{/interceptedelements}}{{#interceptedelements}} + private final InterceptedMethod<{{parent}}, {{elementTypeName}}> __{{id}}__call;{{/interceptedelements}} + + @Inject + {{this.className}}({{#annotationtriggertypenames}} + @Named("{{{.}}}") List> {{id}},{{/annotationtriggertypenames}} + Provider<{{parent}}> provider) { + this.__provider = Objects.requireNonNull(provider); + this.__sp = (provider instanceof ServiceProvider) ? (ServiceProvider<{{parent}}>) __provider : null; + this.__serviceTypeName = DefaultTypeName.create({{parent}}.class); + List> __ctor__interceptors = mergeAndCollapse({{#annotationtriggertypenames}}{{id}}{{#unless @last}}, {{/unless}}{{/annotationtriggertypenames}});{{#interceptedelements}} + this.__{{{id}}}__interceptors = mergeAndCollapse({{interceptedTriggerTypeNames}});{{/interceptedelements}} + + Supplier<{{parent}}> call = __provider::get; + {{parent}} result = createInvokeAndSupply( + DefaultInvocationContext.builder() + .serviceProvider(__sp) + .serviceTypeName(__serviceTypeName) + .classAnnotations(__serviceLevelAnnotations) + .elementInfo(__ctor) + .interceptors(__ctor__interceptors) + /*.build()*/, + call); + this.__impl = Objects.requireNonNull(result);{{#interceptedelements}} + + this.__{{id}}__call = new InterceptedMethod<{{parent}}, {{elementTypeName}}>( + __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __{{id}}__interceptors, __{{id}}{{elementArgInfo}}) { + @Override + public {{elementTypeName}} invoke(Object... args) throws Throwable { + {{#if hasReturn}}return impl().{{id}}({{objArrayArgs}});{{else}}impl().{{id}}({{objArrayArgs}}); + return null;{{/if}} + } + };{{/interceptedelements}} + } +{{#interceptedelements}} + @Override + {{{methodDecl}}} { + {{#if hasReturn}}return {{/if}}createInvokeAndSupply(__{{id}}__call.ctx(), () -> __{{id}}__call.apply({{args}})); + } +{{/interceptedelements}} +} diff --git a/pico/tools/src/main/resources/templates/pico/default/module-info.hbs b/pico/tools/src/main/resources/templates/pico/default/module-info.hbs index 3e9e61b2b4e..e02cbed9098 100644 --- a/pico/tools/src/main/resources/templates/pico/default/module-info.hbs +++ b/pico/tools/src/main/resources/templates/pico/default/module-info.hbs @@ -1,5 +1,5 @@ {{! -Copyright (c) 2022 Oracle and/or its affiliates. +Copyright (c) 2022, 2023 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. @@ -12,12 +12,11 @@ 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. -}}{{#header}}{{{.}}}{{/header}} -{{#hasdescription}}/**{{#description}} +}}{{#header}}{{{.}}}{{/header}}{{#hasdescription}}/**{{#description}} * {{{.}}}{{/description}} */{{/hasdescription}}{{#generatedanno}} -// @Generated({{{generatedanno}}}){{/generatedanno}} +// @Generated({{{.}}}){{/generatedanno}} module {{name}} { {{#items}}{{#precomments}} - {{.}}{{/precomments}} +{{{.}}}{{/precomments}} {{{contents}}};{{/items}} } diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-activator.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-activator.hbs new file mode 100644 index 00000000000..791ef3d8637 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-activator.hbs @@ -0,0 +1,163 @@ +{{! +Copyright (c) 2023 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. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import io.helidon.pico.DefaultDependenciesInfo; +import io.helidon.pico.DefaultServiceInfo; +import io.helidon.pico.DependenciesInfo; +import io.helidon.pico.PostConstructMethod; +import io.helidon.pico.PreDestroyMethod; +import io.helidon.pico.RunLevel; +import io.helidon.pico.services.Dependencies; + +import jakarta.annotation.Generated; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static io.helidon.pico.ElementInfo.Access; +import static io.helidon.pico.ElementInfo.ElementKind; +import static io.helidon.pico.InjectionPointInfo.CONSTRUCTOR; + +/**{{#description}} + * {{{.}}}{{/description}}{{#extraclasscomments}} + * {{{.}}}{{/extraclasscomments}} + */ +// @Singleton{{#weight}} +@Weight({{{.}}}){{/weight}} {{#isrunlevelset}}@RunLevel({{runlevel}}){{/isrunlevelset}} +@SuppressWarnings("unchecked") +@Generated({{{generatedanno}}}) +public class {{flatclassname}}{{activatorsuffix}}{{{activatorgenericdecl}}} + extends {{{parent}}} { + private static final DefaultServiceInfo serviceInfo = + DefaultServiceInfo.builder() + .serviceTypeName({{packagename}}.{{classname}}.class.getName()){{#contracts}} + .addContractsImplemented({{.}}.class.getName()){{/contracts}}{{#externalcontracts}} + .addExternalContractsImplemented({{.}}.class.getName()){{/externalcontracts}} + .activatorTypeName({{flatclassname}}{{activatorsuffix}}.class.getName()){{^isprovider}}{{#scopetypenames}} + .addScopeTypeName({{{.}}}.class.getName()){{/scopetypenames}}{{/isprovider}}{{#qualifiers}} + {{{.}}}{{/qualifiers}}{{#isweightset}} + .declaredWeight({{weight}}){{/isweightset}}{{#isrunlevelset}} + .declaredRunLevel({{runlevel}}){{/isrunlevelset}} + .build(); + + /** + * The global singleton instance for this service provider activator. + */ + public static final {{flatclassname}}{{activatorsuffix}} INSTANCE = new {{flatclassname}}{{activatorsuffix}}(); + + /** + * Default activator constructor. + */ + protected {{flatclassname}}{{activatorsuffix}}() { + serviceInfo(serviceInfo); + } + + /** + * The service type of the managed service. + * + * @return the service type of the managed service + */ + public Class serviceType() { + return {{packagename}}.{{classname}}.class; + } +{{#extracodegen}}{{{.}}} +{{/extracodegen}}{{^isprovider}}{{#if issupportsjsr330instrictmode}} + @Override + public boolean isProvider() { + return false; + } +{{/if}}{{/isprovider}}{{#isprovider}} + @Override + public boolean isProvider() { + return true; + } +{{/isprovider}} + @Override + public DependenciesInfo dependencies() { + DependenciesInfo deps = Dependencies.builder({{packagename}}.{{classname}}.class.getName()){{#dependencies}} + {{{.}}}{{/dependencies}} + .build(); + return Dependencies.combine(super.dependencies(), deps); + } +{{#isconcrete}}{{#if issupportsjsr330instrictmode}}{{#if injectionorder}} + @Override + protected List serviceTypeInjectionOrder() { + List order = new java.util.ArrayList<>();{{#injectionorder}} + order.add("{{{.}}}");{{/injectionorder}} + return order; + } +{{/if}}{{/if}} + @Override + protected {{classname}} createServiceProvider(Map deps) { {{#ctorargs}} + {{{.}}}{{/ctorargs}} + return new {{packagename}}.{{classname}}({{#ctorarglist}}{{.}}{{/ctorarglist}}); + }{{/isconcrete}} +{{#if injectedfields}} + @Override + protected void doInjectingFields(Object t, Map deps, Set injections, String forServiceType) { + super.doInjectingFields(t, deps, injections, forServiceType);{{#if issupportsjsr330instrictmode}} + if (forServiceType != null && !{{packagename}}.{{classname}}.class.getName().equals(forServiceType)) { + return; + } +{{/if}} + {{classname}} target = ({{classname}}) t;{{#if issupportsjsr330instrictmode}}{{#injectedfields}} + if (injections.add("{{{id}}}")) { + target.{{{.}}}; + }{{/injectedfields}}{{else}}{{#injectedfields}} + target.{{{.}}};{{/injectedfields}}{{/if}} + } +{{/if}}{{#if injectedmethods}} + @Override + protected void doInjectingMethods(Object t, Map deps, Set injections, String forServiceType) { {{#if injectedmethodsskippedinparent}} + if (injections.isEmpty()) { {{#injectedmethodsskippedinparent}} + injections.add("{{{id}}}");{{/injectedmethodsskippedinparent}} + }{{/if}} + super.doInjectingMethods(t, deps, injections, forServiceType); +{{#if issupportsjsr330instrictmode}} + if (forServiceType != null && !{{packagename}}.{{classname}}.class.getName().equals(forServiceType)) { + return; + } +{{/if}} + {{classname}} target = ({{classname}}) t; +{{#if issupportsjsr330instrictmode}}{{#injectedmethods}} + if (injections.add("{{{id}}}")) { + target.{{{.}}}; + }{{/injectedmethods}}{{else}}{{#injectedmethods}} + target.{{{.}}};{{/injectedmethods}}{{/if}} + } +{{/if}}{{#postconstruct}} + @Override + public Optional postConstructMethod() { + {{classname}} target = ({{classname}}) serviceRef().orElseThrow(); + return Optional.of(target::{{.}}); + } +{{/postconstruct}}{{#predestroy}} + @Override + public Optional preDestroyMethod() { + {{classname}} target = ({{classname}}) serviceRef().orElseThrow(); + return Optional.of(target::{{.}}); + } +{{/predestroy}} +} diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-application-empty-servicetypebinding.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-empty-servicetypebinding.hbs new file mode 100644 index 00000000000..e54b5d55989 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-empty-servicetypebinding.hbs @@ -0,0 +1,19 @@ +{{! +Copyright (c) 2023 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. +}}/** + * In module name "{{modulename}}". + * @see {@link {{servicetypename}} } - note that there are no known contracts or dependencies for this service type. + */ + // binder.bindTo({{activator}}) diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-application-servicetypebinding.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-servicetypebinding.hbs new file mode 100644 index 00000000000..78060f9dd84 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-servicetypebinding.hbs @@ -0,0 +1,21 @@ +{{! +Copyright (c) 2023 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. +}}/** + * In module name "{{modulename}}". + * @see {@link {{servicetypename}} } + */ + binder.bindTo({{activator}}) + {{#injectionplan}}{{{.}}} + {{/injectionplan}}.commit(); diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-application-stub.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-stub.hbs new file mode 100644 index 00000000000..0fb2e48a392 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-application-stub.hbs @@ -0,0 +1,58 @@ +{{! +Copyright (c) 2023 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. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import java.util.Optional; + +import io.helidon.pico.Application; +import io.helidon.pico.ServiceInjectionPlanBinder; + +import jakarta.annotation.Generated; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * THIS IS A TEMPORARY PLACEHOLDER. + * It is expected that this module should have eventually generated the "final" Application using the pico-maven-plugin + * attached to the compile phase. See the documentation for more information. +*/ +@Generated({{{generatedanno}}}) +@Singleton {{#modulename}}@Named({{classname}}.NAME){{/modulename}} +public final class {{classname}} implements Application { +{{#modulename}} + static final String NAME = "{{{.}}}"; +{{/modulename}}{{^modulename}} + static final String NAME = "unnamed";{{/modulename}} + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + "(temporary):" + getClass().getName(); + } + + @Override + public void configure(ServiceInjectionPlanBinder binder) { + throw new IllegalStateException("The application is in an invalid state.\n" + + "The pico-maven-plugin either did not get run, or failed to generate the final replacement to this class: " + this + + "\nPlease check the logs or consult the documentation."); + } + +} diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-application.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-application.hbs new file mode 100644 index 00000000000..b18bba1fce5 --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-application.hbs @@ -0,0 +1,67 @@ +{{! +Copyright (c) 2023 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. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import java.util.Optional; + +import io.helidon.pico.Application; +import io.helidon.pico.ServiceInjectionPlanBinder; + +import jakarta.annotation.Generated; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/**{{#description}} + * {{{.}}}{{/description}} + */ +@Generated({{{generatedanno}}}) +@Singleton {{#modulename}}@Named({{classname}}.NAME){{/modulename}} +public final class {{classname}} implements Application { +{{#modulename}} + static final String NAME = "{{{.}}}";{{/modulename}}{{^modulename}} + static final String NAME = "unnamed";{{/modulename}} + static boolean enabled = true; + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public {{classname}}() { + } + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + ":" + getClass().getName(); + } + + @Override + public void configure(ServiceInjectionPlanBinder binder) { + if (!enabled) { + return; + } +{{#servicetypebindings}} + {{{.}}}{{/servicetypebindings}} + } + +} diff --git a/pico/tools/src/main/resources/templates/pico/default/service-provider-module.hbs b/pico/tools/src/main/resources/templates/pico/default/service-provider-module.hbs new file mode 100644 index 00000000000..709a3b5487d --- /dev/null +++ b/pico/tools/src/main/resources/templates/pico/default/service-provider-module.hbs @@ -0,0 +1,61 @@ +{{! +Copyright (c) 2023 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. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import io.helidon.pico.Module; +import io.helidon.pico.ServiceBinder; + +import jakarta.annotation.Generated; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import java.util.Optional; + +/**{{#description}} + * {{{.}}}{{/description}} + */ +@Generated({{{generatedanno}}}) +@Singleton {{#modulename}}@Named({{classname}}.NAME){{/modulename}} +public final class {{classname}} implements Module { {{#modulename}} + static final String NAME = "{{{.}}}";{{/modulename}}{{^modulename}} + static final String NAME = "unnamed";{{/modulename}} + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public {{classname}}() { + } + + @Override + public Optional named() { + return Optional.of(NAME); + } + + @Override + public String toString() { + return NAME + ":" + getClass().getName(); + } + + @Override + public void configure(ServiceBinder binder) { +{{#activators}} binder.bind({{{.}}}.INSTANCE); +{{/activators}} + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/AbstractBaseCreator.java b/pico/tools/src/test/java/io/helidon/pico/tools/AbstractBaseCreator.java new file mode 100644 index 00000000000..928732926cf --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/AbstractBaseCreator.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; + +abstract class AbstractBaseCreator { + + T loadAndCreate(Class iface) { + return HelidonServiceLoader + .create(ServiceLoader.load(iface)) + .iterator() + .next(); + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultActivatorCreatorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultActivatorCreatorTest.java new file mode 100644 index 00000000000..7d111132507 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultActivatorCreatorTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.Collections; +import java.util.List; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.testsubjects.HelloPicoWorldImpl; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link io.helidon.pico.tools.DefaultActivatorCreator}. + */ +class DefaultActivatorCreatorTest extends AbstractBaseCreator { + + final ActivatorCreator activatorCreator = loadAndCreate(ActivatorCreator.class); + + @Test + void sanity() { + assertThat(activatorCreator.getClass(), equalTo(DefaultActivatorCreator.class)); + } + + /** + * Note that most of the "real" functional testing will need to occur downstream from this module and test. + */ + @Test + void codegenHelloActivator() { + DefaultActivatorCreator activatorCreator = (DefaultActivatorCreator) this.activatorCreator; + CodeGenPaths codeGenPaths = DefaultCodeGenPaths.builder() + .generatedSourcesPath("target/pico/generated-sources") + .outputPath("target/pico/generated-classes") + .build(); + AbstractFilerMessager directFiler = AbstractFilerMessager + .createDirectFiler(codeGenPaths, System.getLogger(getClass().getName())); + CodeGenFiler filer = CodeGenFiler.create(directFiler); + DefaultActivatorCreatorCodeGen codeGen = DefaultActivatorCreatorCodeGen.builder().build(); + ActivatorCreatorRequest req = DefaultActivatorCreatorRequest.builder() + .serviceTypeNames(List.of(DefaultTypeName.create(HelloPicoWorldImpl.class))) + .codeGen(codeGen) + .codeGenPaths(codeGenPaths) + .configOptions(DefaultActivatorCreatorConfigOptions.builder().build()) + .filer(filer) + .build(); + + ToolsException te = assertThrows(ToolsException.class, () -> activatorCreator.createModuleActivators(req)); + assertEquals("failed in create", te.getMessage()); + + ActivatorCreatorRequest req2 = DefaultActivatorCreatorRequest.builder() + .serviceTypeNames(Collections.singletonList(DefaultTypeName.create(HelloPicoWorldImpl.class))) + .codeGenPaths(DefaultCodeGenPaths.builder().build()) + .throwIfError(Boolean.FALSE) + .codeGen(codeGen) + .configOptions(DefaultActivatorCreatorConfigOptions.builder().build()) + .filer(filer) + .build(); + ActivatorCreatorResponse res = activatorCreator.createModuleActivators(req2); + assertThat(res.toString(), res.success(), is(false)); + assertThat(res.error().orElseThrow().getMessage(), equalTo("failed in create")); + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultApplicationCreatorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultApplicationCreatorTest.java new file mode 100644 index 00000000000..85cbbb7cf72 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultApplicationCreatorTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.stream.Collectors; + +import io.helidon.common.types.DefaultTypeName; +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultServiceInfoCriteria; +import io.helidon.pico.PicoServices; +import io.helidon.pico.ServiceInfoCriteria; +import io.helidon.pico.ServiceProvider; +import io.helidon.pico.Services; +import io.helidon.pico.tools.spi.ApplicationCreator; + +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; +import static org.hamcrest.Matchers.contains; + +/** + * Tests for {@link io.helidon.pico.tools.DefaultActivatorCreator}. + */ +class DefaultApplicationCreatorTest extends AbstractBaseCreator { + + final ApplicationCreator applicationCreator = loadAndCreate(ApplicationCreator.class); + + @Test + void sanity() { + assertThat(applicationCreator.getClass(), equalTo(DefaultApplicationCreator.class)); + } + + /** + * Most of the testing will need to occur downstream from this module. + */ + @Test + void codegenHelloWorldApplication() { + ApplicationCreator creator = this.applicationCreator; + ServiceInfoCriteria allServices = DefaultServiceInfoCriteria.builder().build(); + + PicoServices picoServices = PicoServices.picoServices().orElseThrow(); + Services services = picoServices.services(); + List> serviceProviders = services.lookupAll(allServices); + + List serviceTypeNames = serviceProviders.stream() + .map(sp -> DefaultTypeName.createFromTypeName(sp.serviceInfo().serviceTypeName())) + .collect(Collectors.toList()); + + CodeGenPaths codeGenPaths = DefaultCodeGenPaths.builder() + .generatedSourcesPath("target/pico/generated-sources") + .outputPath("target/pico/generated-classes") + .build(); + AbstractFilerMessager directFiler = AbstractFilerMessager + .createDirectFiler(codeGenPaths, System.getLogger(getClass().getName())); + CodeGenFiler filer = CodeGenFiler.create(directFiler); + + String classpath = System.getProperty("java.class.path"); + String separator = System.getProperty("path.separator"); + String[] ignoredClasspath = classpath.split(separator); + ApplicationCreatorRequest req = DefaultApplicationCreatorRequest.builder() + .codeGen(DefaultApplicationCreatorCodeGen.builder() + .className(DefaultApplicationCreator.toApplicationClassName("test")) + .classPrefixName("test") + .build()) + .codeGenPaths(codeGenPaths) + .configOptions(DefaultApplicationCreatorConfigOptions.builder() + .permittedProviderTypes(ApplicationCreatorConfigOptions.PermittedProviderType.ALL) + .build()) + .filer(filer) + .messager(directFiler) + .serviceTypeNames(serviceTypeNames) + .build(); + + ApplicationCreatorResponse res = creator.createApplication(req); + assertThat(res.error(), optionalEmpty()); + assertThat(res.success(), is(true)); + assertThat(res.serviceTypeNames().stream().map(TypeName::name).collect(Collectors.toList()), + contains("pico.Pico$$TestApplication")); + assertThat(res.templateName(), equalTo("default")); + assertThat(res.moduleName(), optionalEmpty()); + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultExternalModuleCreatorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultExternalModuleCreatorTest.java new file mode 100644 index 00000000000..1badcbee285 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultExternalModuleCreatorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.DefaultQualifierAndValue; +import io.helidon.pico.tools.spi.ActivatorCreator; +import io.helidon.pico.tools.spi.ExternalModuleCreator; + +import org.atinject.tck.auto.Drivers; +import org.atinject.tck.auto.DriversSeat; +import org.atinject.tck.auto.accessories.SpareTire; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests for {@link io.helidon.pico.tools.DefaultExternalModuleCreator}. This test + * effectively demonstrates the behavior of the pico-maven-plugin. + */ +class DefaultExternalModuleCreatorTest extends AbstractBaseCreator { + + final ExternalModuleCreator externalModuleCreator = loadAndCreate(ExternalModuleCreator.class); + + @Test + void sanity() { + assertThat(externalModuleCreator.getClass(), equalTo(DefaultExternalModuleCreator.class)); + } + + @Test + void tck330Gen() { + Thread.currentThread().setContextClassLoader(DefaultExternalModuleCreatorTest.class.getClassLoader()); + + CodeGenPaths codeGenPaths = DefaultCodeGenPaths.builder() + .generatedSourcesPath("target/pico/generated-sources") + .outputPath("target/pico/generated-classes") + .build(); + AbstractFilerMessager directFiler = AbstractFilerMessager + .createDirectFiler(codeGenPaths, System.getLogger(getClass().getName())); + CodeGenFiler filer = CodeGenFiler.create(directFiler); + + ActivatorCreatorConfigOptions activatorCreatorConfigOptions = DefaultActivatorCreatorConfigOptions.builder() + .supportsJsr330InStrictMode(true) + .build(); + + ExternalModuleCreatorRequest req = DefaultExternalModuleCreatorRequest.builder() + .addPackageNamesToScan("org.atinject.tck.auto") + .addPackageNamesToScan("org.atinject.tck.auto.accessories") + .addServiceTypeToQualifiersMap(SpareTire.class.getName(), + Set.of(DefaultQualifierAndValue.createNamed("spare"))) + .addServiceTypeToQualifiersMap(DriversSeat.class.getName(), + Set.of(DefaultQualifierAndValue.create(Drivers.class))) + .activatorCreatorConfigOptions(activatorCreatorConfigOptions) + .innerClassesProcessed(false) + .codeGenPaths(codeGenPaths) + .filer(filer) + .build(); + ExternalModuleCreatorResponse res = externalModuleCreator.prepareToCreateExternalModule(req); + assertThat(res.toString(), res.success(), is(true)); + List desc = res.serviceTypeNames().stream().map(TypeName::name).collect(Collectors.toList()); + assertThat(desc, containsInAnyOrder( + "org.atinject.tck.auto.Convertible", + "org.atinject.tck.auto.DriversSeat", + "org.atinject.tck.auto.Engine", + "org.atinject.tck.auto.FuelTank", + "org.atinject.tck.auto.GasEngine", + "org.atinject.tck.auto.Seat", + "org.atinject.tck.auto.Seatbelt", + "org.atinject.tck.auto.Tire", + "org.atinject.tck.auto.V8Engine", + "org.atinject.tck.auto.accessories.Cupholder", + "org.atinject.tck.auto.accessories.RoundThing", + "org.atinject.tck.auto.accessories.SpareTire" + )); + assertThat(res.moduleName(), optionalValue(equalTo("jakarta.inject.tck"))); + assertThat(res.activatorCreatorRequest(), notNullValue()); + + ActivatorCreator activatorCreator = loadAndCreate(ActivatorCreator.class); + ActivatorCreatorResponse response = activatorCreator.createModuleActivators(res.activatorCreatorRequest()); + assertThat(response.toString(), response.success(), is(true)); + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java new file mode 100644 index 00000000000..5a0e2dd652a --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/DefaultInterceptorCreatorTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.helidon.common.types.DefaultAnnotationAndValue; +import io.helidon.pico.InterceptedTrigger; +import io.helidon.pico.tools.spi.InterceptorCreator; + +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.tools.DefaultInterceptorCreator.AnnotationTypeNameResolver; +import static io.helidon.pico.tools.DefaultInterceptorCreator.createResolverFromReflection; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +class DefaultInterceptorCreatorTest extends AbstractBaseCreator { + + final InterceptorCreator interceptorCreator = loadAndCreate(InterceptorCreator.class); + + @Test + void sanity() { + assertThat(interceptorCreator.getClass(), equalTo(DefaultInterceptorCreator.class)); + assertThat(interceptorCreator.strategy(), is(InterceptorCreator.Strategy.BLENDED)); + assertThat(interceptorCreator.allowListedAnnotationTypes().size(), is(0)); + assertThat(interceptorCreator.isAllowListed(Named.class.getName()), is(false)); + } + + @Test + void resolverByReflection() { + AnnotationTypeNameResolver resolver = createResolverFromReflection(); + assertThat(resolver.resolve(InterceptedTrigger.class.getName()), + containsInAnyOrder( + DefaultAnnotationAndValue.create(Documented.class), + DefaultAnnotationAndValue.create(Retention.class, "java.lang.annotation.RetentionPolicy.CLASS"), + DefaultAnnotationAndValue.create(Target.class, "{java.lang.annotation.ElementType.ANNOTATION_TYPE}") + )); + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/ModuleInfoDescriptorTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/ModuleInfoDescriptorTest.java index be70f873f47..52d2d8311fe 100644 --- a/pico/tools/src/test/java/io/helidon/pico/tools/ModuleInfoDescriptorTest.java +++ b/pico/tools/src/test/java/io/helidon/pico/tools/ModuleInfoDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -35,8 +35,8 @@ class ModuleInfoDescriptorTest { void programmatic() { DefaultModuleInfoDescriptor.Builder builder = DefaultModuleInfoDescriptor.builder(); assertThat(builder.build().contents(), - equalTo("// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + equalTo("// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module unnamed {\n" + "}")); builder.name("my.module"); @@ -46,8 +46,8 @@ void programmatic() { equalTo("/**\n" + " * comments here.\n" + " */\n" - + "// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + +"// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module my.module {\n" + " requires transitive their.module;\n" + "}")); @@ -57,8 +57,8 @@ void programmatic() { equalTo("/**\n" + " * comments here.\n" + " */\n" - + "// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + +"// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module my.module {\n" + " requires transitive their.module;\n" + " uses " + ExternalContracts.class.getName() + ";\n" @@ -69,8 +69,8 @@ void programmatic() { equalTo("/**\n" + " * comments here.\n" + " */\n" - + "// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + +"// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module my.module {\n" + " requires transitive their.module;\n" + " uses " + ExternalContracts.class.getName() + ";\n" @@ -83,16 +83,16 @@ void firstUnqualifiedExport() { ModuleInfoDescriptor descriptor = DefaultModuleInfoDescriptor.builder() .name("test") .addItem(ModuleInfoDescriptor.providesContract("cn2", "impl2")) - .addItem(ModuleInfoDescriptor.providesContract("cn1")) + .addItem(ModuleInfoDescriptor.providesContract("cn1", "impl1")) .addItem(ModuleInfoDescriptor.exportsPackage("export1", "private.module.name")) .addItem(ModuleInfoDescriptor.exportsPackage("export2")) .build(); assertThat(descriptor.contents(), - equalTo("// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + equalTo("// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module test {\n" + " provides cn2 with impl2;\n" - + " provides cn1;\n" + + " provides cn1 with impl1;\n" + " exports export1 to private.module.name;\n" + " exports export2;\n" + "}")); @@ -109,23 +109,23 @@ void sortedWithComments() { .ordering(ModuleInfoDescriptor.Ordering.SORTED) .name("test") .addItem(ModuleInfoDescriptor.providesContract("cn2", "impl2")) - .addItem(ModuleInfoDescriptor.providesContract("cn1")) + .addItem(ModuleInfoDescriptor.providesContract("cn1", "impl1")) .addItem(ModuleInfoDescriptor.exportsPackage("export2")) .addItem(DefaultModuleInfoItem.builder() .exports(true) .target("export1") .addWithOrTo("private.module.name") .addWithOrTo("another.private.module.name") - .addPrecomment("// this is an export1 comment.") + .addPrecomment(" // this is an export1 comment") .build()) .build(); assertThat(descriptor.contents(), - equalTo("// @Generated({provider=null, generator=io.helidon.pico.tools" - + ".DefaultModuleInfoDescriptor, ver=null})\n" + equalTo("// @Generated(value = \"io.helidon.pico.tools" + + ".DefaultModuleInfoDescriptor\", comments = \"version=1\")\n" + "module test {\n" - + " provides cn1;\n" + + " provides cn1 with impl1;\n" + " provides cn2 with impl2;\n" - + " // this is an export1 comment.\n" + + " // this is an export1 comment\n" + " exports export1 to another.private.module.name,\n" + "\t\t\tprivate.module.name;\n" + " exports export2;\n" @@ -180,4 +180,64 @@ void loadCreateAndSave() throws Exception { } } + @Test + void mergeCreate() { + ModuleInfoDescriptor descriptor = ModuleInfoDescriptor + .create(CommonUtils.loadStringFromResource("testsubjects/m0._java_"), + ModuleInfoDescriptor.Ordering.NATURAL); + assertThat(descriptor.contents(false), + equalTo("module io.helidon.pico {\n" + + " requires transitive io.helidon.pico.api;\n" + + " requires static com.fasterxml.jackson.annotation;\n" + + " requires static lombok;\n" + + " requires io.helidon.common;\n" + + " exports io.helidon.pico.spi.impl;\n" + + " provides io.helidon.pico.PicoServices with io.helidon.pico.spi.impl" + + ".DefaultPicoServices;\n" + + " uses io.helidon.pico.Module;\n" + + " uses io.helidon.pico.Application;\n" + + "}")); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> descriptor.mergeCreate(descriptor)); + assertThat(e.getMessage(), equalTo("can't merge with self")); + + ModuleInfoDescriptor mergeCreated = descriptor.mergeCreate(DefaultModuleInfoDescriptor.toBuilder(descriptor)); + assertThat(descriptor.contents(), equalTo(mergeCreated.contents())); + + ModuleInfoDescriptor descriptor1 = DefaultModuleInfoDescriptor.builder() + .addItem(ModuleInfoDescriptor.exportsPackage("one")) + .build(); + ModuleInfoDescriptor descriptor2 = DefaultModuleInfoDescriptor.builder() + .addItem(ModuleInfoDescriptor.exportsPackage("two")) + .build(); + mergeCreated = descriptor1.mergeCreate(descriptor2); + assertThat(mergeCreated.contents(false), + equalTo("module unnamed {\n" + + " exports one;\n" + + " exports two;\n" + + "}")); + } + + @Test + void addIfAbsent() { + DefaultModuleInfoDescriptor.Builder builder = DefaultModuleInfoDescriptor.builder(); + ModuleInfoDescriptor.addIfAbsent(builder, "external", + () -> DefaultModuleInfoItem.builder() + .uses(true) + .target("external") + .addPrecomment(" // 1") + .build()); + ModuleInfoDescriptor.addIfAbsent(builder, "external", + () -> DefaultModuleInfoItem.builder() + .uses(true) + .target("external") + .addPrecomment(" // 2") + .build()); + ModuleInfoDescriptor descriptor = builder.build(); + assertThat(descriptor.contents(false), + equalTo("module unnamed {\n" + + " // 1\n" + + " uses external;\n" + + "}")); + } + } diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/TemplateHelperTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/TemplateHelperTest.java index 3e004c19ba1..e48c6da1286 100644 --- a/pico/tools/src/test/java/io/helidon/pico/tools/TemplateHelperTest.java +++ b/pico/tools/src/test/java/io/helidon/pico/tools/TemplateHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 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. @@ -16,15 +16,12 @@ package io.helidon.pico.tools; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import io.helidon.pico.DefaultBootstrap; - import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -38,7 +35,7 @@ class TemplateHelperTest { @Test void bogusTemplateName() { - TemplateHelper helper = TemplateHelper.create(DefaultBootstrap.builder().build()); + TemplateHelper helper = TemplateHelper.create(); ToolsException e = assertThrows(ToolsException.class, () -> helper.safeLoadTemplate("bogus.hbs")); assertThat(e.getMessage(), equalTo("failed to load: templates/pico/default/bogus.hbs")); @@ -46,7 +43,7 @@ void bogusTemplateName() { @Test void requiredArguments() { - TemplateHelper helper = TemplateHelper.create(DefaultBootstrap.builder().build()); + TemplateHelper helper = TemplateHelper.create(); Set args = helper.requiredArguments("this is {a} little {test}", Optional.of("{"), Optional.of("}")); assertThat(args, contains("a", "test")); @@ -59,8 +56,8 @@ void requiredArguments() { @Test public void applyMustacheSubstitutions() { - TemplateHelper helper = TemplateHelper.create(DefaultBootstrap.builder().build()); - Map props = Collections.singletonMap("little", "big"); + TemplateHelper helper = TemplateHelper.create(); + Map props = Map.of("little", "big"); String val = helper.applySubstitutions("", props, true); assertThat(val, equalTo("")); @@ -72,7 +69,7 @@ public void applyMustacheSubstitutions() { @Test public void moduleInfoTemplate() { Map subst = new HashMap<>(); - TemplateHelper helper = TemplateHelper.create(DefaultBootstrap.builder().build()); + TemplateHelper helper = TemplateHelper.create(); String template = helper.safeLoadTemplate("module-info.hbs"); Set args = helper.requiredArguments(template); @@ -87,8 +84,8 @@ public void moduleInfoTemplate() { subst.put("name", "my-module-name"); subst.put("description", List.of("Description 1.", "Description 2.")); subst.put("hasdescription", true); - subst.put("header", "/*\n Header Line 1\n Header Line 2\n */"); - subst.put("generatedanno", helper.defaultGeneratedStickerFor("generator")); + subst.put("header", "/*\n Header Line 1\n Header Line 2\n */\n"); + subst.put("generatedanno", helper.generatedStickerFor("generator")); codegen = helper.applySubstitutions(template, subst, true); assertThat(codegen, equalTo("/*\n" @@ -99,7 +96,7 @@ public void moduleInfoTemplate() { + " * Description 1.\n" + " * Description 2.\n" + " */\n" - + "// @Generated({provider=null, generator=generator, ver=null})\n" + + "// @Generated(value = \"generator\", comments = \"version=1\")\n" + "module my-module-name { \n" + "}\n")); } diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/TypeToolsTest.java b/pico/tools/src/test/java/io/helidon/pico/tools/TypeToolsTest.java new file mode 100644 index 00000000000..0175ee27405 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/TypeToolsTest.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2023 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.tools; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import io.github.classgraph.ClassInfo; +import io.github.classgraph.FieldInfo; +import io.github.classgraph.MethodParameterInfo; +import io.github.classgraph.ScanResult; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.types.DefaultTypeName.create; +import static io.helidon.pico.tools.TypeTools.extractInjectionPointTypeInfo; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Named("name") +@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) +public class TypeToolsTest { + ScanResult scan = ReflectionHandler.INSTANCE.scan(); + + ClassInfo ignoredTestClassInfo = Objects.requireNonNull( + scan.getClassInfo(TypeToolsTest.class.getName())); + ClassInfo providerOfGenericClassInfo = Objects.requireNonNull( + scan.getClassInfo(ProviderOfGeneric.class.getName())); + ClassInfo providerOfDirectClassInfo = Objects.requireNonNull( + scan.getClassInfo(ProviderOfTypeDirect.class.getName())); + ClassInfo ignoredProviderOfTypeThroughSuperClassInfo = Objects.requireNonNull( + scan.getClassInfo(ProviderOfTypeThroughSuper.class.getName())); + ClassInfo ignoredInjectionPointProviderOfTypeDirectClassInfo = Objects.requireNonNull( + scan.getClassInfo(InjectionPointProviderOfTypeDirect.class.getName())); + + String typeToolsTestTypeName = create(TypeToolsTest.class).name(); + String stringTypeName = create(String.class).name(); + String booleanTypeName = create(Boolean.class).name(); + + @Test + void optionalsProvidersAndListsOfFieldInfo() { + optionalsProvidersAndLists(typeToolsTestTypeName, true, true, false, + providerOfGenericClassInfo.getFieldInfo("listOfProviders")); + optionalsProvidersAndLists(typeToolsTestTypeName, true, false, true, + providerOfGenericClassInfo.getFieldInfo("optionalProvider")); + optionalsProvidersAndLists(typeToolsTestTypeName, false, false, true, + providerOfGenericClassInfo.getFieldInfo("optionalNotProvider")); + optionalsProvidersAndListsException("unsupported type for D in abstract static class io.helidon.pico.tools" + + ".TypeToolsTest$ProviderOfGeneric implements jakarta.inject.Provider", + providerOfGenericClassInfo.getFieldInfo("generic")); + optionalsProvidersAndListsException("unsupported type for D in abstract static class io.helidon.pico.tools" + + ".TypeToolsTest$ProviderOfGeneric implements jakarta.inject.Provider", + providerOfGenericClassInfo.getFieldInfo("listOfProvidersOfGeneric")); + optionalsProvidersAndListsException("unsupported type for D in abstract static class io.helidon.pico.tools" + + ".TypeToolsTest$ProviderOfGeneric implements jakarta.inject.Provider", + providerOfGenericClassInfo.getFieldInfo("optionalProviderOfGeneric")); + optionalsProvidersAndListsException("unsupported type for D in abstract static class io.helidon.pico.tools" + + ".TypeToolsTest$ProviderOfGeneric implements jakarta.inject.Provider", + providerOfGenericClassInfo.getFieldInfo("optionalOfGeneric")); + + optionalsProvidersAndLists(stringTypeName, true, false, false, + providerOfDirectClassInfo.getFieldInfo("providerOfString")); + optionalsProvidersAndLists(stringTypeName, true, true, false, + providerOfDirectClassInfo.getFieldInfo("listOfProvidersOfStrings")); + optionalsProvidersAndLists(stringTypeName, true, false, true, + providerOfDirectClassInfo.getFieldInfo("optionalProviderOfString")); + optionalsProvidersAndLists(stringTypeName, false, false, true, + providerOfDirectClassInfo.getFieldInfo("optionalString")); + optionalsProvidersAndLists(stringTypeName, false, false, false, + providerOfDirectClassInfo.getFieldInfo("string")); + + optionalsProvidersAndLists(stringTypeName, true, false, false, + providerOfDirectClassInfo.getFieldInfo("providerOfString")); + optionalsProvidersAndLists(stringTypeName, true, true, false, + providerOfDirectClassInfo.getFieldInfo("listOfProvidersOfStrings")); + optionalsProvidersAndLists(stringTypeName, true, false, true, + providerOfDirectClassInfo.getFieldInfo("optionalProviderOfString")); + optionalsProvidersAndLists(stringTypeName, false, false, true, + providerOfDirectClassInfo.getFieldInfo("optionalString")); + optionalsProvidersAndLists(stringTypeName, false, false, false, + providerOfDirectClassInfo.getFieldInfo("string")); + } + + @Test + void optionalsProvidersAndListsOfMethodParams() { + optionalsProvidersAndLists(typeToolsTestTypeName, true, false, false, + providerOfGenericClassInfo.getMethodInfo("setProvider") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndLists(typeToolsTestTypeName, true, true, false, + providerOfGenericClassInfo.getMethodInfo("setListOfProviders") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndLists(typeToolsTestTypeName, true, false, true, + providerOfGenericClassInfo.getMethodInfo("setOptionalProvider") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndLists(typeToolsTestTypeName, false, false, true, + providerOfGenericClassInfo.getMethodInfo("setOptionalNotProvider") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndLists(typeToolsTestTypeName, false, false, false, + providerOfGenericClassInfo.getMethodInfo("setTyped") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndListsException("unsupported type for D in public void setGeneric(D)", + providerOfGenericClassInfo.getMethodInfo("setGeneric") + .get(0).getParameterInfo()[0]); + + optionalsProvidersAndLists(stringTypeName, true, false, false, + providerOfDirectClassInfo.getMethodInfo("setProviderOfString") + .get(0).getParameterInfo()[0]); + optionalsProvidersAndLists(stringTypeName, false, false, false, + providerOfDirectClassInfo.getMethodInfo("setString") + .get(0).getParameterInfo()[0]); + } + + private void optionalsProvidersAndLists(String expectedType, + boolean expectedProvider, + boolean expectedList, + boolean expectedOptional, + FieldInfo fld) { + assertNotNull(fld); + AtomicReference isProvider = new AtomicReference<>(); + AtomicReference isList = new AtomicReference<>(); + AtomicReference isOptional = new AtomicReference<>(); + assertThat(extractInjectionPointTypeInfo(fld, isProvider, isList, isOptional), equalTo(expectedType)); + assertThat("provider for " + fld, isProvider.get(), is(expectedProvider)); + assertThat("list for " + fld, isList.get(), equalTo(expectedList)); + assertThat("optional for " + fld, isOptional.get(), is(expectedOptional)); + } + + private void optionalsProvidersAndListsException(String exceptedException, + FieldInfo fld) { + AtomicReference isProvider = new AtomicReference<>(); + AtomicReference isList = new AtomicReference<>(); + AtomicReference isOptional = new AtomicReference<>(); + ToolsException te = assertThrows(ToolsException.class, + () -> extractInjectionPointTypeInfo(fld, isProvider, isList, isOptional)); + assertThat(fld.toString(), te.getMessage(), equalTo(exceptedException)); + } + + private void optionalsProvidersAndLists(String expectedType, + boolean expectedProvider, + boolean expectedList, + boolean expectedOptional, + MethodParameterInfo fld) { + AtomicReference isProvider = new AtomicReference<>(); + AtomicReference isList = new AtomicReference<>(); + AtomicReference isOptional = new AtomicReference<>(); + assertThat(extractInjectionPointTypeInfo(fld, isProvider, isList, isOptional), equalTo(expectedType)); + assertThat("provider for " + fld, isProvider.get(), is(expectedProvider)); + assertThat("list for " + fld, isList.get(), equalTo(expectedList)); + assertThat("optional for " + fld, isOptional.get(), is(expectedOptional)); + } + + private void optionalsProvidersAndListsException(String exceptedException, + MethodParameterInfo fld) { + AtomicReference isProvider = new AtomicReference<>(); + AtomicReference isList = new AtomicReference<>(); + AtomicReference isOptional = new AtomicReference<>(); + ToolsException te = assertThrows(ToolsException.class, + () -> extractInjectionPointTypeInfo(fld, isProvider, isList, isOptional)); + assertThat(fld.toString(), te.getMessage(), equalTo(exceptedException)); + } + + + static abstract class ProviderOfGeneric implements Provider { + List> listOfProviders; + Optional> optionalProvider; + Optional optionalNotProvider; + D generic; + List> listOfProvidersOfGeneric; + Optional> optionalProviderOfGeneric; + Optional optionalOfGeneric; + + public void setProvider(Provider ignored) { + } + + public void setListOfProviders(List> ignored) { + } + + public void setOptionalProvider(Optional> ignored) { + } + + public void setOptionalNotProvider(Optional ignored) { + } + + public void setTyped(TypeToolsTest ignored) { + } + + public void setGeneric(D ignored) { + } + } + + + static class ProviderOfTypeDirect implements Provider { + Provider providerOfString; + List> listOfProvidersOfStrings; + Optional> optionalProviderOfString; + Optional optionalString; + String string; + + public ProviderOfTypeDirect(Provider provider) { + this.providerOfString = provider; + } + + @Override + public String get() { + return (providerOfString == null) ? null : providerOfString.get(); + } + + void setProviderOfString(Provider provider) { + this.providerOfString = provider; + } + + void setString(String ignored) { + } + } + + static class ProviderOfTypeThroughSuper extends ProviderOfGeneric { + Provider provider; + + public ProviderOfTypeThroughSuper(Provider provider) { + this.provider = provider; + } + + @Override + public Integer get() { + return (provider == null) ? null : provider.get(); + } + } + + static class InjectionPointProviderOfTypeDirect extends ProviderOfGeneric implements Provider { + Provider provider; + + public InjectionPointProviderOfTypeDirect(Provider provider) { + this.provider = provider; + } + + @Override + public Boolean get() { + return (provider == null) ? null : provider.get(); + } + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorld.java b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorld.java new file mode 100644 index 00000000000..f44959e67bf --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorld.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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.tools.testsubjects; + +import io.helidon.pico.Contract; + +/** + * For testing. + */ +@Contract +public interface HelloPicoWorld { + + /** + * For testing. + * + * @return for testing + */ + String sayHello(); + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java new file mode 100644 index 00000000000..58ea85b0926 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/HelloPicoWorldImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 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.tools.testsubjects; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +@RunLevel(0) +public class HelloPicoWorldImpl implements HelloPicoWorld { + + @Inject + PicoWorld world; + + @Inject + Provider worldRef; + + @Inject + List> listOfWorldRefs; + + @Inject + List listOfWorlds; + + @Inject @Named("red") + Optional redWorld; + + private PicoWorld setWorld; + + int postConstructCallCount; + int preDestroyCallCount; + + @Override + public String sayHello() { + assert(postConstructCallCount == 1); + assert(preDestroyCallCount == 0); + assert(world == worldRef.get()); + assert(world == setWorld); + assert(redWorld.isEmpty()); + + return "Hello " + world.name(); + } + + @Inject + void world(PicoWorld world) { + this.setWorld = world; + } + + @PostConstruct + public void postConstruct() { + postConstructCallCount++; + } + + @PreDestroy + public void preDestroy() { + preDestroyCallCount++; + } + + public int postConstructCallCount() { + return postConstructCallCount; + } + + public int preDestroyCallCount() { + return preDestroyCallCount; + } + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorld.java b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorld.java new file mode 100644 index 00000000000..82fc9a50fd4 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorld.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.tools.testsubjects; + +/** + * For testing. + */ +// @Contract - we will test ExternalContracts here instead +public interface PicoWorld { + + /** + * For testing. + * + * @return for testing + */ + String name(); + +} diff --git a/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorldImpl.java b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorldImpl.java new file mode 100644 index 00000000000..4b2f36ab203 --- /dev/null +++ b/pico/tools/src/test/java/io/helidon/pico/tools/testsubjects/PicoWorldImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 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.tools.testsubjects; + +import io.helidon.pico.ExternalContracts; + +@ExternalContracts(PicoWorld.class) +public class PicoWorldImpl implements PicoWorld { + private final String name; + + PicoWorldImpl() { + this("pico"); + } + + PicoWorldImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + +} diff --git a/pico/types/README.md b/pico/types/README.md deleted file mode 100644 index 1035810eb54..00000000000 --- a/pico/types/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# pico-types - -This module provides a minimal set of types for runtime use in order to avoid the need to bring in the entire Pico SPI module. diff --git a/pom.xml b/pom.xml index 64a4208b72f..9885f686fa4 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,7 @@ 1.23 2.26.3 3.10 - 4.8.87 + 4.8.154 @@ -1190,6 +1190,12 @@ commons-io ${version.lib.commons-io} + + + jakarta.inject + jakarta.inject-tck + ${version.lib.jakarta.inject} + diff --git a/tests/integration/native-image/mp-1/pom.xml b/tests/integration/native-image/mp-1/pom.xml index e6a4c64316b..02c0d12057f 100644 --- a/tests/integration/native-image/mp-1/pom.xml +++ b/tests/integration/native-image/mp-1/pom.xml @@ -51,6 +51,11 @@ io.helidon.security.providers helidon-security-providers-oidc + + io.helidon.pico + helidon-pico-services + true + io.helidon.tracing helidon-tracing-jaeger diff --git a/tests/integration/native-image/mp-1/src/main/java/module-info.java b/tests/integration/native-image/mp-1/src/main/java/module-info.java index b6508ef88ac..5323575b40c 100644 --- a/tests/integration/native-image/mp-1/src/main/java/module-info.java +++ b/tests/integration/native-image/mp-1/src/main/java/module-info.java @@ -41,6 +41,9 @@ requires io.opentracing.api; requires io.opentracing.util; + // needed to compile pico generated classes + requires static io.helidon.pico.services; + exports io.helidon.tests.integration.nativeimage.mp1; exports io.helidon.tests.integration.nativeimage.mp1.other; diff --git a/tests/integration/native-image/mp-3/pom.xml b/tests/integration/native-image/mp-3/pom.xml index 26ab2a271bb..fbcc537139d 100644 --- a/tests/integration/native-image/mp-3/pom.xml +++ b/tests/integration/native-image/mp-3/pom.xml @@ -38,6 +38,11 @@ io.helidon.microprofile.bundles helidon-microprofile + + io.helidon.pico + helidon-pico-services + true + org.jboss jandex