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

@@ -43,6 +43,7 @@ Notable changes:
- Helidon DB Client [657](https://github.com/oracle/helidon/pull/657) [1329](https://github.com/oracle/helidon/pull/1329)
- Native image: Helidon MP support [1328](https://github.com/oracle/helidon/pull/1328) [1295](https://github.com/oracle/helidon/pull/1295) [1259](https://github.com/oracle/helidon/pull/1259)
- Config: Helidon Config now implements MicroProfile config, so you can cast between these two types
- Security: Basic auth and OIDC in MP native image [1330](https://github.com/oracle/helidon/pull/1330)
- Security: JWT and OIDC security providers now support groups claim. [1324](https://github.com/oracle/helidon/pull/1324)
- Support for Helidon Features [1240](https://github.com/oracle/helidon/pull/1240)
@@ -112,17 +113,55 @@ Here are the details:
- If you use the `TracerBuilder` abstraction in Helidon and have no custom Spans, there is no change required
#### Config
Meta configuration has been refactored to be done through `ServiceLoader` services. If you created
a custom `ConfigSource`, `PollingStrategy` or `RetryPolicy`, please have a look at the new documentation.
Config now implements MicroProfile config (not explicitly, you can cast between MP Config and Helidon Config).
There is a very small behavior change between MP methods and SE methods of config related to system
property handling:
##### Helidon MP
When using MP Config through the API, there are no backward incompatible changes in Helidon.
##### Helidon SE Config Usage
The following changes are relevant when using Helidon Config:
1. File watching is now done through a `ChangeWatcher` - use of `PollingStrategies.watch()` needs to be refactored to
`FileSystemWatcher.create()` and the method to configure it on config source builder has changed to
`changeWatcher(ChangeWatcher)`
2. Methods on `ConfigSources` now return specific builders (they use to return `AbstractParsableConfigSource.Builder` with
a complex type declaration). If you store such a builder in a variable, either change it to the correct type, or use `var`
3. Some APIs were cleaned up to be aligned with the development guidelines of Helidon. When using Git config source, or etcd
config source, the factory methods moved to the config source itself, and the builder now accepts all configuration
options through methods
4. The API of config source builders has been cleaned, so now only methods that are relevant to a specific config source type
can be invoked on such a builder. Previously you could configure a polling strategy on a source that did not support
polling
5. There is a small change in behavior of Helidon Config vs. MicroProfile config:
The MP TCK require that system properties are fully mutable (e.g. as soon as the property is changed, it
must be used), so MP Config methods work in this manner (with a certain performance overhead).
Helidon Config treats System properties as a mutable config source, with a time based polling strategy. So
Helidon Config treats System properties as a mutable config source, with a (optional) time based polling strategy. So
the change is reflected as well, though not immediately (this is only relevant if you use change notifications).
6. `CompositeConfigSource` has been removed from `Config`. If you need to configure `MerginStrategy`, you can do it now on
`Config` `Builder`
##### Helidon SE Config Extensibility
1. Meta configuration has been refactored to be done through `ServiceLoader` services. If you created
a custom `ConfigSource`, `PollingStrategy` or `RetryPolicy`, please have a look at the new documentation.
2. To implement a custom config source, you need to choose appropriate (new) interface(s) to implement. This is the choice:
From "how we obtain the source of data" point of view:
* `ParsableSource` - for sources that provide bytes (used to be reader, now `InputStream`)
* `NodeConfigSource` - for sources that provide a tree structure directly
* `LazyConfigSource` - for sources that cannot read the full config tree in advance
From mutability point of view (immutable config sources can ignore this):
* `PollableSource` - a config source that is capable of identifying a change based on a data "stamp"
* `WatchableSource` - a config source using a target that can be watched for changes without polling (such as `Path`)
* `EventConfigSource` - a config source that can trigger change events on its own
3. `AbstractConfigSource` and `AbstractConfigSourceBuilder` are now in package `io.helidon.config`
4. `ConfigContext` no longer contains method to obtain a `ConfigParser`, as this is no longer responsibility of
a config source
5. Do not throw an exception when config source does not exist, just return
an empty `Optional` from `load` method, or `false` from `exists()` method
6. Overall change support is handled by the config module and is no longer the responsibility
of the config source, just implement appropriate SPI methods if changes are supported,
such as `PollableSource.isModified(Object stamp)`
#### Metrics
Helidon now supports only MicroProfile Metrics 2.x. Modules for Metrics 1.x were removed, and

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.
@@ -64,8 +64,7 @@ public final class MediaTypes {
public static Optional<String> detectType(URL url) {
return DETECTORS.stream()
.map(mtd -> mtd.detectType(url))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
}
@@ -79,8 +78,7 @@ public final class MediaTypes {
public static Optional<String> detectType(URI uri) {
return DETECTORS.stream()
.map(mtd -> mtd.detectType(uri))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
}
@@ -94,8 +92,7 @@ public final class MediaTypes {
public static Optional<String> detectType(Path file) {
return DETECTORS.stream()
.map(mtd -> mtd.detectType(file))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
}
@@ -113,8 +110,7 @@ public final class MediaTypes {
public static Optional<String> detectType(String fileName) {
return DETECTORS.stream()
.map(mtd -> mtd.detectType(fileName))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst();
}
@@ -129,8 +125,7 @@ public final class MediaTypes {
return CACHE.computeIfAbsent(fileSuffix, it ->
DETECTORS.stream()
.map(mtd -> mtd.detectExtensionType(fileSuffix))
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(Optional::stream)
.findFirst());
}
}

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> {
}

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