MP Config fixes (#1721)

MP Config implementation to correctly support mutable config sources.
This commit is contained in:
Tomas Langer
2020-05-06 11:44:31 +02:00
committed by GitHub
parent b842c911ba
commit 7d2f70897f
122 changed files with 3576 additions and 1822 deletions

View File

@@ -248,6 +248,11 @@
<artifactId>helidon-config-object-mapping</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-mp</artifactId>
<version>${helidon.version}</version>
</dependency>
<!-- security -->
<dependency>
<groupId>io.helidon.security</groupId>

View File

@@ -39,14 +39,7 @@
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -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 final class Priorities {
* @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 <S> 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 <S> Comparator<S> priorityComparator(int defaultPriority) {
return Comparator.comparingInt(it -> {
if (it instanceof Class) {
return find((Class<?>) it, defaultPriority);
} else {
return find(it, defaultPriority);
}
}));
});
}
}

View File

@@ -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<Service> 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<Service> 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<Service> 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;
}
}
}

View File

@@ -16,33 +16,40 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.config.tests</groupId>
<artifactId>helidon-config-tests-project</artifactId>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-project</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>helidon-config-test-mp-reference</artifactId>
<name>Helidon Config Tests MP Reference</name>
<description>
Integration tests of reference in MP
</description>
<artifactId>helidon-config-mp</artifactId>
<name>Helidon Config MP</name>
<description>Core of the implementation of MicroProfile Config specification</description>
<dependencies>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-service-loader</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>

View File

@@ -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.
* <p>
* 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<Object> cl = (Class<Object>) clazz;
builder.addStringMapper(cl, converter::convert);
});
}
Map<String, String> 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<String> optionalValue = mpConfig.getOptionalValue(it, String.class);
optionalValue.ifPresent(value -> allConfig.put(it, value));
});
return builder.addSource(ConfigSources.create(allConfig))
.build();
}
}

View File

@@ -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<OrdinalSource> sources = new LinkedList<>();
private final List<OrdinalConverter> 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 <T> ConfigBuilder withConverter(Class<T> aClass, int ordinal, Converter<T> 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<ConfigSource> sources = new LinkedList<>();
HashMap<Class<?>, Converter<?>> converters = new HashMap<>();
this.sources.forEach(ordinal -> sources.add(ordinal.source));
this.converters.forEach(ordinal -> converters.putIfAbsent(ordinal.type, ordinal.converter));
List<MpConfigFilter> filters = HelidonServiceLoader.create(ServiceLoader.load(MpConfigFilter.class))
.asList();
return new MpConfigImpl(sources, converters, filters);
}
private <T> void addBuiltIn(List<OrdinalConverter> converters, Class<T> clazz, Converter<T> 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());
}
}

View File

