mirror of
https://github.com/jlengrand/helidon.git
synced 2026-03-10 08:21:17 +00:00
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:
51
CHANGELOG.md
51
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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())));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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() ? "" : "*");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}<{@link String},{@link String},{@link
|
||||
@@ -1476,8 +1454,8 @@ public interface Config {
|
||||
* Registers a {@link ConfigFilter} provider as a {@link Function}<{@link Config}, {@link ConfigFilter}>. 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> </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> </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> </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> </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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "__";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>}.
|
||||
* <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
config/config/src/main/java/io/helidon/config/UrlHelper.java
Normal file
72
config/config/src/main/java/io/helidon/config/UrlHelper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>}.
|
||||
* <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user