Skip to content

Commit

Permalink
2.x: Configuration fixes (#6159)
Browse files Browse the repository at this point in the history
* 2.x: Configuration fixes (#6145) (#6150)
- allow unresolved key references (configurable)
- config created from another config honors node types
- Key resolving does not fail by default when reference cannot be found - aligned with value resolving filter
- Config.Builder methods added for key resolving and value resolving "fail on missing reference"
- Fixed switch statement, too new java feature...

Signed-off-by: Tomas Langer <tomas.langer@oracle.com>
  • Loading branch information
tomas-langer authored Feb 13, 2023
1 parent 89f00ef commit 14c14b9
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 32 deletions.
19 changes: 18 additions & 1 deletion config/config/src/main/java/io/helidon/config/BuilderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ class BuilderImpl implements Config.Builder {
*/
private boolean cachingEnabled;
private boolean keyResolving;
private boolean keyResolvingFailOnMissing;
private boolean valueResolving;
private boolean valueResolvingFailOnMissing;
private boolean systemPropertiesSourceEnabled;
private boolean environmentVariablesSourceEnabled;
private boolean envVarAliasGeneratorEnabled;
Expand Down Expand Up @@ -261,12 +263,24 @@ public Config.Builder disableKeyResolving() {
return this;
}

@Override
public Config.Builder failOnMissingKeyReference(boolean shouldFail) {
this.keyResolvingFailOnMissing = shouldFail;
return this;
}

@Override
public Config.Builder disableValueResolving() {
this.valueResolving = false;
return this;
}

@Override
public Config.Builder failOnMissingValueReference(boolean shouldFail) {
this.valueResolvingFailOnMissing = shouldFail;
return this;
}

@Override
public Config.Builder disableEnvironmentVariablesSource() {
environmentVariablesSourceEnabled = false;
Expand All @@ -282,7 +296,7 @@ public Config.Builder disableSystemPropertiesSource() {
@Override
public AbstractConfigImpl build() {
if (valueResolving) {
addFilter(ConfigFilters.valueResolving());
addFilter(ConfigFilters.valueResolving().failOnMissingReference(valueResolvingFailOnMissing));
}
if (null == changesExecutor) {
changesExecutor = Executors.newCachedThreadPool(new ConfigThreadFactory("config-changes"));
Expand Down Expand Up @@ -329,6 +343,7 @@ public AbstractConfigImpl build() {
cachingEnabled,
changesExecutor,
keyResolving,
keyResolvingFailOnMissing,
aliasGenerator)
.newConfig();
}
Expand Down Expand Up @@ -439,6 +454,7 @@ ProviderImpl createProvider(ConfigMapperManager configMapperManager,
boolean cachingEnabled,
Executor changesExecutor,
boolean keyResolving,
boolean keyResolvingFailOnMissing,
Function<String, List<String>> aliasGenerator) {
return new ProviderImpl(configMapperManager,
targetConfigSource,
Expand All @@ -447,6 +463,7 @@ ProviderImpl createProvider(ConfigMapperManager configMapperManager,
cachingEnabled,
changesExecutor,
keyResolving,
keyResolvingFailOnMissing,
aliasGenerator);
}

Expand Down
49 changes: 40 additions & 9 deletions config/config/src/main/java/io/helidon/config/Config.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,9 +38,9 @@
import io.helidon.config.spi.OverrideSource;

/**
* <h1>Configuration</h1>
* <h2>Configuration</h2>
* Immutable tree-structured configuration.
* <h2>Loading Configuration</h2>
* <h3>Loading Configuration</h3>
* Load the default configuration using the {@link Config#create} method.
* <pre>{@code
* Config config = Config.create();
Expand Down Expand Up @@ -95,7 +95,7 @@
* </tr>
* </table>
*
* <h2>Navigating in a Configuration Tree</h2>
* <h3>Navigating in a Configuration Tree</h3>
* Each loaded configuration is a tree of {@code Config} objects. The
* application can access an arbitrary node in the tree by passing its
* fully-qualified name to {@link Config#get}:
Expand Down Expand Up @@ -146,8 +146,8 @@
* <p>
* To get node value, use {@link #as(Class)} to access this config node as a {@link ConfigValue}
*
* <h2>Converting Configuration Values to Types</h2>
* <h3>Explicit Conversion by the Application</h3>
* <h3>Converting Configuration Values to Types</h3>
* <h4>Explicit Conversion by the Application</h4>
* The interpretation of a configuration node, including what datatype to use,
* is up to the application. To interpret a node's value as a type other than
* {@code String} the application can invoke one of these convenience methods:
Expand Down Expand Up @@ -231,14 +231,19 @@
* that can handle classes that fulfill some requirements (see documentation), such as a public constructor,
* static "create(Config)" method etc.
*
* <h2><a id="multipleSources">Handling Multiple Configuration
* Sources</a></h2>
* <h3><a id="multipleSources">Handling Multiple Configuration
* Sources</a></h3>
* A {@code Config} instance, including the default {@code Config} returned by
* {@link Config#create}, might be associated with multiple {@link ConfigSource}s. The
* config system merges these together so that values from config sources with higher priority have
* precedence over values from config sources with lower priority.
*/
public interface Config {
/**
* Generic type of configuration.
*/
GenericType<Config> GENERIC_TYPE = GenericType.create(Config.class);

/**
* Returns empty instance of {@code Config}.
*
Expand Down Expand Up @@ -922,6 +927,12 @@ interface Key extends Comparable<Key> {
@Override
String toString();

/**
* Create a child key to the current key.
*
* @param key child key (relative to current key)
* @return a new resolved key
*/
Key child(Key key);

/**
Expand Down Expand Up @@ -1138,7 +1149,7 @@ interface Context {
* @see ConfigParser
* @see ConfigFilter
*/
interface Builder {
interface Builder extends io.helidon.common.Builder<Config> {
/**
* Sets ordered list of {@link ConfigSource} instance to be used as single source of configuration
* to be wrapped into {@link Config} API.
Expand Down Expand Up @@ -1297,6 +1308,16 @@ default Builder sources(Supplier<? extends ConfigSource> configSource,
*/
Builder disableKeyResolving();

/**
* When key resolving is enabled and a reference cannot be resolved, should we fail, or use the key verbatim.
* Defaults to {@code false}, so key resolving does not fail when a reference is missing.
*
* @param shouldFail whether to fail when key reference cannot be resolved
* @return updated builder
* @see #disableKeyResolving()
*/
Builder failOnMissingKeyReference(boolean shouldFail);

/**
* Disables an usage of resolving value tokens.
* <p>
Expand All @@ -1309,6 +1330,16 @@ default Builder sources(Supplier<? extends ConfigSource> configSource,
*/
Builder disableValueResolving();

/**
* When value resolving is enabled and a reference cannot be resolved, should we fail, or use the value verbatim.
* Defaults to {@code false}, so value resolving does not fail when a reference is missing.
*
* @param shouldFail whether to fail when value reference cannot be resolved
* @return updated builder
* @see #disableValueResolving()
*/
Builder failOnMissingValueReference(boolean shouldFail);

/**
* Disables use of {@link ConfigSources#environmentVariables() environment variables config source}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -69,7 +69,7 @@ public static ConfigSource empty() {
* @return {@code ConfigSource} for the same {@code Config} as the original
*/
public static ConfigSource create(Config config) {
return ConfigSources.create(config.asMap().get()).get();
return create(ObjectNodeImpl.create(config));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
* Copyright (c) 2020, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -75,6 +75,14 @@ public Set<Entry<String, ConfigNode>> entrySet() {
return members.entrySet();
}

static ObjectNode create(Config config) {
ObjectNode.Builder root = ObjectNode.builder();

addObjectNode(root, config);

return root.build();
}

static void initDescription(ConfigNode node, String description) {
switch (node.nodeType()) {
case OBJECT:
Expand Down Expand Up @@ -105,6 +113,56 @@ public MergeableNode merge(MergeableNode node) {
}
}

private static void addObjectNode(Builder parentBuilder, Config parent) {
parent.asNodeList().ifPresent(it -> {
for (Config child : it) {
switch (child.type()) {
case OBJECT:
Builder objectChildBuilder = ObjectNode.builder();
addObjectNode(objectChildBuilder, child);
parentBuilder.addObject(child.name(), objectChildBuilder.build());
break;
case LIST:
ListNode.Builder listChildBuilder = ListNode.builder();
addListNode(listChildBuilder, child);
parentBuilder.addList(child.name(), listChildBuilder.build());
break;
case VALUE:
parentBuilder.addValue(child.name(), child.asString().get());
break;
default:
// do nothing
break;
}
}
});
}

private static void addListNode(ListNode.Builder parentBuilder, Config parent) {
parent.asNodeList().ifPresent(it -> {
for (Config child : it) {
switch (child.type()) {
case OBJECT:
Builder objectChildBuilder = ObjectNode.builder();
addObjectNode(objectChildBuilder, child);
parentBuilder.addObject(objectChildBuilder.build());
break;
case LIST:
ListNode.Builder listChildBuilder = ListNode.builder();
addListNode(listChildBuilder, child);
parentBuilder.addList(listChildBuilder.build());
break;
case VALUE:
parentBuilder.addValue(child.asString().get());
break;
default:
// do nothing
break;
}
}
});
}

private MergeableNode mergeWithValueNode(ValueNodeImpl node) {
ObjectNodeBuilderImpl builder = ObjectNodeBuilderImpl.create(members, resolveTokenFunction);
builder.value(node.value());
Expand Down
63 changes: 47 additions & 16 deletions config/config/src/main/java/io/helidon/config/ProviderImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2022 Oracle and/or its affiliates.
* Copyright (c) 2017, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -54,6 +54,7 @@ class ProviderImpl implements Config.Context {

private final Executor changesExecutor;
private final boolean keyResolving;
private final boolean keyResolvingFailOnMissing;
private final Function<String, List<String>> aliasGenerator;

private ConfigDiff lastConfigsDiff;
Expand All @@ -68,6 +69,7 @@ class ProviderImpl implements Config.Context {
boolean cachingEnabled,
Executor changesExecutor,
boolean keyResolving,
boolean keyResolvingFailOnMissing,
Function<String, List<String>> aliasGenerator) {
this.configMapperManager = configMapperManager;
this.configSource = configSource;
Expand All @@ -80,6 +82,7 @@ class ProviderImpl implements Config.Context {
this.lastConfig = (AbstractConfigImpl) Config.empty();

this.keyResolving = keyResolving;
this.keyResolvingFailOnMissing = keyResolvingFailOnMissing;
this.aliasGenerator = aliasGenerator;
}

Expand Down Expand Up @@ -153,36 +156,56 @@ private ObjectNode resolveKeys(ObjectNode rootNode) {
}

Map<String, String> tokenValueMap = tokenToValueMap(flattenValueNodes);
boolean failOnMissingKeyReference = getBoolean(flattenValueNodes,
"config.key-resolving.fail-on-missing-reference",
keyResolvingFailOnMissing);

resolveTokenFunction = (token) -> {
if (token.startsWith("$")) {
return tokenValueMap.get(parseTokenReference(token));
String tokenRef = parseTokenReference(token);
String resolvedValue = tokenValueMap.get(tokenRef);
if (resolvedValue.isEmpty()) {
if (failOnMissingKeyReference) {
throw new ConfigException(String.format("Missing token '%s' to resolve a key reference.", tokenRef));
} else {
return token;
}
}
return resolvedValue;
}
return token;
};
}
return ObjectNodeBuilderImpl.create(rootNode, resolveTokenFunction).build();
}

/*
* Returns a map of required replacement tokens to their respective values from the current config tree.
* The values may be empty strings, representing unresolved references.
*/
private Map<String, String> tokenToValueMap(Map<String, String> flattenValueNodes) {
return flattenValueNodes.keySet()
.stream()
.flatMap(this::tokensFromKey)
.distinct()
.collect(Collectors.toMap(Function.identity(), t ->
flattenValueNodes.compute(Config.Key.unescapeName(t), (k, v) -> {
if (v == null) {
throw new ConfigException(String.format("Missing token '%s' to resolve.", t));
} else if (v.equals("")) {
throw new ConfigException(String.format("Missing value in token '%s' definition.", t));
} else if (v.startsWith("$")) {
throw new ConfigException(String.format(
"Key token '%s' references to a reference in value. A recursive references is not "
+ "allowed.",
t));
}
return Config.Key.escapeName(v);
})));
.collect(Collectors.toMap(Function.identity(), t -> {
// t is the reference we need to resolve
// we cannot use compute, as that modifies the map we are currently navigating
String value = flattenValueNodes.get(Config.Key.unescapeName(t));
if (value == null) {
value = "";
} else {
if (value.startsWith("$")) {
throw new ConfigException(String.format(
"Key token '%s' references to a reference in value. A recursive"
+ " references is not allowed.",
t));
}
value = Config.Key.escapeName(value);
}
// either null (not found), or escaped value
return value;
}));
}

private Stream<String> tokensFromKey(String s) {
Expand Down Expand Up @@ -249,6 +272,14 @@ private void initializeFilters(Config config, ChainConfigFilter chain) {
.forEachOrdered(filter -> filter.init(config));
}

private boolean getBoolean(Map<String, String> valueNodes, String key, boolean defaultValue) {
String value = valueNodes.get(key);
if (value == null) {
return defaultValue;
}
return Boolean.parseBoolean(value);
}

/**
* Config filter chain that can combine a collection of {@link ConfigFilter} and wrap them into one config filter.
*/
Expand Down
Loading

0 comments on commit 14c14b9

Please sign in to comment.