From 70fdca446717bf9de62c0a2361e0ffd7d32f72de Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 18 Aug 2023 01:04:05 +0200 Subject: [PATCH 1/3] Test for supplier --- .../test/testsubjects/SupplierBlueprint.java | 42 ++++++++++++++++ .../io/helidon/builder/test/SupplierTest.java | 49 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java create mode 100644 builder/tests/builder/src/test/java/io/helidon/builder/test/SupplierTest.java diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java new file mode 100644 index 00000000000..f44063053ca --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java @@ -0,0 +1,42 @@ +/* + * 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.test.testsubjects; + +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 SupplierBlueprint { + /** + * 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(); +} 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..5e333a6bb49 --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/SupplierTest.java @@ -0,0 +1,49 @@ +package io.helidon.builder.test; + +import java.util.Optional; +import java.util.function.BiConsumer; + +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; + +class SupplierTest { + private static final String KEY = "string-supplier"; + private static final String ORIGINAL_VALUE = "value"; + private static final String NEW_VALUE = "new-value"; + + @Test + void testChange() { + Config config = Config.just(new TestSource()); + + } + + private static class TestSource implements ConfigSource, EventConfigSource, NodeConfigSource { + + private BiConsumer consumer; + + @Override + public void onChange(BiConsumer changedNode) { + this.consumer = changedNode; + } + + @Override + public Optional load() throws ConfigException { + return Optional.of(ConfigContent.NodeContent.builder() + .node(ConfigNode.ObjectNode.builder() + .addValue(KEY, ORIGINAL_VALUE) + .build()) + .build()); + } + + void update() { + consumer.accept(KEY, ConfigNode.ValueNode.create(NEW_VALUE)); + } + } +} From e800c18fdad1359e4361564a81b932aa8c5e4588 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Fri, 18 Aug 2023 10:37:38 +0200 Subject: [PATCH 2/3] Support for suppliers in builder, using config suppliers when configured. --- .../builder/processor/TypeHandler.java | 15 +- .../builder/processor/TypeHandlerMap.java | 3 +- .../processor/TypeHandlerSupplier.java | 190 ++++++++++++++++++ ...eprint.java => SupplierBeanBlueprint.java} | 4 +- .../io/helidon/builder/test/SupplierTest.java | 39 +++- .../io/helidon/common/config/ConfigValue.java | 39 +++- .../io/helidon/common/config/EmptyConfig.java | 16 ++ 7 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.java rename builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/{SupplierBlueprint.java => SupplierBeanBlueprint.java} (93%) 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..c3507669f48 --- /dev/null +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.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.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 { + method.add(setterName() + "("); + method.add(configGet(configured)); + method.add(generateFromConfig(factoryMethods)); + method.addLine(".supplier());"); + } + } + + @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) { + Method.Builder builder = Method.builder() + .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)); + builder.typeName(Objects.class).addLine(".requireNonNull(" + name() + ");"); + builder.addLine("this." + name() + " = " + name() + "::get;") + .addLine("return self();"); + classBuilder.addMethod(builder); + } +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.java similarity index 93% rename from builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java rename to builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.java index f44063053ca..93314e93c3d 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBlueprint.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/SupplierBeanBlueprint.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. @@ -27,7 +27,7 @@ */ @Prototype.Blueprint @Configured -interface SupplierBlueprint { +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. 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 index 5e333a6bb49..7900d4e8721 100644 --- 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 @@ -1,8 +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.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; @@ -13,17 +31,34 @@ import org.junit.jupiter.api.Test; +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 ORIGINAL_VALUE = "value"; private static final String NEW_VALUE = "new-value"; @Test - void testChange() { - Config config = Config.just(new TestSource()); + void testChangeString() { + TestSource testSource = new TestSource(); + Config config = Config.just(testSource); + SupplierBean b = SupplierBean.create(config); + + Supplier stringSupplier = b.stringSupplier(); + Supplier arraySupplier = b.charSupplier(); + + assertThat(stringSupplier.get(), is(ORIGINAL_VALUE)); + assertThat(arraySupplier.get(), is(ORIGINAL_VALUE.toCharArray())); + testSource.update(); + + assertThat(stringSupplier.get(), is(NEW_VALUE)); + assertThat(arraySupplier.get(), is(NEW_VALUE.toCharArray())); } + + private static class TestSource implements ConfigSource, EventConfigSource, NodeConfigSource { private BiConsumer consumer; 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 { From d6b4e029895ab74a31d61e2580146b859022a89b Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Sun, 20 Aug 2023 20:43:52 +0200 Subject: [PATCH 3/3] Support for supplier of optional values --- .../processor/TypeHandlerSupplier.java | 30 +++++-- .../testsubjects/SupplierBeanBlueprint.java | 7 ++ .../io/helidon/builder/test/SupplierTest.java | 80 ++++++++++++++++--- 3 files changed, 101 insertions(+), 16 deletions(-) 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 index c3507669f48..0a900e6e7dc 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerSupplier.java @@ -65,6 +65,11 @@ void generateFromConfig(Method.Builder method, PrototypeProperty.ConfiguredOptio 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)); @@ -73,6 +78,19 @@ void generateFromConfig(Method.Builder method, PrototypeProperty.ConfiguredOptio } } + 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, @@ -173,18 +191,16 @@ protected void declaredSetter(InnerClass.Builder classBuilder, PrototypeProperty.ConfiguredOption configured, TypeName returnType, Javadoc blueprintJavadoc) { - Method.Builder builder = Method.builder() - .name(setterName()) + 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)); - builder.typeName(Objects.class).addLine(".requireNonNull(" + name() + ");"); - builder.addLine("this." + name() + " = " + name() + "::get;") - .addLine("return self();"); - classBuilder.addMethod(builder); + .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 index 93314e93c3d..470be31f843 100644 --- 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 @@ -16,6 +16,7 @@ package io.helidon.builder.test.testsubjects; +import java.util.Optional; import java.util.function.Supplier; import io.helidon.builder.api.Prototype; @@ -39,4 +40,10 @@ interface SupplierBeanBlueprint { @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 index 7900d4e8721..e47d0aae29f 100644 --- 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 @@ -31,38 +31,88 @@ 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(); + 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.toCharArray())); + assertThat(arraySupplier.get(), is(ORIGINAL_VALUE_CHARS)); + assertThat(optionalSupplier.get(), optionalValue(is(ORIGINAL_VALUE))); + assertThat(optionalCharSupplier.get(), optionalValue(is(ORIGINAL_VALUE_CHARS))); - testSource.update(); + testSource.update(NEW_VALUE); assertThat(stringSupplier.get(), is(NEW_VALUE)); - assertThat(arraySupplier.get(), is(NEW_VALUE.toCharArray())); + 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; @@ -70,15 +120,27 @@ public void onChange(BiConsumer 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(ConfigNode.ObjectNode.builder() - .addValue(KEY, ORIGINAL_VALUE) - .build()) + .node(rootNode.build()) .build()); } - void update() { - consumer.accept(KEY, ConfigNode.ValueNode.create(NEW_VALUE)); + 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()); } } }