Config change support refactoring (#1417)

* Removal of project Reactor.

* Change support refactoring.

* Big change polling + changes + lazy etc.

* Documentation + javadoc fixes.

* Intermittent failure fix

* Changelog update.

* Fixed intermittent test failure - race condition.
This commit is contained in:
Tomas Langer
2020-03-09 10:48:19 +01:00
committed by GitHub
parent 1d327c5edf
commit c38554957d
260 changed files with 9796 additions and 12575 deletions

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
@@ -24,7 +24,7 @@
<Match>
<!-- False positive. See https://github.com/spotbugs/spotbugs/issues/756 -->
<Class name="io.helidon.config.internal.PropertiesConfigParser"/>
<Class name="io.helidon.config.PropertiesConfigParser"/>
<Method name="parse"/>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/>
</Match>

View File

@@ -25,17 +25,9 @@ import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.internal.ConfigKeyImpl;
import org.eclipse.microprofile.config.spi.ConfigSource;
/**
@@ -51,14 +43,10 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
private final ConfigKeyImpl realKey;
private final ConfigFactory factory;
private final Type type;
private final Flow.Publisher<Config> changesPublisher;
private final Context context;
private final ConfigMapperManager mapperManager;
private volatile Flow.Subscriber<ConfigDiff> subscriber;
private final ReentrantReadWriteLock subscriberLock = new ReentrantReadWriteLock();
private final AtomicReference<Config> latestConfig = new AtomicReference<>();
private boolean useSystemProperties;
private boolean useEnvironmentVariables;
private final boolean useSystemProperties;
private final boolean useEnvironmentVariables;
/**
* Initializes Config implementation.
@@ -84,8 +72,27 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
this.factory = factory;
this.type = type;
changesPublisher = new FilteringConfigChangeEventPublisher(factory.changes());
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;
}
/**
@@ -157,35 +164,12 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
return asList(Config.class);
}
void subscribe() {
try {
subscriberLock.readLock().lock();
if (subscriber == null) {
subscriberLock.readLock().unlock();
subscriberLock.writeLock().lock();
try {
try {
if (subscriber == null) {
waitForSubscription(1, TimeUnit.SECONDS);
}
} finally {
subscriberLock.readLock().lock();
}
} finally {
subscriberLock.writeLock().unlock();
}
}
} finally {
subscriberLock.readLock().unlock();
}
}
/*
* MicroProfile Config methods
*/
@Override
public <T> T getValue(String propertyName, Class<T> propertyType) {
Config config = latestConfig.get();
Config config = factory.context().last();
try {
return mpFindValue(config, propertyName, propertyType);
} catch (MissingValueException e) {
@@ -208,7 +192,7 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
@Override
public Iterable<String> getPropertyNames() {
Set<String> keys = new HashSet<>(latestConfig.get()
Set<String> keys = new HashSet<>(factory.context().last()
.asMap()
.orElseGet(Collections::emptyMap)
.keySet());
@@ -222,7 +206,7 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
@Override
public Iterable<ConfigSource> getConfigSources() {
Config config = latestConfig.get();
Config config = factory.context().last();
if (null == config) {
// maybe we are in progress of initializing this config (e.g. filter processing)
config = this;
@@ -291,41 +275,6 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
return null;
}
/**
* We should wait for a subscription, otherwise, we might miss some changes.
*/
private void waitForSubscription(long timeout, TimeUnit unit) {
CountDownLatch subscribeLatch = new CountDownLatch(1);
subscriber = new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
subscribeLatch.countDown();
}
@Override
public void onNext(ConfigDiff item) {
}
@Override
public void onError(Throwable throwable) {
LOGGER.log(Level.CONFIG, "Error while subscribing a supplier to the changes.", throwable);
}
@Override
public void onComplete() {
LOGGER.log(Level.CONFIG, "The config suppliers will no longer receive any change.");
}
};
factory.provider().changes().subscribe(subscriber);
try {
subscribeLatch.await(timeout, unit);
} catch (InterruptedException e) {
LOGGER.log(Level.CONFIG, "Waiting for a supplier subscription has been interrupted.", e);
Thread.currentThread().interrupt();
}
}
private Config contextConfig(Config rootConfig) {
return rootConfig
.get(AbstractConfigImpl.this.prefix)
@@ -335,127 +284,13 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
@Override
public void onChange(Consumer<Config> onChangeConsumer) {
changesPublisher.subscribe(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
// I want all
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Config item) {
onChangeConsumer.accept(item);
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
}
void initMp() {
this.latestConfig.set(this);
List<io.helidon.config.spi.ConfigSource> configSources = factory.configSources();
if (configSources.isEmpty()) {
// if no config sources, then no changes
return;
}
if (configSources.size() == 1) {
if (configSources.get(0) == ConfigSources.EmptyConfigSourceHolder.EMPTY) {
// if the only config source is the empty one, then no changes
return;
}
}
io.helidon.config.spi.ConfigSource first = configSources.get(0);
if (first instanceof BuilderImpl.HelidonSourceWrapper) {
first = ((BuilderImpl.HelidonSourceWrapper) first).unwrap();
}
if (first instanceof ConfigSources.SystemPropertiesConfigSource) {
this.useSystemProperties = true;
}
for (io.helidon.config.spi.ConfigSource configSource : configSources) {
io.helidon.config.spi.ConfigSource it = configSource;
if (it instanceof BuilderImpl.HelidonSourceWrapper) {
it = ((BuilderImpl.HelidonSourceWrapper) it).unwrap();
}
if (it instanceof ConfigSources.EnvironmentVariablesConfigSource) {
// there is an env var config source
this.useEnvironmentVariables = true;
break;
}
}
onChange(latestConfig::set);
}
/**
* {@link Flow.Publisher} implementation that filters general {@link ConfigFactory#changes()} events to be wrapped by
* {@link FilteringConfigChangeEventSubscriber} for appropriate Config key and subscribers on the config node.
*/
private class FilteringConfigChangeEventPublisher implements Flow.Publisher<Config> {
private Flow.Publisher<ConfigDiff> delegate;
private FilteringConfigChangeEventPublisher(Flow.Publisher<ConfigDiff> delegate) {
this.delegate = delegate;
}
@Override
public void subscribe(Flow.Subscriber<? super Config> subscriber) {
delegate.subscribe(new FilteringConfigChangeEventSubscriber(subscriber));
}
}
/**
* {@link Flow.Subscriber} wrapper implementation that filters general {@link ConfigFactory#changes()} events
* for appropriate Config key and subscribers on the config node.
*
* @see FilteringConfigChangeEventPublisher
*/
private class FilteringConfigChangeEventSubscriber implements Flow.Subscriber<ConfigDiff> {
private final Flow.Subscriber<? super Config> delegate;
private Flow.Subscription subscription;
private FilteringConfigChangeEventSubscriber(Flow.Subscriber<? super Config> delegate) {
this.delegate = delegate;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
delegate.onSubscribe(subscription);
}
@Override
public void onNext(ConfigDiff event) {
//(3. fire just on case the sub-node has changed)
if (event.changedKeys().contains(AbstractConfigImpl.this.realKey)) {
delegate.onNext(AbstractConfigImpl.this.contextConfig(event.config()));
} else {
subscription.request(1);
}
}
@Override
public void onError(Throwable throwable) {
delegate.onError(throwable);
}
@Override
public void onComplete() {
delegate.onComplete();
}
factory.provider()
.onChange(event -> {
// check if change contains this node
if (event.changedKeys().contains(realKey)) {
onChangeConsumer.accept(contextConfig(event.config()));
}
});
}
/**
@@ -470,9 +305,6 @@ abstract class AbstractConfigImpl implements Config, org.eclipse.microprofile.co
@Override
public Config last() {
//the 'last config' behaviour is based on switched-on changes support
subscribe();
return AbstractConfigImpl.this.contextConfig(AbstractConfigImpl.this.factory.context().last());
}

View File

@@ -0,0 +1,199 @@
/*
* 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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Function;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
/**
* A base implementation for config sources, that combines configuration from any type of a config source.
* This class does not directly implement the interfaces - this is left to the implementer of the config source.
* This class provides configuration methods as {@code protected}, so you can make them public in your implementation, to only
* expose methods that must be implemented.
* <p>
* Other methods of the config source interfaces must be implemented by each source as they are data specific,
* such as {@link io.helidon.config.spi.PollableSource#isModified(Object)}.
*
* All other methods return reasonable defaults.
* Config framework analyzes the config source based on interfaces it implements.
*
* @see io.helidon.config.spi.ConfigSource
* @see io.helidon.config.spi.WatchableSource
* @see io.helidon.config.spi.PollableSource
* @see io.helidon.config.spi.ParsableSource
*/
public abstract class AbstractConfigSource extends AbstractSource implements ConfigSource {
private final Optional<String> mediaType;
private final Optional<ConfigParser> parser;
private final Optional<Function<Config.Key, Optional<String>>> mediaTypeMapping;
private final Optional<Function<Config.Key, Optional<ConfigParser>>> parserMapping;
private final boolean mediaMappingSupported;
/**
* Use common data from the builder to setup media type, parser, media type mapping, and
* parser mapping on this instance. Additional common data is handled by
* {@link io.helidon.config.AbstractSource#AbstractSource(AbstractSourceBuilder)}.
*
* @param builder builder used to set up this config source
*/
protected AbstractConfigSource(AbstractConfigSourceBuilder<?, ?> builder) {
super(builder);
this.mediaType = builder.mediaType();
this.parser = builder.parser();
this.mediaTypeMapping = builder.mediaTypeMapping();
this.parserMapping = builder.parserMapping();
this.mediaMappingSupported = mediaTypeMapping.isPresent() || parserMapping.isPresent();
}
/**
* Media type if on eis configured for parsing content of {@link io.helidon.config.spi.ParsableSource}.
* If there is none configured (default), a parser is chosen based on
* {@link io.helidon.config.spi.ConfigParser.Content#mediaType()} - media type detected during load of data.
*
* @return configured media type or empty if none configured
*/
protected Optional<String> mediaType() {
return mediaType;
}
/**
* Config parser if one is configured to use for parsing content of {@link io.helidon.config.spi.ParsableSource}.
* If one is not configured on a source (default), a parser is chosen based on {@link #mediaType()}.
*
* @return a configured parser, or empty if one should be chosen from media type (or if this is not a parsable source)
*/
protected Optional<ConfigParser> parser() {
return parser;
}
@Override
public String toString() {
return description();
}
ObjectNode processNodeMapping(Function<String, Optional<ConfigParser>> mediaToParser,
ConfigKeyImpl configKey,
ObjectNode loaded) {
if (!mediaMappingSupported) {
return loaded;
}
return processObject(mediaToParser, configKey, loaded);
}
private ObjectNode processObject(Function<String, Optional<ConfigParser>> mediaToParser,
ConfigKeyImpl key,
ObjectNode objectNode) {
ObjectNode.Builder builder = ObjectNode.builder();
objectNode.forEach((name, node) -> builder.addNode(name, processNode(mediaToParser, key.child(name), node)));
return builder.build();
}
private ConfigNode processNode(Function<String, Optional<ConfigParser>> mediaToParser,
ConfigKeyImpl key,
ConfigNode node) {
switch (node.nodeType()) {
case OBJECT:
return processObject(mediaToParser, key, (ObjectNode) node);
case LIST:
return processList(mediaToParser, key, (ConfigNode.ListNode) node);
case VALUE:
return processValue(mediaToParser, key, (ConfigNode.ValueNode) node);
default:
throw new IllegalArgumentException("Unsupported node type: " + node.getClass().getName());
}
}
private ConfigNode.ListNode processList(Function<String, Optional<ConfigParser>> mediaToParser,
ConfigKeyImpl key,
ConfigNode.ListNode listNode) {
ListNodeBuilderImpl builder = (ListNodeBuilderImpl) ConfigNode.ListNode.builder();
for (int i = 0; i < listNode.size(); i++) {
builder.addNode(processNode(mediaToParser, key.child(Integer.toString(i)), listNode.get(i)));
}
return builder.build();
}
private ConfigNode processValue(Function<String, Optional<ConfigParser>> mediaToParser,
Config.Key key,
ConfigNode.ValueNode valueNode) {
Optional<ConfigParser> parser = findParserForKey(mediaToParser, key);
if (parser.isEmpty()) {
return valueNode;
}
ConfigParser found = parser.get();
return found.parse(ConfigParser.Content.builder()
// value node must have a value
.data(toStream(valueNode.get()))
.charset(StandardCharsets.UTF_8)
.build());
}
private InputStream toStream(String string) {
return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8));
}
private Optional<ConfigParser> findParserForKey(Function<String, Optional<ConfigParser>> mediaToParser,
Config.Key key) {
// try to find it in parser mapping (explicit parser for a key)
Optional<ConfigParser> parser = parserMapping.flatMap(it -> it.apply(key));
if (parser.isPresent()) {
return parser;
}
// now based on media type
Optional<String> maybeMedia = mediaTypeMapping.flatMap(it -> it.apply(key));
if (maybeMedia.isEmpty()) {
// no media type configured, return empty
return Optional.empty();
}
String mediaType = maybeMedia.get();
// if media is explicit, parser is required
return Optional.of(mediaToParser.apply(mediaType)
.orElseThrow(() -> new ConfigException("Cannot find parser for media type "
+ mediaType
+ " for key "
+ key
+ " in config source "
+ description())));
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.Source;
/**
* Common ancestor for config source builders, taking care of configurable options understood by the config
* module.
*
* @param <B> Type of the builder implementation
* @param <U> Type of a target if this builder supports change watching, use {@code Void} if not
*/
public abstract class AbstractConfigSourceBuilder<B extends AbstractConfigSourceBuilder<B, U>, U>
extends AbstractSourceBuilder<B, U>
implements Source.Builder<B> {
private ConfigParser parser;
private String mediaType;
private Function<Config.Key, Optional<String>> mediaTypeMapping;
private Function<Config.Key, Optional<ConfigParser>> parserMapping;
@SuppressWarnings("unchecked")
private B me = (B) this;
/**
* {@inheritDoc}
*
* <table class="config">
* <caption>Media type and type mapping</caption>
* <tr>
* <td>media-type</td>
* <td>Media type from loaded data is used by default for parsable config sources</td>
* <td>Explicit media type to use, such as when a file has invalid suffix, or when we need to explicitly mark
* the media type.</td>
* </tr>
* <tr>
* <td>media-type-mapping</td>
* <td>No media type mapping is done by default</td>
* <td>A mapping of key to a media type, allowing us to have a key that contains a sub-tree (e.g. a key that contains
* json data)
* - when we configure a mapping of the key to {@code application/json}, the data would be expanded into config
* as a proper tree structure</td>
* </tr>
* </table>
* @param metaConfig meta configuration of this source
* @return updated builder instance
*/
protected B config(Config metaConfig) {
super.config(metaConfig);
metaConfig.get("media-type").asString().ifPresent(this::mediaType);
metaConfig.get("media-type-mapping").detach().asMap()
.ifPresent(this::mediaTypeMappingConfig);
return me;
}
private void mediaTypeMappingConfig(Map<String, String> mappingMap) {
mediaTypeMapping(key -> Optional.ofNullable(mappingMap.get(key.toString())));
}
/**
* Sets a function that maps keys to media type.
* This supports parsing of values using a {@link io.helidon.config.spi.ConfigParser} to expand an inlined
* configuration.
*
* @param mediaTypeMapping a mapping function
* @return a modified builder
*/
public B mediaTypeMapping(Function<Config.Key, Optional<String>> mediaTypeMapping) {
Objects.requireNonNull(mediaTypeMapping, "mediaTypeMapping cannot be null");
this.mediaTypeMapping = mediaTypeMapping;
return me;
}
/**
* Sets a function that maps keys to a parser.
* This supports parsing of specific values using a custom parser to expand an inlined configuration.
*
* @param parserMapping a mapping function
* @return a modified builder
*/
public B parserMapping(Function<Config.Key, Optional<ConfigParser>> parserMapping) {
Objects.requireNonNull(parserMapping, "parserMapping cannot be null");
this.parserMapping = parserMapping;
return me;
}
/**
* A parser if this is a {@link io.helidon.config.spi.ParsableSource} and explicit parser
* is configured.
*
* @param parser parser configured for this source
* @return updated builder instance
*/
protected B parser(ConfigParser parser) {
this.parser = parser;
return me;
}
Optional<Function<Config.Key, Optional<String>>> mediaTypeMapping() {
return Optional.ofNullable(mediaTypeMapping);
}
/**
* Parser mapping function.
* @return parser mapping
*/
Optional<Function<Config.Key, Optional<ConfigParser>>> parserMapping() {
return Optional.ofNullable(parserMapping);
}
/**
* Media type if this is a {@link io.helidon.config.spi.ParsableSource} and explicit media type
* is configured.
*
* @param mediaType media type configured for this source
* @return updated builder instance
*/
protected B mediaType(String mediaType) {
this.mediaType = mediaType;
return me;
}
Optional<ConfigParser> parser() {
return Optional.ofNullable(parser);
}
Optional<String> mediaType() {
return Optional.ofNullable(mediaType);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 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,13 +14,12 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
@@ -37,6 +36,7 @@ public abstract class AbstractNodeBuilderImpl<ID, B> {
private final B thisBuilder;
private Function<String, String> tokenResolver;
@SuppressWarnings("unchecked")
AbstractNodeBuilderImpl(Function<String, String> tokenResolver) {
this.tokenResolver = tokenResolver;
thisBuilder = (B) this;
@@ -155,10 +155,10 @@ public abstract class AbstractNodeBuilderImpl<ID, B> {
}
private void mergeValueMember(ValueNode member, MergingKey key, MergeableNode node, ID id) {
ObjectNode on = ObjectNodeBuilderImpl.create(Map.of(), tokenResolver).value(member.get()).build();
ObjectNode on = ObjectNodeBuilderImpl.create(Map.of(), tokenResolver).value(member.value()).build();
ConfigNode merged = ObjectNodeBuilderImpl
.create(on, tokenResolver) // make copy of member
.value(on.get())
.value(on.value())
.deepMerge(key.rest(), node) // merge it with specified node
.build();
@@ -169,7 +169,7 @@ public abstract class AbstractNodeBuilderImpl<ID, B> {
try {
// deep merge of list with specified node
ConfigNode merged = ListNodeBuilderImpl.from(member, tokenResolver) // make copy of member
.value(member.get())
.value(member.value())
.deepMerge(key.rest(), node) // merge it with specified node
.build();
// updates/replaces original member associated by id with new merged value
@@ -184,7 +184,7 @@ public abstract class AbstractNodeBuilderImpl<ID, B> {
// deep merge of object with specified node
ConfigNode merged = ObjectNodeBuilderImpl
.create(member, tokenResolver) // make copy of member
.value(member.get())
.value(member.value())
.deepMerge(key.rest(), node) // merge it with specified node
.build();
// updates/replaces original member associated by id with new merged value

View File

@@ -0,0 +1,109 @@
/*
* 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.util.Optional;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.RetryPolicy;
import io.helidon.config.spi.Source;
/**
* Source options as a super set of all possible combinations of source implementation.
* When used as a base class, together with {@link io.helidon.config.AbstractSourceBuilder}, you can set up any
* source type.
*
* @see io.helidon.config.AbstractSourceBuilder
* @see io.helidon.config.AbstractConfigSource
* @see io.helidon.config.AbstractConfigSourceBuilder
*/
public class AbstractSource implements Source {
// any source
private final boolean optional;
private final Optional<RetryPolicy> retryPolicy;
// pollable source
private final Optional<PollingStrategy> pollingStrategy;
// watchable source
private final Optional<ChangeWatcher<Object>> changeWatcher;
/**
* A new instance configured from the provided builder.
* The builder is used to set the following:
* <ul>
* <li>{@link #optional()} - for any source, whether the content must be present, or is optional</li>
* <li>{@link #retryPolicy()} - for any source, policy used to retry attempts at reading the content</li>
* <li>{@link #pollingStrategy()} - for {@link io.helidon.config.spi.PollableSource}, the polling strategy (if any)</li>
* <li>{@link #changeWatcher()} - for {@link io.helidon.config.spi.WatchableSource}, the change watcher (if any)</li>
* </ul>
* @param builder builder used to read the configuration options
*/
@SuppressWarnings("unchecked")
protected AbstractSource(AbstractSourceBuilder<?, ?> builder) {
this.optional = builder.isOptional();
this.pollingStrategy = builder.pollingStrategy();
this.retryPolicy = builder.retryPolicy();
this.changeWatcher = builder.changeWatcher().map(it -> (ChangeWatcher<Object>) it);
}
@Override
public Optional<RetryPolicy> retryPolicy() {
return retryPolicy;
}
@Override
public boolean optional() {
return optional;
}
/**
* A polling strategy of this source, if it implements {@link io.helidon.config.spi.PollableSource} and has one
* configured.
*
* @return polling strategy if any configured
*/
protected Optional<PollingStrategy> pollingStrategy() {
return pollingStrategy;
}
/**
* A change watcher of this source, if it implements {@link io.helidon.config.spi.WatchableSource} and has one
* configured.
*
* @return change watcher if any configured
*/
protected Optional<ChangeWatcher<Object>> changeWatcher() {
return changeWatcher;
}
/**
* Returns universal id of source to be used to construct {@link #description()}.
*
* @return universal id of source
*/
protected String uid() {
return "";
}
@Override
public String description() {
return Source.super.description()
+ "[" + uid() + "]"
+ (optional() ? "?" : "")
+ (pollingStrategy().isEmpty() ? "" : "*");
}
}

View File

@@ -0,0 +1,160 @@
/*
* 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.util.Optional;
import java.util.function.Supplier;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.RetryPolicy;
import io.helidon.config.spi.Source;
import io.helidon.config.spi.WatchableSource;
/**
* Base class for common builder methods of a {@link io.helidon.config.spi.Source}
* implementation.
*
* @param <B> type of implementation class of the builder
* @param <U> type of target for watchable sources, use {@code Void} if not supported
*/
public abstract class AbstractSourceBuilder<B extends AbstractSourceBuilder<B, U>, U> implements Source.Builder<B> {
private PollingStrategy pollingStrategy;
private RetryPolicy retryPolicy;
private ChangeWatcher<U> changeWatcher;
private boolean optional = false;
@SuppressWarnings("unchecked")
private B me = (B) this;
/**
* Configure builder from meta configuration.
* <p>
* The following configuration options are supported:
* <table class="config">
* <caption>Optional configuration parameters</caption>
* <tr>
* <th>key</th>
* <th>default value</th>
* <th>description</th>
* </tr>
* <tr>
* <td>optional</td>
* <td>{@code false}</td>
* <td>Configure to {@code true} if this source should not fail configuration setup when underlying data is missing.</td>
* </tr>
* <tr>
* <td>polling-strategy</td>
* <td>No polling strategy is added by default</td>
* <td>Meta configuration of a polling strategy to be used with this source, add configuration to {@code properties}
* sub node.</td>
* </tr>
* <tr>
* <td>change-watcher</td>
* <td>No change watcher is added by default</td>
* <td>Meta configuration of a change watcher to be used with this source, add configuration to {@code properties}
* sub node.</td>
* </tr>
* <tr>
* <td>retry-policy</td>
* <td>No retry policy is added by default</td>
* <td>Meta configuration of a retry policy to be used to load this source, add configuration to {@code properties}
* sub node.</td>
* </tr>
* </table>
*
* @param metaConfig meta configuration of this source
* @return updated builder instance
*/
@SuppressWarnings("unchecked")
protected B config(Config metaConfig) {
metaConfig.get("optional").asBoolean().ifPresent(this::optional);
metaConfig.get("polling-strategy").as(MetaConfig::pollingStrategy).ifPresent(this::pollingStrategy);
metaConfig.get("change-watcher").as(MetaConfig::changeWatcher).ifPresent(it -> changeWatcher((ChangeWatcher<U>) it));
metaConfig.get("retry-policy").as(MetaConfig::retryPolicy).ifPresent(this::retryPolicy);
return me;
}
@Override
public B retryPolicy(Supplier<? extends RetryPolicy> policy) {
this.retryPolicy = policy.get();
return me;
}
@Override
public B optional(boolean optional) {
this.optional = optional;
return me;
}
/**
* Configure a change watcher.
* This method must be exposed by builders of sources that change watching ({@link io.helidon.config.spi.WatchableSource}).
* The type of the change watcher must match the type of the target of this source.
*
* @param changeWatcher change watcher to use, such as {@link io.helidon.config.FileSystemWatcher}
* @return updated builder instance
*/
protected B changeWatcher(ChangeWatcher<U> changeWatcher) {
if (!(this instanceof WatchableSource.Builder)) {
throw new ConfigException("You are attempting to configure a change watcher on a source builder that does "
+ "not support it: " + getClass().getName());
}
this.changeWatcher = changeWatcher;
return me;
}
/**
* Configure a polling strategy.
* This method must be exposed by builders of sources that support polling.
*
* If you see this method as being protected in your builder, the source has removed
* support for polling, such as {@link io.helidon.config.ClasspathConfigSource}.
*
* @param pollingStrategy polling strategy to use
* @return updated builder instance
*/
protected B pollingStrategy(PollingStrategy pollingStrategy) {
if (!(this instanceof PollableSource.Builder)) {
throw new ConfigException("You are attempting to configure a polling strategy on a source builder that does "
+ "not support it: " + getClass().getName());
}
this.pollingStrategy = pollingStrategy;
return me;
}
Optional<PollingStrategy> pollingStrategy() {
return Optional.ofNullable(pollingStrategy);
}
Optional<RetryPolicy> retryPolicy() {
return Optional.ofNullable(retryPolicy);
}
Optional<ChangeWatcher<U>> changeWatcher() {
return Optional.ofNullable(changeWatcher);
}
boolean isOptional() {
return optional;
}
}

View File

@@ -18,26 +18,24 @@ package io.helidon.config;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Priority;
import io.helidon.common.GenericType;
import io.helidon.common.HelidonFeatures;
import io.helidon.common.HelidonFlavor;
@@ -45,28 +43,24 @@ import io.helidon.common.Prioritized;
import io.helidon.common.serviceloader.HelidonServiceLoader;
import io.helidon.common.serviceloader.Priorities;
import io.helidon.config.ConfigMapperManager.MapperProviders;
import io.helidon.config.internal.ConfigThreadFactory;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.spi.AbstractMpSource;
import io.helidon.config.spi.AbstractSource;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigMapper;
import io.helidon.config.spi.ConfigMapperProvider;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigParser;
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.
*/
class BuilderImpl implements Config.Builder {
static final Executor DEFAULT_CHANGES_EXECUTOR = Executors.newCachedThreadPool(new ConfigThreadFactory("config"));
static {
HelidonFeatures.register(HelidonFlavor.SE, "Config");
}
@@ -75,11 +69,16 @@ class BuilderImpl implements Config.Builder {
* Config sources
*/
// sources to be sorted by priority
private final List<PrioritizedConfigSource> prioritizedSources = new ArrayList<>();
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 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;
private boolean hasEnvVarSource;
/*
* Config mapper providers
*/
@@ -97,13 +96,11 @@ class BuilderImpl implements Config.Builder {
*/
private final List<Function<Config, ConfigFilter>> filterProviders;
private boolean filterServicesEnabled;
/**
* Change support now uses Flow API, which seems like an overkill.
* @deprecated change support must be refactored, it is too complex
/*
* change support
*/
@Deprecated
private Executor changesExecutor;
private int changesMaxBuffer;
/*
* Other configuration.
*/
@@ -114,6 +111,7 @@ class BuilderImpl implements Config.Builder {
*/
private boolean cachingEnabled;
private boolean keyResolving;
private boolean valueResolving;
private boolean systemPropertiesSourceEnabled;
private boolean environmentVariablesSourceEnabled;
private boolean envVarAliasGeneratorEnabled;
@@ -131,9 +129,8 @@ class BuilderImpl implements Config.Builder {
filterProviders = new ArrayList<>();
filterServicesEnabled = true;
cachingEnabled = true;
changesExecutor = DEFAULT_CHANGES_EXECUTOR;
changesMaxBuffer = Flow.defaultBufferSize();
keyResolving = true;
valueResolving = true;
systemPropertiesSourceEnabled = true;
environmentVariablesSourceEnabled = true;
envVarAliasGeneratorEnabled = false;
@@ -163,12 +160,15 @@ class BuilderImpl implements Config.Builder {
sources.add(source);
if (source instanceof ConfigSources.EnvironmentVariablesConfigSource) {
envVarAliasGeneratorEnabled = true;
hasEnvVarSource = true;
} else if (source instanceof ConfigSources.SystemPropertiesConfigSource) {
hasSystemPropertiesSource = true;
}
return this;
}
@Override
public Config.Builder overrides(Supplier<OverrideSource> overridingSource) {
public Config.Builder overrides(Supplier<? extends OverrideSource> overridingSource) {
this.overrideSource = overridingSource.get();
return this;
}
@@ -288,12 +288,6 @@ class BuilderImpl implements Config.Builder {
return this;
}
@Override
public Config.Builder changesMaxBuffer(int changesMaxBuffer) {
this.changesMaxBuffer = changesMaxBuffer;
return this;
}
@Override
public Config.Builder disableKeyResolving() {
keyResolving = false;
@@ -302,6 +296,7 @@ class BuilderImpl implements Config.Builder {
@Override
public Config.Builder disableValueResolving() {
this.valueResolving = false;
return this;
}
@@ -319,6 +314,12 @@ class BuilderImpl implements Config.Builder {
@Override
public AbstractConfigImpl build() {
if (valueResolving) {
addFilter(ConfigFilters.valueResolving());
}
if (null == changesExecutor) {
changesExecutor = Executors.newCachedThreadPool(new ConfigThreadFactory("config-changes"));
}
if (configSourceServicesEnabled) {
// add MP config sources from service loader (if not already done)
mpAddDiscoveredSources();
@@ -328,14 +329,66 @@ class BuilderImpl implements Config.Builder {
mpAddDiscoveredConverters();
}
return buildProvider().newConfig();
/*
* Now prepare the config runtime.
* We need the following setup:
* 1. Mappers
* 2. Filters
* 3. Config Sources
*/
/*
Mappers
*/
ConfigMapperManager configMapperManager = buildMappers(prioritizedMappers,
mapperProviders,
mapperServicesEnabled);
/*
Filters
*/
if (filterServicesEnabled) {
addAutoLoadedFilters();
}
/*
Config Sources
*/
// collect all sources (in correct order)
ConfigContextImpl context = new ConfigContextImpl(changesExecutor, buildParsers(parserServicesEnabled, parsers));
ConfigSourcesRuntime configSources = buildConfigSources(context);
Function<String, List<String>> aliasGenerator = envVarAliasGeneratorEnabled
? EnvironmentVariableAliases::aliasesOf
: null;
//config provider
return createProvider(configMapperManager,
configSources,
new OverrideSourceRuntime(overrideSource),
filterProviders,
cachingEnabled,
changesExecutor,
keyResolving,
aliasGenerator)
.newConfig();
}
private static void addBuiltInMapperServices(List<PrioritizedMapperProvider> prioritizedMappers) {
// we must add default mappers using a known priority (200), so they can be overridden by services
// and yet we can still define a service that is only used after these (such as config beans)
prioritizedMappers
.add(new HelidonMapperWrapper(new InternalMapperProvider(ConfigMappers.essentialMappers(),
"essential"), 200));
prioritizedMappers
.add(new HelidonMapperWrapper(new InternalMapperProvider(ConfigMappers.builtInMappers(),
"built-in"), 200));
}
@Override
public Config.Builder config(Config metaConfig) {
metaConfig.get("caching.enabled").asBoolean().ifPresent(this::cachingEnabled);
metaConfig.get("key-resolving.enabled").asBoolean().ifPresent(this::keyResolvingEnabled);
metaConfig.get("value-resolving.enabled").asBoolean().ifPresent(this::valueResolvingEnabled);
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);
@@ -347,7 +400,7 @@ class BuilderImpl implements Config.Builder {
metaConfig.get("sources")
.asNodeList()
.ifPresent(list -> list.forEach(it -> sourceList.add(MetaConfig.configSource(it))));
.ifPresent(list -> list.forEach(it -> sourceList.addAll(MetaConfig.configSource(it))));
sourceList.forEach(this::addSource);
sourceList.clear();
@@ -360,6 +413,12 @@ class BuilderImpl implements Config.Builder {
return this;
}
@Override
public Config.Builder mergingStrategy(MergingStrategy strategy) {
this.mergingStrategy = strategy;
return this;
}
private void configSourceServicesEnabled(boolean enabled) {
this.configSourceServicesEnabled = enabled;
}
@@ -393,13 +452,24 @@ class BuilderImpl implements Config.Builder {
}
void mpAddDefaultSources() {
prioritizedSources.add(new HelidonSourceWrapper(ConfigSources.systemProperties(), 100));
prioritizedSources.add(new HelidonSourceWrapper(ConfigSources.environmentVariables(), 100));
prioritizedSources.add(new HelidonSourceWrapper(ConfigSources.classpath("application.yaml").optional().build(), 100));
hasEnvVarSource = true;
hasSystemPropertiesSource = true;
prioritizedSources.add(new HelidonSourceWithPriority(ConfigSources.systemProperties()
.pollingStrategy(PollingStrategies
.regular(Duration.ofSeconds(2))
.build())
.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(AbstractSource.Builder::build)
.map(source -> new HelidonSourceWrapper(source, 100))
.map(io.helidon.common.Builder::build)
.map(source -> new HelidonSourceWithPriority(source, 100))
.forEach(prioritizedSources::add);
}
@@ -424,7 +494,7 @@ class BuilderImpl implements Config.Builder {
.forEach(mpSources::add));
for (org.eclipse.microprofile.config.spi.ConfigSource source : mpSources) {
prioritizedSources.add(new MpSourceWrapper(source));
prioritizedMpSources.add(new PrioritizedMpSource(source));
}
}
@@ -447,14 +517,11 @@ class BuilderImpl implements Config.Builder {
void mpWithSources(org.eclipse.microprofile.config.spi.ConfigSource... sources) {
for (org.eclipse.microprofile.config.spi.ConfigSource source : sources) {
PrioritizedConfigSource pcs;
if (source instanceof AbstractMpSource) {
pcs = new HelidonSourceWrapper((AbstractMpSource<?>) source);
if (source instanceof AbstractConfigSource) {
prioritizedSources.add(new HelidonSourceWithPriority((ConfigSource) source, null));
} else {
pcs = new MpSourceWrapper(source);
prioritizedMpSources.add(new PrioritizedMpSource(source));
}
this.prioritizedSources.add(pcs);
}
}
@@ -492,114 +559,72 @@ class BuilderImpl implements Config.Builder {
parserServicesEnabled = aBoolean;
}
private void valueResolvingEnabled(Boolean aBoolean) {
// TODO this is a noop as is disableValueResolving
}
private void keyResolvingEnabled(Boolean aBoolean) {
this.keyResolving = aBoolean;
}
private ProviderImpl buildProvider() {
private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) {
List<ConfigSourceRuntimeBase> targetSources = new LinkedList<>();
//context
ConfigContext context = new ConfigContextImpl(buildParsers(parserServicesEnabled, parsers));
//source
ConfigSourceConfiguration targetConfigSource = targetConfigSource(context);
//mappers
Priorities.sort(prioritizedMappers);
// as the mapperProviders.add adds the last as first, we need to reverse order
Collections.reverse(prioritizedMappers);
prioritizedMappers.forEach(mapperProviders::add);
ConfigMapperManager configMapperManager = buildMappers(mapperServicesEnabled, mapperProviders);
if (filterServicesEnabled) {
addAutoLoadedFilters();
if (systemPropertiesSourceEnabled && !hasSystemPropertiesSource) {
hasSystemPropertiesSource = true;
targetSources.add(context.sourceRuntimeBase(ConfigSources.systemProperties().build()));
}
Function<String, List<String>> aliasGenerator = envVarAliasGeneratorEnabled
? EnvironmentVariableAliases::aliasesOf
: null;
//config provider
return createProvider(configMapperManager,
targetConfigSource,
overrideSource,
filterProviders,
cachingEnabled,
changesExecutor,
changesMaxBuffer,
keyResolving,
aliasGenerator);
}
private ConfigSourceConfiguration targetConfigSource(ConfigContext context) {
List<ConfigSource> targetSources = new LinkedList<>();
if (systemPropertiesSourceEnabled
&& !hasSourceType(ConfigSources.SystemPropertiesConfigSource.class)) {
targetSources.add(ConfigSources.systemProperties());
if (environmentVariablesSourceEnabled && !hasEnvVarSource) {
hasEnvVarSource = true;
targetSources.add(context.sourceRuntimeBase(ConfigSources.environmentVariables()));
}
if (hasSourceType(ConfigSources.EnvironmentVariablesConfigSource.class)) {
envVarAliasGeneratorEnabled = true;
} else if (environmentVariablesSourceEnabled) {
targetSources.add(ConfigSources.environmentVariables());
if (hasEnvVarSource) {
envVarAliasGeneratorEnabled = true;
}
if (sources.isEmpty() && prioritizedSources.isEmpty()) {
// if there are no sources configured, use meta-configuration (only in case we have no prioritized sources)
targetSources.addAll(MetaConfig.configSources(mediaType -> context.findParser(mediaType).isPresent()));
boolean nothingConfigured = sources.isEmpty() && prioritizedSources.isEmpty() && prioritizedMpSources.isEmpty();
if (nothingConfigured) {
// use meta configuration to load all sources
MetaConfig.configSources(mediaType -> context.findParser(mediaType).isPresent())
.stream()
.map(context::sourceRuntimeBase)
.forEach(targetSources::add);
} else {
targetSources.addAll(sources);
// add all configured or discovered sources
// configured sources are always first in the list (explicitly added by user)
sources.stream()
.map(context::sourceRuntimeBase)
.forEach(targetSources::add);
// prioritized sources are next
targetSources.addAll(mergePrioritized(context));
}
// initialize all target sources
targetSources.forEach(it -> it.init(context));
if (!prioritizedSources.isEmpty()) {
// initialize all prioritized sources (before we sort them - otherwise we cannot get priority)
prioritizedSources.forEach(it -> it.init(context));
Priorities.sort(prioritizedSources);
targetSources.addAll(prioritizedSources);
}
if (targetSources.size() == 1) {
// the only source does not require a composite wrapper
return new ConfigSourceConfiguration(targetSources.get(0), targetSources);
}
return new ConfigSourceConfiguration(ConfigSources.create(targetSources.toArray(new ConfigSource[0])).build(),
targetSources);
// targetSources now contain runtimes correctly ordered for each config source
return new ConfigSourcesRuntime(targetSources, mergingStrategy);
}
private boolean hasSourceType(Class<?> sourceType) {
private List<ConfigSourceRuntimeBase> mergePrioritized(ConfigContextImpl context) {
List<PrioritizedConfigSource> allPrioritized = new ArrayList<>(this.prioritizedMpSources);
prioritizedSources.stream()
.map(it -> new PrioritizedHelidonSource(it, context))
.forEach(allPrioritized::add);
for (ConfigSource source : sources) {
if (sourceType.isAssignableFrom(source.getClass())) {
return true;
}
}
Priorities.sort(allPrioritized);
for (PrioritizedConfigSource prioritizedSource : prioritizedSources) {
if (sourceType.isAssignableFrom(prioritizedSource.configSourceClass())) {
return true;
}
}
return false;
return allPrioritized
.stream()
.map(it -> it.runtime(context))
.collect(Collectors.toList());
}
@SuppressWarnings("ParameterNumber")
ProviderImpl createProvider(ConfigMapperManager configMapperManager,
ConfigSourceConfiguration targetConfigSource,
OverrideSource overrideSource,
ConfigSourcesRuntime targetConfigSource,
OverrideSourceRuntime overrideSource,
List<Function<Config, ConfigFilter>> filterProviders,
boolean cachingEnabled,
Executor changesExecutor,
int changesMaxBuffer,
boolean keyResolving,
Function<String, List<String>> aliasGenerator) {
return new ProviderImpl(configMapperManager,
@@ -608,7 +633,6 @@ class BuilderImpl implements Config.Builder {
filterProviders,
cachingEnabled,
changesExecutor,
changesMaxBuffer,
keyResolving,
aliasGenerator);
}
@@ -624,40 +648,40 @@ class BuilderImpl implements Config.Builder {
return parsers;
}
static ConfigMapperManager buildMappers(boolean servicesEnabled,
MapperProviders userDefinedProviders) {
// this is a unit test method
static ConfigMapperManager buildMappers(MapperProviders userDefinedProviders) {
return buildMappers(new ArrayList<>(), userDefinedProviders, false);
}
static ConfigMapperManager buildMappers(List<PrioritizedMapperProvider> prioritizedMappers,
MapperProviders userDefinedProviders,
boolean mapperServicesEnabled) {
// prioritized mapper providers
if (mapperServicesEnabled) {
loadMapperServices(prioritizedMappers);
}
addBuiltInMapperServices(prioritizedMappers);
Priorities.sort(prioritizedMappers);
// as the mapperProviders.add adds the last as first, we need to reverse order
Collections.reverse(prioritizedMappers);
MapperProviders providers = MapperProviders.create();
List<ConfigMapperProvider> prioritizedProviders = new LinkedList<>();
// we must add default mappers using a known priority (49), so they can be overridden by services
// and yet we can still define a service that is only used after these (such as config beans)
prioritizedProviders.add(new InternalPriorityMapperProvider(ConfigMappers.essentialMappers()));
prioritizedProviders.add(new InternalPriorityMapperProvider(ConfigMappers.builtInMappers()));
if (servicesEnabled) {
loadMapperServices(prioritizedProviders);
}
prioritizedProviders = ConfigUtils
.asPrioritizedStream(prioritizedProviders, ConfigMapperProvider.PRIORITY)
.collect(Collectors.toList());
// add built in converters and converters from service loader
prioritizedProviders.forEach(providers::add);
// these are added first, as they end up last
prioritizedMappers.forEach(providers::add);
// user defined converters always have priority over anything else
providers.addAll(userDefinedProviders);
return new ConfigMapperManager(providers);
}
private static void loadMapperServices(List<ConfigMapperProvider> providers) {
private static void loadMapperServices(List<PrioritizedMapperProvider> providers) {
HelidonServiceLoader.builder(ServiceLoader.load(ConfigMapperProvider.class))
.defaultPriority(ConfigMapperProvider.PRIORITY)
.build()
.forEach(providers::add);
.forEach(mapper -> providers.add(new HelidonMapperWrapper(mapper, Priorities.find(mapper, 100))));
}
private static List<ConfigParser> loadParserServices() {
@@ -698,28 +722,42 @@ 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 Executor changesExecutor;
private final List<ConfigParser> configParsers;
/**
* Creates a config context.
*
* @param configParsers a config parsers
*/
ConfigContextImpl(List<ConfigParser> configParsers) {
ConfigContextImpl(Executor changesExecutor, List<ConfigParser> configParsers) {
this.changesExecutor = changesExecutor;
this.configParsers = configParsers;
}
@Override
public Optional<ConfigParser> findParser(String mediaType) {
if (mediaType == null) {
throw new NullPointerException("Unknown media type of resource.");
}
public ConfigSourceRuntime sourceRuntime(ConfigSource source) {
return sourceRuntimeBase(source);
}
private ConfigSourceRuntimeBase sourceRuntimeBase(ConfigSource source) {
return runtimes.computeIfAbsent(source, it -> new ConfigSourceRuntimeImpl(this, source));
}
Optional<ConfigParser> findParser(String mediaType) {
Objects.requireNonNull(mediaType, "Unknown media type of resource.");
return configParsers.stream()
.filter(parser -> parser.supportedMediaTypes().contains(mediaType))
.findFirst();
}
ConfigSourceRuntimeBase sourceRuntime(org.eclipse.microprofile.config.spi.ConfigSource source) {
return mpRuntimes.computeIfAbsent(source, it -> new ConfigSourceMpRuntimeImpl(source));
}
Executor changesExecutor() {
return changesExecutor;
}
}
/**
@@ -744,21 +782,24 @@ class BuilderImpl implements Config.Builder {
}
/**
* Internal mapper with low priority to enable overrides.
*/
@Priority(200)
static class InternalPriorityMapperProvider implements ConfigMapperProvider {
static class InternalMapperProvider implements ConfigMapperProvider {
private final Map<Class<?>, Function<Config, ?>> converterMap;
private final String name;
InternalPriorityMapperProvider(Map<Class<?>, Function<Config, ?>> converterMap) {
InternalMapperProvider(Map<Class<?>, Function<Config, ?>> converterMap, String name) {
this.converterMap = converterMap;
this.name = name;
}
@Override
public Map<Class<?>, Function<Config, ?>> mappers() {
return converterMap;
}
@Override
public String toString() {
return name + " internal mappers";
}
}
private interface PrioritizedMapperProvider extends Prioritized,
@@ -775,9 +816,7 @@ class BuilderImpl implements Config.Builder {
int priority) {
this.converter = converter;
this.priority = priority;
this.converterMap.put(theClass, config -> {
return config.asString().as(converter::convert).get();
});
this.converterMap.put(theClass, config -> config.asString().as(converter::convert).get());
}
@Override
@@ -829,24 +868,28 @@ class BuilderImpl implements Config.Builder {
public <T> Optional<BiFunction<Config, ConfigMapper, T>> mapper(GenericType<T> type) {
return delegate.mapper(type);
}
@Override
public String toString() {
return priority + ": " + delegate;
}
}
private interface PrioritizedConfigSource extends Prioritized,
ConfigSource,
org.eclipse.microprofile.config.spi.ConfigSource {
Class<?> configSourceClass();
private interface PrioritizedConfigSource extends Prioritized {
ConfigSourceRuntimeBase runtime(ConfigContextImpl context);
}
private static final class MpSourceWrapper implements PrioritizedConfigSource {
private static final class PrioritizedMpSource implements PrioritizedConfigSource {
private final org.eclipse.microprofile.config.spi.ConfigSource delegate;
private MpSourceWrapper(org.eclipse.microprofile.config.spi.ConfigSource delegate) {
private PrioritizedMpSource(org.eclipse.microprofile.config.spi.ConfigSource delegate) {
this.delegate = delegate;
}
@Override
public Class<?> configSourceClass() {
return delegate.getClass();
public ConfigSourceRuntimeBase runtime(ConfigContextImpl context) {
return context.sourceRuntime(delegate);
}
@Override
@@ -869,172 +912,57 @@ class BuilderImpl implements Config.Builder {
// priority of 1
return 101 - priority;
}
@Override
public Optional<ConfigNode.ObjectNode> load() throws ConfigException {
return Optional.of(ConfigUtils.mapToObjectNode(getProperties(), false));
}
@Override
public Map<String, String> getProperties() {
return delegate.getProperties();
}
@Override
public String getValue(String propertyName) {
return delegate.getValue(propertyName);
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public String description() {
return delegate.toString();
}
@Override
public String toString() {
return description();
}
}
static final class HelidonSourceWrapper implements PrioritizedConfigSource {
private static final class PrioritizedHelidonSource implements PrioritizedConfigSource {
private final HelidonSourceWithPriority source;
private final ConfigContext context;
private final AbstractMpSource<?> delegate;
private Integer explicitPriority;
private HelidonSourceWrapper(AbstractMpSource<?> delegate) {
this.delegate = delegate;
}
private HelidonSourceWrapper(AbstractMpSource<?> delegate, int explicitPriority) {
this.delegate = delegate;
this.explicitPriority = explicitPriority;
}
AbstractMpSource<?> unwrap() {
return delegate;
private PrioritizedHelidonSource(HelidonSourceWithPriority source, ConfigContext context) {
this.source = source;
this.context = context;
}
@Override
public Class<?> configSourceClass() {
return delegate.getClass();
public ConfigSourceRuntimeBase runtime(ConfigContextImpl context) {
return context.sourceRuntimeBase(source.unwrap());
}
@Override
public int priority() {
// ordinal from data
String value = delegate.getValue(CONFIG_ORDINAL);
if (null != value) {
return 101 - Integer.parseInt(value);
}
return source.priority(context);
}
}
private static final class HelidonSourceWithPriority {
private final ConfigSource configSource;
private final Integer explicitPriority;
private HelidonSourceWithPriority(ConfigSource configSource, Integer explicitPriority) {
this.configSource = configSource;
this.explicitPriority = explicitPriority;
}
ConfigSource unwrap() {
return configSource;
}
int priority(ConfigContext context) {
// first - explicit priority. If configured by user, return it
if (null != explicitPriority) {
return explicitPriority;
}
// priority from Prioritized and annotation
return Priorities.find(delegate, 100);
}
@Override
public Map<String, String> getProperties() {
return delegate.getProperties();
}
@Override
public String getValue(String propertyName) {
return delegate.getValue(propertyName);
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public Set<String> getPropertyNames() {
return delegate.getPropertyNames();
}
@Override
public String description() {
return delegate.description();
}
@Override
public Optional<ConfigNode.ObjectNode> load() throws ConfigException {
return delegate.load();
}
@Override
public ConfigSource get() {
return delegate.get();
}
@Override
public void init(ConfigContext context) {
delegate.init(context);
}
@Override
public Flow.Publisher<Optional<ConfigNode.ObjectNode>> changes() {
return delegate.changes();
}
@Override
public void close() throws Exception {
delegate.close();
}
@Override
public String toString() {
return description();
// ordinal from data
return context.sourceRuntime(configSource)
.node(CONFIG_ORDINAL)
.flatMap(node -> node.value()
.map(Integer::parseInt))
.orElseGet(() -> {
// the config source does not have an ordinal configured, I need to get it from other places
return Priorities.find(configSource, 100);
});
}
}
static final class ConfigSourceConfiguration {
private static final ConfigSourceConfiguration EMPTY =
new ConfigSourceConfiguration(ConfigSources.empty(), List.of(ConfigSources.empty()));
private final ConfigSource compositeSource;
private final List<ConfigSource> allSources;
private ConfigSourceConfiguration(ConfigSource compositeSource, List<ConfigSource> allSources) {
this.compositeSource = compositeSource;
this.allSources = allSources;
}
static ConfigSourceConfiguration empty() {
return EMPTY;
}
ConfigSource compositeSource() {
return compositeSource;
}
List<ConfigSource> allSources() {
return allSources;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConfigSourceConfiguration that = (ConfigSourceConfiguration) o;
return compositeSource.equals(that.compositeSource)
&& allSources.equals(that.allSources);
}
@Override
public int hashCode() {
return Objects.hash(compositeSource, allSources);
}
}
}

View File

@@ -16,32 +16,45 @@
package io.helidon.config;
import java.nio.file.Path;
import java.time.Instant;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import io.helidon.common.LazyValue;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.spi.AbstractParsableConfigSource;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParser.Content;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.ParsableSource;
/**
* {@link ConfigSource} implementation that loads configuration content from a resource on a classpath.
*
* @see AbstractParsableConfigSource.Builder
* Classpath config source does not support changes (neither through polling nor through change notifications).
*/
public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant> {
private static final String RESOURCE_KEY = "resource";
public class ClasspathConfigSource extends AbstractConfigSource implements ConfigSource,
ParsableSource {
private final String resource;
private final URL resourceUrl;
private final LazyValue<Optional<String>> mediaType;
ClasspathConfigSource(ClasspathBuilder builder, String resource) {
ClasspathConfigSource(Builder builder) {
super(builder);
this.resource = resource.startsWith("/")
? resource.substring(1)
: resource;
this.resource = builder.resource;
this.resourceUrl = builder.url;
mediaType = LazyValue.create(() -> {
if (null == resourceUrl) {
return MediaTypes.detectType(resource);
} else {
return MediaTypes.detectType(resource);
}
});
}
/**
@@ -51,7 +64,7 @@ public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant>
* <ul>
* <li>{@code resource} - type {@code String}</li>
* </ul>
* Optional {@code properties}: see {@link AbstractParsableConfigSource.Builder#config(io.helidon.config.Config)}.
* Optional {@code properties}: see {@link AbstractConfigSourceBuilder#config(io.helidon.config.Config)}.
*
* @param metaConfig meta-configuration used to initialize returned config source instance from.
* @return new instance of config source described by {@code metaConfig}
@@ -60,7 +73,7 @@ public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant>
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see io.helidon.config.ConfigSources#classpath(String)
* @see AbstractParsableConfigSource.Builder#config(Config)
* @see AbstractConfigSourceBuilder#config(Config)
*/
public static ClasspathConfigSource create(Config metaConfig) throws ConfigMappingException, MissingValueException {
return builder()
@@ -68,48 +81,102 @@ public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant>
.build();
}
/**
* Create a config source for the first resource on the classpath.
*
* @param resource resource to find
* @return a config source based on the classpath resource
*/
public static ClasspathConfigSource create(String resource) {
return builder().resource(resource).build();
}
/**
* Create config source for each resource on the classpath.
*
* @param resource resource to find
* @return a collection of sources for each resource present on the classpath, always at least one
*/
public static Collection<? super ClasspathConfigSource> createAll(String resource) {
Enumeration<URL> resources = findAllResources(resource);
if (resources.hasMoreElements()) {
List<? super ClasspathConfigSource> sources = new LinkedList<>();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
sources.add(builder().url(url).build());
}
return sources;
} else {
// there is none - let the default source handle it, to manage optional vs. mandatory
// with configuration and not an empty list
return List.of(create(resource));
}
}
/**
* Create config source for each resource on the classpath.
*
* @param metaConfig meta configuration of the config source
* @return a collection of sources for each resource present on the classpath
*/
public static List<ConfigSource> createAll(Config metaConfig) {
// this must fail if the resource is not defined
String resource = metaConfig.get("resource").asString().get();
Enumeration<URL> resources = findAllResources(resource);
if (resources.hasMoreElements()) {
List<ConfigSource> sources = new LinkedList<>();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
sources.add(builder()
.config(metaConfig)
// url must be configured after meta config, to override the default
.url(url)
.build());
}
return sources;
} else {
// there is none - let the default source handle it, to manage optional vs. mandatory
// with configuration and not an empty list
return List.of(create(metaConfig));
}
}
/**
* Create a new fluent API builder for classpath config source.
*
* @return a new builder instance
*/
public static ClasspathBuilder builder() {
return new ClasspathBuilder();
public static Builder builder() {
return new Builder();
}
@Override
protected String uid() {
return ClasspathSourceHelper.uid(resource);
return (null == resourceUrl) ? resource : resourceUrl.toString();
}
@Override
protected Optional<String> mediaType() {
return super.mediaType()
.or(this::probeContentType);
}
public Optional<Content> load() throws ConfigException {
if (null == resourceUrl) {
return Optional.empty();
}
private Optional<String> probeContentType() {
return MediaTypes.detectType(resource);
}
InputStream inputStream;
try {
inputStream = resourceUrl.openStream();
} catch (IOException e) {
throw new ConfigException("Failed to read configuration from classpath, resource: " + resource, e);
}
@Override
protected Optional<Instant> dataStamp() {
return Optional.of(ClasspathSourceHelper.resourceTimestamp(resource));
}
Content.Builder builder = Content.builder()
.data(inputStream);
@Override
protected ConfigParser.Content<Instant> content() throws ConfigException {
return ClasspathSourceHelper.content(resource,
description(),
(inputStreamReader, instant) -> {
ConfigParser.Content.Builder<Instant> builder = ConfigParser.Content
.builder(inputStreamReader);
mediaType.get().ifPresent(builder::mediaType);
builder.stamp(instant);
mediaType().ifPresent(builder::mediaType);
return builder.build();
});
return Optional.of(builder.build());
}
@Override
@@ -117,70 +184,54 @@ public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant>
return "classpath: " + resource;
}
@Override
public Optional<String> mediaType() {
return super.mediaType();
}
@Override
public Optional<ConfigParser> parser() {
return super.parser();
}
private static Enumeration<URL> findAllResources(String resource) {
String cleaned = resource.startsWith("/") ? resource.substring(1) : resource;
try {
return Thread.currentThread()
.getContextClassLoader()
.getResources(cleaned);
} catch (IOException e) {
throw new ConfigException("Could not access config resource " + resource, e);
}
}
/**
* Classpath ConfigSource Builder.
* <p>
* It allows to configure following properties:
* <ul>
* <li>{@code resource} - configuration resource name;</li>
* <li>{@code mandatory} - is existence of configuration resource mandatory (by default) or is {@code optional}?</li>
* <li>{@code optional} - is existence of configuration resource mandatory (by default) or is {@code optional}?</li>
* <li>{@code media-type} - configuration content media type to be used to look for appropriate {@link ConfigParser};</li>
* <li>{@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;</li>
* </ul>
* <p>
* If the ConfigSource is {@code mandatory} and a {@code resource} does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* then {@link io.helidon.config.spi.ParsableSource#load} throws {@link ConfigException}.
* <p>
* If {@code media-type} not set it tries to guess it from resource extension.
*/
public static final class ClasspathBuilder extends Builder<ClasspathBuilder, Path, ClasspathConfigSource> {
public static final class Builder extends AbstractConfigSourceBuilder<Builder, Void>
implements ParsableSource.Builder<Builder>,
io.helidon.common.Builder<ClasspathConfigSource> {
private URL url;
private String resource;
/**
* Initialize builder.
*/
private ClasspathBuilder() {
super(Path.class);
}
/**
* Configure the classpath resource to load the configuration from.
*
* @param resource resource on classpath
* @return updated builder instance
*/
public ClasspathBuilder resource(String resource) {
this.resource = resource;
return this;
}
/**
* {@inheritDoc}
* <ul>
* <li>{@code resource} - the classpath resource to load</li>
* </ul>
* @param metaConfig configuration properties used to configure a builder instance.
* @return updated builder instance
*/
@Override
public ClasspathBuilder config(Config metaConfig) {
metaConfig.get(RESOURCE_KEY).asString().ifPresent(this::resource);
return super.config(metaConfig);
}
@Override
protected Path target() {
try {
Path resourcePath = ClasspathSourceHelper.resourcePath(resource);
if (resourcePath != null) {
return resourcePath;
} else {
throw new ConfigException("Could not find a filesystem path for resource '" + resource + "'.");
}
} catch (Exception ex) {
throw new ConfigException("Could not find a filesystem path for resource '" + resource + "'.", ex);
}
private Builder() {
}
/**
@@ -192,14 +243,55 @@ public class ClasspathConfigSource extends AbstractParsableConfigSource<Instant>
*/
@Override
public ClasspathConfigSource build() {
if (null == resource) {
throw new IllegalArgumentException("resource must be defined");
}
return new ClasspathConfigSource(this, resource);
return new ClasspathConfigSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
/**
* {@inheritDoc}
* <ul>
* <li>{@code resource} - the classpath resource to load</li>
* </ul>
* @param metaConfig configuration properties used to configure a builder instance.
* @return updated builder instance
*/
@Override
public Builder config(Config metaConfig) {
metaConfig.get("resource").asString().ifPresent(this::resource);
return super.config(metaConfig);
}
@Override
public Builder parser(ConfigParser parser) {
return super.parser(parser);
}
@Override
public Builder mediaType(String mediaType) {
return super.mediaType(mediaType);
}
/**
* Configure the classpath resource to load the configuration from.
*
* @param resource resource on classpath
* @return updated builder instance
*/
public Builder resource(String resource) {
String cleaned = resource.startsWith("/") ? resource.substring(1) : resource;
this.resource = resource;
// the URL may not exist, and that is fine - maybe we are an optional config source
this.url = Thread.currentThread()
.getContextClassLoader()
.getResource(cleaned);
return this;
}
private Builder url(URL url) {
this.url = url;
return this;
}
}
}

View File

@@ -16,29 +16,30 @@
package io.helidon.config;
import java.nio.file.Path;
import java.time.Instant;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import io.helidon.config.spi.AbstractOverrideSource;
import io.helidon.config.spi.ConfigContent.OverrideContent;
import io.helidon.config.spi.OverrideSource;
import io.helidon.config.spi.PollingStrategy;
/**
* {@link OverrideSource} implementation that loads override definitions from a resource on a classpath.
*
* @see Builder
* @see io.helidon.config.spi.Source.Builder
*/
public class ClasspathOverrideSource extends AbstractOverrideSource<Instant> {
public class ClasspathOverrideSource extends AbstractSource implements OverrideSource {
private final String resource;
private final URL resourceUrl;
ClasspathOverrideSource(ClasspathBuilder builder) {
ClasspathOverrideSource(Builder builder) {
super(builder);
String builderResource = builder.resource;
this.resource = builderResource.startsWith("/")
? builderResource.substring(1)
: builderResource;
this.resource = builder.resource;
this.resourceUrl = builder.url;
}
@Override
@@ -47,19 +48,22 @@ public class ClasspathOverrideSource extends AbstractOverrideSource<Instant> {
}
@Override
protected Optional<Instant> dataStamp() {
return Optional.of(ClasspathSourceHelper.resourceTimestamp(resource));
}
public Optional<OverrideContent> load() throws ConfigException {
if (null == resourceUrl) {
return Optional.empty();
}
@Override
public Data<OverrideData, Instant> loadData() throws ConfigException {
return ClasspathSourceHelper.content(resource,
description(),
(inputStreamReader, instant) -> {
return new Data<>(
Optional.of(OverrideData.create(inputStreamReader)),
Optional.of(instant));
});
InputStream inputStream;
try {
inputStream = resourceUrl.openStream();
} catch (IOException e) {
throw new ConfigException("Failed to read configuration from classpath, resource: " + resource, e);
}
OverrideData data = OverrideData.create(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
return Optional.of(OverrideContent.builder()
.data(data)
.build());
}
/**
@@ -76,8 +80,8 @@ public class ClasspathOverrideSource extends AbstractOverrideSource<Instant> {
*
* @return a new builder
*/
public static ClasspathBuilder builder() {
return new ClasspathBuilder();
public static Builder builder() {
return new Builder();
}
/**
@@ -92,55 +96,16 @@ public class ClasspathOverrideSource extends AbstractOverrideSource<Instant> {
* If the {@code OverrideSource} is {@code mandatory} and the {@code resource} does not exist
* then {@link OverrideSource#load} throws {@link ConfigException}.
*/
public static final class ClasspathBuilder extends Builder<ClasspathBuilder, Path> {
public static final class Builder extends AbstractSourceBuilder<Builder, Void>
implements io.helidon.common.Builder<ClasspathOverrideSource> {
private URL url;
private String resource;
/**
* Initialize builder.
*/
private ClasspathBuilder() {
super(Path.class);
}
/**
* Configure the classpath resource to be used as a source.
*
* @param resource classpath resource path
* @return updated builder instance
*/
public ClasspathBuilder resource(String resource) {
this.resource = resource;
return this;
}
/**
* Update builder from meta configuration.
*
* @param metaConfig meta configuration to load this override source from
* @return updated builder instance
*/
public ClasspathBuilder config(Config metaConfig) {
metaConfig.get("resource").asString().ifPresent(this::resource);
return super.config(metaConfig);
}
@Override
protected Path target() {
if (null == resource) {
throw new IllegalArgumentException("Resource name must be defined.");
}
try {
Path resourcePath = ClasspathSourceHelper.resourcePath(resource);
if (resourcePath != null) {
return resourcePath;
} else {
throw new ConfigException("Could not find a filesystem path for resource '" + resource + "'.");
}
} catch (Exception ex) {
throw new ConfigException("Could not find a filesystem path for resource '" + resource + "'.", ex);
}
private Builder() {
}
/**
@@ -153,8 +118,34 @@ public class ClasspathOverrideSource extends AbstractOverrideSource<Instant> {
return new ClasspathOverrideSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
/**
* Update builder from meta configuration.
*
* @param metaConfig meta configuration to load this override source from
* @return updated builder instance
*/
public Builder config(Config metaConfig) {
metaConfig.get("resource").asString().ifPresent(this::resource);
return super.config(metaConfig);
}
/**
* Configure the classpath resource to be used as a source.
*
* @param resource classpath resource path
* @return updated builder instance
*/
public Builder resource(String resource) {
String cleaned = resource.startsWith("/") ? resource.substring(1) : resource;
this.resource = resource;
// the URL may not exist, and that is fine - maybe we are an optional config source
this.url = Thread.currentThread()
.getContextClassLoader()
.getResource(cleaned);
return this;
}
}
}

View File

@@ -17,11 +17,9 @@
package io.helidon.config;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -91,7 +89,7 @@ class ClasspathSourceHelper {
static <T> T content(String resource,
String description,
BiFunction<InputStreamReader, Instant, T> processor) throws ConfigException {
BiFunction<InputStream, Instant, T> processor) throws ConfigException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(resource);
@@ -110,7 +108,7 @@ class ClasspathSourceHelper {
LOGGER.log(Level.FINE, "Error to get resource '" + resource + "' path. Used ClassLoader: " + classLoader, ex);
}
return processor.apply(new InputStreamReader(inputStream, StandardCharsets.UTF_8), resourceTimestamp);
return processor.apply(inputStream, resourceTimestamp);
}
}

View File

@@ -1,271 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SubmissionPublisher;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import io.helidon.config.internal.ConfigThreadFactory;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.internal.ObjectNodeImpl;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigSource;
/**
* A {@link ConfigSource} that wraps an ordered collection of {@code ConfigSource} instances
* as a single configuration source.
* <p>
* The constructor accepts a {@link ConfigSources.MergingStrategy} which it uses
* to resolve same-named properties loaded from different underlying sources.
*
* @see ConfigSources.CompositeBuilder
* @see ConfigSources.MergingStrategy
*/
class CompositeConfigSource implements ConfigSource {
//TODO would be possible to extend AbstractConfigSource also by CompositeConfigSource?
static final ScheduledExecutorService DEFAULT_CHANGES_EXECUTOR_SERVICE =
Executors.newScheduledThreadPool(0, new ConfigThreadFactory("composite-source"));
private static final Logger LOGGER = Logger.getLogger(CompositeConfigSource.class.getName());
private final Map<ConfigSource, Optional<ObjectNode>> lastObjectNodes;
private final ConfigSources.MergingStrategy mergingStrategy;
private final String description;
private final SubmissionPublisher<Optional<ObjectNode>> changesSubmitter;
private final Flow.Publisher<Optional<ObjectNode>> changesPublisher;
private final ConfigUtils.ScheduledTask reloadTask;
private Optional<ObjectNode> lastObjectNode;
private List<ConfigSourceChangeEventSubscriber> compositeConfigSourcesSubscribers;
CompositeConfigSource(List<ConfigSource> configSources,
ConfigSources.MergingStrategy mergingStrategy,
ScheduledExecutorService reloadExecutorService,
Duration debounceTimeout,
int changesMaxBuffer) {
this.mergingStrategy = mergingStrategy;
description = configSources.stream()
.map(ConfigSource::description)
.collect(Collectors.joining("->"));
changesSubmitter = new SubmissionPublisher<>(Runnable::run, //deliver events on current thread
changesMaxBuffer);
changesPublisher = ConfigHelper.suspendablePublisher(
changesSubmitter,
this::subscribeConfigSourceChangesSubscriptions,
this::cancelConfigSourceChangesSubscriptions);
lastObjectNodes = new LinkedHashMap<>(configSources.size());
configSources.forEach(source -> lastObjectNodes.put(source, Optional.empty()));
reloadTask = new ConfigUtils.ScheduledTask(reloadExecutorService, this::reload, debounceTimeout);
}
private void subscribeConfigSourceChangesSubscriptions() {
compositeConfigSourcesSubscribers = new LinkedList<>();
for (ConfigSource source : lastObjectNodes.keySet()) {
ConfigSourceChangeEventSubscriber subscriber = new ConfigSourceChangeEventSubscriber(source);
source.changes().subscribe(subscriber);
compositeConfigSourcesSubscribers.add(subscriber);
}
}
private void cancelConfigSourceChangesSubscriptions() {
compositeConfigSourcesSubscribers.forEach(ConfigSourceChangeEventSubscriber::cancelSubscription);
compositeConfigSourcesSubscribers = null;
}
@Override
public String description() {
return description;
}
@Override
public void init(ConfigContext context) {
for (ConfigSource configSource : lastObjectNodes.keySet()) {
configSource.init(context);
}
}
@Override
public Optional<ObjectNode> load() {
//load
for (ConfigSource configSource : lastObjectNodes.keySet()) {
Optional<ObjectNode> loadedNode = configSource.load()
.map(ObjectNodeImpl::wrap)
.map(objectNode -> objectNode.initDescription(configSource.description()));
lastObjectNodes.put(configSource, loadedNode);
}
//merge
lastObjectNode = mergeLoaded();
return lastObjectNode;
}
Optional<ObjectNode> lastObjectNode() {
return lastObjectNode;
}
List<ConfigSourceChangeEventSubscriber> compositeConfigSourcesSubscribers() {
return compositeConfigSourcesSubscribers;
}
private Optional<ObjectNode> mergeLoaded() {
List<ObjectNode> rootNodes = lastObjectNodes.values().stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
if (rootNodes.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(mergingStrategy.merge(rootNodes));
}
}
private void scheduleReload(ConfigSource source, Optional<ObjectNode> changedObjectNode) {
Optional<ObjectNode> originalObjectNode = lastObjectNodes.get(source);
if (!Objects.equals(originalObjectNode, changedObjectNode)) {
lastObjectNodes.put(source, changedObjectNode);
reloadTask.schedule();
} else {
LOGGER.log(Level.FINE,
String.format("Source data has not changed. Will not try to reload from config source %s.",
source.description()));
}
}
/**
* Method is executed asynchronously.
*/
private void reload() {
try {
Optional<ObjectNode> newObjectNode = mergeLoaded();
if (!Objects.equals(lastObjectNode, newObjectNode)) {
LOGGER.log(Level.FINER, String.format("Config source %s has been reloaded.", description()));
lastObjectNode = newObjectNode;
fireChangeEvent();
} else {
LOGGER.log(Level.FINE,
String.format("Source data has not changed. Will not try to reload from config source %s.",
description()));
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING,
String.format("Error merging of loaded config sources %s. "
+ "New configuration has not been used. Maybe later.",
description()));
LOGGER.log(Level.CONFIG, String.format("Failing reload of '%s' cause.", description()), ex);
}
}
private void fireChangeEvent() {
changesSubmitter.offer(lastObjectNode,
(subscriber, objectNode) -> {
LOGGER.log(Level.FINER,
String.format("Object node %s has not been delivered to %s.",
objectNode,
subscriber));
return false;
});
}
/**
* {@inheritDoc}
* <p>
* All subscribers are notified on a same thread provided by
* {@link ConfigSources.CompositeBuilder#changesExecutor(ScheduledExecutorService) changes executor service}.
*/
@Override
public Flow.Publisher<Optional<ObjectNode>> changes() {
return changesPublisher;
}
/**
* Composite {@link Flow.Subscriber} that is used to subscribe on each {@link ConfigSource} the Composite ConfigSource
* delegates to.
*/
private class ConfigSourceChangeEventSubscriber implements Flow.Subscriber<Optional<ObjectNode>> {
private final ConfigSource configSource;
private Flow.Subscription subscription;
private ConfigSourceChangeEventSubscriber(ConfigSource configSource) {
this.configSource = configSource;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(Optional<ObjectNode> objectNode) {
LOGGER.fine(String.format("'%s' config source has changed: %s", configSource, objectNode));
CompositeConfigSource.this.scheduleReload(configSource, objectNode);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
CompositeConfigSource.this.changesSubmitter
.closeExceptionally(new ConfigException(
String.format("'%s' config source changes support has failed. %s",
configSource.description(),
throwable.getLocalizedMessage()),
throwable));
//TODO whenever the last completed/error we should call: CompositeConfigSource.this.changesPublisher.close()
}
@Override
public void onComplete() {
LOGGER.fine(String.format("'%s' config source changes support has completed.", configSource.description()));
//TODO whenever the last completed/error we should call: CompositeConfigSource.this.changesPublisher.close()
}
private void cancelSubscription() {
if (subscription != null) {
subscription.cancel();
}
}
}
}

View File

@@ -22,7 +22,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -30,11 +29,11 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import io.helidon.common.GenericType;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigMapperProvider;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.MergingStrategy;
import io.helidon.config.spi.OverrideSource;
/**
@@ -235,28 +234,8 @@ import io.helidon.config.spi.OverrideSource;
* 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
* config system deals with multiple sources as follows.
* <p>
* The {@link ConfigSources.CompositeBuilder} class handles multiple config
* sources; in fact the config system uses an instance of that builder
* automatically when your application invokes {@link Config#create} and
* {@link Config#builder}, for example. Each such composite builder has a
* merging strategy that controls how the config system will search the multiple
* config sources for a given key. By default each {@code CompositeBuilder} uses
* the {@link FallbackMergingStrategy}: configuration sources earlier in the
* list have a higher priority than the later ones. The system behaves as if,
* when resolving a value of a key, it checks each source in sequence order. As
* soon as one source contains a value for the key the system returns that
* value, ignoring any sources that fall later in the list.
* <p>
* Your application can set a different strategy by constructing its own
* {@code CompositeBuilder} and invoking
* {@link ConfigSources.CompositeBuilder#mergingStrategy(ConfigSources.MergingStrategy)}, passing the strategy
* to be used:
* <pre>
* Config.withSources(ConfigSources.create(source1, source2, source3)
* .mergingStrategy(new MyMergingStrategy());
* </pre>
* config system merges these together so that values from config sources with higher priority have
* precedence over values from config sources with lower priority.
*/
public interface Config {
/**
@@ -353,9 +332,6 @@ public interface Config {
* @see Builder#sources(List)
* @see Builder#disableEnvironmentVariablesSource()
* @see Builder#disableSystemPropertiesSource()
* @see ConfigSources#create(Supplier[])
* @see ConfigSources.CompositeBuilder
* @see ConfigSources.MergingStrategy
*/
@SafeVarargs
static Config create(Supplier<? extends ConfigSource>... configSources) {
@@ -383,9 +359,6 @@ public interface Config {
* @see Builder#sources(List)
* @see Builder#disableEnvironmentVariablesSource()
* @see Builder#disableSystemPropertiesSource()
* @see ConfigSources#create(Supplier[])
* @see ConfigSources.CompositeBuilder
* @see ConfigSources.MergingStrategy
*/
@SafeVarargs
static Builder builder(Supplier<? extends ConfigSource>... configSources) {
@@ -985,12 +958,12 @@ public interface Config {
enum Type {
/**
* Config node is an object of named members
* ({@link #VALUE values}, {@link #LIST lists} or other {@link #OBJECT objects}).
* ({@link #VALUE values}, {@link #LIST lists} or other objects).
*/
OBJECT(true, false),
/**
* Config node is a list of indexed elements
* ({@link #VALUE values}, {@link #OBJECT objects} or other {@link #LIST lists}).
* ({@link #VALUE values}, {@link #OBJECT objects} or other lists).
*/
LIST(true, false),
/**
@@ -1054,7 +1027,7 @@ public interface Config {
* Returns instance of Config node related to same Config {@link Config#key() key}
* as original {@link Config#context() node} used to get Context from.
* <p>
* If the configuration has not been reloaded yet it returns original Config node instance.
* This method uses the last known value of the node, as provided through change support.
*
* @return the last instance of Config node associated with same key as original node
* @see Config#context()
@@ -1158,15 +1131,6 @@ public interface Config {
* the value is found in a configuration source, the value immediately is returned without consulting any of the remaining
* configuration sources in the prioritized collection.
* <p>
* This is default implementation of
* {@link ConfigSources#create(Supplier...)} Composite ConfigSource} provided by
* {@link ConfigSources.MergingStrategy#fallback() Fallback MergingStrategy}.
* It is possible to {@link ConfigSources.CompositeBuilder#mergingStrategy(ConfigSources.MergingStrategy)
* use custom implementation of merging strategy}.
* <pre>
* builder.source(ConfigSources.create(source1, source2, source3)
* .mergingStrategy(new MyMergingStrategy));
* </pre>
* Target source is composed from following sources, in order:
* <ol>
* <li>{@link ConfigSources#environmentVariables() environment variables config source}<br>
@@ -1180,9 +1144,6 @@ public interface Config {
* @return an updated builder instance
* @see #disableEnvironmentVariablesSource()
* @see #disableSystemPropertiesSource()
* @see ConfigSources#create(Supplier...)
* @see ConfigSources.CompositeBuilder
* @see ConfigSources.MergingStrategy
*/
Builder sources(List<Supplier<? extends ConfigSource>> configSources);
@@ -1194,6 +1155,21 @@ public interface Config {
*/
Builder addSource(ConfigSource source);
/**
* Merging Strategy to use when more than one config source is used.
*
* @param strategy strategy to use, defaults to a strategy where a value for first source wins over values from later
* sources
* @return updated builder instance
*/
Builder mergingStrategy(MergingStrategy strategy);
/**
* Add a single config source to this builder.
*
* @param source config source to add
* @return updated builder instance
*/
default Builder addSource(Supplier<? extends ConfigSource> source) {
return addSource(source.get());
}
@@ -1292,7 +1268,7 @@ public interface Config {
* @param overridingSource a source with overriding key patterns and assigned values
* @return an updated builder instance
*/
Builder overrides(Supplier<OverrideSource> overridingSource);
Builder overrides(Supplier<? extends OverrideSource> overridingSource);
/**
* Disables an usage of resolving key tokens.
@@ -1309,7 +1285,9 @@ public interface Config {
* <p>
* A value can contain tokens enclosed in {@code ${}} (i.e. ${name}), that are resolved by default and tokens are replaced
* with a value of the key with the token as a key.
*
* <p>
* By default a value resolving filter is added to configuration. When this method is called, the filter will
* not be added and value resolving will be disabled
* @return an updated builder instance
*/
Builder disableValueResolving();
@@ -1418,10 +1396,10 @@ public interface Config {
Builder disableMapperServices();
/**
* Registers a {@link ConfigParser} instance that can be used by registered {@link ConfigSource}s to
* parse {@link ConfigParser.Content configuration content}.
* Parsers are tried to be used by {@link io.helidon.config.spi.ConfigContext#findParser(String)}
* in same order as was registered by the {@link #addParser(ConfigParser)} method.
* Registers a {@link ConfigParser} instance that can be used by config system to parse
* parse {@link io.helidon.config.spi.ConfigParser.Content} of {@link io.helidon.config.spi.ParsableSource}.
* Parsers {@link io.helidon.config.spi.ConfigParser#supportedMediaTypes()} is queried
* in same order as was registered by this method.
* Programmatically registered parsers have priority over other options.
* <p>
* As another option, parsers are loaded automatically as a {@link java.util.ServiceLoader service}, if not
@@ -1449,7 +1427,7 @@ public interface Config {
* Registers a {@link ConfigFilter} instance that will be used by {@link Config} to
* filter elementary value before it is returned to a user.
* <p>
* Filters are applied in same order as was registered by the {@link #addFilter(ConfigFilter)}, {@link
* Filters are applied in same order as was registered by the this method, {@link
* #addFilter(Function)} or {@link #addFilter(Supplier)} method.
* <p>
* {@link ConfigFilter} is actually a {@link java.util.function.BiFunction}&lt;{@link String},{@link String},{@link
@@ -1476,8 +1454,8 @@ public interface Config {
* Registers a {@link ConfigFilter} provider as a {@link Function}&lt;{@link Config}, {@link ConfigFilter}&gt;. An
* obtained filter will be used by {@link Config} to filter elementary value before it is returned to a user.
* <p>
* Filters are applied in same order as was registered by the {@link #addFilter(ConfigFilter)}, {@link
* #addFilter(Function)} or {@link #addFilter(Supplier)} method.
* Filters are applied in same order as was registered by the {@link #addFilter(ConfigFilter)}, this method,
* or {@link #addFilter(Supplier)} method.
* <p>
* Registered provider's {@link Function#apply(Object)} method is called every time the new Config is created. Eg. when
* this builder's {@link #build} method creates the {@link Config} or when the new
@@ -1497,7 +1475,7 @@ public interface Config {
* returned to a user.
* <p>
* Filters are applied in same order as was registered by the {@link #addFilter(ConfigFilter)}, {@link
* #addFilter(Function)} or {@link #addFilter(Supplier)} method.
* #addFilter(Function)}, or this method.
* <p>
* Registered provider's {@link Function#apply(Object)} method is called every time the new Config is created. Eg. when
* this builder's {@link #build} method creates the {@link Config} or when the new
@@ -1541,33 +1519,17 @@ public interface Config {
/**
* Specifies "observe-on" {@link Executor} to be used by {@link Config#onChange(java.util.function.Consumer)} to deliver
* new Config instance.
* Executor is also used to process reloading of config from appropriate {@link ConfigSource#changes() source}.
* Executor is also used to process reloading of config from appropriate {@link ConfigSource source}.
* <p>
* By default dedicated thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are available is used.
*
* @param changesExecutor the executor to use for async delivery of {@link Config#onChange(java.util.function.Consumer)}
* @return an updated builder instance
* @see #changesMaxBuffer(int)
* @see Config#onChange(java.util.function.Consumer)
*/
Builder changesExecutor(Executor changesExecutor);
/**
* Specifies maximum capacity for each subscriber's buffer to be used by
* {@link Config#onChange(java.util.function.Consumer)} to deliver new Config instance.
* <p>
* By default {@link Flow#defaultBufferSize()} is used.
* <p>
* Note: Not consumed events will be dropped off.
*
* @param changesMaxBuffer the maximum capacity for each subscriber's buffer of new config events.
* @return an updated builder instance
* @see #changesExecutor(Executor)
* @see Config#onChange(java.util.function.Consumer)
*/
Builder changesMaxBuffer(int changesMaxBuffer);
/**
* Builds new instance of {@link Config}.
*
@@ -1664,6 +1626,12 @@ public interface Config {
* <td>{@link io.helidon.config.spi.ConfigSourceProvider#create(String, Config)}</td>
* </tr>
* <tr>
* <td>multi-source</td>
* <td>{@code false}</td>
* <td>If set to true, the provider creates more than one config source to be added</td>
* <td>{@link io.helidon.config.spi.ConfigSourceProvider#createMulti(String, Config)}</td>
* </tr>
* <tr>
* <td>properties</td>
* <td>&nbsp;</td>
* <td>Configuration options to configure the config source (meta configuration of a source)</td>
@@ -1673,21 +1641,28 @@ public interface Config {
* <tr>
* <td>properties.optional</td>
* <td>false</td>
* <td>Most config sources can be configured to be optional</td>
* <td>{@link io.helidon.config.spi.AbstractSource.Builder#optional(boolean)}</td>
* <td>Config sources can be configured to be optional</td>
* <td>{@link io.helidon.config.spi.Source#optional()}</td>
* </tr>
* <tr>
* <td>properties.polling-strategy</td>
* <td>&nbsp;</td>
* <td>Some config sources can have a polling strategy defined</td>
* <td>{@link io.helidon.config.spi.AbstractSource.Builder#pollingStrategy(java.util.function.Function)},
* <td>{@link io.helidon.config.spi.PollableSource.Builder#pollingStrategy(io.helidon.config.spi.PollingStrategy)},
* {@link MetaConfig#pollingStrategy(Config)}</td>
* </tr>
* <tr>
* <td>properties.change-watcher</td>
* <td>&nbsp;</td>
* <td>Some config sources can have a change watcher defined</td>
* <td>{@link io.helidon.config.spi.WatchableSource.Builder#changeWatcher(io.helidon.config.spi.ChangeWatcher)},
* {@link MetaConfig#changeWatcher(Config)}</td>
* </tr>
* <tr>
* <td>properties.retry-policy</td>
* <td>&nbsp;</td>
* <td>Some config sources can have a retry policy defined</td>
* <td>{@link io.helidon.config.spi.AbstractSource.Builder#retryPolicy(io.helidon.config.spi.RetryPolicy)},
* <td>Config sources can have a retry policy defined</td>
* <td>{@link io.helidon.config.spi.Source#retryPolicy()},
* {@link MetaConfig#retryPolicy(Config)}</td>
* </tr>
* </table>
@@ -1702,7 +1677,7 @@ public interface Config {
* optional: true
* path: "conf/dev-application.yaml"
* polling-strategy:
* type: "watch"
* type: "regular"
* retry-policy:
* type: "repeat"
* properties:
@@ -1712,6 +1687,9 @@ public interface Config {
* optional: true
* resource: "application.yaml"
* </pre>
*
* @param metaConfig meta configuration to set this builder up
* @return updated builder from meta configuration
*/
Builder config(Config metaConfig);
}

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.
@@ -21,7 +21,6 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode;

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.
@@ -22,7 +22,6 @@ import java.util.Optional;
import java.util.function.Function;
import io.helidon.common.GenericType;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode;
@@ -56,19 +55,13 @@ abstract class ConfigExistingImpl<N extends ConfigNode> extends AbstractConfigIm
@Override
public final Optional<String> value() throws ConfigMappingException {
String value = node().get();
if (null != value) {
return Optional.ofNullable(filter.apply(realKey(), value));
} else {
// even if this is a tree node, we want to return empty, as this node does not have a value
// and that is a good state (as complex nodes are allowed to have a direct value)
return Optional.empty();
}
return node.value()
.map(it -> filter.apply(realKey(), it));
}
@Override
public boolean hasValue() {
return null != node().get();
return node().value().isPresent();
}
@Override

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.
@@ -19,24 +19,14 @@ package io.helidon.config;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
@@ -55,9 +45,8 @@ final class ConfigFactory {
private final ProviderImpl provider;
private final Function<String, List<String>> aliasGenerator;
private final ConcurrentMap<PrefixedKey, Reference<AbstractConfigImpl>> configCache;
private final Flow.Publisher<ConfigDiff> changesPublisher;
private final Instant timestamp;
private final List<ConfigSource> configSources;
private final List<ConfigSourceRuntimeBase> configSources;
private final List<org.eclipse.microprofile.config.spi.ConfigSource> mpConfigSources;
/**
@@ -74,7 +63,7 @@ final class ConfigFactory {
ConfigFilter filter,
ProviderImpl provider,
Function<String, List<String>> aliasGenerator,
List<ConfigSource> configSources) {
List<ConfigSourceRuntimeBase> configSources) {
Objects.requireNonNull(mapperManager, "mapperManager argument is null.");
Objects.requireNonNull(node, "node argument is null.");
@@ -82,52 +71,20 @@ final class ConfigFactory {
Objects.requireNonNull(provider, "provider argument is null.");
this.mapperManager = mapperManager;
this.fullKeyToNodeMap = createFullKeyToNodeMap(node);
this.fullKeyToNodeMap = ConfigHelper.createFullKeyToNodeMap(node);
this.filter = filter;
this.provider = provider;
this.aliasGenerator = aliasGenerator;
this.configSources = configSources;
configCache = new ConcurrentHashMap<>();
changesPublisher = new FilteringConfigChangeEventPublisher(provider.changes());
timestamp = Instant.now();
this.mpConfigSources = configSources.stream()
.map(ConfigFactory::toMpSource)
.map(ConfigSourceRuntime::asMpSource)
.collect(Collectors.toList());
}
private static Map<ConfigKeyImpl, ConfigNode> createFullKeyToNodeMap(ObjectNode objectNode) {
Map<ConfigKeyImpl, ConfigNode> result;
Stream<Map.Entry<ConfigKeyImpl, ConfigNode>> flattenNodes = objectNode.entrySet()
.stream()
.map(node -> flattenNodes(ConfigKeyImpl.of(node.getKey()), node.getValue()))
.reduce(Stream.empty(), Stream::concat);
result = flattenNodes.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
result.put(ConfigKeyImpl.of(), objectNode);
return result;
}
static Stream<Map.Entry<ConfigKeyImpl, ConfigNode>> flattenNodes(ConfigKeyImpl key, ConfigNode node) {
switch (node.nodeType()) {
case OBJECT:
return ((ObjectNode) node).entrySet().stream()
.map(e -> flattenNodes(key.child(e.getKey()), e.getValue()))
.reduce(Stream.of(new AbstractMap.SimpleEntry<>(key, node)), Stream::concat);
case LIST:
return IntStream.range(0, ((ListNode) node).size())
.boxed()
.map(i -> flattenNodes(key.child(Integer.toString(i)), ((ListNode) node).get(i)))
.reduce(Stream.of(new AbstractMap.SimpleEntry<>(key, node)), Stream::concat);
case VALUE:
return Stream.of(new AbstractMap.SimpleEntry<>(key, node));
default:
throw new IllegalArgumentException("Invalid node type.");
}
}
public Instant timestamp() {
return timestamp;
}
@@ -199,10 +156,6 @@ final class ConfigFactory {
return node;
}
public Flow.Publisher<ConfigDiff> changes() {
return changesPublisher;
}
/**
* Returns whole configuration context.
*
@@ -216,7 +169,7 @@ final class ConfigFactory {
return provider;
}
List<ConfigSource> configSources() {
List<ConfigSourceRuntimeBase> configSources() {
return configSources;
}
@@ -224,206 +177,6 @@ final class ConfigFactory {
return mpConfigSources;
}
private static org.eclipse.microprofile.config.spi.ConfigSource toMpSource(ConfigSource helidonCs) {
if (helidonCs instanceof org.eclipse.microprofile.config.spi.ConfigSource) {
return (org.eclipse.microprofile.config.spi.ConfigSource) helidonCs;
} else {
// this is a non-Helidon ConfigSource
return new MpConfigSource(helidonCs);
}
}
private static final class MpConfigSource implements org.eclipse.microprofile.config.spi.ConfigSource {
private final AtomicReference<Map<String, String>> currentValues = new AtomicReference<>();
private final Object lock = new Object();
private final ConfigSource delegate;
private MpConfigSource(ConfigSource helidonCs) {
this.delegate = helidonCs;
delegate.changes()
.subscribe(new Flow.Subscriber<Optional<ObjectNode>>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Optional<ObjectNode> item) {
synchronized (lock) {
currentValues.set(loadMap(item));
}
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
}
@Override
public Map<String, String> getProperties() {
ensureValue();
return currentValues.get();
}
@Override
public String getValue(String propertyName) {
ensureValue();
return currentValues.get().get(propertyName);
}
@Override
public String getName() {
return delegate.description();
}
private void ensureValue() {
synchronized (lock) {
if (null == currentValues.get()) {
currentValues.set(loadMap(delegate.load()));
}
}
}
private static Map<String, String> loadMap(Optional<ObjectNode> item) {
if (item.isPresent()) {
ConfigNode.ObjectNode node = item.get();
Map<String, String> values = new TreeMap<>();
processNode(values, "", node);
return values;
} else {
return Map.of();
}
}
private static void processNode(Map<String, String> values, String keyPrefix, ConfigNode.ObjectNode node) {
node.forEach((key, configNode) -> {
switch (configNode.nodeType()) {
case OBJECT:
processNode(values, key(keyPrefix, key), (ConfigNode.ObjectNode) configNode);
break;
case LIST:
processNode(values, key(keyPrefix, key), ((ConfigNode.ListNode) configNode));
break;
case VALUE:
values.put(key(keyPrefix, key), configNode.get());
break;
default:
throw new IllegalStateException("Config node of type: " + configNode.nodeType() + " not supported");
}
});
}
private static void processNode(Map<String, String> values, String keyPrefix, ConfigNode.ListNode node) {
List<String> directValue = new LinkedList<>();
Map<String, String> thisListValues = new HashMap<>();
boolean hasDirectValue = true;
for (int i = 0; i < node.size(); i++) {
ConfigNode configNode = node.get(i);
String nextKey = key(keyPrefix, String.valueOf(i));
switch (configNode.nodeType()) {
case OBJECT:
processNode(thisListValues, nextKey, (ConfigNode.ObjectNode) configNode);
hasDirectValue = false;
break;
case LIST:
processNode(thisListValues, nextKey, (ConfigNode.ListNode) configNode);
hasDirectValue = false;
break;
case VALUE:
String value = configNode.get();
directValue.add(value);
thisListValues.put(nextKey, value);
break;
default:
throw new IllegalStateException("Config node of type: " + configNode.nodeType() + " not supported");
}
}
if (hasDirectValue) {
values.put(keyPrefix, String.join(",", directValue));
} else {
values.putAll(thisListValues);
}
}
private static String key(String keyPrefix, String key) {
if (keyPrefix.isEmpty()) {
return key;
}
return keyPrefix + "." + key;
}
}
/**
* {@link Flow.Publisher} implementation that filters original {@link ProviderImpl#changes()} events to be wrapped by
* {@link FilteringConfigChangeEventSubscriber} to ignore events about current Config instance.
*/
private class FilteringConfigChangeEventPublisher implements Flow.Publisher<ConfigDiff> {
private Flow.Publisher<ConfigDiff> delegate;
private FilteringConfigChangeEventPublisher(Flow.Publisher<ConfigDiff> delegate) {
this.delegate = delegate;
}
@Override
public void subscribe(Flow.Subscriber<? super ConfigDiff> subscriber) {
delegate.subscribe(new FilteringConfigChangeEventSubscriber(subscriber));
}
}
/**
* {@link Flow.Subscriber} wrapper implementation that filters original {@link ProviderImpl#changes()} events
* and ignore events about current Config instance.
*
* @see FilteringConfigChangeEventPublisher
*/
private class FilteringConfigChangeEventSubscriber implements Flow.Subscriber<ConfigDiff> {
private final Flow.Subscriber<? super ConfigDiff> delegate;
private Flow.Subscription subscription;
private FilteringConfigChangeEventSubscriber(Flow.Subscriber<? super ConfigDiff> delegate) {
this.delegate = delegate;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
delegate.onSubscribe(subscription);
}
@Override
public void onNext(ConfigDiff event) {
if (ConfigFactory.this.config() == event.config()) { //ignore events about current Config instance
//missed event must be requested once more
subscription.request(1);
} else {
delegate.onNext(event);
}
}
@Override
public void onError(Throwable throwable) {
delegate.onError(throwable);
}
@Override
public void onComplete() {
delegate.onComplete();
}
}
/**
* Prefix represents detached roots.
*/

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.
@@ -19,7 +19,6 @@ package io.helidon.config;
import java.util.function.Function;
import java.util.function.Supplier;
import io.helidon.config.internal.ValueResolvingFilter;
import io.helidon.config.spi.ConfigFilter;
/**

View File

@@ -16,63 +16,26 @@
package io.helidon.config;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.CharBuffer;
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;
import io.helidon.config.spi.ConfigNode;
/**
* Common Configuration utilities.
*/
public final class ConfigHelper {
private static final int DEFAULT_BUFFER_CAPACITY = 1024;
private static final Logger LOGGER = Logger.getLogger(ConfigHelper.class.getName());
private ConfigHelper() {
throw new AssertionError("Instantiation not allowed.");
}
/**
* Creates a {@link Reader} from the given {@link Readable} object.
* <p>
* Equivalent to {@code createReader(readable, 1024)}. See
* {@link #createReader(Readable,int)}.
*
* @param readable a readable
* @return a reader
* @throws IOException when {@link Readable#read(CharBuffer)} encounters an error
*/
public static Reader createReader(Readable readable) throws IOException {
return createReader(readable, DEFAULT_BUFFER_CAPACITY);
}
/**
* Creates a {@link Reader} from the given {@link Readable} object using the
* specified buffer size.
*
* @param readable a readable
* @param bufferCapacity a new buffer capacity, in chars
* @return a reader
* @throws IOException when {@link Readable#read(CharBuffer)} encounters an error
*/
static Reader createReader(Readable readable, int bufferCapacity) throws IOException {
if (readable instanceof Reader) {
return (Reader) readable;
}
CharBuffer cb = CharBuffer.allocate(bufferCapacity);
StringBuilder sb = new StringBuilder();
while (readable.read(cb) != -1) {
cb.flip();
sb.append(cb.toString());
}
return new StringReader(sb.toString());
}
/**
* Creates a {@link ConfigHelper#subscriber(Function) Flow.Subscriber} that
* will delegate {@link Flow.Subscriber#onNext(Object)} to the specified
@@ -96,36 +59,35 @@ public final class ConfigHelper {
return new OnNextFunctionSubscriber<>(onNextFunction);
}
/**
* Creates a {@link Flow.Publisher} which wraps the provided one and also
* supports "active" and "suspended" states.
* <p>
* The new {@code Publisher} starts in the "suspended" state.
* Upon the first subscriber request the {@code Publisher} transitions into the "active" state
* and invokes the caller-supplied {@code onFirstSubscriptionRequest} {@code Runnable}.
* When the last subscriber cancels the returned {@code Publisher} transitions into the "suspended" state and
* invokes the caller-provided {@code onLastSubscriptionCancel} {@code Runnable}.
*
* @param delegatePublisher publisher to be wrapped
* @param onFirstSubscriptionRequest hook invoked when the first subscriber requests events from the publisher
* @param onLastSubscriptionCancel hook invoked when last remaining subscriber cancels its subscription
* @param <T> the type of the items provided by the publisher
* @return new instance of suspendable {@link Flow.Publisher}
*/
public static <T> Flow.Publisher<T> suspendablePublisher(Flow.Publisher<T> delegatePublisher,
Runnable onFirstSubscriptionRequest,
Runnable onLastSubscriptionCancel) {
return new SuspendablePublisher<T>(delegatePublisher) {
@Override
protected void onFirstSubscriptionRequest() {
onFirstSubscriptionRequest.run();
}
static Map<ConfigKeyImpl, ConfigNode> createFullKeyToNodeMap(ConfigNode.ObjectNode objectNode) {
Map<ConfigKeyImpl, ConfigNode> result;
@Override
protected void onLastSubscriptionCancel() {
onLastSubscriptionCancel.run();
}
};
Stream<Map.Entry<ConfigKeyImpl, ConfigNode>> flattenNodes = objectNode.entrySet()
.stream()
.map(node -> flattenNodes(ConfigKeyImpl.of(node.getKey()), node.getValue()))
.reduce(Stream.empty(), Stream::concat);
result = flattenNodes.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
result.put(ConfigKeyImpl.of(), objectNode);
return result;
}
static Stream<Map.Entry<ConfigKeyImpl, ConfigNode>> flattenNodes(ConfigKeyImpl key, ConfigNode node) {
switch (node.nodeType()) {
case OBJECT:
return ((ConfigNode.ObjectNode) node).entrySet().stream()
.map(e -> flattenNodes(key.child(e.getKey()), e.getValue()))
.reduce(Stream.of(new AbstractMap.SimpleEntry<>(key, node)), Stream::concat);
case LIST:
return IntStream.range(0, ((ConfigNode.ListNode) node).size())
.boxed()
.map(i -> flattenNodes(key.child(Integer.toString(i)), ((ConfigNode.ListNode) node).get(i)))
.reduce(Stream.of(new AbstractMap.SimpleEntry<>(key, node)), Stream::concat);
case VALUE:
return Stream.of(new AbstractMap.SimpleEntry<>(key, node));
default:
throw new IllegalArgumentException("Invalid node type.");
}
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* 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,7 +14,7 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.ArrayList;
import java.util.Arrays;
@@ -23,12 +23,10 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import io.helidon.config.Config;
/**
* Implementation of {@link Config.Key Config Key}.
*/
public class ConfigKeyImpl implements Config.Key {
class ConfigKeyImpl implements Config.Key {
private final String name;
private final ConfigKeyImpl parent;
private final List<String> path;
@@ -48,7 +46,7 @@ public class ConfigKeyImpl implements Config.Key {
path.addAll(parent.path);
fullSB.append(parent.fullKey);
}
if (!name.equals("")) {
if (!name.isEmpty()) {
if (fullSB.length() > 0) {
fullSB.append(".");
}
@@ -62,7 +60,7 @@ public class ConfigKeyImpl implements Config.Key {
@Override
public ConfigKeyImpl parent() {
if (null == parent) {
if (isRoot()) {
throw new IllegalStateException("Attempting to get parent of a root node. Guard by isRoot instead");
}
return parent;
@@ -78,7 +76,7 @@ public class ConfigKeyImpl implements Config.Key {
*
* @return new instance of ConfigKeyImpl.
*/
public static ConfigKeyImpl of() {
static ConfigKeyImpl of() {
return new ConfigKeyImpl(null, "");
}
@@ -88,7 +86,7 @@ public class ConfigKeyImpl implements Config.Key {
* @param key key
* @return new instance of ConfigKeyImpl.
*/
public static ConfigKeyImpl of(String key) {
static ConfigKeyImpl of(String key) {
return of().child(key);
}
@@ -98,7 +96,7 @@ public class ConfigKeyImpl implements Config.Key {
* @param key sub-key
* @return new child instance of ConfigKeyImpl.
*/
public ConfigKeyImpl child(String key) {
ConfigKeyImpl child(String key) {
return child(Arrays.asList(key.split("\\.")));
}
@@ -126,7 +124,7 @@ public class ConfigKeyImpl implements Config.Key {
private ConfigKeyImpl child(List<String> path) {
ConfigKeyImpl result = this;
for (String name : path) {
if ("".equals(name)) {
if (name.isEmpty()) {
continue;
}
result = new ConfigKeyImpl(result, name);

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.
@@ -26,7 +26,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode.ValueNode;

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.
@@ -21,7 +21,6 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode.ListNode;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 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.
@@ -189,8 +189,7 @@ class ConfigMapperManager implements ConfigMapper {
<T> Optional<Mapper<T>> findMapper(GenericType<T> type, Config.Key key) {
return providers.stream()
.map(provider -> provider.apply(type))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst()
.map(mapper -> castMapper(type, mapper, key))
.map(Mapper::create);

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.
@@ -24,7 +24,6 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import io.helidon.common.GenericType;
import io.helidon.config.internal.ConfigKeyImpl;
/**
* Implementation of {@link Config} that represents {@link Config.Type#MISSING missing} node.

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.
@@ -20,7 +20,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode.ObjectNode;

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.
@@ -16,7 +16,6 @@
package io.helidon.config;
import io.helidon.config.internal.PropertiesConfigParser;
import io.helidon.config.spi.ConfigParser;
/**
@@ -32,7 +31,7 @@ public final class ConfigParsers {
/**
* Returns a {@link ConfigParser} implementation that parses Java Properties content
* (the media type {@value io.helidon.config.internal.PropertiesConfigParser#MEDIA_TYPE_TEXT_JAVA_PROPERTIES}).
* (the media type {@value PropertiesConfigParser#MEDIA_TYPE_TEXT_JAVA_PROPERTIES}).
* <p>
* @return {@code ConfigParser} that parses Java Properties content
*/

View File

@@ -0,0 +1,69 @@
/*
* 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.util.Optional;
import java.util.function.BiConsumer;
import io.helidon.config.spi.ConfigNode;
import org.eclipse.microprofile.config.spi.ConfigSource;
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) {
// this is a no-op - MP config sources do not support changes
}
@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();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 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.Optional;
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.
*/
public interface ConfigSourceRuntime {
/**
* Change support for a runtime.
*
* @param change change listener
*/
void onChange(BiConsumer<String, ConfigNode> change);
/**
* Load the config source if it is eager (such as {@link io.helidon.config.spi.ParsableSource} or
* {@link io.helidon.config.spi.NodeConfigSource}.
* <p>
* For {@link io.helidon.config.spi.LazyConfigSource}, this
* method may return an empty optional (if no key was yet requested), or a node with currently known keys and values.
*
* @return loaded data
*/
Optional<ConfigNode.ObjectNode> load();
/**
* Get a single config node based on the key.
* Use this method if you are interested in a specific key, as it works both for eager and lazy config sources.
*
* @param key key of the node to retrieve
* @return value on the key, or empty if not present
*/
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
*/
String description();
/**
* If a config source is lazy, its {@link #load()} method always returns empty and you must use
* {@link #node(String)} methods to retrieve its values.
*
* @return {@code true} if the underlying config source cannot load whole configuration tree
*/
boolean isLazy();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,19 @@
* limitations under the License.
*/
/**
* Internal HOCON module package.
*/
package io.helidon.config.hocon.internal;
package io.helidon.config;
abstract class ConfigSourceRuntimeBase implements ConfigSourceRuntime {
boolean isSystemProperties() {
return false;
}
boolean isEnvironmentVariables() {
return false;
}
boolean changesSupported() {
return false;
}
}

View File

@@ -0,0 +1,541 @@
/*
* Copyright (c) 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.time.Instant;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.spi.ChangeEventType;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.EventConfigSource;
import io.helidon.config.spi.LazyConfigSource;
import io.helidon.config.spi.NodeConfigSource;
import io.helidon.config.spi.ParsableSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* The runtime of a config source. For a single {@link io.helidon.config.Config}, there is one source runtime for each configured
* config source.
*
*/
public class ConfigSourceRuntimeImpl extends ConfigSourceRuntimeBase implements org.eclipse.microprofile.config.spi.ConfigSource {
private static final Logger LOGGER = Logger.getLogger(ConfigSourceRuntimeImpl.class.getName());
private final List<BiConsumer<String, ConfigNode>> listeners = new LinkedList<>();
private final BuilderImpl.ConfigContextImpl configContext;
private final ConfigSource configSource;
private final boolean changesSupported;
private final Supplier<Optional<ObjectNode>> reloader;
private final Runnable changesRunnable;
private final Function<String, Optional<ConfigNode>> singleNodeFunction;
private final boolean isLazy;
// we only want to start change support if somebody listens for changes
private boolean changesWanted = false;
// set to true if changes started
private boolean changesStarted = false;
// set to true when the content is loaded (to start changes whether the registration for change is before or after load)
private boolean dataLoaded = false;
// 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) {
this.configContext = configContext;
this.configSource = source;
Supplier<Optional<ObjectNode>> reloader;
Function<String, Optional<ConfigNode>> singleNodeFunction;
boolean lazy = false;
// content source
AtomicReference<Object> lastStamp = new AtomicReference<>();
if (configSource instanceof ParsableSource) {
// eager parsable config source
reloader = new ParsableConfigSourceReloader(configContext, (ParsableSource) source, lastStamp);
singleNodeFunction = objectNodeToSingleNode();
} else if (configSource instanceof NodeConfigSource) {
// eager node config source
reloader = new NodeConfigSourceReloader((NodeConfigSource) source, lastStamp);
singleNodeFunction = objectNodeToSingleNode();
} else if (configSource instanceof LazyConfigSource) {
LazyConfigSource lazySource = (LazyConfigSource) source;
// lazy config source
reloader = Optional::empty;
singleNodeFunction = lazySource::node;
lazy = true;
} else {
throw new ConfigException("Config source " + source + ", class: " + source.getClass().getName() + " does not "
+ "implement any of required interfaces. A config source must at least "
+ "implement one of the following: ParsableSource, or NodeConfigSource, or "
+ "LazyConfigSource");
}
this.isLazy = lazy;
this.reloader = reloader;
this.singleNodeFunction = singleNodeFunction;
// change support
boolean changesSupported = false;
Runnable changesRunnable = null;
if (configSource instanceof WatchableSource) {
WatchableSource<Object> watchable = (WatchableSource<Object>) source;
Optional<ChangeWatcher<Object>> changeWatcher = watchable.changeWatcher();
if (changeWatcher.isPresent()) {
changesSupported = true;
changesRunnable = new WatchableChangesStarter(configContext,
listeners,
reloader,
source,
watchable,
changeWatcher.get());
}
}
if (!changesSupported && (configSource instanceof PollableSource)) {
PollableSource<Object> pollable = (PollableSource<Object>) source;
Optional<PollingStrategy> pollingStrategy = pollable.pollingStrategy();
if (pollingStrategy.isPresent()) {
changesSupported = true;
changesRunnable = new PollingStrategyStarter(configContext,
listeners,
reloader,
source,
pollable,
pollingStrategy.get(),
lastStamp);
}
}
if (!changesSupported && (configSource instanceof EventConfigSource)) {
EventConfigSource event = (EventConfigSource) source;
changesSupported = true;
changesRunnable = () -> event.onChange((key, config) -> listeners.forEach(it -> it.accept(key, config)));
}
this.changesRunnable = changesRunnable;
this.changesSupported = changesSupported;
}
@Override
public synchronized void onChange(BiConsumer<String, ConfigNode> change) {
if (!changesSupported) {
return;
}
this.listeners.add(change);
this.changesWanted = true;
startChanges();
}
@Override
public synchronized Optional<ObjectNode> load() {
if (dataLoaded) {
throw new ConfigException("Attempting to load a single config source multiple times. This is a bug.");
}
initialLoad();
return this.initialData;
}
@Override
public boolean isLazy() {
return isLazy;
}
@Override
boolean changesSupported() {
return changesSupported;
}
@Override
public String toString() {
return "Runtime for " + configSource;
}
@Override
public int hashCode() {
return Objects.hash(configSource);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
ConfigSourceRuntimeImpl that = (ConfigSourceRuntimeImpl) o;
return configSource.equals(that.configSource);
}
private synchronized void initialLoad() {
if (dataLoaded) {
return;
}
configSource.init(configContext);
Optional<ObjectNode> loadedData = configSource.retryPolicy()
.map(policy -> policy.execute(reloader))
.orElseGet(reloader);
if (loadedData.isEmpty() && !configSource.optional()) {
throw new ConfigException("Cannot load data from mandatory source: " + configSource);
}
// we may have media type mapping per node configured as well
if (configSource instanceof AbstractConfigSource) {
loadedData = loadedData.map(it -> ((AbstractConfigSource) configSource)
.processNodeMapping(configContext::findParser, ConfigKeyImpl.of(), it));
}
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);
});
});
dataLoaded = true;
startChanges();
}
@Override
public Optional<ConfigNode> node(String key) {
return singleNodeFunction.apply(key);
}
@Override
public org.eclipse.microprofile.config.spi.ConfigSource asMpSource() {
return this;
}
@Override
public String description() {
return configSource.description();
}
/*
* MP Config related methods
*/
@Override
public Map<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) {
throw new IllegalStateException("Single node of an eager source requested before load method was called."
+ " This is a bug.");
}
return Optional.ofNullable(loadedData.get(key));
};
}
private void startChanges() {
if (!changesStarted && dataLoaded && changesWanted) {
changesStarted = true;
changesRunnable.run();
}
}
private static void triggerChanges(BuilderImpl.ConfigContextImpl configContext,
List<BiConsumer<String, ConfigNode>> listeners,
Optional<ObjectNode> objectNode) {
configContext.changesExecutor()
.execute(() -> {
for (BiConsumer<String, ConfigNode> listener : listeners) {
listener.accept("", objectNode.orElse(ObjectNode.empty()));
}
});
}
ConfigSource unwrap() {
return configSource;
}
private static final class PollingStrategyStarter implements Runnable {
private final PollingStrategy pollingStrategy;
private final PollingStrategyListener listener;
private PollingStrategyStarter(BuilderImpl.ConfigContextImpl configContext,
List<BiConsumer<String, ConfigNode>> listeners,
Supplier<Optional<ObjectNode>> reloader,
ConfigSource source,
PollableSource<Object> pollable,
PollingStrategy pollingStrategy,
AtomicReference<Object> lastStamp) {
this.pollingStrategy = pollingStrategy;
this.listener = new PollingStrategyListener(configContext, listeners, reloader, source, pollable, lastStamp);
}
@Override
public void run() {
pollingStrategy.start(listener);
}
}
private static final class PollingStrategyListener implements PollingStrategy.Polled {
private final BuilderImpl.ConfigContextImpl configContext;
private final List<BiConsumer<String, ConfigNode>> listeners;
private final Supplier<Optional<ObjectNode>> reloader;
private final ConfigSource source;
private final PollableSource<Object> pollable;
private final AtomicReference<Object> lastStamp;
private PollingStrategyListener(BuilderImpl.ConfigContextImpl configContext,
List<BiConsumer<String, ConfigNode>> listeners,
Supplier<Optional<ObjectNode>> reloader,
ConfigSource source,
PollableSource<Object> pollable,
AtomicReference<Object> lastStamp) {
this.configContext = configContext;
this.listeners = listeners;
this.reloader = reloader;
this.source = source;
this.pollable = pollable;
this.lastStamp = lastStamp;
}
@Override
public ChangeEventType poll(Instant when) {
Object lastStampValue = lastStamp.get();
if ((null == lastStampValue) || pollable.isModified(lastStampValue)) {
Optional<ObjectNode> objectNode = reloader.get();
if (objectNode.isEmpty()) {
if (source.optional()) {
// this is a valid change
triggerChanges(configContext, listeners, objectNode);
} else {
LOGGER.info("Mandatory config source is not available, ignoring change.");
}
return ChangeEventType.DELETED;
} else {
triggerChanges(configContext, listeners, objectNode);
return ChangeEventType.CHANGED;
}
}
return ChangeEventType.UNCHANGED;
}
}
private static final class WatchableChangesStarter implements Runnable {
private final WatchableSource<Object> watchable;
private final WatchableListener listener;
private final ChangeWatcher<Object> changeWatcher;
private WatchableChangesStarter(BuilderImpl.ConfigContextImpl configContext,
List<BiConsumer<String, ConfigNode>> listeners,
Supplier<Optional<ObjectNode>> reloader,
ConfigSource configSource,
WatchableSource<Object> watchable,
ChangeWatcher<Object> changeWatcher) {
this.watchable = watchable;
this.changeWatcher = changeWatcher;
this.listener = new WatchableListener(configContext, listeners, reloader, configSource);
}
@Override
public void run() {
Object target = watchable.target();
changeWatcher.start(target, listener);
}
}
private static final class WatchableListener implements Consumer<ChangeWatcher.ChangeEvent<Object>> {
private final BuilderImpl.ConfigContextImpl configContext;
private final List<BiConsumer<String, ConfigNode>> listeners;
private final Supplier<Optional<ObjectNode>> reloader;
private final ConfigSource configSource;
private WatchableListener(BuilderImpl.ConfigContextImpl configContext,
List<BiConsumer<String, ConfigNode>> listeners,
Supplier<Optional<ObjectNode>> reloader,
ConfigSource configSource) {
this.configContext = configContext;
this.listeners = listeners;
this.reloader = reloader;
this.configSource = configSource;
}
@Override
public void accept(ChangeWatcher.ChangeEvent<Object> change) {
try {
Optional<ObjectNode> objectNode = reloader.get();
if (objectNode.isEmpty()) {
if (configSource.optional()) {
// this is a valid change
triggerChanges(configContext, listeners, objectNode);
} else {
LOGGER.info("Mandatory config source is not available, ignoring change.");
}
} else {
triggerChanges(configContext, listeners, objectNode);
}
} catch (Exception e) {
LOGGER.info("Failed to reload config source "
+ configSource
+ ", exception available in finest log level. "
+ "Change that triggered this event: "
+ change);
LOGGER.log(Level.FINEST, "Failed to reload config source", e);
}
}
}
private static final class NodeConfigSourceReloader implements Supplier<Optional<ObjectNode>> {
private final NodeConfigSource configSource;
private final AtomicReference<Object> lastStamp;
private NodeConfigSourceReloader(NodeConfigSource configSource,
AtomicReference<Object> lastStamp) {
this.configSource = configSource;
this.lastStamp = lastStamp;
}
@Override
public Optional<ObjectNode> get() {
return configSource.load()
.map(content -> {
lastStamp.set(content.stamp().orElse(null));
return content.data();
});
}
}
private static final class ParsableConfigSourceReloader implements Supplier<Optional<ObjectNode>> {
private final BuilderImpl.ConfigContextImpl configContext;
private final ParsableSource configSource;
private final AtomicReference<Object> lastStamp;
private ParsableConfigSourceReloader(BuilderImpl.ConfigContextImpl configContext,
ParsableSource configSource,
AtomicReference<Object> lastStamp) {
this.configContext = configContext;
this.configSource = configSource;
this.lastStamp = lastStamp;
}
@Override
public Optional<ObjectNode> get() {
return configSource.load()
.map(content -> {
lastStamp.set(content.stamp().orElse(null));
Optional<ConfigParser> parser = configSource.parser();
if (parser.isPresent()) {
return parser.get().parse(content);
}
// media type should either be configured on config source, or in content
Optional<String> mediaType = configSource.mediaType().or(content::mediaType);
if (mediaType.isPresent()) {
parser = configContext.findParser(mediaType.get());
if (parser.isEmpty()) {
throw new ConfigException("Cannot find suitable parser for '" + mediaType
.get() + "' media type for config source " + configSource.description());
}
return parser.get().parse(content);
}
throw new ConfigException("Could not find media type of config source " + configSource.description());
});
}
}
}

View File

@@ -16,39 +16,27 @@
package io.helidon.config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Flow;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import io.helidon.common.Builder;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.internal.MapConfigSource;
import io.helidon.config.internal.PrefixedConfigSource;
import io.helidon.config.spi.AbstractMpSource;
import io.helidon.config.spi.AbstractSource;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigContent;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import static java.util.Objects.requireNonNull;
import io.helidon.config.spi.NodeConfigSource;
/**
* Provides access to built-in {@link ConfigSource} implementations.
@@ -85,11 +73,10 @@ public final class ConfigSources {
}
/**
* Returns a {@link ConfigSource} that wraps the specified {@code objectNode}
* and returns it when {@link ConfigSource#load()} is invoked.
* Returns a {@link ConfigSource} that wraps the specified {@code objectNode}.
*
* @param objectNode hierarchical configuration representation that will be
* returned by {@link ConfigSource#load()}
* returned by the created config source
* @return new instance of {@link ConfigSource}
* @see ConfigNode.ObjectNode
* @see ConfigNode.ListNode
@@ -97,73 +84,60 @@ public final class ConfigSources {
* @see ConfigNode.ObjectNode.Builder
* @see ConfigNode.ListNode.Builder
*/
public static ConfigSource create(ConfigNode.ObjectNode objectNode) {
return new ConfigSource() {
@Override
public String description() {
return "InMemoryConfig[ObjectNode]";
}
@Override
public Optional<ConfigNode.ObjectNode> load() throws ConfigException {
return Optional.of(objectNode);
}
};
public static NodeConfigSource create(ConfigNode.ObjectNode objectNode) {
return InMemoryConfigSource.create("ObjectNode", ConfigContent.NodeContent.builder()
.node(objectNode)
.build());
}
/**
* Provides a {@link ConfigSource} from the provided {@link Readable readable content} and
* with the specified {@code mediaType}.
* <p>
* {@link Instant#now()} is the {@link ConfigParser.Content#stamp() content timestamp}.
* {@link Instant#now()} is the {@link io.helidon.config.spi.ConfigContent#stamp() content timestamp}.
*
* @param readable a {@code Readable} providing the configuration content
* @param data a {@code InputStream} providing the configuration content
* @param mediaType a configuration media type
* @param <T> dual type to mark parameter both readable and auto closeable
* @return a config source
*/
public static <T extends Readable & AutoCloseable> ConfigSource create(T readable, String mediaType) {
return InMemoryConfigSource.builder()
public static ConfigSource create(InputStream data, String mediaType) {
return InMemoryConfigSource.create("Readable", ConfigParser.Content.builder()
.data(data)
.mediaType(mediaType)
.changesExecutor(Runnable::run)
.changesMaxBuffer(1)
.content("Readable", ConfigParser.Content.create(readable, mediaType, Instant.now()))
.build();
.build());
}
/**
* Provides a {@link ConfigSource} from the provided {@code String} content and
* with the specified {@code mediaType}.
* <p>
* {@link Instant#now()} is the {@link ConfigParser.Content#stamp() content timestamp}.
* {@link Instant#now()} is the {@link io.helidon.config.spi.ConfigContent#stamp() content timestamp}.
*
* @param content a configuration content
* @param mediaType a configuration media type
* @return a config source
*/
public static ConfigSource create(String content, String mediaType) {
return InMemoryConfigSource.builder()
return InMemoryConfigSource.create("String", ConfigParser.Content.builder()
.data(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
.mediaType(mediaType)
.changesExecutor(Runnable::run)
.changesMaxBuffer(1)
.content("String", ConfigParser.Content.create(new StringReader(content), mediaType, Instant.now()))
.build();
.build());
}
/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource}
* Provides a {@link MapConfigSource.Builder} for creating a {@code ConfigSource}
* for a {@code Map}.
*
* @param map a map
* @return new Builder instance
* @see #create(Properties)
*/
public static MapBuilder create(Map<String, String> map) {
public static MapConfigSource.Builder create(Map<String, String> map) {
return create(map, DEFAULT_MAP_NAME);
}
/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource}
* Provides a {@link MapConfigSource.Builder} for creating a {@code ConfigSource}
* for a {@code Map}.
*
* @param map a map
@@ -171,24 +145,26 @@ public final class ConfigSources {
* @return new Builder instance
* @see #create(Properties)
*/
public static MapBuilder create(Map<String, String> map, String name) {
return new MapBuilder(map, name);
public static MapConfigSource.Builder create(Map<String, String> map, String name) {
return MapConfigSource.builder()
.map(map)
.name(name);
}
/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource} for a
* Provides a {@link MapConfigSource.Builder} for creating a {@code ConfigSource} for a
* {@code Map} from a {@code Properties} object.
*
* @param properties properties
* @return new Builder instance
* @see #create(Map)
*/
public static MapBuilder create(Properties properties) {
public static MapConfigSource.Builder create(Properties properties) {
return create(properties, DEFAULT_PROPERTIES_NAME);
}
/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource} for a
* Provides a {@link MapConfigSource.Builder} for creating a {@code ConfigSource} for a
* {@code Map} from a {@code Properties} object.
*
* @param properties properties
@@ -196,8 +172,10 @@ public final class ConfigSources {
* @return new Builder instance
* @see #create(Map)
*/
public static MapBuilder create(Properties properties, String name) {
return new MapBuilder(properties, name);
public static MapConfigSource.Builder create(Properties properties, String name) {
return MapConfigSource.builder()
.properties(properties)
.name(name);
}
/**
@@ -208,7 +186,7 @@ public final class ConfigSources {
* @param sourceSupplier a config source supplier
* @return new @{code ConfigSource} for the newly-prefixed content
*/
public static ConfigSource prefixed(String key, Supplier<ConfigSource> sourceSupplier) {
public static ConfigSource prefixed(String key, Supplier<? extends ConfigSource> sourceSupplier) {
return PrefixedConfigSource.create(key, sourceSupplier.get());
}
@@ -218,8 +196,9 @@ public final class ConfigSources {
*
* @return {@code ConfigSource} for config derived from system properties
*/
public static AbstractMpSource<Instant> systemProperties() {
return new SystemPropertiesConfigSource();
public static SystemPropertiesConfigSource.Builder systemProperties() {
return new SystemPropertiesConfigSource.Builder()
.properties(System.getProperties());
}
/**
@@ -228,7 +207,7 @@ public final class ConfigSources {
*
* @return {@code ConfigSource} for config derived from environment variables
*/
public static AbstractMpSource<Instant> environmentVariables() {
public static MapConfigSource environmentVariables() {
return new EnvironmentVariablesConfigSource();
}
@@ -244,7 +223,7 @@ public final class ConfigSources {
* @param resource a name of the resource
* @return builder for a {@code ConfigSource} for the classpath-based resource
*/
public static ClasspathConfigSource.ClasspathBuilder classpath(String resource) {
public static ClasspathConfigSource.Builder classpath(String resource) {
return ClasspathConfigSource.builder().resource(resource);
}
@@ -253,9 +232,9 @@ public final class ConfigSources {
* @param resource resource to look for
* @return a list of classpath config source builders
*/
public static List<UrlConfigSource.UrlBuilder> classpathAll(String resource) {
public static List<UrlConfigSource.Builder> classpathAll(String resource) {
List<UrlConfigSource.UrlBuilder> result = new LinkedList<>();
List<UrlConfigSource.Builder> result = new LinkedList<>();
try {
Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(resource);
while (resources.hasMoreElements()) {
@@ -276,7 +255,7 @@ public final class ConfigSources {
* @param path a file path
* @return builder for the file-based {@code ConfigSource}
*/
public static FileConfigSource.FileBuilder file(String path) {
public static FileConfigSource.Builder file(String path) {
return FileConfigSource.builder().path(Paths.get(path));
}
@@ -287,7 +266,7 @@ public final class ConfigSources {
* @param path a file path
* @return builder for the file-based {@code ConfigSource}
*/
public static FileConfigSource.FileBuilder file(Path path) {
public static FileConfigSource.Builder file(Path path) {
return FileConfigSource.builder().path(path);
}
@@ -298,7 +277,7 @@ public final class ConfigSources {
* @param path a directory path
* @return new Builder instance
*/
public static DirectoryConfigSource.DirectoryBuilder directory(String path) {
public static DirectoryConfigSource.Builder directory(String path) {
return DirectoryConfigSource.builder().path(Paths.get(path));
}
@@ -310,374 +289,10 @@ public final class ConfigSources {
* @return new Builder instance
* @see #url(URL)
*/
public static UrlConfigSource.UrlBuilder url(URL url) {
public static UrlConfigSource.Builder url(URL url) {
return UrlConfigSource.builder().url(url);
}
/**
* Provides a {@link CompositeBuilder} for creating a composite
* {@code ConfigSource} based on the specified {@link ConfigSource}s, used
* in the order in which they are passed as arguments.
* <p>
* By default the resulting {@code ConfigSource} combines the various
* {@code ConfigSource}s using the system-provided
* {@link MergingStrategy#fallback() fallback merging strategy}. The
* application can invoke {@link CompositeBuilder#mergingStrategy} to change
* how the sources are combined.
*
* @param configSources original config sources to be treated as one
* @return new composite config source builder instance initialized from config sources.
* @see CompositeBuilder
* @see MergingStrategy
* @see #create(Supplier[])
* @see #create(List)
* @see Config#create(Supplier[])
* @see Config#builder(Supplier[])
*/
@SafeVarargs
public static CompositeBuilder create(Supplier<? extends ConfigSource>... configSources) {
return create(List.of(configSources));
}
/**
* Provides a {@link CompositeBuilder} for creating a composite
* {@code ConfigSource} based on the {@link ConfigSource}s and their order
* in the specified list.
* <p>
* By default the resulting {@code ConfigSource} combines the various
* {@code ConfigSource}s using the system-provided
* {@link MergingStrategy#fallback() fallback merging strategy}. The
* application can invoke {@link CompositeBuilder#mergingStrategy} to change
* how the sources are combined.
*
* @param configSources original config sources to be treated as one
* @return new composite config source builder instance initialized from config sources.
* @see CompositeBuilder
* @see MergingStrategy
* @see #create(Supplier[])
* @see #create(List)
* @see Config#create(Supplier[])
* @see Config#builder(Supplier[])
*/
public static CompositeBuilder create(List<Supplier<? extends ConfigSource>> configSources) {
return new CompositeBuilder(configSources);
}
/**
* Builder of a {@code ConfigSource} based on a {@code Map} containing
* config entries.
* <p>
* The caller constructs a {@code MapBuilder} with either a {@code Map} or a
* {@code Properties} object. The builder uses the {@code Map} entries or
* {@code Properties} name/value pairs to create config entries:
* <ul>
* <li>Each {@code Map} key or {@code Properties} property name is the
* fully-qualified dotted-name format key for the corresponding
* {@code Config} node.
* <li>Each {@code Map} value or {@code Properties} property value is the
* corresponding value of the corresponding {@code Config} node.
* </ul>
* By default, if the provided {@code Map} or {@code Properties} object
* contains duplicate keys then the {@link ConfigSource#load} on the
* returned {@code ConfigSource} will fail. The caller can invoke
* {@link MapBuilder#lax} to relax this restriction, in which case the
* {@code load} operation will log collision warnings but continue.
* <p>
* For example, the following properties collide:
* <pre>{@code
* app.port = 8080
* app = app-name
* }</pre>
* <p>
* The {@code MapConfigSource} returned by {@link #build} and {@link #get}
* works with an immutable copy of original map; it does
* <strong>not</strong> support
* {@link ConfigSource#changes() ConfigSource mutability}.
*/
public static final class MapBuilder implements Builder<ConfigSource> {
private Map<String, String> map;
private boolean strict;
private String mapSourceName;
private MapBuilder(final Map<String, String> map, final String name) {
requireNonNull(name, "name cannot be null");
requireNonNull(map, "map cannot be null");
this.map = Collections.unmodifiableMap(map);
this.strict = true;
this.mapSourceName = name;
}
private MapBuilder(final Properties properties, final String name) {
requireNonNull(name, "name cannot be null");
requireNonNull(properties, "properties cannot be null");
this.map = ConfigUtils.propertiesToMap(properties);
this.strict = true;
this.mapSourceName = name;
}
/**
* Switches off strict mode.
* <p>
* In lax mode {@link ConfigSource#load()} does not fail if config keys
* collide; it logs warnings and continues.
*
* @return updated builder
*/
public MapBuilder lax() {
strict = false;
return this;
}
/**
* Builds new instance of {@code MapConfigSource} from the {@code Map}
* or {@code Properties} passed to a constructor.
*
* @return {@link MapConfigSource} based on the specified {@code Map} or {@code Properties}
*/
@Override
public ConfigSource build() {
return MapConfigSource.create(map, strict, mapSourceName);
}
}
/**
* Builder of a {@code ConfigSource} that encapsulates multiple separate
* {@code ConfigSource}s.
* <p>
* The caller invokes {@link #add} one or more times to assemble an ordered
* list of {@code ConfigSource}s to be combined. The {@link #build} and
* {@link #get} methods return a single {@code ConfigSource} that combines
* the ordered sequence of {@code ConfigSource}s using a merging strategy
* (by default, the
* {@link MergingStrategy#fallback() fallback merging strategy}). The caller
* can change the merging strategy by passing an alternative strategy to the
* {@link #mergingStrategy} method.
* <p>
* The {@code CompositeBuilder} also supports change monitoring. The
* application can control these aspects:
* <table class="config">
* <caption>Application Control of Change Monitoring</caption>
* <tr>
* <th>Change Support Behavior</th>
* <th>Use</th>
* <th>Method</th>
* </tr>
* <tr>
* <td>reload executor</td>
* <td>The executor used to reload the configuration upon detection of a change in the
* underlying {@code ConfigSource}</td>
* <td>{@link #changesExecutor(java.util.concurrent.ScheduledExecutorService)
* }</td>
* </tr>
* <tr>
* <td>debounce timeout</td>
* <td>Minimum delay between reloads - helps reduce multiple reloads due to
* multiple changes in a short period, collecting a group of changes into
* one notification</td>
* <td>{@link #changesDebounce(java.time.Duration)}</td>
* </tr>
* <tr>
* <td>buffer size</td>
* <td>Maximum number of changes allowed in the change flow</td>
* <td>{@link #changesMaxBuffer(int) }</td>
* </tr>
* </table>
*
* @see ConfigSources#create(Supplier...)
* @see MergingStrategy
* @see MergingStrategy#fallback() default merging strategy
*/
public static class CompositeBuilder implements Builder<ConfigSource> {
private static final long DEFAULT_CHANGES_DEBOUNCE_TIMEOUT = 100;
private final List<ConfigSource> configSources;
private MergingStrategy mergingStrategy;
private ScheduledExecutorService changesExecutor;
private int changesMaxBuffer;
private Duration debounceTimeout;
private volatile ConfigSource configSource;
private CompositeBuilder(List<Supplier<? extends ConfigSource>> configSources) {
this.configSources = initConfigSources(configSources);
changesExecutor = CompositeConfigSource.DEFAULT_CHANGES_EXECUTOR_SERVICE;
debounceTimeout = Duration.ofMillis(DEFAULT_CHANGES_DEBOUNCE_TIMEOUT);
changesMaxBuffer = Flow.defaultBufferSize();
}
private static List<ConfigSource> initConfigSources(List<Supplier<? extends ConfigSource>> sourceSuppliers) {
List<ConfigSource> configSources = new LinkedList<>();
for (Supplier<? extends ConfigSource> configSupplier : sourceSuppliers) {
configSources.add(configSupplier.get());
}
return configSources;
}
/**
* Adds a {@code ConfigSource} to the ordered list of sources.
*
* @param source config source
* @return updated builder
*/
public CompositeBuilder add(Supplier<? extends ConfigSource> source) {
requireNonNull(source, "source cannot be null");
configSources.add(source.get());
return this;
}
/**
* Sets the strategy to be used for merging the root nodes provided by
* the list of {@code ConfigSource}s.
*
* @param mergingStrategy strategy for merging root nodes from the
* config sources
* @return updated builder
* @see MergingStrategy#fallback()
*/
public CompositeBuilder mergingStrategy(MergingStrategy mergingStrategy) {
requireNonNull(mergingStrategy, "mergingStrategy cannot be null");
this.mergingStrategy = mergingStrategy;
return this;
}
/**
* Specifies {@link ScheduledExecutorService} on which reloads of the
* config source will occur.
* <p>
* By default, a dedicated thread pool that can schedule reload commands to
* run after a given {@link #debounceTimeout timeout} is used.
*
* @param changesExecutor the executor used for scheduling config source
* reloads
* @return updated builder
* @see #changesDebounce(Duration)
*/
public CompositeBuilder changesExecutor(ScheduledExecutorService changesExecutor) {
this.changesExecutor = changesExecutor;
return this;
}
/**
* Specifies debounce timeout for reloading the config after the
* underlying config source(s) change.
* <p>
* Debouncing reduces the number of change events by collecting any
* changes over the debounce timeout interval into a single event.
* <p>
* The default is {@code 100} milliseconds.
*
* @param debounceTimeout debounce timeout to process reloading
* @return modified builder instance
* @see #changesExecutor(ScheduledExecutorService)
*/
public CompositeBuilder changesDebounce(Duration debounceTimeout) {
this.debounceTimeout = debounceTimeout;
return this;
}
/**
* Specifies maximum capacity for each subscriber's buffer to be used to deliver
* {@link ConfigSource#changes() config source changes}.
* <p>
* By default {@link Flow#defaultBufferSize()} is used.
* <p>
* Note: Any events not consumed by a subscriber will be lost.
*
* @param changesMaxBuffer the maximum capacity for each subscriber's buffer of {@link ConfigSource#changes()} events.
* @return modified builder instance
* @see #changesExecutor(ScheduledExecutorService)
* @see ConfigSource#changes()
*/
public CompositeBuilder changesMaxBuffer(int changesMaxBuffer) {
this.changesMaxBuffer = changesMaxBuffer;
return this;
}
/**
* Builds new instance of Composite ConfigSource.
*
* @return new instance of Composite ConfigSource.
*/
@Override
public ConfigSource build() {
final List<ConfigSource> finalConfigSources = new LinkedList<>(configSources);
final MergingStrategy finalMergingStrategy = mergingStrategy != null
? mergingStrategy
: new FallbackMergingStrategy();
return createCompositeConfigSource(finalConfigSources, finalMergingStrategy, changesExecutor, debounceTimeout,
changesMaxBuffer);
}
@Override
public ConfigSource get() {
if (configSource == null) {
configSource = build();
}
return configSource;
}
CompositeConfigSource createCompositeConfigSource(List<ConfigSource> finalConfigSources,
MergingStrategy finalMergingStrategy,
ScheduledExecutorService reloadExecutorService,
Duration debounceTimeout,
int changesMaxBuffer) {
return new CompositeConfigSource(finalConfigSources, finalMergingStrategy, reloadExecutorService, debounceTimeout,
changesMaxBuffer);
}
}
/**
* An algorithm for combining multiple {@code ConfigNode.ObjectNode} root nodes
* into a single {@code ConfigNode.ObjectNode} root node.
*
* @see ConfigSources#create(Supplier...)
* @see CompositeBuilder
* @see CompositeBuilder#mergingStrategy(MergingStrategy)
* @see #fallback() default merging strategy
*/
public interface MergingStrategy {
/**
* Merges an ordered list of {@link ConfigNode.ObjectNode}s into a
* single instance.
* <p>
* Typically nodes (object, list or value) from a root earlier in the
* list are considered to have a higher priority than nodes from a root
* that appears later in the list, but this is not required and is
* entirely up to each {@code MergingStrategy} implementation.
*
* @param rootNodes list of root nodes to combine
* @return ObjectNode root node resulting from the merge
*/
ConfigNode.ObjectNode merge(List<ConfigNode.ObjectNode> rootNodes);
/**
* Returns an implementation of {@code MergingStrategy} in which nodes
* from a root earlier in the list have higher priority than nodes from
* a root later in the list.
* <p>
* The merged behavior is as if the resulting merged {@code Config},
* when resolving a value of a key, consults the {@code Config} roots in
* the order they were passed to {@code merge}. As soon as it finds a
* {@code Config} tree containing a value for the key is it immediately
* returns that value, disregarding other later config roots.
*
* @return new instance of fallback merging strategy
*/
static ConfigSources.MergingStrategy fallback() {
return new FallbackMergingStrategy();
}
}
/**
* Holder of singleton instance of {@link ConfigSource}.
*
@@ -692,16 +307,26 @@ public final class ConfigSources {
/**
* EMPTY singleton instance.
*/
static final ConfigSource EMPTY = new ConfigSource() {
static final ConfigSource EMPTY = new NodeConfigSource() {
@Override
public String description() {
return "Empty";
}
@Override
public Optional<ConfigNode.ObjectNode> load() throws ConfigException {
public Optional<ConfigContent.NodeContent> load() throws ConfigException {
return Optional.empty();
}
@Override
public String toString() {
return "EmptyConfigSource";
}
@Override
public boolean optional() {
return true;
}
};
}
@@ -713,62 +338,34 @@ public final class ConfigSources {
* Constructor.
*/
EnvironmentVariablesConfigSource() {
super(EnvironmentVariables.expand(), false, "");
super(MapConfigSource.builder().map(EnvironmentVariables.expand()).name(""));
}
}
/**
* System properties config source.
*/
public static final class SystemPropertiesConfigSource extends AbstractMpSource<Instant> {
public static final class SystemPropertiesConfigSource extends MapConfigSource {
private SystemPropertiesConfigSource(Builder builder) {
super(builder);
}
/**
* Constructor.
* A fluent API builder for {@link io.helidon.config.ConfigSources.SystemPropertiesConfigSource}.
*/
SystemPropertiesConfigSource() {
// need builder to be able to customize polling strategy
super(new Builder().pollingStrategy(PollingStrategies.regular(Duration.of(5, ChronoUnit.SECONDS))));
}
@Override
protected Data<ConfigNode.ObjectNode, Instant> loadData() throws ConfigException {
return new Data<>(Optional.of(ConfigUtils
.mapToObjectNode(ConfigUtils.propertiesToMap(System.getProperties()), false)),
Optional.of(Instant.now()));
}
@Override
protected Optional<Instant> dataStamp() {
// each polling event will trigger a load and comparison of config tree
return Optional.of(Instant.now());
}
@Override
public Set<String> getPropertyNames() {
return System.getProperties().stringPropertyNames();
}
@Override
public void init(ConfigContext context) {
}
@Override
public Map<String, String> getProperties() {
Map<String, String> result = new HashMap<>();
System.getProperties().stringPropertyNames()
.forEach(it -> result.put(it, System.getProperty(it)));
return result;
}
private static final class Builder extends AbstractSource.Builder<Builder, Instant, SystemPropertiesConfigSource> {
public static final class Builder extends MapBuilder<Builder> {
private Builder() {
super(Instant.class);
}
@Override
public SystemPropertiesConfigSource build() {
return new SystemPropertiesConfigSource();
public MapConfigSource build() {
super.name("");
return new SystemPropertiesConfigSource(this);
}
@Override
public Builder name(String sourceName) {
return this;
}
}
}

View File

@@ -0,0 +1,220 @@
/*
* 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.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.MergingStrategy;
/**
* Runtime of all configured configuration sources.
*/
final class ConfigSourcesRuntime {
private final List<RuntimeWithData> loadedData = new LinkedList<>();
private List<ConfigSourceRuntimeBase> allSources;
private MergingStrategy mergingStrategy;
private Consumer<Optional<ObjectNode>> changeListener;
ConfigSourcesRuntime(List<ConfigSourceRuntimeBase> allSources,
MergingStrategy mergingStrategy) {
this.allSources = allSources;
this.mergingStrategy = mergingStrategy;
}
// for the purpose of tests
static ConfigSourcesRuntime empty() {
return new ConfigSourcesRuntime(List.of(new ConfigSourceRuntimeImpl(null, ConfigSources.empty())),
MergingStrategy.fallback());
}
List<ConfigSourceRuntimeBase> allSources() {
return allSources;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConfigSourcesRuntime that = (ConfigSourcesRuntime) o;
return allSources.equals(that.allSources);
}
@Override
public int hashCode() {
return Objects.hash(allSources);
}
@Override
public String toString() {
return allSources.toString();
}
void changeListener(Consumer<Optional<ObjectNode>> changeListener) {
this.changeListener = changeListener;
}
void startChanges() {
loadedData.stream()
.filter(loaded -> loaded.runtime().changesSupported())
.forEach(loaded -> loaded.runtime()
.onChange((key, configNode) -> {
loaded.data(processChange(loaded.data, key, configNode));
changeListener.accept(latest());
}));
}
private Optional<ObjectNode> processChange(Optional<ObjectNode> oldData, String changedKey, ConfigNode changeNode) {
ConfigKeyImpl key = ConfigKeyImpl.of(changedKey);
ObjectNode changeObjectNode = toObjectNode(changeNode);
if (key.isRoot()) {
// we have a root, no merging with original data, just return it
return Optional.of(changeObjectNode);
}
ObjectNode newRootNode = ObjectNode.builder().addObject(changedKey, changeObjectNode).build();
// old data was empty, this is the only data we have
if (oldData.isEmpty()) {
return Optional.of(newRootNode);
}
// we had data, now we have new data (not on root), let's merge
return Optional.of(mergingStrategy.merge(List.of(newRootNode, oldData.get())));
}
private ObjectNode toObjectNode(ConfigNode changeNode) {
switch (changeNode.nodeType()) {
case OBJECT:
return (ObjectNode) changeNode;
case LIST:
return ObjectNode.builder().addList("", (ConfigNode.ListNode) changeNode).build();
case VALUE:
return ObjectNode.builder().value(((ConfigNode.ValueNode) changeNode).get()).build();
default:
throw new IllegalArgumentException("Unsupported node type: " + changeNode.nodeType());
}
}
synchronized Optional<ObjectNode> latest() {
List<ObjectNode> objectNodes = loadedData.stream()
.map(RuntimeWithData::data)
.flatMap(Optional::stream)
.collect(Collectors.toList());
return Optional.of(mergingStrategy.merge(objectNodes));
}
synchronized Optional<ObjectNode> load() {
for (ConfigSourceRuntimeBase source : allSources) {
if (source.isLazy()) {
loadedData.add(new RuntimeWithData(source, Optional.empty()));
} else {
loadedData.add(new RuntimeWithData(source, source.load()
.map(ObjectNodeImpl::wrap)
.map(objectNode -> objectNode.initDescription(source.description()))));
}
}
Set<String> allKeys = loadedData.stream()
.map(RuntimeWithData::data)
.flatMap(Optional::stream)
.flatMap(this::streamKeys)
.collect(Collectors.toSet());
if (allKeys.isEmpty()) {
return Optional.empty();
}
// now we have all the keys, let's load them from the lazy sources
for (RuntimeWithData data : loadedData) {
if (data.runtime().isLazy()) {
data.data(loadLazy(data.runtime(), allKeys));
}
}
List<ObjectNode> objectNodes = loadedData.stream()
.map(RuntimeWithData::data)
.flatMap(Optional::stream)
.collect(Collectors.toList());
return Optional.of(mergingStrategy.merge(objectNodes));
}
private Optional<ObjectNode> loadLazy(ConfigSourceRuntime runtime, Set<String> allKeys) {
Map<String, ConfigNode> nodes = new HashMap<>();
for (String key : allKeys) {
runtime.node(key).ifPresent(it -> nodes.put(key, it));
}
if (nodes.isEmpty()) {
return Optional.empty();
}
ObjectNode.Builder builder = ObjectNode.builder();
nodes.forEach(builder::addNode);
return Optional.of(builder.build());
}
private Stream<String> streamKeys(ObjectNode objectNode) {
return ConfigHelper.createFullKeyToNodeMap(objectNode)
.keySet()
.stream()
.map(ConfigKeyImpl::toString);
}
private static final class RuntimeWithData {
private final ConfigSourceRuntimeBase runtime;
private Optional<ObjectNode> data;
private RuntimeWithData(ConfigSourceRuntimeBase runtime, Optional<ObjectNode> data) {
this.runtime = runtime;
this.data = data;
}
private void data(Optional<ObjectNode> data) {
this.data = data;
}
private ConfigSourceRuntimeBase runtime() {
return runtime;
}
private Optional<ObjectNode> data() {
return data;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
@@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Config internal {@link ThreadFactory} implementation to customize thread name.
*/
public class ConfigThreadFactory implements ThreadFactory {
class ConfigThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
@@ -36,7 +36,7 @@ public class ConfigThreadFactory implements ThreadFactory {
*
* @param type name of type of thread factory used just to customize thread name
*/
public ConfigThreadFactory(String type) {
ConfigThreadFactory(String type) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "config-" + POOL_NUMBER.getAndIncrement() + ":" + type + "-";
@@ -55,5 +55,4 @@ public class ConfigThreadFactory implements ThreadFactory {
return t;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* 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,7 +14,7 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -38,13 +38,12 @@ import java.util.stream.StreamSupport;
import javax.annotation.Priority;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
/**
* Internal config utilities.
*/
public final class ConfigUtils {
final class ConfigUtils {
private static final Logger LOGGER = Logger.getLogger(ConfigUtils.class.getName());
@@ -59,7 +58,7 @@ public final class ConfigUtils {
* @param <S> expected streamed item type.
* @return stream of items.
*/
public static <S> Stream<S> asStream(Iterable<? extends S> items) {
static <S> Stream<S> asStream(Iterable<? extends S> items) {
return asStream(items.iterator());
}
@@ -70,7 +69,7 @@ public final class ConfigUtils {
* @param iterator iterator over the items
* @return stream of the items
*/
public static <S> Stream<S> asStream(Iterator<? extends S> iterator) {
static <S> Stream<S> asStream(Iterator<? extends S> iterator) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
}
@@ -88,7 +87,7 @@ public final class ConfigUtils {
* @param <S> item type.
* @return prioritized stream of items.
*/
public static <S> Stream<? extends S> asPrioritizedStream(Iterable<? extends S> items, int defaultPriority) {
static <S> Stream<? extends S> asPrioritizedStream(Iterable<? extends S> items, int defaultPriority) {
return asStream(items).sorted(priorityComparator(defaultPriority));
}
@@ -102,7 +101,7 @@ public final class ConfigUtils {
* lack the {@code Priority} annotation
* @return comparator
*/
public static <S> Comparator<S> priorityComparator(int defaultPriority) {
static <S> Comparator<S> priorityComparator(int defaultPriority) {
return (service1, service2) -> {
int service1Priority = Optional.ofNullable(service1.getClass().getAnnotation(Priority.class))
.map(Priority::value)
@@ -123,11 +122,11 @@ public final class ConfigUtils {
* @param strict In strict mode, properties overlapping causes failure during loading into internal structure.
* @return built object node from map source.
*/
public static ConfigNode.ObjectNode mapToObjectNode(Map<String, String> map, boolean strict) {
static ConfigNode.ObjectNode mapToObjectNode(Map<?, ?> map, boolean strict) {
ConfigNode.ObjectNode.Builder builder = ConfigNode.ObjectNode.builder();
for (Map.Entry<String, String> entry : map.entrySet()) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
try {
builder.addValue(entry.getKey(), entry.getValue());
builder.addValue(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
} catch (ConfigException ex) {
if (strict) {
throw ex;
@@ -151,7 +150,7 @@ public final class ConfigUtils {
* @param properties properties to be transformed to map
* @return transformed map
*/
public static Map<String, String> propertiesToMap(Properties properties) {
static Map<String, String> propertiesToMap(Properties properties) {
return properties.stringPropertyNames().stream()
.collect(Collectors.toMap(k -> k, properties::getProperty));
}
@@ -161,7 +160,7 @@ public final class ConfigUtils {
*
* @param executor executor to be shutdown.
*/
public static void shutdownExecutor(ScheduledExecutorService executor) {
static void shutdownExecutor(ScheduledExecutorService executor) {
executor.shutdown();
try {
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
@@ -179,7 +178,7 @@ public final class ConfigUtils {
* or {@code UTF-8} in case a {@code contentEncoding} is {@code null}
* @throws ConfigException in case of unsupported charset name
*/
public static Charset getContentCharset(String contentEncoding) throws ConfigException {
static Charset getContentCharset(String contentEncoding) throws ConfigException {
try {
return Optional.ofNullable(contentEncoding)
.map(Charset::forName)
@@ -196,7 +195,7 @@ public final class ConfigUtils {
* <p>
* It can be used to implement Rx Debounce operator (http://reactivex.io/documentation/operators/debounce.html).
*/
public static class ScheduledTask {
static class ScheduledTask {
private final ScheduledExecutorService executorService;
private final Runnable command;
private final Duration delay;
@@ -210,7 +209,7 @@ public final class ConfigUtils {
* @param command the command to be executed on {@code executorService}
* @param delay the {@code command} is scheduled with specified delay
*/
public ScheduledTask(ScheduledExecutorService executorService, Runnable command, Duration delay) {
ScheduledTask(ScheduledExecutorService executorService, Runnable command, Duration delay) {
this.executorService = executorService;
this.command = command;
this.delay = delay;
@@ -239,21 +238,4 @@ public final class ConfigUtils {
}
}
/**
* Holder of singleton instance of {@link ConfigNode.ObjectNode}.
*
* @see ConfigNode.ObjectNode#empty()
*/
public static final class EmptyObjectNodeHolder {
private EmptyObjectNodeHolder() {
throw new AssertionError("Instantiation not allowed.");
}
/**
* EMPTY singleton instance.
*/
public static final ConfigNode.ObjectNode EMPTY = ConfigNode.ObjectNode.builder().build();
}
}

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.
@@ -259,7 +259,7 @@ public final class ConfigValues {
@Override
public String toString() {
return key() + ": " + asOptional();
return key() + ": " + asOptional().map(String::valueOf).orElse("");
}
}

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.
@@ -21,28 +21,34 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.Optional;
import io.helidon.config.internal.FileSourceHelper;
import io.helidon.config.spi.AbstractConfigSource;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigContent.NodeContent;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.NodeConfigSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
import static io.helidon.config.FileSourceHelper.lastModifiedTime;
import static java.nio.file.FileVisitOption.FOLLOW_LINKS;
/**
* {@link ConfigSource} implementation that loads configuration content from a directory on a filesystem.
*
* @see io.helidon.config.spi.AbstractSource.Builder
*/
public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
public class DirectoryConfigSource extends AbstractConfigSource
implements PollableSource<Instant>,
WatchableSource<Path>,
NodeConfigSource {
private static final String PATH_KEY = "path";
private final Path directoryPath;
DirectoryConfigSource(DirectoryBuilder builder, Path directoryPath) {
DirectoryConfigSource(Builder builder) {
super(builder);
this.directoryPath = directoryPath;
this.directoryPath = builder.path;
}
/**
@@ -53,7 +59,7 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
* <li>{@code path} - type {@link Path}</li>
* </ul>
* Optional {@code properties}: see
* {@link io.helidon.config.spi.AbstractParsableConfigSource.Builder#config(Config)}.
* {@link AbstractConfigSourceBuilder#config(Config)}.
*
* @param metaConfig meta-configuration used to initialize returned config source instance from.
* @return new instance of config source described by {@code metaConfig}
@@ -62,7 +68,7 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see io.helidon.config.ConfigSources#directory(String)
* @see io.helidon.config.spi.AbstractParsableConfigSource.Builder#config(Config)
* @see AbstractConfigSourceBuilder#config(Config)
*/
public static DirectoryConfigSource create(Config metaConfig) throws ConfigMappingException, MissingValueException {
return builder().config(metaConfig).build();
@@ -73,8 +79,8 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
*
* @return a new builder instance
*/
public static DirectoryBuilder builder() {
return new DirectoryBuilder();
public static Builder builder() {
return new Builder();
}
@Override
@@ -83,12 +89,40 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
}
@Override
protected Optional<Instant> dataStamp() {
return Optional.ofNullable(FileSourceHelper.lastModifiedTime(directoryPath));
public boolean isModified(Instant stamp) {
return FileSourceHelper.isModified(directoryPath, stamp);
}
@Override
protected Data<ConfigNode.ObjectNode, Instant> loadData() throws ConfigException {
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Optional<ChangeWatcher<Object>> changeWatcher() {
return super.changeWatcher();
}
@Override
public Path target() {
return directoryPath;
}
@Override
public boolean exists() {
return Files.exists(directoryPath);
}
@Override
public Class<Path> targetType() {
return Path.class;
}
@Override
public Optional<NodeContent> load() throws ConfigException {
if (!Files.exists(directoryPath)) {
return Optional.empty();
}
try {
ConfigNode.ObjectNode.Builder objectNodeRoot = ConfigNode.ObjectNode.builder();
@@ -99,7 +133,13 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
objectNodeRoot.addValue(path.getFileName().toString(), content);
});
return new Data<>(Optional.of(objectNodeRoot.build()), dataStamp());
NodeContent.Builder builder = NodeContent.builder()
.node(objectNodeRoot.build());
lastModifiedTime(directoryPath).ifPresent(builder::stamp);
return Optional.of(builder.build());
} catch (ConfigException ex) {
throw ex;
} catch (Exception ex) {
@@ -110,47 +150,18 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
/**
* {@inheritDoc}
* <p>
* If the Directory ConfigSource is {@code mandatory} and a {@code directory} does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* A fluent API builder for {@link io.helidon.config.DirectoryConfigSource}.
*/
public static final class DirectoryBuilder extends Builder<DirectoryBuilder, Path, DirectoryConfigSource> {
public static final class Builder extends AbstractConfigSourceBuilder<Builder, Path>
implements PollableSource.Builder<Builder>,
WatchableSource.Builder<Builder, Path>,
io.helidon.common.Builder<DirectoryConfigSource> {
private Path path;
/**
* Initialize builder.
*/
private DirectoryBuilder() {
super(Path.class);
}
/**
* Configuration directory path.
*
* @param path directory
* @return updated builder instance
*/
public DirectoryBuilder path(Path path) {
this.path = path;
return this;
}
/**
* {@inheritDoc}
* <ul>
* <li>{@code path} - directory path</li>
* </ul>
* @param metaConfig configuration properties used to configure a builder instance.
* @return updated builder instance
*/
@Override
public DirectoryBuilder config(Config metaConfig) {
metaConfig.get(PATH_KEY).as(Path.class).ifPresent(this::path);
return super.config(metaConfig);
}
@Override
protected Path target() {
return path;
private Builder() {
}
/**
@@ -163,7 +174,42 @@ public class DirectoryConfigSource extends AbstractConfigSource<Instant> {
if (null == path) {
throw new IllegalArgumentException("path must be defined");
}
return new DirectoryConfigSource(this, path);
return new DirectoryConfigSource(this);
}
/**
* {@inheritDoc}
* <ul>
* <li>{@code path} - directory path</li>
* </ul>
* @param metaConfig configuration properties used to configure a builder instance.
* @return updated builder instance
*/
@Override
public Builder config(Config metaConfig) {
metaConfig.get(PATH_KEY).as(Path.class).ifPresent(this::path);
return super.config(metaConfig);
}
/**
* Configuration directory path.
*
* @param path directory
* @return updated builder instance
*/
public Builder path(Path path) {
this.path = path;
return this;
}
@Override
public Builder changeWatcher(ChangeWatcher<Path> changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
}
}

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.
@@ -50,10 +50,8 @@ import static java.util.Collections.unmodifiableMap;
* an additional mapping is required to produce a matching alias. Given that it must map from legal environment variable names
* and reduce the chances of inadvertent mappings, a verbose mapping is used: {@code "_dash_"} substrings (upper and lower case)
* are first replaced by {@code '-'}. See {@link #expand()} for the aliases produced.
* <p>
*
*/
public class EnvironmentVariables {
public final class EnvironmentVariables {
private static final Pattern DASH_PATTERN = Pattern.compile("_dash_|_DASH_");
private static final String UNDERSCORE = "_";
private static final String DOUBLE_UNDERSCORE = "__";

View File

@@ -16,25 +16,30 @@
package io.helidon.config;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Logger;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.internal.FileSourceHelper;
import io.helidon.config.spi.AbstractParsableConfigSource;
import io.helidon.config.FileSourceHelper.DataAndDigest;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParser.Content;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.ParsableSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* {@link ConfigSource} implementation that loads configuration content from a file on a filesystem.
*
* @see FileBuilder
* @see io.helidon.config.FileConfigSource.Builder
*/
public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
public class FileConfigSource extends AbstractConfigSource
implements WatchableSource<Path>, ParsableSource, PollableSource<byte[]> {
private static final Logger LOGGER = Logger.getLogger(FileConfigSource.class.getName());
private static final String PATH_KEY = "path";
@@ -46,7 +51,7 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
*
* @param builder builder with configured path and other options of this source
*/
protected FileConfigSource(FileBuilder builder) {
FileConfigSource(Builder builder) {
super(builder);
this.filePath = builder.path;
@@ -59,7 +64,7 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* <ul>
* <li>{@code path} - type {@link Path}</li>
* </ul>
* Optional {@code properties}: see {@link AbstractParsableConfigSource.Builder#config(Config)}.
* Optional {@code properties}: see {@link AbstractConfigSourceBuilder#config(Config)}.
*
* @param metaConfig meta-configuration used to initialize returned config source instance from.
* @return new instance of config source described by {@code metaConfig}
@@ -68,7 +73,7 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see io.helidon.config.ConfigSources#file(String)
* @see AbstractParsableConfigSource.Builder#config(Config)
* @see AbstractConfigSourceBuilder#config(Config)
*/
public static FileConfigSource create(Config metaConfig) throws ConfigMappingException, MissingValueException {
return FileConfigSource.builder()
@@ -80,8 +85,8 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* Get a builder instance to create a new config source.
* @return a fluent API builder
*/
public static FileBuilder builder() {
return new FileBuilder();
public static Builder builder() {
return new Builder();
}
@Override
@@ -90,30 +95,64 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
}
@Override
protected Optional<String> mediaType() {
return super.mediaType()
.or(this::probeContentType);
}
private Optional<String> probeContentType() {
return MediaTypes.detectType(filePath);
public boolean isModified(byte[] stamp) {
return FileSourceHelper.isModified(filePath, stamp);
}
@Override
protected Optional<byte[]> dataStamp() {
return Optional.ofNullable(FileSourceHelper.digest(filePath));
public Path target() {
return filePath;
}
@Override
protected Content<byte[]> content() throws ConfigException {
public Optional<ConfigParser.Content> load() throws ConfigException {
LOGGER.fine(() -> String.format("Getting content from '%s'", filePath));
Content.Builder<byte[]> builder = Content.builder(new StringReader(FileSourceHelper.safeReadContent(filePath)));
// now we need to create all the necessary steps in one go, to make sure the digest matches the file
Optional<DataAndDigest> dataAndDigest = FileSourceHelper.readDataAndDigest(filePath);
dataStamp().ifPresent(builder::stamp);
mediaType().ifPresent(builder::mediaType);
if (dataAndDigest.isEmpty()) {
return Optional.empty();
}
return builder.build();
DataAndDigest dad = dataAndDigest.get();
InputStream dataStream = new ByteArrayInputStream(dad.data());
/*
* Build the content
*/
var builder = ConfigParser.Content.builder()
.stamp(dad.digest())
.data(dataStream);
MediaTypes.detectType(filePath).ifPresent(builder::mediaType);
return Optional.of(builder.build());
}
@Override
public Optional<ConfigParser> parser() {
return super.parser();
}
@Override
public Optional<String> mediaType() {
return super.mediaType();
}
@Override
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Optional<ChangeWatcher<Object>> changeWatcher() {
return super.changeWatcher();
}
@Override
public boolean exists() {
return Files.exists(filePath);
}
/**
@@ -122,21 +161,22 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* It allows to configure following properties:
* <ul>
* <li>{@code path} - configuration file path;</li>
* <li>{@code mandatory} - is existence of configuration resource mandatory (by default) or is {@code optional}?</li>
* <li>{@code optional} - is existence of configuration resource optional, or mandatory (by default)?</li>
* <li>{@code media-type} - configuration content media type to be used to look for appropriate {@link ConfigParser};</li>
* <li>{@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;</li>
* </ul>
* <p>
* If the File ConfigSource is {@code mandatory} and a {@code file} does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* <p>
* If {@code media-type} not set it tries to guess it from file extension.
*/
public static final class FileBuilder extends Builder<FileBuilder, Path, FileConfigSource> {
public static final class Builder extends AbstractConfigSourceBuilder<Builder, Path>
implements PollableSource.Builder<Builder>,
WatchableSource.Builder<Builder, Path>,
ParsableSource.Builder<Builder>,
io.helidon.common.Builder<FileConfigSource> {
private Path path;
private FileBuilder() {
super(Path.class);
private Builder() {
}
/**
@@ -145,7 +185,7 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* @param path path of a file to use
* @return updated builder instance
*/
public FileBuilder path(Path path) {
public Builder path(Path path) {
this.path = path;
return this;
}
@@ -160,14 +200,29 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
* @return modified builder instance
*/
@Override
public FileBuilder config(Config metaConfig) {
public Builder config(Config metaConfig) {
metaConfig.get(PATH_KEY).as(Path.class).ifPresent(this::path);
return super.config(metaConfig);
}
@Override
protected Path target() {
return path;
public Builder parser(ConfigParser parser) {
return super.parser(parser);
}
@Override
public Builder mediaType(String mediaType) {
return super.mediaType(mediaType);
}
@Override
public Builder changeWatcher(ChangeWatcher<Path> changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
/**
@@ -177,15 +232,12 @@ public class FileConfigSource extends AbstractParsableConfigSource<byte[]> {
*
* @return new instance of File ConfigSource.
*/
@Override
public FileConfigSource build() {
if (null == path) {
throw new IllegalArgumentException("File path cannot be null");
}
return new FileConfigSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 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.
@@ -14,35 +14,38 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.AbstractOverrideSource;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigContent;
import io.helidon.config.spi.OverrideSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* {@link OverrideSource} implementation that loads override definitions from a file on a filesystem.
*
* @see FileBuilder
* @see FileOverrideSource.Builder
*/
public class FileOverrideSource extends AbstractOverrideSource<byte[]> {
public final class FileOverrideSource extends AbstractSource
implements OverrideSource, PollableSource<byte[]>, WatchableSource<Path> {
private static final Logger LOGGER = Logger.getLogger(FileOverrideSource.class.getName());
private final Path filePath;
FileOverrideSource(FileBuilder builder) {
FileOverrideSource(Builder builder) {
super(builder);
this.filePath = builder.path;
@@ -54,19 +57,45 @@ public class FileOverrideSource extends AbstractOverrideSource<byte[]> {
}
@Override
protected Optional<byte[]> dataStamp() {
return Optional.ofNullable(FileSourceHelper.digest(filePath));
public boolean exists() {
return Files.exists(filePath);
}
@Override
protected Data<OverrideData, byte[]> loadData() throws ConfigException {
Optional<byte[]> digest = dataStamp();
public Optional<ConfigContent.OverrideContent> load() throws ConfigException {
LOGGER.log(Level.FINE, String.format("Getting content from '%s'.", filePath));
OverrideData overrideData = OverrideSource.OverrideData
.create(new StringReader(FileSourceHelper.safeReadContent(filePath)));
return new Data<>(Optional.of(overrideData), digest);
return FileSourceHelper.readDataAndDigest(filePath)
.map(dad -> ConfigContent.OverrideContent.builder()
.data(OverrideData.create(new InputStreamReader(new ByteArrayInputStream(dad.data()),
StandardCharsets.UTF_8)))
.stamp(dad.digest())
.build());
}
@Override
public boolean isModified(byte[] stamp) {
return FileSourceHelper.isModified(filePath, stamp);
}
@Override
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Path target() {
return filePath;
}
@Override
public Optional<ChangeWatcher<Object>> changeWatcher() {
return super.changeWatcher();
}
@Override
public Class<Path> targetType() {
return Path.class;
}
/**
@@ -84,57 +113,30 @@ public class FileOverrideSource extends AbstractOverrideSource<byte[]> {
*
* @return builder to create new instances of file override source
*/
public static FileBuilder builder() {
return new FileBuilder();
public static Builder builder() {
return new Builder();
}
/**
* File ConfigSource Builder.
* File OverrideSource Builder.
* <p>
* It allows to configure following properties:
* <ul>
* <li>{@code path} - configuration file path;</li>
* <li>{@code mandatory} - is existence of configuration resource mandatory (by default) or is {@code optional}?</li>
* <li>{@code media-type} - configuration content media type to be used to look for appropriate {@link ConfigParser};</li>
* <li>{@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;</li>
* </ul>
* <p>
* If the File ConfigSource is {@code mandatory} and a {@code file} does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* <p>
* If {@code media-type} not set it tries to guess it from file extension.
*/
public static final class FileBuilder extends Builder<FileBuilder, Path> {
public static final class Builder extends AbstractSourceBuilder<Builder, Path>
implements PollableSource.Builder<Builder>,
WatchableSource.Builder<Builder, Path>,
io.helidon.common.Builder<FileOverrideSource> {
private Path path;
/**
* Initialize builder.
*/
private FileBuilder() {
super(Path.class);
}
/**
* Configure path to look for the source.
*
* @param path file path
* @return updated builder
*/
public FileBuilder path(Path path) {
this.path = path;
return this;
}
@Override
public FileBuilder config(Config metaConfig) {
metaConfig.get("path").as(Path.class).ifPresent(this::path);
return super.config(metaConfig);
}
@Override
protected Path target() {
return path;
private Builder() {
}
/**
@@ -150,8 +152,32 @@ public class FileOverrideSource extends AbstractOverrideSource<byte[]> {
return new FileOverrideSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
@Override
public Builder config(Config metaConfig) {
metaConfig.get("path").as(Path.class).ifPresent(this::path);
return super.config(metaConfig);
}
/**
* Configure path to look for the source.
*
* @param path file path
* @return updated builder
*/
public Builder path(Path path) {
this.path = path;
return this;
}
@Override
public Builder changeWatcher(ChangeWatcher<Path> changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
}
}

View File

@@ -0,0 +1,263 @@
/*
* 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.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.FileLock;
import java.nio.channels.NonWritableChannelException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* Utilities for file-related source classes.
*
* @see io.helidon.config.FileConfigSource
* @see FileOverrideSource
* @see io.helidon.config.DirectoryConfigSource
*/
public final class FileSourceHelper {
private static final Logger LOGGER = Logger.getLogger(FileSourceHelper.class.getName());
private static final int FILE_BUFFER_SIZE = 4096;
private FileSourceHelper() {
throw new AssertionError("Instantiation not allowed.");
}
/**
* Returns the last modified time of the given file or directory.
*
* @param path a file or directory
* @return the last modified time
*/
public static Optional<Instant> lastModifiedTime(Path path) {
try {
return Optional.of(Files.getLastModifiedTime(path.toRealPath()).toInstant());
} catch (FileNotFoundException e) {
return Optional.empty();
} catch (IOException e) {
LOGGER.log(Level.FINE, e, () -> "Cannot obtain the last modified time of '" + path + "'.");
}
Instant timestamp = Instant.MIN;
LOGGER.finer("Cannot obtain the last modified time. Used time '" + timestamp + "' as a content timestamp.");
return Optional.of(timestamp);
}
/**
* Reads the content of the specified file.
* <p>
* The file is locked before the reading and the lock is released immediately after the reading.
* <p>
* An expected encoding is UTF-8.
*
* @param path a path to the file
* @return a content of the file
*/
public static String safeReadContent(Path path) {
try (FileInputStream fis = new FileInputStream(path.toFile())) {
FileLock lock = null;
try {
lock = fis.getChannel().tryLock(0L, Long.MAX_VALUE, false);
} catch (NonWritableChannelException ignored) {
// non writable channel means that we do not need to lock it
}
try {
try (BufferedReader bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
return bufferedReader.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new ConfigException(String.format("Cannot read from path '%s'", path), e);
}
} finally {
if (lock != null) {
lock.release();
}
}
} catch (FileNotFoundException e) {
throw new ConfigException(String.format("File '%s' not found. Absolute path: '%s'", path, path.toAbsolutePath()), e);
} catch (IOException e) {
throw new ConfigException(String.format("Cannot obtain a lock for file '%s'. Absolute path: '%s'",
path,
path.toAbsolutePath()), e);
}
}
/**
* Returns an MD5 digest of the specified file or null if the file cannot be read.
* <p>
* The file is locked before the reading and the lock is released immediately after the reading.
*
* @param path a path to the file
* @return an MD5 digest of the file or null if the file cannot be read
*/
@SuppressWarnings("StatementWithEmptyBody")
public static Optional<byte[]> digest(Path path) {
MessageDigest digest = digest();
try (DigestInputStream dis = new DigestInputStream(Files.newInputStream(path), digest)) {
byte[] buffer = new byte[FILE_BUFFER_SIZE];
while (dis.read(buffer) != -1) {
// just discard - we are only interested in the digest information
}
return Optional.of(digest.digest());
} catch (FileNotFoundException e) {
return Optional.empty();
} catch (IOException e) {
throw new ConfigException("Failed to calculate digest for file: " + path, e);
}
}
/**
* Check if a file on the file system is changed, as compared to the digest provided.
*
* @param filePath path of the file
* @param digest digest of the file
* @return {@code true} if the file exists and has the same digest, {@code false} otherwise
*/
public static boolean isModified(Path filePath, byte[] digest) {
return !digest(filePath)
.map(newDigest -> Arrays.equals(digest, newDigest))
// if new stamp is not present, it means the file was deleted
.orElse(false);
}
/**
* Check if a file on the file system is changed based on its last modification timestamp.
*
* @param filePath path of the file
* @param stamp last modification stamp
* @return {@code true} if the file exists and has the same last modification timestamp, {@code false} otherwise
*/
public static boolean isModified(Path filePath, Instant stamp) {
return lastModifiedTime(filePath)
.map(newStamp -> newStamp.isAfter(stamp))
.orElse(false);
}
/**
* Read data and its digest in the same go.
*
* @param filePath path to load data from
* @return data and its digest, or empty if file does not exist
*/
public static Optional<DataAndDigest> readDataAndDigest(Path filePath) {
// lock the file, so somebody does not remove it or update it while we process it
ByteArrayOutputStream baos = createByteArrayOutput(filePath);
MessageDigest md = digest();
// we want to digest and read at the same time
try (FileInputStream fis = new FileInputStream(filePath.toFile())) {
FileLock lock = lockFile(filePath, fis);
try (DigestInputStream dis = new DigestInputStream(fis, md)) {
byte[] buffer = new byte[FILE_BUFFER_SIZE];
int len;
while ((len = dis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
} finally {
if (lock != null) {
lock.release();
}
}
} catch (FileNotFoundException e) {
// race condition - the file disappeared between call to exists and load
return Optional.empty();
} catch (IOException e) {
throw new ConfigException(String.format("Cannot handle file '%s'.", filePath), e);
}
return Optional.of(new DataAndDigest(baos.toByteArray(), md.digest()));
}
private static ByteArrayOutputStream createByteArrayOutput(Path filePath) {
try {
return new ByteArrayOutputStream((int) Files.size(filePath));
} catch (IOException e) {
return new ByteArrayOutputStream(4096);
}
}
private static FileLock lockFile(Path filePath, FileInputStream fis) throws IOException {
try {
FileLock lock = fis.getChannel().tryLock(0L, Long.MAX_VALUE, false);
if (null == lock) {
throw new ConfigException("Failed to acquire a lock on configuration file " + filePath + ", cannot safely "
+ "read it");
}
return lock;
} catch (NonWritableChannelException e) {
// non writable channel means that we do not need to lock it
return null;
}
}
private static MessageDigest digest() {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new ConfigException("Cannot get MD5 digest algorithm.", e);
}
}
/**
* Data and digest of a file.
* Data in an instance are guaranteed to be paired - e.g. the digest is for the bytes provided.
*/
public static final class DataAndDigest {
private final byte[] data;
private final byte[] digest;
private DataAndDigest(byte[] data, byte[] digest) {
this.data = data;
this.digest = digest;
}
/**
* Data loaded from the file.
* @return bytes of the file
*/
public byte[] data() {
byte[] result = new byte[data.length];
System.arraycopy(data, 0, result, 0, data.length);
return result;
}
/**
* Digest of the data that was loaded.
* @return bytes of the digest
*/
public byte[] digest() {
byte[] result = new byte[digest.length];
System.arraycopy(digest, 0, result, 0, digest.length);
return result;
}
}
}

View File

@@ -0,0 +1,454 @@
/*
* Copyright (c) 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.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.spi.ChangeEventType;
import io.helidon.config.spi.ChangeWatcher;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
/**
* This change watcher is backed by {@link WatchService} to fire a polling event with every change on monitored {@link Path}.
* <p>
* When a parent directory of the {@code path} is not available, or becomes unavailable later, a new attempt to register {@code
* WatchService} is scheduled again and again until the directory finally exists and the registration is successful.
* <p>
* This {@link io.helidon.config.spi.ChangeWatcher} might be initialized with a custom {@link ScheduledExecutorService executor}
* or the {@link Executors#newSingleThreadScheduledExecutor()} is used if none is explicitly configured.
* <p>
* This watcher notifies with appropriate change event in the following cases:
* <ul>
* <li>The watched directory is gone</li>
* <li>The watched directory appears</li>
* <li>A file in the watched directory is deleted, created or modified</li>
* </ul>
* <p>
* A single file system watcher may be used to watch multiple targets. In such a case, if {@link #stop()} is invoked, it stops
* watching all of these targets.
*
* @see WatchService
*/
public final class FileSystemWatcher implements ChangeWatcher<Path> {
private static final Logger LOGGER = Logger.getLogger(FileSystemWatcher.class.getName());
/*
* Configurable options through builder.
*/
private final List<WatchEvent.Modifier> watchServiceModifiers = new LinkedList<>();
private ScheduledExecutorService executor;
private final boolean defaultExecutor;
private final long initialDelay;
private final long delay;
private final TimeUnit timeUnit;
/*
* Runtime options.
*/
private final List<TargetRuntime> runtimes = Collections.synchronizedList(new LinkedList<>());
private FileSystemWatcher(Builder builder) {
ScheduledExecutorService executor = builder.executor;
if (executor == null) {
this.executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling"));
this.defaultExecutor = true;
} else {
this.executor = executor;
this.defaultExecutor = false;
}
this.watchServiceModifiers.addAll(builder.watchServiceModifiers);
this.initialDelay = builder.initialDelay;
this.delay = builder.delay;
this.timeUnit = builder.timeUnit;
}
/**
* Fluent API builder for {@link io.helidon.config.FileSystemWatcher}.
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a new file watcher with default configuration.
*
* @return a new file watcher
*/
public static FileSystemWatcher create() {
return builder().build();
}
@Override
public synchronized void start(Path target, Consumer<ChangeEvent<Path>> listener) {
if (defaultExecutor && executor.isShutdown()) {
executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling"));
}
if (executor.isShutdown()) {
throw new ConfigException("Cannot start a watcher for path " + target + ", as the executor service is shutdown");
}
Monitor monitor = new Monitor(
listener,
target,
watchServiceModifiers);
ScheduledFuture<?> future = executor.scheduleWithFixedDelay(monitor, initialDelay, delay, timeUnit);
this.runtimes.add(new TargetRuntime(monitor, future));
}
@Override
public synchronized void stop() {
runtimes.forEach(TargetRuntime::stop);
if (defaultExecutor) {
ConfigUtils.shutdownExecutor(executor);
}
}
@Override
public Class<Path> type() {
return Path.class;
}
/**
* Add modifiers to be used when registering the {@link WatchService}.
* See {@link Path#register(WatchService, WatchEvent.Kind[],
* WatchEvent.Modifier...) Path.register}.
*
* @param modifiers the modifiers to add
*/
public void initWatchServiceModifiers(WatchEvent.Modifier... modifiers) {
watchServiceModifiers.addAll(Arrays.asList(modifiers));
}
private static final class TargetRuntime {
private final Monitor monitor;
private final ScheduledFuture<?> future;
private TargetRuntime(Monitor monitor, ScheduledFuture<?> future) {
this.monitor = monitor;
this.future = future;
}
public void stop() {
monitor.stop();
future.cancel(true);
}
}
private static final class Monitor implements Runnable {
private final WatchService watchService;
private final Consumer<ChangeEvent<Path>> listener;
private final Path target;
private final List<WatchEvent.Modifier> watchServiceModifiers;
private final boolean watchingFile;
private final Path watchedDir;
/*
* Runtime handling
*/
// we have failed - retry registration on next trigger
private volatile boolean failed = true;
// maybe we were stopped, do not do anything (the scheduled future will be cancelled shortly)
private volatile boolean shouldStop = false;
// last file information
private volatile boolean fileExists;
private WatchKey watchKey;
private Monitor(Consumer<ChangeEvent<Path>> listener,
Path target,
List<WatchEvent.Modifier> watchServiceModifiers) {
try {
this.watchService = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new ConfigException("Cannot obtain WatchService.", e);
}
this.listener = listener;
this.target = target;
this.watchServiceModifiers = watchServiceModifiers;
this.fileExists = Files.exists(target);
this.watchingFile = !Files.isDirectory(target);
this.watchedDir = watchingFile ? target.getParent() : target;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
if (shouldStop) {
return;
}
if (failed) {
register();
}
if (failed) {
return;
}
// if we used `take`, we would block the thread forever. This way we can use the same thread to handle
// multiple targets
WatchKey key = watchService.poll();
if (null == key) {
return;
}
List<WatchEvent<?>> watchEvents = key.pollEvents();
if (watchEvents.isEmpty()) {
// something happened, cannot get details
key.cancel();
listener.accept(ChangeEvent.create(target, ChangeEventType.CHANGED));
failed = true;
return;
}
// we actually have some changes
for (WatchEvent<?> watchEvent : watchEvents) {
WatchEvent<Path> event = (WatchEvent<Path>) watchEvent;
Path eventPath = event.context();
// as we watch on whole directory
// make sure this is the watched file (if only interested in a single file)
if (watchingFile && !target.endsWith(eventPath)) {
continue;
}
eventPath = watchedDir.resolve(eventPath);
WatchEvent.Kind<Path> kind = event.kind();
if (kind.equals(OVERFLOW)) {
LOGGER.finest("Overflow event on path: " + eventPath);
continue;
}
if (kind.equals(ENTRY_CREATE)) {
LOGGER.finest("Entry created. Path: " + eventPath);
listener.accept(ChangeEvent.create(eventPath, ChangeEventType.CREATED));
} else if (kind == ENTRY_DELETE) {
LOGGER.finest("Entry deleted. Path: " + eventPath);
listener.accept(ChangeEvent.create(eventPath, ChangeEventType.DELETED));
} else if (kind == ENTRY_MODIFY) {
LOGGER.finest("Entry changed. Path: " + eventPath);
listener.accept(ChangeEvent.create(eventPath, ChangeEventType.CHANGED));
}
}
if (!key.reset()) {
LOGGER.log(Level.FINE, () -> "Directory of '" + target + "' is no more valid to be watched.");
failed = true;
}
}
private void fire(Path target, ChangeEventType eventType) {
listener.accept(ChangeEvent.create(target, eventType));
}
private synchronized void register() {
if (shouldStop) {
failed = true;
return;
}
boolean oldFileExists = fileExists;
try {
Path cleanTarget = target(this.target);
Path watchedDirectory = Files.isDirectory(cleanTarget) ? cleanTarget : parentDir(cleanTarget);
WatchKey oldWatchKey = watchKey;
watchKey = watchedDirectory.register(watchService,
new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE},
watchServiceModifiers.toArray(new WatchEvent.Modifier[0]));
failed = false;
if (null != oldWatchKey) {
oldWatchKey.cancel();
}
} catch (IOException e) {
LOGGER.log(Level.FINEST, "Failed to register watch service", e);
this.failed = true;
}
// in either case, let's see if our target has changed
this.fileExists = Files.exists(target);
if (fileExists != oldFileExists) {
if (fileExists) {
fire(this.target, ChangeEventType.CREATED);
} else {
fire(this.target, ChangeEventType.DELETED);
}
}
}
private synchronized void stop() {
this.shouldStop = true;
if (null != watchKey) {
watchKey.cancel();
}
try {
watchService.close();
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to close watch service", e);
}
}
private Path target(Path path) throws IOException {
Path target = path;
while (Files.isSymbolicLink(target)) {
target = target.toRealPath();
}
return target;
}
private Path parentDir(Path path) {
Path parent = path.getParent();
if (parent == null) {
throw new ConfigException(
String.format("Cannot find parent directory for '%s' to register watch service.", path));
}
return parent;
}
}
/**
* Fluent API builder for {@link FileSystemWatcher}.
*/
public static final class Builder implements io.helidon.common.Builder<FileSystemWatcher> {
private final List<WatchEvent.Modifier> watchServiceModifiers = new LinkedList<>();
private ScheduledExecutorService executor;
private long initialDelay = 1000;
private long delay = 100;
private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
private Builder() {
}
@Override
public FileSystemWatcher build() {
return new FileSystemWatcher(this);
}
/**
* Update this builder from meta configuration.
* <p>
* Currently these options are supported:
* <ul>
* <li>{@code initial-delay-millis} - delay between the time this watcher is started
* and the time the first check is triggered</li>
* <li>{@code delay-millis} - how often do we check the watcher service for changes</li>
* </ul>
* As the watcher is implemented as non-blocking, a single watcher can be used to watch multiple
* directories using the same thread.
*
* @param metaConfig configuration of file system watcher
* @return updated builder instance
*/
public Builder config(Config metaConfig) {
metaConfig.get("initial-delay-millis")
.asLong()
.ifPresent(initDelay -> initialDelay = timeUnit.convert(initDelay, TimeUnit.MILLISECONDS));
metaConfig.get("delay-millis")
.asLong()
.ifPresent(delayMillis -> delay = timeUnit.convert(delayMillis, TimeUnit.MILLISECONDS));
return this;
}
/**
* Executor to use for this watcher.
* The task is scheduled for regular execution and is only blocking a thread for the time needed
* to process changed files.
*
* @param executor executor service to use
* @return updated builder instance
*/
public Builder executor(ScheduledExecutorService executor) {
this.executor = executor;
return this;
}
/**
* Configure schedule of the file watcher.
*
* @param initialDelay initial delay before regular scheduling starts
* @param delay delay between schedules
* @param timeUnit time unit of the delays
* @return updated builer instance
*/
public Builder schedule(long initialDelay, long delay, TimeUnit timeUnit) {
this.initialDelay = initialDelay;
this.delay = delay;
this.timeUnit = timeUnit;
return this;
}
/**
* Add a modifier of the watch service.
* Currently only implementation specific modifier are available, such as
* {@code com.sun.nio.file.SensitivityWatchEventModifier}.
*
* @param modifier modifier to use
* @return updated builder instance
*/
public Builder addWatchServiceModifier(WatchEvent.Modifier modifier) {
this.watchServiceModifiers.add(modifier);
return this;
}
/**
* Set modifiers to use for the watch service.
* Currently only implementation specific modifier are available, such as
* {@code com.sun.nio.file.SensitivityWatchEventModifier}.
*
* @param modifiers modifiers to use (replacing current configuration)
* @return updated builder instance
*/
public Builder watchServiceModifiers(List<WatchEvent.Modifier> modifiers) {
this.watchServiceModifiers.clear();
this.watchServiceModifiers.addAll(modifiers);
return this;
}
}
}

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.
@@ -19,76 +19,84 @@ package io.helidon.config;
import java.util.Objects;
import java.util.Optional;
import io.helidon.config.spi.AbstractParsableConfigSource;
import io.helidon.config.spi.ConfigContent.NodeContent;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.NodeConfigSource;
import io.helidon.config.spi.ParsableSource;
/**
* In-memory implementation of config source.
*/
class InMemoryConfigSource extends AbstractParsableConfigSource<Object> {
private final String uri;
private final ConfigParser.Content<Object> content;
InMemoryConfigSource(InMemoryConfigSource.Builder builder) {
super(builder);
uri = builder.uri();
content = builder.content();
class InMemoryConfigSource {
private InMemoryConfigSource() {
}
@Override
protected String uid() {
return uri;
static NodeConfigSource create(String uri, NodeContent content) {
Objects.requireNonNull(uri, "uri cannot be null");
Objects.requireNonNull(content, "content cannot be null");
return new NodeInMemory(uri, content);
}
@Override
protected Optional<Object> dataStamp() {
return Optional.of(this);
static ConfigSource create(String uri, ConfigParser.Content content) {
Objects.requireNonNull(uri, "uri cannot be null");
Objects.requireNonNull(content, "content cannot be null");
return new ParsableInMemory(uri, content);
}
@Override
protected ConfigParser.Content<Object> content() throws ConfigException {
return content;
}
private static class InMemory implements ConfigSource {
private final String uid;
static Builder builder() {
return new Builder();
}
static final class Builder
extends AbstractParsableConfigSource.Builder<InMemoryConfigSource.Builder, Void, InMemoryConfigSource> {
private String uri;
private ConfigParser.Content<Object> content;
Builder() {
super(Void.class);
}
Builder content(String uri, ConfigParser.Content<Object> content) {
Objects.requireNonNull(uri, "uri cannot be null");
Objects.requireNonNull(content, "content cannot be null");
this.uri = uri;
this.content = content;
return this;
protected InMemory(String uid) {
this.uid = uid;
}
@Override
public InMemoryConfigSource build() {
Objects.requireNonNull(uri, "uri cannot be null");
Objects.requireNonNull(content, "content cannot be null");
public String description() {
return ConfigSource.super.description()
+ "[" + uid + "]";
}
}
return new InMemoryConfigSource(this);
private static final class NodeInMemory extends InMemory implements NodeConfigSource {
private final NodeContent content;
private NodeInMemory(String uid, NodeContent nodeContent) {
super(uid);
this.content = nodeContent;
}
private String uri() {
return uri;
@Override
public Optional<NodeContent> load() throws ConfigException {
return Optional.of(content);
}
}
private static final class ParsableInMemory extends InMemory implements ParsableSource {
private final ConfigParser.Content content;
protected ParsableInMemory(String uid, ConfigParser.Content content) {
super(uid);
this.content = content;
}
private ConfigParser.Content<Object> content() {
return content;
@Override
public Optional<ConfigParser.Content> load() throws ConfigException {
return Optional.of(content);
}
@Override
public Optional<ConfigParser> parser() {
return Optional.empty();
}
@Override
public Optional<String> mediaType() {
return Optional.empty();
}
}
}

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.
@@ -23,18 +23,17 @@ import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import io.helidon.config.spi.AbstractOverrideSource;
import io.helidon.config.spi.ConfigContent.OverrideContent;
import io.helidon.config.spi.OverrideSource;
/**
* In-memory implementation of override source.
*/
class InMemoryOverrideSource extends AbstractOverrideSource<Object> {
public class InMemoryOverrideSource implements OverrideSource {
private final OverrideData overrideData;
private InMemoryOverrideSource(Builder builder) {
super(builder);
this.overrideData = builder.overrideData;
}
@@ -45,28 +44,22 @@ class InMemoryOverrideSource extends AbstractOverrideSource<Object> {
.collect(Collectors.toList()));
}
static Builder builder(List<Map.Entry<String, String>> overrideValues) {
return new Builder(overrideValues);
}
@Override
protected Optional<Object> dataStamp() {
return Optional.of(this);
public Optional<OverrideContent> load() throws ConfigException {
return Optional.of(OverrideContent.builder()
.data(overrideData)
.build());
}
@Override
protected Data<OverrideData, Object> loadData() throws ConfigException {
return new Data<>(Optional.of(overrideData), Optional.of(this));
}
static final class Builder extends AbstractOverrideSource.Builder<InMemoryOverrideSource.Builder, Void> {
/**
* Fluent API builder for {@link io.helidon.config.InMemoryOverrideSource}.
*/
public static final class Builder implements io.helidon.common.Builder<InMemoryOverrideSource> {
private OverrideData overrideData;
private List<Map.Entry<String, String>> overrideWildcards;
Builder(List<Map.Entry<String, String>> overrideWildcards) {
super(Void.class);
Objects.requireNonNull(overrideWildcards, "overrideValues cannot be null");
this.overrideWildcards = overrideWildcards;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
@@ -97,6 +97,13 @@ public class ListNodeBuilderImpl extends AbstractNodeBuilderImpl<Integer, ListNo
return this;
}
// this is a shortcut method to keep current fluent code
// even though value is now optional
ListNodeBuilderImpl value(Optional<String> value) {
value.ifPresent(this::value);
return this;
}
@Override
protected String typeDescription() {
return "a LIST node";

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,19 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.AbstractList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
import static io.helidon.config.internal.AbstractNodeBuilderImpl.formatFrom;
import static io.helidon.config.AbstractNodeBuilderImpl.formatFrom;
/**
* Implements {@link ListNode}.
@@ -54,7 +54,7 @@ class ListNodeImpl extends AbstractList<ConfigNode> implements ListNode, Mergeab
return (ListNodeImpl) listNode;
}
return ListNodeBuilderImpl.from(listNode)
.value(listNode.get())
.value(listNode.value())
.build();
}
@@ -106,7 +106,7 @@ class ListNodeImpl extends AbstractList<ConfigNode> implements ListNode, Mergeab
final ListNodeBuilderImpl builder = new ListNodeBuilderImpl(Function.identity());
if (node.hasValue()) {
builder.value(node.get());
builder.value(node.value());
} else if (hasValue()) {
builder.value(value);
}
@@ -158,7 +158,7 @@ class ListNodeImpl extends AbstractList<ConfigNode> implements ListNode, Mergeab
}
@Override
public String get() {
return value;
public Optional<String> value() {
return Optional.ofNullable(value);
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright (c) 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.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import io.helidon.config.spi.ConfigContent.NodeContent;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.NodeConfigSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
/**
* {@link ConfigSource} implementation based on {@link Map Map&lt;String, String&gt;}.
* <p>
* Map key format must conform to {@link Config#key() Config key} format.
*
* @see io.helidon.config.MapConfigSource.Builder
*/
public class MapConfigSource extends AbstractConfigSource implements ConfigSource,
NodeConfigSource,
PollableSource<Map<?, ?>> {
private final Map<?, ?> map;
private final String mapSourceName;
MapConfigSource(MapBuilder<?> builder) {
super(builder);
// we intentionally keep the original instance, so we can watch for changes
this.map = builder.map();
this.mapSourceName = builder.sourceName();
}
/**
* Create a new fluent API builder.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a new config source from the provided map.
*
* @param map config properties
* @return a new map config source
*/
public static MapConfigSource create(Map<String, String> map) {
Objects.requireNonNull(map);
return builder().map(map).build();
}
/**
* Create a new config source from the provided properties.
* @param properties properties to serve as source of data
* @return a new map config source
*/
public static MapConfigSource create(Properties properties) {
Objects.requireNonNull(properties);
return builder().properties(properties).build();
}
@Override
public boolean isModified(Map<?, ?> stamp) {
return !this.map.equals(stamp);
}
@Override
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Optional<NodeContent> load() throws ConfigException {
return Optional.of(NodeContent.builder()
.node(ConfigUtils.mapToObjectNode(map, false))
.build());
}
@Override
protected String uid() {
return mapSourceName.isEmpty() ? "" : mapSourceName;
}
/**
* Fluent API builder for {@link io.helidon.config.MapConfigSource}.
*/
public static final class Builder extends MapBuilder<Builder> {
private Builder() {
}
@Override
public MapConfigSource build() {
return new MapConfigSource(this);
}
}
/**
* An abstract fluent API builder for {@link MapConfigSource}.
* If you want to extend {@link io.helidon.config.MapConfigSource}, you can use this class as a base for
* your own builder.
*
* @param <T> type of the implementing builder
*/
public abstract static class MapBuilder<T extends MapBuilder<T>> extends AbstractConfigSourceBuilder<T, Void>
implements io.helidon.common.Builder<MapConfigSource>,
PollableSource.Builder<T> {
private Map<?, ?> map;
private String sourceName = "";
@SuppressWarnings("unchecked")
private final T me = (T) this;
/**
* Creat a new builder instance.
*/
protected MapBuilder() {
}
/**
* Map to be used as config source underlying data.
* The same instance is kept by the config source, to support polling.
*
* @param map map to use
* @return updated builder instance
*/
public T map(Map<String, String> map) {
this.map = map;
return me;
}
/**
* Properties to be used as config source underlying data.
* The same instance is kept by the config source, to support polling.
*
* @param properties properties to use
* @return updated builder instance
*/
public T properties(Properties properties) {
this.map = properties;
return me;
}
/**
* Name of this source.
*
* @param sourceName name of this source
* @return updated builder instance
*/
public T name(String sourceName) {
this.sourceName = Objects.requireNonNull(sourceName);
return me;
}
@Override
public T pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
/**
* Map used as data of this config source.
*
* @return map with the data
*/
protected Map<?, ?> map() {
return map;
}
/**
* Name of the source.
*
* @return name
*/
protected String sourceName() {
return sourceName;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,8 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
/**

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.
@@ -25,6 +25,7 @@ import java.util.function.Function;
import java.util.logging.Logger;
import io.helidon.common.serviceloader.HelidonServiceLoader;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.OverrideSource;
@@ -33,10 +34,39 @@ import io.helidon.config.spi.RetryPolicy;
/**
* Meta configuration.
*
* TODO document meta configuration
* - files loaded as part of meta config lookup
* - options to specify in meta configuration
* <p>
* Configuration allows configuring itself using meta configuration.
* Config looks for {@code meta-config.*} files in the current directory and on the classpath, where the {@code *} is
* one of the supported media type suffixes (such as {@code yaml} when {@code helidon-config-yaml} module is on the classpath).
* <p>
* Meta configuration can define which config sources to load, including possible retry policy, polling strategy and change
* watchers.
* <p>
* Example of a YAML meta configuration file:
* <pre>
* sources:
* - type: "environment-variables"
* - type: "system-properties"
* - type: "file"
* properties:
* path: "conf/dev.yaml"
* optional: true
* - type: "file"
* properties:
* path: "conf/config.yaml"
* optional: true
* - type: "classpath"
* properties:
* resource: "default.yaml"
* </pre>
* This configuration would load the following config sources (in the order specified):
* <ul>
* <li>Environment variables config source
* <li>System properties config source</li>
* <li>File config source from file {@code conf/dev.yaml} that is optional</li>
* <li>File config source from file {@code conf/config.yaml} that is optional</li>
* <li>Classpath resource config source for resource {@code default.yaml} that is mandatory</li>
* </ul>
*/
public final class MetaConfig {
private static final Logger LOGGER = Logger.getLogger(MetaConfig.class.getName());
@@ -81,13 +111,28 @@ public final class MetaConfig {
* Load a polling strategy based on its meta configuration.
*
* @param metaConfig meta configuration of a polling strategy
* @return a function that creates a polling strategy instance for an instance of target type
* @return a polling strategy instance
*/
public static Function<Object, PollingStrategy> pollingStrategy(Config metaConfig) {
public static PollingStrategy pollingStrategy(Config metaConfig) {
return MetaProviders.pollingStrategy(metaConfig.get("type").asString().get(),
metaConfig.get("properties"));
}
/**
* Load a change watcher based on its meta configuration.
*
* @param metaConfig meta configuration of a change watcher
* @return a change watcher instance
*/
public static ChangeWatcher<?> changeWatcher(Config metaConfig) {
String type = metaConfig.get("type").asString().get();
ChangeWatcher<?> changeWatcher = MetaProviders.changeWatcher(type, metaConfig.get("properties"));
LOGGER.fine(() -> "Loaded change watcher of type \"" + type + "\", class: " + changeWatcher.getClass().getName());
return changeWatcher;
}
/**
* Load a retry policy based on its meta configuration.
*
@@ -96,8 +141,7 @@ public final class MetaConfig {
*/
public static RetryPolicy retryPolicy(Config metaConfig) {
String type = metaConfig.get("type").asString().get();
RetryPolicy retryPolicy = MetaProviders.retryPolicy(type,
metaConfig.get("properties"));
RetryPolicy retryPolicy = MetaProviders.retryPolicy(type, metaConfig.get("properties"));
LOGGER.fine(() -> "Loaded retry policy of type \"" + type + "\", class: " + retryPolicy.getClass().getName());
@@ -105,21 +149,33 @@ public final class MetaConfig {
}
/**
* Load a config source based on its meta configuration.
* Load a config source (or config sources) based on its meta configuration.
* The metaConfig must contain a key {@code type} that defines the type of the source to be found via providers, and
* a key {@code properties} with configuration of the config sources
* @param sourceMetaConfig meta configuration of a config source
* @return config source instance
* @see Config.Builder#config(Config)
*/
public static ConfigSource configSource(Config sourceMetaConfig) {
public static List<ConfigSource> configSource(Config sourceMetaConfig) {
String type = sourceMetaConfig.get("type").asString().get();
ConfigSource source = MetaProviders.configSource(type,
sourceMetaConfig.get("properties"));
boolean multiSource = sourceMetaConfig.get("multi-source").asBoolean().orElse(false);
LOGGER.fine(() -> "Loaded source of type \"" + type + "\", class: " + source.getClass().getName());
Config sourceProperties = sourceMetaConfig.get("properties");
if (multiSource) {
List<ConfigSource> sources = MetaProviders.configSources(type, sourceProperties);
LOGGER.fine(() -> "Loaded sources of type \"" + type + "\", values: " + sources);
return sources;
} else {
ConfigSource source = MetaProviders.configSource(type, sourceProperties);
LOGGER.fine(() -> "Loaded source of type \"" + type + "\", class: " + source.getClass().getName());
return List.of(source);
}
return source;
}
// override config source
@@ -138,7 +194,7 @@ public final class MetaConfig {
metaConfig.get("sources")
.asNodeList()
.ifPresent(list -> list.forEach(it -> configSources.add(MetaConfig.configSource(it))));
.ifPresent(list -> list.forEach(it -> configSources.addAll(MetaConfig.configSource(it))));
return configSources;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020 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.
@@ -93,8 +93,7 @@ final class MetaConfigFinder {
source = validSuffixes.stream()
.map(suf -> configPrefix + suf)
.map(it -> findFile(it, type))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
if (source.isPresent()) {
@@ -105,8 +104,7 @@ final class MetaConfigFinder {
return validSuffixes.stream()
.map(suf -> configPrefix + suf)
.map(resource -> MetaConfigFinder.findClasspath(cl, resource, type))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
}

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.
@@ -15,7 +15,6 @@
*/
package io.helidon.config;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -27,9 +26,8 @@ import java.util.function.Function;
import javax.annotation.Priority;
import io.helidon.common.serviceloader.HelidonServiceLoader;
import io.helidon.config.internal.FileOverrideSource;
import io.helidon.config.internal.PrefixedConfigSource;
import io.helidon.config.internal.UrlOverrideSource;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ChangeWatcherProvider;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.ConfigSourceProvider;
import io.helidon.config.spi.OverrideSource;
@@ -46,11 +44,13 @@ final class MetaProviders {
private static final List<ConfigSourceProvider> CONFIG_SOURCE_PROVIDERS;
private static final List<RetryPolicyProvider> RETRY_POLICY_PROVIDERS;
private static final List<PollingStrategyProvider> POLLING_STRATEGY_PROVIDERS;
private static final List<ChangeWatcherProvider> CHANGE_WATCHER_PROVIDERS;
private static final List<OverrideSourceProvider> OVERRIDE_SOURCE_PROVIDERS;
private static final Set<String> SUPPORTED_CONFIG_SOURCES = new HashSet<>();
private static final Set<String> SUPPORTED_RETRY_POLICIES = new HashSet<>();
private static final Set<String> SUPPORTED_POLLING_STRATEGIES = new HashSet<>();
private static final Set<String> SUPPORTED_CHANGE_WATCHERS = new HashSet<>();
private static final Set<String> SUPPORTED_OVERRIDE_SOURCES = new HashSet<>();
static {
@@ -84,6 +84,17 @@ final class MetaProviders {
.map(PollingStrategyProvider::supported)
.forEach(SUPPORTED_POLLING_STRATEGIES::addAll);
CHANGE_WATCHER_PROVIDERS = HelidonServiceLoader
.builder(ServiceLoader.load(ChangeWatcherProvider.class))
.addService(new BuiltInChangeWatchers())
.build()
.asList();
CHANGE_WATCHER_PROVIDERS.stream()
.map(ChangeWatcherProvider::supported)
.forEach(SUPPORTED_CHANGE_WATCHERS::addAll);
OVERRIDE_SOURCE_PROVIDERS = HelidonServiceLoader
.builder(ServiceLoader.load(OverrideSourceProvider.class))
.addService(new BuiltinOverrideSourceProvider())
@@ -98,7 +109,7 @@ final class MetaProviders {
private MetaProviders() {
}
public static ConfigSource configSource(String type, Config config) {
static ConfigSource configSource(String type, Config config) {
return CONFIG_SOURCE_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
@@ -107,7 +118,16 @@ final class MetaProviders {
+ " Supported types: " + SUPPORTED_CONFIG_SOURCES));
}
public static OverrideSource overrideSource(String type, Config config) {
static List<ConfigSource> configSources(String type, Config sourceProperties) {
return CONFIG_SOURCE_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
.map(provider -> provider.createMulti(type, sourceProperties))
.orElseThrow(() -> new IllegalArgumentException("Config source of type " + type + " is not supported."
+ " Supported types: " + SUPPORTED_CONFIG_SOURCES));
}
static OverrideSource overrideSource(String type, Config config) {
return OVERRIDE_SOURCE_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
@@ -116,7 +136,7 @@ final class MetaProviders {
+ " Supported types: " + SUPPORTED_OVERRIDE_SOURCES));
}
public static Function<Object, PollingStrategy> pollingStrategy(String type, Config config) {
static PollingStrategy pollingStrategy(String type, Config config) {
return POLLING_STRATEGY_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
@@ -125,7 +145,7 @@ final class MetaProviders {
+ " Supported types: " + SUPPORTED_POLLING_STRATEGIES));
}
public static RetryPolicy retryPolicy(String type, Config config) {
static RetryPolicy retryPolicy(String type, Config config) {
return RETRY_POLICY_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
@@ -134,40 +154,52 @@ final class MetaProviders {
+ " Supported types: " + SUPPORTED_RETRY_POLICIES));
}
public static ChangeWatcher<?> changeWatcher(String type, Config config) {
return CHANGE_WATCHER_PROVIDERS.stream()
.filter(provider -> provider.supports(type))
.findFirst()
.map(provider -> provider.create(type, config))
.orElseThrow(() -> new IllegalArgumentException("Change watcher of type " + type + " is not supported."
+ " Supported types: " + SUPPORTED_CHANGE_WATCHERS));
}
@Priority(Integer.MAX_VALUE)
private static final class BuiltInPollingStrategyProvider implements PollingStrategyProvider {
private static final String REGULAR_TYPE = "regular";
private static final String WATCH_TYPE = "watch";
private static final Map<String, Function<Config, Function<Object, PollingStrategy>>> BUILT_IN =
Map.of(
REGULAR_TYPE, config -> target -> PollingStrategies.ScheduledBuilder.create(config).build(),
WATCH_TYPE, config -> BuiltInPollingStrategyProvider::watchStrategy
);
private static PollingStrategy watchStrategy(Object target) {
if (target instanceof Path) {
Path path = (Path) target;
return PollingStrategies.watch(path).build();
}
throw new ConfigException("Incorrect target type ('" + target.getClass().getName()
+ "') for WATCH polling strategy. Expected '" + Path.class.getName() + "'.");
}
private static final class BuiltInChangeWatchers implements ChangeWatcherProvider {
private static final String FILE_WATCH = "file";
@Override
public boolean supports(String type) {
return BUILT_IN.containsKey(type);
return FILE_WATCH.equals(type);
}
@Override
public Function<Object, PollingStrategy> create(String type, Config metaConfig) {
return BUILT_IN.get(type).apply(metaConfig);
public ChangeWatcher<?> create(String type, Config metaConfig) {
return FileSystemWatcher.builder().config(metaConfig).build();
}
@Override
public Set<String> supported() {
return BUILT_IN.keySet();
return Set.of(FILE_WATCH);
}
}
@Priority(Integer.MAX_VALUE)
private static final class BuiltInPollingStrategyProvider implements PollingStrategyProvider {
private static final String REGULAR_TYPE = "regular";
@Override
public boolean supports(String type) {
return REGULAR_TYPE.equals(type);
}
@Override
public PollingStrategy create(String type, Config metaConfig) {
return PollingStrategies.ScheduledBuilder.create(metaConfig).build();
}
@Override
public Set<String> supported() {
return Set.of(REGULAR_TYPE);
}
}
@@ -185,10 +217,7 @@ final class MetaProviders {
@Override
public RetryPolicy create(String type, Config metaConfig) {
// This method is actually dedicated to repeat type and does no reflection at all
// TODO refactor to proper factory methods and a builder
// (e.g. RetryPolicies.repeatBuilder().config(metaConfig).build())
return RetryPolicies.Builder.create(metaConfig).build();
return SimpleRetryPolicy.create(metaConfig);
}
@Override
@@ -240,9 +269,10 @@ final class MetaProviders {
private static final Map<String, Function<Config, ConfigSource>> BUILT_INS = new HashMap<>();
static {
BUILT_INS.put(SYSTEM_PROPERTIES_TYPE, config -> ConfigSources.systemProperties());
BUILT_INS.put(SYSTEM_PROPERTIES_TYPE, config -> ConfigSources.systemProperties().config(config).build());
BUILT_INS.put(ENVIRONMENT_VARIABLES_TYPE, config -> ConfigSources.environmentVariables());
BUILT_INS.put(CLASSPATH_TYPE, ClasspathConfigSource::create);
BUILT_INS.put(FILE_TYPE, FileConfigSource::create);
BUILT_INS.put(DIRECTORY_TYPE, DirectoryConfigSource::create);
BUILT_INS.put(URL_TYPE, UrlConfigSource::create);
@@ -259,6 +289,15 @@ final class MetaProviders {
return BUILT_INS.get(type).apply(metaConfig);
}
@Override
public List<ConfigSource> createMulti(String type, Config metaConfig) {
if (CLASSPATH_TYPE.equals(type)) {
return ClasspathConfigSource.createAll(metaConfig);
}
throw new ConfigException("Config source of type \"" + type + "\" does not support multiple config sources"
+ " from a single configuration");
}
@Override
public Set<String> supported() {
return BUILT_INS.keySet();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 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,20 +14,21 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
/**
* Implementation of {@link ObjectNode.Builder}.
* This class is {@code public} for the time being, though it should not be.
*/
public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, ObjectNode.Builder> implements ObjectNode.Builder {
@@ -37,7 +38,7 @@ public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, Objec
/**
* Initialize object builder.
*/
public ObjectNodeBuilderImpl() {
ObjectNodeBuilderImpl() {
this(Function.identity());
}
@@ -51,6 +52,15 @@ public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, Objec
this.members = new HashMap<>();
}
/**
* Create a new builder instance.
*
* @return a new builder
*/
public static ObjectNodeBuilderImpl create() {
return new ObjectNodeBuilderImpl();
}
/**
* Creates new instance of the builder initialized from original map of members.
*
@@ -90,6 +100,7 @@ public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, Objec
* @param node new node
* @return modified builder
*/
@Override
public ObjectNodeBuilderImpl addNode(String name, ConfigNode node) {
members.put(tokenResolver().apply(name), wrap(node, tokenResolver()));
return this;
@@ -140,6 +151,13 @@ public class ObjectNodeBuilderImpl extends AbstractNodeBuilderImpl<String, Objec
return this;
}
// this is a shortcut method to keep current fluent code
// even though value is now optional
ObjectNodeBuilderImpl value(Optional<String> value) {
value.ifPresent(this::value);
return this;
}
@Override
public ObjectNode.Builder addValue(String key, ConfigNode.ValueNode value) {
return deepMerge(MergingKey.of(encodeDotsInTokenReferences(tokenResolver().apply(key))), ValueNodeImpl.wrap(value));

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,19 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import io.helidon.config.ConfigException;
import io.helidon.config.internal.AbstractNodeBuilderImpl.MergingKey;
import io.helidon.config.AbstractNodeBuilderImpl.MergingKey;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import static io.helidon.config.internal.AbstractNodeBuilderImpl.formatFrom;
import static io.helidon.config.AbstractNodeBuilderImpl.formatFrom;
/**
* Implements {@link ObjectNode}.
@@ -67,7 +67,7 @@ public class ObjectNodeImpl extends AbstractMap<String, ConfigNode> implements O
*/
public static ObjectNodeImpl wrap(ObjectNode objectNode, Function<String, String> resolveTokenFunction) {
return ObjectNodeBuilderImpl.create(objectNode, resolveTokenFunction)
.value(objectNode.get())
.value(objectNode.value())
.build();
}
@@ -111,7 +111,7 @@ public class ObjectNodeImpl extends AbstractMap<String, ConfigNode> implements O
private MergeableNode mergeWithValueNode(ValueNodeImpl node) {
ObjectNodeBuilderImpl builder = ObjectNodeBuilderImpl.create(members, resolveTokenFunction);
builder.value(node.get());
builder.value(node.value());
return builder.build();
}
@@ -164,7 +164,7 @@ public class ObjectNodeImpl extends AbstractMap<String, ConfigNode> implements O
}
@Override
public String get() {
return value;
public Optional<String> value() {
return Optional.ofNullable(value);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.List;
import java.util.Map;
@@ -22,7 +22,6 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import io.helidon.config.Config;
import io.helidon.config.spi.ConfigFilter;
/**

View File

@@ -0,0 +1,352 @@
/*
* Copyright (c) 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.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.spi.ChangeEventType;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.OverrideSource;
import io.helidon.config.spi.OverrideSource.OverrideData;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
class OverrideSourceRuntime {
private static final Logger LOGGER = Logger.getLogger(OverrideSourceRuntime.class.getName());
private final OverrideReloader reloader;
private final Runnable changesRunnable;
private final OverrideSource source;
// we only want to start change support if changes are supported by the source
private final boolean changesSupported;
// the data used by filter to retrieve override data
private final AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData = new AtomicReference<>(List.of());
// reference to change listener (the change listening is started after construction of this class)
private final AtomicReference<Runnable> changeListener = new AtomicReference<>();
// set to true if changes started
private boolean changesStarted = false;
// set to true when the content is loaded (to start changes whether the registration for change is before or after load)
private boolean dataLoaded = false;
@SuppressWarnings("unchecked")
OverrideSourceRuntime(OverrideSource overrideSource) {
this.source = overrideSource;
// content source
AtomicReference<Object> lastStamp = new AtomicReference<>();
this.reloader = new OverrideReloader(lastStamp, overrideSource);
// change support
boolean changesSupported = false;
Runnable changesRunnable = null;
if (overrideSource instanceof WatchableSource) {
WatchableSource<Object> watchable = (WatchableSource<Object>) source;
Optional<ChangeWatcher<Object>> changeWatcher = watchable.changeWatcher();
if (changeWatcher.isPresent()) {
changesSupported = true;
changesRunnable = new WatchableChangesStarter(
lastData,
reloader,
source,
watchable,
changeWatcher.get(),
changeListener);
}
}
if (!changesSupported && (overrideSource instanceof PollableSource)) {
PollableSource<Object> pollable = (PollableSource<Object>) source;
Optional<PollingStrategy> pollingStrategy = pollable.pollingStrategy();
if (pollingStrategy.isPresent()) {
changesSupported = true;
changesRunnable = new PollingStrategyStarter(
lastData,
reloader,
source,
pollable,
pollingStrategy.get(),
lastStamp,
changeListener);
}
}
this.changesRunnable = changesRunnable;
this.changesSupported = changesSupported;
}
// for testing purposes
static OverrideSourceRuntime empty() {
return new OverrideSourceRuntime(OverrideSources.empty());
}
// this happens once per config
void addFilter(ProviderImpl.ChainConfigFilter targetFilter) {
if (!dataLoaded) {
initialLoad();
}
if (!source.equals(OverrideSources.empty())) {
// we need to have a single set of data for a single config
var data = lastData.get();
OverrideConfigFilter filter = new OverrideConfigFilter(() -> data);
targetFilter.addFilter(filter);
}
}
void startChanges() {
if (!changesStarted && dataLoaded && changesSupported) {
changesStarted = true;
changesRunnable.run();
}
}
@Override
public String toString() {
return "Runtime for " + source;
}
@Override
public int hashCode() {
return Objects.hash(source);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
OverrideSourceRuntime that = (OverrideSourceRuntime) o;
return source.equals(that.source);
}
void initialLoad() {
synchronized (source) {
if (dataLoaded) {
throw new ConfigException("Attempting to load a single override source multiple times. This is a bug");
}
Optional<OverrideData> loadedData = source.retryPolicy()
.map(policy -> policy.execute(reloader))
.orElseGet(reloader);
if (loadedData.isEmpty() && !source.optional()) {
throw new ConfigException("Cannot load data from mandatory source: " + source);
}
// initial data do not trigger a change notification
lastData.set(loadedData.map(OverrideData::data).orElseGet(List::of));
dataLoaded = true;
}
}
public String description() {
return source.description();
}
private static void setData(AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData,
Optional<OverrideData> data,
AtomicReference<Runnable> changeListener) {
lastData.set(data.map(OverrideData::data).orElseGet(List::of));
Runnable runnable = changeListener.get();
if (null == runnable) {
LOGGER.finest("Wrong order - change triggered before a change listener is registered in "
+ OverrideSourceRuntime.class.getName());
} else {
runnable.run();
}
}
void changeListener(Runnable listener) {
this.changeListener.set(listener);
}
private static final class PollingStrategyStarter implements Runnable {
private final PollingStrategy pollingStrategy;
private final PollingStrategyListener listener;
private PollingStrategyStarter(AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData,
OverrideReloader reloader,
OverrideSource source,
PollableSource<Object> pollable,
PollingStrategy pollingStrategy,
AtomicReference<Object> lastStamp,
AtomicReference<Runnable> changeListener) {
this.pollingStrategy = pollingStrategy;
this.listener = new PollingStrategyListener(lastData, reloader, source, pollable, lastStamp, changeListener);
}
@Override
public void run() {
pollingStrategy.start(listener);
}
}
private static final class PollingStrategyListener implements PollingStrategy.Polled {
private final AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData;
private final Supplier<Optional<OverrideData>> reloader;
private final OverrideSource source;
private final PollableSource<Object> pollable;
private final AtomicReference<Object> lastStamp;
private final AtomicReference<Runnable> changeListener;
private PollingStrategyListener(AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData,
OverrideReloader reloader,
OverrideSource source,
PollableSource<Object> pollable,
AtomicReference<Object> lastStamp,
AtomicReference<Runnable> changeListener) {
this.lastData = lastData;
this.reloader = reloader;
this.source = source;
this.pollable = pollable;
this.lastStamp = lastStamp;
this.changeListener = changeListener;
}
@Override
public ChangeEventType poll(Instant when) {
Object lastStampValue = lastStamp.get();
synchronized (pollable) {
if ((null == lastStampValue) || pollable.isModified(lastStampValue)) {
Optional<OverrideData> overrideData = reloader.get();
if (overrideData.isEmpty()) {
if (source.optional()) {
// this is a valid change
setData(lastData, overrideData, changeListener);
} else {
LOGGER.info("Mandatory config source is not available, ignoring change.");
}
return ChangeEventType.DELETED;
} else {
setData(lastData, overrideData, changeListener);
return ChangeEventType.CHANGED;
}
}
}
return ChangeEventType.UNCHANGED;
}
}
private static final class WatchableChangesStarter implements Runnable {
private final WatchableSource<Object> watchable;
private final WatchableListener listener;
private final ChangeWatcher<Object> changeWatcher;
private WatchableChangesStarter(AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData,
OverrideReloader reloader,
OverrideSource source,
WatchableSource<Object> watchable,
ChangeWatcher<Object> changeWatcher,
AtomicReference<Runnable> changeListener) {
this.watchable = watchable;
this.changeWatcher = changeWatcher;
this.listener = new WatchableListener(lastData, reloader, source, changeListener);
}
@Override
public void run() {
Object target = watchable.target();
changeWatcher.start(target, listener);
}
}
private static final class WatchableListener implements Consumer<ChangeWatcher.ChangeEvent<Object>> {
private final AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData;
private final OverrideReloader reloader;
private final OverrideSource source;
private final AtomicReference<Runnable> changeListener;
private WatchableListener(AtomicReference<List<Map.Entry<Predicate<Config.Key>, String>>> lastData,
OverrideReloader reloader,
OverrideSource source,
AtomicReference<Runnable> changeListener) {
this.lastData = lastData;
this.reloader = reloader;
this.source = source;
this.changeListener = changeListener;
}
@Override
public void accept(ChangeWatcher.ChangeEvent<Object> change) {
try {
Optional<OverrideData> overrideData = reloader.get();
if (overrideData.isEmpty()) {
if (source.optional()) {
// this is a valid change
setData(lastData, overrideData, changeListener);
} else {
LOGGER.info("Mandatory config source is not available, ignoring change.");
}
} else {
setData(lastData, overrideData, changeListener);
}
} catch (Exception e) {
LOGGER.info("Failed to reload config source "
+ source
+ ", exception available in finest log level. "
+ "Change that triggered this event: "
+ change);
LOGGER.log(Level.FINEST, "Failed to reload config source", e);
}
}
}
private static final class OverrideReloader implements Supplier<Optional<OverrideData>> {
private final AtomicReference<Object> lastStamp;
private final OverrideSource overrideSource;
private OverrideReloader(AtomicReference<Object> lastStamp,
OverrideSource overrideSource) {
this.lastStamp = lastStamp;
this.overrideSource = overrideSource;
}
@Override
public Optional<OverrideData> get() {
synchronized (overrideSource) {
return overrideSource.load()
.map(content -> {
lastStamp.set(content.stamp().orElse(null));
return content.data();
});
}
}
}
}

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.
@@ -17,14 +17,11 @@
package io.helidon.config;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import io.helidon.config.internal.FileOverrideSource;
import io.helidon.config.internal.UrlOverrideSource;
import io.helidon.config.spi.AbstractOverrideSource;
import io.helidon.config.spi.ConfigContent;
import io.helidon.config.spi.OverrideSource;
/**
@@ -72,8 +69,7 @@ public final class OverrideSources {
* @param resourceName a name of the resource
* @return new Builder instance
*/
public static AbstractOverrideSource.Builder
<? extends AbstractOverrideSource.Builder<?, Path>, Path> classpath(String resourceName) {
public static ClasspathOverrideSource.Builder classpath(String resourceName) {
return ClasspathOverrideSource.builder().resource(resourceName);
}
@@ -83,8 +79,7 @@ public final class OverrideSources {
* @param file a file with an override value map
* @return an instance of builder
*/
public static AbstractOverrideSource.Builder
<? extends AbstractOverrideSource.Builder<?, Path>, Path> file(String file) {
public static FileOverrideSource.Builder file(String file) {
return FileOverrideSource.builder().path(Paths.get(file));
}
@@ -94,8 +89,7 @@ public final class OverrideSources {
* @param url an URL with an override value map
* @return an instance of builder
*/
public static AbstractOverrideSource.Builder
<? extends AbstractOverrideSource.Builder<?, URL>, URL> url(URL url) {
public static UrlOverrideSource.Builder url(URL url) {
return UrlOverrideSource.builder().url(url);
}
@@ -109,12 +103,27 @@ public final class OverrideSources {
/**
* EMPTY singleton instance.
*/
private static final OverrideSource EMPTY = Optional::empty;
private static final OverrideSource EMPTY = new EmptyOverrideSource();
private OverridingSourceHolder() {
throw new AssertionError("Instantiation not allowed.");
}
}
private static final class EmptyOverrideSource implements OverrideSource {
@Override
public Optional<ConfigContent.OverrideContent> load() throws ConfigException {
return Optional.empty();
}
@Override
public String toString() {
return "EmptyOverrideSource";
}
@Override
public boolean optional() {
return true;
}
}
}

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.
@@ -16,17 +16,11 @@
package io.helidon.config;
import java.nio.file.Path;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchService;
import java.time.Duration;
import java.util.concurrent.Flow;
import java.util.concurrent.ScheduledExecutorService;
import io.helidon.common.Builder;
import io.helidon.config.internal.FilesystemWatchPollingStrategy;
import io.helidon.config.internal.ScheduledPollingStrategy;
import io.helidon.config.internal.ScheduledPollingStrategy.RegularRecurringPolicy;
import io.helidon.config.ScheduledPollingStrategy.RegularRecurringPolicy;
import io.helidon.config.spi.PollingStrategy;
/**
@@ -36,7 +30,6 @@ import io.helidon.config.spi.PollingStrategy;
* {@link PollingStrategy} implementations.
*
* @see #regular(java.time.Duration)
* @see #watch(java.nio.file.Path)
* @see #nop()
* @see io.helidon.config.spi.PollingStrategy
*/
@@ -65,16 +58,6 @@ public final class PollingStrategies {
return new ScheduledBuilder(new RegularRecurringPolicy(interval));
}
/**
* Provides a filesystem watch polling strategy with a specified watched path.
*
* @param watchedPath a path which should be watched
* @return a filesystem watching polling strategy
*/
public static FilesystemWatchBuilder watch(Path watchedPath) {
return new FilesystemWatchBuilder(watchedPath);
}
/**
* A builder for a scheduled polling strategy.
*/
@@ -139,66 +122,6 @@ public final class PollingStrategies {
}
}
/**
* A builder for a filesystem watch polling strategy.
*/
public static final class FilesystemWatchBuilder implements Builder<PollingStrategy> {
private final Path path;
private ScheduledExecutorService executor = null;
private Modifier[] modifiers = null;
/*private*/ FilesystemWatchBuilder(Path path) {
this.path = path;
}
/**
* Sets a custom {@link ScheduledExecutorService executor} used to watch filesystem changes on.
* <p>
* By default single-threaded executor is used.
*
* @param executor the custom scheduled executor service
* @return a modified builder instance
*/
public FilesystemWatchBuilder executor(ScheduledExecutorService executor) {
this.executor = executor;
return this;
}
/**
* Add modifiers to be used when registering the {@link java.nio.file.WatchService}.
* See {@link Path#register(WatchService, java.nio.file.WatchEvent.Kind[], Modifier...)}
* Path.register}.
*
* @param modifiers the modifiers to add
* @return a modified builder instance
*/
public FilesystemWatchBuilder modifiers(Modifier... modifiers){
this.modifiers = modifiers;
return this;
}
/**
* Builds a new polling strategy.
*
* @return the new instance
*/
@Override
public PollingStrategy build() {
FilesystemWatchPollingStrategy strategy =
new FilesystemWatchPollingStrategy(path, executor);
if (modifiers != null && modifiers.length > 0) {
strategy.initWatchServiceModifiers(modifiers);
}
return strategy;
}
@Override
public PollingStrategy get() {
return build();
}
}
/**
* Holder of singleton instance of NOP implementation of {@link PollingStrategy}.
* Returned strategy does not fire an event at all.
@@ -212,7 +135,8 @@ public final class PollingStrategies {
/**
* NOP singleton instance.
*/
private static final PollingStrategy NOP = () -> Flow.Subscriber::onComplete;
private static final PollingStrategy NOP = polled -> {
};
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 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.
@@ -14,28 +14,36 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.MetaConfig;
import io.helidon.config.spi.ConfigContent;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.EventConfigSource;
import io.helidon.config.spi.NodeConfigSource;
import io.helidon.config.spi.RetryPolicy;
/**
* {@link ConfigSource} implementation wraps another config source and add key prefix to original one.
* Only supports "eager" config sources, such as {@link io.helidon.config.spi.ParsableSource}
* and {@link io.helidon.config.spi.NodeConfigSource}.
*
* @see io.helidon.config.ConfigSources#prefixed(String, java.util.function.Supplier)
*/
public class PrefixedConfigSource implements ConfigSource {
public final class PrefixedConfigSource implements ConfigSource,
NodeConfigSource,
EventConfigSource {
private static final String KEY_KEY = "key";
private final String key;
private final ConfigSource source;
private BiConsumer<String, ConfigNode> listener;
private ConfigSourceRuntime sourceRuntime;
private PrefixedConfigSource(String key, ConfigSource source) {
Objects.requireNonNull(key, "key cannot be null");
@@ -55,7 +63,7 @@ public class PrefixedConfigSource implements ConfigSource {
*/
public static PrefixedConfigSource create(Config metaConfig) {
String prefix = metaConfig.get(KEY_KEY).asString().orElse("");
ConfigSource configSource = MetaConfig.configSource(metaConfig);
ConfigSource configSource = MetaConfig.configSource(metaConfig).get(0);
return create(prefix, configSource);
}
@@ -71,21 +79,43 @@ public class PrefixedConfigSource implements ConfigSource {
return new PrefixedConfigSource(key, source);
}
@Override
public void init(ConfigContext context) {
this.sourceRuntime = context.sourceRuntime(source);
}
@Override
public String description() {
return String.format("prefixed[%s]:%s", key, source.description());
}
@Override
public Optional<ConfigNode.ObjectNode> load() throws ConfigException {
return source.load()
.map(originRoot -> new ObjectNodeBuilderImpl().addObject(key, originRoot).build())
.or(Optional::empty);
public Optional<ConfigContent.NodeContent> load() throws ConfigException {
sourceRuntime.onChange((key, config) -> listener.accept(key, config));
return sourceRuntime.load()
.map(originRoot -> ConfigContent.NodeContent.builder()
.node(new ObjectNodeBuilderImpl().addObject(key, originRoot).build())
.build());
}
@Override
public void init(ConfigContext context) {
source.init(context);
public void onChange(BiConsumer<String, ConfigNode> changedNode) {
this.listener = changedNode;
}
@Override
public boolean exists() {
return source.exists();
}
@Override
public Optional<RetryPolicy> retryPolicy() {
return source.retryPolicy();
}
@Override
public boolean optional() {
return source.optional();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 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,14 +14,13 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Priority;
import io.helidon.config.ConfigHelper;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParserException;
@@ -61,13 +60,14 @@ public class PropertiesConfigParser implements ConfigParser {
}
@Override
public <S> ConfigNode.ObjectNode parse(Content<S> content) throws ConfigParserException {
public ConfigNode.ObjectNode parse(ConfigParser.Content content) throws ConfigParserException {
Properties properties = new Properties();
try (AutoCloseable readable = content.asReadable()) {
properties.load(ConfigHelper.createReader((Readable) readable));
try {
properties.load(content.data());
} catch (Exception e) {
throw new ConfigParserException("Cannot read from source: " + e.getLocalizedMessage(), e);
}
return ConfigUtils.mapToObjectNode(ConfigUtils.propertiesToMap(properties), true);
}

View File

@@ -20,30 +20,23 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.internal.ObjectNodeBuilderImpl;
import io.helidon.config.internal.OverrideConfigFilter;
import io.helidon.config.internal.ValueNodeImpl;
import io.helidon.config.spi.ConfigFilter;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.OverrideSource;
/**
* Config provider represents initialization context used to create new instance of Config again and again.
@@ -52,33 +45,29 @@ class ProviderImpl implements Config.Context {
private static final Logger LOGGER = Logger.getLogger(ConfigFactory.class.getName());
private final List<Consumer<ConfigDiff>> listeners = new LinkedList<>();
private final ConfigMapperManager configMapperManager;
private final BuilderImpl.ConfigSourceConfiguration configSource;
private final OverrideSource overrideSource;
private final ConfigSourcesRuntime configSource;
private final OverrideSourceRuntime overrideSource;
private final List<Function<Config, ConfigFilter>> filterProviders;
private final boolean cachingEnabled;
private final Executor changesExecutor;
private final SubmissionPublisher<ConfigDiff> changesSubmitter;
private final Flow.Publisher<ConfigDiff> changesPublisher;
private final boolean keyResolving;
private final Function<String, List<String>> aliasGenerator;
private ConfigSourceChangeEventSubscriber configSourceChangeEventSubscriber;
private ConfigDiff lastConfigsDiff;
private AbstractConfigImpl lastConfig;
private OverrideSourceChangeEventSubscriber overrideSourceChangeEventSubscriber;
private volatile boolean overrideChangeComplete;
private volatile boolean configChangeComplete;
private boolean listening;
@SuppressWarnings("ParameterNumber")
ProviderImpl(ConfigMapperManager configMapperManager,
BuilderImpl.ConfigSourceConfiguration configSource,
OverrideSource overrideSource,
ConfigSourcesRuntime configSource,
OverrideSourceRuntime overrideSource,
List<Function<Config, ConfigFilter>> filterProviders,
boolean cachingEnabled,
Executor changesExecutor,
int changesMaxBuffer,
boolean keyResolving,
Function<String, List<String>> aliasGenerator) {
this.configMapperManager = configMapperManager;
@@ -93,36 +82,43 @@ class ProviderImpl implements Config.Context {
this.keyResolving = keyResolving;
this.aliasGenerator = aliasGenerator;
changesSubmitter = new RepeatLastEventPublisher<>(changesExecutor, changesMaxBuffer);
changesPublisher = ConfigHelper.suspendablePublisher(changesSubmitter,
this::subscribeSources,
this::cancelSourcesSubscriptions);
configSourceChangeEventSubscriber = null;
}
public AbstractConfigImpl newConfig() {
lastConfig = build(configSource.compositeSource().load());
public synchronized AbstractConfigImpl newConfig() {
lastConfig = build(configSource.load());
if (!listening) {
// only start listening for changes once the first config is built
configSource.changeListener(objectNode -> rebuild(objectNode, false));
configSource.startChanges();
overrideSource.changeListener(() -> rebuild(configSource.latest(), false));
overrideSource.startChanges();
listening = true;
}
return lastConfig;
}
@Override
public Config reload() {
rebuild(configSource.compositeSource().load(), true);
public synchronized Config reload() {
rebuild(configSource.latest(), true);
return lastConfig;
}
@Override
public Instant timestamp() {
public synchronized Instant timestamp() {
return lastConfig.timestamp();
}
@Override
public Config last() {
public synchronized Config last() {
return lastConfig;
}
void onChange(Consumer<ConfigDiff> listener) {
this.listeners.add(listener);
}
private synchronized AbstractConfigImpl build(Optional<ObjectNode> rootNode) {
// resolve tokens
@@ -130,11 +126,8 @@ class ProviderImpl implements Config.Context {
// filtering
ChainConfigFilter targetFilter = new ChainConfigFilter();
// add override filter
if (!overrideSource.equals(OverrideSources.empty())) {
OverrideConfigFilter filter = new OverrideConfigFilter(
() -> overrideSource.load().orElse(OverrideSource.OverrideData.empty()).data());
targetFilter.addFilter(filter);
}
overrideSource.addFilter(targetFilter);
// factory
ConfigFactory factory = new ConfigFactory(configMapperManager,
rootNode.orElseGet(ObjectNode::empty),
@@ -149,7 +142,6 @@ class ProviderImpl implements Config.Context {
if (cachingEnabled) {
targetFilter.enableCaching();
}
config.initMp();
return config;
}
@@ -175,7 +167,7 @@ class ProviderImpl implements Config.Context {
}
private Map<String, String> flattenNodes(ConfigNode node) {
return ConfigFactory.flattenNodes(ConfigKeyImpl.of(), node)
return ConfigHelper.flattenNodes(ConfigKeyImpl.of(), node)
.filter(e -> e.getValue() instanceof ValueNodeImpl)
.collect(Collectors.toMap(
e -> e.getKey().toString(),
@@ -205,7 +197,7 @@ class ProviderImpl implements Config.Context {
}
private Stream<String> tokensFromKey(String s) {
String[] tokens = s.split("\\.+(?![^(\\$\\{)]*\\})");
String[] tokens = s.split("\\.+(?![^(${)]*})");
return Arrays.stream(tokens).filter(t -> t.startsWith("$")).map(ProviderImpl::parseTokenReference);
}
@@ -239,50 +231,20 @@ class ProviderImpl implements Config.Context {
}
private void fireLastChangeEvent() {
if (lastConfigsDiff != null) {
LOGGER.log(Level.FINER, String.format("Firing last event %s (again)", lastConfigsDiff));
changesSubmitter.offer(lastConfigsDiff,
(subscriber, event) -> {
LOGGER.log(Level.FINER,
String.format("Event %s has not been delivered to %s.", event, subscriber));
return false;
});
ConfigDiff configDiffs;
synchronized (this) {
configDiffs = this.lastConfigsDiff;
}
}
private void subscribeSources() {
subscribeConfigSource();
subscribeOverrideSource();
//check if source has changed - reload
rebuild(configSource.compositeSource().load(), false);
}
if (configDiffs != null) {
LOGGER.log(Level.FINER, String.format("Firing last event %s (again)", configDiffs));
private void cancelSourcesSubscriptions() {
cancelConfigSource();
cancelOverrideSource();
}
private void subscribeConfigSource() {
configSourceChangeEventSubscriber = new ConfigSourceChangeEventSubscriber();
configSource.compositeSource().changes().subscribe(configSourceChangeEventSubscriber);
}
private void cancelConfigSource() {
if (configSourceChangeEventSubscriber != null) {
configSourceChangeEventSubscriber.cancelSubscription();
configSourceChangeEventSubscriber = null;
}
}
private void subscribeOverrideSource() {
overrideSourceChangeEventSubscriber = new OverrideSourceChangeEventSubscriber();
overrideSource.changes().subscribe(overrideSourceChangeEventSubscriber);
}
private void cancelOverrideSource() {
if (overrideSourceChangeEventSubscriber != null) {
overrideSourceChangeEventSubscriber.cancelSubscription();
overrideSourceChangeEventSubscriber = null;
changesExecutor.execute(() -> {
for (Consumer<ConfigDiff> listener : listeners) {
listener.accept(configDiffs);
}
});
}
}
@@ -297,22 +259,6 @@ class ProviderImpl implements Config.Context {
chain.filterProviders.stream()
.map(providerFunction -> providerFunction.apply(config))
.forEachOrdered(filter -> filter.init(config));
}
/**
* Allows to subscribe on changes of specified ConfigSource that causes creation of new Config instances.
* <p>
* The publisher repeats the last change event with any new subscriber.
*
* @return {@link Flow.Publisher} to be subscribed in. Never returns {@code null}.
* @see Config#onChange(java.util.function.Consumer)
*/
public Flow.Publisher<ConfigDiff> changes() {
return changesPublisher;
}
SubmissionPublisher<ConfigDiff> changesSubmitter() {
return changesSubmitter;
}
/**
@@ -344,17 +290,10 @@ class ProviderImpl implements Config.Context {
filterProviders.add((config) -> filter);
}
void addFilter(Function<Config, ConfigFilter> filterProvider) {
if (cachingEnabled) {
throw new IllegalStateException("Cannot add new filter provider to the chain when cache is already enabled.");
}
filterProviders.add(filterProvider);
}
@Override
public String apply(Config.Key key, String stringValue) {
if (cachingEnabled) {
if (!valueCache.containsKey(key)){
if (!valueCache.containsKey(key)) {
String value = proceedFilters(key, stringValue);
valueCache.put(key, value);
return value;
@@ -378,161 +317,4 @@ class ProviderImpl implements Config.Context {
this.valueCache = new ConcurrentHashMap<>();
}
}
/**
* {@link Flow.Subscriber} implementation to listen on {@link ConfigSource#changes()}.
*/
private class ConfigSourceChangeEventSubscriber implements Flow.Subscriber<Optional<ObjectNode>> {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Optional<ObjectNode> objectNode) {
ProviderImpl.this.changesExecutor.execute(() -> ProviderImpl.this.rebuild(objectNode, false));
}
@Override
public void onError(Throwable throwable) {
ProviderImpl.this.changesSubmitter
.closeExceptionally(new ConfigException(
String.format("'%s' config source changes support has failed. %s",
ProviderImpl.this.configSource.compositeSource().description(),
throwable.getLocalizedMessage()),
throwable));
}
@Override
public void onComplete() {
LOGGER.fine(String.format("'%s' config source changes support has completed.",
ProviderImpl.this.configSource.compositeSource().description()));
ProviderImpl.this.configChangeComplete = true;
if (ProviderImpl.this.overrideChangeComplete) {
ProviderImpl.this.changesSubmitter.close();
}
}
private void cancelSubscription() {
subscription.cancel();
}
}
/**
* {@link Flow.Subscriber} implementation to listen on {@link OverrideSource#changes()}.
*/
private class OverrideSourceChangeEventSubscriber
implements Flow.Subscriber<Optional<OverrideSource.OverrideData>> {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Optional<OverrideSource.OverrideData> overrideData) {
ProviderImpl.this.changesExecutor.execute(ProviderImpl.this::reload);
}
@Override
public void onError(Throwable throwable) {
ProviderImpl.this.changesSubmitter
.closeExceptionally(new ConfigException(
String.format("'%s' override source changes support has failed. %s",
ProviderImpl.this.overrideSource.description(),
throwable.getLocalizedMessage()),
throwable));
}
@Override
public void onComplete() {
LOGGER.fine(String.format("'%s' override source changes support has completed.",
ProviderImpl.this.overrideSource.description()));
ProviderImpl.this.overrideChangeComplete = true;
if (ProviderImpl.this.configChangeComplete) {
ProviderImpl.this.changesSubmitter.close();
}
}
private void cancelSubscription() {
if (subscription != null) {
subscription.cancel();
}
}
}
/**
* {@link Flow.Publisher} implementation that allows to repeat the last event for new-subscribers.
*/
private class RepeatLastEventPublisher<T> extends SubmissionPublisher<T> {
private RepeatLastEventPublisher(Executor executor, int maxBufferCapacity) {
super(executor, maxBufferCapacity);
}
@Override
public void subscribe(Flow.Subscriber<? super T> subscriber) {
super.subscribe(new RepeatLastEventSubscriber<>(subscriber));
// repeat the last event for new-subscribers
ProviderImpl.this.fireLastChangeEvent();
}
}
/**
* {@link Flow.Subscriber} wrapper implementation that allows to repeat the last event for new-subscribers
* and do NOT repeat same event more than once to same Subscriber.
*
* @see RepeatLastEventPublisher
*/
private static class RepeatLastEventSubscriber<T> implements Flow.Subscriber<T> {
private final Flow.Subscriber<? super T> delegate;
private Flow.Subscription subscription;
private T lastEvent;
private RepeatLastEventSubscriber(Flow.Subscriber<? super T> delegate) {
this.delegate = delegate;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
delegate.onSubscribe(subscription);
}
@Override
public void onNext(T event) {
if (lastEvent == event) { // do NOT repeat same event more than once to same Subscriber
//missed event must be requested once more
subscription.request(1);
} else {
lastEvent = event;
delegate.onNext(event);
}
}
@Override
public void onError(Throwable throwable) {
delegate.onError(throwable);
}
@Override
public void onComplete() {
delegate.onComplete();
}
}
}

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.
@@ -16,13 +16,9 @@
package io.helidon.config;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
import io.helidon.config.internal.ConfigThreadFactory;
import io.helidon.config.internal.RetryPolicyImpl;
import io.helidon.config.spi.RetryPolicy;
/**
@@ -37,7 +33,8 @@ public class RetryPolicies {
}
/**
* Creates a new instance of {@link RetryPolicies.Builder} class with a number of retries as a parameter.
* Creates a new instance of {@link io.helidon.config.SimpleRetryPolicy.Builder} class with a number of retries
* as a parameter.
* <p>
* The default values are:
* <ul>
@@ -53,8 +50,8 @@ public class RetryPolicies {
* @param retries a number of retries, excluding the first call
* @return a new builder
*/
public static Builder repeat(int retries) {
return new Builder(retries);
public static SimpleRetryPolicy.Builder repeat(int retries) {
return SimpleRetryPolicy.builder().retries(retries);
}
/**
@@ -78,149 +75,4 @@ public class RetryPolicies {
}
};
}
/**
* A builder of the default {@link RetryPolicy}.
*/
public static final class Builder implements io.helidon.common.Builder<RetryPolicy> {
private static final String RETRIES_KEY = "retries";
private int retries;
private Duration delay;
private double delayFactor;
private Duration callTimeout;
private Duration overallTimeout;
private ScheduledExecutorService executorService;
private Builder(int retries) {
this.retries = retries;
this.delay = Duration.ofMillis(200);
this.delayFactor = 2;
this.callTimeout = Duration.ofMillis(500);
this.overallTimeout = Duration.ofSeconds(2);
this.executorService = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("retry-policy"));
}
/**
* Initializes retry policy instance from configuration properties.
* <p>
* Mandatory {@code properties}, see {@link RetryPolicies#repeat(int)}:
* <ul>
* <li>{@code retries} - type {@code int}</li>
* </ul>
* Optional {@code properties}:
* <ul>
* <li>{@code delay} - type {@link Duration}, see {@link #delay(Duration)}</li>
* <li>{@code delay-factor} - type {@code double}, see {@link #delayFactor(double)}</li>
* <li>{@code call-timeout} - type {@link Duration}, see {@link #callTimeout(Duration)}</li>
* <li>{@code overall-timeout} - type {@link Duration}, see {@link #overallTimeout(Duration)}</li>
* </ul>
*
* @param metaConfig meta-configuration used to initialize returned polling strategy builder instance from.
* @return new instance of polling strategy builder described by {@code metaConfig}
* @throws MissingValueException in case the configuration tree does not contain all expected sub-nodes
* required by the mapper implementation to provide instance of Java type.
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see PollingStrategies#regular(Duration)
*/
public static Builder create(Config metaConfig) throws ConfigMappingException, MissingValueException {
// retries
Builder builder = new Builder(metaConfig.get(RETRIES_KEY).asInt().get());
// delay
metaConfig.get("delay").as(Duration.class)
.ifPresent(builder::delay);
// delay-factor
metaConfig.get("delay-factor").asDouble()
.ifPresent(builder::delayFactor);
// call-timeout
metaConfig.get("call-timeout").as(Duration.class)
.ifPresent(builder::callTimeout);
// overall-timeout
metaConfig.get("overall-timeout").as(Duration.class)
.ifPresent(builder::overallTimeout);
return builder;
}
/**
* Sets an initial delay between invocations, that is repeatedly multiplied by {@code delayFactor}.
* <p>
* The default value is 200ms.
*
* @param delay an overall timeout
* @return a modified builder instance
*/
public Builder delay(Duration delay) {
this.delay = delay;
return this;
}
/**
* Sets a factor that prolongs the delay for an every new execute.
* <p>
* The default value is 2.
*
* @param delayFactor a delay prolonging factor
* @return a modified builder instance
*/
public Builder delayFactor(double delayFactor) {
this.delayFactor = delayFactor;
return this;
}
/**
* Sets a limit for each invocation.
* <p>
* The default value is 500ms.
*
* @param callTimeout an invocation timeout - a limit per call
* @return a modified builder instance
*/
public Builder callTimeout(Duration callTimeout) {
this.callTimeout = callTimeout;
return this;
}
/**
* Sets a overall limit for all invocation, including delays.
* <p>
* The default value is 2s.
*
* @param overallTimeout an overall timeout
* @return a modified builder instance
*/
public Builder overallTimeout(Duration overallTimeout) {
this.overallTimeout = overallTimeout;
return this;
}
/**
* Sets a custom {@link ScheduledExecutorService executor} used to invoke a method call.
* <p>
* By default single-threaded executor is used.
*
* @param executorService the custom scheduled executor service
* @return a modified builder instance
*/
public Builder executor(ScheduledExecutorService executorService) {
this.executorService = executorService;
return this;
}
/**
* Builds a new execute policy.
*
* @return the new instance
*/
public RetryPolicy build() {
return new RetryPolicyImpl(retries, delay, delayFactor, callTimeout, overallTimeout, executorService);
}
@Override
public RetryPolicy get() {
return build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* Copyright (c) 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.
@@ -14,38 +14,49 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.time.Duration;
import java.util.Objects;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.ConfigHelper;
import io.helidon.config.spi.ChangeEventType;
import io.helidon.config.spi.PollingStrategy;
/**
* A strategy which allows the user to schedule periodically fired a polling event.
* A strategy which allows the user to schedule periodically fired polling event.
*/
public class ScheduledPollingStrategy implements PollingStrategy {
private static final Logger LOGGER = Logger.getLogger(ScheduledPollingStrategy.class.getName());
public final class ScheduledPollingStrategy implements PollingStrategy {
/*
* This class will trigger checks in a periodic manner.
* The actual check if the source has changed is done elsewhere, this is just responsible for telling us
* "check now".
* The feedback is an information whether the change happened or not.
*/
private final RecurringPolicy recurringPolicy;
private final SubmissionPublisher<PollingStrategy.PollingEvent> ticksSubmitter;
private final Flow.Publisher<PollingStrategy.PollingEvent> ticksPublisher;
private final boolean defaultExecutor;
private final boolean customExecutor;
private ScheduledFuture<?> scheduledFuture;
private ScheduledExecutorService executor;
private ScheduledFuture<?> scheduledFuture;
private Polled polled;
private ScheduledPollingStrategy(Builder builder) {
this.recurringPolicy = builder.recurringPolicy;
ScheduledExecutorService executor = builder.executor;
if (executor == null) {
this.executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling"));
this.defaultExecutor = true;
} else {
this.executor = executor;
this.defaultExecutor = false;
}
}
/**
* Creates a polling strategy with an interval of the polling as a parameter.
@@ -57,137 +68,68 @@ public class ScheduledPollingStrategy implements PollingStrategy {
* @return configured strategy
*/
public static ScheduledPollingStrategy create(RecurringPolicy recurringPolicy, ScheduledExecutorService executor) {
return new ScheduledPollingStrategy(recurringPolicy, executor);
}
private ScheduledPollingStrategy(RecurringPolicy recurringPolicy, ScheduledExecutorService executor) {
Objects.requireNonNull(recurringPolicy, "recurringPolicy cannot be null");
this.recurringPolicy = recurringPolicy;
if (executor == null) {
this.customExecutor = false;
} else {
this.customExecutor = true;
this.executor = executor;
}
ticksSubmitter = new SubmissionPublisher<>(Runnable::run, //deliver events on current thread
1); //(almost) do not buffer events
ticksPublisher = ConfigHelper.suspendablePublisher(ticksSubmitter,
this::startScheduling,
this::stopScheduling);
}
@Override
public Flow.Publisher<PollingEvent> ticks() {
return ticksPublisher;
}
//@Override //TODO WILL BE PUBLIC API AGAIN LATER, Issue #14.
/*public*/ void configSourceChanged(boolean changed) {
if (changed) {
recurringPolicy.shorten();
} else {
recurringPolicy.lengthen();
}
return builder()
.recurringPolicy(recurringPolicy)
.executor(executor)
.build();
}
/**
* Returns recurring policy.
* Fluent API builder for {@link io.helidon.config.ScheduledPollingStrategy}.
*
* @return recurring policy
* @return a new builder instance
*/
public RecurringPolicy recurringPolicy() {
return recurringPolicy;
public static Builder builder() {
return new Builder();
}
/*
* NOTE: TEMPORARILY MOVED FROM POLLING_STRATEGY, WILL BE PUBLIC API AGAIN LATER, Issue #14.
* <p>
* Creates a new scheduled polling strategy with an adaptive interval.
* <p>
* The minimal interval will not drop bellow {@code initialInterval / 10} and the maximal interval will not exceed {@code
* initialInterval * 5}.
* <p>
* The function that decreases the current interval is defined as a function {@code (currentDuration, changesFactor) ->
* currentDuration.dividedBy(2)} (half the current), whilst the function that increases the current interval is
* defined as {@code (currentDuration, changesFactor) -> currentDuration.multiplyBy(2)} (doubled the current),
* where {@code currentDuration} is the currently valid duration and {@code
* changesFactor} is a number of consecutive reloading configuration that brings the change or not ({@link
* RecurringPolicy#lengthen()} or {@link RecurringPolicy#shorten()} is called by {@link PollingStrategy} from {@link
* ScheduledPollingStrategy#configSourceChanged(boolean)}). The {@code changeFactor} might be positive (changes proceeded) or
* negative (changes did not proceed). In other words, more consecutive reloads with a change mean higher {@code changeFactor}
* and more consecutive reloads without any change mean lower {@code changeFactor}. When {@code changeFactor} is
* negative and on last fired event change has proceeded then {@code changeFactor} will be {@code +1}, and,
* conversely, positive number, regardless how high, is changed to {@code -1} when load was fired for nothing.
* Note that the default implementations do not take {@code changeFactor} into account and {@link
* RecurringPolicy.AdaptiveBuilder#shortenFunction} just halves the current interval and {@link
* RecurringPolicy.AdaptiveBuilder#lengthenFunction} doubles it.
* <p>
* If you need to adjust some of the parameters of the adaptive recurring policy, try {@link
* RecurringPolicy#adaptiveBuilder(Duration)} or make your own {@link RecurringPolicy} and create the scheduled polling
* strategy by calling {@link #recurringPolicyBuilder(RecurringPolicy)}.
*
* @param initialInterval an initial interval
* @return a polling strategy
* @see RecurringPolicy
*/
/*
static PollingStrategy adaptive(Duration initialInterval) {
return new PollingStrategies.ScheduledBuilder(RecurringPolicy.adaptiveBuilder(initialInterval).build()).build();
}
*/
/*
* NOTE: TEMPORARILY MOVED FROM POLLING_STRATEGY, WILL BE PUBLIC API AGAIN LATER, Issue #14.
* <p>
* Creates a scheduling polling strategy builder which allows users to add their own scheduler executor service.
*
* @param recurringPolicy a recurring policy
* @return a new builder
*/
/*
static PollingStrategies.ScheduledBuilder recurringPolicyBuilder(RecurringPolicy recurringPolicy) {
return new PollingStrategies.ScheduledBuilder(recurringPolicy);
}
*/
synchronized void startScheduling() {
if (!customExecutor) {
this.executor = Executors.newScheduledThreadPool(1, new ConfigThreadFactory("scheduled-polling"));
@Override
public synchronized void start(Polled polled) {
if (defaultExecutor && executor.isShutdown()) {
executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling"));
}
if (executor.isShutdown()) {
throw new ConfigException("Cannot start a scheduled polling strategy, as the executor service is shutdown");
}
this.polled = polled;
scheduleNext();
}
@Override
public synchronized void stop() {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
if (defaultExecutor) {
ConfigUtils.shutdownExecutor(executor);
}
}
private void scheduleNext() {
scheduledFuture = executor.schedule(this::fireEvent,
recurringPolicy.interval().toMillis(),
TimeUnit.MILLISECONDS);
}
private void fireEvent() {
ticksSubmitter.offer(
PollingEvent.now(),
(subscriber, pollingEvent) -> {
LOGGER.log(Level.FINER, String.format("Event %s has not been delivered to %s.", pollingEvent, subscriber));
return false;
});
private synchronized void fireEvent() {
ChangeEventType event = polled.poll(Instant.now());
switch (event) {
case CHANGED:
case DELETED:
recurringPolicy.shorten();
break;
case UNCHANGED:
recurringPolicy.lengthen();
break;
case CREATED:
default:
break;
}
scheduleNext();
}
synchronized void stopScheduling() {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
if (!customExecutor) {
ConfigUtils.shutdownExecutor(executor);
executor = null;
}
}
ScheduledFuture<?> scheduledFuture() {
return scheduledFuture;
}
ScheduledExecutorService executor() {
return executor;
}
@@ -199,6 +141,44 @@ public class ScheduledPollingStrategy implements PollingStrategy {
+ '}';
}
/**
* A fluent API builder for {@link io.helidon.config.ScheduledPollingStrategy}.
*/
public static final class Builder implements io.helidon.common.Builder<ScheduledPollingStrategy> {
private RecurringPolicy recurringPolicy;
private ScheduledExecutorService executor;
private Builder() {
}
@Override
public ScheduledPollingStrategy build() {
return new ScheduledPollingStrategy(this);
}
/**
* Configure the recurring policy to use.
*
* @param recurringPolicy policy
* @return updated builder instance
*/
public Builder recurringPolicy(RecurringPolicy recurringPolicy) {
this.recurringPolicy = recurringPolicy;
return this;
}
/**
* Executor service to use to schedule the polling events.
*
* @param executor executor service for scheduling events
* @return updated builder instance
*/
public Builder executor(ScheduledExecutorService executor) {
this.executor = executor;
return this;
}
}
/**
* Regular polling strategy implementation.
*
@@ -231,14 +211,14 @@ public class ScheduledPollingStrategy implements PollingStrategy {
static class AdaptiveRecurringPolicy implements RecurringPolicy {
private final AtomicInteger prolongationFactor = new AtomicInteger(0);
private final Duration min;
private final Duration max;
private final BiFunction<Duration, Integer, Duration> shortenFunction;
private final BiFunction<Duration, Integer, Duration> lengthenFunction;
private Duration delay;
private AtomicInteger prolongationFactor = new AtomicInteger(0);
AdaptiveRecurringPolicy(Duration min,
Duration initialDelay,
Duration max,
@@ -260,26 +240,28 @@ public class ScheduledPollingStrategy implements PollingStrategy {
public void shorten() {
int factor = prolongationFactor.updateAndGet((i) -> {
if (i < 0) {
return --i;
--i;
return i;
} else {
return -1;
}
});
Duration candidate = shortenFunction.apply(delay, -factor);
delay = min.compareTo(candidate) > 0 ? min : candidate;
delay = (min.compareTo(candidate) > 0) ? min : candidate;
}
@Override
public void lengthen() {
int factor = prolongationFactor.updateAndGet((i) -> {
if (i > 0) {
return ++i;
++i;
return i;
} else {
return 1;
}
});
Duration candidate = lengthenFunction.apply(delay, factor);
delay = max.compareTo(candidate) > 0 ? candidate : max;
delay = (max.compareTo(candidate) > 0) ? candidate : max;
}
Duration delay() {
@@ -288,13 +270,11 @@ public class ScheduledPollingStrategy implements PollingStrategy {
}
/**
* NOTE: TEMPORARILY MOVED FROM POLLING_STRATEGY, WILL BE PUBLIC API AGAIN LATER, Issue #14.
* <p>
* An SPI that allows users to define their own policy how to change the interval between scheduled ticking.
* <p>
* The only needed implementation is of {@link #interval()}. Methods {@link #shorten()} and {@link #lengthen()} might be used
* to shorten or to lengthen an interval. Both of them are called from scheduled polling strategy {@link
* ScheduledPollingStrategy#configSourceChanged(boolean) method}.
* to shorten or to lengthen an interval. Both of them are called from scheduled polling strategy depending on
* the result of the polling event ({@link io.helidon.config.spi.PollingStrategy.Polled#poll(java.time.Instant)}.
*/
@FunctionalInterface
public interface RecurringPolicy {
@@ -336,12 +316,11 @@ public class ScheduledPollingStrategy implements PollingStrategy {
/**
* Creates a builder of {@link RecurringPolicy} with an ability to change the behaviour, with a boundaries and
* the possibility to react to feedback given by {@link #shorten()} or {@link #lengthen()}.
* <p>
*/
//* See {@link ScheduledPollingStrategy#adaptive(Duration)} for detailed documentation.
final class AdaptiveBuilder {
private Duration interval;
private final Duration interval;
private Duration min;
private Duration max;
private BiFunction<Duration, Integer, Duration> shortenFunction;
@@ -409,11 +388,11 @@ public class ScheduledPollingStrategy implements PollingStrategy {
* @return the new instance
*/
public RecurringPolicy build() {
Duration min = this.min == null ? interval.dividedBy(10) : this.min;
Duration max = this.max == null ? interval.multipliedBy(5) : this.max;
BiFunction<Duration, Integer, Duration> lengthenFunction = this.lengthenFunction == null
Duration min = (this.min == null) ? interval.dividedBy(10) : this.min;
Duration max = (this.max == null) ? interval.multipliedBy(5) : this.max;
BiFunction<Duration, Integer, Duration> lengthenFunction = (this.lengthenFunction == null)
? DEFAULT_LENGTHEN : this.lengthenFunction;
BiFunction<Duration, Integer, Duration> shortenFunction = this.shortenFunction == null
BiFunction<Duration, Integer, Duration> shortenFunction = (this.shortenFunction == null)
? DEFAULT_SHORTEN : this.shortenFunction;
return new ScheduledPollingStrategy.AdaptiveRecurringPolicy(min, interval, max, shortenFunction,
lengthenFunction);

View File

@@ -0,0 +1,317 @@
/*
* Copyright (c) 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.time.Duration;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.util.logging.Logger;
import io.helidon.config.spi.RetryPolicy;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* A default retry policy implementation with {@link ScheduledExecutorService}.
* Following attributes can be configured:
* <ul>
* <li>number of retries (excluding the first invocation)</li>
* <li>delay between the invocations</li>
* <li>a delay multiplication factor</li>
* <li>a timeout for the individual invocation</li>
* <li>an overall timeout</li>
* <li>an executor service</li>
* </ul>
*/
public final class SimpleRetryPolicy implements RetryPolicy {
private static final Logger LOGGER = Logger.getLogger(SimpleRetryPolicy.class.getName());
private final int retries;
private final Duration delay;
private final double delayFactor;
private final Duration callTimeout;
private final Duration overallTimeout;
private final ScheduledExecutorService executorService;
private volatile ScheduledFuture<?> future;
private SimpleRetryPolicy(Builder builder) {
this.retries = builder.retries;
this.delay = builder.delay;
this.delayFactor = builder.delayFactor;
this.callTimeout = builder.callTimeout;
this.overallTimeout = builder.overallTimeout;
this.executorService = builder.executorService;
}
/**
* Fluent API builder for {@link io.helidon.config.SimpleRetryPolicy}.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Initializes retry policy instance from configuration properties.
* <p>
* Mandatory {@code properties}, see {@link RetryPolicies#repeat(int)}:
* <ul>
* <li>{@code retries} - type {@code int}</li>
* </ul>
* Optional {@code properties}:
* <ul>
* <li>{@code delay} - type {@link Duration}, see {@link Builder#delay(Duration)}</li>
* <li>{@code delay-factor} - type {@code double}, see {@link Builder#delayFactor(double)}</li>
* <li>{@code call-timeout} - type {@link Duration}, see {@link Builder#callTimeout(Duration)}</li>
* <li>{@code overall-timeout} - type {@link Duration}, see {@link Builder#overallTimeout(Duration)}</li>
* </ul>
*
* @param metaConfig meta-configuration used to initialize returned polling strategy builder instance from.
* @return new instance of polling strategy builder described by {@code metaConfig}
* @throws MissingValueException in case the configuration tree does not contain all expected sub-nodes
* required by the mapper implementation to provide instance of Java type.
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see PollingStrategies#regular(Duration)
*/
public static SimpleRetryPolicy create(Config metaConfig) {
return builder().config(metaConfig).build();
}
@Override
public <T> T execute(Supplier<T> call) throws ConfigException {
Duration currentDelay = Duration.ZERO;
long overallTimeoutsLeft = overallTimeout.toMillis();
Throwable last = null;
for (int i = 0; i <= retries; i++) {
try {
LOGGER.finest("next delay: " + currentDelay);
overallTimeoutsLeft -= currentDelay.toMillis();
if (overallTimeoutsLeft < 0) {
LOGGER.finest("overall timeout left [ms]: " + overallTimeoutsLeft);
throw new ConfigException(
"Cannot schedule the next call, the current delay would exceed the overall timeout.");
}
ScheduledFuture<T> localFuture = executorService.schedule(call::get, currentDelay.toMillis(), MILLISECONDS);
future = localFuture;
return localFuture.get(min(currentDelay.plus(callTimeout).toMillis(), overallTimeoutsLeft), MILLISECONDS);
} catch (ConfigException e) {
throw e;
} catch (CancellationException e) {
throw new ConfigException("An invocation has been canceled.", e);
} catch (InterruptedException e) {
throw new ConfigException("An invocation has been interrupted.", e);
} catch (TimeoutException e) {
throw new ConfigException("A timeout has been reached.", e);
} catch (Throwable t) {
last = t;
}
currentDelay = nextDelay(i, currentDelay);
}
throw new ConfigException("All repeated calls failed.", last);
}
Duration nextDelay(int invocation, Duration currentDelay) {
if (invocation == 0) {
return delay;
} else {
return Duration.ofMillis((long) (currentDelay.toMillis() * delayFactor));
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (future != null) {
if (!future.isDone() && !future.isCancelled()) {
return future.cancel(mayInterruptIfRunning);
}
}
return false;
}
/**
* Number of retries.
* @return retries
*/
public int retries() {
return retries;
}
/**
* Delay between retries.
*
* @return delay
*/
public Duration delay() {
return delay;
}
/**
* Delay multiplication factor.
* @return delay factor
*/
public double delayFactor() {
return delayFactor;
}
/**
* Timeout of the call.
* @return call timeout
*/
public Duration callTimeout() {
return callTimeout;
}
/**
* Overall timeout.
* @return overall timeout
*/
public Duration overallTimeout() {
return overallTimeout;
}
/**
* Fluent API builder for {@link io.helidon.config.SimpleRetryPolicy}.
*/
public static final class Builder implements io.helidon.common.Builder<SimpleRetryPolicy> {
private int retries = 3;
private Duration delay = Duration.ofMillis(200);
private double delayFactor = 2;
private Duration callTimeout = Duration.ofMillis(500);
private Duration overallTimeout = Duration.ofSeconds(2);
private ScheduledExecutorService executorService;
@Override
public SimpleRetryPolicy build() {
if (null == executorService) {
this.executorService = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("retry-policy"));
}
return new SimpleRetryPolicy(this);
}
/**
* Update this builder from meta configuration.
* <p>
* Mandatory {@code properties}, see {@link RetryPolicies#repeat(int)}:
* <ul>
* <li>{@code retries} - type {@code int}</li>
* </ul>
* Optional {@code properties}:
* <ul>
* <li>{@code delay} - type {@link Duration}, see {@link #delay(Duration)}</li>
* <li>{@code delay-factor} - type {@code double}, see {@link #delayFactor(double)}</li>
* <li>{@code call-timeout} - type {@link Duration}, see {@link #callTimeout(Duration)}</li>
* <li>{@code overall-timeout} - type {@link Duration}, see {@link #overallTimeout(Duration)}</li>
* </ul>
*
* @param metaConfig meta configuration used to update this builder
* @return updated builder instance
*/
public Builder config(Config metaConfig) {
// retries
metaConfig.get("retries").asInt().ifPresent(this::retries);
// delay
metaConfig.get("delay").as(Duration.class)
.ifPresent(this::delay);
// delay-factor
metaConfig.get("delay-factor").asDouble()
.ifPresent(this::delayFactor);
// call-timeout
metaConfig.get("call-timeout").as(Duration.class)
.ifPresent(this::callTimeout);
// overall-timeout
metaConfig.get("overall-timeout").as(Duration.class)
.ifPresent(this::overallTimeout);
return this;
}
/**
* Number of retries (excluding the first invocation).
*
* @param retries how many times to retry
* @return updated builder instance
*/
public Builder retries(int retries) {
this.retries = retries;
return this;
}
/**
* Delay between the invocations.
*
* @param delay delay between the invocations
* @return updated builder instance
*/
public Builder delay(Duration delay) {
this.delay = delay;
return this;
}
/**
* A delay multiplication factor.
*
* @param delayFactor a delay multiplication factor
* @return updated builder instance
*/
public Builder delayFactor(double delayFactor) {
this.delayFactor = delayFactor;
return this;
}
/**
* Timeout for the individual invocation.
*
* @param callTimeout a timeout for the individual invocation
* @return updated builder instance
*/
public Builder callTimeout(Duration callTimeout) {
this.callTimeout = callTimeout;
return this;
}
/**
* Overall timeout.
*
* @param overallTimeout an overall timeout
* @return updated builder instance
*/
public Builder overallTimeout(Duration overallTimeout) {
this.overallTimeout = overallTimeout;
return this;
}
/**
* An executor service to schedule retries and run timed operations on.
*
* @param executorService service
* @return updated builder instance
*/
public Builder executorService(ScheduledExecutorService executorService) {
this.executorService = executorService;
return this;
}
}
}

View File

@@ -1,160 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Publisher with "suspended" and "running" behavior.
* <p>
* Additional hooks:
* <ul>
* <li>{@link #onFirstSubscriptionRequest()} - invoked when the first subscriber
* requests the publisher
* </li>
* <li>{@link #onLastSubscriptionCancel()} - invoked when the last subscribed
* subscriber cancels its subscription
* </li>
* </ul>
*
* @param <T> the published item type
*/
abstract class SuspendablePublisher<T> implements Flow.Publisher<T> {
private final Flow.Publisher<T> delegatePublisher;
private final AtomicBoolean running;
private final AtomicInteger numberOfSubscribers;
private final Object lock = new Object();
/**
* Creates a new SuspendablePublisher using the given Executor for
* async delivery to subscribers, with the given maximum buffer size
* for each subscriber, and, if non-null, the given handler invoked
* when any Subscriber throws an exception in method {@link
* Flow.Subscriber#onNext(Object) onNext}.
*
* @param delegatePublisher publisher used to delegate to
*/
SuspendablePublisher(Flow.Publisher<T> delegatePublisher) {
this.delegatePublisher = delegatePublisher;
running = new AtomicBoolean(false);
numberOfSubscribers = new AtomicInteger(0);
}
/**
* Hook invoked in case the first subscriber has requested the publisher.
*/
protected abstract void onFirstSubscriptionRequest();
/**
* Hook invoked in case the last subscribed subscriber has canceled it's subscriptions.
*/
protected abstract void onLastSubscriptionCancel();
@Override
public void subscribe(Flow.Subscriber<? super T> subscriber) {
delegatePublisher.subscribe(new SuspendableSubscriber<>(subscriber, this::beforeRequestHook, this::afterCancelHook));
numberOfSubscribers.incrementAndGet();
}
private void beforeRequestHook() {
if (!running.get()) {
synchronized (lock) {
if (!running.get()) {
running.set(true);
onFirstSubscriptionRequest();
}
}
}
}
private void afterCancelHook() {
numberOfSubscribers.decrementAndGet();
if (numberOfSubscribers.intValue() == 0 && running.get()) {
synchronized (lock) {
if (running.get()) {
onLastSubscriptionCancel();
running.set(false);
}
}
}
}
private static class SuspendableSubscriber<T> implements Flow.Subscriber<T> {
private final Flow.Subscriber<? super T> delegate;
private final Runnable beforeRequestHook;
private final Runnable afterCancelHook;
private SuspendableSubscriber(Flow.Subscriber<? super T> delegate, Runnable beforeRequestHook, Runnable afterCancelHook) {
this.delegate = delegate;
this.beforeRequestHook = beforeRequestHook;
this.afterCancelHook = afterCancelHook;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
delegate.onSubscribe(new SuspendableSubscription(subscription, beforeRequestHook, afterCancelHook));
}
@Override
public void onNext(T item) {
delegate.onNext(item);
}
@Override
public void onError(Throwable throwable) {
delegate.onError(throwable);
}
@Override
public void onComplete() {
delegate.onComplete();
}
}
private static class SuspendableSubscription implements Flow.Subscription {
private final Flow.Subscription subscription;
private final Runnable beforeRequestHook;
private final Runnable afterCancelHook;
private SuspendableSubscription(Flow.Subscription subscription, Runnable beforeRequestHook, Runnable afterCancelHook) {
this.subscription = subscription;
this.beforeRequestHook = beforeRequestHook;
this.afterCancelHook = afterCancelHook;
}
@Override
public void request(long n) {
beforeRequestHook.run();
subscription.request(n);
}
@Override
public void cancel() {
subscription.cancel();
afterCancelHook.run();
}
}
}

View File

@@ -17,44 +17,46 @@
package io.helidon.config;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.spi.AbstractParsableConfigSource;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParser.Content;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.ParsableSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* {@link ConfigSource} implementation that loads configuration content from specified endpoint URL.
*
* @see AbstractParsableConfigSource.Builder
* @see AbstractConfigSourceBuilder
*/
public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
public final class UrlConfigSource extends AbstractConfigSource
implements WatchableSource<URL>, ParsableSource, PollableSource<Instant> {
private static final Logger LOGGER = Logger.getLogger(UrlConfigSource.class.getName());
private static final String HEAD_METHOD = "HEAD";
private static final String GET_METHOD = "GET";
private static final String URL_KEY = "url";
private static final int STATUS_NOT_FOUND = 404;
private final URL url;
UrlConfigSource(UrlBuilder builder, URL url) {
private UrlConfigSource(Builder builder) {
super(builder);
this.url = url;
this.url = builder.url;
}
/**
@@ -64,7 +66,7 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
* <ul>
* <li>{@code url} - type {@link URL}</li>
* </ul>
* Optional {@code properties}: see {@link AbstractParsableConfigSource.Builder#config(Config)}.
* Optional {@code properties}: see {@link AbstractConfigSourceBuilder#config(Config)}.
*
* @param metaConfig meta-configuration used to initialize returned config source instance from.
* @return new instance of config source described by {@code metaConfig}
@@ -73,7 +75,7 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see io.helidon.config.ConfigSources#url(URL)
* @see AbstractParsableConfigSource.Builder#config(Config)
* @see AbstractConfigSourceBuilder#config(Config)
*/
public static UrlConfigSource create(Config metaConfig) throws ConfigMappingException, MissingValueException {
return builder()
@@ -86,8 +88,8 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
*
* @return a new builder instance
*/
public static UrlBuilder builder() {
return new UrlBuilder();
public static Builder builder() {
return new Builder();
}
@Override
@@ -96,8 +98,42 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
}
@Override
protected ConfigParser.Content<Instant> content() throws ConfigException {
// assumption about HTTP URL connection is wrong here
public URL target() {
return url;
}
@Override
public Class<URL> targetType() {
return URL.class;
}
@Override
public Optional<ConfigParser> parser() {
return super.parser();
}
@Override
public Optional<String> mediaType() {
return super.mediaType();
}
@Override
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Optional<ChangeWatcher<Object>> changeWatcher() {
return super.changeWatcher();
}
@Override
public boolean isModified(Instant stamp) {
return UrlHelper.isModified(url, stamp);
}
@Override
public Optional<Content> load() throws ConfigException {
try {
URLConnection urlConnection = url.openConnection();
@@ -109,53 +145,61 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
} catch (ConfigException ex) {
throw ex;
} catch (Exception ex) {
throw new ConfigException("Configuration at url '" + url + "' GET is not accessible.", ex);
throw new ConfigException("Configuration at url '" + url + "' is not accessible.", ex);
}
}
private ConfigParser.Content<Instant> genericContent(URLConnection urlConnection) throws IOException, URISyntaxException {
Reader reader = new InputStreamReader(urlConnection.getInputStream(),
StandardCharsets.UTF_8);
private Optional<Content> genericContent(URLConnection urlConnection) throws IOException {
InputStream is = urlConnection.getInputStream();
ConfigParser.Content.Builder<Instant> builder = ConfigParser.Content.builder(reader);
builder.stamp(Instant.now());
mediaType()
.or(this::probeContentType)
.ifPresent(builder::mediaType);
Content.Builder builder = Content.builder()
.data(is)
.stamp(Instant.now());
return builder.build();
this.probeContentType().ifPresent(builder::mediaType);
return Optional.ofNullable(builder.build());
}
private ConfigParser.Content<Instant> httpContent(HttpURLConnection connection) throws IOException, URISyntaxException {
private Optional<Content> httpContent(HttpURLConnection connection) throws IOException {
connection.setRequestMethod(GET_METHOD);
try {
connection.connect();
} catch (IOException e) {
// considering this to be unavailable
LOGGER.log(Level.FINEST, "Failed to connect to " + url + ", considering this source to be missing", e);
return Optional.empty();
}
if (STATUS_NOT_FOUND == connection.getResponseCode()) {
return Optional.empty();
}
Optional<String> mediaType = mediaType(connection.getContentType());
final Instant timestamp;
if (connection.getLastModified() != 0) {
timestamp = Instant.ofEpochMilli(connection.getLastModified());
} else {
if (connection.getLastModified() == 0) {
timestamp = Instant.now();
LOGGER.fine("Missing GET '" + url + "' response header 'Last-Modified'. Used current time '"
+ timestamp + "' as a content timestamp.");
} else {
timestamp = Instant.ofEpochMilli(connection.getLastModified());
}
Reader reader = new InputStreamReader(connection.getInputStream(),
ConfigUtils.getContentCharset(connection.getContentEncoding()));
InputStream inputStream = connection.getInputStream();
Charset charset = ConfigUtils.getContentCharset(connection.getContentEncoding());
ConfigParser.Content.Builder<Instant> builder = ConfigParser.Content.builder(reader);
Content.Builder builder = Content.builder();
builder.data(inputStream);
builder.charset(charset);
builder.stamp(timestamp);
mediaType.ifPresent(builder::mediaType);
return builder.build();
return Optional.of(builder.build());
}
@Override
protected Optional<String> mediaType() {
return super.mediaType();
}
private Optional<String> mediaType(String responseMediaType) throws URISyntaxException {
private Optional<String> mediaType(String responseMediaType) {
return mediaType()
.or(() -> Optional.ofNullable(responseMediaType))
.or(() -> {
@@ -171,33 +215,6 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
return MediaTypes.detectType(url);
}
@Override
protected Optional<Instant> dataStamp() {
// the URL may not be an HTTP URL
try {
URLConnection urlConnection = url.openConnection();
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection connection = (HttpURLConnection) urlConnection;
try {
connection.setRequestMethod(HEAD_METHOD);
if (connection.getLastModified() != 0) {
return Optional.of(Instant.ofEpochMilli(connection.getLastModified()));
}
} finally {
connection.disconnect();
}
}
} catch (IOException ex) {
LOGGER.log(Level.FINE, ex, () -> "Configuration at url '" + url + "' HEAD is not accessible.");
}
Optional<Instant> timestamp = Optional.of(Instant.MAX);
LOGGER.finer("Missing HEAD '" + url + "' response header 'Last-Modified'. Used time '"
+ timestamp + "' as a content timestamp.");
return timestamp;
}
/**
* Url ConfigSource Builder.
* <p>
@@ -209,20 +226,20 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
* <li>{@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;</li>
* </ul>
* <p>
* If the Url ConfigSource is {@code mandatory} and a {@code url} endpoint does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* <p>
* If {@code media-type} not set it uses HTTP response header {@code content-type}.
* If {@code media-type} not returned it tries to guess it from url suffix.
*/
public static final class UrlBuilder extends Builder<UrlBuilder, URL, UrlConfigSource> {
public static final class Builder extends AbstractConfigSourceBuilder<Builder, URL>
implements PollableSource.Builder<Builder>,
WatchableSource.Builder<Builder, URL>,
ParsableSource.Builder<Builder>,
io.helidon.common.Builder<UrlConfigSource> {
private URL url;
/**
* Initialize builder.
*/
private UrlBuilder() {
super(URL.class);
private Builder() {
}
/**
@@ -231,7 +248,7 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
* @param url of configuration source
* @return updated builder instance
*/
public UrlBuilder url(URL url) {
public Builder url(URL url) {
this.url = url;
return this;
}
@@ -245,16 +262,11 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
* @return updated builder instance
*/
@Override
public UrlBuilder config(Config metaConfig) {
public Builder config(Config metaConfig) {
metaConfig.get(URL_KEY).as(URL.class).ifPresent(this::url);
return super.config(metaConfig);
}
@Override
protected URL target() {
return url;
}
/**
* Builds new instance of Url ConfigSource.
* <p>
@@ -262,15 +274,32 @@ public class UrlConfigSource extends AbstractParsableConfigSource<Instant> {
*
* @return new instance of Url ConfigSource.
*/
@Override
public UrlConfigSource build() {
if (null == url) {
throw new IllegalArgumentException("url must be provided");
}
return new UrlConfigSource(this, url);
return new UrlConfigSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
@Override
public Builder parser(ConfigParser parser) {
return super.parser(parser);
}
@Override
public Builder mediaType(String mediaType) {
return super.mediaType(mediaType);
}
@Override
public Builder changeWatcher(ChangeWatcher<URL> changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) 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.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility for URL sources.
*/
final class UrlHelper {
private static final Logger LOGGER = Logger.getLogger(UrlHelper.class.getName());
private static final String HEAD_METHOD = "HEAD";
static final int STATUS_NOT_FOUND = 404;
private UrlHelper() {
}
static boolean isModified(URL url, Instant stamp) {
return dataStamp(url)
.map(newStamp -> newStamp.isAfter(stamp) || newStamp.equals(Instant.MIN))
.orElse(true);
}
static Optional<Instant> dataStamp(URL url) {
// the URL may not be an HTTP URL
try {
URLConnection urlConnection = url.openConnection();
if (urlConnection instanceof HttpURLConnection) {
HttpURLConnection connection = (HttpURLConnection) urlConnection;
try {
connection.setRequestMethod(HEAD_METHOD);
if (STATUS_NOT_FOUND == connection.getResponseCode()) {
return Optional.empty();
}
if (connection.getLastModified() != 0) {
return Optional.of(Instant.ofEpochMilli(connection.getLastModified()));
}
} finally {
connection.disconnect();
}
}
} catch (IOException ex) {
LOGGER.log(Level.FINE, ex, () -> "Configuration at url '" + url + "' HEAD is not accessible.");
return Optional.empty();
}
Instant timestamp = Instant.MIN;
LOGGER.finer("Missing HEAD '" + url + "' response header 'Last-Modified'. Used time '"
+ timestamp + "' as a content timestamp.");
return Optional.of(timestamp);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 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.
@@ -14,9 +14,8 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
@@ -24,34 +23,34 @@ import java.net.URL;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.OverrideSources;
import io.helidon.config.spi.AbstractOverrideSource;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigContent.OverrideContent;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.OverrideSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* {@link io.helidon.config.spi.OverrideSource} implementation that loads configuration override content from specified endpoint URL.
* {@link io.helidon.config.spi.OverrideSource} implementation that loads configuration override content from specified
* endpoint URL.
*
* @see AbstractOverrideSource.Builder
* @see AbstractSource
* @see OverrideSources
*/
public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
public class UrlOverrideSource extends AbstractSource
implements OverrideSource, PollableSource<Instant>, WatchableSource<URL> {
private static final Logger LOGGER = Logger.getLogger(UrlOverrideSource.class.getName());
private static final String GET_METHOD = "GET";
private static final String HEAD_METHOD = "HEAD";
private static final String URL_KEY = "url";
private final URL url;
UrlOverrideSource(UrlBuilder builder) {
UrlOverrideSource(Builder builder) {
super(builder);
this.url = builder.url;
@@ -60,7 +59,7 @@ public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
/**
* Create a new URL override source from meta configuration.
*
* @param metaConfig meta configuration containing at least the {@key url} key
* @param metaConfig meta configuration containing at least the {@code url} key
* @return a new URL override source
*/
public static UrlOverrideSource create(Config metaConfig) {
@@ -72,29 +71,61 @@ public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
*
* @return a new builder
*/
public static UrlBuilder builder() {
return new UrlBuilder();
public static Builder builder() {
return new Builder();
}
@Override
protected Data<OverrideData, Instant> loadData() throws ConfigException {
public boolean isModified(Instant stamp) {
return UrlHelper.isModified(url, stamp);
}
@Override
public Optional<PollingStrategy> pollingStrategy() {
return super.pollingStrategy();
}
@Override
public URL target() {
return url;
}
@Override
public Class<URL> targetType() {
return URL.class;
}
@Override
public Optional<ChangeWatcher<Object>> changeWatcher() {
return super.changeWatcher();
}
@Override
public Optional<OverrideContent> load() throws ConfigException {
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(GET_METHOD);
if (UrlHelper.STATUS_NOT_FOUND == connection.getResponseCode()) {
return Optional.empty();
}
Instant timestamp;
if (connection.getLastModified() != 0) {
timestamp = Instant.ofEpochMilli(connection.getLastModified());
} else {
if (connection.getLastModified() == 0) {
timestamp = Instant.now();
LOGGER.fine("Missing GET '" + url + "' response header 'Last-Modified'. Used current time '"
+ timestamp + "' as a content timestamp.");
} else {
timestamp = Instant.ofEpochMilli(connection.getLastModified());
}
Reader reader = new InputStreamReader(connection.getInputStream(),
ConfigUtils.getContentCharset(connection.getContentEncoding()));
return new Data<>(Optional.of(OverrideData.create(reader)), Optional.of(timestamp));
OverrideContent.Builder builder = OverrideContent.builder()
.data(OverrideData.create(reader))
.stamp(timestamp);
return Optional.of(builder.build());
} catch (ConfigException ex) {
throw ex;
} catch (Exception ex) {
@@ -108,24 +139,6 @@ public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
return url.toString();
}
@Override
protected Optional<Instant> dataStamp() {
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(HEAD_METHOD);
if (connection.getLastModified() != 0) {
return Optional.of(Instant.ofEpochMilli(connection.getLastModified()));
}
} catch (IOException ex) {
LOGGER.log(Level.FINE, ex, () -> "Configuration at url '" + url + "' HEAD is not accessible.");
}
Optional<Instant> timestamp = Optional.of(Instant.MAX);
LOGGER.finer("Missing HEAD '" + url + "' response header 'Last-Modified'. Used time '"
+ timestamp + "' as a content timestamp.");
return timestamp;
}
/**
* Url Override Source Builder.
* <p>
@@ -137,42 +150,19 @@ public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
* <li>{@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;</li>
* </ul>
* <p>
* If the Url ConfigSource is {@code mandatory} and a {@code url} endpoint does not exist
* then {@link ConfigSource#load} throws {@link ConfigException}.
* <p>
* If {@code media-type} not set it uses HTTP response header {@code content-type}.
* If {@code media-type} not returned it tries to guess it from url suffix.
*/
public static final class UrlBuilder extends AbstractOverrideSource.Builder<UrlBuilder, URL> {
public static final class Builder extends AbstractSourceBuilder<Builder, URL>
implements PollableSource.Builder<Builder>,
WatchableSource.Builder<Builder, URL>,
io.helidon.common.Builder<UrlOverrideSource> {
private URL url;
/**
* Initialize builder.
*/
private UrlBuilder() {
super(URL.class);
}
/**
* Configure the URL that is source of this overrides.
*
* @param url url of the resource to load
* @return updated builder instance
*/
public UrlBuilder url(URL url) {
this.url = url;
return this;
}
@Override
public UrlBuilder config(Config metaConfig) {
metaConfig.get(URL_KEY).as(URL.class).ifPresent(this::url);
return super.config(metaConfig);
}
@Override
protected URL target() {
return url;
private Builder() {
}
/**
@@ -188,9 +178,31 @@ public class UrlOverrideSource extends AbstractOverrideSource<Instant> {
return new UrlOverrideSource(this);
}
PollingStrategy pollingStrategyInternal() { //just for testing purposes
return super.pollingStrategy();
@Override
public Builder config(Config metaConfig) {
metaConfig.get(URL_KEY).as(URL.class).ifPresent(this::url);
return super.config(metaConfig);
}
/**
* Configure the URL that is source of this overrides.
*
* @param url url of the resource to load
* @return updated builder instance
*/
public Builder url(URL url) {
this.url = url;
return this;
}
@Override
public Builder changeWatcher(ChangeWatcher<URL> changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Flow;
import io.helidon.config.spi.ConfigContext;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigSource;
/**
* Composite ConfigSource that accepts list of config sources and and the first one that loads data is used.
*/
class UseFirstAvailableConfigSource implements ConfigSource {
private final List<? extends ConfigSource> configSources;
private ConfigSource usedConfigSource;
private String description;
private Flow.Publisher<Optional<ObjectNode>> changesPublisher;
UseFirstAvailableConfigSource(List<? extends ConfigSource> configSources) {
this.configSources = configSources;
changesPublisher = Flow.Subscriber::onComplete;
formatDescription(false);
}
UseFirstAvailableConfigSource(ConfigSource... configSources) {
this(Arrays.asList(configSources));
}
@Override
public void init(ConfigContext context) {
configSources.forEach(source -> source.init(context));
}
@Override
public Optional<ObjectNode> load() {
Optional<ObjectNode> result = Optional.empty();
for (ConfigSource configSource : configSources) {
result = configSource.load();
if (result.isPresent()) {
usedConfigSource = configSource;
changesPublisher = usedConfigSource.changes();
break;
}
}
formatDescription(true);
return result;
}
private void formatDescription(boolean loaded) {
StringBuilder descriptionSB = new StringBuilder();
boolean availableFormatted = false;
for (ConfigSource source : configSources) {
if (descriptionSB.length() > 0) {
descriptionSB.append("->");
}
if (loaded) {
if (source == usedConfigSource) {
availableFormatted = true;
descriptionSB.append("*").append(source.description()).append("*");
} else if (availableFormatted) {
descriptionSB.append("/").append(source.description()).append("/");
} else {
descriptionSB.append("(").append(source.description()).append(")");
}
} else {
descriptionSB.append(source.description());
}
}
description = descriptionSB.toString();
}
@Override
public String description() {
return description;
}
@Override
public Flow.Publisher<Optional<ObjectNode>> changes() {
return changesPublisher;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,10 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.Objects;
import java.util.Optional;
import io.helidon.config.spi.ConfigNode.ValueNode;
@@ -38,6 +39,11 @@ public class ValueNodeImpl implements ValueNode, MergeableNode {
this.description = null;
}
@Override
public Optional<String> value() {
return Optional.of(value);
}
@Override
public String get() {
return value;
@@ -70,7 +76,6 @@ public class ValueNodeImpl implements ValueNode, MergeableNode {
public MergeableNode merge(MergeableNode node) {
switch (node.nodeType()) {
case OBJECT:
return node.merge(this);
case LIST:
return node.merge(this);
case VALUE:

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* 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,7 +14,7 @@
* limitations under the License.
*/
package io.helidon.config.internal;
package io.helidon.config;
import java.util.HashSet;
import java.util.Optional;
@@ -24,10 +24,6 @@ import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.ConfigFilters;
import io.helidon.config.MissingValueException;
import io.helidon.config.spi.ConfigFilter;
/**
@@ -61,7 +57,7 @@ import io.helidon.config.spi.ConfigFilter;
* {@code io.helidon.config.ConfigFilter} on the application's runtime classpath
* to contain this line:
* <pre>
* {@code io.helidon.config.internal.ValueResolvingFilter}</pre>
* {@code io.helidon.config.ValueResolvingFilter}</pre>
* The config system will then use the Java service loader mechanism to create and add this filter to
* every {@code Config.Builder} automatically.
* </li>

View File

@@ -1,133 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.internal;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileLock;
import java.nio.channels.NonWritableChannelException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import io.helidon.config.ConfigException;
/**
* Utilities for file-related source classes.
*
* @see io.helidon.config.FileConfigSource
* @see FileOverrideSource
* @see io.helidon.config.DirectoryConfigSource
*/
public class FileSourceHelper {
private static final Logger LOGGER = Logger.getLogger(FileSourceHelper.class.getName());
private FileSourceHelper() {
throw new AssertionError("Instantiation not allowed.");
}
/**
* Returns the last modified time of the given file or directory.
*
* @param path a file or directory
* @return the last modified time
*/
public static Instant lastModifiedTime(Path path) {
try {
return Files.getLastModifiedTime(path.toRealPath()).toInstant();
} catch (IOException e) {
LOGGER.log(Level.FINE, e, () -> "Cannot obtain the last modified time of '" + path + "'.");
}
Instant timestamp = Instant.MAX;
LOGGER.finer("Cannot obtain the last modified time. Used time '" + timestamp + "' as a content timestamp.");
return timestamp;
}
/**
* Reads the content of the specified file.
* <p>
* The file is locked before the reading and the lock is released immediately after the reading.
* <p>
* An expected encoding is UTF-8.
*
* @param path a path to the file
* @return a content of the file
*/
public static String safeReadContent(Path path) {
try (FileInputStream fis = new FileInputStream(path.toFile())) {
FileLock lock = null;
try {
lock = fis.getChannel().tryLock(0L, Long.MAX_VALUE, false);
} catch (NonWritableChannelException e) {
// non writable channel means that we do not need to lock it
}
try {
try (BufferedReader bufferedReader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
return bufferedReader.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new ConfigException(String.format("Cannot read from path '%s'", path));
}
} finally {
if (lock != null) {
lock.release();
}
}
} catch (FileNotFoundException e) {
throw new ConfigException(String.format("File '%s' not found.", path), e);
} catch (IOException e) {
throw new ConfigException(String.format("Cannot obtain a lock for file '%s'.", path), e);
}
}
/**
* Returns an MD5 digest of the specified file or null if the file cannot be read.
*
* @param path a path to the file
* @return an MD5 digest of the file or null if the file cannot be read
*/
public static byte[] digest(Path path) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new ConfigException("Cannot get MD5 algorithm.", e);
}
try {
try (InputStream is = Files.newInputStream(path);
DigestInputStream dis = new DigestInputStream(is, md)) {
byte[] buffer = new byte[4096];
while (dis.read(buffer) != -1) {
}
}
} catch (IOException e) {
LOGGER.log(Level.FINEST, "Cannot get a digest.", e);
return null;
}
return md.digest();
}
}

View File

@@ -1,268 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.internal;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.ConfigException;
import io.helidon.config.ConfigHelper;
import io.helidon.config.spi.PollingStrategy;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
/**
* This polling strategy is backed by {@link WatchService} to fire a polling event with every change on monitored {@link Path}.
* <p>
* When a parent directory of the {@code path} is not available, or becomes unavailable later, a new attempt to register {@code
* WatchService} is scheduled again and again until the directory finally exists and the registration is successful.
* <p>
* This {@link PollingStrategy} might be initialized with a custom {@link ScheduledExecutorService executor} or the {@link
* Executors#newSingleThreadScheduledExecutor()} is assigned when parameter is {@code null}.
*
* @see WatchService
*/
public class FilesystemWatchPollingStrategy implements PollingStrategy {
private static final Logger LOGGER = Logger.getLogger(FilesystemWatchPollingStrategy.class.getName());
private static final long DEFAULT_RECURRING_INTERVAL = 5;
private final Path path;
private final SubmissionPublisher<PollingEvent> ticksSubmitter;
private final Flow.Publisher<PollingEvent> ticksPublisher;
private final boolean customExecutor;
private ScheduledExecutorService executor;
private WatchService watchService;
private final List<WatchEvent.Modifier> watchServiceModifiers;
private WatchKey watchKey;
private Future<?> watchThreadFuture;
/**
* Creates a strategy with watched {@code path} as a parameters.
*
* @param path a watched file
* @param executor a custom executor or the {@link io.helidon.config.internal.ConfigThreadFactory} is assigned when
* parameter is {@code null}
*/
public FilesystemWatchPollingStrategy(Path path, ScheduledExecutorService executor) {
if (executor == null) {
this.customExecutor = false;
} else {
this.customExecutor = true;
this.executor = executor;
}
this.path = path;
ticksSubmitter = new SubmissionPublisher<>(Runnable::run, //deliver events on current thread
1); //(almost) do not buffer events
ticksPublisher = ConfigHelper.suspendablePublisher(ticksSubmitter,
this::startWatchService,
this::stopWatchService);
watchServiceModifiers = new LinkedList<>();
}
/**
* Configured path.
*
* @return configured path
*/
public Path path() {
return path;
}
@Override
public Flow.Publisher<PollingEvent> ticks() {
return ticksPublisher;
}
private void fireEvent(WatchEvent<?> watchEvent) {
ticksSubmitter().offer(
PollingEvent.now(),
(subscriber, pollingEvent) -> {
LOGGER.log(Level.FINER, String.format("Event %s has not been delivered to %s.", pollingEvent, subscriber));
return false;
});
}
/**
* Add modifiers to be used when registering the {@link WatchService}.
* See {@link Path#register(WatchService, WatchEvent.Kind[],
* WatchEvent.Modifier...) Path.register}.
*
* @param modifiers the modifiers to add
*/
public void initWatchServiceModifiers(WatchEvent.Modifier... modifiers) {
watchServiceModifiers.addAll(Arrays.asList(modifiers));
}
void startWatchService() {
if (!customExecutor) {
executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling"));
}
try {
watchService = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
throw new ConfigException("Cannot obtain WatchService.", e);
}
CountDownLatch latch = new CountDownLatch(1);
watchThreadFuture = executor.scheduleWithFixedDelay(new Monitor(path, latch, watchServiceModifiers),
0,
DEFAULT_RECURRING_INTERVAL,
TimeUnit.SECONDS);
try {
latch.await(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new ConfigException("Thread which is supposed to register to watch service exceeded the limit 1s.", e);
}
}
void stopWatchService() {
if (watchKey != null) {
watchKey.cancel();
}
if (watchThreadFuture != null) {
watchThreadFuture.cancel(true);
}
if (!customExecutor) {
ConfigUtils.shutdownExecutor(executor);
executor = null;
}
}
private class Monitor implements Runnable {
private final Path path;
private final CountDownLatch latch;
private final List<WatchEvent.Modifier> watchServiceModifiers;
private boolean fail;
private Monitor(Path path, CountDownLatch latch, List<WatchEvent.Modifier> watchServiceModifiers) {
this.path = path;
this.latch = latch;
this.watchServiceModifiers = watchServiceModifiers;
}
@Override
public void run() {
Path dir = path.getParent();
try {
register();
if (fail) {
FilesystemWatchPollingStrategy.this.fireEvent(null);
fail = false;
}
} catch (Exception e) {
fail = true;
LOGGER.log(Level.FINE, "Cannot register to watch service.", e);
return;
} finally {
latch.countDown();
}
while (true) {
WatchKey key;
try {
key = watchService.take();
} catch (Exception ie) {
fail = true;
LOGGER.log(Level.FINE, ie, () -> "Watch service on '" + dir + "' directory interrupted.");
break;
}
List<WatchEvent<?>> events = key.pollEvents();
events.stream()
.filter(e -> FilesystemWatchPollingStrategy.this.path.endsWith((Path) e.context()))
.forEach(FilesystemWatchPollingStrategy.this::fireEvent);
if (!key.reset()) {
fail = true;
LOGGER.log(Level.FINE, () -> "Directory '" + dir + "' is no more valid to be watched.");
FilesystemWatchPollingStrategy.this.fireEvent(null);
break;
}
}
}
private void register() throws IOException {
Path target = target(path);
Path dir = parentDir(target);
WatchKey oldWatchKey = watchKey;
watchKey = dir.register(watchService,
List.of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)
.toArray(new WatchEvent.Kind[0]),
watchServiceModifiers.toArray(new WatchEvent.Modifier[0]));
if (oldWatchKey != null) {
oldWatchKey.cancel();
}
}
private Path target(Path path) throws IOException {
Path target = path;
while (Files.isSymbolicLink(target)) {
target = target.toRealPath();
}
return target;
}
private Path parentDir(Path path) {
Path parent = path.getParent();
if (parent == null) {
throw new ConfigException(
String.format("Cannot find parent directory for '%s' to register watch service.", path.toString()));
}
return parent;
}
}
SubmissionPublisher<PollingEvent> ticksSubmitter() {
return ticksSubmitter;
}
Future<?> watchThreadFuture() {
return watchThreadFuture;
}
@Override
public String toString() {
return "FilesystemWatchPollingStrategy{"
+ "path=" + path
+ '}';
}
}

View File

@@ -1,129 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.internal;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.AbstractMpSource;
import io.helidon.config.spi.AbstractSource;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigSource;
/**
* {@link ConfigSource} implementation based on {@link Map Map&lt;String, String&gt;}.
* <p>
* Map key format must conform to {@link Config#key() Config key} format.
*
* @see io.helidon.config.ConfigSources.MapBuilder
*/
public class MapConfigSource extends AbstractMpSource<Instant> {
private final Map<String, String> map;
private final String mapSourceName;
private final boolean strict;
/**
* Initialize map config source.
*
* @param map config properties
* @param strict strict mode flag
* @param mapSourceName name of map source
*/
protected MapConfigSource(Map<String, String> map, boolean strict, String mapSourceName) {
super(new Builder());
Objects.requireNonNull(map, "map cannot be null");
Objects.requireNonNull(mapSourceName, "mapSourceName cannot be null");
this.map = map;
this.strict = strict;
this.mapSourceName = mapSourceName;
}
private MapConfigSource(Builder builder) {
super(builder);
this.map = new HashMap<>();
this.mapSourceName = "empty";
this.strict = true;
}
/**
* Create a new fluent API builder.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Create a new config source from the provided map, with strict mode set to {@code false}.
*
* @param map config properties
* @return a new map config source
*/
public static MapConfigSource create(Map<String, String> map) {
return create(map, false, "");
}
/**
* Create a new config source from the provided map.
*
* @param map config properties
* @param strict strict mode flag, if set to {@code true}, parsing would fail if a tree node and a leaf node conflict,
* such as for {@code http.ssl=true} and {@code http.ssl.port=1024}.
* @param mapSourceName name of map source (for debugging purposes)
* @return a new map config source
*/
public static MapConfigSource create(Map<String, String> map, boolean strict, String mapSourceName) {
return new MapConfigSource(map, strict, mapSourceName);
}
@Override
protected String uid() {
return mapSourceName.isEmpty() ? "" : mapSourceName;
}
@Override
protected Optional<Instant> dataStamp() {
return Optional.of(Instant.EPOCH);
}
@Override
protected Data<ObjectNode, Instant> loadData() throws ConfigException {
return new Data<>(Optional.of(ConfigUtils.mapToObjectNode(map, strict)), Optional.of(Instant.EPOCH));
}
/**
* A fluent API builder for {@link io.helidon.config.internal.MapConfigSource}.
*/
public static final class Builder extends AbstractSource.Builder<Builder, String, MapConfigSource> {
private Builder() {
super(String.class);
}
@Override
public MapConfigSource build() {
return new MapConfigSource(this);
}
}
}

View File

@@ -1,173 +0,0 @@
/*
* Copyright (c) 2017, 2018 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.internal;
import java.time.Duration;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.util.logging.Logger;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.RetryPolicy;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* A default retry policy implementation with {@link ScheduledExecutorService}.
* Following attributes can be configured:
* <ul>
* <li>number of retries (excluding the first invocation)</li>
* <li>delay between the invocations</li>
* <li>a delay multiplication factor</li>
* <li>a timeout for the individual invocation</li>
* <li>an overall timeout</li>
* <li>an executor service</li>
* </ul>
*/
public class RetryPolicyImpl implements RetryPolicy {
private static final Logger LOGGER = Logger.getLogger(RetryPolicyImpl.class.getName());
private final int retries;
private final Duration delay;
private final double delayFactor;
private final Duration callTimeout;
private final Duration overallTimeout;
private final ScheduledExecutorService executorService;
private volatile ScheduledFuture<?> future;
/**
* Initialize retry policy.
*
* @param retries number of retries (excluding the first invocation)
* @param delay delay between the invocations
* @param delayFactor a delay multiplication factor
* @param callTimeout a timeout for the individual invocation
* @param overallTimeout an overall timeout
* @param executorService an executor service
*/
public RetryPolicyImpl(int retries,
Duration delay,
double delayFactor,
Duration callTimeout,
Duration overallTimeout,
ScheduledExecutorService executorService) {
this.retries = retries;
this.delay = delay;
this.delayFactor = delayFactor;
this.callTimeout = callTimeout;
this.overallTimeout = overallTimeout;
this.executorService = executorService;
}
@Override
public <T> T execute(Supplier<T> call) throws ConfigException {
Duration currentDelay = Duration.ZERO;
long overallTimeoutsLeft = overallTimeout.toMillis();
Throwable last = null;
for (int i = 0; i <= retries; i++) {
try {
LOGGER.finest("next delay: " + currentDelay);
overallTimeoutsLeft -= currentDelay.toMillis();
if (overallTimeoutsLeft < 0) {
LOGGER.finest("overall timeout left [ms]: " + overallTimeoutsLeft);
throw new ConfigException(
"Cannot schedule the next call, the current delay would exceed the overall timeout.");
}
ScheduledFuture<T> localFuture = executorService.schedule(call::get, currentDelay.toMillis(), MILLISECONDS);
future = localFuture;
return localFuture.get(min(currentDelay.plus(callTimeout).toMillis(), overallTimeoutsLeft), MILLISECONDS);
} catch (ConfigException e) {
throw e;
} catch (CancellationException e) {
throw new ConfigException("An invocation has been canceled.", e);
} catch (InterruptedException e) {
throw new ConfigException("An invocation has been interrupted.", e);
} catch (TimeoutException e) {
throw new ConfigException("A timeout has been reached.", e);
} catch (Throwable t) {
last = t;
}
currentDelay = nextDelay(i, currentDelay);
}
throw new ConfigException("All repeated calls failed.", last);
}
Duration nextDelay(int invocation, Duration currentDelay) {
if (invocation == 0) {
return delay;
} else {
return Duration.ofMillis((long) (currentDelay.toMillis() * delayFactor));
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (future != null) {
if (!future.isDone() && !future.isCancelled()) {
return future.cancel(mayInterruptIfRunning);
}
}
return false;
}
/**
* Number of retries.
* @return retries
*/
public int retries() {
return retries;
}
/**
* Delay between retries.
*
* @return delay
*/
public Duration delay() {
return delay;
}
/**
* Delay multiplication factor.
* @return delay factor
*/
public double delayFactor() {
return delayFactor;
}
/**
* Timeout of the call.
* @return call timeout
*/
public Duration callTimeout() {
return callTimeout;
}
/**
* Overall timeout.
* @return overall timeout
*/
public Duration overallTimeout() {
return overallTimeout;
}
}

View File

@@ -1,256 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.spi;
import java.io.StringReader;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import io.helidon.config.Config;
import io.helidon.config.internal.ConfigKeyImpl;
import io.helidon.config.internal.ListNodeBuilderImpl;
import io.helidon.config.internal.ObjectNodeBuilderImpl;
import io.helidon.config.spi.ConfigNode.ListNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigNode.ValueNode;
import io.helidon.config.spi.ConfigParser.Content;
/**
* Base abstract implementation of {@link ConfigSource}, suitable for concrete
* implementations to extend.
*
* @param <S> a type of data stamp
* @see Builder
*/
public abstract class AbstractConfigSource<S> extends AbstractMpSource<S> {
private final Function<Config.Key, String> mediaTypeMapping;
private final Function<Config.Key, ConfigParser> parserMapping;
private ConfigContext configContext;
/**
* Initializes config source from builder.
*
* @param builder builder to be initialized from
*/
protected AbstractConfigSource(Builder<?, ?, ?> builder) {
super(builder);
mediaTypeMapping = builder.mediaTypeMapping();
parserMapping = builder.parserMapping();
}
@Override
public final void init(ConfigContext context) {
configContext = context;
super.init(context);
}
/**
* Config context associated with this source.
* @return config context
*/
protected ConfigContext configContext() {
return configContext;
}
@Override
protected Data<ObjectNode, S> processLoadedData(Data<ObjectNode, S> data) {
if (!data.data().isPresent()
|| (mediaTypeMapping == null && parserMapping == null)) {
return data;
}
Data<ObjectNode, S> result = new Data<>(Optional.of(processObject(data.stamp(), ConfigKeyImpl.of(), data.data().get())),
data.stamp());
super.processLoadedData(result);
return result;
}
private ConfigNode processNode(Optional<S> datastamp, ConfigKeyImpl key, ConfigNode node) {
switch (node.nodeType()) {
case OBJECT:
return processObject(datastamp, key, (ObjectNode) node);
case LIST:
return processList(datastamp, key, (ListNode) node);
case VALUE:
return processValue(datastamp, key, (ValueNode) node);
default:
throw new IllegalArgumentException("Unsupported node type: " + node.getClass().getName());
}
}
private ObjectNode processObject(Optional<S> datastamp, ConfigKeyImpl key, ObjectNode objectNode) {
ObjectNodeBuilderImpl builder = (ObjectNodeBuilderImpl) ObjectNode.builder();
objectNode.forEach((name, node) -> builder.addNode(name, processNode(datastamp, key.child(name), node)));
return builder.build();
}
private ListNode processList(Optional<S> datastamp, ConfigKeyImpl key, ListNode listNode) {
ListNodeBuilderImpl builder = (ListNodeBuilderImpl) ListNode.builder();
for (int i = 0; i < listNode.size(); i++) {
builder.addNode(processNode(datastamp, key.child(Integer.toString(i)), listNode.get(i)));
}
return builder.build();
}
private ConfigNode processValue(Optional<S> datastamp, Config.Key key, ValueNode valueNode) {
AtomicReference<ConfigNode> result = new AtomicReference<>(valueNode);
findParserForKey(key)
.ifPresent(parser -> result.set(parser.parse(Content.builder(new StringReader(valueNode.get()))
.stamp(datastamp)
.build())));
return result.get();
}
private Optional<ConfigParser> findParserForKey(Config.Key key) {
return Optional.ofNullable(parserMapping).map(mapping -> mapping.apply(key))
.or(() -> Optional.ofNullable(mediaTypeMapping).map(mapping -> mapping.apply(key))
.flatMap(mediaType -> configContext().findParser(mediaType)));
}
/**
* {@inheritDoc}
* <p>
* All subscribers are notified using specified {@link AbstractSource.Builder#changesExecutor(Executor) executor}.
*/
@Override
public final Flow.Publisher<Optional<ObjectNode>> changes() {
return changesPublisher();
}
/**
* A common {@link ConfigSource} builder ready to be extended by builder implementation related to {@link ConfigSource}
* extensions.
* <p>
* It allows to configure following properties:
* <ul>
* <li>{@code mediaTypeMapping} - a mapping of a key to a media type</li>
* <li>{@code parserMapping} - a mapping of a key to a {@link ConfigParser}</li>
* </ul>
*
* @param <B> type of Builder implementation
* @param <T> type of key source attributes (target) used to construct polling strategy from
* @param <S> Type of the source to be built
*/
public abstract static class Builder<B extends Builder<B, T, S>, T, S extends AbstractMpSource<?>>
extends AbstractSource.Builder<B, T, S>
implements io.helidon.common.Builder<S> {
private static final String MEDIA_TYPE_MAPPING_KEY = "media-type-mapping";
private Function<Config.Key, String> mediaTypeMapping;
private Function<Config.Key, ConfigParser> parserMapping;
private volatile S configSource;
/**
* Initialize builder.
*
* @param targetType target type
*/
protected Builder(Class<T> targetType) {
super(targetType);
mediaTypeMapping = null;
parserMapping = null;
}
@Override
public S get() {
if (configSource == null) {
configSource = build();
}
return configSource;
}
/**
* {@inheritDoc}
* <ul>
* <li>{@code media-type-mapping} - type {@code Map} - key to media type, see {@link #mediaTypeMapping(Function)}</li>
* </ul>
*
* @param metaConfig configuration properties used to configure a builder instance.
* @return modified builder instance
*/
@Override
public B config(Config metaConfig) {
//media-type-mapping
metaConfig.get(MEDIA_TYPE_MAPPING_KEY).detach().asMap()
.ifPresent(this::initMediaTypeMapping);
return super.config(metaConfig);
}
private void initMediaTypeMapping(Map<String, String> mediaTypeMapping) {
mediaTypeMapping(key -> mediaTypeMapping.get(key.toString()));
}
/**
* Sets a function mapping key to media type.
*
* @param mediaTypeMapping a mapping function
* @return a modified builder
*/
public B mediaTypeMapping(Function<Config.Key, String> mediaTypeMapping) {
Objects.requireNonNull(mediaTypeMapping, "mediaTypeMapping cannot be null");
this.mediaTypeMapping = mediaTypeMapping;
return thisBuilder();
}
/**
* Sets a function mapping key to a parser.
*
* @param parserMapping a mapping function
* @return a modified builder
*/
public B parserMapping(Function<Config.Key, ConfigParser> parserMapping) {
Objects.requireNonNull(parserMapping, "parserMapping cannot be null");
this.parserMapping = parserMapping;
return thisBuilder();
}
/**
* Media type mapping function.
* @return media type mapping
*/
protected Function<Config.Key, String> mediaTypeMapping() {
return mediaTypeMapping;
}
/**
* Parser mapping function.
* @return parser mapping
*/
protected Function<Config.Key, ConfigParser> parserMapping() {
return parserMapping;
}
}
}

View File

@@ -1,171 +0,0 @@
/*
* 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.spi;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.microprofile.config.spi.ConfigSource;
/**
* MP Config source basis. Just extend this class to be both Helidon config source and an MP config source.
* @param <S> Type of the stamp of this config source
*/
public abstract class AbstractMpSource<S> extends AbstractSource<ConfigNode.ObjectNode, S> implements ConfigSource, io.helidon.config.spi.ConfigSource {
private final AtomicReference<Map<String, String>> currentValues = new AtomicReference<>();
/**
* Initializes config source from builder.
*
* @param builder builder to be initialized from
*/
protected AbstractMpSource(Builder<?, ?, ?> builder) {
super(builder);
}
@Override
protected Data<ConfigNode.ObjectNode, S> processLoadedData(Data<ConfigNode.ObjectNode, S> data) {
currentValues.set(loadMap(data.data()));
return super.processLoadedData(data);
}
@Override
public void init(ConfigContext context) {
this.changes().subscribe(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Optional<ConfigNode.ObjectNode> item) {
currentValues.set(loadMap(item));
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
}
@Override
public Map<String, String> getProperties() {
if (null == currentValues.get()) {
currentValues.set(loadMap(load()));
}
return currentValues.get();
}
@Override
public String getValue(String propertyName) {
if (null == currentValues.get()) {
currentValues.set(loadMap(load()));
}
return currentValues.get().get(propertyName);
}
@Override
public String getName() {
return description();
}
private static Map<String, String> loadMap(Optional<ConfigNode.ObjectNode> item) {
if (item.isPresent()) {
ConfigNode.ObjectNode node = item.get();
Map<String, String> values = new TreeMap<>();
processNode(values, "", node);
return values;
} else {
return Map.of();
}
}
private static void processNode(Map<String, String> values, String keyPrefix, ConfigNode.ObjectNode node) {
node.forEach((key, configNode) -> {
switch (configNode.nodeType()) {
case OBJECT:
processNode(values, key(keyPrefix, key), (ConfigNode.ObjectNode) configNode);
break;
case LIST:
processNode(values, key(keyPrefix, key), ((ConfigNode.ListNode) configNode));
break;
case VALUE:
break;
default:
throw new IllegalStateException("Config node of type: " + configNode.nodeType() + " not supported");
}
String directValue = configNode.get();
if (null != directValue) {
values.put(key(keyPrefix, key), directValue);
}
});
}
private static void processNode(Map<String, String> values, String keyPrefix, ConfigNode.ListNode node) {
List<String> directValue = new LinkedList<>();
Map<String, String> thisListValues = new HashMap<>();
boolean hasDirectValue = true;
for (int i = 0; i < node.size(); i++) {
ConfigNode configNode = node.get(i);
String nextKey = key(keyPrefix, String.valueOf(i));
switch (configNode.nodeType()) {
case OBJECT:
processNode(thisListValues, nextKey, (ConfigNode.ObjectNode) configNode);
hasDirectValue = false;
break;
case LIST:
processNode(thisListValues, nextKey, (ConfigNode.ListNode) configNode);
hasDirectValue = false;
break;
case VALUE:
String value = configNode.get();
directValue.add(value);
thisListValues.put(nextKey, value);
break;
default:
throw new IllegalStateException("Config node of type: " + configNode.nodeType() + " not supported");
}
}
if (hasDirectValue) {
values.put(keyPrefix, String.join(",", directValue));
} else {
values.putAll(thisListValues);
}
}
private static String key(String keyPrefix, String key) {
if (keyPrefix.isEmpty()) {
return key;
}
return keyPrefix + "." + key;
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.spi;
import java.util.Optional;
import java.util.concurrent.Flow;
/**
* Base abstract implementation of {@link OverrideSource}, suitable for concrete
* implementations to extend.
*
* @param <S> a type of data stamp
* @see Builder
*/
public abstract class AbstractOverrideSource<S> extends AbstractSource<OverrideSource.OverrideData, S> implements OverrideSource {
/**
* Initializes config source from builder.
*
* @param builder builder to be initialized from
*/
protected AbstractOverrideSource(Builder<?, ?> builder) {
super(builder);
}
@Override
public final Flow.Publisher<Optional<OverrideData>> changes() {
return changesPublisher();
}
/**
* A common {@link OverrideSource} builder ready to be extended by builder implementation related to {@link OverrideSource}
* extensions.
* <p>
*
* @param <B> type of Builder implementation
* @param <T> type of key source attributes (target) used to construct polling strategy from
*/
public abstract static class Builder<B extends Builder<B, T>, T>
extends AbstractSource.Builder<B, T, OverrideSource>
implements io.helidon.common.Builder<OverrideSource> {
private volatile OverrideSource overrideSource;
/**
* Initialize builder.
*
* @param targetType target type
*/
protected Builder(Class<T> targetType) {
super(targetType);
}
@Override
public OverrideSource get() {
if (overrideSource == null) {
overrideSource = build();
}
return overrideSource;
}
}
}

View File

@@ -1,210 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.spi;
import java.util.Optional;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.spi.ConfigNode.ObjectNode;
/**
* Abstract implementation of {@link ConfigSource} that uses a
* {@link ConfigParser} to parse
* {@link ConfigParser.Content configuration content} accessible as a
* {@link ConfigParser.Content#asReadable() Readable}.
* <p>
* Typically concrete implementations will extend this class in order to
* delegate to {@link ConfigParser} the loading of configuration content into an
* {@link ObjectNode} representing the hierarchical structure of the configuration.
*
* @param <S> a type of data stamp
* @see Builder
*/
public abstract class AbstractParsableConfigSource<S> extends AbstractConfigSource<S> {
private final Optional<String> mediaType;
private final Optional<ConfigParser> parser;
/**
* Initializes config source from builder.
*
* @param builder builder to be initialized from
*/
protected AbstractParsableConfigSource(AbstractParsableConfigSource.Builder<?, ?, ?> builder) {
super(builder);
// using Optional as field types, as we already get them as optional
mediaType = builder.mediaType();
parser = builder.parser();
}
@Override
protected Data<ObjectNode, S> loadData() {
ConfigParser.Content<S> content = content();
ObjectNode objectNode = parse(configContext(), content);
return new Data<>(Optional.of(objectNode), content.stamp());
}
/**
* Returns source associated media type or {@code null} if unknown.
*
* @return source associated media type or {@code null} if unknown.
*/
protected Optional<String> mediaType() {
return mediaType;
}
/**
* Returns source associated parser or {@code empty} if unknown.
*
* @return source associated parser or {@code empty} if unknown.
*/
protected Optional<ConfigParser> parser() {
return parser;
}
/**
* Returns config source content.
*
* @return config source content. Never returns {@code null}.
* @throws ConfigException in case of loading of configuration from config source failed.
*/
protected abstract ConfigParser.Content<S> content() throws ConfigException;
/**
* Parser config source content into internal config structure.
*
* @param context config context built by {@link io.helidon.config.Config.Builder}
* @param content content to be parsed
* @return parsed configuration into internal structure. Never returns {@code null}.
* @throws ConfigParserException in case of problem to parse configuration from the source
*/
private ObjectNode parse(ConfigContext context, ConfigParser.Content<S> content) throws ConfigParserException {
return parser()
.or(() -> context.findParser(content.mediaType()
.orElseThrow(() -> new ConfigException("Unknown media type."))))
.map(parser -> parser.parse(content))
.orElseThrow(() -> new ConfigException("Cannot find suitable parser for '"
+ content.mediaType().orElse(null) + "' media type."));
}
/**
* Common {@link AbstractParsableConfigSource} Builder, suitable for
* concrete implementations of Builder that are related to
* {@code ConfigSource}s which extend {@link AbstractParsableConfigSource}
* <p>
* The application can control the following behavior:
* <ul>
* <li>{@code mandatory} - whether the configuration source must exist (default: {@code true})
* <li>{@code media-type} - configuration content media type to be used to look for appropriate {@link ConfigParser};</li>
* <li>{@code parser} - the {@link ConfigParser} to be used to parse the source</li>
* <li>changes {@code executor} and subscriber's {@code buffer size} - behavior related to
* {@link AbstractParsableConfigSource#changes()} support</li>
* </ul>
* <p>
* If the {@link ConfigSource} is {@code mandatory} and a source does not exist
* then {@link ConfigSource#load} throws a {@link ConfigException}.
* <p>
* If the application does not explicit set {@code media-type} the
* {@code Builder} tries to infer it from the source, for example from the
* source URI.
*
* @param <B> type of Builder implementation
* @param <T> type of key source attributes (target) used to construct polling strategy from
* @param <S> type of the config source to be built
*/
public abstract static class Builder<B extends Builder<B, T, S>, T, S extends AbstractMpSource<?>>
extends AbstractConfigSource.Builder<B, T, S> {
private static final String MEDIA_TYPE_KEY = "media-type";
private String mediaType;
private ConfigParser parser;
/**
* Initialize builder.
*
* @param targetType target type
*/
protected Builder(Class<T> targetType) {
super(targetType);
}
/**
* {@inheritDoc}
* <ul>
* <li>{@code media-type} - type {@code String}, see {@link #mediaType(String)}</li>
* </ul>
*
* @param metaConfig configuration properties used to configure a builder instance.
* @return modified builder instance
*/
@Override
public B config(Config metaConfig) {
//media-type
metaConfig.get(MEDIA_TYPE_KEY)
.asString()
.ifPresent(this::mediaType);
return super.config(metaConfig);
}
/**
* Sets configuration content media type.
*
* @param mediaType a configuration content media type
* @return modified builder instance
*/
public B mediaType(String mediaType) {
this.mediaType = mediaType;
return thisBuilder();
}
/**
* Sets a {@link ConfigParser} instance to be used to parse configuration content.
* <p>
* If the parser is set, the {@link #mediaType(String) media type} property is ignored.
*
* @param parser parsed used to parse configuration content
* @return modified builder instance
*/
public B parser(ConfigParser parser) {
this.parser = parser;
return thisBuilder();
}
/**
* Returns media type property.
*
* @return media type property.
*/
protected Optional<String> mediaType() {
return Optional.ofNullable(mediaType);
}
/**
* Returns parser property.
*
* @return parser property.
*/
protected Optional<ConfigParser> parser() {
return Optional.ofNullable(parser);
}
}
}

View File

@@ -1,730 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.spi;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.config.Config;
import io.helidon.config.ConfigException;
import io.helidon.config.ConfigHelper;
import io.helidon.config.MetaConfig;
import io.helidon.config.PollingStrategies;
import io.helidon.config.RetryPolicies;
import io.helidon.config.internal.ConfigThreadFactory;
/**
* Abstract base implementation for a variety of sources.
* <p>
* The inner {@link Builder} class is ready-to-extend with built-in support of
* changes (polling strategy, executor, buffer size) and mandatory/optional
* attribute.
*
* @param <T> a type of source data
* @param <S> a type of data stamp
*/
public abstract class AbstractSource<T, S> implements Source<T> {
private static final Logger LOGGER = Logger.getLogger(AbstractSource.class.getName());
private final boolean mandatory;
private final PollingStrategy pollingStrategy;
private final Executor changesExecutor;
private final RetryPolicy retryPolicy;
private final SubmissionPublisher<Optional<T>> changesSubmitter;
private final Flow.Publisher<Optional<T>> changesPublisher;
private Optional<Data<T, S>> lastData;
private PollingEventSubscriber pollingEventSubscriber;
AbstractSource(Builder<?, ?, ?> builder) {
mandatory = builder.isMandatory();
pollingStrategy = builder.pollingStrategy();
changesExecutor = builder.changesExecutor();
retryPolicy = builder.retryPolicy();
changesSubmitter = new SubmissionPublisher<>(changesExecutor, builder.changesMaxBuffer());
changesPublisher = ConfigHelper.suspendablePublisher(changesSubmitter,
this::subscribePollingStrategy,
this::cancelPollingStrategy);
lastData = Optional.empty();
}
/**
* Reloads the source and fires an event if the data was changed.
*/
void reload() {
LOGGER.log(Level.FINEST, "reload");
boolean hasChanged = false;
// find new data
Optional<Data<T, S>> newData = loadDataChangedSinceLastLoad();
if (newData.isPresent()) { // something has changed
Optional<T> newObjectNode = newData.get().data();
if (lastData.isPresent()) {
Optional<T> lastObjectNode = lastData.get().data();
hasChanged = hasChanged(lastObjectNode, newObjectNode);
} else {
hasChanged = true;
}
lastData = newData;
}
// fire event
if (hasChanged) {
fireChangeEvent();
} else {
LOGGER.log(Level.FINE, String.format("Source data %s has not changed.", description()));
}
}
SubmissionPublisher<Optional<T>> changesSubmitter() {
return changesSubmitter;
}
void subscribePollingStrategy() {
pollingEventSubscriber = new PollingEventSubscriber();
pollingStrategy.ticks().subscribe(pollingEventSubscriber);
}
/**
* Returns universal id of source to be used to construct {@link #description()}.
*
* @return universal id of source
*/
protected String uid() {
return "";
}
void cancelPollingStrategy() {
pollingEventSubscriber.cancelSubscription();
pollingEventSubscriber = null;
}
/**
* Publisher of changes of this source.
* @return publisher of source data
*/
protected Flow.Publisher<Optional<T>> changesPublisher() {
return changesPublisher;
}
PollingStrategy pollingStrategy() {
return pollingStrategy;
}
protected boolean isMandatory() {
return mandatory;
}
/**
* Fires a change event when source has changed.
*/
protected void fireChangeEvent() {
changesSubmitter.offer(lastData.flatMap(Data::data),
(subscriber, event) -> {
LOGGER.log(Level.FINER,
String.format("Event %s has not been delivered to %s.", event, subscriber));
return false;
});
}
/**
* Performs any postprocessing of config data after loading.
* By default, the method simply returns the provided input {@code Data}.
*
* @param data an input data
* @return a post-processed data
*/
protected Data<T, S> processLoadedData(Data<T, S> data) {
return data;
}
/**
* Returns current stamp of data in config source.
*
* @return current datastamp of data in config source
*/
protected abstract Optional<S> dataStamp();
Optional<Data<T, S>> lastData() {
return lastData;
}
/**
* Loads data from source when {@code data} expires.
*
* @return the last loaded data
*/
@Override
public final Optional<T> load() {
Optional<Data<T, S>> loadedData = loadDataChangedSinceLastLoad();
if (loadedData.isPresent()) {
lastData = loadedData;
}
if (lastData.isPresent()) {
return lastData.get().data();
} else {
return Optional.empty();
}
}
Optional<Data<T, S>> loadDataChangedSinceLastLoad() {
Optional<S> lastDatastamp = lastData.flatMap(Data::stamp);
Optional<S> dataStamp = dataStamp();
if (!lastData.isPresent() || !dataStamp.equals(lastData.get().stamp())) {
LOGGER.log(Level.FINE,
String.format("Source %s has changed to %s from %s.", description(), dataStamp,
lastDatastamp));
try {
Data<T, S> data = retryPolicy.execute(this::loadData);
if (!data.stamp().equals(lastDatastamp)) {
LOGGER.log(Level.FINE,
String.format("Source %s has changed to %s from %s.", description(), dataStamp,
lastDatastamp));
return Optional.of(processLoadedData(data));
} else {
LOGGER.log(Level.FINE,
String.format("Config data %s has not changed, last stamp was %s.", description(), lastDatastamp));
}
} catch (ConfigException ex) {
processMissingData(ex);
if (lastData.isPresent() && lastData.get().data().isPresent()) {
LOGGER.log(Level.FINE,
String.format("Config data %s has has been removed.", description()));
return Optional.of(new Data<>(Optional.empty(), Optional.empty()));
}
}
}
return Optional.empty();
}
/**
* Loads new data from config source.
*
* @return newly loaded data with appropriate data timestamp used for future method calls
* @throws ConfigException in case it is not possible to load configuration data
*/
protected abstract Data<T, S> loadData() throws ConfigException;
/**
* An action is proceeded when an attempt to load data failed.
* <p>
* Method logs at an appropriate level and possibly throw a wrapped exception.
*
* @param cause an original exception
*/
private void processMissingData(ConfigException cause) {
if (isMandatory()) {
String message = String.format("Cannot load data from mandatory source %s.", description());
if (cause == null) {
throw new ConfigException(message);
} else {
throw new ConfigException(message + " " + cause.getLocalizedMessage(), cause);
}
} else {
String message = String.format("Cannot load data from optional source %s."
+ " Will not be used to load from.",
description());
if (cause == null) {
LOGGER.log(Level.CONFIG, message);
} else {
if (cause instanceof ConfigParserException) {
LOGGER.log(Level.WARNING, message + " " + cause.getLocalizedMessage());
} else {
LOGGER.log(Level.CONFIG, message + " " + cause.getLocalizedMessage());
}
LOGGER.log(Level.FINEST,
String.format("Load of '%s' source failed with an exception.",
description()),
cause);
}
}
}
boolean hasChanged(Optional<T> lastObject, Optional<T> newObject) {
if (lastObject.isPresent()) {
if (newObject.isPresent()) {
//last DATA & new DATA => CHANGED = COMPARE DATA
if (!lastObject.get().equals(newObject.get())) {
return true;
}
} else {
//last DATA & new NO_DATA => CHANGED = TRUE
return true;
}
} else {
if (newObject.isPresent()) {
//last NO_DATA & new DATA => CHANGED = TRUE
return true;
}
}
return false;
}
@Override
public final String description() {
return formatDescription(uid());
}
/**
* Formats config source description.
*
* @param uid description of key parameters to be used in description.
* @return config source description
*/
String formatDescription(String uid) {
return Source.super.description() + "[" + uid + "]" + (isMandatory() ? "" : "?")
+ (pollingStrategy().equals(PollingStrategies.nop()) ? "" : "*");
}
/**
* A common {@link AbstractSource} builder, suitable for concrete {@code Builder} implementations
* related to {@link AbstractSource} extensions to extend.
* <p>
* The application can control this behavior:
* <ul>
* <li>{@code mandatory} - whether the resource is mandatory (by default) or
* optional</li>
* <li>{@code pollingStrategy} - which source reload policy to use</li>
* <li>changes {@code executor} and subscriber's {@code buffer size} -
* related to propagating source changes</li>
* </ul>
*
* @param <B> type of Builder implementation
* @param <T> type of key source attributes (target) used to construct
* polling strategy from
* @param <S> type of source that should be built
*/
public abstract static class Builder<B extends Builder<B, T, S>, T, S extends Source<?>>
implements io.helidon.common.Builder<S> {
/**
* Default executor where the changes threads are run.
*/
static final Executor DEFAULT_CHANGES_EXECUTOR = Executors.newCachedThreadPool(new ConfigThreadFactory("source"));
private static final String OPTIONAL_KEY = "optional";
private static final String POLLING_STRATEGY_KEY = "polling-strategy";
private static final String RETRY_POLICY_KEY = "retry-policy";
private final B thisBuilder;
private final Class<T> targetType;
private boolean mandatory;
private Supplier<PollingStrategy> pollingStrategySupplier;
private Executor changesExecutor;
private int changesMaxBuffer;
private Supplier<RetryPolicy> retryPolicySupplier;
/**
* Initializes builder.
*
* @param targetType target type
*/
protected Builder(Class<T> targetType) {
this.targetType = targetType;
thisBuilder = (B) this;
mandatory = true;
pollingStrategySupplier = PollingStrategies::nop;
changesExecutor = DEFAULT_CHANGES_EXECUTOR;
changesMaxBuffer = Flow.defaultBufferSize();
retryPolicySupplier = RetryPolicies::justCall;
}
/**
* Returns current builder instance.
*
* @return builder instance
*/
protected B thisBuilder() {
return thisBuilder;
}
/**
* Configure this builder from an existing configuration (we use the term meta configuration for this
* type of configuration, as it is a configuration that builds configuration).
* <p>
* Supported configuration {@code properties}:
* <ul>
* <li>{@code optional} - type {@code boolean}, see {@link #optional()}</li>
* <li>{@code polling-strategy} - see {@link PollingStrategy} for details about configuration format,
* see {@link #pollingStrategy(Supplier)} or {@link #pollingStrategy(Function)}</li>
* <li>{@code retry-policy} - see {@link io.helidon.config.spi.RetryPolicy} for details about
* configuration format</li>
* </ul>
*
*
* @param metaConfig configuration to configure this source
* @return modified builder instance
*/
@SuppressWarnings("unchecked")
public B config(Config metaConfig) {
//optional / mandatory
metaConfig.get(OPTIONAL_KEY)
.asBoolean()
.ifPresent(this::optional);
//polling-strategy
metaConfig.get(POLLING_STRATEGY_KEY)
.ifExists(cfg -> pollingStrategy((t -> MetaConfig.pollingStrategy(cfg).apply(t))));
//retry-policy
metaConfig.get(RETRY_POLICY_KEY)
.ifExists(cfg -> retryPolicy(MetaConfig.retryPolicy(cfg)));
return thisBuilder;
}
/**
* Sets a polling strategy.
*
* @param pollingStrategySupplier a polling strategy
* @return a modified builder instance
* @see PollingStrategies#regular(java.time.Duration)
*/
public B pollingStrategy(Supplier<PollingStrategy> pollingStrategySupplier) {
Objects.requireNonNull(pollingStrategySupplier, "pollingStrategy cannot be null");
this.pollingStrategySupplier = pollingStrategySupplier;
return thisBuilder;
}
/**
* Sets the polling strategy that accepts key source attributes.
* <p>
* Concrete subclasses should override {@link #target()} to provide
* the key source attributes (target). For example, the {@code Builder}
* for a {@code FileConfigSource} or {@code ClasspathConfigSource} uses
* the {@code Path} to the corresponding file or resource as the key
* source attribute (target), while the {@code Builder} for a
* {@code UrlConfigSource} uses the {@code URL}.
*
* @param pollingStrategyProvider a polling strategy provider
* @return a modified builder instance
* @throws UnsupportedOperationException if the concrete {@code Builder}
* implementation does not support the polling strategy
* @see #pollingStrategy(Supplier)
* @see #target()
*/
public final B pollingStrategy(Function<T, Supplier<PollingStrategy>> pollingStrategyProvider) {
pollingStrategy(() -> pollingStrategyProvider.apply(target()).get());
return thisBuilder;
}
/**
* Returns key source attributes (target).
*
* @return key source attributes (target).
*/
protected T target() {
return null;
}
/**
* Type of target used by this builder.
*
* @return target type, used by {@link #pollingStrategy(java.util.function.Function)}
*/
public Class<T> targetType() {
return targetType;
}
/**
* Built {@link ConfigSource} will not be mandatory, i.e. it is ignored if configuration target does not exists.
*
* @return a modified builder instance
*/
public B optional() {
this.mandatory = false;
return thisBuilder;
}
/**
* Built {@link ConfigSource} will be optional ({@code true}) or mandatory ({@code false}).
*
* @param optional set to {@code true} to mark this source optional.
* @return a modified builder instance
*/
public B optional(boolean optional) {
this.mandatory = !optional;
return thisBuilder;
}
/**
* Specifies "observe-on" {@link Executor} to be used to deliver
* {@link ConfigSource#changes() config source changes}. The same
* executor is also used to reload the source, as triggered by the
* {@link PollingStrategy#ticks() polling strategy event}.
* <p>
* The default executor is from a dedicated thread pool which reuses
* threads as possible.
*
* @param changesExecutor the executor to use for async delivery of
* {@link ConfigSource#changes()} events
* @return a modified builder instance
* @see #changesMaxBuffer(int)
* @see ConfigSource#changes()
* @see PollingStrategy#ticks()
*/
public B changesExecutor(Executor changesExecutor) {
Objects.requireNonNull(changesExecutor, "changesExecutor cannot be null");
this.changesExecutor = changesExecutor;
return thisBuilder;
}
/**
* Specifies maximum capacity for each subscriber's buffer to be used to deliver
* {@link ConfigSource#changes() config source changes}.
* <p>
* By default {@link Flow#defaultBufferSize()} is used.
* <p>
* Note: Not consumed events will be dropped off.
*
* @param changesMaxBuffer the maximum capacity for each subscriber's buffer of {@link ConfigSource#changes()} events.
* @return a modified builder instance
* @see #changesExecutor(Executor)
* @see ConfigSource#changes()
*/
public B changesMaxBuffer(int changesMaxBuffer) {
this.changesMaxBuffer = changesMaxBuffer;
return thisBuilder;
}
/**
* Sets a supplier of {@link RetryPolicy} that will be responsible for invocation of {@link AbstractSource#load()}.
* <p>
* The default reply policy is {@link RetryPolicies#justCall()}.
* <p>
* Create a custom policy or use the built-in policy constructed with a {@link RetryPolicies#repeat(int) builder}.
*
* @param retryPolicySupplier a execute policy supplier
* @return a modified builder instance
*/
public B retryPolicy(Supplier<RetryPolicy> retryPolicySupplier) {
this.retryPolicySupplier = retryPolicySupplier;
return thisBuilder;
}
/**
* Set a {@link RetryPolicy} that will be responsible for invocation of {@link AbstractSource#load()}.
* <p>
* The default reply policy is {@link RetryPolicies#justCall()}.
* <p>
* Create a custom policy or use the built-in policy constructed with a {@link RetryPolicies#repeat(int) builder}.
*
* @param retryPolicy retry policy
* @return a modified builder instance
*/
public B retryPolicy(RetryPolicy retryPolicy) {
return retryPolicy(() -> retryPolicy);
}
/**
* Builds new instance of {@code S}.
*
* @return new instance of {@code S}.
*/
public abstract S build();
/**
* Returns mandatory property.
*
* @return mandatory property.
*/
protected boolean isMandatory() {
return mandatory;
}
/**
* Returns polling-strategy property.
*
* @return polling-strategy property.
*/
protected PollingStrategy pollingStrategy() {
PollingStrategy pollingStrategy = pollingStrategySupplier.get();
Objects.requireNonNull(pollingStrategy, "pollingStrategy cannot be null");
return pollingStrategy;
}
/**
* Returns changes-executor property.
*
* @return changes-executor property.
*/
protected Executor changesExecutor() {
return changesExecutor;
}
/**
* Returns changes-max-buffer property.
*
* @return changes-max-buffer property.
*/
protected int changesMaxBuffer() {
return changesMaxBuffer;
}
/**
* Retry policy configured in this builder.
* @return retry policy
*/
protected RetryPolicy retryPolicy() {
return retryPolicySupplier.get();
}
}
/**
* Data loaded at appropriate time.
*
* @param <D> an type of loaded data
* @param <S> a type of data stamp
*/
public static final class Data<D, S> {
private final Optional<D> data;
private final Optional<S> stamp;
/**
* Initialize data object for specified timestamp and covered data.
*/
public Data() {
this.stamp = Optional.empty();
this.data = Optional.empty();
}
/**
* Initialize data object for specified timestamp and covered data.
*
* @param data covered object node. Can be {@code null} in case source does not exist.
* @param stamp data stamp
*/
public Data(Optional<D> data, Optional<S> stamp) {
Objects.requireNonNull(data);
Objects.requireNonNull(stamp);
this.stamp = stamp;
this.data = data;
}
/**
* Returns loaded data.
*
* @return loaded data.
*/
public Optional<D> data() {
return data;
}
/**
* Returns stamp of data.
*
* @return stamp of data.
*/
public Optional<S> stamp() {
return stamp;
}
}
/**
* {@link Flow.Subscriber} on {@link PollingStrategy#ticks() polling strategy} to listen on {@link
* PollingStrategy.PollingEvent}.
*/
private class PollingEventSubscriber implements Flow.Subscriber<PollingStrategy.PollingEvent> {
private Flow.Subscription subscription;
private volatile boolean reloadLogged = false;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(PollingStrategy.PollingEvent item) {
AbstractSource.this.changesExecutor.execute(this::safeReload);
}
private void safeReload() {
try {
AbstractSource.this.reload();
if (reloadLogged) {
String message = String.format("Reload of override source [%s] succeeded again. Polling will continue.",
description());
LOGGER.log(isMandatory() ? Level.WARNING : Level.CONFIG, message);
reloadLogged = false;
}
} catch (Exception ex) {
if (!reloadLogged) {
String message = String.format("Reload of override source [%s] failed. Polling will continue. %s",
description(),
ex.getLocalizedMessage());
LOGGER.log(isMandatory() ? Level.WARNING : Level.CONFIG, message);
LOGGER.log(Level.CONFIG,
String.format("Reload of '%s' override source failed with an exception.",
description()),
ex);
reloadLogged = true;
}
} finally {
subscription.request(1);
}
}
@Override
public void onError(Throwable throwable) {
AbstractSource.this.changesSubmitter
.closeExceptionally(new ConfigException(
String.format("Polling strategy '%s' has failed. Polling of '%s' source will not continue. %s",
pollingStrategy,
description(),
throwable.getLocalizedMessage()
),
throwable));
}
@Override
public void onComplete() {
LOGGER.fine(String.format("Polling strategy '%s' has completed. Polling of '%s' source will not continue.",
pollingStrategy,
description()));
AbstractSource.this.changesSubmitter.close();
}
private void cancelSubscription() {
if (subscription != null) {
subscription.cancel();
}
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 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.spi;
/**
* Type of changes that can happen in a {@link io.helidon.config.spi.PollingStrategy.Polled}
* components.
* The {@link PollingStrategy} may use this information to modify its
* behavior.
*/
public enum ChangeEventType {
/**
* Nothing is changed.
*/
UNCHANGED,
/**
* The content is modified.
*/
CHANGED,
/**
* The content is not present.
*/
DELETED,
/**
* The content is now present (was deleted).
*/
CREATED
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 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.spi;
import java.time.Instant;
import java.util.function.Consumer;
/**
* Similar to a {@link io.helidon.config.spi.PollingStrategy} a change watcher is used to
* identify a change and trigger reload of a {@link io.helidon.config.spi.ConfigSource}.
* Where a polling strategy provides polling events to check for changes, change watcher
* is capable of identifying a change in the underlying target using other means.
*
* @param <T> target of this change watcher, such as {@link java.nio.file.Path}
*/
public interface ChangeWatcher<T> {
/**
* Start watching a target for changes.
* If a change happens, notify the listener.
*
* @param target target of this watcher, such as {@link java.nio.file.Path}
* @param listener listener that handles reloading of the resource being watched
*/
void start(T target, Consumer<ChangeEvent<T>> listener);
/**
* Stop watching all targets for changes.
*/
default void stop() {
}
/**
* Target supported by this change watcher.
*
* @return type supported
*/
Class<T> type();
/**
* A change event, carrying the target, type of change and time of change.
*
* @param <T> type of target
*/
interface ChangeEvent<T> {
/**
* Time of change, or as close to that time as we can get.
* @return instant of the change
*/
Instant changeTime();
/**
* Target of the change.
* This may be the same as the target of {@link io.helidon.config.spi.ChangeWatcher},
* though this may also be a different target.
* In case of {@link java.nio.file.Path}, the change watcher may watch a directory,
* yet the change event notifies about a single file within that directory.
*
* @return target that is changed
*/
T target();
/**
* Type of change if available. If no details can be found (e.g. we do not know if
* the target was deleted, created or modified, use
* {@link io.helidon.config.spi.ChangeEventType#CHANGED}.
*
* @return type of change
*/
ChangeEventType type();
static <T> ChangeEvent<T> create(T target, ChangeEventType type, Instant instant) {
return new ChangeEvent<>() {
@Override
public Instant changeTime() {
return instant;
}
@Override
public T target() {
return target;
}
@Override
public ChangeEventType type() {
return type;
}
@Override
public String toString() {
return type + " " + target;
}
};
}
static <T> ChangeEvent<T> create(T target, ChangeEventType type) {
return create(target, type, Instant.now());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 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.
@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.spi;
/**
* Internal classes, not public API.
* Java service loader service to create a polling strategy factory based on meta configuration.
*/
package io.helidon.config.internal;
public interface ChangeWatcherProvider extends MetaConfigurableProvider<ChangeWatcher> {
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.config.spi;
import java.util.Optional;
import java.util.concurrent.Flow;
/**
* A changeable component is a component that may identify a change
* at an arbitrary point in time and that can notify listeners of such a change.
*
* @param <T> a type of source
*/
public interface Changeable<T> { //TODO later to be extended just by selected sources
/**
* Returns a {@code Flow.Publisher} to which the caller can subscribe in
* order to receive change notifications.
* <p>
* Method {@link Flow.Subscriber#onError(Throwable)} is called in case of error listening on config source data.
* Method {@link Flow.Subscriber#onComplete()} is never called.
*
* @return a publisher of events. Never returns {@code null}
*/
@Deprecated
Flow.Publisher<Optional<T>> changes();
}

View File

@@ -0,0 +1,172 @@
/*
* 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.spi;
import java.util.Optional;
/**
* Config content as provided by a config source that can read all its data at once (an "eager" config source).
* This interface provides necessary support for changes of the underlying source and for parsable content.
* <p>
* Content can either provide a {@link io.helidon.config.spi.ConfigNode.ObjectNode} or an array of bytes to be parsed by a
* {@link io.helidon.config.spi.ConfigParser}.
* <p>
* The data stamp can be any object (to be decided by the {@link io.helidon.config.spi.ConfigSource}).
*/
public interface ConfigContent {
/**
* Close the content, as it is no longer needed.
*/
default void close() {
}
/**
* A modification stamp of the content.
* <p>
* @return a stamp of the content
*/
default Optional<Object> stamp() {
return Optional.empty();
}
/**
* A content of an {@link io.helidon.config.spi.OverrideSource}.
*/
interface OverrideContent extends ConfigContent {
/**
* A fluent API builder for {@link io.helidon.config.spi.ConfigContent.OverrideContent}.
*
* @return a new builder instance
*/
static Builder builder() {
return new Builder();
}
/**
* Data of this override source.
*
* @return the data of the underlying source as override data
*/
OverrideSource.OverrideData data();
class Builder extends ConfigContent.Builder<Builder> implements io.helidon.common.Builder<OverrideContent> {
// override data
private OverrideSource.OverrideData data;
/**
* Data of this override source.
* @param data the data of this source
* @return updated builder instance
*/
public Builder data(OverrideSource.OverrideData data) {
this.data = data;
return this;
}
OverrideSource.OverrideData data() {
return data;
}
@Override
public OverrideContent build() {
return new ContentImpl.OverrideContentImpl(this);
}
}
}
/**
* Config content that provides an {@link io.helidon.config.spi.ConfigNode.ObjectNode} directly, with no need
* for parsing.
*/
interface NodeContent extends ConfigContent {
/**
* A fluent API builder for {@link io.helidon.config.spi.ConfigContent.NodeContent}.
*
* @return a new builder instance
*/
static Builder builder() {
return new Builder();
}
/**
* Data of this config source.
* @return the data of the underlying source as an object node
*/
ConfigNode.ObjectNode data();
/**
* Fluent API builder for {@link io.helidon.config.spi.ConfigContent.NodeContent}.
*/
class Builder extends ConfigContent.Builder<Builder> implements io.helidon.common.Builder<NodeContent> {
// node based config source data
private ConfigNode.ObjectNode rootNode;
/**
* Node with the configuration of this content.
*
* @param rootNode the root node that links the configuration tree of this source
* @return updated builder instance
*/
public Builder node(ConfigNode.ObjectNode rootNode) {
this.rootNode = rootNode;
return this;
}
ConfigNode.ObjectNode node() {
return rootNode;
}
@Override
public NodeContent build() {
return new ContentImpl.NodeContentImpl(this);
}
}
}
/**
* Fluent API builder for {@link ConfigContent}, common ancestor for parsable content builder and node content builder.
*
* @param <T> type of the implementing builder
*/
class Builder<T extends Builder<T>> {
private Object stamp;
@SuppressWarnings("unchecked")
private final T me = (T) this;
Builder() {
}
/**
* Content stamp.
*
* @param stamp stamp of the content
* @return updated builder instance
*/
public T stamp(Object stamp) {
this.stamp = stamp;
return me;
}
Object stamp() {
return stamp;
}
}
}

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.
@@ -16,7 +16,7 @@
package io.helidon.config.spi;
import java.util.Optional;
import io.helidon.config.ConfigSourceRuntime;
/**
* Context created by a {@link io.helidon.config.Config.Builder} as it constructs a
@@ -26,25 +26,11 @@ import java.util.Optional;
* interfaces to share common information.
*/
public interface ConfigContext {
/**
* Returns the first appropriate {@link ConfigParser} instance that supports
* the specified
* {@link ConfigParser.Content#mediaType() content media type}.
* <p>
* Note that the application can explicitly register parsers with a builder
* by invoking the
* {@link io.helidon.config.Config.Builder#addParser(ConfigParser)} method. The
* config system also loads parsers using the Java
* {@link java.util.ServiceLoader} mechanism and automatically registers
* such loaded parsers with each {@code Builder} unless the application has
* invoked the {@link io.helidon.config.Config.Builder#disableParserServices()}
* method.
* Create or find a runtime for a config source.
*
* @param mediaType a media type for which a parser is needed
* @return {@code Optional<ConfigParser>} ({@link Optional#empty()} if no
* appropriate parser exists)
* @param source source to create runtime for
* @return a source runtime
*/
Optional<ConfigParser> findParser(String mediaType);
ConfigSourceRuntime sourceRuntime(ConfigSource source);
}

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.
@@ -18,17 +18,16 @@ package io.helidon.config.spi;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.Optional;
import io.helidon.config.internal.ConfigUtils;
import io.helidon.config.internal.ListNodeBuilderImpl;
import io.helidon.config.internal.ObjectNodeBuilderImpl;
import io.helidon.config.internal.ValueNodeImpl;
import io.helidon.config.ListNodeBuilderImpl;
import io.helidon.config.ObjectNodeBuilderImpl;
import io.helidon.config.ValueNodeImpl;
/**
* Marker interface identifying a config node implementation.
*/
public interface ConfigNode extends Supplier<String> {
public interface ConfigNode {
/**
* Get the type of this node.
*
@@ -36,6 +35,13 @@ public interface ConfigNode extends Supplier<String> {
*/
NodeType nodeType();
/**
* Get the direct value of this config node. Any node type can have a direct value.
*
* @return a value if present, {@code empty} otherwise
*/
Optional<String> value();
/**
* Base types of config nodes.
*/
@@ -65,6 +71,12 @@ public interface ConfigNode extends Supplier<String> {
return NodeType.VALUE;
}
/**
* Get the value of this value node.
* @return string with the node value
*/
String get();
/**
* Create new instance of the {@link ValueNode} from specified String {@code value}.
*
@@ -171,7 +183,7 @@ public interface ConfigNode extends Supplier<String> {
* @return empty object node.
*/
static ObjectNode empty() {
return ConfigUtils.EmptyObjectNodeHolder.EMPTY;
return SpiHelper.EmptyObjectNodeHolder.EMPTY;
}
/**
@@ -193,7 +205,7 @@ public interface ConfigNode extends Supplier<String> {
* @return new instance of {@link Builder}.
*/
static Builder builder() {
return new ObjectNodeBuilderImpl();
return ObjectNodeBuilderImpl.create();
}
/**
@@ -254,6 +266,15 @@ public interface ConfigNode extends Supplier<String> {
*/
ObjectNode build();
/**
* Add a config node.
* The method will determine the type of the node and add it to builder.
*
* @param key key of the node
* @param node node to be added
* @return modified builder
*/
Builder addNode(String key, ConfigNode node);
}
}

View File

@@ -16,18 +16,18 @@
package io.helidon.config.spi;
import java.nio.file.Path;
import java.time.Instant;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import io.helidon.config.ConfigException;
import io.helidon.config.ConfigParsers;
import io.helidon.config.spi.ConfigNode.ObjectNode;
/**
* Transforms config {@link Content} into a {@link ConfigNode.ObjectNode} that
* Transforms config {@link io.helidon.config.spi.ConfigParser.Content} into a {@link ConfigNode.ObjectNode} that
* represents the original structure and values from the content.
* <p>
* The application can register parsers on a {@code Builder} using the
@@ -40,10 +40,11 @@ import io.helidon.config.spi.ConfigNode.ObjectNode;
* <p>
* A parser can specify a {@link javax.annotation.Priority}. If no priority is
* explicitly assigned, the value of {@value PRIORITY} is assumed.
* <p>
* Parser is used by the config system and a config source provides data as an input stream.
*
* @see io.helidon.config.Config.Builder#addParser(ConfigParser)
* @see ConfigSource#load()
* @see AbstractParsableConfigSource
* @see io.helidon.config.spi.ParsableSource
* @see ConfigParsers ConfigParsers - access built-in implementations.
*/
public interface ConfigParser {
@@ -56,168 +57,159 @@ public interface ConfigParser {
/**
* Returns set of supported media types by the parser.
* <p>
* Set of supported media types is used while {@link ConfigContext#findParser(String) looking for appropriate parser}
* by {@link ConfigSource} implementations.
* Set of supported media types is used when config system looks for appropriate parser based on media type
* of content.
* <p>
* {@link ConfigSource} implementations usually use {@link java.nio.file.Files#probeContentType(Path)} method
* to guess source media type, if not explicitly set.
* {@link io.helidon.config.spi.ParsableSource} implementations can use {@link io.helidon.common.media.type.MediaTypes}
* to probe for media type of content to provide it to config system through
* {@link io.helidon.config.spi.ConfigParser.Content.Builder#mediaType(String)}.
*
* @return supported media types by the parser
*/
Set<String> supportedMediaTypes();
/**
* Parses a specified {@link Content} into a {@link ObjectNode hierarchical configuration representation}.
* Parses a specified {@link ConfigContent} into a {@link ObjectNode hierarchical configuration representation}.
* <p>
* Never returns {@code null}.
*
* @param content a content to be parsed
* @param <S> a type of data stamp
* @return parsed hierarchical configuration representation
* @throws ConfigParserException in case of problem to parse configuration from the source
*/
<S> ObjectNode parse(Content<S> content) throws ConfigParserException;
ObjectNode parse(Content content) throws ConfigParserException;
/**
* {@link ConfigSource} configuration Content to be {@link ConfigParser#parse(Content) parsed} into
* {@link ObjectNode hierarchical configuration representation}.
*
* @param <S> a type of data stamp
* Config content to be parsed by a {@link ConfigParser}.
*/
interface Content<S> {
default void close() throws ConfigException {
}
interface Content extends ConfigContent {
/**
* A modification stamp of the content.
* <p>
* Default implementation returns {@link Instant#EPOCH}.
*
* @return a stamp of the content
*/
default Optional<S> stamp() {
return Optional.empty();
}
/**
* Returns configuration content media type.
* Media type of the content. This method is only called if
* there is no parser configured.
*
* @return content media type if known, {@code empty} otherwise
*/
Optional<String> mediaType();
/**
* Returns a {@link Readable} that is use to read configuration content from.
* Data of this config source.
*
* @param <T> return type that is {@link Readable} as well as {@link AutoCloseable}
* @return a content as {@link Readable}
* @return the data of the underlying source to be parsed by a {@link ConfigParser}
*/
<T extends Readable & AutoCloseable> T asReadable();
InputStream data();
/**
* Create a fluent API builder for content.
* Charset configured by the config source or {@code UTF-8} if none configured.
*
* @param readable readable to base this content builder on
* @param <S> type of the stamp to use
* @param <T> dual type of readable and autocloseable parameter
* @return a new fluent API builder for content
* @return charset to use when reading {@link #data()} if needed by the parser
*/
static <S, T extends Readable & AutoCloseable> Builder<S> builder(T readable) {
Objects.requireNonNull(readable, "Readable must not be null when creating content");
return new Builder<>(readable);
Charset charset();
/**
* A fluent API builder for {@link io.helidon.config.spi.ConfigParser.Content}.
*
* @return a new builder instance
*/
static Builder builder() {
return new Builder();
}
/**
* Creates {@link Content} from given {@link Readable readable content} and
* with specified {@code mediaType} of configuration format.
* Create content from data, media type and a stamp.
* If not all are available, construct content using {@link #builder()}
*
* @param readable a readable providing configuration.
* @param mediaType a configuration mediaType
* @param stamp content stamp
* @param <S> a type of data stamp
* @param <T> dual type of readable and autocloseable parameter
* @return a config content
* @param data input stream to underlying data
* @param mediaType content media type
* @param stamp stamp of the content
* @return content built from provided information
*/
static <S, T extends Readable & AutoCloseable> Content<S> create(T readable, String mediaType, S stamp) {
Objects.requireNonNull(mediaType, "Media type must not be null when creating content using Content.create()");
Objects.requireNonNull(stamp, "Stamp must not be null when creating content using Content.create()");
Builder<S> builder = builder(readable);
return builder
static Content create(InputStream data, String mediaType, Object stamp) {
return builder().data(data)
.mediaType(mediaType)
.stamp(stamp)
.build();
}
/**
* Fluent API builder for {@link io.helidon.config.spi.ConfigParser.Content}.
*
* @param <S> type of the stamp of the built content
* Fluent API builder for {@link Content}.
*/
class Builder<S> implements io.helidon.common.Builder<Content<S>> {
private final AutoCloseable readable;
class Builder extends ConfigContent.Builder<Builder> implements io.helidon.common.Builder<Content> {
private InputStream data;
private String mediaType;
private S stamp;
private Charset charset = StandardCharsets.UTF_8;
private <T extends Readable & AutoCloseable> Builder(T readable) {
this.readable = readable;
}
@Override
public Content<S> build() {
final Optional<String> mediaType = Optional.ofNullable(this.mediaType);
final Optional<S> stamp = Optional.ofNullable(this.stamp);
return new Content<>() {
@Override
public Optional<String> mediaType() {
return mediaType;
}
@SuppressWarnings("unchecked")
@Override
public <T extends Readable & AutoCloseable> T asReadable() {
return (T) readable;
}
@Override
public void close() throws ConfigException {
try {
readable.close();
} catch (ConfigException ex) {
throw ex;
} catch (Exception ex) {
throw new ConfigException("Error while closing readable [" + readable + "].", ex);
}
}
@Override
public Optional<S> stamp() {
return stamp;
}
};
private Builder() {
}
/**
* Content media type.
* @param mediaType type of the content
* Data of the config source as loaded from underlying storage.
*
* @param data to be parsed
* @return updated builder instance
*/
public Builder<S> mediaType(String mediaType) {
public Builder data(InputStream data) {
Objects.requireNonNull(data, "Parsable input stream must be provided");
this.data = data;
return this;
}
/**
* Media type of the content if known by the config source.
* Media type is configured on content, as sometimes you need the actual file to exist to be able to
* "guess" its media type, and this is the place we are sure it exists.
*
* @param mediaType media type of the content as understood by the config source
* @return updated builder instance
*/
public Builder mediaType(String mediaType) {
Objects.requireNonNull(mediaType, "Media type must be provided, or this method should not be called");
this.mediaType = mediaType;
return this;
}
/**
* Content stamp.
* A shortcut method to invoke with result of {@link io.helidon.common.media.type.MediaTypes#detectType(String)}
* and similar methods. Only sets media type if the parameter is present.
*
* @param stamp stamp of the content
* @param mediaType optional of media type
* @return updated builder instance
*/
public Builder<S> stamp(S stamp) {
this.stamp = stamp;
public Builder mediaType(Optional<String> mediaType) {
mediaType.ifPresent(this::mediaType);
return this;
}
/**
* Configure charset if known by the config source.
*
* @param charset charset to use if the content should be read using a reader
* @return updated builder instance
*/
public Builder charset(Charset charset) {
Objects.requireNonNull(charset, "Charset must be provided, or this method should not be called");
this.charset = charset;
return this;
}
InputStream data() {
return data;
}
String mediaType() {
return mediaType;
}
Charset charset() {
return charset;
}
@Override
public Content build() {
if (null == data) {
throw new ConfigParserException("Parsable content exists, yet input stream was not configured.");
}
return new ContentImpl.ParsableContentImpl(this);
}
}
}
}

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.
@@ -20,21 +20,49 @@ import java.util.function.Supplier;
import io.helidon.config.Config;
import io.helidon.config.ConfigSources;
import io.helidon.config.spi.ConfigNode.ObjectNode;
/**
* {@link Source} of configuration.
*
* There is a set of interfaces that you can implement to support various aspect of a config source.
* <p>
* Config sources by "eagerness" of loading of data. The config source either loads all data when asked to (and this is
* the preferred way for Helidon Config), or loads each key separately.
* <ul>
* <li>{@link io.helidon.config.spi.ParsableSource} - an eager source that provides an input stream with data to be
* parsed based on its content type</li>
* <li>{@link io.helidon.config.spi.NodeConfigSource} - an eager source that provides a
* {@link io.helidon.config.spi.ConfigNode.ObjectNode} with its configuration tree</li>
* <li>{@link io.helidon.config.spi.LazyConfigSource} - a lazy source that provides values key by key</li>
* </ul>
*
* <p>
* Config sources by "mutability" of data. The config source may be immutable (default), or provide a means for
* change support
* <ul>
* <li>{@link io.helidon.config.spi.PollableSource} - a source that can generate a "stamp" of the data that can
* be used to check for possible changes in underlying data (such as file digest, a timestamp, data version)</li>
* <li>{@link io.helidon.config.spi.WatchableSource} - a source that is based on data that have a specific change
* watcher that can notify the config framework of changes without the need for regular polling (such as file)</li>
* <li>{@link io.helidon.config.spi.EventConfigSource} - a source that can directly notify about changes</li>
* </ul>
*
* Each of the interfaces mentioned above also has an inner class with a builder interface, if any configuration is needed.
* The {@link io.helidon.config.AbstractConfigSource} implements a super set of all the configuration methods from all interfaces
* as {@code protected}, so you can use them in your implementation.
* <p>
* {@link io.helidon.config.AbstractConfigSourceBuilder} implements the configuration methods, so you can simply extend it with
* your builder and implement all the builders that make sense for your config source type.
*
*
* @see Config.Builder#sources(Supplier)
* @see Config.Builder#sources(Supplier, Supplier)
* @see Config.Builder#sources(Supplier, Supplier, Supplier)
* @see Config.Builder#sources(java.util.List)
* @see AbstractConfigSource
* @see AbstractParsableConfigSource
* @see io.helidon.config.AbstractConfigSource
* @see ConfigSources ConfigSources - access built-in implementations.
*/
@FunctionalInterface
public interface ConfigSource extends Source<ObjectNode>, Supplier<ConfigSource> {
public interface ConfigSource extends Supplier<ConfigSource>, Source {
@Override
default ConfigSource get() {
return this;

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.
@@ -15,8 +15,29 @@
*/
package io.helidon.config.spi;
import java.util.List;
import io.helidon.config.Config;
/**
* Java service loader service to provide a config source based on meta configuration.
*/
public interface ConfigSourceProvider extends MetaConfigurableProvider<ConfigSource> {
/**
* Create a list of configuration sources from a single configuration.
* <p>
* This method is called (only) when the meta configuration property {@code multi-source}
* is set to {@code true}.
* <p>
* Example: for classpath config source, we may want to read all instances of the resource
* on classpath.
*
* @param type type of the config source
* @param metaConfig meta configuration of the config source
* @return a list of config sources, at least one MUST be returned, so we can correctly validate
* optional/mandatory sources.
*/
default List<ConfigSource> createMulti(String type, Config metaConfig) {
return List.of();
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.spi;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
abstract class ContentImpl implements ConfigContent {
private static final Logger LOGGER = Logger.getLogger(ContentImpl.class.getName());
private final Object stamp;
ContentImpl(Builder<?> builder) {
this.stamp = builder.stamp();
}
@Override
public Optional<Object> stamp() {
return Optional.ofNullable(stamp);
}
static class ParsableContentImpl extends ContentImpl implements ConfigParser.Content {
private final String mediaType;
private final InputStream data;
private final Charset charset;
ParsableContentImpl(ConfigParser.Content.Builder builder) {
super(builder);
this.mediaType = builder.mediaType();
this.data = builder.data();
this.charset = builder.charset();
}
@Override
public void close() {
try {
data.close();
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to close input stream", e);
}
}
@Override
public Optional<String> mediaType() {
return Optional.ofNullable(mediaType);
}
@Override
public InputStream data() {
return data;
}
@Override
public Charset charset() {
return charset;
}
}
static class NodeContentImpl extends ContentImpl implements NodeContent {
private final ConfigNode.ObjectNode data;
NodeContentImpl(NodeContent.Builder builder) {
super(builder);
this.data = builder.node();
}
@Override
public ConfigNode.ObjectNode data() {
return data;
}
}
static class OverrideContentImpl extends ContentImpl implements OverrideContent {
private final OverrideSource.OverrideData data;
OverrideContentImpl(OverrideContent.Builder builder) {
super(builder);
this.data = builder.data();
}
@Override
public OverrideSource.OverrideData data() {
return data;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.spi;
import java.util.function.BiConsumer;
/**
* A source that supports notifications when changed.
*/
public interface EventConfigSource {
/**
* Register a change listener.
*
* @param changedNode the key and node of the configuration that changed. This may be the whole config tree, or a specific
* node depending on how fine grained the detection mechanism is. To notify of a whole node being changed,
* use empty string as a key
*/
void onChange(BiConsumer<String, ConfigNode> changedNode);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,18 @@
* limitations under the License.
*/
package io.helidon.config;
package io.helidon.config.spi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.helidon.config.spi.ConfigNode;
import io.helidon.config.spi.ConfigNode.ListNode;
import io.helidon.config.spi.ConfigNode.ObjectNode;
import io.helidon.config.spi.ConfigNode.ValueNode;
/**
* An implementation of {@link ConfigSources.MergingStrategy} in which nodes
* An implementation of {@link MergingStrategy} in which nodes
* from a root earlier in the list of roots have higher priority than nodes from
* a root later in the list.
* <p>
@@ -34,13 +34,13 @@ import io.helidon.config.spi.ConfigNode.ValueNode;
* they were passed to {@code merge}. As soon as it finds a {@code Config} tree
* containing a value for the key is it immediately returns that value,
* disregarding other later config roots.
*
* @see CompositeConfigSource
*/
final class FallbackMergingStrategy implements ConfigSources.MergingStrategy {
final class FallbackMergingStrategy implements MergingStrategy {
@Override
public ObjectNode merge(List<ObjectNode> rootNodes) {
public ObjectNode merge(List<ObjectNode> rootNodesParam) {
// we may get an immutable list
List<ObjectNode> rootNodes = new ArrayList<>(rootNodesParam);
Collections.reverse(rootNodes);
ObjectNode.Builder builder = ObjectNode.builder();

View File

@@ -0,0 +1,40 @@
/*
* 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.spi;
import java.util.Optional;
/**
* A source that is not capable of loading all keys at once.
* Even though such a source can be used in Helidon Config, there are limitations to its use.
*
* The following methods may ignore data from a lazy source (may for cases when the node was not invoked directly):
* <ul>
* <li>{@link io.helidon.config.Config#asMap()}</li>
* <li>{@link io.helidon.config.Config#asNodeList()}</li>
* <li>{@link io.helidon.config.Config#traverse()}</li>
* </ul>
*/
public interface LazyConfigSource {
/**
* Provide a value for the node on the requested key.
*
* @param key config key to obtain
* @return value of the node if available in the source
*/
Optional<ConfigNode> node(String key);
}

View File

@@ -0,0 +1,59 @@
/*
* 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.spi;
import java.util.List;
/**
* An algorithm for combining multiple {@code ConfigNode.ObjectNode} root nodes
* into a single {@code ConfigNode.ObjectNode} root node.
*
* @see #fallback() default merging strategy
*/
public interface MergingStrategy {
/**
* Merges an ordered list of {@link io.helidon.config.spi.ConfigNode.ObjectNode}s into a
* single instance.
* <p>
* Typically nodes (object, list or value) from a root earlier in the
* list are considered to have a higher priority than nodes from a root
* that appears later in the list, but this is not required and is
* entirely up to each {@code MergingStrategy} implementation.
*
* @param rootNodes list of root nodes to combine
* @return ObjectNode root node resulting from the merge
*/
ConfigNode.ObjectNode merge(List<ConfigNode.ObjectNode> rootNodes);
/**
* Returns an implementation of {@code MergingStrategy} in which nodes
* from a root earlier in the list have higher priority than nodes from
* a root later in the list.
* <p>
* The merged behavior is as if the resulting merged {@code Config},
* when resolving a value of a key, consults the {@code Config} roots in
* the order they were passed to {@code merge}. As soon as it finds a
* {@code Config} tree containing a value for the key is it immediately
* returns that value, disregarding other later config roots.
*
* @return new instance of fallback merging strategy
*/
static MergingStrategy fallback() {
return new FallbackMergingStrategy();
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.spi;
import java.util.Optional;
import io.helidon.config.ConfigException;
/**
* An eager source that can read all data from the underlying origin as a configuration node.
*/
public interface NodeConfigSource extends ConfigSource {
/**
* Loads the underlying source data. This method is only called when the source {@link #exists()}.
* <p>
* The method can be invoked repeatedly, for example during retries.
*
* @return An instance of {@code T} as read from the underlying origin of the data (if it exists)
* @throws io.helidon.config.ConfigException in case of errors loading from the underlying origin
*/
Optional<ConfigContent.NodeContent> load() throws ConfigException;
}

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.
@@ -22,6 +22,7 @@ import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -51,13 +52,21 @@ import io.helidon.config.ConfigException;
*
* @see OverrideData
*/
public interface OverrideSource extends Source<OverrideSource.OverrideData>, Supplier<OverrideSource> {
public interface OverrideSource extends Source, Supplier<OverrideSource> {
@Override
default OverrideSource get() {
return this;
}
/**
* Load override data from the underlying source.
*
* @return override data if present, empty otherwise
* @throws ConfigException in case the loading of data failed
*/
Optional<ConfigContent.OverrideContent> load() throws ConfigException;
/**
* Group of config override settings.
* <p>
@@ -137,8 +146,8 @@ public interface OverrideSource extends Source<OverrideSource.OverrideData>, Sup
*/
public static OverrideData create(Reader reader) {
OrderedProperties properties = new OrderedProperties();
try (Reader autocloseableReader = reader) {
properties.load(autocloseableReader);
try (Reader autoCloseableReader = reader) {
properties.load(autoCloseableReader);
} catch (IOException e) {
throw new ConfigException("Cannot load data from reader.", e);
}

View File

@@ -0,0 +1,81 @@
/*
* 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.spi;
import java.util.Optional;
import io.helidon.config.ConfigException;
/**
* An eager source that can read all data from the underlying origin as a stream that can be
* parsed based on its media type (or using an explicit {@link io.helidon.config.spi.ConfigParser}.
*/
public interface ParsableSource extends Source {
/**
* Loads the underlying source data. This method is only called when the source {@link #exists()}.
* <p>
* The method can be invoked repeatedly, for example during retries.
* In case the underlying data is gone or does not exist, return an empty optional.
*
* @return An instance of {@code T} as read from the underlying origin of the data (if it exists)
* @throws io.helidon.config.ConfigException in case of errors loading from the underlying origin
*/
Optional<ConfigParser.Content> load() throws ConfigException;
/**
* If a parser is configured with this source, return it.
* The source implementation does not need to handle config parser.
*
* @return content parser if one is configured on this source
*/
Optional<ConfigParser> parser();
/**
* If media type is configured on this source, or can be guessed from the underlying origin, return it.
* The media type may be used to locate a {@link io.helidon.config.spi.ConfigParser} if one is not explicitly
* configured.
*
* @return media type if configured or detected from content
*/
Optional<String> mediaType();
/**
* A builder for a parsable source.
*
* @param <B> type of the builder, used when extending this builder ({@code MyBuilder implements Builder<MyBuilder>}
* @see io.helidon.config.AbstractConfigSourceBuilder
* @see io.helidon.config.AbstractConfigSource
*/
interface Builder<B extends Builder<B>> extends ConfigSource.Builder<B> {
/**
* Configure an explicit parser to be used with the source.
*
* @param parser parser to use
* @return updated builder instance
*/
B parser(ConfigParser parser);
/**
* Configure an explicit media type to be used with this source.
* This method is ignored if a parser was configured.
*
* @param mediaType media type to use
* @return updated builder instance
*/
B mediaType(String mediaType);
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.spi;
import java.util.Optional;
import java.util.function.Supplier;
/**
* A source implementing this interface provides support for polling using a {@link io.helidon.config.spi.PollingStrategy}.
* This is achieved through a stamp passed between the source and the strategy to check for changes.
* @see io.helidon.config.spi.PollingStrategy
*
* @param <S> the type of the stamp used by the source (such as byte[] with a digest of a file)
*/
public interface PollableSource<S> {
/**
* This method is invoked to check if this source has changed.
*
* @param stamp the stamp of the last loaded content
* @return {@code true} if the current data of this config source differ from the loaded data, including
* cases when the source has disappeared
*/
boolean isModified(S stamp);
/**
* If a polling strategy is configured with this source, return it.
* The source implementation does not need to handle polling strategy.
*
* @return polling strategy if one is configured on this source
*/
Optional<PollingStrategy> pollingStrategy();
/**
* A builder for pollable source.
*
* @param <T> type of the builder, used when extending this builder
* @see io.helidon.config.AbstractConfigSourceBuilder
* @see io.helidon.config.AbstractConfigSource
*/
interface Builder<T extends Builder<T>> {
T pollingStrategy(PollingStrategy pollingStrategy);
default T pollingStrategy(Supplier<? extends PollingStrategy> pollingStrategy) {
return pollingStrategy(pollingStrategy.get());
}
}
}

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.
@@ -16,85 +16,58 @@
package io.helidon.config.spi;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Flow;
import java.util.function.Supplier;
import io.helidon.config.Config;
import io.helidon.config.PollingStrategies;
/**
* Mechanism for notifying interested subscribers when they should check for
* Mechanism for notifying interested listeners when they should check for
* changes that might have been made to the data used to create a {@code Config}
* tree, as accessed through {@link ConfigSource}s.
* tree, as accessed through {@link io.helidon.config.spi.PollableSource}s.
* <p>
* Once it loads a {@link Config} tree from {@link ConfigSource}s the config
* system does not itself change the in-memory {@code Config} tree. Even so, the
* underlying data available via the tree's {@code ConfigSource}s can change.
* Implementations of {@code PollingStrategy} other interested code to learn
* when changes to that underlying data might have occurred.
* The Config system nevertheless supports change notification through
* {@link Config#onChange(java.util.function.Consumer)} and that is enabled
* also by polling strategies.
* <p>
* In implementations of {@code PollingStrategy} the {@link #ticks()} method
* returns a {@link Flow.Publisher} of {@link PollingEvent}s to which the
* application or the {@code ConfigSource}s themselves can subscribe. Generally,
* each event is a hint to the application or a {@code ConfigSource} itself that
* it should check to see if any of the underlying config data it relies on
* might have changed. Note that a {@code PollingStrategy}'s publication of an
* event does not necessarily guarantee that the underlying data has in fact
* changed, although this might be true for some {@code PollingStrategy}
* implementations.
* In implementations of {@code PollingStrategy} provide a notification mechanism
* through {@link #start(io.helidon.config.spi.PollingStrategy.Polled)}, where the
* polled component receives events that should check for changes.
* In config system itself, this is handled by internals and is not exposed outside
* of it.
* <p>
* Typically a custom {@link ConfigSource} implementation creates a
* {@code Flow.Subscriber} which it uses to subscribe to the
* {@code Flow.Publisher} that is returned from the
* {@code PollingStrategy.ticks()} method. When that subscriber receives a
* {@code PollingEvent} it triggers the {@code ConfigSource} to reload the
* configuration from the possibly changed underlying data. For example, each
* {@link AbstractParsableConfigSource} can use a different
* {@code PollingStrategy}.
* A config source implements appropriate functionality in method
* {@link io.helidon.config.spi.PollableSource#isModified(Object)}, which will
* be invoked each time a polling strategy triggers the listener.
* <p>
* As described with {@link io.helidon.config.MetaConfig#configSource(io.helidon.config.Config)}, the config system can
* load {@code ConfigSource}s using meta-configuration, which supports
* specifying polling strategies. All {@link PollingStrategies built-in polling
* strategies} and custom ones are supported. (The support is tightly connected
* with {@link AbstractSource.Builder#config(Config) AbstractSource extensions}
* and will not be automatically provided by any another config source
* implementations.)
* strategies} and custom ones are supported.
* See {@link io.helidon.config.spi.PollingStrategyProvider} for details.
* <p>
* The meta-configuration for a config source can set the property
* {@code polling-strategy} using the following nested {@code properties}:
* <ul>
* <li>{@code type} - name of the polling strategy implementation.
* <li>{@code type} - name of the polling strategy implementation (referencing the Java Service Loader service)
* <table class="config">
* <caption>Built-in Polling Strategies</caption>
* <tr>
* <th>Name</th>
* <th>Strategy</th>
* <th>Required Properties</th>
* </tr>
* <tr>
* <td>{@code regular}</td>
* <td>Scheduled polling at regular intervals. See
* {@link PollingStrategies#regular(Duration)}.</td>
* <td>{@code interval} in {@link Duration} format, e.g. {@code PT15S} means 15
* seconds</td>
* </tr>
* <tr>
* <td>{@code watch}</td>
* <td>Filesystem monitoring of the {@code Path} specified in the config source
* definition. See {@link PollingStrategies#watch(Path)}.
* <p>
* Use this strategy only with config sources based on
* {@link AbstractSource.Builder} that are paramaterized with {@code Path}. This
* includes null {@link io.helidon.config.ConfigSources#classpath(String) classpath},
* {@link io.helidon.config.ConfigSources#file(String) file} and
* {@link io.helidon.config.ConfigSources#directory(String) directory} config
* sources.
* </td>
* <td>n/a</td>
* </tr>
* <tr>
* <th>Name</th>
* <th>Strategy</th>
* <th>Required Properties</th>
* </tr>
* <tr>
* <td>{@code regular}</td>
* <td>Scheduled polling at regular intervals. See
* {@link PollingStrategies#regular(Duration)}.</td>
* <td>{@code interval} in {@link Duration} format, e.g. {@code PT15S} means 15
* seconds</td>
* </tr>
* </table>
* <p>
* </li>
@@ -108,118 +81,52 @@ import io.helidon.config.PollingStrategies;
* ignores the {@code class} setting.
* <h3>Meta-configuration Support for Custom Polling Strategies</h3>
* To support settings in meta-configuration, a custom polling strategy must
* follow these patterns.
* <ol>
* <li>Auto-configuration from meta-configuration properties
* <p>
* The implementation class should define a Java bean property for each
* meta-configuration property it needs to support. The config system uses
* mapping functions to convert the text in the
* meta-configuration into the correct Java type and then assigns the value to
* the correspondingly-named Java bean property defined on the custom strategy
* instance. See the built-in mappers defined in
* {@link io.helidon.config.ConfigMappers} to see what Java types are automatically
* supported.
* </li>
* <li>Accessing the {@code ConfigSource} meta-config attributes
* <p>
* The custom polling strategy can get access to the same meta-configuration
* attributes that are used to construct the associated {@code ConfigSource}. To
* do so the custom implementation class should implement a constructor that
* accepts the same Java type as that returned by the
* {@link AbstractSource.Builder#target()} method on the builder that is used
* to construct the {@code ConfigSource}.
* <p>
* For example, a custom polling strategy useful with {@code ConfigSource}s
* based on a {@code Path} would implement a constructor that accepts a
* {@code Path} argument.
* </li>
* </ol>
* be capable of processing the meta configuration provided to
* {@link io.helidon.config.spi.PollingStrategyProvider#create(String, io.helidon.config.Config)}
*
* @see AbstractParsableConfigSource.Builder#pollingStrategy(Supplier)
* @see Flow.Publisher
* @see PollingStrategies PollingStrategies - access built-in implementations.
* @see io.helidon.config.spi.PollingStrategyProvider to implement custom polling strategies
* @see io.helidon.config.spi.ChangeWatcher to implement change watchers that notify config system when a target actually changes
*/
public interface PollingStrategy extends Supplier<PollingStrategy> {
@FunctionalInterface
public interface PollingStrategy {
@Override
default PollingStrategy get() {
return this;
/**
* Start this polling strategy. From this point in time, the polled will receive
* events on {@link Polled#poll(java.time.Instant)}.
* It is the responsibility of the {@link io.helidon.config.spi.PollingStrategy.Polled}
* to handle such requests.
* A {@link io.helidon.config.spi.ConfigSource} needs only support for polling stamps
* to support a polling strategy, the actual reloading is handled by the
* configuration component.
* There is no need to implement {@link io.helidon.config.spi.PollingStrategy.Polled} yourself,
* unless you want to implement a new component that supports polling.
* Possible reloads of configuration are happening within the thread that invokes this method.
*
* @param polled a component receiving polling events.
*/
void start(Polled polled);
/**
* Stop polling and release all resources.
*/
default void stop() {
}
/**
* Returns a {@link Flow.Publisher} which fires {@link PollingEvent}s.
* <p>
* Note that {@code PollingStrategy} implementations can generate
* {@code PollingEvent}s whether or not any subscribers have subscribed to
* the publisher of the events.
* <p>
* Subscribers typically invoke {@link Flow.Subscription#request(long)}
* asking for one event initially, and then after it has processed each
* event the subscriber requests one more event.
* <p>
* The subscriber might not receive every event broadcast, for example if it
* subscribes to the publisher after an event has been delivered to the
* publisher.
* <p>
* Each {@code PollingStrategy} implementation chooses which executor to use
* for notifying subscribers. The recommended practice is to use the same
* thread as the polling strategy implementation runs on.
*
* @return a publisher of events
* A polled component. For config this interface is implemented by the config system itself.
*/
Flow.Publisher<PollingEvent> ticks();
// /**
// * Notifies a polling strategy that a configuration source has been changed since a precedent {@link PollingEvent} had been
// * fired.
// * <p>
// * The default implementation does not do anything, but can be overridden to change a behaviour of the polling strategy, for
// * example, to change delay between ticking or just to log it.
// *
// * @param changed {@code true} if source was changed since a precedent {@link PollingEvent}
// * @see RecurringPolicy
// * @see RecurringPolicy#shorten()
// * @see RecurringPolicy#lengthen()
// */
//default void configSourceChanged(boolean changed) { //TODO WILL BE PUBLIC API AGAIN LATER, Issue #14.
//}
/**
* Event indicating that data used in constructing a given {@code Config}
* tree might have changed.
*
* @see PollingStrategy#ticks()
*/
interface PollingEvent {
@FunctionalInterface
interface Polled {
/**
* Returns the event timestamp.
* Poll for changes.
* The result may be used to modify behavior of the {@link io.helidon.config.spi.PollingStrategy} triggering this
* poll event.
*
* @return event timestamp
* @param when instant this polling request was created
* @return result of the polling
*/
Instant timestamp();
/**
* Creates a new instance of {@link PollingEvent} with
* {@link Instant#now()} used as its timestamp.
*
* @return new instance of event
*/
static PollingEvent now() {
Instant timestamp = Instant.now();
return new PollingEvent() {
@Override
public Instant timestamp() {
return timestamp;
}
@Override
public String toString() {
return "PollingEvent @ " + timestamp;
}
};
}
ChangeEventType poll(Instant when);
}
}

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.
@@ -15,10 +15,8 @@
*/
package io.helidon.config.spi;
import java.util.function.Function;
/**
* Java service loader service to create a polling strategy factory based on meta configuration.
*/
public interface PollingStrategyProvider extends MetaConfigurableProvider<Function<Object, PollingStrategy>> {
public interface PollingStrategyProvider extends MetaConfigurableProvider<PollingStrategy> {
}

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.
@@ -19,19 +19,18 @@ package io.helidon.config.spi;
import java.util.function.Supplier;
/**
* Mechanism for controlling retry of attempts to load data by an
* {@link AbstractSource}.
* Mechanism for controlling retry of attempts to load data by a {@link io.helidon.config.spi.ConfigSource}.
* <p>
* When an {@code AbstractSource} attempts to load the underlying data it uses a
* {@code RetryPolicy} to govern if and how it retries the load operation in
* case of errors.
* When a {@link io.helidon.config.Config} attempts to load the underlying data
* of a {@link io.helidon.config.spi.ConfigSource} it uses a {@code RetryPolicy} to govern if and how it
* retries the load operation in case of errors.
* <p>
* The {@link #execute(java.util.function.Supplier) } method of each policy
* implementation must perform at least one attempt to load the data, even if it
* chooses not to retry in case of errors.
*/
@FunctionalInterface
public interface RetryPolicy {
public interface RetryPolicy extends Supplier<RetryPolicy> {
/**
* Invokes the provided {@code Supplier} to read the source data and returns
* that data.
@@ -66,4 +65,9 @@ public interface RetryPolicy {
default boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
default RetryPolicy get() {
return this;
}
}

View File

@@ -17,17 +17,18 @@
package io.helidon.config.spi;
import java.util.Optional;
import java.util.concurrent.Flow;
import io.helidon.config.ConfigException;
import java.util.function.Supplier;
/**
* Source of the specified type {@code <T>} of data.
* Source of data.
*
* @param <T> a type of source
* The actual loading of the data depends on the type of the source.
*
* @see io.helidon.config.spi.ParsableSource
* @see io.helidon.config.spi.NodeConfigSource
* @see LazyConfigSource
*/
public interface Source<T> extends Changeable<T>, AutoCloseable {
public interface Source {
/**
* Short, human-readable summary referring to the underlying source.
* <p>
@@ -49,37 +50,66 @@ public interface Source<T> extends Changeable<T>, AutoCloseable {
}
/**
* Loads the underlying source data, converting it into an {@code Optional}
* around the parameterized type {@code T}.
* <p>
* Implementations should return {@link Optional#empty()} if the underlying
* origin does not exist.
* <p>
* The method can be invoked repeatedly, for example during retries.
* If the underlying data exist at this time.
* This is to prevent us loading such a source if we know it does not exist.
*
* @return {@code Optional<T>} referring to an instance of {@code T} as read
* from the underlying origin of the data (if it exists) or
* {@link Optional#empty()} otherwise
* @throws ConfigException in case of errors loading from the underlying
* origin
* @return {@code true} if the source exists, {@code false} otherwise
*/
Optional<T> load() throws ConfigException;
//
// source changes
//
@Override
default Flow.Publisher<Optional<T>> changes() { //TODO later remove, see Changeable interface
return Flow.Subscriber::onComplete;
default boolean exists() {
return true;
}
/**
* Closes the @{code Source}, releasing any resources it holds.
* Retry policy configured on this config source.
*
* @throws Exception in case of errors encountered while closing the source
* @return configured retry policy
*/
@Override
default void close() throws Exception {
default Optional<RetryPolicy> retryPolicy() {
return Optional.empty();
}
/**
* Whether this source is optional.
*
* @return return {@code true} for optional source, returns {@code false} by default
*/
default boolean optional() {
return false;
}
/**
* Configurable options of a {@link io.helidon.config.spi.Source}.
*
* @param <B> type implementation class of this interface
*/
interface Builder<B extends Builder<B>> {
/**
* Configure a retry policy to be used with this source.
* If none is configured, the source is invoked directly with no retries.
*
* @param policy retry policy to use
* @return updated builder instance
*/
B retryPolicy(Supplier<? extends RetryPolicy> policy);
/**
* Whether the source is optional or not.
* When configured to be optional, missing underlying data do not cause an exception to be raised.
*
* @param optional {@code true} when this source should be optional
* @return updated builder instance
*/
B optional(boolean optional);
/**
* Configure this source to be optional.
* <p>
* Same as calling {@link #optional(boolean) optional(true)}.
*
* @return updated buidler instance
*/
default B optional() {
return optional(true);
}
}
}

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