-
Notifications
You must be signed in to change notification settings - Fork 566
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
MicroProfile Config 3.0 #3644
MicroProfile Config 3.0 #3644
Changes from all commits
77df674
3f79945
129cfb9
36d88e5
1989dd3
fb6bbbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* Copyright (c) 2021 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.mp; | ||
|
||
import org.eclipse.microprofile.config.ConfigValue; | ||
|
||
record ConfigValueImpl(String name, | ||
String value, | ||
String rawValue, | ||
String sourceName, | ||
int sourceOrdinal) implements ConfigValue { | ||
|
||
@Override | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public String getValue() { | ||
return value; | ||
} | ||
|
||
@Override | ||
public String getRawValue() { | ||
return rawValue; | ||
} | ||
|
||
@Override | ||
public String getSourceName() { | ||
return sourceName; | ||
} | ||
|
||
@Override | ||
public int getSourceOrdinal() { | ||
return sourceOrdinal; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,18 +47,16 @@ | |
|
||
/** | ||
* Implementation of the basic MicroProfile {@link org.eclipse.microprofile.config.Config} API. | ||
* @deprecated This is an internal class that was exposed accidentaly. It will be package local in next major release. | ||
*/ | ||
@Deprecated | ||
public class MpConfigImpl implements Config { | ||
class MpConfigImpl implements Config { | ||
private static final Logger LOGGER = Logger.getLogger(MpConfigImpl.class.getName()); | ||
// for references resolving | ||
// matches string between ${ } with a negative lookbehind if there is not backslash | ||
private static final String REGEX_REFERENCE = "(?<!\\\\)\\$\\{([^}]+)\\}"; | ||
private static final String REGEX_REFERENCE = "(?<!\\\\)\\$\\{([^${}:]+)(:[^$}]*)?}"; | ||
private static final Pattern PATTERN_REFERENCE = Pattern.compile(REGEX_REFERENCE); | ||
// for encoding backslashes | ||
// matches a backslash with a positive lookahead if it is the backslash that encodes ${} | ||
private static final String REGEX_BACKSLASH = "\\\\(?=\\$\\{([^}]+)\\})"; | ||
private static final String REGEX_BACKSLASH = "\\\\(?=\\$\\{([^}]+)})"; | ||
private static final Pattern PATTERN_BACKSLASH = Pattern.compile(REGEX_BACKSLASH); | ||
// I only care about unresolved key happening within the same thread | ||
private static final ThreadLocal<Set<String>> UNRESOLVED_KEYS = ThreadLocal.withInitial(HashSet::new); | ||
|
@@ -94,7 +92,8 @@ public class MpConfigImpl implements Config { | |
this.converters.putIfAbsent(String.class, value -> value); | ||
this.configProfile = profile; | ||
|
||
this.valueResolving = getOptionalValue("helidon.config.value-resolving.enabled", Boolean.class) | ||
this.valueResolving = getOptionalValue("mp.config.property.expressions.enabled", Boolean.class) | ||
.or(() -> getOptionalValue("helidon.config.value-resolving.enabled", Boolean.class)) | ||
.orElse(true); | ||
|
||
// we need to initialize the filters first, before we set up filters | ||
|
@@ -107,11 +106,15 @@ public class MpConfigImpl implements Config { | |
}); | ||
} | ||
|
||
// TODO 3.0.0-JAKARTA | ||
@Override | ||
public ConfigValue getConfigValue(String s) { | ||
// TODO requires implementation of latest config version | ||
return null; | ||
public ConfigValue getConfigValue(String key) { | ||
if (configProfile == null) { | ||
return findConfigValue(key) | ||
.orElseGet(() -> new ConfigValueImpl(key, null, null, null, 0)); | ||
} | ||
return findConfigValue("%" + configProfile + "." + key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the need for all this String concatenation required by the spec for profiles? Seems like not the fastest option There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The specification for profile requires that you first attempt to find the configuration value by the config profile prefix (which implies string concatenation), and then (if not found) you use the original key. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caching is not an option, as MP Config requires the values to be always requested from config source(s) |
||
.or(() -> findConfigValue(key)) | ||
.orElseGet(() -> new ConfigValueImpl(key, null, null, null, 0)); | ||
} | ||
|
||
@Override | ||
|
@@ -140,7 +143,11 @@ private <T> Optional<T> optionalValue(String propertyName, Class<T> propertyType | |
// first try to see if we have a direct value | ||
Optional<String> optionalValue = getOptionalValue(propertyName, String.class); | ||
if (optionalValue.isPresent()) { | ||
return Optional.of((T) toArray(propertyName, optionalValue.get(), componentType)); | ||
try { | ||
return Optional.of((T) toArray(propertyName, optionalValue.get(), componentType)); | ||
} catch (NoSuchElementException e) { | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
/* | ||
|
@@ -181,8 +188,8 @@ private <T> Optional<T> optionalValue(String propertyName, Class<T> propertyType | |
return Optional.empty(); | ||
} | ||
} else { | ||
return getStringValue(propertyName) | ||
.flatMap(it -> applyFilters(propertyName, it)) | ||
return findConfigValue(propertyName) | ||
.map(ConfigValue::getValue) | ||
.map(it -> convert(propertyName, propertyType, it)); | ||
} | ||
} | ||
|
@@ -320,7 +327,7 @@ private <T> T convert(String propertyName, Class<T> type, String value) { | |
} | ||
} | ||
|
||
private Optional<String> getStringValue(String propertyName) { | ||
private Optional<ConfigValue> findConfigValue(String propertyName) { | ||
for (ConfigSource source : sources) { | ||
String value = source.getValue(propertyName); | ||
|
||
|
@@ -329,8 +336,22 @@ private Optional<String> getStringValue(String propertyName) { | |
continue; | ||
} | ||
|
||
LOGGER.finest("Found property " + propertyName + " in source " + source.getName()); | ||
return Optional.of(resolveReferences(propertyName, value)); | ||
if (value.isEmpty()) { | ||
if (LOGGER.isLoggable(Level.FINEST)) { | ||
LOGGER.finest("Found property " + propertyName | ||
+ " in source " + source.getName() | ||
+ " and it is empty (removed)"); | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
if (LOGGER.isLoggable(Level.FINEST)) { | ||
LOGGER.finest("Found property " + propertyName + " in source " + source.getName()); | ||
} | ||
String rawValue = value; | ||
return applyFilters(propertyName, value) | ||
.map(it -> resolveReferences(propertyName, it)) | ||
.map(it -> new ConfigValueImpl(propertyName, it, rawValue, source.getName(), source.getOrdinal())); | ||
} | ||
|
||
return Optional.empty(); | ||
|
@@ -365,32 +386,66 @@ private String resolveReferences(String key, String value) { | |
} | ||
if (!UNRESOLVED_KEYS.get().add(key)) { | ||
UNRESOLVED_KEYS.get().clear(); | ||
throw new IllegalStateException("Recursive resolving of references for key " + key + ", value: " + value); | ||
throw new IllegalArgumentException("Recursive resolving of references for key " + key + ", value: " + value); | ||
} | ||
try { | ||
return format(value); | ||
} catch (NoSuchElementException e) { | ||
LOGGER.log(Level.FINER, e, () -> String.format("Reference for key %s not found. Value: %s", key, value)); | ||
return value; | ||
return value.contains("${") ? processExpressions(value) : value; | ||
} finally { | ||
UNRESOLVED_KEYS.get().remove(key); | ||
} | ||
} | ||
|
||
private String processExpressions(String value) { | ||
if (value.equals("${EMPTY}")) { | ||
return ""; | ||
} | ||
|
||
int iteration = 0; | ||
String current; | ||
String replaced = value; | ||
do { | ||
if (iteration > 4) { | ||
throw new IllegalArgumentException("Too many iterations on property expression. Original value: " + value + ", " | ||
+ "currentValue: " + replaced); | ||
} | ||
current = replaced; | ||
replaced = format(current); | ||
iteration++; | ||
|
||
} while (!replaced.equals(current)); | ||
|
||
// remove all backslash that encodes ${...} | ||
Matcher m = PATTERN_BACKSLASH.matcher(replaced); | ||
return m.replaceAll(""); | ||
} | ||
|
||
private String format(String value) { | ||
Matcher m = PATTERN_REFERENCE.matcher(value); | ||
final StringBuffer sb = new StringBuffer(); | ||
while (m.find()) { | ||
String propertyName = m.group(1); | ||
Optional<String> propertyValue = getOptionalValue(propertyName, String.class); | ||
String defaultValue = m.group(2); | ||
String finalValue; | ||
if (defaultValue == null) { | ||
//the specification requires us to fail if property not found | ||
//finalValue = propertyValue.orElseGet(() -> "${" + propertyName + "}"); | ||
finalValue = propertyValue | ||
.orElseThrow(() -> new NoSuchElementException("Property " | ||
+ propertyName | ||
+ " used in expression " | ||
+ value | ||
+ " does not exist")); | ||
} else { | ||
// the capturing group captures the : separator, so let's only use the value after it | ||
finalValue = propertyValue.orElse(defaultValue.substring(1)); | ||
} | ||
m.appendReplacement(sb, | ||
Matcher.quoteReplacement(getOptionalValue(propertyName, String.class) | ||
.orElseGet(() -> "${" + propertyName + "}"))); | ||
Matcher.quoteReplacement(finalValue)); | ||
} | ||
m.appendTail(sb); | ||
// remove all backslash that encodes ${...} | ||
m = PATTERN_BACKSLASH.matcher(sb.toString()); | ||
|
||
return m.replaceAll(""); | ||
return sb.toString(); | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
|
@@ -476,11 +531,20 @@ HashMap<Class<?>, Converter<?>> converters() { | |
static String[] toArray(String stringValue) { | ||
String[] values = SPLIT_PATTERN.split(stringValue, -1); | ||
|
||
for (int i = 0; i < values.length; i++) { | ||
String value = values[i]; | ||
values[i] = ESCAPED_COMMA_PATTERN.matcher(value).replaceAll(Matcher.quoteReplacement(",")); | ||
List<String> result = new ArrayList<>(values.length); | ||
|
||
for (String s : values) { | ||
String value = ESCAPED_COMMA_PATTERN.matcher(s).replaceAll(Matcher.quoteReplacement(",")); | ||
if (!value.isEmpty()) { | ||
result.add(value); | ||
} | ||
} | ||
return values; | ||
|
||
if (result.isEmpty()) { | ||
throw new NoSuchElementException("Value " + stringValue + " resolved into an empty array"); | ||
} | ||
|
||
return result.toArray(new String[0]); | ||
} | ||
|
||
private static class FailingConverter<T> implements Converter<T> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (LOGGER.isLoggable(Level.FINEST)) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed (also it is fine not finest). If the string is a constant, there is not much overhead doing it this way. The guarding of logs statements should only be used when passing variables to output