diff --git a/bom/pom.xml b/bom/pom.xml index 3d052bc7104..074c02c1bc9 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -248,6 +248,11 @@ helidon-config-object-mapping ${helidon.version} + + io.helidon.config + helidon-config-mp + ${helidon.version} + io.helidon.security diff --git a/common/metrics/pom.xml b/common/metrics/pom.xml index 606a3f8335e..3fd541fe7d0 100644 --- a/common/metrics/pom.xml +++ b/common/metrics/pom.xml @@ -39,14 +39,7 @@ org.eclipse.microprofile.metrics microprofile-metrics-api - ${version.lib.microprofile-metrics-api} provided - - - org.osgi - org.osgi.annotation.versioning - - diff --git a/common/service-loader/src/main/java/io/helidon/common/serviceloader/Priorities.java b/common/service-loader/src/main/java/io/helidon/common/serviceloader/Priorities.java index 411e1a138aa..5d3300fb287 100644 --- a/common/service-loader/src/main/java/io/helidon/common/serviceloader/Priorities.java +++ b/common/service-loader/src/main/java/io/helidon/common/serviceloader/Priorities.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,12 +89,26 @@ public static void sort(List list) { * @param defaultPriority default priority for elements that do not have it */ public static void sort(List list, int defaultPriority) { - list.sort(Comparator.comparingInt(it -> { + list.sort(priorityComparator(defaultPriority)); + } + + /** + * Returns a comparator for two objects, the classes for which are implementations of + * {@link io.helidon.common.Prioritized}, and/or optionally annotated with {@link javax.annotation.Priority} + * and which applies a specified default priority if either or both classes lack the annotation. + * + * @param type of object being compared + * @param defaultPriority used if the classes for either or both objects + * lack the {@code Priority} annotation + * @return comparator + */ + public static Comparator priorityComparator(int defaultPriority) { + return Comparator.comparingInt(it -> { if (it instanceof Class) { return find((Class) it, defaultPriority); } else { return find(it, defaultPriority); } - })); + }); } } diff --git a/common/service-loader/src/test/java/io/helidon/common/serviceloader/PrioritiesTest.java b/common/service-loader/src/test/java/io/helidon/common/serviceloader/PrioritiesTest.java new file mode 100644 index 00000000000..bebc3e03505 --- /dev/null +++ b/common/service-loader/src/test/java/io/helidon/common/serviceloader/PrioritiesTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.serviceloader; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Priority; + +import io.helidon.common.Prioritized; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class PrioritiesTest { + @Test + void testSort() { + List services = new ArrayList<>(5); + services.add(new DefaultPriority()); + services.add(new VeryLowPriority()); + services.add(new LowestPriority()); + services.add(new HigherPriority()); + services.add(new LowerPriority()); + + Priorities.sort(services, 100); + + validate(services); + } + + @Test + void testComparator() { + List services = new ArrayList<>(5); + // intentionally different order than in other methods, to make sure it is not working "by accident" + services.add(new LowestPriority()); + services.add(new LowerPriority()); + services.add(new VeryLowPriority()); + services.add(new HigherPriority()); + services.add(new DefaultPriority()); + + services.sort(Priorities.priorityComparator(100)); + + validate(services); + } + + private void validate(List services) { + assertThat("There must be 5 services in the list", services.size(), is(5)); + assertThat(services.get(0).getIt(), is(HigherPriority.IT)); + assertThat(services.get(1).getIt(), is(LowerPriority.IT)); + assertThat(services.get(2).getIt(), is(DefaultPriority.IT)); + assertThat(services.get(3).getIt(), is(VeryLowPriority.IT)); + assertThat(services.get(4).getIt(), is(LowestPriority.IT)); + } + + private interface Service { + String getIt(); + } + + @Priority(1) + private static class HigherPriority implements Service { + private static final String IT = "higher"; + + @Override + public String getIt() { + return IT; + } + } + + @Priority(2) + private static class LowerPriority implements Service { + private static final String IT = "lower"; + + @Override + public String getIt() { + return IT; + } + } + + private static class DefaultPriority implements Service { + private static final String IT = "default"; + + @Override + public String getIt() { + return IT; + } + } + + @Priority(101) + private static class VeryLowPriority implements Service { + private static final String IT = "veryLow"; + + @Override + public String getIt() { + return IT; + } + } + + private static class LowestPriority implements Service, Prioritized { + private static final String IT = "lowest"; + + @Override + public String getIt() { + return IT; + } + + @Override + public int priority() { + return 1000; + } + } +} diff --git a/config/tests/test-mp-reference/pom.xml b/config/config-mp/pom.xml similarity index 63% rename from config/tests/test-mp-reference/pom.xml rename to config/config-mp/pom.xml index 8e91315428a..98090d26d33 100644 --- a/config/tests/test-mp-reference/pom.xml +++ b/config/config-mp/pom.xml @@ -16,33 +16,40 @@ --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.helidon.config.tests - helidon-config-tests-project + io.helidon.config + helidon-config-project 2.0.0-SNAPSHOT - helidon-config-test-mp-reference - Helidon Config Tests MP Reference - - - Integration tests of reference in MP - + helidon-config-mp + Helidon Config MP + Core of the implementation of MicroProfile Config specification - org.eclipse.microprofile.config - microprofile-config-api + jakarta.annotation + jakarta.annotation-api + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-service-loader io.helidon.config helidon-config - runtime - + + org.eclipse.microprofile.config + microprofile-config-api + org.junit.jupiter junit-jupiter-api diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java new file mode 100644 index 00000000000..8b6fb7f9d4b --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.helidon.config.ConfigSources; + +import org.eclipse.microprofile.config.Config; + +/** + * Utilities for Helidon MicroProfile Config implementation. + */ +public final class MpConfig { + private MpConfig() { + } + + /** + * This method allows use to use Helidon Config on top of an MP config. + * There is a limitation - the converters configured with MP config will not be available, unless + * the implementation is coming from Helidon. + *

+ * If you want to use the Helidon {@link io.helidon.config.Config} API instead of the MicroProfile + * {@link org.eclipse.microprofile.config.Config} one, this method will create a Helidon config + * instance that is based on the provided configuration instance. + * + * @param mpConfig MP Config instance + * @return a new Helidon config using only the mpConfig as its config source + */ + @SuppressWarnings("unchecked") + public static io.helidon.config.Config toHelidonConfig(Config mpConfig) { + if (mpConfig instanceof io.helidon.config.Config) { + return (io.helidon.config.Config) mpConfig; + } + + io.helidon.config.Config.Builder builder = io.helidon.config.Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .disableMapperServices() + .disableCaching() + .disableParserServices() + .disableFilterServices(); + + if (mpConfig instanceof MpConfigImpl) { + ((MpConfigImpl) mpConfig).converters() + .forEach((clazz, converter) -> { + Class cl = (Class) clazz; + builder.addStringMapper(cl, converter::convert); + }); + } + + Map allConfig = new HashMap<>(); + mpConfig.getPropertyNames() + .forEach(it -> { + // covering the condition where a config key disappears between getting the property names and requesting + // the value + Optional optionalValue = mpConfig.getOptionalValue(it, String.class); + optionalValue.ifPresent(value -> allConfig.put(it, value)); + }); + + return builder.addSource(ConfigSources.create(allConfig)) + .build(); + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigBuilder.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigBuilder.java new file mode 100644 index 00000000000..9ddf8e6922c --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigBuilder.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.io.File; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.SimpleTimeZone; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; + +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.common.serviceloader.Priorities; +import io.helidon.config.ConfigMappers; +import io.helidon.config.mp.spi.MpConfigFilter; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigBuilder; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.eclipse.microprofile.config.spi.Converter; + +/** + * Configuration builder. + */ +public class MpConfigBuilder implements ConfigBuilder { + private boolean useDefaultSources = false; + private boolean useDiscoveredSources = false; + private boolean useDiscoveredConverters = false; + + private final List sources = new LinkedList<>(); + private final List converters = new LinkedList<>(); + + private ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + MpConfigBuilder() { + } + + @Override + public ConfigBuilder addDefaultSources() { + useDefaultSources = true; + return this; + } + + @Override + public ConfigBuilder addDiscoveredSources() { + useDiscoveredSources = true; + return this; + } + + @Override + public ConfigBuilder addDiscoveredConverters() { + useDiscoveredConverters = true; + return this; + } + + @Override + public ConfigBuilder forClassLoader(ClassLoader loader) { + this.classLoader = loader; + return this; + } + + @Override + public ConfigBuilder withSources(ConfigSource... sources) { + for (ConfigSource source : sources) { + this.sources.add(new OrdinalSource(source)); + } + return this; + } + + @Override + public ConfigBuilder withConverter(Class aClass, int ordinal, Converter converter) { + this.converters.add(new OrdinalConverter(converter, aClass, ordinal)); + return this; + } + + @Override + public ConfigBuilder withConverters(Converter... converters) { + for (Converter converter : converters) { + this.converters.add(new OrdinalConverter(converter)); + } + return this; + } + + @Override + public Config build() { + if (useDefaultSources) { + sources.add(new OrdinalSource(MpConfigSources.systemProperties(), 400)); + sources.add(new OrdinalSource(MpConfigSources.environmentVariables(), 300)); + // microprofile-config.properties + MpConfigSources.classPath(classLoader, "META-INF/microprofile-config.properties") + .stream() + .map(OrdinalSource::new) + .forEach(sources::add); + } + // built-in converters - required by specification + addBuiltIn(converters, Boolean.class, ConfigMappers::toBoolean); + addBuiltIn(converters, Boolean.TYPE, ConfigMappers::toBoolean); + addBuiltIn(converters, Byte.class, Byte::parseByte); + addBuiltIn(converters, Byte.TYPE, Byte::parseByte); + addBuiltIn(converters, Short.class, Short::parseShort); + addBuiltIn(converters, Short.TYPE, Short::parseShort); + addBuiltIn(converters, Integer.class, Integer::parseInt); + addBuiltIn(converters, Integer.TYPE, Integer::parseInt); + addBuiltIn(converters, Long.class, Long::parseLong); + addBuiltIn(converters, Long.TYPE, Long::parseLong); + addBuiltIn(converters, Float.class, Float::parseFloat); + addBuiltIn(converters, Float.TYPE, Float::parseFloat); + addBuiltIn(converters, Double.class, Double::parseDouble); + addBuiltIn(converters, Double.TYPE, Double::parseDouble); + addBuiltIn(converters, Character.class, MpConfigBuilder::toChar); + addBuiltIn(converters, Character.TYPE, MpConfigBuilder::toChar); + addBuiltIn(converters, Class.class, MpConfigBuilder::toClass); + + // built-in converters - Helidon + //javax.math + addBuiltIn(converters, BigDecimal.class, ConfigMappers::toBigDecimal); + addBuiltIn(converters, BigInteger.class, ConfigMappers::toBigInteger); + //java.time + addBuiltIn(converters, Duration.class, ConfigMappers::toDuration); + addBuiltIn(converters, Period.class, ConfigMappers::toPeriod); + addBuiltIn(converters, LocalDate.class, ConfigMappers::toLocalDate); + addBuiltIn(converters, LocalDateTime.class, ConfigMappers::toLocalDateTime); + addBuiltIn(converters, LocalTime.class, ConfigMappers::toLocalTime); + addBuiltIn(converters, ZonedDateTime.class, ConfigMappers::toZonedDateTime); + addBuiltIn(converters, ZoneId.class, ConfigMappers::toZoneId); + addBuiltIn(converters, ZoneOffset.class, ConfigMappers::toZoneOffset); + addBuiltIn(converters, Instant.class, ConfigMappers::toInstant); + addBuiltIn(converters, OffsetTime.class, ConfigMappers::toOffsetTime); + addBuiltIn(converters, OffsetDateTime.class, ConfigMappers::toOffsetDateTime); + addBuiltIn(converters, YearMonth.class, YearMonth::parse); + //java.io + addBuiltIn(converters, File.class, MpConfigBuilder::toFile); + //java.nio + addBuiltIn(converters, Path.class, MpConfigBuilder::toPath); + addBuiltIn(converters, Charset.class, ConfigMappers::toCharset); + //java.net + addBuiltIn(converters, URI.class, ConfigMappers::toUri); + addBuiltIn(converters, URL.class, ConfigMappers::toUrl); + //java.util + addBuiltIn(converters, Pattern.class, ConfigMappers::toPattern); + addBuiltIn(converters, UUID.class, ConfigMappers::toUUID); + + // obsolete stuff + // noinspection UseOfObsoleteDateTimeApi + addBuiltIn(converters, Date.class, ConfigMappers::toDate); + // noinspection UseOfObsoleteDateTimeApi + addBuiltIn(converters, Calendar.class, ConfigMappers::toCalendar); + // noinspection UseOfObsoleteDateTimeApi + addBuiltIn(converters, GregorianCalendar.class, ConfigMappers::toGregorianCalendar); + // noinspection UseOfObsoleteDateTimeApi + addBuiltIn(converters, TimeZone.class, ConfigMappers::toTimeZone); + // noinspection UseOfObsoleteDateTimeApi + addBuiltIn(converters, SimpleTimeZone.class, ConfigMappers::toSimpleTimeZone); + + if (useDiscoveredConverters) { + ServiceLoader.load(Converter.class) + .forEach(it -> converters.add(new OrdinalConverter(it))); + } + + if (useDiscoveredSources) { + ServiceLoader.load(ConfigSource.class) + .forEach(it -> sources.add(new OrdinalSource(it))); + + ServiceLoader.load(ConfigSourceProvider.class) + .forEach(it -> { + it.getConfigSources(classLoader) + .forEach(source -> sources.add(new OrdinalSource(source))); + }); + } + + // now it is from lowest to highest + sources.sort(Comparator.comparingInt(o -> o.ordinal)); + converters.sort(Comparator.comparingInt(o -> o.ordinal)); + + // revert to have the first one the most significant + Collections.reverse(sources); + Collections.reverse(converters); + + List sources = new LinkedList<>(); + HashMap, Converter> converters = new HashMap<>(); + + this.sources.forEach(ordinal -> sources.add(ordinal.source)); + this.converters.forEach(ordinal -> converters.putIfAbsent(ordinal.type, ordinal.converter)); + + List filters = HelidonServiceLoader.create(ServiceLoader.load(MpConfigFilter.class)) + .asList(); + + return new MpConfigImpl(sources, converters, filters); + } + + private void addBuiltIn(List converters, Class clazz, Converter converter) { + converters.add(new OrdinalConverter(converter, clazz, 1)); + } + + ConfigBuilder metaConfig(io.helidon.config.Config metaConfig) { + io.helidon.config.Config helidonConfig = io.helidon.config.Config.builder() + .config(metaConfig) + .build(); + this.sources.add(new OrdinalSource(MpConfigSources.create(helidonConfig))); + return this; + } + + private static File toFile(String value) { + return new File(value); + } + + private static Path toPath(String value) { + return Paths.get(value); + } + + private static Class toClass(String stringValue) { + try { + return Class.forName(stringValue); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Failed to convert property " + stringValue + " to class", e); + } + } + + private static char toChar(String stringValue) { + if (stringValue.length() != 1) { + throw new IllegalArgumentException("The string to map must be a single character, but is: " + stringValue); + } + return stringValue.charAt(0); + } + + private static class OrdinalSource { + private final int ordinal; + private final ConfigSource source; + + private OrdinalSource(ConfigSource source) { + this.source = source; + this.ordinal = findOrdinal(source); + } + + private OrdinalSource(ConfigSource source, int ordinal) { + this.ordinal = ordinal; + this.source = source; + } + + private static int findOrdinal(ConfigSource source) { + int ordinal = source.getOrdinal(); + if (ordinal == ConfigSource.DEFAULT_ORDINAL) { + return Priorities.find(source, ConfigSource.DEFAULT_ORDINAL); + } + return ordinal; + } + + @Override + public String toString() { + return ordinal + " " + source.getName(); + } + } + + private static class OrdinalConverter { + private final int ordinal; + private final Class type; + private final Converter converter; + + private OrdinalConverter(Converter converter, Class aClass, int ordinal) { + this.ordinal = ordinal; + this.type = aClass; + this.converter = converter; + } + + private OrdinalConverter(Converter converter) { + this(converter, getConverterType(converter.getClass()), Priorities.find(converter, 100)); + } + } + + private static Class getConverterType(Class converterClass) { + Class type = doGetType(converterClass); + if (null == type) { + throw new IllegalArgumentException("Converter " + converterClass + " must be a ParameterizedType."); + } + return type; + } + + private static Class doGetType(Class clazz) { + if (clazz.equals(Object.class)) { + return null; + } + + Type[] genericInterfaces = clazz.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) genericInterface; + if (pt.getRawType().equals(Converter.class)) { + Type[] typeArguments = pt.getActualTypeArguments(); + if (typeArguments.length != 1) { + throw new IllegalStateException("Converter " + clazz + " must be a ParameterizedType."); + } + Type typeArgument = typeArguments[0]; + if (typeArgument instanceof Class) { + return (Class) typeArgument; + } + throw new IllegalStateException("Converter " + clazz + " must convert to a class, not " + typeArgument); + } + } + } + + return doGetType(clazz.getSuperclass()); + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java new file mode 100644 index 00000000000..dc31b77c9a0 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.config.mp.spi.MpConfigFilter; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.Converter; + +public class MpConfigImpl implements Config { + private static final Logger LOGGER = Logger.getLogger(MpConfigImpl.class.getName()); + // for references resolving + // matches string between ${ } with a negative lookbehind if there is not backslash + private static final String REGEX_REFERENCE = "(?> UNRESOLVED_KEYS = ThreadLocal.withInitial(HashSet::new); + + private static final Pattern SPLIT_PATTERN = Pattern.compile("(? sources = new LinkedList<>(); + private final HashMap, Converter> converters = new LinkedHashMap<>(); + private final boolean valueResolving; + private final List filters = new ArrayList<>(); + + MpConfigImpl(List sources, + HashMap, Converter> converters, + List filters) { + this.sources.addAll(sources); + this.converters.putAll(converters); + this.converters.putIfAbsent(String.class, value -> value); + + this.valueResolving = getOptionalValue("helidon.config.value-resolving.enabled", Boolean.class) + .orElse(true); + + // we need to initialize the filters first, before we set up filters + filters.forEach(it -> { + // initialize filter with filters with higher priority already in place + it.init(this); + // and then add it to the list of active filters + // do not do this first, as we would end up in using an uninitialized filter + this.filters.add(it); + }); + } + + @Override + public T getValue(String propertyName, Class propertyType) { + return getOptionalValue(propertyName, propertyType) + .orElseThrow(() -> new NoSuchElementException("Property \"" + propertyName + "\" is not available in " + + "configuration")); + } + + @SuppressWarnings("unchecked") + @Override + public Optional getOptionalValue(String propertyName, Class propertyType) { + // let's resolve arrays + if (propertyType.isArray()) { + Class componentType = propertyType.getComponentType(); + // first try to see if we have a direct value + Optional optionalValue = getOptionalValue(propertyName, String.class); + if (optionalValue.isPresent()) { + return Optional.of((T) toArray(propertyName, optionalValue.get(), componentType)); + } + + /* + we also support indexed value + e.g. for key "my.list" you can have both: + my.list=12,13,14 + or (not and): + my.list.0=12 + my.list.1=13 + */ + + String indexedConfigKey = propertyName + ".0"; + optionalValue = getOptionalValue(indexedConfigKey, String.class); + if (optionalValue.isPresent()) { + List result = new LinkedList<>(); + + // first element is already in + result.add(convert(indexedConfigKey, componentType, optionalValue.get())); + + // hardcoded limit to lists of 1000 elements + for (int i = 1; i < 1000; i++) { + indexedConfigKey = propertyName + "." + i; + optionalValue = getOptionalValue(indexedConfigKey, String.class); + if (optionalValue.isPresent()) { + result.add(convert(indexedConfigKey, componentType, optionalValue.get())); + } else { + // finish the iteration on first missing index + break; + } + } + Object array = Array.newInstance(componentType, result.size()); + for (int i = 0; i < result.size(); i++) { + Object component = result.get(i); + Array.set(array, i, component); + } + return Optional.of((T) array); + } else { + return Optional.empty(); + } + } else { + return getStringValue(propertyName) + .flatMap(it -> applyFilters(propertyName, it)) + .map(it -> convert(propertyName, propertyType, it)); + } + } + + @Override + public Iterable getPropertyNames() { + Set names = new LinkedHashSet<>(); + for (ConfigSource source : sources) { + names.addAll(source.getPropertyNames()); + } + return names; + } + + @Override + public Iterable getConfigSources() { + return Collections.unmodifiableList(sources); + } + + /** + * Return the {@link Converter} used by this instance to produce instances of the specified type from string values. + * + * This method is from a future version of MP Config specification and may changed before it + * is released. It is nevertheless needed to process annotations with default values. + * + * @param the conversion type + * @param forType the type to be produced by the converter + * @return an {@link java.util.Optional} containing the converter, or empty if no converter is available for the specified type + */ + @SuppressWarnings("unchecked") + public Optional> getConverter(Class forType) { + return converters.entrySet() + .stream() + .filter(it -> forType.isAssignableFrom(it.getKey())) + .findFirst() + .map(Map.Entry::getValue) + .map(it -> (Converter) it) + .or(() -> findImplicit(forType)); + } + + /** + * Convert a String to a specific type. + * This is a helper method to allow for processing of default values that cannot be typed (e.g. in annotations). + * + * @param propertyName name of the property, used for error messages + * @param type type of the property + * @param value String value (may be null) + * @param type + * @return instance of the correct type, may return null in case null was provided and converter did not do this + * @throws IllegalArgumentException in case the String provided cannot be converted to the type expected + */ + private T convert(String propertyName, Class type, String value) { + try { + return findConverter(type) + .convert(value); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to convert property \"" + + propertyName + + "\" from its value \"" + + value + + "\" to " + + type.getName(), + e); + } + } + + private Optional getStringValue(String propertyName) { + for (ConfigSource source : sources) { + String value = source.getValue(propertyName); + + if (null == value) { + // not in this one + continue; + } + + LOGGER.finest("Found property " + propertyName + " in source " + source.getName()); + return Optional.of(resolveReferences(propertyName, value)); + } + + return Optional.empty(); + } + + private Optional applyFilters(String propertyName, String stringValue) { + String result = stringValue; + + for (MpConfigFilter filter : filters) { + result = filter.apply(propertyName, result); + } + + return Optional.ofNullable(result); + } + + private Object toArray(String propertyName, String stringValue, Class componentType) { + String[] values = toArray(stringValue); + + Object array = Array.newInstance(componentType, values.length); + + for (int i = 0; i < values.length; i++) { + String value = values[i]; + Array.set(array, i, convert(propertyName, componentType, value)); + } + + return array; + } + + private String resolveReferences(String key, String value) { + if (!valueResolving) { + return value; + } + if (!UNRESOLVED_KEYS.get().add(key)) { + UNRESOLVED_KEYS.get().clear(); + throw new IllegalStateException("Recursive resolving of references for key " + key + ", value: " + value); + } + try { + return format(value); + } catch (NoSuchElementException e) { + LOGGER.log(Level.FINER, e, () -> String.format("Reference for key %s not found. Value: %s", key, value)); + return value; + } finally { + UNRESOLVED_KEYS.get().remove(key); + } + } + + private String format(String value) { + Matcher m = PATTERN_REFERENCE.matcher(value); + final StringBuffer sb = new StringBuffer(); + while (m.find()) { + String propertyName = m.group(1); + m.appendReplacement(sb, + Matcher.quoteReplacement(getOptionalValue(propertyName, String.class) + .orElseGet(() -> "${" + propertyName + "}"))); + } + m.appendTail(sb); + // remove all backslash that encodes ${...} + m = PATTERN_BACKSLASH.matcher(sb.toString()); + + return m.replaceAll(""); + } + + @SuppressWarnings("unchecked") + Converter findConverter(Class type) { + Converter converter = converters.get(type); + if (null != converter) { + return (Converter) converter; + } + + return getConverter(type) + .orElseGet(() -> new FailingConverter<>(type)); + } + + @SuppressWarnings("unchecked") + private Optional> findImplicit(Class type) { + // enums must be explicitly supported + if (Enum.class.isAssignableFrom(type)) { + return Optional.of(value -> { + Class enumClass = (Class) type; + return (T) Enum.valueOf(enumClass, value); + }); + } + // any class that has a "public static T method()" + Optional method = findMethod(type, "of", String.class) + .or(() -> findMethod(type, "valueOf", String.class)) + .or(() -> findMethod(type, "parse", CharSequence.class)) + .or(() -> findMethod(type, "parse", String.class)); + + if (method.isPresent()) { + Method m = method.get(); + return Optional.of(value -> { + try { + return (T) m.invoke(null, value); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to convert to " + type.getName() + " using a static method", e); + } + }); + } + + // constructor with a single string parameter + try { + Constructor constructor = type.getConstructor(String.class); + if (constructor.canAccess(null)) { + return Optional.of(value -> { + try { + return constructor.newInstance(value); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to convert to " + type.getName() + " using a constructor", e); + } + }); + } else { + LOGGER.finest("Constructor with String parameter is not accessible on type " + type); + } + } catch (NoSuchMethodException e) { + LOGGER.log(Level.FINEST, "There is no public constructor with string parameter on class " + type.getName(), e); + } + + return Optional.empty(); + } + + private Optional findMethod(Class type, String name, Class... parameterTypes) { + try { + Method result = type.getDeclaredMethod(name, parameterTypes); + if (!result.canAccess(null)) { + LOGGER.finest(() -> "Method " + name + "(" + Arrays + .toString(parameterTypes) + ") is not accessible on class " + type.getName()); + return Optional.empty(); + } + if (!Modifier.isStatic(result.getModifiers())) { + LOGGER.finest(() -> "Method " + name + "(" + Arrays + .toString(parameterTypes) + ") is not static on class " + type.getName()); + return Optional.empty(); + } + + return Optional.of(result); + } catch (NoSuchMethodException e) { + LOGGER.log(Level.FINEST, + "Method " + name + "(" + Arrays.toString(parameterTypes) + ") is not avilable on class " + type.getName(), + e); + return Optional.empty(); + } + } + + HashMap, Converter> converters() { + return converters; + } + + static String[] toArray(String stringValue) { + String[] values = SPLIT_PATTERN.split(stringValue, -1); + + for (int i = 0; i < values.length; i++) { + String value = values[i]; + values[i] = ESCAPED_COMMA_PATTERN.matcher(value).replaceAll(Matcher.quoteReplacement(",")); + } + return values; + } + + private static class FailingConverter implements Converter { + private final Class type; + + private FailingConverter(Class type) { + this.type = type; + } + + @Override + public T convert(String value) { + throw new IllegalArgumentException("Cannot convert \"" + value + "\" to type " + type.getName()); + } + } +} diff --git a/config/config/src/main/java/io/helidon/config/MpConfigProviderResolver.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java similarity index 85% rename from config/config/src/main/java/io/helidon/config/MpConfigProviderResolver.java rename to config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java index fa1865fc8e0..3eb4245eb92 100644 --- a/config/config/src/main/java/io/helidon/config/MpConfigProviderResolver.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.config; +package io.helidon.config.mp; import java.time.Instant; import java.util.IdentityHashMap; @@ -31,6 +31,8 @@ import java.util.stream.Stream; import io.helidon.common.GenericType; +import io.helidon.config.ConfigValue; +import io.helidon.config.MetaConfig; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -115,7 +117,7 @@ public void registerConfig(Config config, ClassLoader classLoader) { * * @param config configuration to use */ - public static void runtimeStart(io.helidon.config.Config config) { + public static void runtimeStart(Config config) { if (BUILD_CONFIG.isEmpty()) { return; } @@ -144,13 +146,11 @@ private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) config = ((ConfigDelegate) config).delegate(); } - io.helidon.config.Config helidonConfig = (io.helidon.config.Config) config; - if (null != currentConfig) { - currentConfig.set(helidonConfig); + currentConfig.set(config); } - ConfigDelegate newConfig = new ConfigDelegate(helidonConfig); + ConfigDelegate newConfig = new ConfigDelegate(config); CONFIGS.put(classLoader, newConfig); return newConfig; @@ -192,18 +192,24 @@ public void releaseConfig(Config config) { * that hold a reference to configuration obtained at build time. */ public static final class ConfigDelegate implements io.helidon.config.Config, Config { - private AtomicReference delegate; + private final AtomicReference delegate = new AtomicReference<>(); + private final AtomicReference helidonDelegate = new AtomicReference<>(); - private ConfigDelegate(io.helidon.config.Config delegate) { - this.delegate = new AtomicReference<>(delegate); + private ConfigDelegate(Config delegate) { + set(delegate); } - private void set(io.helidon.config.Config newDelegate) { - this.delegate.set(newDelegate); + void set(Config delegate) { + this.delegate.set(delegate); + if (delegate instanceof io.helidon.config.Config) { + this.helidonDelegate.set((io.helidon.config.Config) delegate); + } else { + this.helidonDelegate.set(MpConfig.toHelidonConfig(delegate)); + } } private io.helidon.config.Config getCurrent() { - return delegate.get().context().last(); + return helidonDelegate.get().context().last(); } @Override @@ -242,7 +248,7 @@ public Stream traverse(Predicate T convert(Class type, String value) throws ConfigMappingException { + public T convert(Class type, String value) { return getCurrent().convert(type, value); } @@ -262,43 +268,43 @@ public ConfigValue as(Function mapper) { } @Override - public ConfigValue> asList(Class type) throws ConfigMappingException { + public ConfigValue> asList(Class type) { return getCurrent().asList(type); } @Override - public ConfigValue> asList(Function mapper) throws ConfigMappingException { + public ConfigValue> asList(Function mapper) { return getCurrent().asList(mapper); } @Override - public ConfigValue> asNodeList() throws ConfigMappingException { + public ConfigValue> asNodeList() { return getCurrent().asNodeList(); } @Override - public ConfigValue> asMap() throws MissingValueException { + public ConfigValue> asMap() { return getCurrent().asMap(); } @Override public T getValue(String propertyName, Class propertyType) { - return ((Config) getCurrent()).getValue(propertyName, propertyType); + return delegate.get().getValue(propertyName, propertyType); } @Override public Optional getOptionalValue(String propertyName, Class propertyType) { - return ((Config) getCurrent()).getOptionalValue(propertyName, propertyType); + return delegate.get().getOptionalValue(propertyName, propertyType); } @Override public Iterable getPropertyNames() { - return ((Config) getCurrent()).getPropertyNames(); + return delegate.get().getPropertyNames(); } @Override public Iterable getConfigSources() { - return ((Config) getCurrent()).getConfigSources(); + return delegate.get().getConfigSources(); } /** @@ -307,7 +313,7 @@ public Iterable getConfigSources() { * @return the instance backing this config delegate */ public Config delegate() { - return (Config) getCurrent(); + return delegate.get(); } } } diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigSources.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigSources.java new file mode 100644 index 00000000000..c0c6a3b2fb6 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigSources.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import io.helidon.config.Config; +import io.helidon.config.ConfigException; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Utilities for MicroProfile Config {@link org.eclipse.microprofile.config.spi.ConfigSource}. + *

+ * The following methods create MicroProfile config sources to help with manual setup of Config + * from {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}: + *

    + *
  • {@link #systemProperties()} - system properties config source
  • + *
  • {@link #environmentVariables()} - environment variables config source
  • + *
  • {@link #create(java.nio.file.Path)} - load a properties file from file system
  • + *
  • {@link #create(String, java.nio.file.Path)} - load a properties file from file system with custom name
  • + *
  • {@link #create(java.util.Map)} - create an in-memory source from map
  • + *
  • {@link #create(String, java.util.Map)} - create an in-memory source from map with custom name
  • + *
  • {@link #create(java.util.Properties)} - create an in-memory source from properties
  • + *
  • {@link #create(String, java.util.Properties)} - create an in-memory source from properties with custom name
  • + *
+ * The following methods add integration with Helidon SE Config: + *
    + *
  • {@link #create(io.helidon.config.spi.ConfigSource)} - create a MicroProfile config source from Helidon SE config + * source
  • + *
  • {@link #create(io.helidon.config.Config)} - create a MicroProfile config source from Helidon SE Config instance
  • + *
+ */ +public final class MpConfigSources { + private MpConfigSources() { + } + + /** + * In memory config source based on the provided map. + * The config source queries the map each time {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} + * is called. + * + * @param name name of the source + * @param theMap map serving as configuration data + * @return a new config source + */ + public static ConfigSource create(String name, Map theMap) { + return new MpMapSource(name, theMap); + } + + /** + * In memory config source based on the provided map. + * The config source queries the map each time {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} + * is called. + * + * @param theMap map serving as configuration data + * @return a new config source + */ + public static ConfigSource create(Map theMap) { + return create("Map", theMap); + } + + /** + * {@link java.util.Properties} config source based on a file on file system. + * The file is read just once, when the source is created and further changes to the underlying file are + * ignored. + * + * @param path path of the properties file on the file system + * @return a new config source + */ + public static ConfigSource create(Path path) { + return create(path.toString(), path); + } + + /** + * {@link java.util.Properties} config source based on a URL. + * The URL is read just once, when the source is created and further changes to the underlying resource are + * ignored. + * + * @param url url of the properties file (any URL scheme supported by JVM can be used) + * @return a new config source + */ + public static ConfigSource create(URL url) { + String name = url.toString(); + + try { + URLConnection urlConnection = url.openConnection(); + try (InputStream inputStream = urlConnection.getInputStream()) { + Properties properties = new Properties(); + properties.load(inputStream); + + return create(name, properties); + } + } catch (Exception e) { + throw new ConfigException("Failed to load ", e); + } + } + + /** + * {@link java.util.Properties} config source based on a file on file system. + * The file is read just once, when the source is created and further changes to the underlying file are + * ignored. + * + * @param name name of the config source + * @param path path of the properties file on the file system + * @return a new config source + */ + public static ConfigSource create(String name, Path path) { + Properties props = new Properties(); + + try (InputStream in = Files.newInputStream(path)) { + props.load(in); + } catch (IOException e) { + throw new ConfigException("Failed to read properties from " + path.toAbsolutePath()); + } + + return create(name, props); + } + + /** + * In memory config source based on the provided properties. + * The config source queries the properties each time + * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} + * is called. + * + * @param properties serving as configuration data + * @return a new config source + */ + public static ConfigSource create(Properties properties) { + return create("Properties", properties); + } + + /** + * In memory config source based on the provided properties. + * The config source queries the properties each time + * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} + * is called. + * + * @param name name of the config source + * @param properties serving as configuration data + * @return a new config source + */ + public static ConfigSource create(String name, Properties properties) { + Map result = new HashMap<>(); + for (String key : properties.stringPropertyNames()) { + result.put(key, properties.getProperty(key)); + } + return new MpMapSource(name, result); + } + + /** + * Environment variables config source. + * This source takes care of replacement of properties by environment variables as defined + * in MicroProfile Config specification. + * This config source is immutable and caching. + * + * @return a new config source + */ + public static ConfigSource environmentVariables() { + return new MpEnvironmentVariablesSource(); + } + + /** + * In memory config source based on system properties. + * The config source queries the properties each time + * {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} + * is called. + * + * @return a new config source + */ + public static ConfigSource systemProperties() { + return new MpSystemPropertiesSource(); + } + + /** + * Find all resources on classpath and return a config source for each. + * Order is kept as provided by class loader. + * + * @param resource resource to find + * @return a config source for each resource on classpath, empty if none found + */ + public static List classPath(String resource) { + return classPath(Thread.currentThread().getContextClassLoader(), resource); + } + + /** + * Find all resources on classpath and return a config source for each. + * Order is kept as provided by class loader. + * + * @param classLoader class loader to use to locate the resources + * @param resource resource to find + * @return a config source for each resource on classpath, empty if none found + */ + public static List classPath(ClassLoader classLoader, String resource) { + List sources = new LinkedList<>(); + try { + classLoader.getResources(resource) + .asIterator() + .forEachRemaining(it -> sources.add(create(it))); + } catch (IOException e) { + throw new IllegalStateException("Failed to read \"" + resource + "\" from classpath"); + } + + return sources; + } + + /** + * Config source based on a Helidon SE config source. + * This is to support Helidon SE features in Helidon MP. + * + * The config source will be immutable regardless of configured polling strategy or change watchers. + * + * @param helidonConfigSource config source to use + * @return a new MicroProfile Config config source + */ + public static ConfigSource create(io.helidon.config.spi.ConfigSource helidonConfigSource) { + return MpHelidonSource.create(helidonConfigSource); + } + + /** + * Config source base on a Helidon SE config instance. + * This is to support advanced Helidon SE features in Helidon MP. + * + * The config source will be mutable if the config uses polling strategy and/or change watchers. + * Each time the {@link org.eclipse.microprofile.config.spi.ConfigSource#getValue(String)} is called, + * the latest config version will be queried. + * + * @param config Helidon SE configuration + * @return a new MicroProfile Config config source + */ + public static ConfigSource create(Config config) { + return new MpHelidonConfigSource(config); + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpEnvironmentVariablesSource.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpEnvironmentVariablesSource.java new file mode 100644 index 00000000000..9b6cad6e3f4 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpEnvironmentVariablesSource.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import javax.annotation.Priority; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +@Priority(300) +class MpEnvironmentVariablesSource implements ConfigSource { + private static final Pattern DISALLOWED_CHARS = Pattern.compile("[^a-zA-Z0-9_]"); + private static final String UNDERSCORE = "_"; + + private final Map env; + private final Map cache = new ConcurrentHashMap<>(); + + MpEnvironmentVariablesSource() { + this.env = System.getenv(); + } + + @Override + public Map getProperties() { + return env; + } + + @Override + public String getValue(String propertyName) { + // environment variable config source is immutable - we can safely cache all requested keys, so we + // do not execute the regular expression on every get + return cache.computeIfAbsent(propertyName, theKey -> { + // According to the spec, we have three ways of looking for a property + // 1. Exact match + String result = env.get(propertyName); + if (null != result) { + return new Cached(result); + } + // 2. replace non alphanumeric characters with _ + String rule2 = rule2(propertyName); + result = env.get(rule2); + if (null != result) { + return new Cached(result); + } + // 3. replace same as above, but uppercase + String rule3 = rule2.toUpperCase(); + result = env.get(rule3); + return new Cached(result); + }).value; + } + + @Override + public String getName() { + return "Environment Variables"; + } + + /** + * Rule #2 states: Replace each character that is neither alphanumeric nor _ with _ (i.e. com_ACME_size). + * + * @param propertyName name of property as requested by user + * @return name of environment variable we look for + */ + private static String rule2(String propertyName) { + return DISALLOWED_CHARS.matcher(propertyName).replaceAll(UNDERSCORE); + } + + private static final class Cached { + private final String value; + + private Cached(String value) { + this.value = value; + } + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonConfigSource.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonConfigSource.java new file mode 100644 index 00000000000..d780822f17e --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonConfigSource.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +final class MpHelidonConfigSource implements ConfigSource { + private final io.helidon.config.Config helidonConfig; + + MpHelidonConfigSource(io.helidon.config.Config helidonConfig) { + this.helidonConfig = helidonConfig; + } + + @Override + public Map getProperties() { + return helidonConfig.context() + .last() + .asMap() + .orElseGet(Map::of); + } + + @Override + public String getValue(String s) { + return helidonConfig.context() + .last() + .get(s) + .asString() + .orElse(null); + } + + @Override + public String getName() { + return "Helidon Config"; + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonSource.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonSource.java new file mode 100644 index 00000000000..0f1ea6cb5e4 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpHelidonSource.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +import io.helidon.common.serviceloader.HelidonServiceLoader; +import io.helidon.config.ConfigException; +import io.helidon.config.ConfigHelper; +import io.helidon.config.spi.ConfigContent; +import io.helidon.config.spi.ConfigNode; +import io.helidon.config.spi.ConfigParser; +import io.helidon.config.spi.LazyConfigSource; +import io.helidon.config.spi.NodeConfigSource; +import io.helidon.config.spi.ParsableSource; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +final class MpHelidonSource { + private MpHelidonSource() { + } + + static ConfigSource create(io.helidon.config.spi.ConfigSource source) { + source.init(it -> { + throw new UnsupportedOperationException( + "Source runtimes are not available in MicroProfile Config implementation"); + }); + + if (!source.exists() && !source.optional()) { + throw new ConfigException("Config source " + source + " is mandatory, yet it does not exist."); + } + + if (source instanceof NodeConfigSource) { + Optional load = ((NodeConfigSource) source).load(); + // load the data, create a map from it + return MpConfigSources.create(source.description(), + load.map(ConfigContent.NodeContent::data) + .map(ConfigHelper::flattenNodes) + .orElseGet(Map::of)); + } + + if (source instanceof ParsableSource) { + return HelidonParsableSource.create((ParsableSource) source); + } + + if (source instanceof LazyConfigSource) { + return new HelidonLazySource(source, (LazyConfigSource) source); + } + + throw new IllegalArgumentException( + "Helidon config source must be one of: node source, parsable source, or lazy source. Provided is neither: " + + source.getClass().getName()); + } + + private static class HelidonParsableSource { + public static ConfigSource create(ParsableSource source) { + Optional load = source.load(); + if (load.isEmpty()) { + return MpConfigSources.create(source.description(), Map.of()); + } + ConfigParser.Content content = load.get(); + + String mediaType = content.mediaType() + .or(source::mediaType) + .orElseThrow(() -> new ConfigException("Source " + source + " does not provide media type, cannot use it.")); + + ConfigParser parser = source.parser() + .or(() -> findParser(mediaType)) + .orElseThrow(() -> new ConfigException("Could not locate config parser for media type: \"" + + mediaType + "\"")); + + // create a map from parsed node + return MpConfigSources.create(source.description(), + ConfigHelper.flattenNodes(parser.parse(content))); + + } + + private static Optional findParser(String mediaType) { + return HelidonServiceLoader.create(ServiceLoader.load(ConfigParser.class)) + .asList() + .stream() + .filter(it -> it.supportedMediaTypes().contains(mediaType)) + .findFirst(); + } + } + + private static class HelidonLazySource implements ConfigSource { + private final Map loadedProperties = new ConcurrentHashMap<>(); + private final LazyConfigSource lazy; + private final io.helidon.config.spi.ConfigSource source; + + private HelidonLazySource(io.helidon.config.spi.ConfigSource source, LazyConfigSource lazy) { + this.lazy = lazy; + this.source = source; + } + + @Override + public Map getProperties() { + return Collections.unmodifiableMap(loadedProperties); + } + + @Override + public String getValue(String propertyName) { + String value = lazy.node(propertyName) + .flatMap(ConfigNode::value) + .orElse(null); + + if (null == value) { + loadedProperties.remove(propertyName); + } else { + loadedProperties.put(propertyName, value); + } + + return value; + } + + @Override + public String getName() { + return source.description(); + } + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpMapSource.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpMapSource.java new file mode 100644 index 00000000000..1ad2bad479e --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpMapSource.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.Collections; +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Map based config source. + */ +class MpMapSource implements ConfigSource { + private final Map map; + private final String name; + + MpMapSource(String name, Map map) { + this.name = name; + this.map = map; + } + + @Override + public Map getProperties() { + return Collections.unmodifiableMap(map); + } + + @Override + public String getValue(String propertyName) { + return map.get(propertyName); + } + + @Override + public String getName() { + return name; + } +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpSystemPropertiesSource.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpSystemPropertiesSource.java new file mode 100644 index 00000000000..7ef8ba9f573 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpSystemPropertiesSource.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.Priority; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +@Priority(400) +class MpSystemPropertiesSource implements ConfigSource { + private final Properties props; + + MpSystemPropertiesSource() { + this.props = System.getProperties(); + } + + @Override + public Map getProperties() { + Set strings = props.stringPropertyNames(); + + Map result = new HashMap<>(); + strings.forEach(it -> result.put(it, props.getProperty(it))); + return result; + } + + @Override + public String getValue(String propertyName) { + return props.getProperty(propertyName); + } + + @Override + public String getName() { + return "System Properties"; + } +} diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeBase.java b/config/config-mp/src/main/java/io/helidon/config/mp/package-info.java similarity index 67% rename from config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeBase.java rename to config/config-mp/src/main/java/io/helidon/config/mp/package-info.java index 73972af045b..57ad5b9501e 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeBase.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/package-info.java @@ -14,19 +14,7 @@ * limitations under the License. */ -package io.helidon.config; - -abstract class ConfigSourceRuntimeBase implements ConfigSourceRuntime { - - boolean isSystemProperties() { - return false; - } - - boolean isEnvironmentVariables() { - return false; - } - - boolean changesSupported() { - return false; - } -} +/** + * Helidon implementation of microprofile config. + */ +package io.helidon.config.mp; diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/spi/MpConfigFilter.java b/config/config-mp/src/main/java/io/helidon/config/mp/spi/MpConfigFilter.java new file mode 100644 index 00000000000..f4a8716e2e6 --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/spi/MpConfigFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp.spi; + +import org.eclipse.microprofile.config.Config; + +/** + * Filtering support for MicroProfile config implementation. + * The current specification does not have a way to intercept values as they are + * delivered. + * As we want to support filtering capabilities (such as for configuration encryption), + * this is a temporary solution (or permanent if the MP spec does not add any similar feature). + */ +public interface MpConfigFilter { + /** + * Initialize this filter from configuration. + * The config instance provided only has filters with higher priority than this filter. + * + * @param config configuration to set this filter up. + */ + default void init(Config config) { + } + + /** + * Apply this filter on the provided value. + * + * @param propertyName name of the property (its key) + * @param value the current value of the property as retrieved from the config source, or from previous + * filters + * @return value as processed by this filter + */ + String apply(String propertyName, String value); +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/spi/package-info.java b/config/config-mp/src/main/java/io/helidon/config/mp/spi/package-info.java new file mode 100644 index 00000000000..5f85752af2a --- /dev/null +++ b/config/config-mp/src/main/java/io/helidon/config/mp/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 specific extension support for MicroProfile Config. + */ +package io.helidon.config.mp.spi; diff --git a/config/config-mp/src/main/java/module-info.java b/config/config-mp/src/main/java/module-info.java new file mode 100644 index 00000000000..f0c082201b8 --- /dev/null +++ b/config/config-mp/src/main/java/module-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Implementation of the non-CDI parts of Eclipse MicroProfile Config specification. + */ +module io.helidon.config.mp { + requires java.logging; + requires io.helidon.common; + requires io.helidon.config; + requires transitive microprofile.config.api; + requires java.annotation; + requires io.helidon.common.serviceloader; + + exports io.helidon.config.mp; + exports io.helidon.config.mp.spi; + + uses org.eclipse.microprofile.config.spi.ConfigSource; + uses org.eclipse.microprofile.config.spi.ConfigSourceProvider; + uses org.eclipse.microprofile.config.spi.Converter; + uses io.helidon.config.mp.spi.MpConfigFilter; + uses io.helidon.config.spi.ConfigParser; + + provides org.eclipse.microprofile.config.spi.ConfigProviderResolver with io.helidon.config.mp.MpConfigProviderResolver; +} \ No newline at end of file diff --git a/config/config/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver b/config/config-mp/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver similarity index 82% rename from config/config/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver rename to config/config-mp/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver index d1cabddfa38..462e78f791e 100644 --- a/config/config/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver +++ b/config/config-mp/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigProviderResolver @@ -1,5 +1,5 @@ # -# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2020 Oracle and/or 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,4 +14,4 @@ # limitations under the License. # -io.helidon.config.MpConfigProviderResolver +io.helidon.config.mp.MpConfigProviderResolver diff --git a/config/tests/test-mp-reference/src/test/java/io/helidon/config/tests/mpref/MpConfigReferenceTest.java b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigReferenceTest.java similarity index 85% rename from config/tests/test-mp-reference/src/test/java/io/helidon/config/tests/mpref/MpConfigReferenceTest.java rename to config/config-mp/src/test/java/io/helidon/config/mp/MpConfigReferenceTest.java index a2cd90172a5..bcc5b44285f 100644 --- a/config/tests/test-mp-reference/src/test/java/io/helidon/config/tests/mpref/MpConfigReferenceTest.java +++ b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigReferenceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.config.tests.mpref; +package io.helidon.config.mp; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -53,6 +53,17 @@ void testBoth() { test("3", "1", VALUE_1 + "-" + VALUE_2); } + @Test + void testMissingRefs() { + String key = "referencing4-1"; + String actual = config.getValue(key, String.class); + assertThat(actual, is("${missing}")); + + key = "referencing4-2"; + actual = config.getValue(key, String.class); + assertThat(actual, is("${missing}-value")); + } + private void test(String prefix, String value) { test(prefix, "1", value); test(prefix, "2", value + "-ref"); diff --git a/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigSourcesTest.java b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigSourcesTest.java new file mode 100644 index 00000000000..d16d882e84f --- /dev/null +++ b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigSourcesTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.mp; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.config.ConfigException; +import io.helidon.config.ConfigSources; +import io.helidon.config.PropertiesConfigParser; +import io.helidon.config.spi.ConfigContent; +import io.helidon.config.spi.ConfigContext; +import io.helidon.config.spi.ConfigNode; +import io.helidon.config.spi.ConfigParser; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.LazyConfigSource; +import io.helidon.config.spi.NodeConfigSource; +import io.helidon.config.spi.ParsableSource; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MpConfigSourcesTest { + @Test + void testHelidonMap() { + Map values = Map.of( + "key.first", "first", + "key.second", "second" + ); + org.eclipse.microprofile.config.spi.ConfigSource mpSource = MpConfigSources.create(ConfigSources.create(values).build()); + + assertThat(mpSource.getValue("key.first"), is("first")); + assertThat(mpSource.getValue("key.second"), is("second")); + } + @Test + void testHelidonParsable() { + ParsableImpl helidonSource = new ParsableImpl(); + org.eclipse.microprofile.config.spi.ConfigSource mpSource = MpConfigSources.create(helidonSource); + + assertThat(mpSource.getValue(ParsableImpl.KEY + ".notThere"), nullValue()); + assertThat(mpSource.getValue(ParsableImpl.KEY), is(ParsableImpl.VALUE)); + + assertThat(mpSource.getName(), is(ParsableImpl.DESCRIPTION)); + assertThat("init called exactly once", helidonSource.inits.get(), is(1)); + assertThat("exists called exactly once", helidonSource.exists.get(), is(1)); + } + @Test + void testHelidonNode() { + NodeImpl helidonSource = new NodeImpl(); + org.eclipse.microprofile.config.spi.ConfigSource mpSource = MpConfigSources.create(helidonSource); + + assertThat(mpSource.getValue(NodeImpl.KEY + ".notThere"), nullValue()); + assertThat(mpSource.getValue(NodeImpl.KEY), is(NodeImpl.VALUE)); + + assertThat(mpSource.getName(), is(NodeImpl.DESCRIPTION)); + assertThat("init called exactly once", helidonSource.inits.get(), is(1)); + assertThat("exists called exactly once", helidonSource.exists.get(), is(1)); + } + + @Test + void testHelidonLazy() { + LazyImpl lazy = new LazyImpl(); + + org.eclipse.microprofile.config.spi.ConfigSource mpSource = MpConfigSources.create(lazy); + assertThat(mpSource.getValue("key-1"), nullValue()); + + lazy.put("key-1", "value-1"); + assertThat(mpSource.getValue("key-1"), is("value-1")); + + lazy.remove("key-1"); + assertThat(mpSource.getValue("key-1"), nullValue()); + + assertThat(mpSource.getName(), is(LazyImpl.DESCRIPTION)); + assertThat("init called exactly once", lazy.inits.get(), is(1)); + assertThat("exists called exactly once", lazy.exists.get(), is(1)); + } + + private static final class NodeImpl implements ConfigSource, NodeConfigSource { + private static final String DESCRIPTION = "node-unit-test"; + private static final String KEY = "key"; + private static final String VALUE = "value"; + + private final AtomicInteger inits = new AtomicInteger(); + private final AtomicInteger exists = new AtomicInteger(); + + @Override + public Optional load() throws ConfigException { + return Optional.of(ConfigContent.NodeContent.builder() + .node(ConfigNode.ObjectNode.builder() + .addValue(KEY, VALUE) + .build()) + .build()); + } + + @Override + public void init(ConfigContext context) { + inits.incrementAndGet(); + } + + @Override + public boolean exists() { + exists.incrementAndGet(); + return true; + } + + @Override + public String description() { + return DESCRIPTION; + } + } + + private static final class LazyImpl implements ConfigSource, LazyConfigSource { + private static final String DESCRIPTION = "lazy-unit-test"; + + private final Map values = new ConcurrentHashMap<>(); + private final AtomicInteger inits = new AtomicInteger(); + private final AtomicInteger exists = new AtomicInteger(); + + @Override + public void init(ConfigContext context) { + inits.incrementAndGet(); + } + + @Override + public boolean exists() { + exists.incrementAndGet(); + return true; + } + + @Override + public String description() { + return DESCRIPTION; + } + + @Override + public Optional node(String key) { + return Optional.ofNullable(values.get(key)) + .map(ConfigNode.ValueNode::create); + } + + private void put(String key, String value) { + values.put(key, value); + } + + private void remove(String key) { + values.remove(key); + } + } + + private static final class ParsableImpl implements ConfigSource, ParsableSource { + private static final String DESCRIPTION = "parsable-unit-test"; + private static final String KEY = "parsable.key"; + private static final String VALUE = "parsableValue"; + private static final String CONTENT = KEY + "=" + VALUE; + + private final AtomicInteger inits = new AtomicInteger(); + private final AtomicInteger exists = new AtomicInteger(); + + @Override + public Optional load() throws ConfigException { + return Optional.of(content()); + } + + private ConfigParser.Content content() { + return ConfigParser.Content.builder() + .charset(StandardCharsets.UTF_8) + .data(new ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8))) + .mediaType(PropertiesConfigParser.MEDIA_TYPE_TEXT_JAVA_PROPERTIES) + .build(); + } + + @Override + public Optional parser() { + return Optional.empty(); + } + + @Override + public Optional mediaType() { + return Optional.empty(); + } + + @Override + public void init(ConfigContext context) { + inits.incrementAndGet(); + } + + @Override + public boolean exists() { + exists.incrementAndGet(); + return true; + } + + @Override + public String description() { + return DESCRIPTION; + } + } +} diff --git a/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigTest.java b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigTest.java new file mode 100644 index 00000000000..00ccb22805c --- /dev/null +++ b/config/config-mp/src/test/java/io/helidon/config/mp/MpConfigTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.config.mp; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test MicroProfile config implementation. + */ +public class MpConfigTest { + private static Config config; + + @BeforeAll + static void initClass() { + config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(Map.of("mp-1", "mp-value-1", + "mp-2", "mp-value-2", + "app.storageEnabled", "false", + "mp-array", "a,b,c", + "mp-list.0", "1", + "mp-list.1", "2", + "mp-list.2", "3")), + MpConfigSources.create(Map.of("app.storageEnabled", "true", + ConfigSource.CONFIG_ORDINAL, "1000"))) + .build(); + } + + @Test + void testConfigSources() { + Iterable configSources = config.getConfigSources(); + List asList = new ArrayList<>(); + for (ConfigSource configSource : configSources) { + asList.add(configSource); + } + + assertThat(asList, hasSize(2)); + + assertThat(asList.get(0), instanceOf(MpMapSource.class)); + assertThat(asList.get(1), instanceOf(MpMapSource.class)); + + // first is the one with higher config ordinal + ConfigSource map = asList.get(0); + assertThat(map.getValue("app.storageEnabled"), is("true")); + + map = asList.get(1); + assertThat(map.getValue("mp-1"), is("mp-value-1")); + assertThat(map.getValue("mp-2"), is("mp-value-2")); + assertThat(map.getValue("app.storageEnabled"), is("false")); + } + + @Test + void testOptionalValue() { + assertThat(config.getOptionalValue("app.storageEnabled", Boolean.class), is(Optional.of(true))); + assertThat(config.getOptionalValue("mp-1", String.class), is(Optional.of("mp-value-1"))); + } + + @Test + void testStringArray() { + String[] values = config.getValue("mp-array", String[].class); + assertThat(values, arrayContaining("a", "b", "c")); + } + + @Test + void testIntArray() { + Integer[] values = config.getValue("mp-list", Integer[].class); + assertThat(values, arrayContaining(1, 2, 3)); + } + + @Test + void mutableTest() { + // THIS MUST WORK - the spec says the sources can be mutable and config must use the latest values + var mutable = new MutableConfigSource(); + + Config config = ConfigProviderResolver.instance().getBuilder() + .withSources(mutable) + .build(); + + String value = config.getValue("key", String.class); + assertThat(value, is("initial")); + + String updated = "updated"; + mutable.set(updated); + value = config.getValue("key", String.class); + assertThat(value, is(updated)); + } + + @Test + void arrayTest() { + MutableConfigSource cs = new MutableConfigSource(); + cs.set("large:cheese\\,mushroom,medium:chicken,small:pepperoni"); + Config config = ConfigProviderResolver.instance().getBuilder() + .withConverter(Pizza.class, 10, value -> { + String[] parts = value.split(":"); + if (parts.length == 2) { + String size = parts[0]; + String flavor = parts[1]; + return new Pizza(flavor, size); + } + + return null; + }) + .withSources(cs) + .build(); + + Pizza[] value = config.getValue("key", + Pizza[].class); + + assertThat(value, notNullValue()); + assertThat(value, arrayWithSize(3)); + assertThat(value, is(new Pizza[] { + new Pizza("cheese,mushroom", "large"), + new Pizza("chicken", "medium"), + new Pizza("pepperoni", "small") + })); + } + + private static class MutableConfigSource implements ConfigSource { + private final AtomicReference value = new AtomicReference<>("initial"); + + @Override + public Map getProperties() { + return Map.of("key", value.get()); + } + + @SuppressWarnings("ReturnOfNull") + @Override + public String getValue(String propertyName) { + if ("key".equals(propertyName)) { + return value.get(); + } + // this is required by the specification (null returns if not found) + return null; + } + + @Override + public String getName() { + return getClass().getName(); + } + + private void set(String value) { + this.value.set(value); + } + } + + public static class Pizza { + private final String flavor; + private final String size; + + private Pizza(String flavour, String size) { + this.flavor = flavour; + this.size = size; + } + + @Override + public String toString() { + return flavor + ":" + size; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pizza pizza = (Pizza) o; + return flavor.equals(pizza.flavor) && + size.equals(pizza.size); + } + + @Override + public int hashCode() { + return Objects.hash(flavor, size); + } + } +} + diff --git a/config/tests/test-mp-reference/src/main/resources/META-INF/microprofile-config.properties b/config/config-mp/src/test/resources/META-INF/microprofile-config.properties similarity index 93% rename from config/tests/test-mp-reference/src/main/resources/META-INF/microprofile-config.properties rename to config/config-mp/src/test/resources/META-INF/microprofile-config.properties index 6bbb95a236a..45ff59e5d8a 100644 --- a/config/tests/test-mp-reference/src/main/resources/META-INF/microprofile-config.properties +++ b/config/config-mp/src/test/resources/META-INF/microprofile-config.properties @@ -27,3 +27,7 @@ referencing2-3=ref-${value2} referencing2-4=ref-${value2}-ref referencing3-1=${value1}-${value2} + +referencing4-1=${missing} +referencing4-2=${missing}-${value1} + diff --git a/config/config/pom.xml b/config/config/pom.xml index a19fbfa3b32..32a79284559 100644 --- a/config/config/pom.xml +++ b/config/config/pom.xml @@ -54,10 +54,6 @@ io.helidon.common helidon-common-media-type - - org.eclipse.microprofile.config - microprofile-config-api - org.junit.jupiter junit-jupiter-api diff --git a/config/config/src/main/java/io/helidon/config/AbstractConfigImpl.java b/config/config/src/main/java/io/helidon/config/AbstractConfigImpl.java index 656ecdd42fa..6e8070fce6e 100644 --- a/config/config/src/main/java/io/helidon/config/AbstractConfigImpl.java +++ b/config/config/src/main/java/io/helidon/config/AbstractConfigImpl.java @@ -17,24 +17,17 @@ package io.helidon.config; import java.time.Instant; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.function.Consumer; import java.util.logging.Logger; -import org.eclipse.microprofile.config.spi.ConfigSource; - /** * Abstract common implementation of {@link Config} extended by appropriate Config node types: * {@link ConfigListImpl}, {@link ConfigMissingImpl}, {@link ConfigObjectImpl}, {@link ConfigLeafImpl}. */ -abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.config.Config { +abstract class AbstractConfigImpl implements Config { public static final Logger LOGGER = Logger.getLogger(AbstractConfigImpl.class.getName()); @@ -45,8 +38,6 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co private final Type type; private final Context context; private final ConfigMapperManager mapperManager; - private final boolean useSystemProperties; - private final boolean useEnvironmentVariables; /** * Initializes Config implementation. @@ -73,26 +64,6 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co this.type = type; context = new NodeContextImpl(); - - boolean sysProps = false; - boolean envVars = false; - int index = 0; - for (ConfigSourceRuntimeBase configSource : factory.configSources()) { - if (index == 0 && configSource.isSystemProperties()) { - sysProps = true; - } - if (configSource.isEnvironmentVariables()) { - envVars = true; - } - - if (sysProps && envVars) { - break; - } - index++; - } - - this.useEnvironmentVariables = envVars; - this.useSystemProperties = sysProps; } /** @@ -164,117 +135,6 @@ public ConfigValue> asNodeList() throws ConfigMappingException { return asList(Config.class); } - /* - * MicroProfile Config methods - */ - @Override - public T getValue(String propertyName, Class propertyType) { - Config config = factory.context().last(); - try { - return mpFindValue(config, propertyName, propertyType); - } catch (MissingValueException e) { - throw new NoSuchElementException(e.getMessage()); - } catch (ConfigMappingException e) { - throw new IllegalArgumentException(e); - } - } - - @Override - public Optional getOptionalValue(String propertyName, Class propertyType) { - try { - return Optional.of(getValue(propertyName, propertyType)); - } catch (NoSuchElementException e) { - return Optional.empty(); - } catch (ConfigMappingException e) { - throw new IllegalArgumentException(e); - } - } - - @Override - public Iterable getPropertyNames() { - Set keys = new HashSet<>(factory.context().last() - .asMap() - .orElseGet(Collections::emptyMap) - .keySet()); - - if (useSystemProperties) { - keys.addAll(System.getProperties().stringPropertyNames()); - } - - return keys; - } - - @Override - public Iterable getConfigSources() { - Config config = factory.context().last(); - if (null == config) { - // maybe we are in progress of initializing this config (e.g. filter processing) - config = this; - } - - if (config instanceof AbstractConfigImpl) { - return ((AbstractConfigImpl) config).mpConfigSources(); - } - return Collections.emptyList(); - } - - private Iterable mpConfigSources() { - return new LinkedList<>(factory.mpConfigSources()); - } - - private T mpFindValue(Config config, String propertyName, Class propertyType) { - // this is a workaround TCK tests that expect system properties to be mutable - // Helidon config does the same, yet with a slight delay (polling reasons) - // we need to check if system properties are enabled and first - if so, do this - - String property = null; - if (useSystemProperties) { - property = System.getProperty(propertyName); - } - - if (null == property) { - ConfigValue value = config - .get(propertyName) - .as(propertyType); - - if (value.isPresent()) { - return value.get(); - } - - // try to find in env vars - if (useEnvironmentVariables) { - T envVar = mpFindEnvVar(config, propertyName, propertyType); - if (null != envVar) { - return envVar; - } - } - - return value.get(); - } else { - return config.get(propertyName).convert(propertyType, property); - } - } - - private T mpFindEnvVar(Config config, String propertyName, Class propertyType) { - String result = System.getenv(propertyName); - - // now let's resolve all variants required by the specification - if (null == result) { - for (String alias : EnvironmentVariableAliases.aliasesOf(propertyName)) { - result = System.getenv(alias); - if (null != result) { - break; - } - } - } - - if (null != result) { - return config.convert(propertyType, result); - } - - return null; - } - private Config contextConfig(Config rootConfig) { return rootConfig .get(AbstractConfigImpl.this.prefix) @@ -312,7 +172,5 @@ public Config last() { public Config reload() { return AbstractConfigImpl.this.contextConfig(AbstractConfigImpl.this.factory.context().reload()); } - } - } diff --git a/config/config/src/main/java/io/helidon/config/AbstractConfigSourceBuilder.java b/config/config/src/main/java/io/helidon/config/AbstractConfigSourceBuilder.java index 8594a93cde3..a2b09fe0502 100644 --- a/config/config/src/main/java/io/helidon/config/AbstractConfigSourceBuilder.java +++ b/config/config/src/main/java/io/helidon/config/AbstractConfigSourceBuilder.java @@ -41,7 +41,7 @@ public abstract class AbstractConfigSourceBuilder> parserMapping; @SuppressWarnings("unchecked") - private B me = (B) this; + private final B me = (B) this; /** * {@inheritDoc} diff --git a/config/config/src/main/java/io/helidon/config/AbstractNodeBuilderImpl.java b/config/config/src/main/java/io/helidon/config/AbstractNodeBuilderImpl.java index 168d5b68294..1e5be476d05 100644 --- a/config/config/src/main/java/io/helidon/config/AbstractNodeBuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/AbstractNodeBuilderImpl.java @@ -34,7 +34,7 @@ public abstract class AbstractNodeBuilderImpl { private final B thisBuilder; - private Function tokenResolver; + private final Function tokenResolver; @SuppressWarnings("unchecked") AbstractNodeBuilderImpl(Function tokenResolver) { @@ -80,13 +80,6 @@ static String formatFrom(String from) { } } - /** - * Human readable description of current builder implementation to be used in logs and exception messages. - * - * @return builder description - */ - protected abstract String typeDescription(); - /** * Returns id computed from key. * diff --git a/config/config/src/main/java/io/helidon/config/AbstractSourceBuilder.java b/config/config/src/main/java/io/helidon/config/AbstractSourceBuilder.java index 097c34e5cbc..9dab0a952d2 100644 --- a/config/config/src/main/java/io/helidon/config/AbstractSourceBuilder.java +++ b/config/config/src/main/java/io/helidon/config/AbstractSourceBuilder.java @@ -41,7 +41,7 @@ public abstract class AbstractSourceBuilder prioritizedSources = new ArrayList<>(); - private final List prioritizedMpSources = new ArrayList<>(); // sources "pre-sorted" - all user defined sources without priority will be ordered // as added, as well as config sources from meta configuration private final List sources = new LinkedList<>(); - private boolean configSourceServicesEnabled; // to use when more than one source is configured private MergingStrategy mergingStrategy = MergingStrategy.fallback(); private boolean hasSystemPropertiesSource; @@ -84,7 +74,6 @@ class BuilderImpl implements Config.Builder { private final List prioritizedMappers = new ArrayList<>(); private final MapperProviders mapperProviders; private boolean mapperServicesEnabled; - private boolean mpMapperServicesEnabled; /* * Config parsers */ @@ -104,7 +93,7 @@ class BuilderImpl implements Config.Builder { * Other configuration. */ private OverrideSource overrideSource; - private ClassLoader classLoader; + /* * Other switches */ @@ -114,15 +103,11 @@ class BuilderImpl implements Config.Builder { private boolean systemPropertiesSourceEnabled; private boolean environmentVariablesSourceEnabled; private boolean envVarAliasGeneratorEnabled; - private boolean mpDiscoveredSourcesAdded; - private boolean mpDiscoveredConvertersAdded; BuilderImpl() { - configSourceServicesEnabled = true; overrideSource = OverrideSources.empty(); mapperProviders = MapperProviders.create(); mapperServicesEnabled = true; - mpMapperServicesEnabled = true; parsers = new ArrayList<>(); parserServicesEnabled = true; filterProviders = new ArrayList<>(); @@ -135,12 +120,6 @@ class BuilderImpl implements Config.Builder { envVarAliasGeneratorEnabled = false; } - @Override - public Config.Builder disableSourceServices() { - this.configSourceServicesEnabled = false; - return this; - } - @Override public Config.Builder sources(List> sourceSuppliers) { // replace current config sources with the ones provided @@ -178,15 +157,14 @@ public Config.Builder disableMapperServices() { return this; } - void disableMpMapperServices() { - this.mpMapperServicesEnabled = false; - } - @Override public Config.Builder addStringMapper(Class type, Function mapper) { Objects.requireNonNull(type); Objects.requireNonNull(mapper); + if (String.class.equals(type)) { + return this; + } addMapper(type, config -> mapper.apply(config.asString().get())); return this; @@ -319,14 +297,6 @@ public AbstractConfigImpl build() { if (null == changesExecutor) { changesExecutor = Executors.newCachedThreadPool(new ConfigThreadFactory("config-changes")); } - if (configSourceServicesEnabled) { - // add MP config sources from service loader (if not already done) - mpAddDiscoveredSources(); - } - if (mpMapperServicesEnabled) { - // add MP discovered converters from service loader (if not already done) - mpAddDiscoveredConverters(); - } /* * Now prepare the config runtime. @@ -390,7 +360,6 @@ public Config.Builder config(Config metaConfig) { metaConfig.get("key-resolving.enabled").asBoolean().ifPresent(this::keyResolvingEnabled); metaConfig.get("parsers.enabled").asBoolean().ifPresent(this::parserServicesEnabled); metaConfig.get("mappers.enabled").asBoolean().ifPresent(this::mapperServicesEnabled); - metaConfig.get("config-source-services.enabled").asBoolean().ifPresent(this::configSourceServicesEnabled); disableSystemPropertiesSource(); disableEnvironmentVariablesSource(); @@ -418,129 +387,6 @@ public Config.Builder mergingStrategy(MergingStrategy strategy) { return this; } - private void configSourceServicesEnabled(boolean enabled) { - this.configSourceServicesEnabled = enabled; - } - - void mpWithConverters(Converter... converters) { - for (Converter converter : converters) { - addMpConverter(converter); - } - } - - void mpWithConverter(Class type, int ordinal, Converter converter) { - // priority 1 is highest, 100 is default - // MP ordinal 1 is lowest, 100 is default - - // 100 - priority 1 - // 101 - priority 0 - int priority = 101 - ordinal; - prioritizedMappers.add(new MpConverterWrapper(type, converter, priority)); - } - - @SuppressWarnings("unchecked") - private void addMpConverter(Converter converter) { - Class type = (Class) getTypeOfMpConverter(converter.getClass()); - if (type == null) { - throw new IllegalStateException("Converter " + converter.getClass() + " must be a ParameterizedType"); - } - - mpWithConverter(type, - Priorities.find(converter.getClass(), 100), - converter); - } - - void mpAddDefaultSources() { - hasEnvVarSource = true; - hasSystemPropertiesSource = true; - - prioritizedSources.add(new HelidonSourceWithPriority(ConfigSources.systemProperties().build(), 100)); - prioritizedSources.add(new HelidonSourceWithPriority(ConfigSources.environmentVariables(), 100)); - prioritizedSources.add(new HelidonSourceWithPriority(ConfigSources.classpath("application.yaml") - .optional(true) - .build(), 100)); - - ConfigSources.classpathAll("META-INF/microprofile-config.properties") - .stream() - .map(io.helidon.common.Builder::build) - .map(source -> new HelidonSourceWithPriority(source, 100)) - .forEach(prioritizedSources::add); - } - - void mpAddDiscoveredSources() { - if (mpDiscoveredSourcesAdded) { - return; - } - this.mpDiscoveredSourcesAdded = true; - this.configSourceServicesEnabled = true; - final ClassLoader usedCl = ((null == classLoader) ? Thread.currentThread().getContextClassLoader() : classLoader); - - List mpSources = new LinkedList<>(); - - // service loader MP sources - HelidonServiceLoader - .create(ServiceLoader.load(org.eclipse.microprofile.config.spi.ConfigSource.class, usedCl)) - .forEach(mpSources::add); - - // config source providers - HelidonServiceLoader.create(ServiceLoader.load(ConfigSourceProvider.class, usedCl)) - .forEach(csp -> csp.getConfigSources(usedCl) - .forEach(mpSources::add)); - - for (org.eclipse.microprofile.config.spi.ConfigSource source : mpSources) { - prioritizedMpSources.add(new PrioritizedMpSource(source)); - } - } - - void mpAddDiscoveredConverters() { - if (mpDiscoveredConvertersAdded) { - return; - } - this.mpDiscoveredConvertersAdded = true; - this.mpMapperServicesEnabled = true; - - final ClassLoader usedCl = ((null == classLoader) ? Thread.currentThread().getContextClassLoader() : classLoader); - - HelidonServiceLoader.create(ServiceLoader.load(Converter.class, usedCl)) - .forEach(this::addMpConverter); - } - - void mpForClassLoader(ClassLoader loader) { - this.classLoader = loader; - } - - void mpWithSources(org.eclipse.microprofile.config.spi.ConfigSource... sources) { - for (org.eclipse.microprofile.config.spi.ConfigSource source : sources) { - if (source instanceof AbstractConfigSource) { - prioritizedSources.add(new HelidonSourceWithPriority((ConfigSource) source, null)); - } else { - prioritizedMpSources.add(new PrioritizedMpSource(source)); - } - } - } - - private Type getTypeOfMpConverter(Class clazz) { - if (clazz.equals(Object.class)) { - return null; - } - - Type[] genericInterfaces = clazz.getGenericInterfaces(); - for (Type genericInterface : genericInterfaces) { - if (genericInterface instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) genericInterface; - if (pt.getRawType().equals(Converter.class)) { - Type[] typeArguments = pt.getActualTypeArguments(); - if (typeArguments.length != 1) { - throw new IllegalStateException("Converter " + clazz + " must be a ParameterizedType."); - } - return typeArguments[0]; - } - } - } - - return getTypeOfMpConverter(clazz.getSuperclass()); - } - private void cachingEnabled(boolean enabled) { this.cachingEnabled = enabled; } @@ -558,7 +404,7 @@ private void keyResolvingEnabled(Boolean aBoolean) { } private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { - List targetSources = new LinkedList<>(); + List targetSources = new LinkedList<>(); if (systemPropertiesSourceEnabled && !hasSystemPropertiesSource) { hasSystemPropertiesSource = true; @@ -574,7 +420,7 @@ private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { envVarAliasGeneratorEnabled = true; } - boolean nothingConfigured = sources.isEmpty() && prioritizedSources.isEmpty() && prioritizedMpSources.isEmpty(); + boolean nothingConfigured = sources.isEmpty() && prioritizedSources.isEmpty(); if (nothingConfigured) { // use meta configuration to load all sources @@ -598,10 +444,10 @@ private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { return new ConfigSourcesRuntime(targetSources, mergingStrategy); } - private List mergePrioritized(ConfigContextImpl context) { - List allPrioritized = new ArrayList<>(this.prioritizedMpSources); + private List mergePrioritized(ConfigContextImpl context) { + List allPrioritized = new ArrayList<>(); prioritizedSources.stream() - .map(it -> new PrioritizedHelidonSource(it, context)) + .map(it -> new PrioritizedConfigSource(it, context)) .forEach(allPrioritized::add); Priorities.sort(allPrioritized); @@ -708,7 +554,7 @@ private void addAutoLoadedFilters() { .build() .asList() .stream() - .map(filter -> (Function) (Config t) -> filter) + .map(LoadedFilterProvider::new) .forEach(this::addFilter); } @@ -716,9 +562,7 @@ private void addAutoLoadedFilters() { * {@link ConfigContext} implementation. */ static class ConfigContextImpl implements ConfigContext { - private final Map runtimes = new IdentityHashMap<>(); - private final Map mpRuntimes - = new IdentityHashMap<>(); + private final Map runtimes = new IdentityHashMap<>(); private final Executor changesExecutor; private final List configParsers; @@ -733,7 +577,7 @@ public ConfigSourceRuntime sourceRuntime(ConfigSource source) { return sourceRuntimeBase(source); } - private ConfigSourceRuntimeBase sourceRuntimeBase(ConfigSource source) { + private ConfigSourceRuntimeImpl sourceRuntimeBase(ConfigSource source) { return runtimes.computeIfAbsent(source, it -> new ConfigSourceRuntimeImpl(this, source)); } @@ -745,10 +589,6 @@ Optional findParser(String mediaType) { .findFirst(); } - ConfigSourceRuntimeBase sourceRuntime(org.eclipse.microprofile.config.spi.ConfigSource source) { - return mpRuntimes.computeIfAbsent(source, it -> new ConfigSourceMpRuntimeImpl(source)); - } - Executor changesExecutor() { return changesExecutor; } @@ -767,7 +607,6 @@ private EmptyConfigHolder() { // config sources .sources(ConfigSources.empty()) .overrides(OverrideSources.empty()) - .disableSourceServices() .disableEnvironmentVariablesSource() .disableSystemPropertiesSource() .disableParserServices() @@ -800,35 +639,6 @@ private interface PrioritizedMapperProvider extends Prioritized, ConfigMapperProvider { } - private static final class MpConverterWrapper implements PrioritizedMapperProvider { - private final Map, Function> converterMap = new HashMap<>(); - private final Converter converter; - private final int priority; - - private MpConverterWrapper(Class theClass, - Converter converter, - int priority) { - this.converter = converter; - this.priority = priority; - this.converterMap.put(theClass, config -> config.asString().as(converter::convert).get()); - } - - @Override - public int priority() { - return priority; - } - - @Override - public Map, Function> mappers() { - return converterMap; - } - - @Override - public String toString() { - return converter.toString(); - } - } - private static final class HelidonMapperWrapper implements PrioritizedMapperProvider { private final ConfigMapperProvider delegate; private final int priority; @@ -869,56 +679,16 @@ public String toString() { } } - private interface PrioritizedConfigSource extends Prioritized { - ConfigSourceRuntimeBase runtime(ConfigContextImpl context); - } - - private static final class PrioritizedMpSource implements PrioritizedConfigSource { - private final org.eclipse.microprofile.config.spi.ConfigSource delegate; - - private PrioritizedMpSource(org.eclipse.microprofile.config.spi.ConfigSource delegate) { - this.delegate = delegate; - - } - - @Override - public ConfigSourceRuntimeBase runtime(ConfigContextImpl context) { - return context.sourceRuntime(delegate); - } - - @Override - public int priority() { - // MP config is using "ordinals" - the higher the number, the more important it is - // We are using "priorities" - the lower the number, the more important it is - String value = delegate.getValue(CONFIG_ORDINAL); - - int priority; - - if (null != value) { - priority = Integer.parseInt(value); - } else { - priority = Priorities.find(delegate, 100); - } - - // priority from Prioritized and annotation (MP has it reversed) - // it is a tough call how to merge priorities and ordinals - // now we use a "101" as a constant, so components with ordinal 100 will have - // priority of 1 - return 101 - priority; - } - } - - private static final class PrioritizedHelidonSource implements PrioritizedConfigSource { + private static final class PrioritizedConfigSource implements Prioritized { private final HelidonSourceWithPriority source; private final ConfigContext context; - private PrioritizedHelidonSource(HelidonSourceWithPriority source, ConfigContext context) { + private PrioritizedConfigSource(HelidonSourceWithPriority source, ConfigContext context) { this.source = source; this.context = context; } - @Override - public ConfigSourceRuntimeBase runtime(ConfigContextImpl context) { + private ConfigSourceRuntimeImpl runtime(ConfigContextImpl context) { return context.sourceRuntimeBase(source.unwrap()); } @@ -949,7 +719,7 @@ int priority(ConfigContext context) { // ordinal from data return context.sourceRuntime(configSource) - .node(CONFIG_ORDINAL) + .node("config_priority") .flatMap(node -> node.value() .map(Integer::parseInt)) .orElseGet(() -> { @@ -959,4 +729,21 @@ int priority(ConfigContext context) { } } + private static class LoadedFilterProvider implements Function { + private final ConfigFilter filter; + + private LoadedFilterProvider(ConfigFilter filter) { + this.filter = filter; + } + + @Override + public ConfigFilter apply(Config config) { + return filter; + } + + @Override + public String toString() { + return filter.toString(); + } + } } diff --git a/config/config/src/main/java/io/helidon/config/Config.java b/config/config/src/main/java/io/helidon/config/Config.java index 2772f0ba692..13100181ccb 100644 --- a/config/config/src/main/java/io/helidon/config/Config.java +++ b/config/config/src/main/java/io/helidon/config/Config.java @@ -40,7 +40,7 @@ *

Configuration

* Immutable tree-structured configuration. *

Loading Configuration

- * Load the default configuration using the {@link #create} method. + * Load the default configuration using the {@link Config#create} method. *
{@code
  * Config config = Config.create();
  * }
Use {@link Config.Builder} to construct a new {@code Config} instance @@ -233,7 +233,7 @@ *

Handling Multiple Configuration * Sources

* A {@code Config} instance, including the default {@code Config} returned by - * {@link #create}, might be associated with multiple {@link ConfigSource}s. The + * {@link Config#create}, might be associated with multiple {@link ConfigSource}s. The * config system merges these together so that values from config sources with higher priority have * precedence over values from config sources with lower priority. */ @@ -809,6 +809,7 @@ default ConfigValue asNode() { // // config changes // + /** * Register a {@link Consumer} that is invoked each time a change occurs on whole Config or on a particular Config node. *

@@ -976,8 +977,8 @@ enum Type { */ MISSING(false, false); - private boolean exists; - private boolean isLeaf; + private final boolean exists; + private final boolean isLeaf; Type(boolean exists, boolean isLeaf) { this.exists = exists; @@ -1114,14 +1115,6 @@ interface Context { * @see ConfigFilter */ interface Builder { - /** - * Disable loading of config sources from Java service loader. - * This disables loading of MicroProfile Config sources. - * - * @return updated builder instance - */ - Builder disableSourceServices(); - /** * Sets ordered list of {@link ConfigSource} instance to be used as single source of configuration * to be wrapped into {@link Config} API. diff --git a/config/config/src/main/java/io/helidon/config/ConfigDiff.java b/config/config/src/main/java/io/helidon/config/ConfigDiff.java index ad986ff1c46..13126dbf629 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigDiff.java +++ b/config/config/src/main/java/io/helidon/config/ConfigDiff.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,6 @@ static ConfigDiff from(Config origConfig, Config newConfig) { .map(Config::key) .distinct() .flatMap(ConfigDiff::expandKey) - .distinct() .collect(toSet()); return new ConfigDiff(newConfig, changedKeys); diff --git a/config/config/src/main/java/io/helidon/config/ConfigFactory.java b/config/config/src/main/java/io/helidon/config/ConfigFactory.java index a4c3f785b55..34b7ff0ebf4 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigFactory.java +++ b/config/config/src/main/java/io/helidon/config/ConfigFactory.java @@ -23,7 +23,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -import java.util.stream.Collectors; import io.helidon.config.spi.ConfigFilter; import io.helidon.config.spi.ConfigNode; @@ -44,8 +43,6 @@ final class ConfigFactory { private final Function> aliasGenerator; private final ConcurrentMap configCache; private final Instant timestamp; - private final List configSources; - private final List mpConfigSources; /** * Create new instance of the factory operating on specified {@link ConfigSource}. @@ -60,8 +57,7 @@ final class ConfigFactory { ObjectNode node, ConfigFilter filter, ProviderImpl provider, - Function> aliasGenerator, - List configSources) { + Function> aliasGenerator) { Objects.requireNonNull(mapperManager, "mapperManager argument is null."); Objects.requireNonNull(node, "node argument is null."); @@ -73,14 +69,9 @@ final class ConfigFactory { this.filter = filter; this.provider = provider; this.aliasGenerator = aliasGenerator; - this.configSources = configSources; configCache = new ConcurrentHashMap<>(); timestamp = Instant.now(); - - this.mpConfigSources = configSources.stream() - .map(ConfigSourceRuntime::asMpSource) - .collect(Collectors.toList()); } Instant timestamp() { @@ -161,14 +152,6 @@ ProviderImpl provider() { return provider; } - List configSources() { - return configSources; - } - - List mpConfigSources() { - return mpConfigSources; - } - /** * Prefix represents detached roots. */ diff --git a/config/config/src/main/java/io/helidon/config/ConfigHelper.java b/config/config/src/main/java/io/helidon/config/ConfigHelper.java index 71ccbf100e7..60b8f1a9a27 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigHelper.java +++ b/config/config/src/main/java/io/helidon/config/ConfigHelper.java @@ -18,10 +18,6 @@ import java.util.AbstractMap; import java.util.Map; -import java.util.concurrent.Flow; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -37,26 +33,18 @@ private ConfigHelper() { } /** - * Creates a {@link ConfigHelper#subscriber(Function) Flow.Subscriber} that - * will delegate {@link Flow.Subscriber#onNext(Object)} to the specified - * {@code onNextFunction} function. - *

- * The new subscriber's - * {@link Flow.Subscriber#onSubscribe(Flow.Subscription)} method - * automatically invokes {@link Flow.Subscription#request(long)} to request - * all events that are available in the subscription. - *

- * The caller-provided {@code onNextFunction} should return {@code false} in - * order to {@link Flow.Subscription#cancel() cancel} current subscription. + * Create a map of keys to string values from an object node. * - * @param onNextFunction function to be invoked during {@code onNext} - * processing - * @param the type of the items provided by the subscription - * @return {@code Subscriber} that delegates its {@code onNext} to the - * caller-provided function + * @param objectNode node to flatten + * @return a map of all nodes */ - public static Flow.Subscriber subscriber(Function onNextFunction) { - return new OnNextFunctionSubscriber<>(onNextFunction); + public static Map flattenNodes(ConfigNode.ObjectNode objectNode) { + return ConfigHelper.flattenNodes(ConfigKeyImpl.of(), objectNode) + .filter(e -> e.getValue() instanceof ValueNodeImpl) + .collect(Collectors.toMap( + e -> e.getKey().toString(), + e -> Config.Key.escapeName(((ValueNodeImpl) e.getValue()).get()) + )); } static Map createFullKeyToNodeMap(ConfigNode.ObjectNode objectNode) { @@ -89,55 +77,4 @@ static Stream> flattenNodes(ConfigKeyImpl k throw new IllegalArgumentException("Invalid node type."); } } - - /** - * Implementation of {@link ConfigHelper#subscriber(Function)}. - * - * @param the subscribed item type - * @see ConfigHelper#subscriber(Function) - */ - private static class OnNextFunctionSubscriber implements Flow.Subscriber { - private final Function onNextFunction; - private final Logger logger; - private Flow.Subscription subscription; - - private OnNextFunctionSubscriber(Function onNextFunction) { - this.onNextFunction = onNextFunction; - this.logger = Logger.getLogger(OnNextFunctionSubscriber.class.getName() + "." - + Integer.toHexString(System.identityHashCode(onNextFunction))); - } - - @Override - public void onSubscribe(Flow.Subscription subscription) { - logger.finest(() -> "onSubscribe: " + subscription); - - this.subscription = subscription; - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(T item) { - boolean cancel = !onNextFunction.apply(item); - - logger.finest(() -> "onNext: " + item + " => " + (cancel ? "CANCEL" : "FOLLOW")); - - if (cancel) { - subscription.cancel(); - } - } - - @Override - public void onError(Throwable throwable) { - logger.log(Level.WARNING, - throwable, - () -> "Config Changes support failed. " + throwable.getLocalizedMessage()); - } - - @Override - public void onComplete() { - logger.config("Config Changes support finished. There will no other Config reload."); - } - - } - } diff --git a/config/config/src/main/java/io/helidon/config/ConfigLeafImpl.java b/config/config/src/main/java/io/helidon/config/ConfigLeafImpl.java index d09f698a894..0853dcec606 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigLeafImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigLeafImpl.java @@ -59,7 +59,7 @@ public ConfigValue> asList(Class type) throws ConfigMappingExcept } Optional value = value(); - if (!value.isPresent()) { + if (value.isEmpty()) { return ConfigValues.create(this, Optional::empty, aConfig -> aConfig.asList(type)); } @@ -89,7 +89,7 @@ public ConfigValue> asList(Class type) throws ConfigMappingExcept @Override public ConfigValue> asList(Function mapper) throws ConfigMappingException { Optional value = value(); - if (!value.isPresent()) { + if (value.isEmpty()) { return ConfigValues.create(this, Optional::empty, aConfig -> aConfig.asList(mapper)); } diff --git a/config/config/src/main/java/io/helidon/config/ConfigMappers.java b/config/config/src/main/java/io/helidon/config/ConfigMappers.java index 204f1a7ab96..52a0c72a134 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigMappers.java +++ b/config/config/src/main/java/io/helidon/config/ConfigMappers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,10 +44,8 @@ import java.time.temporal.TemporalAccessor; import java.util.AbstractMap; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -81,100 +79,96 @@ private ConfigMappers() { } private static Map, Function> initEssentialMappers() { - Map, Function> essentials = new HashMap<>(); - essentials.put(Config.class, (node) -> node); - - essentials.put(String.class, wrap(value -> value)); - - essentials.put(OptionalInt.class, (node) -> { - if (!node.exists()) { - return OptionalInt.empty(); - } - - return OptionalInt.of(wrap(Integer::parseInt).apply(node)); - }); + return Map.of(Config.class, (node) -> node, + String.class, wrap(value -> value), + OptionalInt.class, ConfigMappers::optionalIntEssential, + OptionalLong.class, ConfigMappers::optionalLongEssential, + OptionalDouble.class, ConfigMappers::optionalDoubleEssential); + } - essentials.put(OptionalLong.class, (node) -> { - if (!node.exists()) { - return OptionalLong.empty(); - } + static Map, Function> essentialMappers() { + return ESSENTIAL_MAPPERS; + } - return OptionalLong.of(wrap(Long::parseLong).apply(node)); - }); + private static OptionalDouble optionalDoubleEssential(Config node) { + if (!node.exists()) { + return OptionalDouble.empty(); + } - essentials.put(OptionalDouble.class, (node) -> { - if (!node.exists()) { - return OptionalDouble.empty(); - } + return OptionalDouble.of(wrap(Double::parseDouble).apply(node)); + } - return OptionalDouble.of(wrap(Double::parseDouble).apply(node)); - }); + private static OptionalLong optionalLongEssential(Config node) { + if (!node.exists()) { + return OptionalLong.empty(); + } - return Collections.unmodifiableMap(essentials); + return OptionalLong.of(wrap(Long::parseLong).apply(node)); } - static Map, Function> essentialMappers() { - return ESSENTIAL_MAPPERS; + private static OptionalInt optionalIntEssential(Config node) { + if (!node.exists()) { + return OptionalInt.empty(); + } + + return OptionalInt.of(wrap(Integer::parseInt).apply(node)); } private static Map, Function> initBuiltInMappers() { - Map, Function> builtIns = new HashMap<>(); //primitive types - builtIns.put(Byte.class, wrap(ConfigMappers::toByte)); - builtIns.put(Short.class, wrap(ConfigMappers::toShort)); - builtIns.put(Integer.class, wrap(ConfigMappers::toInt)); - builtIns.put(Long.class, wrap(ConfigMappers::toLong)); - builtIns.put(Float.class, wrap(ConfigMappers::toFloat)); - builtIns.put(Double.class, wrap(ConfigMappers::toDouble)); - builtIns.put(Boolean.class, wrap(ConfigMappers::toBoolean)); - builtIns.put(Character.class, wrap(ConfigMappers::toChar)); - //java.lang - builtIns.put(Class.class, wrap(ConfigMappers::toClass)); - //javax.math - builtIns.put(BigDecimal.class, wrap(ConfigMappers::toBigDecimal)); - builtIns.put(BigInteger.class, wrap(ConfigMappers::toBigInteger)); - //java.time - builtIns.put(Duration.class, wrap(ConfigMappers::toDuration)); - builtIns.put(Period.class, wrap(ConfigMappers::toPeriod)); - builtIns.put(LocalDate.class, wrap(ConfigMappers::toLocalDate)); - builtIns.put(LocalDateTime.class, wrap(ConfigMappers::toLocalDateTime)); - builtIns.put(LocalTime.class, wrap(ConfigMappers::toLocalTime)); - builtIns.put(ZonedDateTime.class, wrap(ConfigMappers::toZonedDateTime)); - builtIns.put(ZoneId.class, wrap(ConfigMappers::toZoneId)); - builtIns.put(ZoneOffset.class, wrap(ConfigMappers::toZoneOffset)); - builtIns.put(Instant.class, wrap(ConfigMappers::toInstant)); - builtIns.put(OffsetTime.class, wrap(ConfigMappers::toOffsetTime)); - builtIns.put(OffsetDateTime.class, wrap(ConfigMappers::toOffsetDateTime)); - builtIns.put(YearMonth.class, wrap(YearMonth::parse)); - //java.io - builtIns.put(File.class, wrap(ConfigMappers::toFile)); - //java.nio - builtIns.put(Path.class, wrap(ConfigMappers::toPath)); - builtIns.put(Charset.class, wrap(ConfigMappers::toCharset)); - //java.net - builtIns.put(URI.class, wrap(ConfigMappers::toUri)); - builtIns.put(URL.class, wrap(ConfigMappers::toUrl)); - //java.util - builtIns.put(Pattern.class, wrap(ConfigMappers::toPattern)); - builtIns.put(UUID.class, wrap(ConfigMappers::toUUID)); - builtIns.put(Map.class, wrapMapper(ConfigMappers::toMap)); - builtIns.put(Properties.class, wrapMapper(ConfigMappers::toProperties)); - - // obsolete stuff - //noinspection UseOfObsoleteDateTimeApi,deprecation - builtIns.put(Date.class, wrap(ConfigMappers::toDate)); - //noinspection UseOfObsoleteDateTimeApi,deprecation - builtIns.put(Calendar.class, wrap(ConfigMappers::toCalendar)); - //noinspection UseOfObsoleteDateTimeApi,deprecation - builtIns.put(GregorianCalendar.class, wrap(ConfigMappers::toGregorianCalendar)); - //noinspection UseOfObsoleteDateTimeApi,deprecation - builtIns.put(TimeZone.class, wrap(ConfigMappers::toTimeZone)); - //noinspection UseOfObsoleteDateTimeApi,deprecation - builtIns.put(SimpleTimeZone.class, wrap(ConfigMappers::toSimpleTimeZone)); - - return Collections.unmodifiableMap(builtIns); + return Map.ofEntries(Map.entry(Byte.class, wrap(ConfigMappers::toByte)), + Map.entry(Short.class, wrap(ConfigMappers::toShort)), + Map.entry(Integer.class, wrap(ConfigMappers::toInt)), + Map.entry(Long.class, wrap(ConfigMappers::toLong)), + Map.entry(Float.class, wrap(ConfigMappers::toFloat)), + Map.entry(Double.class, wrap(ConfigMappers::toDouble)), + Map.entry(Boolean.class, wrap(ConfigMappers::toBoolean)), + Map.entry(Character.class, wrap(ConfigMappers::toChar)), + //java.lang + Map.entry(Class.class, wrap(ConfigMappers::toClass)), + //javax.math + Map.entry(BigDecimal.class, wrap(ConfigMappers::toBigDecimal)), + Map.entry(BigInteger.class, wrap(ConfigMappers::toBigInteger)), + //java.time + Map.entry(Duration.class, wrap(ConfigMappers::toDuration)), + Map.entry(Period.class, wrap(ConfigMappers::toPeriod)), + Map.entry(LocalDate.class, wrap(ConfigMappers::toLocalDate)), + Map.entry(LocalDateTime.class, wrap(ConfigMappers::toLocalDateTime)), + Map.entry(LocalTime.class, wrap(ConfigMappers::toLocalTime)), + Map.entry(ZonedDateTime.class, wrap(ConfigMappers::toZonedDateTime)), + Map.entry(ZoneId.class, wrap(ConfigMappers::toZoneId)), + Map.entry(ZoneOffset.class, wrap(ConfigMappers::toZoneOffset)), + Map.entry(Instant.class, wrap(ConfigMappers::toInstant)), + Map.entry(OffsetTime.class, wrap(ConfigMappers::toOffsetTime)), + Map.entry(OffsetDateTime.class, wrap(ConfigMappers::toOffsetDateTime)), + Map.entry(YearMonth.class, wrap(YearMonth::parse)), + //java.io + Map.entry(File.class, wrap(ConfigMappers::toFile)), + //java.nio + Map.entry(Path.class, wrap(ConfigMappers::toPath)), + Map.entry(Charset.class, wrap(ConfigMappers::toCharset)), + //java.net + Map.entry(URI.class, wrap(ConfigMappers::toUri)), + Map.entry(URL.class, wrap(ConfigMappers::toUrl)), + //java.util + Map.entry(Pattern.class, wrap(ConfigMappers::toPattern)), + Map.entry(UUID.class, wrap(ConfigMappers::toUUID)), + Map.entry(Map.class, wrapMapper(ConfigMappers::toMap)), + Map.entry(Properties.class, wrapMapper(ConfigMappers::toProperties)), + + // obsolete stuff + // noinspection UseOfObsoleteDateTimeApi + Map.entry(Date.class, wrap(ConfigMappers::toDate)), + // noinspection UseOfObsoleteDateTimeApi + Map.entry(Calendar.class, wrap(ConfigMappers::toCalendar)), + // noinspection UseOfObsoleteDateTimeApi + Map.entry(GregorianCalendar.class, wrap(ConfigMappers::toGregorianCalendar)), + // noinspection UseOfObsoleteDateTimeApi + Map.entry(TimeZone.class, wrap(ConfigMappers::toTimeZone)), + // noinspection UseOfObsoleteDateTimeApi + Map.entry(SimpleTimeZone.class, wrap(ConfigMappers::toSimpleTimeZone))); } static Map, Function> builtInMappers() { diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceMpRuntimeImpl.java b/config/config/src/main/java/io/helidon/config/ConfigSourceMpRuntimeImpl.java deleted file mode 100644 index edc1b88a694..00000000000 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceMpRuntimeImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.logging.Level; - -import io.helidon.config.spi.ConfigNode; - -import org.eclipse.microprofile.config.spi.ConfigSource; - -import static io.helidon.config.AbstractConfigImpl.LOGGER; - -class ConfigSourceMpRuntimeImpl extends ConfigSourceRuntimeBase { - private final ConfigSource source; - - ConfigSourceMpRuntimeImpl(ConfigSource source) { - this.source = source; - } - - @Override - public boolean isLazy() { - // MP config sources are considered eager - return false; - } - - @Override - public void onChange(BiConsumer change) { - try { - // this is not a documented feature - // it is to enable MP config sources to be "mutable" in Helidon - // this requires some design decisions (and clarification of the MP Config Specification), as this - // is open to different interpretations for now - Method method = source.getClass().getMethod("registerChangeListener", BiConsumer.class); - BiConsumer mpListener = (key, value) -> change.accept(key, ConfigNode.ValueNode.create(value)); - - method.invoke(source, mpListener); - } catch (NoSuchMethodException e) { - LOGGER.finest("No registerChangeListener(BiConsumer) method found on " + source.getClass() + ", change" - + " support not enabled for this config source (" + source.getName() + ")"); - } catch (IllegalAccessException e) { - LOGGER.log(Level.WARNING, "Cannot invoke registerChangeListener(BiConsumer) method on " + source.getClass() + ", " - + "change support not enabled for this config source (" - + source.getName() + ")", e); - } catch (InvocationTargetException e) { - LOGGER.log(Level.WARNING, "Invocation of registerChangeListener(BiConsumer) method on " + source.getClass() - + " failed with an exception, " - + "change support not enabled for this config source (" - + source.getName() + ")", e); - } - } - - @Override - public Optional load() { - return Optional.of(ConfigUtils.mapToObjectNode(source.getProperties(), false)); - } - - @Override - public Optional node(String key) { - String value = source.getValue(key); - - if (null == value) { - return Optional.empty(); - } - - return Optional.of(ConfigNode.ValueNode.create(value)); - } - - @Override - public ConfigSource asMpSource() { - return source; - } - - @Override - public String description() { - return source.getName(); - } - - @Override - boolean changesSupported() { - // supported through a known method signature - return true; - } -} diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntime.java b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntime.java index 9f4208e3054..d96a69751b6 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntime.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntime.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020 Oracle and/or 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,8 +20,6 @@ import io.helidon.config.spi.ConfigNode; -import org.eclipse.microprofile.config.spi.ConfigSource; - /** * The runtime of a config source. For a single {@link Config}, there is one source runtime for each configured * config source. @@ -54,13 +52,6 @@ public interface ConfigSourceRuntime { */ Optional node(String key); - /** - * Get the underlying config source as a MicroProfile {@link org.eclipse.microprofile.config.spi.ConfigSource}. - * - * @return MP Config source - */ - ConfigSource asMpSource(); - /** * Description of the underlying config source. * @return description of the source diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java index b19fb600318..7f35b230971 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020 Oracle and/or 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,7 +22,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -50,7 +49,7 @@ * config source. * */ -public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements org.eclipse.microprofile.config.spi.ConfigSource { +class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { private static final Logger LOGGER = Logger.getLogger(ConfigSourceRuntimeImpl.class.getName()); private final List> listeners = new LinkedList<>(); @@ -72,7 +71,6 @@ public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements // for eager sources, this is the data we get initially, everything else is handled through change listeners private Optional initialData; private Map loadedData; - private Map mpData; @SuppressWarnings("unchecked") ConfigSourceRuntimeImpl(BuilderImpl.ConfigContextImpl configContext, ConfigSource source) { @@ -182,7 +180,6 @@ public boolean isLazy() { return isLazy; } - @Override boolean changesSupported() { return changesSupported; } @@ -233,16 +230,11 @@ private synchronized void initialLoad() { this.initialData = loadedData; this.loadedData = new HashMap<>(); - mpData = new HashMap<>(); initialData.ifPresent(data -> { Map keyToNodeMap = ConfigHelper.createFullKeyToNodeMap(data); - keyToNodeMap.forEach((key, value) -> { - Optional directValue = value.value(); - directValue.ifPresent(stringValue -> mpData.put(key.toString(), stringValue)); - this.loadedData.put(key.toString(), value); - }); + keyToNodeMap.forEach((key, value) -> this.loadedData.put(key.toString(), value)); }); dataLoaded = true; @@ -254,61 +246,16 @@ public Optional node(String key) { return singleNodeFunction.apply(key); } - @Override - public org.eclipse.microprofile.config.spi.ConfigSource asMpSource() { - return this; - } @Override public String description() { return configSource.description(); } - /* - * MP Config related methods - */ - - @Override - public Map getProperties() { - if (isSystemProperties()) { - // this is a "hack" for MP TCK tests - // System properties act as a mutable source for the purpose of MicroProfile - Map result = new HashMap<>(); - Properties properties = System.getProperties(); - for (String key : properties.stringPropertyNames()) { - result.put(key, properties.getProperty(key)); - } - return result; - } - initialLoad(); - return new HashMap<>(mpData); - } - - @Override - public String getValue(String propertyName) { - initialLoad(); - return mpData.get(propertyName); - } - - @Override - public String getName() { - return configSource.description(); - } - /* Runtime impl base */ - @Override - boolean isSystemProperties() { - return configSource instanceof ConfigSources.SystemPropertiesConfigSource; - } - - @Override - boolean isEnvironmentVariables() { - return configSource instanceof ConfigSources.EnvironmentVariablesConfigSource; - } - private Function> objectNodeToSingleNode() { return key -> { if (null == loadedData) { @@ -340,10 +287,6 @@ private static void triggerChanges(BuilderImpl.ConfigContextImpl configContext, } - ConfigSource unwrap() { - return configSource; - } - private static final class PollingStrategyStarter implements Runnable { private final PollingStrategy pollingStrategy; private final PollingStrategyListener listener; diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourcesRuntime.java b/config/config/src/main/java/io/helidon/config/ConfigSourcesRuntime.java index 103b5dc22f9..383f846ee20 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourcesRuntime.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourcesRuntime.java @@ -37,11 +37,11 @@ final class ConfigSourcesRuntime { private final List loadedData = new LinkedList<>(); - private List allSources; - private MergingStrategy mergingStrategy; - private Consumer> changeListener; + private final List allSources; + private final MergingStrategy mergingStrategy; + private volatile Consumer> changeListener; - ConfigSourcesRuntime(List allSources, + ConfigSourcesRuntime(List allSources, MergingStrategy mergingStrategy) { this.allSources = allSources; this.mergingStrategy = mergingStrategy; @@ -53,10 +53,6 @@ static ConfigSourcesRuntime empty() { MergingStrategy.fallback()); } - List allSources() { - return allSources; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -137,7 +133,7 @@ synchronized Optional latest() { synchronized Optional load() { - for (ConfigSourceRuntimeBase source : allSources) { + for (ConfigSourceRuntimeImpl source : allSources) { if (source.isLazy()) { loadedData.add(new RuntimeWithData(source, Optional.empty())); } else { @@ -197,10 +193,10 @@ private Stream streamKeys(ObjectNode objectNode) { } private static final class RuntimeWithData { - private final ConfigSourceRuntimeBase runtime; + private final ConfigSourceRuntimeImpl runtime; private Optional data; - private RuntimeWithData(ConfigSourceRuntimeBase runtime, Optional data) { + private RuntimeWithData(ConfigSourceRuntimeImpl runtime, Optional data) { this.runtime = runtime; this.data = data; } @@ -209,7 +205,7 @@ private void data(Optional data) { this.data = data; } - private ConfigSourceRuntimeBase runtime() { + private ConfigSourceRuntimeImpl runtime() { return runtime; } diff --git a/config/config/src/main/java/io/helidon/config/ConfigUtils.java b/config/config/src/main/java/io/helidon/config/ConfigUtils.java index a088b1129ab..2ffdcfd583f 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigUtils.java +++ b/config/config/src/main/java/io/helidon/config/ConfigUtils.java @@ -19,24 +19,14 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; -import java.time.Duration; -import java.util.Comparator; -import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.Properties; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import javax.annotation.Priority; import io.helidon.config.spi.ConfigNode; @@ -51,68 +41,6 @@ private ConfigUtils() { throw new AssertionError("Instantiation not allowed."); } - /** - * Convert iterable items to an ordered serial stream. - * - * @param items items to be streamed. - * @param expected streamed item type. - * @return stream of items. - */ - static Stream asStream(Iterable items) { - return asStream(items.iterator()); - } - - /** - * Converts an iterator to a stream. - * - * @param type of the base items - * @param iterator iterator over the items - * @return stream of the items - */ - static Stream asStream(Iterator iterator) { - return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); - } - - /** - * Sorts items represented by an {@link Iterable} instance based on a {@code Priority} annotation attached to each - * item's Java type and return the sorted items as an ordered serial {@link Stream}. - *

- * Instances are sorted by {@code Priority.value()} attached directly to the instance of each item's class. - * If there is no {@code Priority} annotation attached to an item's class the {@code defaultPriority} value is used - * instead. Items with higher priority values have higher priority and take precedence (are returned sooner from - * the stream). - * - * @param items items to be ordered by priority and streamed. - * @param defaultPriority default priority to be used in case an item does not have a priority defined. - * @param item type. - * @return prioritized stream of items. - */ - static Stream asPrioritizedStream(Iterable items, int defaultPriority) { - return asStream(items).sorted(priorityComparator(defaultPriority)); - } - - /** - * Returns a comparator for two objects, the classes for which are - * optionally annotated with {@link Priority} and which applies a specified - * default priority if either or both classes lack the annotation. - * - * @param type of object being compared - * @param defaultPriority used if the classes for either or both objects - * lack the {@code Priority} annotation - * @return comparator - */ - static Comparator priorityComparator(int defaultPriority) { - return (service1, service2) -> { - int service1Priority = Optional.ofNullable(service1.getClass().getAnnotation(Priority.class)) - .map(Priority::value) - .orElse(defaultPriority); - int service2Priority = Optional.ofNullable(service2.getClass().getAnnotation(Priority.class)) - .map(Priority::value) - .orElse(defaultPriority); - return service2Priority - service1Priority; - }; - } - /** * Builds map into object node. *

@@ -187,55 +115,4 @@ static Charset getContentCharset(String contentEncoding) throws ConfigException throw new ConfigException("Unsupported response content-encoding '" + contentEncoding + "'.", ex); } } - - /** - * Allows to {@link #schedule()} execution of specified {@code command} using specified {@link ScheduledExecutorService}. - * Task is not executed immediately but scheduled with specified {@code delay}. - * It is possible to postpone an execution of the command by calling {@link #schedule()} again before the command is finished. - *

- * It can be used to implement Rx Debounce operator (http://reactivex.io/documentation/operators/debounce.html). - */ - static class ScheduledTask { - private final ScheduledExecutorService executorService; - private final Runnable command; - private final Duration delay; - private volatile ScheduledFuture scheduled; - private final Object lock = new Object(); - - /** - * Initialize task. - * - * @param executorService service to be used to schedule {@code command} execution on - * @param command the command to be executed on {@code executorService} - * @param delay the {@code command} is scheduled with specified delay - */ - ScheduledTask(ScheduledExecutorService executorService, Runnable command, Duration delay) { - this.executorService = executorService; - this.command = command; - this.delay = delay; - } - - /** - * Schedule execution of {@code command} on specified {@code executorService} with initial {@code delay}. - *

- * Scheduling can be repeated. Not finished task is canceled. - * - * @return whether a previously-scheduled action was canceled in scheduling this new new action - */ - public boolean schedule() { - boolean result = false; - synchronized (lock) { - if (scheduled != null) { - if (!scheduled.isCancelled() && !scheduled.isDone()) { - scheduled.cancel(false); - LOGGER.log(Level.FINER, String.format("Cancelling and rescheduling %s task.", command)); - result = true; - } - } - scheduled = executorService.schedule(command, delay.toMillis(), TimeUnit.MILLISECONDS); - } - return result; - } - } - } diff --git a/config/config/src/main/java/io/helidon/config/ConfigValue.java b/config/config/src/main/java/io/helidon/config/ConfigValue.java index b2a231025c2..d6bceaf9798 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigValue.java +++ b/config/config/src/main/java/io/helidon/config/ConfigValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -172,7 +172,7 @@ default Optional or(Supplier> supplier) { Objects.requireNonNull(supplier); Optional optional = asOptional(); - if (!optional.isPresent()) { + if (optional.isEmpty()) { Optional supplied = supplier.get(); Objects.requireNonNull(supplied); optional = supplied; @@ -348,6 +348,6 @@ default T orElseThrow(Supplier exceptionSuppl * @return the optional value as a {@code Stream} */ default Stream stream() { - return asOptional().map(Stream::of).orElseGet(Stream::empty); + return asOptional().stream(); } } diff --git a/config/config/src/main/java/io/helidon/config/ConfigValues.java b/config/config/src/main/java/io/helidon/config/ConfigValues.java index d89bf344015..3f83f72d06b 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigValues.java +++ b/config/config/src/main/java/io/helidon/config/ConfigValues.java @@ -39,7 +39,7 @@ private ConfigValues() { * @return a config value that is empty */ public static ConfigValue empty() { - return new ConfigValueBase(Config.Key.create("")) { + return new ConfigValueBase<>(Config.Key.create("")) { @Override public Optional asOptional() { return Optional.empty(); @@ -83,7 +83,7 @@ public String toString() { * @return a config value that uses the value provided */ public static ConfigValue simpleValue(T value) { - return new ConfigValueBase(Config.Key.create("")) { + return new ConfigValueBase<>(Config.Key.create("")) { @Override public Optional asOptional() { return Optional.ofNullable(value); diff --git a/config/config/src/main/java/io/helidon/config/ListNodeBuilderImpl.java b/config/config/src/main/java/io/helidon/config/ListNodeBuilderImpl.java index ee300645cc3..ad690ca0ff1 100644 --- a/config/config/src/main/java/io/helidon/config/ListNodeBuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/ListNodeBuilderImpl.java @@ -104,11 +104,6 @@ ListNodeBuilderImpl value(Optional value) { return this; } - @Override - protected String typeDescription() { - return "a LIST node"; - } - @Override protected Integer id(MergingKey key) { String name = key.first(); diff --git a/config/config/src/main/java/io/helidon/config/MpConfigBuilder.java b/config/config/src/main/java/io/helidon/config/MpConfigBuilder.java deleted file mode 100644 index 369eacd5106..00000000000 --- a/config/config/src/main/java/io/helidon/config/MpConfigBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.eclipse.microprofile.config.spi.Converter; - -/** - * Configuration builder. - */ -public class MpConfigBuilder implements ConfigBuilder { - private final BuilderImpl delegate = new BuilderImpl(); - - MpConfigBuilder() { - delegate.disableSystemPropertiesSource(); - delegate.disableEnvironmentVariablesSource(); - delegate.disableSourceServices(); - delegate.disableMpMapperServices(); - } - - @Override - public ConfigBuilder addDefaultSources() { - delegate.mpAddDefaultSources(); - return this; - } - - @Override - public ConfigBuilder addDiscoveredSources() { - delegate.mpAddDiscoveredSources(); - return this; - } - - @Override - public ConfigBuilder addDiscoveredConverters() { - delegate.mpAddDiscoveredConverters(); - return this; - } - - @Override - public ConfigBuilder forClassLoader(ClassLoader loader) { - delegate.mpForClassLoader(loader); - return this; - } - - @Override - public ConfigBuilder withSources(ConfigSource... sources) { - delegate.mpWithSources(sources); - return this; - } - - @Override - public ConfigBuilder withConverter(Class aClass, int ordinal, Converter converter) { - delegate.mpWithConverter(aClass, ordinal, converter); - return this; - } - - @Override - public ConfigBuilder withConverters(Converter... converters) { - delegate.mpWithConverters(converters); - return this; - } - - @Override - public Config build() { - return delegate.build(); - } - - ConfigBuilder metaConfig(io.helidon.config.Config metaConfig) { - delegate.config(metaConfig); - return this; - } -} diff --git a/config/config/src/main/java/io/helidon/config/ObjectNodeBuilderImpl.java b/config/config/src/main/java/io/helidon/config/ObjectNodeBuilderImpl.java index 11724e7da68..136b894f3d3 100644 --- a/config/config/src/main/java/io/helidon/config/ObjectNodeBuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/ObjectNodeBuilderImpl.java @@ -106,11 +106,6 @@ public ObjectNodeBuilderImpl addNode(String name, ConfigNode node) { return this; } - @Override - protected String typeDescription() { - return "an OBJECT node"; - } - @Override protected String id(MergingKey key) { return key.first(); diff --git a/config/config/src/main/java/io/helidon/config/ProviderImpl.java b/config/config/src/main/java/io/helidon/config/ProviderImpl.java index a26b90f06df..404134193b6 100644 --- a/config/config/src/main/java/io/helidon/config/ProviderImpl.java +++ b/config/config/src/main/java/io/helidon/config/ProviderImpl.java @@ -35,7 +35,6 @@ import java.util.stream.Stream; import io.helidon.config.spi.ConfigFilter; -import io.helidon.config.spi.ConfigNode; import io.helidon.config.spi.ConfigNode.ObjectNode; /** @@ -133,8 +132,7 @@ private synchronized AbstractConfigImpl build(Optional rootNode) { rootNode.orElseGet(ObjectNode::empty), targetFilter, this, - aliasGenerator, - configSource.allSources()); + aliasGenerator); AbstractConfigImpl config = factory.config(); // initialize filters initializeFilters(config, targetFilter); @@ -148,7 +146,7 @@ private synchronized AbstractConfigImpl build(Optional rootNode) { private ObjectNode resolveKeys(ObjectNode rootNode) { Function resolveTokenFunction = Function.identity(); if (keyResolving) { - Map flattenValueNodes = flattenNodes(rootNode); + Map flattenValueNodes = ConfigHelper.flattenNodes(rootNode); if (flattenValueNodes.isEmpty()) { return rootNode; @@ -166,15 +164,6 @@ private ObjectNode resolveKeys(ObjectNode rootNode) { return ObjectNodeBuilderImpl.create(rootNode, resolveTokenFunction).build(); } - private Map flattenNodes(ConfigNode node) { - return ConfigHelper.flattenNodes(ConfigKeyImpl.of(), node) - .filter(e -> e.getValue() instanceof ValueNodeImpl) - .collect(Collectors.toMap( - e -> e.getKey().toString(), - e -> Config.Key.escapeName(((ValueNodeImpl) e.getValue()).get()) - )); - } - private Map tokenToValueMap(Map flattenValueNodes) { return flattenValueNodes.keySet() .stream() @@ -253,7 +242,6 @@ private void initializeFilters(Config config, ChainConfigFilter chain) { filterProviders.stream() .map(providerFunction -> providerFunction.apply(config)) - .sorted(ConfigUtils.priorityComparator(ConfigFilter.PRIORITY)) .forEachOrdered(chain::addFilter); chain.filterProviders.stream() diff --git a/config/config/src/main/java/io/helidon/config/UrlOverrideSource.java b/config/config/src/main/java/io/helidon/config/UrlOverrideSource.java index 4a6232f1c23..65c1be68a12 100644 --- a/config/config/src/main/java/io/helidon/config/UrlOverrideSource.java +++ b/config/config/src/main/java/io/helidon/config/UrlOverrideSource.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2017, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/config/config/src/main/java/io/helidon/config/ValueResolvingFilter.java b/config/config/src/main/java/io/helidon/config/ValueResolvingFilter.java index 2602076f019..57ba026484f 100644 --- a/config/config/src/main/java/io/helidon/config/ValueResolvingFilter.java +++ b/config/config/src/main/java/io/helidon/config/ValueResolvingFilter.java @@ -141,7 +141,7 @@ public void init(Config config) { * either case save the result in a simple boolean for efficiency in * #apply. */ - if (!failOnMissingReferenceSetting.isPresent()) { + if (failOnMissingReferenceSetting.isEmpty()) { failOnMissingReferenceSetting = Optional.of( config .get(ConfigFilters.ValueResolvingBuilder.FAIL_ON_MISSING_REFERENCE_KEY_NAME) diff --git a/config/config/src/main/java/io/helidon/config/spi/ChangeWatcherProvider.java b/config/config/src/main/java/io/helidon/config/spi/ChangeWatcherProvider.java index 9251a9731a7..2a0f5bc292c 100644 --- a/config/config/src/main/java/io/helidon/config/spi/ChangeWatcherProvider.java +++ b/config/config/src/main/java/io/helidon/config/spi/ChangeWatcherProvider.java @@ -18,5 +18,5 @@ /** * Java service loader service to create a polling strategy factory based on meta configuration. */ -public interface ChangeWatcherProvider extends MetaConfigurableProvider { +public interface ChangeWatcherProvider extends MetaConfigurableProvider> { } diff --git a/config/config/src/main/java/io/helidon/config/spi/FallbackMergingStrategy.java b/config/config/src/main/java/io/helidon/config/spi/FallbackMergingStrategy.java index 9f6c7d9b0bf..48494a34126 100644 --- a/config/config/src/main/java/io/helidon/config/spi/FallbackMergingStrategy.java +++ b/config/config/src/main/java/io/helidon/config/spi/FallbackMergingStrategy.java @@ -50,14 +50,17 @@ public ObjectNode merge(List rootNodesParam) { return builder.build(); } - private static ObjectNode.Builder addNode(ObjectNode.Builder builder, String key, ConfigNode node) { + private static void addNode(ObjectNode.Builder builder, String key, ConfigNode node) { switch (node.nodeType()) { case OBJECT: - return builder.addObject(key, (ObjectNode) node); + builder.addObject(key, (ObjectNode) node); + return; case LIST: - return builder.addList(key, (ListNode) node); + builder.addList(key, (ListNode) node); + return; case VALUE: - return builder.addValue(key, (ValueNode) node); + builder.addValue(key, (ValueNode) node); + return; default: throw new IllegalArgumentException("Unsupported node type: " + node.getClass().getName()); } diff --git a/config/config/src/main/java/io/helidon/config/spi/OrderedProperties.java b/config/config/src/main/java/io/helidon/config/spi/OrderedProperties.java index 705c470dc64..ccda50b9f12 100644 --- a/config/config/src/main/java/io/helidon/config/spi/OrderedProperties.java +++ b/config/config/src/main/java/io/helidon/config/spi/OrderedProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or 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,9 +26,9 @@ */ final class OrderedProperties { - private LinkedHashMap map = new LinkedHashMap<>(); + private final LinkedHashMap map = new LinkedHashMap<>(); - private Properties properties = new Properties() { + private final Properties properties = new Properties() { @Override public synchronized Object put(Object key, Object value) { return map.put(key.toString(), value.toString()); diff --git a/config/config/src/main/java/io/helidon/config/spi/SpiHelper.java b/config/config/src/main/java/io/helidon/config/spi/SpiHelper.java index c2b73f79e85..43e783f66a4 100644 --- a/config/config/src/main/java/io/helidon/config/spi/SpiHelper.java +++ b/config/config/src/main/java/io/helidon/config/spi/SpiHelper.java @@ -25,7 +25,7 @@ private SpiHelper() { * * @see io.helidon.config.spi.ConfigNode.ObjectNode#empty() */ - public static final class EmptyObjectNodeHolder { + static final class EmptyObjectNodeHolder { private EmptyObjectNodeHolder() { throw new AssertionError("Instantiation not allowed."); diff --git a/config/config/src/main/java/module-info.java b/config/config/src/main/java/module-info.java index 3f385a4e806..c80d301ca72 100644 --- a/config/config/src/main/java/module-info.java +++ b/config/config/src/main/java/module-info.java @@ -17,7 +17,7 @@ import io.helidon.config.PropertiesConfigParser; /** - * config module. + * Helidon SE Config module. */ module io.helidon.config { @@ -27,9 +27,9 @@ requires transitive io.helidon.common; requires transitive io.helidon.common.reactive; - requires io.helidon.common.serviceloader; requires transitive io.helidon.common.media.type; - requires transitive microprofile.config.api; + + requires io.helidon.common.serviceloader; exports io.helidon.config; exports io.helidon.config.spi; @@ -41,13 +41,8 @@ uses io.helidon.config.spi.OverrideSourceProvider; uses io.helidon.config.spi.RetryPolicyProvider; uses io.helidon.config.spi.PollingStrategyProvider; - - uses org.eclipse.microprofile.config.spi.ConfigSource; - uses org.eclipse.microprofile.config.spi.ConfigSourceProvider; - uses org.eclipse.microprofile.config.spi.Converter; uses io.helidon.config.spi.ChangeWatcherProvider; provides io.helidon.config.spi.ConfigParser with PropertiesConfigParser; - provides org.eclipse.microprofile.config.spi.ConfigProviderResolver with io.helidon.config.MpConfigProviderResolver; } diff --git a/config/config/src/test/java/io/helidon/config/AutoLoadedConfigPriority.java b/config/config/src/test/java/io/helidon/config/AutoLoadedConfigPriority.java index a3eff0eb4d3..833bfcabba9 100644 --- a/config/config/src/test/java/io/helidon/config/AutoLoadedConfigPriority.java +++ b/config/config/src/test/java/io/helidon/config/AutoLoadedConfigPriority.java @@ -18,6 +18,8 @@ import io.helidon.config.spi.ConfigFilter; +import static io.helidon.config.FilterLoadingTest.ORIGINAL_VALUE_SUBJECT_TO_AUTO_FILTERING; + /** * Abstract superclass for making sure simple priority works. *

@@ -34,7 +36,11 @@ public abstract class AutoLoadedConfigPriority implements ConfigFilter { @Override public String apply(Config.Key key, String stringValue) { - if (key.toString().equals(KEY_SUBJECT_TO_AUTO_FILTERING)) { + // the original implementation was wrong (priorities were inversed and this test was wrong) + // the new approach makes sure the filter with higher priority modifies the value, and + // any filter down the filter chain sees the modified value, and ignores it + if (key.toString().equals(KEY_SUBJECT_TO_AUTO_FILTERING) + && stringValue.equals(ORIGINAL_VALUE_SUBJECT_TO_AUTO_FILTERING)) { return getExpectedValue(); } return stringValue; diff --git a/config/config/src/test/java/io/helidon/config/ConfigHelperTest.java b/config/config/src/test/java/io/helidon/config/ConfigHelperTest.java index 9de4ed4b182..e2d4e4d0a3b 100644 --- a/config/config/src/test/java/io/helidon/config/ConfigHelperTest.java +++ b/config/config/src/test/java/io/helidon/config/ConfigHelperTest.java @@ -16,51 +16,36 @@ package io.helidon.config; -import java.util.concurrent.Flow; -import java.util.function.Function; +import java.util.Map; + +import io.helidon.config.spi.ConfigNode; import org.junit.jupiter.api.Test; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; -/** - * Tests {@link ConfigHelper}. - */ -public class ConfigHelperTest { +class ConfigHelperTest { @Test - public void testSubscriber() { - //mocks - Function onNextFunction = mock(Function.class); - Flow.Subscription subscription = mock(Flow.Subscription.class); - - //create Subscriber - Flow.Subscriber subscriber = ConfigHelper.subscriber(onNextFunction); - - //onSubscribe - subscriber.onSubscribe(subscription); - // request(Long.MAX_VALUE) has been invoked - verify(subscription).request(Long.MAX_VALUE); - - //MOCK onNext - when(onNextFunction.apply(1L)).thenReturn(true); - when(onNextFunction.apply(2L)).thenReturn(true); - when(onNextFunction.apply(3L)).thenReturn(false); - // 2x onNext -> true - subscriber.onNext(1L); - subscriber.onNext(2L); - // function invoked 2x, cancel never - verify(onNextFunction, times(2)).apply(any()); - verify(subscription, never()).cancel(); - // 1x onNext -> false - subscriber.onNext(3L); - // function invoked 2+1x, cancel 1x - verify(onNextFunction, times(2 + 1)).apply(any()); - verify(subscription, times(1)).cancel(); + void testFlattenNodes() { + ConfigNode.ObjectNode node = ConfigNode.ObjectNode.builder() + .addValue("simple", "value") + .addList("list", ConfigNode.ListNode.builder() + .addValue("first") + .addValue("second") + .build()) + .addObject("object", ConfigNode.ObjectNode.builder() + .addValue("value", "value2") + .build()) + .build(); + + Map map = ConfigHelper.flattenNodes(node); + Map expected = Map.of( + "simple", "value", + "list.0", "first", + "list.1", "second", + "object.value", "value2" + ); + assertThat(map, is(expected)); } - -} +} \ No newline at end of file diff --git a/config/config/src/test/java/io/helidon/config/ConfigUtilsTest.java b/config/config/src/test/java/io/helidon/config/ConfigUtilsTest.java deleted file mode 100644 index db51e1aae2a..00000000000 --- a/config/config/src/test/java/io/helidon/config/ConfigUtilsTest.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2020 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.config; - -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import javax.annotation.Priority; - -import io.helidon.config.ConfigUtils.ScheduledTask; - -import org.hamcrest.Matchers; -import org.hamcrest.core.IsInstanceOf; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; - -/** - * Tests {@link io.helidon.config.ConfigUtils}. - */ -public class ConfigUtilsTest { - - private void testAsStream(Iterable integers) { - List list = ConfigUtils.asStream(integers).sorted(Integer::compare).collect(Collectors.toList()); - assertThat(list, Matchers.hasSize(4)); - assertThat(list.get(0), equalTo(0)); - assertThat(list.get(1), equalTo(10)); - assertThat(list.get(2), equalTo(20)); - assertThat(list.get(3), equalTo(30)); - } - - @Test - public void testAsStream() { - testAsStream(Arrays.asList(20, 0, 30, 10)); - testAsStream(Arrays.asList(10, 30, 0, 20)); - testAsStream(Arrays.asList(0, 10, 20, 30)); - } - - private void testAsPrioritizedStream(Iterable providers) { - List list = ConfigUtils.asPrioritizedStream(providers, 0).collect(Collectors.toList()); - assertThat(list, Matchers.hasSize(4)); - assertThat(list.get(0), IsInstanceOf.instanceOf(Provider3.class)); - assertThat(list.get(1), IsInstanceOf.instanceOf(Provider1.class)); - assertThat(list.get(2), IsInstanceOf.instanceOf(Provider4.class)); - assertThat(list.get(3), IsInstanceOf.instanceOf(Provider2.class)); - } - - @Test - public void testAsPrioritizedStream() { - testAsPrioritizedStream(Arrays.asList(new Provider1(), new Provider2(), new Provider3(), new Provider4())); - testAsPrioritizedStream(Arrays.asList(new Provider4(), new Provider3(), new Provider2(), new Provider1())); - testAsPrioritizedStream(Arrays.asList(new Provider2(), new Provider4(), new Provider1(), new Provider3())); - } - - @Test - public void testScheduledTaskInterruptedRepeatedly() throws InterruptedException { - AtomicInteger counter = new AtomicInteger(); - ScheduledTask task = new ScheduledTask(Executors.newSingleThreadScheduledExecutor(), - counter::incrementAndGet, - Duration.ofMillis(80)); - task.schedule(); - task.schedule(); - task.schedule(); - task.schedule(); - task.schedule(); - - //not yet finished - assertThat(counter.get(), is(0)); - - TimeUnit.MILLISECONDS.sleep(120); - assertThat(counter.get(), is(1)); - } - - @Test - public void testScheduledTaskExecutedRepeatedly() throws InterruptedException { - CountDownLatch execLatch = new CountDownLatch(5); - ScheduledTask task = new ScheduledTask(Executors.newSingleThreadScheduledExecutor(), - execLatch::countDown, - Duration.ZERO); - /* - Because invoking 'schedule' can cancel an existing action, keep track - of cancelations in case the latch expires without reaching 0. - */ - final long RESCHEDULE_DELAY_MS = 5; - final int ACTIONS_TO_SCHEDULE = 5; - int cancelations = 0; - - for (int i = 0; i < ACTIONS_TO_SCHEDULE; i++ ) { - if (task.schedule()) { - cancelations++; - } - TimeUnit.MILLISECONDS.sleep(RESCHEDULE_DELAY_MS); - } - /* - The latch can either complete -- because all the scheduled actions finished -- - or it can expire at the timeout because at least one action did not finish, in - which case the remaining latch value should not exceed the number of actions - canceled. (Do not check for exact equality; some attempts to cancel - an action might occur after the action was deemed to be not-yet-run or in-progress - but actually runs to completion before the cancel is actually invoked. - */ - assertThat( - "Current execLatch count: " + execLatch.getCount() + ", cancelations: " - + "" + cancelations, - execLatch.await(3000, TimeUnit.MILLISECONDS) || execLatch.getCount() <= cancelations, - is(true)); - } - - // - // providers ... - // - - interface Provider { - } - - @Priority(20) - static class Provider1 implements Provider { - } - - static class Provider2 implements Provider { - } - - @Priority(30) - static class Provider3 implements Provider { - } - - @Priority(10) - static class Provider4 implements Provider { - } - -} diff --git a/config/config/src/test/java/io/helidon/config/FilterLoadingTest.java b/config/config/src/test/java/io/helidon/config/FilterLoadingTest.java index 8040c822823..d337a89a646 100644 --- a/config/config/src/test/java/io/helidon/config/FilterLoadingTest.java +++ b/config/config/src/test/java/io/helidon/config/FilterLoadingTest.java @@ -28,7 +28,7 @@ */ public class FilterLoadingTest { - private static final String ORIGINAL_VALUE_SUBJECT_TO_AUTO_FILTERING = "originalValue"; + static final String ORIGINAL_VALUE_SUBJECT_TO_AUTO_FILTERING = "originalValue"; private static final String ORIGINAL_VALUE_SUBJECT_TO_AUTO_FILTERING_VIA_PROVIDER = "originalValueForProviderTest"; private static final String UNAFFECTED_KEY = "key1"; diff --git a/config/config/src/test/java/io/helidon/config/MpConfigTest.java b/config/config/src/test/java/io/helidon/config/MpConfigTest.java deleted file mode 100644 index f43889023f5..00000000000 --- a/config/config/src/test/java/io/helidon/config/MpConfigTest.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.config; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.hasSize; - -/** - * Test MicroProfile config implementation. - */ -public class MpConfigTest { - private static Config config; - - @BeforeAll - static void initClass() { - Object helidonConfig = io.helidon.config.Config.builder() - .addSource(ConfigSources.classpath("io/helidon/config/application.properties")) - .addSource(ConfigSources.create(Map.of("mp-1", "mp-value-1", - "mp-2", "mp-value-2", - "app.storageEnabled", "false", - "mp-array", "a,b,c", - "mp-list.0", "1", - "mp-list.1", "2", - "mp-list.2", "3"))) - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .build(); - - assertThat(helidonConfig, instanceOf(Config.class)); - - config = (Config) helidonConfig; - } - - @Test - void testConfigSources() { - Iterable configSources = config.getConfigSources(); - List asList = new ArrayList<>(); - for (ConfigSource configSource : configSources) { - asList.add(configSource); - } - - assertThat(asList, hasSize(2)); - - ConfigSourceRuntimeImpl helidonWrapper = (ConfigSourceRuntimeImpl) asList.get(0); - assertThat(helidonWrapper.unwrap(), instanceOf(ClasspathConfigSource.class)); - helidonWrapper = (ConfigSourceRuntimeImpl) asList.get(1); - assertThat(helidonWrapper.unwrap(), instanceOf(MapConfigSource.class)); - - ConfigSource classpath = asList.get(0); - assertThat(classpath.getValue("app.storageEnabled"), is("true")); - - ConfigSource map = asList.get(1); - assertThat(map.getValue("mp-1"), is("mp-value-1")); - assertThat(map.getValue("mp-2"), is("mp-value-2")); - assertThat(map.getValue("app.storageEnabled"), is("false")); - } - - @Test - void testOptionalValue() { - assertThat(config.getOptionalValue("app.storageEnabled", Boolean.class), is(Optional.of(true))); - assertThat(config.getOptionalValue("mp-1", String.class), is(Optional.of("mp-value-1"))); - } - - @Test - void testStringArray() { - String[] values = config.getValue("mp-array", String[].class); - assertThat(values, arrayContaining("a", "b", "c")); - } - - @Test - void testIntArray() { - Integer[] values = config.getValue("mp-list", Integer[].class); - assertThat(values, arrayContaining(1, 2, 3)); - } -} diff --git a/config/encryption/pom.xml b/config/encryption/pom.xml index e3c2046e28a..d4bbd7a40c4 100644 --- a/config/encryption/pom.xml +++ b/config/encryption/pom.xml @@ -43,6 +43,15 @@ io.helidon.common helidon-common-key-util + + + io.helidon.config + helidon-config-mp + provided + true + io.helidon.config helidon-config-yaml diff --git a/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionFilter.java b/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionFilter.java index a8d5a67400a..ff39774d039 100644 --- a/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionFilter.java +++ b/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionFilter.java @@ -37,7 +37,7 @@ *

* Password in properties must be stored as follows: *

    - *
  • ${AES=base64} - encrypted password using a master password (must be provided to Prime through configuration, system + *
  • ${AES=base64} - encrypted password using a master password (must be provided to prime through configuration, system * property or environment variable)
  • *
  • ${RSA=base64} - encrypted password using a public key (private key must be available to Prime instance, * its location must be provided to prime through configuration, system property or environment variable)
  • diff --git a/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionUtil.java b/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionUtil.java index 20be8b44e4a..7ac4fa9a3bc 100644 --- a/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionUtil.java +++ b/config/encryption/src/main/java/io/helidon/config/encryption/EncryptionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import io.helidon.common.pki.KeyConfig; import io.helidon.config.Config; import io.helidon.config.ConfigValue; +import io.helidon.config.mp.MpConfig; /** * Encryption utilities for secrets protection. @@ -280,6 +281,29 @@ static String decryptAes(char[] masterPassword, String encryptedBase64) throws C } } + static Optional resolveMasterPassword(boolean requireEncryption, org.eclipse.microprofile.config.Config config) { + Optional result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE) + .or(() -> { + Optional value = config.getOptionalValue(ConfigProperties.MASTER_PASSWORD_CONFIG_KEY, String.class); + if (value.isPresent()) { + if (requireEncryption) { + LOGGER.warning( + "Master password is configured as clear text in configuration when encryption is required. " + + "This value will be ignored. System property or environment variable expected!!!"); + return Optional.empty(); + } + } + return value; + }) + .map(String::toCharArray); + + if (result.isEmpty()) { + LOGGER.fine("Securing properties using master password is not available, as master password is not configured"); + } + + return result; + } + static Optional resolveMasterPassword(boolean requireEncryption, Config config) { Optional result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE) .or(() -> { @@ -303,6 +327,10 @@ static Optional resolveMasterPassword(boolean requireEncryption, Config return result; } + static Optional resolvePrivateKey(org.eclipse.microprofile.config.Config config){ + return resolvePrivateKey(MpConfig.toHelidonConfig(config).get("security.config.rsa")); + } + static Optional resolvePrivateKey(Config config) { // load configuration values KeyConfig.PemBuilder pemBuilder = KeyConfig.pemBuilder().config(config); diff --git a/config/encryption/src/main/java/io/helidon/config/encryption/MpEncryptionFilter.java b/config/encryption/src/main/java/io/helidon/config/encryption/MpEncryptionFilter.java new file mode 100644 index 00000000000..c72740f8a47 --- /dev/null +++ b/config/encryption/src/main/java/io/helidon/config/encryption/MpEncryptionFilter.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.encryption; + +import java.security.PrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.util.HashSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.HelidonFeatures; +import io.helidon.config.mp.spi.MpConfigFilter; + +import org.eclipse.microprofile.config.Config; + +/** + * Provides possibility to decrypt passwords from configuration sources. + * Configuration can be used to enforce encryption (e.g. we will fail on clear-text value). + *

    + * Password in properties must be stored as follows: + *

      + *
    • ${AES=base64} - encrypted password using a master password (must be provided to prime through configuration, system + * property or environment variable)
    • + *
    • ${RSA=base64} - encrypted password using a public key (private key must be available to Prime instance, + * its location must be provided to prime through configuration, system property or environment variable)
    • + *
    • ${ALIAS=alias_name} - reference to another property, that is encrypted
    • + *
    • ${CLEAR=text} - clear-text password. Intentionally denoting this value as a protectable one, so we can enforce encryption + * (e.g. in prod)
    • + *
    + * Example: + *
    + * google_client_secret=${AES=mYRkg+4Q4hua1kvpCCI2hg==}
    + * service_password=${RSA=mYRkg+4Q4hua1kvpCCI2hg==}
    + * another_password=${ALIAS=service_password}
    + * cleartext_password=${CLEAR=known_password}
    + * 
    + * + * @see ConfigProperties#PRIVATE_KEYSTORE_PATH_ENV_VARIABLE + * @see ConfigProperties#MASTER_PASSWORD_ENV_VARIABLE + * @see ConfigProperties#MASTER_PASSWORD_CONFIG_KEY + * @see ConfigProperties#REQUIRE_ENCRYPTION_ENV_VARIABLE + */ +public final class MpEncryptionFilter implements MpConfigFilter { + private static final String PREFIX_LEGACY_AES = "${AES="; + private static final String PREFIX_LEGACY_RSA = "${RSA="; + static final String PREFIX_GCM = "${GCM="; + static final String PREFIX_RSA = "${RSA-P="; + private static final Logger LOGGER = Logger.getLogger(MpEncryptionFilter.class.getName()); + private static final String PREFIX_ALIAS = "${ALIAS="; + private static final String PREFIX_CLEAR = "${CLEAR="; + + static { + HelidonFeatures.register("Config", "Encryption"); + } + + private PrivateKey privateKey; + private char[] masterPassword; + + private boolean requireEncryption; + + private MpConfigFilter clearFilter; + private MpConfigFilter rsaFilter; + private MpConfigFilter aesFilter; + private MpConfigFilter aliasFilter; + + /** + * This constructor is only for use by {@link io.helidon.config.mp.spi.MpConfigFilter} service loader. + */ + @Deprecated + public MpEncryptionFilter() { + } + + @Override + public void init(org.eclipse.microprofile.config.Config config) { + this.requireEncryption = EncryptionUtil.getEnv(ConfigProperties.REQUIRE_ENCRYPTION_ENV_VARIABLE) + .map(Boolean::parseBoolean) + .or(() -> config.getOptionalValue(ConfigProperties.REQUIRE_ENCRYPTION_CONFIG_KEY, Boolean.class)) + .orElse(true); + + this.masterPassword = EncryptionUtil.resolveMasterPassword(requireEncryption, config) + .orElse(null); + this.privateKey = EncryptionUtil.resolvePrivateKey(config) + .orElse(null); + + if (null != privateKey && !(privateKey instanceof RSAPrivateKey)) { + throw new ConfigEncryptionException("Private key must be an RSA private key, but is: " + + privateKey.getClass().getName()); + } + + MpConfigFilter noOp = (key, stringValue) -> stringValue; + + aesFilter = (null == masterPassword ? noOp : (key, stringValue) -> decryptAes(masterPassword, stringValue)); + rsaFilter = (null == privateKey ? noOp : (key, stringValue) -> decryptRsa(privateKey, stringValue)); + clearFilter = this::clearText; + aliasFilter = (key, stringValue) -> aliased(stringValue, config); + + } + + @Override + public String apply(String propertyName, String value) { + return maybeDecode(propertyName, value); + } + + private static String removePlaceholder(String prefix, String value) { + return value.substring(prefix.length(), value.length() - 1); + } + + private String maybeDecode(String propertyName, String value) { + Set processedValues = new HashSet<>(); + + do { + processedValues.add(value); + if (!value.startsWith("${") && !value.endsWith("}")) { + //this is not encoded, safely return + return value; + } + value = aliasFilter.apply(propertyName, value); + value = clearFilter.apply(propertyName, value); + value = rsaFilter.apply(propertyName, value); + value = aesFilter.apply(propertyName, value); + } while (!processedValues.contains(value)); + + return value; + } + + private String clearText(String propertyName, String value) { + // cleartext_password=${CLEAR=known_password} + if (value.startsWith(PREFIX_CLEAR)) { + if (requireEncryption) { + throw new ConfigEncryptionException("Key \"" + propertyName + "\" is a clear text password, yet encryption is " + + "required"); + } + return removePlaceholder(PREFIX_CLEAR, value); + } + + return value; + } + + private String aliased(String value, Config config) { + + if (value.startsWith(PREFIX_ALIAS)) { + // another_password=${ALIAS=service_password} + String alias = removePlaceholder(PREFIX_ALIAS, value); + + return config.getOptionalValue(alias, String.class) + .orElseThrow(() -> new NoSuchElementException("Aliased key not found. Value: " + value)); + } + + return value; + } + + private String decryptRsa(PrivateKey privateKey, String value) { + // service_password=${RSA=mYRkg+4Q4hua1kvpCCI2hg==} + if (value.startsWith(PREFIX_LEGACY_RSA)) { + LOGGER.log(Level.WARNING, () -> "You are using legacy RSA encryption. Please re-encrypt the value with RSA-P."); + String b64Value = removePlaceholder(PREFIX_LEGACY_RSA, value); + try { + return EncryptionUtil.decryptRsaLegacy(privateKey, b64Value); + } catch (ConfigEncryptionException e) { + LOGGER.log(Level.FINEST, e, () -> "Failed to decrypt " + value); + return value; + } + } else if (value.startsWith(PREFIX_RSA)) { + String b64Value = removePlaceholder(PREFIX_RSA, value); + try { + return EncryptionUtil.decryptRsa(privateKey, b64Value); + } catch (ConfigEncryptionException e) { + LOGGER.log(Level.FINEST, e, () -> "Failed to decrypt " + value); + return value; + } + } + + return value; + } + + private String decryptAes(char[] masterPassword, String value) { + // google_client_secret=${AES=mYRkg+4Q4hua1kvpCCI2hg==} + + if (value.startsWith(PREFIX_LEGACY_AES)) { + LOGGER.log(Level.WARNING, () -> "You are using legacy AES encryption. Please re-encrypt the value with GCM."); + String b64Value = value.substring(PREFIX_LEGACY_AES.length(), value.length() - 1); + try { + return EncryptionUtil.decryptAesLegacy(masterPassword, b64Value); + } catch (ConfigEncryptionException e) { + LOGGER.log(Level.FINEST, e, () -> "Failed to decrypt " + value); + return value; + } + } else if (value.startsWith(PREFIX_GCM)) { + String b64Value = value.substring(PREFIX_GCM.length(), value.length() - 1); + try { + return EncryptionUtil.decryptAes(masterPassword, b64Value); + } catch (ConfigEncryptionException e) { + LOGGER.log(Level.FINEST, e, () -> "Failed to decrypt " + value); + return value; + } + } + + return value; + } +} diff --git a/config/encryption/src/main/java/module-info.java b/config/encryption/src/main/java/module-info.java index 2a3e186b0aa..7ddafa9482d 100644 --- a/config/encryption/src/main/java/module-info.java +++ b/config/encryption/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2020 Oracle and/or 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,10 @@ requires transitive io.helidon.common.pki; requires transitive io.helidon.config; + requires static io.helidon.config.mp; + exports io.helidon.config.encryption; provides io.helidon.config.spi.ConfigFilter with io.helidon.config.encryption.EncryptionFilterService; + provides io.helidon.config.mp.spi.MpConfigFilter with io.helidon.config.encryption.MpEncryptionFilter; } diff --git a/config/encryption/src/main/resources/META-INF/services/io.helidon.config.mp.spi.MpConfigFilter b/config/encryption/src/main/resources/META-INF/services/io.helidon.config.mp.spi.MpConfigFilter new file mode 100644 index 00000000000..a35947c05d7 --- /dev/null +++ b/config/encryption/src/main/resources/META-INF/services/io.helidon.config.mp.spi.MpConfigFilter @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.config.encryption.MpEncryptionFilter diff --git a/config/pom.xml b/config/pom.xml index 9c3cad4825e..9921045b569 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -46,6 +46,7 @@ testing test-infrastructure tests + config-mp diff --git a/config/tests/pom.xml b/config/tests/pom.xml index 8e9f97c090a..fce3466f866 100644 --- a/config/tests/pom.xml +++ b/config/tests/pom.xml @@ -58,6 +58,5 @@ test-mappers-2-complex test-meta-source test-parsers-1-complex - test-mp-reference diff --git a/config/yaml/pom.xml b/config/yaml/pom.xml index c8941a4de73..54e9566e862 100644 --- a/config/yaml/pom.xml +++ b/config/yaml/pom.xml @@ -50,6 +50,12 @@ jakarta.annotation jakarta.annotation-api + + org.eclipse.microprofile.config + microprofile-config-api + provided + true + io.helidon.config helidon-config-testing diff --git a/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSource.java b/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSource.java new file mode 100644 index 00000000000..00475d5d2de --- /dev/null +++ b/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSource.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.yaml; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.config.ConfigException; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.yaml.snakeyaml.Yaml; + +/** + * MicroProfile {@link org.eclipse.microprofile.config.spi.ConfigSource} that can be used + * to add YAML files from classpath or file system using the + * {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}. + *

    The YAML file is transformed to a flat map as follows:

    + * Object nodes + *

    + * Each node in the tree is dot separated. + *

    + * server:
    + *     host: "localhost"
    + *     port: 8080
    + * 
    + * Will be transformed to the following properties: + *
    + * server.host=localhost
    + * server.port=8080
    + * 
    + * List nodes (arrays) + *

    + * Each node will be indexed (0 based) + *

    + * providers:
    + *   - abac:
    + *       enabled: true
    + * names: ["first", "second", "third"]
    + * 
    + * Will be transformed to the following properties: + *
    + * providers.0.abac.enabled=true
    + * names.0=first
    + * names.1=second
    + * names.2=third
    + * 
    + */ +public class YamlMpConfigSource implements ConfigSource { + private final Map properties; + private final String name; + + private YamlMpConfigSource(String name, Map properties) { + this.properties = properties; + this.name = "yaml: " + name; + } + + /** + * Load a YAML config source from file system. + * + * @param path path to the YAML file + * @return config source loaded from the file + * @see #create(java.net.URL) + */ + public static ConfigSource create(Path path) { + try { + return create(path.toUri().toURL()); + } catch (MalformedURLException e) { + throw new ConfigException("Failed to load YAML config source from path: " + path.toAbsolutePath(), e); + } + } + + /** + * Load a YAML config source from URL. + * The URL may be any URL which is support by the used JVM. + * + * @param url url of the resource + * @return config source loaded from the URL + */ + public static ConfigSource create(URL url) { + try (InputStreamReader reader = new InputStreamReader(url.openConnection().getInputStream(), StandardCharsets.UTF_8)) { + return create(url.toString(), reader); + } catch (Exception e) { + throw new ConfigException("Failed to configure YAML config source", e); + } + } + + /** + * Create from YAML content as a reader. + * This method will NOT close the reader. + * + * @param name name of the config source + * @param content reader with the YAML content + * @return config source loaded from the content + */ + public static ConfigSource create(String name, Reader content) { + Map yamlMap; + Yaml yaml = new Yaml(); + yamlMap = yaml.loadAs(content, Map.class); + if (yamlMap == null) { // empty source + return new YamlMpConfigSource(name, Map.of()); + } + + return new YamlMpConfigSource(name, fromMap(yamlMap)); + + } + + @Override + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Override + public String getValue(String propertyName) { + return properties.get(propertyName); + } + + @Override + public String getName() { + return name; + } + + private static Map fromMap(Map yamlMap) { + Map result = new HashMap<>(); + process(result, "", yamlMap); + + return result; + } + + private static void process(Map resultMap, String prefix, Map yamlMap) { + yamlMap.forEach((key, value) -> { + processNext(resultMap, prefix(prefix, key.toString()), value); + }); + } + + private static void process(Map resultMap, String prefix, List yamlList) { + int counter = 0; + for (Object value : yamlList) { + processNext(resultMap, prefix(prefix, String.valueOf(counter)), value); + + counter++; + } + } + + private static void processNext(Map resultMap, + String prefix, + Object value) { + if (value instanceof List) { + process(resultMap, prefix, (List) value); + } else if (value instanceof Map) { + process(resultMap, prefix, (Map) value); + } else { + String stringValue = (null == value) ? "" : value.toString(); + resultMap.put(prefix, stringValue); + } + } + + private static String prefix(String prefix, String stringKey) { + if (prefix.isEmpty()) { + return stringKey; + } + + return prefix + "." + stringKey; + } +} diff --git a/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSourceProvider.java b/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSourceProvider.java new file mode 100644 index 00000000000..ad38f09eaab --- /dev/null +++ b/config/yaml/src/main/java/io/helidon/config/yaml/YamlMpConfigSourceProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.yaml; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +import io.helidon.config.ConfigException; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; + +/** + * YAML config source provider for MicroProfile config that supports file {@code application.yaml}. + * This class should not be used directly - it is loaded automatically by Java service loader. + */ +public class YamlMpConfigSourceProvider implements ConfigSourceProvider { + @Override + public Iterable getConfigSources(ClassLoader classLoader) { + Enumeration resources; + try { + resources = classLoader.getResources("application.yaml"); + } catch (IOException e) { + throw new ConfigException("Failed to read resources from classpath", e); + } + + List result = new LinkedList<>(); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + result.add(YamlMpConfigSource.create(url)); + } + + return result; + } +} diff --git a/config/yaml/src/main/java/module-info.java b/config/yaml/src/main/java/module-info.java index 40b384f0ed2..f6d2155f0fd 100644 --- a/config/yaml/src/main/java/module-info.java +++ b/config/yaml/src/main/java/module-info.java @@ -15,6 +15,7 @@ */ import io.helidon.config.yaml.YamlConfigParser; +import io.helidon.config.yaml.YamlMpConfigSourceProvider; /** * YAML Parser implementation. @@ -27,9 +28,11 @@ requires transitive io.helidon.config; requires io.helidon.common; + requires static microprofile.config.api; exports io.helidon.config.yaml; provides io.helidon.config.spi.ConfigParser with YamlConfigParser; + provides org.eclipse.microprofile.config.spi.ConfigSourceProvider with YamlMpConfigSourceProvider; } diff --git a/config/yaml/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider b/config/yaml/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider new file mode 100644 index 00000000000..53d463485ba --- /dev/null +++ b/config/yaml/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider @@ -0,0 +1,17 @@ +# +# Copyright (c) 2020 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.config.yaml.YamlMpConfigSourceProvider \ No newline at end of file diff --git a/config/yaml/src/test/java/io/helidon/config/yaml/YamlMpConfigSourceTest.java b/config/yaml/src/test/java/io/helidon/config/yaml/YamlMpConfigSourceTest.java new file mode 100644 index 00000000000..d8f6a876985 --- /dev/null +++ b/config/yaml/src/test/java/io/helidon/config/yaml/YamlMpConfigSourceTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.yaml; + +import java.io.StringReader; + +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class YamlMpConfigSourceTest { + private static final String TEST_1 = "server:\n" + + " host: \"localhost\"\n" + + " port: 8080\n"; + + private static final String TEST_2 = "providers:\n" + + " - abac:\n" + + " enabled: true\n" + + "names: [\"first\", \"second\", \"third\"]"; + @Test + void testObjectNode() { + ConfigSource source = YamlMpConfigSource.create("testObjectNode", new StringReader(TEST_1)); + assertThat(source.getValue("server.host"), is("localhost")); + assertThat(source.getValue("server.port"), is("8080")); + } + + @Test + void testListNode() { + ConfigSource source = YamlMpConfigSource.create("testObjectNode", new StringReader(TEST_2)); + assertThat(source.getValue("providers.0.abac.enabled"), is("true")); + assertThat(source.getValue("names.0"), is("first")); + assertThat(source.getValue("names.1"), is("second")); + assertThat(source.getValue("names.2"), is("third")); + } +} diff --git a/dependencies/pom.xml b/dependencies/pom.xml index b51b42881f3..4420915d745 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -79,7 +79,7 @@ 1.1.6 5.6.2 2.10 - 1.3 + 1.4 2.1 1.1.1 2.2 @@ -601,6 +601,39 @@
    + + org.eclipse.microprofile.metrics + microprofile-metrics-api + ${version.lib.microprofile-metrics-api} + + + org.osgi + org.osgi.annotation.versioning + + + + + org.eclipse.microprofile.metrics + microprofile-metrics-rest-tck + ${version.lib.microprofile-metrics-api} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + + + org.eclipse.microprofile.metrics + microprofile-metrics-api-tck + ${version.lib.microprofile-metrics-api} + + + org.jboss.arquillian.junit + arquillian-junit-container + + + org.eclipse.microprofile.reactive-streams-operators microprofile-reactive-streams-operators-api diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml index 008c03fbfaf..fde2b7e7bab 100644 --- a/integrations/cdi/jpa-cdi/pom.xml +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -77,8 +77,8 @@ test - io.helidon.microprofile.config - helidon-microprofile-config + io.helidon.microprofile.cdi + helidon-microprofile-cdi test diff --git a/integrations/graal/native-image-extension/pom.xml b/integrations/graal/native-image-extension/pom.xml index b58b2388f14..05603cb630d 100644 --- a/integrations/graal/native-image-extension/pom.xml +++ b/integrations/graal/native-image-extension/pom.xml @@ -41,6 +41,10 @@ io.helidon.webserver helidon-webserver + + io.helidon.config + helidon-config-mp + org.glassfish jakarta.json diff --git a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java index 28cbf970a1a..7633fd29514 100644 --- a/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java +++ b/integrations/graal/native-image-extension/src/main/java/io/helidon/integrations/graal/nativeimage/extension/HelidonReflectionFeature.java @@ -39,7 +39,7 @@ import javax.json.JsonReaderFactory; import javax.json.stream.JsonParsingException; -import io.helidon.config.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfigProviderResolver; import com.oracle.svm.core.annotate.AutomaticFeature; import com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry; diff --git a/messaging/connectors/kafka/pom.xml b/messaging/connectors/kafka/pom.xml index df2a07e3ded..1a537da6955 100644 --- a/messaging/connectors/kafka/pom.xml +++ b/messaging/connectors/kafka/pom.xml @@ -48,6 +48,10 @@ io.helidon.config helidon-config + + io.helidon.config + helidon-config-mp + io.helidon.common helidon-common-context diff --git a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConnector.java b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConnector.java index 5b54916cef1..5bf2b8333ad 100644 --- a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConnector.java +++ b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConnector.java @@ -30,6 +30,7 @@ import io.helidon.common.configurable.ScheduledThreadPoolSupplier; import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.spi.Connector; @@ -89,14 +90,18 @@ void terminate(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event) @Override public PublisherBuilder> getPublisherBuilder(org.eclipse.microprofile.config.Config config) { - KafkaPublisher publisher = KafkaPublisher.builder().config((Config) config).scheduler(scheduler).build(); + KafkaPublisher publisher = KafkaPublisher.builder() + .config(MpConfig.toHelidonConfig(config)) + .scheduler(scheduler) + .build(); + resources.add(publisher); return ReactiveStreams.fromPublisher(publisher); } @Override public SubscriberBuilder, Void> getSubscriberBuilder(org.eclipse.microprofile.config.Config config) { - return ReactiveStreams.fromSubscriber(KafkaSubscriber.create((Config) config)); + return ReactiveStreams.fromSubscriber(KafkaSubscriber.create(MpConfig.toHelidonConfig(config))); } /** diff --git a/messaging/connectors/kafka/src/main/java/module-info.java b/messaging/connectors/kafka/src/main/java/module-info.java index af86e645c89..4d555eeafe5 100644 --- a/messaging/connectors/kafka/src/main/java/module-info.java +++ b/messaging/connectors/kafka/src/main/java/module-info.java @@ -22,10 +22,12 @@ requires static kafka.clients; requires org.reactivestreams; requires transitive io.helidon.config; + requires io.helidon.config.mp; requires transitive microprofile.reactive.messaging.api; requires transitive microprofile.reactive.streams.operators.api; requires io.helidon.common.context; requires io.helidon.common.configurable; + requires microprofile.config.api; exports io.helidon.messaging.connectors.kafka; } \ No newline at end of file diff --git a/metrics/metrics/pom.xml b/metrics/metrics/pom.xml index 04c69aab999..b2ff82e3f35 100644 --- a/metrics/metrics/pom.xml +++ b/metrics/metrics/pom.xml @@ -46,16 +46,13 @@ io.helidon.common helidon-common-metrics + + io.helidon.config + helidon-config-mp + org.eclipse.microprofile.metrics microprofile-metrics-api - ${version.lib.microprofile-metrics-api} - - - org.osgi - org.osgi.annotation.versioning - - io.helidon.webserver diff --git a/metrics/metrics/src/main/java/module-info.java b/metrics/metrics/src/main/java/module-info.java index 02e483bc7e6..92197c1c702 100644 --- a/metrics/metrics/src/main/java/module-info.java +++ b/metrics/metrics/src/main/java/module-info.java @@ -29,6 +29,8 @@ requires io.helidon.webserver; requires io.helidon.media.jsonp.server; requires java.json; + requires io.helidon.config.mp; + requires microprofile.config.api; provides io.helidon.common.metrics.InternalBridge with io.helidon.metrics.InternalBridgeImpl; diff --git a/microprofile/cdi/pom.xml b/microprofile/cdi/pom.xml index 486d39bd87a..c83504c286e 100644 --- a/microprofile/cdi/pom.xml +++ b/microprofile/cdi/pom.xml @@ -44,7 +44,7 @@ io.helidon.config - helidon-config + helidon-config-mp io.helidon.common diff --git a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java index bd19330799a..f7485f9d34e 100644 --- a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java +++ b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java @@ -43,9 +43,10 @@ import io.helidon.common.HelidonFlavor; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; -import io.helidon.config.Config; -import io.helidon.config.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfig; +import io.helidon.config.mp.MpConfigProviderResolver; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.weld.AbstractCDI; import org.jboss.weld.bean.builtin.BeanManagerProxy; @@ -151,7 +152,9 @@ public Collection getResources(String name) { }; setResourceLoader(resourceLoader); - Config config = (Config) ConfigProvider.getConfig(); + Config mpConfig = ConfigProvider.getConfig(); + io.helidon.config.Config config = MpConfig.toHelidonConfig(mpConfig); + Map properties = config.get("cdi") .detach() .asMap() @@ -263,7 +266,7 @@ private HelidonContainerImpl doStart() { bm = CDI.current().getBeanManager(); } - Config config = (Config) ConfigProvider.getConfig(); + org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); MpConfigProviderResolver.runtimeStart(config); @@ -316,7 +319,8 @@ public void close() throws SecurityException { now = System.currentTimeMillis() - now; LOGGER.fine("Container started in " + now + " millis (this excludes the initialization time)"); - HelidonFeatures.print(HelidonFlavor.MP, config.get("features.print-details").asBoolean().orElse(false)); + HelidonFeatures.print(HelidonFlavor.MP, + config.getOptionalValue("features.print-details", Boolean.class).orElse(false)); // shutdown hook should be added after all initialization is done, otherwise a race condition may happen Runtime.getRuntime().addShutdownHook(shutdownHook); diff --git a/microprofile/cdi/src/main/java/module-info.java b/microprofile/cdi/src/main/java/module-info.java index fa9cdeb696d..7c113dbb62d 100644 --- a/microprofile/cdi/src/main/java/module-info.java +++ b/microprofile/cdi/src/main/java/module-info.java @@ -29,6 +29,7 @@ requires io.helidon.common; requires io.helidon.config; + requires io.helidon.config.mp; requires weld.core.impl; requires weld.spi; @@ -36,6 +37,7 @@ requires weld.se.core; requires io.helidon.common.context; requires jakarta.inject.api; + requires microprofile.config.api; exports io.helidon.microprofile.cdi; diff --git a/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/HelidonContainerInitializerTest.java b/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/HelidonContainerInitializerTest.java index c1ffb069aa9..1b5dc850f83 100644 --- a/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/HelidonContainerInitializerTest.java +++ b/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/HelidonContainerInitializerTest.java @@ -23,9 +23,9 @@ import javax.enterprise.inject.se.SeContainerInitializer; import javax.enterprise.inject.spi.CDI; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; +import io.helidon.config.mp.MpConfigSources; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -59,7 +59,11 @@ void resetConfig() { @Test void testRestart() { // this is a reproducer for bug 1554 - Config config = Config.create(ConfigSources.create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))); + Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))) + .build(); + configResolver.registerConfig((org.eclipse.microprofile.config.Config) config, cl); // now we can start using SeContainerInitializer SeContainer container = SeContainerInitializer.newInstance() @@ -79,7 +83,11 @@ void testRestart() { @Test void testRestart2() { // this is a reproducer for bug 1554 - Config config = Config.create(ConfigSources.create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))); + Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))) + .build(); + configResolver.registerConfig((org.eclipse.microprofile.config.Config) config, cl); // now we can start using SeContainerInitializer SeContainerInitializer seContainerInitializer = SeContainerInitializer.newInstance(); @@ -103,8 +111,13 @@ void testSeInitializerFails() { assertThrows(IllegalStateException.class, SeContainerInitializer::newInstance); - Config config = Config.create(ConfigSources.create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))); - configResolver.registerConfig((org.eclipse.microprofile.config.Config) config, cl); + Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources + .create(Map.of(HelidonContainerInitializer.CONFIG_ALLOW_INITIALIZER, "true"))) + .build(); + + configResolver.registerConfig(config, cl); SeContainerInitializer seContainerInitializer = SeContainerInitializer.newInstance(); assertThat(seContainerInitializer, instanceOf(HelidonContainerInitializer.class)); diff --git a/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/MainTest.java b/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/MainTest.java index 0864bad2c91..484e888f8b1 100644 --- a/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/MainTest.java +++ b/microprofile/cdi/src/test/java/io/helidon/microprofile/cdi/MainTest.java @@ -23,10 +23,10 @@ import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.config.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfigSources; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.junit.jupiter.api.Test; @@ -68,13 +68,18 @@ void testEvents() { assertThat(extension.events(), contains(TestExtension.BUILD_TIME_START, TestExtension.BUILD_TIME_END)); - Config config = Config.create(ConfigSources.create(Map.of("key", "value"))); + + Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(Map.of("key", "value"))) + .build(); + ConfigProviderResolver.instance() - .registerConfig((org.eclipse.microprofile.config.Config) config, Thread.currentThread().getContextClassLoader()); + .registerConfig(config, Thread.currentThread().getContextClassLoader()); instance.start(); - Config runtimeConfig = extension.runtimeConfig(); + Object runtimeConfig = extension.runtimeConfig(); assertThat(runtimeConfig, instanceOf(MpConfigProviderResolver.ConfigDelegate.class)); assertThat(((MpConfigProviderResolver.ConfigDelegate) runtimeConfig).delegate(), sameInstance(config)); diff --git a/microprofile/config/pom.xml b/microprofile/config/pom.xml index e0c53d548a8..336010d22fc 100644 --- a/microprofile/config/pom.xml +++ b/microprofile/config/pom.xml @@ -37,20 +37,21 @@ io.helidon.config - helidon-config + helidon-config-mp - org.eclipse.microprofile.config - microprofile-config-api + io.helidon.config + helidon-config-yaml + runtime io.helidon.config - helidon-config-object-mapping + helidon-config-encryption runtime - io.helidon.microprofile.cdi - helidon-microprofile-cdi + io.helidon.config + helidon-config-object-mapping runtime @@ -64,6 +65,11 @@ jakarta.inject-api provided + + io.helidon.microprofile.cdi + helidon-microprofile-cdi + test + io.helidon.microprofile.bundles internal-test-libs diff --git a/microprofile/config/src/main/java/io/helidon/microprofile/config/ConfigCdiExtension.java b/microprofile/config/src/main/java/io/helidon/microprofile/config/ConfigCdiExtension.java index c5e2cd18b58..fdcc1070e00 100644 --- a/microprofile/config/src/main/java/io/helidon/microprofile/config/ConfigCdiExtension.java +++ b/microprofile/config/src/main/java/io/helidon/microprofile/config/ConfigCdiExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or 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,7 +26,6 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -38,6 +37,8 @@ import java.util.Set; import java.util.function.Supplier; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; @@ -62,19 +63,23 @@ import io.helidon.common.HelidonFeatures; import io.helidon.common.HelidonFlavor; import io.helidon.common.NativeImageHelper; -import io.helidon.config.Config; -import io.helidon.config.MissingValueException; +import io.helidon.config.mp.MpConfig; +import io.helidon.config.mp.MpConfigImpl; +import io.helidon.config.mp.MpConfigProviderResolver; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.ConfigSource; /** - * Extension to enable config injection in CDI container (all of {@link Config}, {@link org.eclipse.microprofile.config.Config} - * and {@link ConfigProperty}). + * Extension to enable config injection in CDI container (all of {@link io.helidon.config.Config}, + * {@link org.eclipse.microprofile.config.Config} and {@link ConfigProperty}). */ public class ConfigCdiExtension implements Extension { private static final Logger LOGGER = Logger.getLogger(ConfigCdiExtension.class.getName()); + private static final Pattern SPLIT_PATTERN = Pattern.compile("(? annotationType() { } private final List ips = new LinkedList<>(); - private Config runtimeHelidonConfig; /** * Constructor invoked by CDI container. @@ -174,10 +178,17 @@ private void registerConfigProducer(@Observes AfterBeanDiscovery abd) { .createWith(creationalContext -> new SerializableConfig()); abd.addBean() - .addTransitiveTypeClosure(Config.class) - .beanClass(Config.class) + .addTransitiveTypeClosure(io.helidon.config.Config.class) + .beanClass(io.helidon.config.Config.class) .scope(ApplicationScoped.class) - .createWith(creationalContext -> (Config) ConfigProvider.getConfig()); + .createWith(creationalContext -> { + Config config = ConfigProvider.getConfig(); + if (config instanceof io.helidon.config.Config) { + return config; + } else { + return MpConfig.toHelidonConfig(config); + } + }); Set types = ips.stream() .map(InjectionPoint::getType) @@ -233,7 +244,7 @@ private Object produce(InjectionPoint ip) { // this is build-time of native-image - e.g. run from command line or maven // logging may not be working/configured to deliver this message as it should System.err.println("You are accessing configuration key '" + configKey + "' during" - + " container initialization. This will not work nice with Graal native-image"); + + " container initialization. This will not work nicely with Graal native-image"); } Type type = ip.getType(); @@ -255,7 +266,11 @@ any java class (except for parameterized types) Map - a detached key/value mapping of whole subtree */ FieldTypes fieldTypes = FieldTypes.create(type); - Config config = (Config) ConfigProvider.getConfig(); + org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); + if (config instanceof MpConfigProviderResolver.ConfigDelegate) { + // get the actual instance to have access to Helidon specific methods + config = ((MpConfigProviderResolver.ConfigDelegate) config).delegate(); + } String defaultValue = defaultValue(annotation); Object value = configValue(config, fieldTypes, configKey, defaultValue); @@ -284,9 +299,31 @@ private Object configValue(Config config, FieldTypes fieldTypes, String configKe } private static T withDefault(Config config, String key, Class type, String defaultValue) { - return config.get(key) - .as(type) - .orElseGet(() -> (null == defaultValue) ? null : config.convert(type, defaultValue)); + return config.getOptionalValue(key, type) + .orElseGet(() -> convert(key, config, defaultValue, type)); + } + + @SuppressWarnings("unchecked") + private static T convert(String key, Config config, String value, Class type) { + if (null == value) { + return null; + } + if (String.class.equals(type)) { + return (T) value; + } + if (config instanceof MpConfigImpl) { + return ((MpConfigImpl) config).getConverter(type) + .orElseThrow(() -> new IllegalArgumentException("Did not find converter for type " + + type.getName() + + ", for key " + + key)) + .convert(value); + } + + throw new IllegalArgumentException("Helidon CDI MP Config implementation requires Helidon config instance. " + + "Current config is " + config.getClass().getName() + + ", which is not supported, as we cannot convert arbitrary String values"); + } private static Object parameterizedConfigValue(Config config, @@ -321,7 +358,13 @@ private static Object parameterizedConfigValue(Config config, typeArg2); } } else if (Map.class.isAssignableFrom(rawType)) { - return config.get(configKey).detach().asMap().get(); + Map result = new HashMap<>(); + config.getPropertyNames() + .forEach(name -> { + // workaround for race condition (if key disappears from source after we call getPropertyNames + config.getOptionalValue(name, String.class).ifPresent(value -> result.put(name, value)); + }); + return result; } else if (Set.class.isAssignableFrom(rawType)) { return new LinkedHashSet<>(asList(config, configKey, typeArg, defaultValue)); } else { @@ -330,32 +373,71 @@ private static Object parameterizedConfigValue(Config config, } } - private static List asList(Config config, String configKey, Class typeArg, String defaultValue) { - try { - return config.get(configKey).asList(typeArg).get(); - } catch (MissingValueException e) { - // if default - if (null == defaultValue) { - //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException - throw new NoSuchElementException("Missing list value for key " - + configKey - + ", original message: " - + e.getMessage()); - } else { - if (defaultValue.isEmpty()) { - return Collections.emptyList(); - } + static String[] toArray(String stringValue) { + String[] values = SPLIT_PATTERN.split(stringValue, -1); - List result = new LinkedList<>(); + for (int i = 0; i < values.length; i++) { + String value = values[i]; + values[i] = ESCAPED_COMMA_PATTERN.matcher(value).replaceAll(Matcher.quoteReplacement(",")); + } + return values; + } - String[] values = defaultValue.split(","); - for (String value : values) { - result.add(config.convert(typeArg, value)); - } + private static List asList(Config config, String configKey, Class typeArg, String defaultValue) { + // first try to see if we have a direct value + Optional optionalValue = config.getOptionalValue(configKey, String.class); + if (optionalValue.isPresent()) { + return toList(configKey, config, optionalValue.get(), typeArg); + } - return result; + /* + we also support indexed value + e.g. for key "my.list" you can have both: + my.list=12,13,14 + or (not and): + my.list.0=12 + my.list.1=13 + */ + + String indexedConfigKey = configKey + ".0"; + optionalValue = config.getOptionalValue(indexedConfigKey, String.class); + if (optionalValue.isPresent()) { + List result = new LinkedList<>(); + + // first element is already in + result.add(convert(indexedConfigKey, config, optionalValue.get(), typeArg)); + + // hardcoded limit to lists of 1000 elements + for (int i = 1; i < 1000; i++) { + indexedConfigKey = configKey + "." + i; + optionalValue = config.getOptionalValue(indexedConfigKey, String.class); + if (optionalValue.isPresent()) { + result.add(convert(indexedConfigKey, config, optionalValue.get(), typeArg)); + } else { + // finish the iteration on first missing index + break; + } } + return result; + } else { + if (null == defaultValue) { + throw new NoSuchElementException("Missing list value for key " + configKey); + } + + return toList(configKey, config, defaultValue, typeArg); + } + } + + private static List toList(String configKey, Config config, String stringValue, Class typeArg) { + if (stringValue.isEmpty()) { + return List.of(); + } + // we have a comma separated list + List result = new LinkedList<>(); + for (String value : toArray(stringValue)) { + result.add(convert(configKey, config, value, typeArg)); } + return result; } private String defaultValue(ConfigProperty annotation) { diff --git a/microprofile/config/src/main/java/io/helidon/microprofile/config/package-info.java b/microprofile/config/src/main/java/io/helidon/microprofile/config/package-info.java index 3012b0c9534..9c29b6f5e29 100644 --- a/microprofile/config/src/main/java/io/helidon/microprofile/config/package-info.java +++ b/microprofile/config/src/main/java/io/helidon/microprofile/config/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or 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 implementation of microprofile config. + * Helidon implementation of microprofile config for CDI. */ package io.helidon.microprofile.config; diff --git a/microprofile/config/src/main/java/module-info.java b/microprofile/config/src/main/java/module-info.java index 7ebf104d009..15ce3099d00 100644 --- a/microprofile/config/src/main/java/module-info.java +++ b/microprofile/config/src/main/java/module-info.java @@ -23,7 +23,10 @@ requires jakarta.inject.api; requires io.helidon.common; requires io.helidon.config; - requires microprofile.config.api; + requires transitive microprofile.config.api; + requires io.helidon.config.mp; + requires java.annotation; + requires io.helidon.common.serviceloader; exports io.helidon.microprofile.config; diff --git a/microprofile/config/src/test/java/io/helidon/microprofile/config/MutableMpTest.java b/microprofile/config/src/test/java/io/helidon/microprofile/config/MutableMpTest.java index fca589467ae..4cdc273bfa0 100644 --- a/microprofile/config/src/test/java/io/helidon/microprofile/config/MutableMpTest.java +++ b/microprofile/config/src/test/java/io/helidon/microprofile/config/MutableMpTest.java @@ -118,7 +118,9 @@ public String getName() { public void setValue(String value) { this.value.set(value); - listener.accept("value", value); + if (null != listener) { + listener.accept("value", value); + } } } } diff --git a/microprofile/config/src/test/resources/META-INF/microprofile-config.properties b/microprofile/config/src/test/resources/META-INF/microprofile-config.properties index 0dcc20a6d59..2fd4f3c5f50 100644 --- a/microprofile/config/src/test/resources/META-INF/microprofile-config.properties +++ b/microprofile/config/src/test/resources/META-INF/microprofile-config.properties @@ -14,6 +14,7 @@ # limitations under the License. # -# needed to run unit tests independently (without beans.xml) +# needed to run unit tests independently based on explicit beans using SeContainerInitializer mp.initializer.allow=true mp.initializer.warn=false + diff --git a/microprofile/fault-tolerance/pom.xml b/microprofile/fault-tolerance/pom.xml index 962e37df85f..da0caca3c2b 100644 --- a/microprofile/fault-tolerance/pom.xml +++ b/microprofile/fault-tolerance/pom.xml @@ -65,14 +65,7 @@ org.eclipse.microprofile.metrics microprofile-metrics-api - ${version.lib.microprofile-metrics-api} provided - - - org.osgi - org.osgi.annotation.versioning - - com.netflix.hystrix diff --git a/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java b/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java index c1529415f2a..349436fb6f8 100644 --- a/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java +++ b/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java @@ -881,6 +881,18 @@ public Builder config(Config config) { mpConfig.getOptionalValue(CONFIG_PUBLIC_KEY_PATH, String.class).ifPresent(this::publicKeyPath); mpConfig.getOptionalValue(CONFIG_EXPECTED_ISSUER, String.class).ifPresent(this::expectedIssuer); + if (null == publicKey && null == publicKeyPath) { + // this is a fix for incomplete TCK tests + // we will configure this location in our tck configuration + String key = "helidon.mp.jwt.verify.publickey.location"; + mpConfig.getOptionalValue(key, String.class).ifPresent(it -> { + publicKeyPath(it); + LOGGER.warning("You have configured public key for JWT-Auth provider using a property" + + " reserved for TCK tests (" + key + "). Please use " + + CONFIG_PUBLIC_KEY_PATH + " instead."); + }); + } + return this; } diff --git a/microprofile/messaging/src/main/java/io/helidon/microprofile/messaging/AdHocConfigBuilder.java b/microprofile/messaging/src/main/java/io/helidon/microprofile/messaging/AdHocConfigBuilder.java index 1681915fcc1..6ad32f3f53d 100644 --- a/microprofile/messaging/src/main/java/io/helidon/microprofile/messaging/AdHocConfigBuilder.java +++ b/microprofile/messaging/src/main/java/io/helidon/microprofile/messaging/AdHocConfigBuilder.java @@ -21,7 +21,9 @@ import java.util.Map; import io.helidon.config.Config; -import io.helidon.config.ConfigSources; +import io.helidon.config.mp.MpConfigSources; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; /** * Detached configuration of a single connector. @@ -49,13 +51,9 @@ AdHocConfigBuilder putAll(Config configToPut) { } org.eclipse.microprofile.config.Config build() { - Config newConfig = Config.builder(ConfigSources.create(configuration)) - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .disableFilterServices() - .disableSourceServices() - .disableParserServices() + return ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(configuration)) .build(); - return (org.eclipse.microprofile.config.Config) newConfig; } } diff --git a/microprofile/messaging/src/main/java/module-info.java b/microprofile/messaging/src/main/java/module-info.java index fc875b97ad9..c6e6189efad 100644 --- a/microprofile/messaging/src/main/java/module-info.java +++ b/microprofile/messaging/src/main/java/module-info.java @@ -25,6 +25,7 @@ requires static jakarta.activation; requires jakarta.interceptor.api; requires io.helidon.config; + requires io.helidon.config.mp; requires io.helidon.microprofile.config; requires io.helidon.microprofile.server; requires io.helidon.microprofile.reactive; diff --git a/microprofile/messaging/src/test/java/io/helidon/microprofile/messaging/AbstractCDITest.java b/microprofile/messaging/src/test/java/io/helidon/microprofile/messaging/AbstractCDITest.java index 49d38cc82fa..98c58ce4beb 100644 --- a/microprofile/messaging/src/test/java/io/helidon/microprofile/messaging/AbstractCDITest.java +++ b/microprofile/messaging/src/test/java/io/helidon/microprofile/messaging/AbstractCDITest.java @@ -16,8 +16,6 @@ package io.helidon.microprofile.messaging; -import java.io.IOException; -import java.io.InputStream; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collections; @@ -28,38 +26,24 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.logging.LogManager; import java.util.stream.Stream; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.se.SeContainerInitializer; import javax.enterprise.inject.spi.CDI; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.microprofile.server.Server; +import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.server.ServerCdiExtension; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -public abstract class AbstractCDITest { - - static Server singleServerReference; - - static { - try (InputStream is = AbstractCDITest.class.getResourceAsStream("/logging.properties")) { - LogManager.getLogManager().readConfiguration(is); - } catch (IOException e) { - fail(e); - } - } +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +public abstract class AbstractCDITest { protected SeContainer cdiContainer; protected Map cdiConfig() { @@ -81,7 +65,6 @@ public void setUp() { @AfterEach public void tearDown() { try { - singleServerReference.stop(); cdiContainer.close(); } catch (Throwable t) { //emergency cleanup see #1446 @@ -111,18 +94,13 @@ protected static SeContainer startCdiContainer(Map p, Class.. private static SeContainer startCdiContainer(Map p, Set> beanClasses) { p = new HashMap<>(p); p.put("mp.initializer.allow", "true"); - Config config = Config.builder() - .sources(ConfigSources.create(p)) + Config config = ConfigProviderResolver.instance().getBuilder() + .withSources(MpConfigSources.create(p)) .build(); - final Server.Builder builder = Server.builder(); - assertNotNull(builder); - builder.config(config); - singleServerReference = builder.build(); ConfigProviderResolver.instance() - .registerConfig((org.eclipse.microprofile.config.Config) config, Thread.currentThread().getContextClassLoader()); + .registerConfig(config, Thread.currentThread().getContextClassLoader()); final SeContainerInitializer initializer = SeContainerInitializer.newInstance(); - assertNotNull(initializer); initializer.addBeanClasses(beanClasses.toArray(new Class[0])); return initializer.initialize(); } diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index a329a4abc58..57d92b99377 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -32,10 +32,6 @@ Server of the microprofile implementation - - org.eclipse.microprofile.config - microprofile-config-api - io.helidon.microprofile.cdi helidon-microprofile-cdi @@ -74,7 +70,6 @@ io.helidon.microprofile.config helidon-microprofile-config - runtime diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java index b78d9dea45f..aef46e89588 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsCdiExtension.java @@ -104,9 +104,7 @@ public List applicationsToRun() throws IllegalStateException { if (applications.isEmpty() && applicationMetas.isEmpty()) { // create a synthetic application from all resource classes // the classes set must be created before the lambda, as resources are cleared later on - if (resources.isEmpty()) { - LOGGER.warning("There are no JAX-RS applications or resources. Maybe you forgot META-INF/beans.xml file?"); - } else { + if (!resources.isEmpty()) { Set> classes = new HashSet<>(resources); applicationMetas.add(JaxRsApplication.builder() .synthetic(true) diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java index d17f689cda1..4cfea7e8918 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/Server.java @@ -30,6 +30,8 @@ import io.helidon.common.configurable.ServerThreadPoolSupplier; import io.helidon.common.context.Contexts; +import io.helidon.config.mp.MpConfig; +import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.cdi.HelidonContainer; import org.eclipse.microprofile.config.Config; @@ -207,7 +209,7 @@ private Server doBuild() { if (null == defaultExecutorService) { defaultExecutorService = ServerThreadPoolSupplier.builder() .name("server") - .config(((io.helidon.config.Config) config) + .config(MpConfig.toHelidonConfig(config) .get("server.executor-service")) .build(); } @@ -317,7 +319,11 @@ public Builder port(int port) { * @return modified builder */ public Builder config(io.helidon.config.Config config) { - this.config = (Config) config; + this.config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(config)) + .build(); + return this; } diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index 1cb357e3d45..150b302c577 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -159,8 +159,12 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( private void registerJaxRsApplications(BeanManager beanManager, ServerConfiguration serverConfig) { JaxRsCdiExtension jaxRs = beanManager.getExtension(JaxRsCdiExtension.class); - jaxRs.applicationsToRun() - .forEach(it -> addApplication(serverConfig, jaxRs, it)); + List jaxRsApplications = jaxRs.applicationsToRun(); + if (jaxRsApplications.isEmpty()) { + LOGGER.warning("There are no JAX-RS applications or resources. Maybe you forgot META-INF/beans.xml file?"); + } else { + jaxRsApplications.forEach(it -> addApplication(serverConfig, jaxRs, it)); + } } private void registerDefaultRedirect() { @@ -256,7 +260,7 @@ private void addApplication(ServerConfiguration serverConfig, JaxRsCdiExtension } Routing.Builder routing = routingBuilder(namedRouting, routingNameRequired, serverConfig, - applicationMeta.appName()); + applicationMeta.appName()); JerseySupport jerseySupport = jaxRs.toJerseySupport(jaxRsExecutorService, applicationMeta); if (contextRoot.isPresent()) { @@ -281,19 +285,19 @@ private void addApplication(ServerConfiguration serverConfig, JaxRsCdiExtension */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public Routing.Builder routingBuilder(Optional namedRouting, boolean routingNameRequired, - ServerConfiguration serverConfig, String appName) { + ServerConfiguration serverConfig, String appName) { if (namedRouting.isPresent()) { String socket = namedRouting.get(); if (null == serverConfig.socket(socket)) { if (routingNameRequired) { throw new IllegalStateException("Application " - + appName - + " requires routing " - + socket - + " to exist, yet such a socket is not configured for web server"); + + appName + + " requires routing " + + socket + + " to exist, yet such a socket is not configured for web server"); } else { LOGGER.info("Routing " + socket + " does not exist, using default routing for application " - + appName); + + appName); return serverRoutingBuilder(); } @@ -319,7 +323,6 @@ private void registerWebServerServices(BeanManager beanManager, } } - private static List> prioritySort(Set> beans) { List> prioritized = new ArrayList<>(beans); prioritized.sort((o1, o2) -> { diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index 52bbffc4696..5158ce58fb9 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -26,6 +26,8 @@ requires transitive io.helidon.microprofile.cdi; + requires io.helidon.config.mp; + requires io.helidon.microprofile.config; requires transitive jakarta.enterprise.cdi.api; requires transitive java.ws.rs; requires jakarta.interceptor.api; @@ -35,6 +37,7 @@ // there is now a hardcoded dependency on Weld, to configure additional bean defining annotation requires java.management; + requires microprofile.config.api; exports io.helidon.microprofile.server; diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java index 1f8466065fd..877d933cf6c 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonContainerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2020 Oracle and/or 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,11 +35,8 @@ */ public class HelidonContainerConfiguration implements ContainerConfiguration { private String appClassName = null; - private String resourceClassName = null; private int port = 8080; private boolean deleteTmp = true; - private boolean addResourcesToApps = false; - private boolean replaceConfigSourcesWithMp = false; private boolean useRelativePath = false; public String getApp() { @@ -50,14 +47,6 @@ public void setApp(String app) { this.appClassName = app; } - public String getResource() { - return resourceClassName; - } - - public void setResource(String resource) { - this.resourceClassName = resource; - } - public int getPort() { return port; } @@ -82,22 +71,6 @@ public void setUseRelativePath(boolean b) { this.useRelativePath = b; } - public boolean getAddResourcesToApps() { - return addResourcesToApps; - } - - public void setAddResourcesToApps(boolean addResourcesToApps) { - this.addResourcesToApps = addResourcesToApps; - } - - public void setReplaceConfigSourcesWithMp(boolean replaceConfigSourcesWithMp) { - this.replaceConfigSourcesWithMp = replaceConfigSourcesWithMp; - } - - public boolean getReplaceConfigSourcesWithMp() { - return replaceConfigSourcesWithMp; - } - @Override public void validate() throws ConfigurationException { if ((port <= 0) || (port > Short.MAX_VALUE)) { diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java index 038be3fde32..81b8767c8ab 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java @@ -22,6 +22,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -29,17 +31,11 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Properties; -import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -47,11 +43,13 @@ import javax.enterprise.inject.spi.CDI; import javax.enterprise.inject.spi.DefinitionException; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.config.spi.ConfigSource; +import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.server.Server; +import io.helidon.microprofile.server.ServerCdiExtension; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; import org.jboss.arquillian.container.spi.client.container.DeploymentException; import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; @@ -82,6 +80,8 @@ */ public class HelidonDeployableContainer implements DeployableContainer { private static final Logger LOGGER = Logger.getLogger(HelidonDeployableContainer.class.getName()); + // runnables that must be executed on stop + private static final ConcurrentLinkedQueue STOP_RUNNABLES = new ConcurrentLinkedQueue<>(); /** * The configuration for this container. @@ -93,7 +93,6 @@ public class HelidonDeployableContainer implements DeployableContainer contexts = new HashMap<>(); - private static ConcurrentLinkedQueue stopCalls = new ConcurrentLinkedQueue<>(); private Server dummyServer = null; @Override @@ -108,18 +107,29 @@ public void setup(HelidonContainerConfiguration configuration) { @Override public void start() { - dummyServer = Server.builder().build(); + try { + if (!CDI.current() + .getBeanManager() + .getExtension(ServerCdiExtension.class) + .started()) { + dummyServer = Server.builder().build(); + } + } catch (IllegalStateException e) { + // CDI not running + dummyServer = Server.builder().build(); + } } @Override public void stop() { - // No-op + if (null != dummyServer) { + dummyServer.stop(); + } } @Override public ProtocolDescription getDefaultProtocol() { return new ProtocolDescription(HelidonLocalProtocol.PROTOCOL_NAME); - // return new ProtocolDescription(LocalProtocol.NAME); } @Override @@ -140,53 +150,30 @@ public ProtocolMetaData deploy(Archive archive) throws DeploymentException { } LOGGER.info("Running Arquillian tests in directory: " + context.deployDir.toAbsolutePath()); - // Copy the archive into deployDir. Save off the class names for all classes included in the - // "classes" dir. Later I will visit each of these and see if they are JAX-RS Resources or - // Applications, so I can add those to the Server automatically. - final Set classNames = new TreeSet<>(); - copyArchiveToDeployDir(archive, context.deployDir, p -> { - if (p.endsWith(".class")) { - final int prefixLength = isJavaArchive ? 1 : "/WEB-INF/classes/".length(); - classNames.add(p.substring(prefixLength, p.lastIndexOf(".class")).replace('/', '.')); - } - }); + copyArchiveToDeployDir(archive, context.deployDir); - // If the configuration specified a Resource to load, add that to the set of class names - if (containerConfig.getResource() != null) { - classNames.add(containerConfig.getResource()); - } - - // If the configuration specified an Application to load, add that to the set of class names. - // The "Main" method (see Main.template) will go through all these classes and discover whether - // they are apps or resources and call the right builder methods on the Server.Builder. - if (containerConfig.getApp() != null) { - classNames.add(containerConfig.getApp()); - } - - URL[] classPath; + List classPath = new ArrayList<>(); Path rootDir = context.deployDir.resolve(""); if (isJavaArchive) { ensureBeansXml(rootDir); - classPath = new URL[] { - rootDir.toUri().toURL() - }; + classPath.add(rootDir); } else { // Prepare the launcher files Path webInfDir = context.deployDir.resolve("WEB-INF"); Path classesDir = webInfDir.resolve("classes"); Path libDir = webInfDir.resolve("lib"); ensureBeansXml(classesDir); - classPath = getServerClasspath(classesDir, libDir, rootDir); + addServerClasspath(classPath, classesDir, libDir, rootDir); } - startServer(context, classPath, classNames); + startServer(context, classPath.toArray(new Path[0])); } catch (IOException e) { LOGGER.log(Level.INFO, "Failed to start container", e); throw new DeploymentException("Failed to copy the archive assets into the deployment directory", e); } catch (ReflectiveOperationException e) { LOGGER.log(Level.INFO, "Failed to start container", e); - throw new DefinitionException(e.getCause()); // validation exceptions + throw new DefinitionException(e); // validation exceptions } // Server has started, so we're done. @@ -196,50 +183,18 @@ public ProtocolMetaData deploy(Archive archive) throws DeploymentException { return new ProtocolMetaData(); } - void startServer(RunContext context, URL[] classPath, Set classNames) + void startServer(RunContext context, Path[] classPath) throws ReflectiveOperationException { Optional.ofNullable((SeContainer) CDI.current()) .ifPresent(SeContainer::close); stopAll(); - context.classLoader = new MyClassloader(new URLClassLoader(classPath)); + context.classLoader = new MyClassloader(new URLClassLoader(toUrls(classPath))); context.oldClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(context.classLoader); - List> configSources = new LinkedList<>(); - configSources.add(ConfigSources.file(context.deployDir.resolve("META-INF/microprofile-config.properties").toString()) - .optional()); - // The following line supports MP OpenAPI, which allows an alternate - // location for the config file. - configSources.add(ConfigSources.file( - context.deployDir.resolve("WEB-INF/classes/META-INF/microprofile-config.properties").toString()) - .optional()); - configSources.add(ConfigSources.file(context.deployDir.resolve("arquillian.properties").toString()).optional()); - configSources.add(ConfigSources.file(context.deployDir.resolve("application.properties").toString()).optional()); - configSources.add(ConfigSources.file(context.deployDir.resolve("application.yaml").toString()).optional()); - configSources.add(ConfigSources.classpath("tck-application.yaml").optional()); - - // workaround for tck-fault-tolerance - if (containerConfig.getReplaceConfigSourcesWithMp()) { - URL mpConfigProps = context.classLoader.getResource("META-INF/microprofile-config.properties"); - if (mpConfigProps != null) { - try { - Properties props = new Properties(); - props.load(mpConfigProps.openStream()); - configSources.clear(); - configSources.add(ConfigSources.create(props)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - Config config = Config.builder() - .sources(configSources) - .build(); - context.runnerClass = context.classLoader .loadClass("io.helidon.microprofile.arquillian.ServerRunner"); @@ -247,7 +202,7 @@ void startServer(RunContext context, URL[] classPath, Set classNames) .getDeclaredConstructor() .newInstance(); - stopCalls.add(() -> { + STOP_RUNNABLES.add(() -> { try { context.runnerClass.getDeclaredMethod("stop").invoke(context.runner); } catch (ReflectiveOperationException e) { @@ -255,34 +210,79 @@ void startServer(RunContext context, URL[] classPath, Set classNames) } }); + // Configuration needs to be explicit, as some TCK libraries contain an unfortunate + // META-INF/microprofile-config.properties (such as JWT-Auth) + Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(findMpConfigSources(classPath)) + .addDiscoveredConverters() + // will read application.yaml + .addDiscoveredSources() + .build(); + context.runnerClass - .getDeclaredMethod("start", Config.class, HelidonContainerConfiguration.class, Set.class, ClassLoader.class) - .invoke(context.runner, config, containerConfig, classNames, context.classLoader); + .getDeclaredMethod("start", Config.class, Integer.TYPE) + .invoke(context.runner, config, containerConfig.getPort()); } - URL[] getServerClasspath(Path classesDir, Path libDir, Path rootDir) throws IOException { - List urls = new ArrayList<>(); + private URL[] toUrls(Path[] classPath) { + List result = new ArrayList<>(); + for (Path path : classPath) { + try { + result.add(path.toUri().toURL()); + } catch (MalformedURLException e) { + throw new IllegalStateException("Classpath failed to be constructed for path: " + path); + } + } + + return result.toArray(new URL[0]); + } + + private ConfigSource[] findMpConfigSources(Path[] classPath) { + String location = "META-INF/microprofile-config.properties"; + List sources = new ArrayList<>(5); + + for (Path path : classPath) { + if (Files.isDirectory(path)) { + Path mpConfig = path.resolve(location); + if (Files.exists(mpConfig)) { + sources.add(MpConfigSources.create(mpConfig)); + } + } else { + // this must be a jar file (classpath is either jar file or a directory) + FileSystem fs; + try { + fs = FileSystems.newFileSystem(path, Thread.currentThread().getContextClassLoader()); + Path mpConfig = fs.getPath(location); + if (Files.exists(mpConfig)) { + sources.add(MpConfigSources.create(path + "!" + mpConfig, mpConfig)); + } + } catch (IOException e) { + // ignored + } + } + } + + // add the expected sysprops and env vars + sources.add(MpConfigSources.environmentVariables()); + sources.add(MpConfigSources.systemProperties()); + + return sources.toArray(new ConfigSource[0]); + } + + void addServerClasspath(List classpath, Path classesDir, Path libDir, Path rootDir) throws IOException { // classes directory - urls.add(classesDir.toUri().toURL()); + classpath.add(classesDir); // lib directory - need to find each jar file if (Files.exists(libDir)) { Files.list(libDir) .filter(path -> path.getFileName().toString().endsWith(".jar")) - .forEach(path -> { - try { - urls.add(path.toUri().toURL()); - } catch (MalformedURLException e) { - throw new HelidonArquillianException("Failed to get URL from library on path: " - + path.toAbsolutePath(), e); - } - }); + .forEach(classpath::add); } - urls.add(rootDir.toUri().toURL()); - - return urls.toArray(new URL[0]); + classpath.add(rootDir); } private void ensureBeansXml(Path classesDir) throws IOException { @@ -302,7 +302,6 @@ private void ensureBeansXml(Path classesDir) throws IOException { Files.copy(beanXmlTemplate, beansPath); } - } } @@ -349,10 +348,10 @@ public void undeploy(Archive archive) { } void stopAll() { - Runnable polled = stopCalls.poll(); + Runnable polled = STOP_RUNNABLES.poll(); while (Objects.nonNull(polled)) { polled.run(); - polled = stopCalls.poll(); + polled = STOP_RUNNABLES.poll(); } dummyServer.stop(); } @@ -372,11 +371,10 @@ public void undeploy(Descriptor descriptor) { * * @param archive The archive to deploy. This cannot be null. * @param deployDir The directory to deploy to. This cannot be null. - * @param callback The callback to invoke per item. This can be null. * @throws IOException if there is an I/O failure related to copying the archive assets to the deployment * directory. If this happens, the entire setup is bad and must be terminated. */ - private void copyArchiveToDeployDir(Archive archive, Path deployDir, Consumer callback) throws IOException { + private void copyArchiveToDeployDir(Archive archive, Path deployDir) throws IOException { Map archiveContents = archive.getContent(); for (Map.Entry entry : archiveContents.entrySet()) { ArchivePath path = entry.getKey(); @@ -393,11 +391,6 @@ private void copyArchiveToDeployDir(Archive archive, Path deployDir, Consumer } // Copy the asset to the destination location Files.copy(n.getAsset().openStream(), f, StandardCopyOption.REPLACE_EXISTING); - // Invoke the callback if one was registered - String p = n.getPath().get(); - if (callback != null) { - callback.accept(p); - } } } } @@ -438,8 +431,9 @@ static class MyClassloader extends ClassLoader implements Closeable { public InputStream getResourceAsStream(String name) { InputStream stream = wrapped.getResourceAsStream(name); if ((null == stream) && name.startsWith("/")) { - return wrapped.getResourceAsStream(name.substring(1)); + stream = wrapped.getResourceAsStream(name.substring(1)); } + return stream; } diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/ServerRunner.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/ServerRunner.java index 83a5c5aa5a2..6b96ae39831 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/ServerRunner.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/ServerRunner.java @@ -16,32 +16,31 @@ package io.helidon.microprofile.arquillian; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; import java.util.logging.Logger; import javax.enterprise.inject.se.SeContainer; import javax.enterprise.inject.spi.CDI; import javax.ws.rs.ApplicationPath; -import javax.ws.rs.Path; -import javax.ws.rs.core.Application; -import io.helidon.config.Config; import io.helidon.microprofile.server.Server; import io.helidon.microprofile.server.ServerCdiExtension; -import org.glassfish.jersey.server.ResourceConfig; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; /** * Runner to start server using reflection (as we need to run in a different classloader). + * As we invoke this from a different classloader, the class must be public. */ -class ServerRunner { +public class ServerRunner { private static final Logger LOGGER = Logger.getLogger(ServerRunner.class.getName()); private Server server; - ServerRunner() { + /** + * Needed for reflection. + */ + public ServerRunner() { } private static String getContextRoot(Class application) { @@ -53,22 +52,34 @@ private static String getContextRoot(Class application) { return value.startsWith("/") ? value : "/" + value; } - void start(Config config, HelidonContainerConfiguration containerConfig, Set classNames, ClassLoader cl) { - Server.Builder builder = Server.builder() - .port(containerConfig.getPort()) - .config(config); + /** + * Start the server. Needed for reflection. + * + * @param config configuration + * @param port port to start the server on + */ + public void start(Config config, int port) { + // attempt a stop + stop(); + + ConfigProviderResolver.instance() + .registerConfig(config, Thread.currentThread().getContextClassLoader()); + + server = Server.builder() + .port(port) + .config(config) + .build() + // this is a blocking operation, we will be released once the server is started + // or it fails to start + .start(); - - handleClasses(cl, classNames, builder, containerConfig.getAddResourcesToApps()); - - server = builder.build(); - // this is a blocking operation, we will be released once the server is started - // or it fails to start - server.start(); LOGGER.finest(() -> "Started server"); } - void stop() { + /** + * Stop the server. Needed for reflection. + */ + public void stop() { if (null != server) { LOGGER.finest(() -> "Stopping server"); server.stop(); @@ -92,47 +103,4 @@ private static void stopCdiContainer() { //noop container is not running } } - - @SuppressWarnings("unchecked") - private void handleClasses(ClassLoader classLoader, - Set classNames, - Server.Builder builder, - boolean addResourcesToApps) { - - // first create classes end get all applications - List> applicationClasses = new LinkedList<>(); - List> resourceClasses = new LinkedList<>(); - - for (String className : classNames) { - try { - LOGGER.finest(() -> "Will attempt to add class: " + className); - final Class c = classLoader.loadClass(className); - if (Application.class.isAssignableFrom(c)) { - LOGGER.finest(() -> "Adding application class: " + c.getName()); - applicationClasses.add(c); - } else if (c.isAnnotationPresent(Path.class) && !c.isInterface()) { - LOGGER.finest(() -> "Adding resource class: " + c.getName()); - resourceClasses.add(c); - } else { - LOGGER.finest(() -> "Class " + c.getName() + " is neither annotated with Path nor an application."); - } - } catch (NoClassDefFoundError | ClassNotFoundException e) { - throw new HelidonArquillianException("Failed to load class to be added to server: " + className, e); - } - } - - // workaround for tck-jwt-auth - if (addResourcesToApps) { - for (Class aClass : applicationClasses) { - ResourceConfig resourceConfig = ResourceConfig.forApplicationClass((Class) aClass); - resourceClasses.forEach(resourceConfig::register); - builder.addApplication(getContextRoot(aClass), resourceConfig); - } - if (applicationClasses.isEmpty()) { - for (Class resourceClass : resourceClasses) { - builder.addResourceClass(resourceClass); - } - } - } - } } diff --git a/microprofile/tests/tck/tck-health/src/test/resources/tck-application.yaml b/microprofile/tests/tck/tck-health/src/test/resources/application.yaml similarity index 100% rename from microprofile/tests/tck/tck-health/src/test/resources/tck-application.yaml rename to microprofile/tests/tck/tck-health/src/test/resources/application.yaml diff --git a/microprofile/tests/tck/tck-jwt-auth/src/test/resources/application.yaml b/microprofile/tests/tck/tck-jwt-auth/src/test/resources/application.yaml new file mode 100644 index 00000000000..88d3ffb3851 --- /dev/null +++ b/microprofile/tests/tck/tck-jwt-auth/src/test/resources/application.yaml @@ -0,0 +1,4 @@ +# Configure this source as the least important one +config_ordinal: 0 +mp.jwt.verify.issuer: "https://server.example.com" +helidon.mp.jwt.verify.publickey.location: "/publicKey.pem" \ No newline at end of file diff --git a/microprofile/tests/tck/tck-jwt-auth/src/test/resources/arquillian.xml b/microprofile/tests/tck/tck-jwt-auth/src/test/resources/arquillian.xml index 85e4f16f094..be0905730f5 100644 --- a/microprofile/tests/tck/tck-jwt-auth/src/test/resources/arquillian.xml +++ b/microprofile/tests/tck/tck-jwt-auth/src/test/resources/arquillian.xml @@ -26,10 +26,4 @@ target/deployments 8080 - - - - true - - \ No newline at end of file diff --git a/microprofile/tests/tck/tck-messaging/src/test/resources/arquillian.xml b/microprofile/tests/tck/tck-messaging/src/test/resources/arquillian.xml index dd777b90178..e7e46418d50 100644 --- a/microprofile/tests/tck/tck-messaging/src/test/resources/arquillian.xml +++ b/microprofile/tests/tck/tck-messaging/src/test/resources/arquillian.xml @@ -18,13 +18,12 @@ --> target/deployments - -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=y - + diff --git a/microprofile/tests/tck/tck-metrics/pom.xml b/microprofile/tests/tck/tck-metrics/pom.xml index 3caf6fbd653..54777b91442 100644 --- a/microprofile/tests/tck/tck-metrics/pom.xml +++ b/microprofile/tests/tck/tck-metrics/pom.xml @@ -49,26 +49,12 @@ org.eclipse.microprofile.metrics microprofile-metrics-rest-tck - ${version.lib.microprofile-metrics-api} test - - - org.jboss.arquillian.junit - arquillian-junit-container - - org.eclipse.microprofile.metrics microprofile-metrics-api-tck - ${version.lib.microprofile-metrics-api} test - - - org.jboss.arquillian.junit - arquillian-junit-container - - org.hamcrest diff --git a/microprofile/tests/tck/tck-opentracing/src/test/resources/tck-application.yaml b/microprofile/tests/tck/tck-opentracing/src/test/resources/application.yaml similarity index 100% rename from microprofile/tests/tck/tck-opentracing/src/test/resources/tck-application.yaml rename to microprofile/tests/tck/tck-opentracing/src/test/resources/application.yaml diff --git a/microprofile/tests/tck/tck-opentracing/src/test/resources/arquillian.xml b/microprofile/tests/tck/tck-opentracing/src/test/resources/arquillian.xml index 6c1253f8261..d5bf34f9c31 100644 --- a/microprofile/tests/tck/tck-opentracing/src/test/resources/arquillian.xml +++ b/microprofile/tests/tck/tck-opentracing/src/test/resources/arquillian.xml @@ -26,10 +26,4 @@ target/deployments 8080 - - - - true - - \ No newline at end of file diff --git a/microprofile/tests/tck/tck-rest-client/pom.xml b/microprofile/tests/tck/tck-rest-client/pom.xml index f8465a329f8..5f83aea1c34 100644 --- a/microprofile/tests/tck/tck-rest-client/pom.xml +++ b/microprofile/tests/tck/tck-rest-client/pom.xml @@ -34,13 +34,6 @@ helidon-arquillian ${project.version} test - - - - jakarta.json.bind - jakarta.json.bind-api - - org.eclipse.microprofile.rest.client diff --git a/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/RestClientExtension.java b/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/RestClientExtension.java new file mode 100644 index 00000000000..ce89fc491b3 --- /dev/null +++ b/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/RestClientExtension.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018, 2020 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.restclient.tck; + +import org.jboss.arquillian.container.test.impl.enricher.resource.URLResourceProvider; +import org.jboss.arquillian.core.spi.LoadableExtension; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; + +/** + * LoadableExtension that will load the HealthResourceProvider. + */ +public class RestClientExtension implements LoadableExtension { + @Override + public void register(ExtensionBuilder extensionBuilder) { + extensionBuilder.override(ResourceProvider.class, URLResourceProvider.class, UrlResourceProvider.class); + } +} diff --git a/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/UrlResourceProvider.java b/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/UrlResourceProvider.java new file mode 100644 index 00000000000..55e306aff5b --- /dev/null +++ b/microprofile/tests/tck/tck-rest-client/src/test/java/io/helidon/microprofile/restclient/tck/UrlResourceProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.restclient.tck; + +import java.lang.annotation.Annotation; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.test.spi.enricher.resource.ResourceProvider; + +/** + * TCKs use addition when creating URL for a client. The default Arquillian implementation returns url without the trailing + * /. + */ +public class UrlResourceProvider implements ResourceProvider { + @Override + public Object lookup(ArquillianResource arquillianResource, Annotation... annotations) { + try { + return URI.create("http://localhost:8080/").toURL(); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public boolean canProvide(Class type) { + return URL.class.isAssignableFrom(type); + } +} diff --git a/microprofile/tests/tck/tck-rest-client/src/test/resources/arquillian.xml b/microprofile/tests/tck/tck-rest-client/src/test/resources/arquillian.xml index d37c6decc55..696fee786d4 100644 --- a/microprofile/tests/tck/tck-rest-client/src/test/resources/arquillian.xml +++ b/microprofile/tests/tck/tck-rest-client/src/test/resources/arquillian.xml @@ -27,8 +27,6 @@ - false - true false diff --git a/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaCdiExtensionTest.java b/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaCdiExtensionTest.java index ed9393d6c48..1108260e7c8 100644 --- a/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaCdiExtensionTest.java +++ b/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaCdiExtensionTest.java @@ -17,14 +17,6 @@ package io.helidon.messaging.connectors.kafka; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import com.salesforce.kafka.test.junit5.SharedKafkaTestResource; - import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; @@ -49,9 +41,11 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; -import io.helidon.config.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfigProviderResolver; +import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.messaging.MessagingCdiExtension; +import com.salesforce.kafka.test.junit5.SharedKafkaTestResource; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; @@ -59,6 +53,7 @@ import org.apache.kafka.common.serialization.LongSerializer; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.eclipse.microprofile.reactive.messaging.spi.Connector; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -66,6 +61,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + class KafkaCdiExtensionTest { private static final Logger LOGGER = Logger.getLogger(KafkaCdiExtensionTest.class.getName()); @@ -481,11 +482,13 @@ private Instance getInstance(Class beanType, Annotation annotation){ private SeContainer startCdiContainer(Map p, Set> beanClasses) { p.put("mp.initializer.allow", "true"); - Config config = Config.builder() - .sources(ConfigSources.create(p)) + org.eclipse.microprofile.config.Config config = ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(p)) .build(); + MpConfigProviderResolver.instance() - .registerConfig((org.eclipse.microprofile.config.Config) config, + .registerConfig(config, Thread.currentThread().getContextClassLoader()); final SeContainerInitializer initializer = SeContainerInitializer.newInstance(); assertNotNull(initializer); diff --git a/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java b/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java index 4adec2d10e1..a3462f780b6 100644 --- a/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java +++ b/tests/integration/native-image/mp-1/src/main/java/io/helidon/tests/integration/nativeimage/mp1/Mp1Main.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; +import java.util.ArrayList; import java.util.Base64; import java.util.LinkedList; import java.util.List; @@ -50,6 +51,8 @@ import io.opentracing.Tracer; import io.opentracing.util.GlobalTracer; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException; import static io.helidon.common.http.Http.Status.FORBIDDEN_403; @@ -106,19 +109,23 @@ public static void main(final String[] args) { long time = System.currentTimeMillis() - now; System.out.println("Tests finished in " + time + " millis"); + Config config = ConfigProvider.getConfig(); + List names = new ArrayList<>(); + config.getPropertyNames() + .forEach(names::add); + names.sort(String::compareTo); + + System.out.println("All configuration options:"); + names.forEach(it -> { + config.getOptionalValue(it, String.class) + .ifPresent(value -> System.out.println(it + "=" + value)); + }); + server.stop(); if (failed) { System.exit(-1); } - - -// try { -// Thread.sleep(5000); -// System.setProperty("app.message", "New message through change support"); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// } } private static String generateJwtToken() { 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 5c97c50d350..1a093c0a84d 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 @@ -31,6 +31,7 @@ requires microprofile.rest.client.api; requires microprofile.metrics.api; requires java.json.bind; + requires microprofile.config.api; exports io.helidon.tests.integration.nativeimage.mp1; diff --git a/microprofile/tests/tck/tck-jwt-auth/src/test/resources/tck-application.yaml b/tests/integration/security/gh1487/src/main/resources/logging.properties similarity index 58% rename from microprofile/tests/tck/tck-jwt-auth/src/test/resources/tck-application.yaml rename to tests/integration/security/gh1487/src/main/resources/logging.properties index 637a0afbc9e..9d84702edcc 100644 --- a/microprofile/tests/tck/tck-jwt-auth/src/test/resources/tck-application.yaml +++ b/tests/integration/security/gh1487/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2020 Oracle and/or 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,10 @@ # limitations under the License. # -mp: - jwt: - verify: - issuer: "https://server.example.com" +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler -security: - providers: - - mp-jwt-auth: - atn-token: - # must use helidon specific configuration, as otherwise MP validation could fail if public key location is defined - # as well - verify-key: "/publicKey.pem" - - abac: \ No newline at end of file +# Global logging level. Can be overridden by specific loggers +.level=WARNING + +AUDIT.level=FINEST diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/NettyWebServer.java b/webserver/webserver/src/main/java/io/helidon/webserver/NettyWebServer.java index 15739bb6d7d..5a19a50b266 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/NettyWebServer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/NettyWebServer.java @@ -16,6 +16,7 @@ package io.helidon.webserver; +import java.net.BindException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.HashMap; @@ -214,8 +215,16 @@ public synchronized CompletionStage start() { if (!channelFuture.isSuccess()) { LOGGER.info(() -> "Channel '" + name + "' startup failed with message '" + channelFuture.cause().getMessage() + "'."); - channelsUpFuture.completeExceptionally(new IllegalStateException("Channel startup failed: " + name, + Throwable cause = channelFuture.cause(); + + String message = "Channel startup failed: " + name; + if (cause instanceof BindException) { + message = message + ", failed to listen on " + configuration.bindAddress() + ":" + port; + } + + channelsUpFuture.completeExceptionally(new IllegalStateException(message, channelFuture.cause())); + return; } @@ -274,7 +283,7 @@ public synchronized CompletionStage start() { private void started(WebServer server) { if (EXIT_ON_STARTED) { - LOGGER.info(String.format("Exiting, -D%s set.", EXIT_ON_STARTED_KEY)); + LOGGER.info(String.format("Exiting, -D%s set.", EXIT_ON_STARTED_KEY)); System.exit(0); } else { startFuture.complete(server); @@ -330,7 +339,7 @@ private CompletionStage shutdownThreadGroups() { } else { StringBuilder sb = new StringBuilder(); sb.append(workerFuture.cause() != null ? "Worker Group problem: " + workerFuture.cause().getMessage() : "") - .append(bossFuture.cause() != null ? "Boss Group problem: " + bossFuture.cause().getMessage() : ""); + .append(bossFuture.cause() != null ? "Boss Group problem: " + bossFuture.cause().getMessage() : ""); threadGroupsShutdownFuture .completeExceptionally(new IllegalStateException("Unable to shutdown Netty thread groups: " + sb)); }