Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x: Configuration fixes #6145

Merged
merged 2 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -91,7 +91,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 @@ -262,12 +264,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 @@ -283,7 +297,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 @@ -330,6 +344,7 @@ public AbstractConfigImpl build() {
cachingEnabled,
changesExecutor,
keyResolving,
keyResolvingFailOnMissing,
aliasGenerator)
.newConfig();
}
Expand Down Expand Up @@ -440,6 +455,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 @@ -448,6 +464,7 @@ ProviderImpl createProvider(ConfigMapperManager configMapperManager,
cachingEnabled,
changesExecutor,
keyResolving,
keyResolvingFailOnMissing,
aliasGenerator);
}

Expand Down
22 changes: 21 additions & 1 deletion 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, 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 @@ -1314,6 +1314,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 @@ -1326,6 +1336,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, 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 @@ -70,7 +70,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 childBuilder = ObjectNode.builder();
addObjectNode(childBuilder, child);
parentBuilder.addObject(child.name(), childBuilder.build());
}
case LIST -> {
ListNode.Builder childBuilder = ListNode.builder();
addListNode(childBuilder, child);
parentBuilder.addList(child.name(), childBuilder.build());
}
case VALUE -> {
parentBuilder.addValue(child.name(), child.asString().get());
}
default -> {
// do nothing
}
}
}
});
}

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

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 @@ -53,6 +53,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 @@ -67,6 +68,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 @@ -79,6 +81,7 @@ class ProviderImpl implements Config.Context {
this.lastConfig = (AbstractConfigImpl) Config.empty();

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

Expand Down Expand Up @@ -152,36 +155,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 @@ -248,6 +271,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
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 @@ -67,6 +67,7 @@ public void testBuildDefault() {
eq(true), //cachingEnabled
notNull(), //changesExecutor
eq(true), //keyResolving
eq(false), // fail when key resolving cannot find ref
isNull() //aliasGenerator
);
}
Expand All @@ -91,6 +92,7 @@ public void testBuildCustomChanges() {
eq(true), //cachingEnabled
eq(myExecutor), //changesExecutor
eq(true), //keyResolving
eq(false), // fail when key resolving cannot find ref
isNull() //aliasGenerator
);
}
Expand All @@ -116,6 +118,7 @@ public void testBuildDisableKeyResolving() {
eq(true), //cachingEnabled
eq(myExecutor), //changesExecutor
eq(false), //keyResolving
eq(false), // fail when key resolving cannot find ref
isNull() //aliasGenerator
);
}
Expand Down
Loading