diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java index 39b75b9a754..cb9e831fe90 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandler.java @@ -56,6 +56,9 @@ static TypeHandler create(String name, String getterName, String setterName, Typ if (TypeNames.OPTIONAL.equals(returnType)) { return new TypeHandlerOptional(name, getterName, setterName, returnType); } + if (TypeNames.SUPPLIER.equals(returnType)) { + return new TypeHandlerSupplier(name, getterName, setterName, returnType); + } if (TypeNames.SET.equals(returnType)) { return new TypeHandlerSet(name, getterName, setterName, returnType); } @@ -202,7 +205,7 @@ String configGet(PrototypeProperty.ConfiguredOption configured) { String generateFromConfig(FactoryMethods factoryMethods) { if (actualType().fqName().equals("char[]")) { - return ".asString().map(String::toCharArray)"; + return ".asString().as(String::toCharArray)"; } TypeName boxed = actualType().boxed(); @@ -214,7 +217,7 @@ String generateFromConfig(FactoryMethods factoryMethods) { void generateFromConfig(Method.Builder method, FactoryMethods factoryMethods) { if (actualType().fqName().equals("char[]")) { - method.add(".asString().map(").typeName(String.class).add("::toCharArray)"); + method.add(".asString().as(").typeName(String.class).add("::toCharArray)"); return; } @@ -296,10 +299,10 @@ boolean builderGetterOptional(boolean required, boolean hasDefault) { } - private void declaredSetter(InnerClass.Builder classBuilder, - PrototypeProperty.ConfiguredOption configured, - TypeName returnType, - Javadoc blueprintJavadoc) { + protected void declaredSetter(InnerClass.Builder classBuilder, + PrototypeProperty.ConfiguredOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { Method.Builder builder = Method.builder() .name(setterName()) .returnType(returnType, "updated builder instance") diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java index 92136ec25b4..69493254098 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java @@ -416,7 +416,8 @@ private void declaredSetterAdd(InnerClass.Builder classBuilder, PrototypePropert .addLine("return self();")); } - private void declaredSetter(InnerClass.Builder classBuilder, + @Override + protected void declaredSetter(InnerClass.Builder classBuilder, PrototypeProperty.ConfiguredOption configured, TypeName returnType, Javadoc blueprintJavadoc) { diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.java new file mode 100644 index 00000000000..0a900e6e7dc --- /dev/null +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.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.processor; + +import java.util.Objects; +import java.util.function.Consumer; + +import io.helidon.common.processor.classmodel.Field; +import io.helidon.common.processor.classmodel.InnerClass; +import io.helidon.common.processor.classmodel.Javadoc; +import io.helidon.common.processor.classmodel.Method; +import io.helidon.common.types.TypeName; + +import static io.helidon.builder.processor.Types.CHAR_ARRAY_TYPE; +import static io.helidon.builder.processor.Types.STRING_TYPE; +import static io.helidon.common.types.TypeNames.SUPPLIER; + +class TypeHandlerSupplier extends TypeHandler.OneTypeHandler { + + TypeHandlerSupplier(String name, String getterName, String setterName, TypeName declaredType) { + super(name, getterName, setterName, declaredType); + } + + @Override + Field.Builder fieldDeclaration(PrototypeProperty.ConfiguredOption configured, boolean isBuilder, boolean alwaysFinal) { + Field.Builder builder = Field.builder() + .type(declaredType()) + .name(name()) + .isFinal(alwaysFinal || !isBuilder); + + if (isBuilder && configured.hasDefault()) { + builder.defaultValue("() -> " + configured.defaultValue()); + } + + return builder; + } + + @Override + TypeName argumentTypeName() { + return TypeName.builder(SUPPLIER) + .addTypeArgument(toWildcard(actualType())) + .build(); + } + + @Override + void generateFromConfig(Method.Builder method, PrototypeProperty.ConfiguredOption configured, FactoryMethods factoryMethods) { + if (configured.provider()) { + return; + } + if (factoryMethods.createFromConfig().isPresent()) { + method.addLine(configGet(configured) + + generateFromConfig(factoryMethods) + + ".ifPresent(this::" + setterName() + ");"); + } else if (actualType().isOptional()) { + method.add(setterName() + "("); + method.add(configGet(configured)); + method.add(generateFromConfigOptional(factoryMethods)); + method.addLine(".optionalSupplier());"); + } else { + method.add(setterName() + "("); + method.add(configGet(configured)); + method.add(generateFromConfig(factoryMethods)); + method.addLine(".supplier());"); + } + } + + String generateFromConfigOptional(FactoryMethods factoryMethods) { + TypeName optionalType = actualType().typeArguments().get(0); + if (optionalType.fqName().equals("char[]")) { + return ".asString().as(String::toCharArray)"; + } + + TypeName boxed = optionalType.boxed(); + return factoryMethods.createFromConfig() + .map(it -> ".map(" + it.typeWithFactoryMethod().genericTypeName().fqName() + "::" + it.createMethodName() + ")") + .orElseGet(() -> ".as(" + boxed.fqName() + ".class)"); + + } + + @Override + void setters(InnerClass.Builder classBuilder, + PrototypeProperty.ConfiguredOption configured, + PrototypeProperty.Singular singular, + FactoryMethods factoryMethod, + TypeName returnType, + Javadoc blueprintJavadoc) { + + declaredSetter(classBuilder, configured, returnType, blueprintJavadoc); + + // and add the setter with the actual type + Method.Builder method = Method.builder() + .name(setterName()) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(name()) + .type(actualType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .typeName(Objects.class) + .addLine(".requireNonNull(" + name() + ");") + .addLine("this." + name() + " = () -> " + name() + ";") + .addLine("return self();"); + classBuilder.addMethod(method); + + if (actualType().equals(CHAR_ARRAY_TYPE)) { + classBuilder.addMethod(builder -> builder.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(STRING_TYPE) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .typeName(Objects.class).addLine(".requireNonNull(" + name() + ");") + .addLine("this." + name() + " = () -> " + name() + ".toCharArray();") + .addLine("return self();")); + } + + if (factoryMethod.createTargetType().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.createTargetType().get(); + String argumentName = name() + "Config"; + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(argumentName) + .type(fm.argumentType()) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .typeName(Objects.class) + .addLine(".requireNonNull(" + argumentName + ");") + .add("this." + name() + " = ") + .typeName(fm.typeWithFactoryMethod().genericTypeName()) + .addLine("." + fm.createMethodName() + "(" + argumentName + ");") + .addLine("return self();")); + } + + if (factoryMethod.builder().isPresent()) { + // if there is a factory method for the return type, we also have setters for the type (probably config object) + FactoryMethods.FactoryMethod fm = factoryMethod.builder().get(); + + TypeName builderType; + String className = fm.factoryMethodReturnType().className(); + if (className.equals("Builder") || className.endsWith(".Builder")) { + builderType = fm.factoryMethodReturnType(); + } else { + builderType = TypeName.create(fm.factoryMethodReturnType().fqName() + ".Builder"); + } + String argumentName = "consumer"; + TypeName argumentType = TypeName.builder() + .type(Consumer.class) + .addTypeArgument(builderType) + .build(); + + classBuilder.addMethod(builder -> builder.name(setterName()) + .accessModifier(setterAccessModifier(configured)) + .description(blueprintJavadoc.content()) + .returnType(returnType, "updated builder instance") + .addParameter(param -> param.name(argumentName) + .type(argumentType) + .description(blueprintJavadoc.returnDescription())) + .addJavadocTag("see", "#" + getterName() + "()") + .typeName(Objects.class) + .addLine(".requireNonNull(" + argumentName + ");") + .add("var builder = ") + .typeName(fm.typeWithFactoryMethod().genericTypeName()) + .addLine("." + fm.createMethodName() + "();") + .addLine("consumer.accept(builder);") + .addLine("this." + name() + "(builder.build());") + .addLine("return self();")); + } + } + + protected void declaredSetter(InnerClass.Builder classBuilder, + PrototypeProperty.ConfiguredOption configured, + TypeName returnType, + Javadoc blueprintJavadoc) { + classBuilder.addMethod(method -> method.name(setterName()) + .returnType(returnType, "updated builder instance") + .description(blueprintJavadoc.content()) + .addJavadocTag("see", "#" + getterName() + "()") + .addParameter(param -> param.name(name()) + .type(argumentTypeName()) + .description(blueprintJavadoc.returnDescription())) + .accessModifier(setterAccessModifier(configured)) + .typeName(Objects.class).addLine(".requireNonNull(" + name() + ");") + .addLine("this." + name() + " = " + name() + "::get;") + .addLine("return self();")); + } +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.java new file mode 100644 index 00000000000..470be31f843 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.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.builder.test.testsubjects; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.api.Prototype; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Blueprint with a supplier from configuration. + */ +@Prototype.Blueprint +@Configured +interface SupplierBeanBlueprint { + /** + * This value is either explicitly configured, or uses config to get the supplier. + * If config source with change support is changed, the supplier should provide the latest value from configuration. + * + * @return supplier with latest value + */ + @ConfiguredOption + Supplier stringSupplier(); + + @ConfiguredOption(key = "string-supplier") + Supplier charSupplier(); + + @ConfiguredOption + Supplier> optionalSupplier(); + + @ConfiguredOption(key = "optional-supplier") + Supplier> optionalCharSupplier(); +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/SupplierTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/SupplierTest.java new file mode 100644 index 00000000000..e47d0aae29f --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/SupplierTest.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.builder.test; + +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import io.helidon.builder.test.testsubjects.SupplierBean; +import io.helidon.config.Config; +import io.helidon.config.ConfigException; +import io.helidon.config.spi.ConfigContent; +import io.helidon.config.spi.ConfigNode; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.EventConfigSource; +import io.helidon.config.spi.NodeConfigSource; + +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.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class SupplierTest { + private static final String KEY = "string-supplier"; + private static final String KEY_OPTIONAL = "optional-supplier"; + private static final String ORIGINAL_VALUE = "value"; + public static final char[] ORIGINAL_VALUE_CHARS = ORIGINAL_VALUE.toCharArray(); + private static final String NEW_VALUE = "new-value"; + public static final char[] NEW_VALUE_CHARS = NEW_VALUE.toCharArray(); + + @Test + void testChangeString() { + TestSource testSource = new TestSource(ORIGINAL_VALUE); + Config config = Config.just(testSource); + SupplierBean b = SupplierBean.create(config); + + Supplier stringSupplier = b.stringSupplier(); + Supplier arraySupplier = b.charSupplier(); + Supplier> optionalSupplier = b.optionalSupplier(); + Supplier> optionalCharSupplier = b.optionalCharSupplier(); + + assertThat(stringSupplier.get(), is(ORIGINAL_VALUE)); + assertThat(arraySupplier.get(), is(ORIGINAL_VALUE_CHARS)); + assertThat(optionalSupplier.get(), optionalValue(is(ORIGINAL_VALUE))); + assertThat(optionalCharSupplier.get(), optionalValue(is(ORIGINAL_VALUE_CHARS))); + + testSource.update(NEW_VALUE); + + assertThat(stringSupplier.get(), is(NEW_VALUE)); + assertThat(arraySupplier.get(), is(NEW_VALUE_CHARS)); + assertThat(optionalSupplier.get(), optionalValue(is(NEW_VALUE))); + assertThat(optionalCharSupplier.get(), optionalValue(is(NEW_VALUE_CHARS))); + } + + @Test + void testChangeOptionalStringFromEmpty() { + TestSource testSource = new TestSource(null); + Config config = Config.just(testSource); + SupplierBean b = SupplierBean.create(config); + + Supplier> optionalSupplier = b.optionalSupplier(); + Supplier> optionalCharSupplier = b.optionalCharSupplier(); + + assertThat(optionalSupplier.get(), optionalEmpty()); + assertThat(optionalCharSupplier.get(), optionalEmpty()); + + testSource.update(NEW_VALUE); + + assertThat(optionalSupplier.get(), optionalValue(is(NEW_VALUE))); + assertThat(optionalCharSupplier.get(), optionalValue(is(NEW_VALUE_CHARS))); + } + + @Test + void testChangeOptionalStringToEmpty() { + TestSource testSource = new TestSource(ORIGINAL_VALUE); + Config config = Config.just(testSource); + SupplierBean b = SupplierBean.create(config); + + Supplier> optionalSupplier = b.optionalSupplier(); + Supplier> optionalCharSupplier = b.optionalCharSupplier(); + + assertThat(optionalSupplier.get(), optionalValue(is(ORIGINAL_VALUE))); + assertThat(optionalCharSupplier.get(), optionalValue(is(ORIGINAL_VALUE_CHARS))); + + testSource.update(null); + + assertThat(optionalSupplier.get(), optionalEmpty()); + assertThat(optionalCharSupplier.get(), optionalEmpty()); + } + + private static class TestSource implements ConfigSource, EventConfigSource, NodeConfigSource { + + private final String optionalValue; + private BiConsumer consumer; + + private TestSource(String optionalValue) { + this.optionalValue = optionalValue; + } + + @Override + public void onChange(BiConsumer changedNode) { + this.consumer = changedNode; + } + + @Override + public Optional load() throws ConfigException { + ConfigNode.ObjectNode.Builder rootNode = ConfigNode.ObjectNode.builder() + .addValue(KEY, ORIGINAL_VALUE); + if (optionalValue != null) { + rootNode.addValue(KEY_OPTIONAL, optionalValue); + + } + return Optional.of(ConfigContent.NodeContent.builder() + .node(rootNode.build()) + .build()); + } + + void update(String newOptionalValue) { + ConfigNode.ObjectNode.Builder rootNode = ConfigNode.ObjectNode.builder() + .addValue(KEY, NEW_VALUE); + if (newOptionalValue == null) { + rootNode.addObject(KEY_OPTIONAL, ConfigNode.ObjectNode.empty()); + } else { + rootNode.addValue(KEY_OPTIONAL, newOptionalValue); + } + + consumer.accept("", rootNode.build()); + } + } +} diff --git a/common/config/src/main/java/io/helidon/common/config/ConfigValue.java b/common/config/src/main/java/io/helidon/common/config/ConfigValue.java index 1faa622723f..fc2f95e8be4 100644 --- a/common/config/src/main/java/io/helidon/common/config/ConfigValue.java +++ b/common/config/src/main/java/io/helidon/common/config/ConfigValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -284,4 +284,41 @@ default Stream stream() { return asOptional().stream(); } + /** + * Returns a supplier of a typed value. The semantics depend on implementation, this may always return the + * same value, or it may provide latest value if changes are enabled. + *

+ * Note that {@link Supplier#get()} can throw a {@link io.helidon.common.config.ConfigException} if the value + * cannot be converted, or if the value is missing. + * + * @return a supplier of a typed value + */ + Supplier supplier(); + + /** + * Returns a supplier of a typed value with a default. + *

+ * The semantics depend on implementation, this may always return the + * same value, or it may provide latest value if changes are enabled. + *

+ * Note that {@link Supplier#get()} can throw a {@link io.helidon.common.config.ConfigException} if the value + * cannot be converted. + * + * @param defaultValue a value to be returned if the supplied value represents a {@link Config} node that has no direct + * value + * @return a supplier of a typed value + */ + Supplier supplier(T defaultValue); + + /** + * Returns a {@link Supplier} of an {@link Optional Optional<T>} of the configuration node. + *

+ * Supplier returns a {@link Optional#empty() empty} if the node does not have a direct value. + * + * @return a supplier of the value as an {@link Optional} typed instance, {@link Optional#empty() empty} in case the node + * does not have a direct value + * @see #asOptional() + * @see #supplier() + */ + Supplier> optionalSupplier(); } diff --git a/common/config/src/main/java/io/helidon/common/config/EmptyConfig.java b/common/config/src/main/java/io/helidon/common/config/EmptyConfig.java index c5681f51eab..797f023c7c8 100644 --- a/common/config/src/main/java/io/helidon/common/config/EmptyConfig.java +++ b/common/config/src/main/java/io/helidon/common/config/EmptyConfig.java @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; final class EmptyConfig { static final Config.Key ROOT_KEY = new KeyImpl(null, ""); @@ -139,6 +140,21 @@ public T get() throws ConfigException { public ConfigValue as(Function mapper) { return new EmptyValue<>(key); } + + @Override + public Supplier supplier() { + return this::get; + } + + @Override + public Supplier supplier(T defaultValue) { + return () -> defaultValue; + } + + @Override + public Supplier> optionalSupplier() { + return this::asOptional; + } } private static final class EmptyNode implements Config {