Skip to content

Commit

Permalink
MicroProfile Config 3.0 (#3644)
Browse files Browse the repository at this point in the history
* MP Config upgrade
* Add placeholder for empty values in MP config
  • Loading branch information
tomas-langer authored Nov 22, 2021
1 parent aa65620 commit db35b26
Show file tree
Hide file tree
Showing 28 changed files with 866 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
#javax.sql.DataSource.test.dataSource.url=jdbc:h2:tcp://localhost:1521/test
javax.sql.DataSource.test.dataSource.user=sa
javax.sql.DataSource.test.dataSource.password=
javax.sql.DataSource.test.dataSource.password=${EMPTY}

# Microprofile server properties
server.port=8080
Expand Down
5 changes: 5 additions & 0 deletions config/config-mp/etc/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@
<Method name="create" />
<Bug pattern="URLCONNECTION_SSRF_FD" />
</Match>
<Match>
<!-- Regular expression is used only to parse application data, not external data -->
<Class name="io.helidon.config.mp.MpConfigImpl" />
<Bug pattern="REDOS" />
</Match>

</FindBugsFilter>
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
Expand Up @@ -79,8 +79,7 @@
/**
* Configuration builder.
*/
@Deprecated
public class MpConfigBuilder implements ConfigBuilder {
class MpConfigBuilder implements ConfigBuilder {
private static final Logger LOGGER = Logger.getLogger(MpConfigBuilder.class.getName());
private static final String DEFAULT_CONFIG_SOURCE = "META-INF/microprofile-config.properties";

Expand Down Expand Up @@ -474,6 +473,10 @@ public Config build() {
Collections.reverse(ordinalSources);
Collections.reverse(ordinalConverters);

if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("The following config sources are used (ordered): " + ordinalSources);
}

List<ConfigSource> targetSources = new LinkedList<>();
HashMap<Class<?>, Converter<?>> targetConverters = new HashMap<>();

Expand All @@ -484,6 +487,9 @@ public Config build() {

// if we already have a profile configured, we have loaded it and can safely return
if (profile != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Built MP config for profile " + profile);
}
return result;
}

Expand All @@ -492,7 +498,12 @@ public Config build() {

// nope, return the result
if (configuredProfile == null) {
LOGGER.fine("Built MP config with no profile");
return result;
} else {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("MP profile configured, rebuilding: " + configuredProfile);
}
}

// yes, update it and re-build with profile information
Expand Down Expand Up @@ -545,6 +556,10 @@ private void addDefaultSources(List<OrdinalSource> targetConfigSources) {
.map(OrdinalSource::new)
.forEach(targetConfigSources::add);
}

if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("The following default config sources discovered: " + targetConfigSources);
}
}
}

Expand Down Expand Up @@ -627,7 +642,7 @@ private OrdinalSource(ConfigSource source) {
}

private OrdinalSource(ConfigSource source, int ordinal) {
this.ordinal = ordinal;
this.ordinal = findOrdinal(source, ordinal);
this.source = source;
}

Expand Down
124 changes: 94 additions & 30 deletions config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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)
.or(() -> findConfigValue(key))
.orElseGet(() -> new ConfigValueImpl(key, null, null, null, 0));
}

@Override
Expand Down Expand Up @@ -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();
}
}

/*
Expand Down Expand Up @@ -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));
}
}
Expand Down Expand Up @@ -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);

Expand All @@ -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();
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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> {
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, 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.
Expand All @@ -16,6 +16,8 @@

package io.helidon.config.mp;

import java.util.NoSuchElementException;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -24,6 +26,7 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class MpConfigReferenceTest {
private static final String VALUE_1 = "value";
Expand Down Expand Up @@ -58,13 +61,9 @@ void testBoth() {

@Test
void testMissingRefs() {
String key = "referencing4-1";
String actual = config.getValue(key, String.class);
assertThat(actual, is("${missing}"));

key = "referencing4-2";
actual = config.getValue(key, String.class);
assertThat(actual, is("${missing}-value"));
// since Config 2.0, missing references must throw an exception
assertThrows(NoSuchElementException.class, () -> config.getValue("referencing4-1", String.class));
assertThrows(NoSuchElementException.class, () -> config.getValue( "referencing4-2", String.class));
}

private void test(String prefix, String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ db:
#
# url: "jdbc:h2:tcp://localhost:9092/~test"
# username: h2
# password: ""
# password: "${EMPTY}"
# poolName: h2
initializationFailTimeout: -1
connectionTimeout: 2000
Expand Down
Loading

0 comments on commit db35b26

Please sign in to comment.