@@ -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 = "(?<!\\\\)\\$\\{([^}]+)\\}";
private static final Pattern PATTERN_REFERENCE = Pattern.compile(REGEX_REFERENCE);
// for encoding backslashes
// matches a backslash with a positive lookahead if it is the backslash that encodes ${}
private static final String REGEX_BACKSLASH = "\\\\(?=\\$\\{([^}]+)\\})";
private static final Pattern PATTERN_BACKSLASH = Pattern.compile(REGEX_BACKSLASH);
// I only care about unresolved key happening within the same thread
private static final ThreadLocal<Set<String>> UNRESOLVED_KEYS = ThreadLocal.withInitial(HashSet::new);
private static final Pattern SPLIT_PATTERN = Pattern.compile("(?<!\\\\),");
private static final Pattern ESCAPED_COMMA_PATTERN = Pattern.compile("\\,", Pattern.LITERAL);
private final List<ConfigSource> sources = new LinkedList<>();
private final HashMap<Class<?>, Converter<?>> converters = new LinkedHashMap<>();
private final boolean valueResolving;
private final List<MpConfigFilter> filters = new ArrayList<>();
MpConfigImpl(List<ConfigSource> sources,
HashMap<Class<?>, Converter<?>> converters,
List<MpConfigFilter> 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> T getValue(String propertyName, Class<T> propertyType) {
return getOptionalValue(propertyName, propertyType)
.orElseThrow(() -> new NoSuchElementException("Property \"" + propertyName + "\" is not available in "
+ "configuration"));
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType) {
// let's resolve arrays
if (propertyType.isArray()) {
Class<?> componentType = propertyType.getComponentType();
// first try to see if we have a direct value
Optional<String> 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<Object> 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<String> getPropertyNames() {
Set<String> names = new LinkedHashSet<>();
for (ConfigSource source : sources) {
names.addAll(source.getPropertyNames());
}
return names;
}
@Override
public Iterable<ConfigSource> 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 <T> 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 <T> Optional<Converter<T>> getConverter(Class<T> forType) {
return converters.entrySet()
.stream()
.filter(it -> forType.isAssignableFrom(it.getKey()))
.findFirst()
.map(Map.Entry::getValue)
.map(it -> (Converter<T>) 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 <T> 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> T convert(String propertyName, Class<T> 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<String> 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<String> 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")
<T> Converter<T> findConverter(Class<T> type) {
Converter<?> converter = converters.get(type);
if (null != converter) {
return (Converter<T>) converter;
}
return getConverter(type)
.orElseGet(() -> new FailingConverter<>(type));
}
@SuppressWarnings("unchecked")
private <T> Optional<Converter<T>> findImplicit(Class<T> type) {
// enums must be explicitly supported
if (Enum.class.isAssignableFrom(type)) {
return Optional.of(value -> {
Class<? extends Enum> enumClass = (Class<? extends Enum>) type;
return (T) Enum.valueOf(enumClass, value);
});
}
// any class that has a "public static T method()"
Optional<Method> 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<T> 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<Method> 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<Class<?>, 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<T> implements Converter<T> {
private final Class<T> type;
private FailingConverter(Class<T> type) {
this.type = type;
}
@Override
public T convert(String value) {
throw new IllegalArgumentException("Cannot convert \"" + value + "\" to type " + type.getName());
}
}
}

View File

@@ -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.function.Predicate;
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 class MpConfigProviderResolver extends ConfigProviderResolver {
*
* @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 @@ public class MpConfigProviderResolver extends ConfigProviderResolver {
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 class MpConfigProviderResolver extends ConfigProviderResolver {
* that hold a reference to configuration obtained at build time.
*/
public static final class ConfigDelegate implements io.helidon.config.Config, Config {
private AtomicReference<io.helidon.config.Config> delegate;
private final AtomicReference<Config> delegate = new AtomicReference<>();
private final AtomicReference<io.helidon.config.Config> 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 class MpConfigProviderResolver extends ConfigProviderResolver {
}
@Override
public <T> T convert(Class<T> type, String value) throws ConfigMappingException {
public <T> T convert(Class<T> type, String value) {
return getCurrent().convert(type, value);
}
@@ -262,43 +268,43 @@ public class MpConfigProviderResolver extends ConfigProviderResolver {
}
@Override
public <T> ConfigValue<List<T>> asList(Class<T> type) throws ConfigMappingException {
public <T> ConfigValue<List<T>> asList(Class<T> type) {
return getCurrent().asList(type);
}
@Override
public <T> ConfigValue<List<T>> asList(Function<io.helidon.config.Config, T> mapper) throws ConfigMappingException {
public <T> ConfigValue<List<T>> asList(Function<io.helidon.config.Config, T> mapper) {
return getCurrent().asList(mapper);
}
@Override
public ConfigValue<List<io.helidon.config.Config>> asNodeList() throws ConfigMappingException {
public ConfigValue<List<io.helidon.config.Config>> asNodeList() {
return getCurrent().asNodeList();
}
@Override
public ConfigValue<Map<String, String>> asMap() throws MissingValueException {
public ConfigValue<Map<String, String>> asMap() {
return getCurrent().asMap();
}
@Override
public <T> T getValue(String propertyName, Class<T> propertyType) {
return ((Config) getCurrent()).getValue(propertyName, propertyType);
return delegate.get().getValue(propertyName, propertyType);
}
@Override
public <T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType) {
return ((Config) getCurrent()).getOptionalValue(propertyName, propertyType);
return delegate.get().getOptionalValue(propertyName, propertyType);
}
@Override
public Iterable<String> getPropertyNames() {
return ((Config) getCurrent()).getPropertyNames();
return delegate.get().getPropertyNames();
}
@Override
public Iterable<ConfigSource> getConfigSources() {
return ((Config) getCurrent()).getConfigSources();
return delegate.get().getConfigSources();
}
/**
@@ -307,7 +313,7 @@ public class MpConfigProviderResolver extends ConfigProviderResolver {
* @return the instance backing this config delegate
*/
public Config delegate() {
return (Config) getCurrent();
return delegate.get();
}
}
}

View File

@@ -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}.
* <p>
* The following methods create MicroProfile config sources to help with manual setup of Config
* from {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver#getBuilder()}:
* <ul>
* <li>{@link #systemProperties()} - system properties config source</li>
* <li>{@link #environmentVariables()} - environment variables config source</li>
* <li>{@link #create(java.nio.file.Path)} - load a properties file from file system</li>
* <li>{@link #create(String, java.nio.file.Path)} - load a properties file from file system with custom name</li>
* <li>{@link #create(java.util.Map)} - create an in-memory source from map</li>
* <li>{@link #create(String, java.util.Map)} - create an in-memory source from map with custom name</li>
* <li>{@link #create(java.util.Properties)} - create an in-memory source from properties</li>
* <li>{@link #create(String, java.util.Properties)} - create an in-memory source from properties with custom name</li>
* </ul>
* The following methods add integration with Helidon SE Config:
* <ul>
* <li>{@link #create(io.helidon.config.spi.ConfigSource)} - create a MicroProfile config source from Helidon SE config
* source</li>
* <li>{@link #create(io.helidon.config.Config)} - create a MicroProfile config source from Helidon SE Config instance</li>
* </ul>
*/
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<String, String> 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<String, String> 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<String, String> 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<ConfigSource> 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<ConfigSource> classPath(ClassLoader classLoader, String resource) {
List<ConfigSource> 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);
}
}

View File

@@ -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<String, String> env;
private final Map<String, Cached> cache = new ConcurrentHashMap<>();
MpEnvironmentVariablesSource() {
this.env = System.getenv();
}
@Override
public Map<String, String> 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;
}
}
}

View File

@@ -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<String, String> 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";
}
}

View File

@@ -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<ConfigContent.NodeContent> 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<ConfigParser.Content> 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<ConfigParser> 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<String, String> 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<String, String> 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();
}
}
}

View File

@@ -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<String, String> map;
private final String name;
MpMapSource(String name, Map<String, String> map) {
this.name = name;
this.map = map;
}
@Override
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(map);
}
@Override
public String getValue(String propertyName) {
return map.get(propertyName);
}
@Override
public String getName() {
return name;
}
}

View File

@@ -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<String, String> getProperties() {
Set<String> strings = props.stringPropertyNames();
Map<String, String> 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";
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 @@ public class MpConfigReferenceTest {
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");

View File

@@ -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<String, String> 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<ConfigContent.NodeContent> 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<String, String> 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<ConfigNode> 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<ConfigParser.Content> 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<ConfigParser> parser() {
return Optional.empty();
}
@Override
public Optional<String> 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;
}
}
}

View File

@@ -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<ConfigSource> configSources = config.getConfigSources();
List<ConfigSource> 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<String> value = new AtomicReference<>("initial");
@Override
public Map<String, String> 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);
}
}
}

View File

@@ -27,3 +27,7 @@ referencing2-3=ref-${value2}
referencing2-4=ref-${value2}-ref
referencing3-1=${value1}-${value2}
referencing4-1=${missing}
referencing4-2=${missing}-${value1}

View File

@@ -54,10 +54,6 @@
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-media-type</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>

View File

@@ -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 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
return asList(Config.class);
}
/*
* MicroProfile Config methods
*/
@Override
public <T> T getValue(String propertyName, Class<T> 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 <T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType) {
try {
return Optional.of(getValue(propertyName, propertyType));
} catch (NoSuchElementException e) {
return Optional.empty();
} catch (ConfigMappingException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public Iterable<String> getPropertyNames() {
Set<String> keys = new HashSet<>(factory.context().last()
.asMap()
.orElseGet(Collections::emptyMap)
.keySet());
if (useSystemProperties) {
keys.addAll(System.getProperties().stringPropertyNames());
}
return keys;
}
@Override
public Iterable<ConfigSource> 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<ConfigSource> mpConfigSources() {
return new LinkedList<>(factory.mpConfigSources());
}
private <T> T mpFindValue(Config config, String propertyName, Class<T> 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<T> 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> T mpFindEnvVar(Config config, String propertyName, Class<T> 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 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
public Config reload() {
return AbstractConfigImpl.this.contextConfig(AbstractConfigImpl.this.factory.context().reload());
}
}
}

View File

@@ -41,7 +41,7 @@ public abstract class AbstractConfigSourceBuilder<B extends AbstractConfigSource
private Function<Config.Key, Optional<ConfigParser>> parserMapping;
@SuppressWarnings("unchecked")
private B me = (B) this;
private final B me = (B) this;
/**
* {@inheritDoc}

View File

@@ -34,7 +34,7 @@ import io.helidon.config.spi.ConfigNode.ValueNode;
public abstract class AbstractNodeBuilderImpl<ID, B> {
private final B thisBuilder;
private Function<String, String> tokenResolver;
private final Function<String, String> tokenResolver;
@SuppressWarnings("unchecked")
AbstractNodeBuilderImpl(Function<String, String> tokenResolver) {
@@ -80,13 +80,6 @@ public abstract class AbstractNodeBuilderImpl<ID, B> {
}
}
/**
* 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.
*

View File

@@ -41,7 +41,7 @@ public abstract class AbstractSourceBuilder<B extends AbstractSourceBuilder<B, U
private boolean optional = false;
@SuppressWarnings("unchecked")
private B me = (B) this;
private final B me = (B) this;
/**
* Configure builder from meta configuration.

View File

@@ -16,11 +16,8 @@
package io.helidon.config;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
@@ -51,11 +48,6 @@ import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.MergingStrategy;
import io.helidon.config.spi.OverrideSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.eclipse.microprofile.config.spi.Converter;
import static org.eclipse.microprofile.config.spi.ConfigSource.CONFIG_ORDINAL;
/**
* {@link Config} Builder implementation.
*/
@@ -69,11 +61,9 @@ class BuilderImpl implements Config.Builder {
*/
// sources to be sorted by priority
private final List<HelidonSourceWithPriority> prioritizedSources = new ArrayList<>();
private final List<PrioritizedMpSource> 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<ConfigSource> 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<PrioritizedMapperProvider> 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<Supplier<? extends ConfigSource>> sourceSuppliers) {
// replace current config sources with the ones provided
@@ -178,15 +157,14 @@ class BuilderImpl implements Config.Builder {
return this;
}
void disableMpMapperServices() {
this.mpMapperServicesEnabled = false;
}
@Override
public <T> Config.Builder addStringMapper(Class<T> type, Function<String, T> 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 @@ class BuilderImpl implements Config.Builder {
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 @@ class BuilderImpl implements Config.Builder {
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 @@ class BuilderImpl implements Config.Builder {
return this;
}
private void configSourceServicesEnabled(boolean enabled) {
this.configSourceServicesEnabled = enabled;
}
void mpWithConverters(Converter<?>... converters) {
for (Converter<?> converter : converters) {
addMpConverter(converter);
}
}
<T> void mpWithConverter(Class<T> type, int ordinal, Converter<T> 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 <T> void addMpConverter(Converter<T> converter) {
Class<T> type = (Class<T>) 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<org.eclipse.microprofile.config.spi.ConfigSource> 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 @@ class BuilderImpl implements Config.Builder {
}
private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) {
List<ConfigSourceRuntimeBase> targetSources = new LinkedList<>();
List<ConfigSourceRuntimeImpl> targetSources = new LinkedList<>();
if (systemPropertiesSourceEnabled && !hasSystemPropertiesSource) {
hasSystemPropertiesSource = true;
@@ -574,7 +420,7 @@ class BuilderImpl implements Config.Builder {
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 @@ class BuilderImpl implements Config.Builder {
return new ConfigSourcesRuntime(targetSources, mergingStrategy);
}
private List<ConfigSourceRuntimeBase> mergePrioritized(ConfigContextImpl context) {
List<PrioritizedConfigSource> allPrioritized = new ArrayList<>(this.prioritizedMpSources);
private List<ConfigSourceRuntimeImpl> mergePrioritized(ConfigContextImpl context) {
List<PrioritizedConfigSource> 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 @@ class BuilderImpl implements Config.Builder {
.build()
.asList()
.stream()
.map(filter -> (Function<Config, ConfigFilter>) (Config t) -> filter)
.map(LoadedFilterProvider::new)
.forEach(this::addFilter);
}
@@ -716,9 +562,7 @@ class BuilderImpl implements Config.Builder {
* {@link ConfigContext} implementation.
*/
static class ConfigContextImpl implements ConfigContext {
private final Map<ConfigSource, ConfigSourceRuntimeBase> runtimes = new IdentityHashMap<>();
private final Map<org.eclipse.microprofile.config.spi.ConfigSource, ConfigSourceRuntimeBase> mpRuntimes
= new IdentityHashMap<>();
private final Map<ConfigSource, ConfigSourceRuntimeImpl> runtimes = new IdentityHashMap<>();
private final Executor changesExecutor;
private final List<ConfigParser> configParsers;
@@ -733,7 +577,7 @@ class BuilderImpl implements Config.Builder {
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 @@ class BuilderImpl implements Config.Builder {
.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 @@ class BuilderImpl implements Config.Builder {
// config sources
.sources(ConfigSources.empty())
.overrides(OverrideSources.empty())
.disableSourceServices()
.disableEnvironmentVariablesSource()
.disableSystemPropertiesSource()
.disableParserServices()
@@ -800,35 +639,6 @@ class BuilderImpl implements Config.Builder {
ConfigMapperProvider {
}
private static final class MpConverterWrapper implements PrioritizedMapperProvider {
private final Map<Class<?>, Function<Config, ?>> 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<Class<?>, Function<Config, ?>> 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 @@ class BuilderImpl implements Config.Builder {
}
}
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 @@ class BuilderImpl implements Config.Builder {
// 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 @@ class BuilderImpl implements Config.Builder {
}
}
private static class LoadedFilterProvider implements Function<Config, ConfigFilter> {
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();
}
}
}

View File

@@ -40,7 +40,7 @@ import io.helidon.config.spi.OverrideSource;
* <h1>Configuration</h1>
* Immutable tree-structured configuration.
* <h2>Loading Configuration</h2>
* Load the default configuration using the {@link #create} method.
* Load the default configuration using the {@link Config#create} method.
* <pre>{@code
* Config config = Config.create();
* }</pre> Use {@link Config.Builder} to construct a new {@code Config} instance
@@ -233,7 +233,7 @@ import io.helidon.config.spi.OverrideSource;
* <h2><a id="multipleSources">Handling Multiple Configuration
* Sources</a></h2>
* 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 @@ public interface Config {
//
// config changes
//
/**
* Register a {@link Consumer} that is invoked each time a change occurs on whole Config or on a particular Config node.
* <p>
@@ -976,8 +977,8 @@ public interface Config {
*/
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 @@ public interface Config {
* @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.

View File

@@ -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 @@ class ConfigDiff {
.map(Config::key)
.distinct()
.flatMap(ConfigDiff::expandKey)
.distinct()
.collect(toSet());
return new ConfigDiff(newConfig, changedKeys);

View File

@@ -23,7 +23,6 @@ import java.util.Objects;
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<String, List<String>> aliasGenerator;
private final ConcurrentMap<PrefixedKey, AbstractConfigImpl> configCache;
private final Instant timestamp;
private final List<ConfigSourceRuntimeBase> configSources;
private final List<org.eclipse.microprofile.config.spi.ConfigSource> 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<String, List<String>> aliasGenerator,
List<ConfigSourceRuntimeBase> configSources) {
Function<String, List<String>> 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 @@ final class ConfigFactory {
return provider;
}
List<ConfigSourceRuntimeBase> configSources() {
return configSources;
}
List<org.eclipse.microprofile.config.spi.ConfigSource> mpConfigSources() {
return mpConfigSources;
}
/**
* Prefix represents detached roots.
*/

View File

@@ -18,10 +18,6 @@ package io.helidon.config;
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 @@ public final class ConfigHelper {
}
/**
* Creates a {@link ConfigHelper#subscriber(Function) Flow.Subscriber} that
* will delegate {@link Flow.Subscriber#onNext(Object)} to the specified
* {@code onNextFunction} function.
* <p>
* 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.
* <p>
* 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 <T> 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 <T> Flow.Subscriber<T> subscriber(Function<T, Boolean> onNextFunction) {
return new OnNextFunctionSubscriber<>(onNextFunction);
public static Map<String, String> 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<ConfigKeyImpl, ConfigNode> createFullKeyToNodeMap(ConfigNode.ObjectNode objectNode) {
@@ -89,55 +77,4 @@ public final class ConfigHelper {
throw new IllegalArgumentException("Invalid node type.");
}
}
/**
* Implementation of {@link ConfigHelper#subscriber(Function)}.
*
* @param <T> the subscribed item type
* @see ConfigHelper#subscriber(Function)
*/
private static class OnNextFunctionSubscriber<T> implements Flow.Subscriber<T> {
private final Function<T, Boolean> onNextFunction;
private final Logger logger;
private Flow.Subscription subscription;
private OnNextFunctionSubscriber(Function<T, Boolean> 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.");
}
}
}

View File

@@ -59,7 +59,7 @@ class ConfigLeafImpl extends ConfigExistingImpl<ValueNode> {
}
Optional<String> value = value();
if (!value.isPresent()) {
if (value.isEmpty()) {
return ConfigValues.create(this, Optional::empty, aConfig -> aConfig.asList(type));
}
@@ -89,7 +89,7 @@ class ConfigLeafImpl extends ConfigExistingImpl<ValueNode> {
@Override
public <T> ConfigValue<List<T>> asList(Function<Config, T> mapper) throws ConfigMappingException {
Optional<String> value = value();
if (!value.isPresent()) {
if (value.isEmpty()) {
return ConfigValues.create(this, Optional::empty, aConfig -> aConfig.asList(mapper));
}

View File

@@ -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.ChronoField;
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 @@ public final class ConfigMappers {
}
private static Map<Class<?>, Function<Config, ?>> initEssentialMappers() {
Map<Class<?>, Function<Config, ?>> 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));
});
essentials.put(OptionalLong.class, (node) -> {
if (!node.exists()) {
return OptionalLong.empty();
}
return OptionalLong.of(wrap(Long::parseLong).apply(node));
});
essentials.put(OptionalDouble.class, (node) -> {
if (!node.exists()) {
return OptionalDouble.empty();
}
return OptionalDouble.of(wrap(Double::parseDouble).apply(node));
});
return Collections.unmodifiableMap(essentials);
return Map.of(Config.class, (node) -> node,
String.class, wrap(value -> value),
OptionalInt.class, ConfigMappers::optionalIntEssential,
OptionalLong.class, ConfigMappers::optionalLongEssential,
OptionalDouble.class, ConfigMappers::optionalDoubleEssential);
}
static Map<Class<?>, Function<Config, ?>> essentialMappers() {
return ESSENTIAL_MAPPERS;
}
private static OptionalDouble optionalDoubleEssential(Config node) {
if (!node.exists()) {
return OptionalDouble.empty();
}
return OptionalDouble.of(wrap(Double::parseDouble).apply(node));
}
private static OptionalLong optionalLongEssential(Config node) {
if (!node.exists()) {
return OptionalLong.empty();
}
return OptionalLong.of(wrap(Long::parseLong).apply(node));
}
private static OptionalInt optionalIntEssential(Config node) {
if (!node.exists()) {
return OptionalInt.empty();
}
return OptionalInt.of(wrap(Integer::parseInt).apply(node));
}
private static Map<Class<?>, Function<Config, ?>> initBuiltInMappers() {
Map<Class<?>, Function<Config, ?>> 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));
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,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);
// 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<Class<?>, Function<Config, ?>> builtInMappers() {

View File

@@ -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<String, ConfigNode> 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<String, String> 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<ConfigNode.ObjectNode> load() {
return Optional.of(ConfigUtils.mapToObjectNode(source.getProperties(), false));
}
@Override
public Optional<ConfigNode> 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;
}
}

View File

@@ -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 java.util.function.BiConsumer;
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<ConfigNode> 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

View File

@@ -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.List;
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 @@ import io.helidon.config.spi.WatchableSource;
* 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<BiConsumer<String, ConfigNode>> 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<ObjectNode> initialData;
private Map<String, ConfigNode> loadedData;
private Map<String, String> mpData;
@SuppressWarnings("unchecked")
ConfigSourceRuntimeImpl(BuilderImpl.ConfigContextImpl configContext, ConfigSource source) {
@@ -182,7 +180,6 @@ public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements
return isLazy;
}
@Override
boolean changesSupported() {
return changesSupported;
}
@@ -233,16 +230,11 @@ public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements
this.initialData = loadedData;
this.loadedData = new HashMap<>();
mpData = new HashMap<>();
initialData.ifPresent(data -> {
Map<ConfigKeyImpl, ConfigNode> keyToNodeMap = ConfigHelper.createFullKeyToNodeMap(data);
keyToNodeMap.forEach((key, value) -> {
Optional<String> 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 class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements
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<String, String> getProperties() {
if (isSystemProperties()) {
// this is a "hack" for MP TCK tests
// System properties act as a mutable source for the purpose of MicroProfile
Map<String, String> 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<String, Optional<ConfigNode>> objectNodeToSingleNode() {
return key -> {
if (null == loadedData) {
@@ -340,10 +287,6 @@ public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements
}
ConfigSource unwrap() {
return configSource;
}
private static final class PollingStrategyStarter implements Runnable {
private final PollingStrategy pollingStrategy;
private final PollingStrategyListener listener;

View File

@@ -37,11 +37,11 @@ import io.helidon.config.spi.MergingStrategy;
final class ConfigSourcesRuntime {
private final List<RuntimeWithData> loadedData = new LinkedList<>();
private List<ConfigSourceRuntimeBase> allSources;
private MergingStrategy mergingStrategy;
private Consumer<Optional<ObjectNode>> changeListener;
private final List<ConfigSourceRuntimeImpl> allSources;
private final MergingStrategy mergingStrategy;
private volatile Consumer<Optional<ObjectNode>> changeListener;
ConfigSourcesRuntime(List<ConfigSourceRuntimeBase> allSources,
ConfigSourcesRuntime(List<ConfigSourceRuntimeImpl> allSources,
MergingStrategy mergingStrategy) {
this.allSources = allSources;
this.mergingStrategy = mergingStrategy;
@@ -53,10 +53,6 @@ final class ConfigSourcesRuntime {
MergingStrategy.fallback());
}
List<ConfigSourceRuntimeBase> allSources() {
return allSources;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -137,7 +133,7 @@ final class ConfigSourcesRuntime {
synchronized Optional<ObjectNode> load() {
for (ConfigSourceRuntimeBase source : allSources) {
for (ConfigSourceRuntimeImpl source : allSources) {
if (source.isLazy()) {
loadedData.add(new RuntimeWithData(source, Optional.empty()));
} else {
@@ -197,10 +193,10 @@ final class ConfigSourcesRuntime {
}
private static final class RuntimeWithData {
private final ConfigSourceRuntimeBase runtime;
private final ConfigSourceRuntimeImpl runtime;
private Optional<ObjectNode> data;
private RuntimeWithData(ConfigSourceRuntimeBase runtime, Optional<ObjectNode> data) {
private RuntimeWithData(ConfigSourceRuntimeImpl runtime, Optional<ObjectNode> data) {
this.runtime = runtime;
this.data = data;
}
@@ -209,7 +205,7 @@ final class ConfigSourcesRuntime {
this.data = data;
}
private ConfigSourceRuntimeBase runtime() {
private ConfigSourceRuntimeImpl runtime() {
return runtime;
}

View File

@@ -19,24 +19,14 @@ package io.helidon.config;
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 @@ final class ConfigUtils {
throw new AssertionError("Instantiation not allowed.");
}
/**
* Convert iterable items to an ordered serial stream.
*
* @param items items to be streamed.
* @param <S> expected streamed item type.
* @return stream of items.
*/
static <S> Stream<S> asStream(Iterable<? extends S> items) {
return asStream(items.iterator());
}
/**
* Converts an iterator to a stream.
*
* @param <S> type of the base items
* @param iterator iterator over the items
* @return stream of the items
*/
static <S> Stream<S> asStream(Iterator<? extends S> 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}.
* <p>
* Instances are sorted by {@code Priority.value()} attached <em>directly</em> 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 <S> item type.
* @return prioritized stream of items.
*/
static <S> Stream<? extends S> asPrioritizedStream(Iterable<? extends S> 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 <S> type of object being compared
* @param defaultPriority used if the classes for either or both objects
* lack the {@code Priority} annotation
* @return comparator
*/
static <S> Comparator<S> 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.
* <p>
@@ -187,55 +115,4 @@ final class ConfigUtils {
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.
* <p>
* 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}.
* <p>
* 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;
}
}
}

View File

@@ -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 @@ public interface ConfigValue<T> {
Objects.requireNonNull(supplier);
Optional<T> optional = asOptional();
if (!optional.isPresent()) {
if (optional.isEmpty()) {
Optional<T> supplied = supplier.get();
Objects.requireNonNull(supplied);
optional = supplied;
@@ -348,6 +348,6 @@ public interface ConfigValue<T> {
* @return the optional value as a {@code Stream}
*/
default Stream<T> stream() {
return asOptional().map(Stream::of).orElseGet(Stream::empty);
return asOptional().stream();
}
}

View File

@@ -39,7 +39,7 @@ public final class ConfigValues {
* @return a config value that is empty
*/
public static <T> ConfigValue<T> empty() {
return new ConfigValueBase<T>(Config.Key.create("")) {
return new ConfigValueBase<>(Config.Key.create("")) {
@Override
public Optional<T> asOptional() {
return Optional.empty();
@@ -83,7 +83,7 @@ public final class ConfigValues {
* @return a config value that uses the value provided
*/
public static <T> ConfigValue<T> simpleValue(T value) {
return new ConfigValueBase<T>(Config.Key.create("")) {
return new ConfigValueBase<>(Config.Key.create("")) {
@Override
public Optional<T> asOptional() {
return Optional.ofNullable(value);

View File

@@ -104,11 +104,6 @@ public class ListNodeBuilderImpl extends AbstractNodeBuilderImpl<Integer, ListNo
return this;
}
@Override
protected String typeDescription() {
return "a LIST node";
}
@Override
protected Integer id(MergingKey key) {
String name = key.first();

View File

@@ -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 <T> ConfigBuilder withConverter(Class<T> aClass, int ordinal, Converter<T> 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;
}
}

View File

@@ -106,11 +106,6 @@ public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, Objec
return this;
}
@Override
protected String typeDescription() {
return "an OBJECT node";
}
@Override
protected String id(MergingKey key) {
return key.first();

View File

@@ -35,7 +35,6 @@ import java.util.stream.Collectors;
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 @@ class ProviderImpl implements Config.Context {
rootNode.orElseGet(ObjectNode::empty),
targetFilter,
this,
aliasGenerator,
configSource.allSources());
aliasGenerator);
AbstractConfigImpl config = factory.config();
// initialize filters
initializeFilters(config, targetFilter);
@@ -148,7 +146,7 @@ class ProviderImpl implements Config.Context {
private ObjectNode resolveKeys(ObjectNode rootNode) {
Function<String, String> resolveTokenFunction = Function.identity();
if (keyResolving) {
Map<String, String> flattenValueNodes = flattenNodes(rootNode);
Map<String, String> flattenValueNodes = ConfigHelper.flattenNodes(rootNode);
if (flattenValueNodes.isEmpty()) {
return rootNode;
@@ -166,15 +164,6 @@ class ProviderImpl implements Config.Context {
return ObjectNodeBuilderImpl.create(rootNode, resolveTokenFunction).build();
}
private Map<String, String> 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<String, String> tokenToValueMap(Map<String, String> flattenValueNodes) {
return flattenValueNodes.keySet()
.stream()
@@ -253,7 +242,6 @@ class ProviderImpl implements Config.Context {
filterProviders.stream()
.map(providerFunction -> providerFunction.apply(config))
.sorted(ConfigUtils.priorityComparator(ConfigFilter.PRIORITY))
.forEachOrdered(chain::addFilter);
chain.filterProviders.stream()

View File

@@ -1,4 +1,4 @@
/**
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");

View File

@@ -141,7 +141,7 @@ public class ValueResolvingFilter implements ConfigFilter {
* 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)

View File

@@ -18,5 +18,5 @@ package io.helidon.config.spi;
/**
* Java service loader service to create a polling strategy factory based on meta configuration.
*/
public interface ChangeWatcherProvider extends MetaConfigurableProvider<ChangeWatcher> {
public interface ChangeWatcherProvider extends MetaConfigurableProvider<ChangeWatcher<?>> {
}

View File

@@ -50,14 +50,17 @@ final class FallbackMergingStrategy implements MergingStrategy {
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());
}

View File

@@ -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 @@ import java.util.Properties;
*/
final class OrderedProperties {
private LinkedHashMap<String, String> map = new LinkedHashMap<>();
private final LinkedHashMap<String, String> 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());

View File

@@ -25,7 +25,7 @@ final class 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.");

View File

@@ -17,7 +17,7 @@
import io.helidon.config.PropertiesConfigParser;
/**
* config module.
* Helidon SE Config module.
*/
module io.helidon.config {
@@ -27,9 +27,9 @@ module io.helidon.config {
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 @@ module io.helidon.config {
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;
}

View File

@@ -18,6 +18,8 @@ package io.helidon.config;
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.
* <p>
@@ -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;

View File

@@ -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<Long, Boolean> onNextFunction = mock(Function.class);
Flow.Subscription subscription = mock(Flow.Subscription.class);
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();
//create Subscriber
Flow.Subscriber<Long> 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();
Map<String, String> map = ConfigHelper.flattenNodes(node);
Map<String, String> expected = Map.of(
"simple", "value",
"list.0", "first",
"list.1", "second",
"object.value", "value2"
);
assertThat(map, is(expected));
}
}
}

View File

@@ -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<Integer> integers) {
List<Integer> 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<Provider> providers) {
List<Provider> 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 {
}
}

View File

@@ -28,7 +28,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
*/
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";

View File

@@ -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<ConfigSource> configSources = config.getConfigSources();
List<ConfigSource> 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));
}
}

View File

@@ -43,6 +43,15 @@
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-key-util</artifactId>
</dependency>
<dependency>
<!-- an implementation of a filter is part of this library - it only makes sense
when MP config implementation is on the classpath, not needed or used otherwise
-->
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-mp</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>

View File

@@ -37,7 +37,7 @@ import io.helidon.config.spi.ConfigFilter;
* <p>
* Password in properties must be stored as follows:
* <ul>
* <li>${AES=base64} - encrypted password using a master password (must be provided to Prime through configuration, system
* <li>${AES=base64} - encrypted password using a master password (must be provided to prime through configuration, system
* property or environment variable)</li>
* <li>${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)</li>

View File

@@ -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.configurable.Resource;
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 @@ public final class EncryptionUtil {
}
}
static Optional<char[]> resolveMasterPassword(boolean requireEncryption, org.eclipse.microprofile.config.Config config) {
Optional<char[]> result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE)
.or(() -> {
Optional<String> 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<char[]> resolveMasterPassword(boolean requireEncryption, Config config) {
Optional<char[]> result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE)
.or(() -> {
@@ -303,6 +327,10 @@ public final class EncryptionUtil {
return result;
}
static Optional<PrivateKey> resolvePrivateKey(org.eclipse.microprofile.config.Config config){
return resolvePrivateKey(MpConfig.toHelidonConfig(config).get("security.config.rsa"));
}
static Optional<PrivateKey> resolvePrivateKey(Config config) {
// load configuration values
KeyConfig.PemBuilder pemBuilder = KeyConfig.pemBuilder().config(config);

View File

@@ -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).
* <p>
* Password in properties must be stored as follows:
* <ul>
* <li>${AES=base64} - encrypted password using a master password (must be provided to prime through configuration, system
* property or environment variable)</li>
* <li>${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)</li>
* <li>${ALIAS=alias_name} - reference to another property, that is encrypted</li>
* <li>${CLEAR=text} - clear-text password. Intentionally denoting this value as a protectable one, so we can enforce encryption
* (e.g. in prod)</li>
* </ul>
* Example:
* <pre>
* google_client_secret=${AES=mYRkg+4Q4hua1kvpCCI2hg==}
* service_password=${RSA=mYRkg+4Q4hua1kvpCCI2hg==}
* another_password=${ALIAS=service_password}
* cleartext_password=${CLEAR=known_password}
* </pre>
*
* @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<String> 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;
}
}

View File

@@ -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 @@ module io.helidon.config.encryption {
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;
}

View File

@@ -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

View File

@@ -46,6 +46,7 @@
<module>testing</module>
<module>test-infrastructure</module>
<module>tests</module>
<module>config-mp</module>
</modules>
<build>

View File

@@ -58,6 +58,5 @@
<module>test-mappers-2-complex</module>
<module>test-meta-source</module>
<module>test-parsers-1-complex</module>
<module>test-mp-reference</module>
</modules>
</project>

View File

@@ -50,6 +50,12 @@
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-testing</artifactId>

View File

@@ -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()}.
* <p>The YAML file is transformed to a flat map as follows:</p>
* <strong>Object nodes</strong>
* <p>
* Each node in the tree is dot separated.
* <pre>
* server:
* host: "localhost"
* port: 8080
* </pre>
* Will be transformed to the following properties:
* <pre>
* server.host=localhost
* server.port=8080
* </pre>
* <strong>List nodes (arrays)</strong>
* <p>
* Each node will be indexed (0 based)
* <pre>
* providers:
* - abac:
* enabled: true
* names: ["first", "second", "third"]
* </pre>
* Will be transformed to the following properties:
* <pre>
* providers.0.abac.enabled=true
* names.0=first
* names.1=second
* names.2=third
* </pre>
*/
public class YamlMpConfigSource implements ConfigSource {
private final Map<String, String> properties;
private final String name;
private YamlMpConfigSource(String name, Map<String, String> 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<String, String> getProperties() {
return Collections.unmodifiableMap(properties);
}
@Override
public String getValue(String propertyName) {
return properties.get(propertyName);
}
@Override
public String getName() {
return name;
}
private static Map<String, String> fromMap(Map yamlMap) {
Map<String, String> result = new HashMap<>();
process(result, "", yamlMap);
return result;
}
private static void process(Map<String, String> resultMap, String prefix, Map yamlMap) {
yamlMap.forEach((key, value) -> {
processNext(resultMap, prefix(prefix, key.toString()), value);
});
}
private static void process(Map<String, String> 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<String, String> 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;
}
}

View File

@@ -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<ConfigSource> getConfigSources(ClassLoader classLoader) {
Enumeration<URL> resources;
try {
resources = classLoader.getResources("application.yaml");
} catch (IOException e) {
throw new ConfigException("Failed to read resources from classpath", e);
}
List<ConfigSource> result = new LinkedList<>();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
result.add(YamlMpConfigSource.create(url));
}
return result;
}
}

View File

@@ -15,6 +15,7 @@
*/
import io.helidon.config.yaml.YamlConfigParser;
import io.helidon.config.yaml.YamlMpConfigSourceProvider;
/**
* YAML Parser implementation.
@@ -27,9 +28,11 @@ module io.helidon.config.yaml {
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;
}

View File

@@ -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

View File

@@ -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"));
}
}

35
dependencies/pom.xml vendored
View File

@@ -79,7 +79,7 @@
<version.lib.jsonp-impl>1.1.6</version.lib.jsonp-impl>
<version.lib.junit>5.6.2</version.lib.junit>
<version.lib.maven-wagon>2.10</version.lib.maven-wagon>
<version.lib.microprofile-config>1.3</version.lib.microprofile-config>
<version.lib.microprofile-config>1.4</version.lib.microprofile-config>
<version.lib.microprofile-health>2.1</version.lib.microprofile-health>
<version.lib.microprofile-jwt>1.1.1</version.lib.microprofile-jwt>
<version.lib.microprofile-metrics-api>2.2</version.lib.microprofile-metrics-api>
@@ -601,6 +601,39 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-rest-tck</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api-tck</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<exclusions>
<exclusion>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.reactive-streams-operators</groupId>
<artifactId>microprofile-reactive-streams-operators-api</artifactId>

View File

@@ -77,8 +77,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.config</groupId>
<artifactId>helidon-microprofile-config</artifactId>
<groupId>io.helidon.microprofile.cdi</groupId>
<artifactId>helidon-microprofile-cdi</artifactId>
<scope>test</scope>
</dependency>

View File

@@ -41,6 +41,10 @@
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-mp</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>

View File

@@ -39,7 +39,7 @@ import javax.json.JsonObject;
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;

View File

@@ -48,6 +48,10 @@
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-mp</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-context</artifactId>

View File

@@ -30,6 +30,7 @@ import javax.inject.Inject;
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 @@ public class KafkaConnector implements IncomingConnectorFactory, OutgoingConnect
@Override
public PublisherBuilder<? extends Message<?>> getPublisherBuilder(org.eclipse.microprofile.config.Config config) {
KafkaPublisher<Object, Object> publisher = KafkaPublisher.builder().config((Config) config).scheduler(scheduler).build();
KafkaPublisher<Object, Object> publisher = KafkaPublisher.builder()
.config(MpConfig.toHelidonConfig(config))
.scheduler(scheduler)
.build();
resources.add(publisher);
return ReactiveStreams.fromPublisher(publisher);
}
@Override
public SubscriberBuilder<? extends Message<?>, Void> getSubscriberBuilder(org.eclipse.microprofile.config.Config config) {
return ReactiveStreams.fromSubscriber(KafkaSubscriber.create((Config) config));
return ReactiveStreams.fromSubscriber(KafkaSubscriber.create(MpConfig.toHelidonConfig(config)));
}
/**

View File

@@ -22,10 +22,12 @@ module io.helidon.messaging.connectors.kafka {
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;
}

View File

@@ -46,16 +46,13 @@
<groupId>io.helidon.common</groupId>
<artifactId>helidon-common-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-mp</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>

View File

@@ -29,6 +29,8 @@ module io.helidon.metrics {
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;

View File

@@ -44,7 +44,7 @@
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<artifactId>helidon-config-mp</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.common</groupId>

View File

@@ -43,9 +43,10 @@ import io.helidon.common.HelidonFeatures;
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 @@ final class HelidonContainerImpl extends Weld implements HelidonContainer {
};
setResourceLoader(resourceLoader);
Config config = (Config) ConfigProvider.getConfig();
Config mpConfig = ConfigProvider.getConfig();
io.helidon.config.Config config = MpConfig.toHelidonConfig(mpConfig);
Map<String, String> properties = config.get("cdi")
.detach()
.asMap()
@@ -263,7 +266,7 @@ final class HelidonContainerImpl extends Weld implements HelidonContainer {
bm = CDI.current().getBeanManager();
}
Config config = (Config) ConfigProvider.getConfig();
org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig();
MpConfigProviderResolver.runtimeStart(config);
@@ -316,7 +319,8 @@ final class HelidonContainerImpl extends Weld implements HelidonContainer {
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);

View File

@@ -29,6 +29,7 @@ module io.helidon.microprofile.cdi {
requires io.helidon.common;
requires io.helidon.config;
requires io.helidon.config.mp;
requires weld.core.impl;
requires weld.spi;
@@ -36,6 +37,7 @@ module io.helidon.microprofile.cdi {
requires weld.se.core;
requires io.helidon.common.context;
requires jakarta.inject.api;
requires microprofile.config.api;
exports io.helidon.microprofile.cdi;

View File

@@ -23,9 +23,9 @@ 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.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 @@ class HelidonContainerInitializerTest {
@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 @@ class HelidonContainerInitializerTest {
@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 @@ class HelidonContainerInitializerTest {
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));

View File

@@ -23,10 +23,10 @@ import javax.enterprise.inject.Instance;
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 @@ class MainTest {
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));

View File

@@ -37,22 +37,23 @@
<dependencies>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<artifactId>helidon-config-mp</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-encryption</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-object-mapping</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.cdi</groupId>
<artifactId>helidon-microprofile-cdi</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
@@ -64,6 +65,11 @@
<artifactId>jakarta.inject-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.cdi</groupId>
<artifactId>helidon-microprofile-cdi</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.bundles</groupId>
<artifactId>internal-test-libs</artifactId>

View File

@@ -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.Method;
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.Optional;
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 javax.inject.Provider;
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("(?<!\\\\),");
private static final Pattern ESCAPED_COMMA_PATTERN = Pattern.compile("\\,", Pattern.LITERAL);
private static final Annotation CONFIG_PROPERTY_LITERAL = new ConfigProperty() {
@Override
public String name() {
@@ -111,7 +116,6 @@ public class ConfigCdiExtension implements Extension {
}
private final List<InjectionPoint> ips = new LinkedList<>();
private Config runtimeHelidonConfig;
/**
* Constructor invoked by CDI container.
@@ -174,10 +178,17 @@ public class ConfigCdiExtension implements Extension {
.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<Type> types = ips.stream()
.map(InjectionPoint::getType)
@@ -233,7 +244,7 @@ public class ConfigCdiExtension implements Extension {
// 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 @@ public class ConfigCdiExtension implements Extension {
Map<String, String> - 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 @@ public class ConfigCdiExtension implements Extension {
}
private static <T> T withDefault(Config config, String key, Class<T> 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> T convert(String key, Config config, String value, Class<T> 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 @@ public class ConfigCdiExtension implements Extension {
typeArg2);
}
} else if (Map.class.isAssignableFrom(rawType)) {
return config.get(configKey).detach().asMap().get();
Map<String, String> 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 @@ public class ConfigCdiExtension implements Extension {
}
}
private static <T> List<T> asList(Config config, String configKey, Class<T> 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<T> result = new LinkedList<>();
String[] values = defaultValue.split(",");
for (String value : values) {
result.add(config.convert(typeArg, value));
}
return result;
}
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 <T> List<T> asList(Config config, String configKey, Class<T> typeArg, String defaultValue) {
// first try to see if we have a direct value
Optional<String> optionalValue = config.getOptionalValue(configKey, String.class);
if (optionalValue.isPresent()) {
return toList(configKey, config, optionalValue.get(), typeArg);
}
/*
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<T> 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 <T> List<T> toList(String configKey, Config config, String stringValue, Class<T> typeArg) {
if (stringValue.isEmpty()) {
return List.of();
}
// we have a comma separated list
List<T> result = new LinkedList<>();
for (String value : toArray(stringValue)) {
result.add(convert(configKey, config, value, typeArg));
}
return result;
}
private String defaultValue(ConfigProperty annotation) {

View File

@@ -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;

View File

@@ -23,7 +23,10 @@ module io.helidon.microprofile.config {
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;

View File

@@ -118,7 +118,9 @@ public class MutableMpTest {
public void setValue(String value) {
this.value.set(value);
listener.accept("value", value);
if (null != listener) {
listener.accept("value", value);
}
}
}
}

View File

@@ -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

View File

@@ -65,14 +65,7 @@
<dependency>
<groupId>org.eclipse.microprofile.metrics</groupId>
<artifactId>microprofile-metrics-api</artifactId>
<version>${version.lib.microprofile-metrics-api}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.versioning</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>

View File

@@ -881,6 +881,18 @@ public class JwtAuthProvider extends SynchronousProvider implements Authenticati
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;
}

View File

@@ -21,7 +21,9 @@ import java.util.HashMap;
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 @@ class AdHocConfigBuilder {
}
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;
}
}

View File

@@ -25,6 +25,7 @@ module io.helidon.microprofile.messaging {
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;

View File

@@ -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.Optional;
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;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
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);
}
}
protected SeContainer cdiContainer;
protected Map<String, String> cdiConfig() {
@@ -81,7 +65,6 @@ public abstract class AbstractCDITest {
@AfterEach
public void tearDown() {
try {
singleServerReference.stop();
cdiContainer.close();
} catch (Throwable t) {
//emergency cleanup see #1446
@@ -111,18 +94,13 @@ public abstract class AbstractCDITest {
private static SeContainer startCdiContainer(Map<String, String> p, Set<Class<?>> 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();
}

View File

@@ -32,10 +32,6 @@
<description>Server of the microprofile implementation</description>
<dependencies>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.cdi</groupId>
<artifactId>helidon-microprofile-cdi</artifactId>
@@ -74,7 +70,6 @@
<dependency>
<groupId>io.helidon.microprofile.config</groupId>
<artifactId>helidon-microprofile-config</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<!-- this is to include JSON-P native image configuration -->

View File

@@ -104,9 +104,7 @@ public class JaxRsCdiExtension implements Extension {
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<Class<?>> classes = new HashSet<>(resources);
applicationMetas.add(JaxRsApplication.builder()
.synthetic(true)

Some files were not shown because too many files have changed in this diff Show More