Skip to content

Commit

Permalink
Generated dotted properties from Env before other interceptors, so th…
Browse files Browse the repository at this point in the history
…e properties can be modified by profiles, relocates, etc. (#987)
  • Loading branch information
radcortez committed Aug 30, 2023
1 parent 87adb2c commit c519eb7
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.smallrye.config;

import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.microprofile.config.spi.ConfigSource;

/**
* This interceptor adds additional entries to {@link org.eclipse.microprofile.config.Config#getPropertyNames}.
*/
Expand All @@ -12,6 +18,11 @@ class PropertyNamesConfigSourceInterceptor implements ConfigSourceInterceptor {

private final Set<String> properties = new HashSet<>();

public PropertyNamesConfigSourceInterceptor(final ConfigSourceInterceptorContext context,
final List<ConfigSource> sources) {
this.properties.addAll(generateDottedProperties(context, sources));
}

@Override
public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) {
return context.proceed(name);
Expand All @@ -31,4 +42,61 @@ public Iterator<String> iterateNames(final ConfigSourceInterceptorContext contex
void addProperties(final Set<String> properties) {
this.properties.addAll(properties);
}

/**
* Generate dotted properties from Env properties.
* <br>
* These are required when a consumer relies on the list of properties to find additional
* configurations. The list of properties is not normalized due to environment variables, which follow specific
* naming rules. The MicroProfile Config specification defines a set of conversion rules to look up and find
* values from environment variables even when using their dotted version, but this does not apply to the
* properties list.
* <br>
* Because an environment variable name may only be represented by a subset of characters, it is not possible
* to represent exactly a dotted version name from an environment variable name. Additional dotted properties
* mapped from environment variables are only added if a relationship cannot be found between all properties
* using the conversions look up rules of the MicroProfile Config specification. Example:
* <br>
* If <code>foo.bar</code> is present and <code>FOO_BAR</code> is also present, no property is required.
* If <code>foo-bar</code> is present and <code>FOO_BAR</code> is also present, no property is required.
* If <code>FOO_BAR</code> is present a property <code>foo.bar</code> is required.
*/
private static Set<String> generateDottedProperties(final ConfigSourceInterceptorContext current,
final List<ConfigSource> sources) {
// Collect all known properties
Set<String> properties = new HashSet<>();
Iterator<String> iterateNames = current.iterateNames();
while (iterateNames.hasNext()) {
properties.add(iterateNames.next());
}

// Collect only properties from the EnvSources
Set<String> envProperties = new HashSet<>();
for (ConfigSource source : sources) {
if (source instanceof EnvConfigSource) {
envProperties.addAll(source.getPropertyNames());
}
}
properties.removeAll(envProperties);

// Collect properties that have the same semantic meaning
Set<String> overrides = new HashSet<>();
for (String property : properties) {
String semanticProperty = replaceNonAlphanumericByUnderscores(property);
for (String envProperty : envProperties) {
if (envProperty.equalsIgnoreCase(semanticProperty)) {
overrides.add(envProperty);
break;
}
}
}

// Remove them - Remaining properties can only be found in the EnvSource - generate a dotted version
envProperties.removeAll(overrides);
Set<String> dottedProperties = new HashSet<>();
for (String envProperty : envProperties) {
dottedProperties.add(toLowerCaseAndDotted(envProperty));
}
return dottedProperties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
package io.smallrye.config;

import static io.smallrye.config.ConfigSourceInterceptor.EMPTY;
import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores;
import static io.smallrye.config.common.utils.StringUtil.toLowerCaseAndDotted;

import java.io.ObjectStreamException;
import java.io.Serializable;
Expand Down Expand Up @@ -553,22 +551,21 @@ private static class ConfigSources implements Serializable {
List<String> profiles = getProfiles(interceptors);
List<ConfigSourceWithPriority> sourcesWithPriorities = mapLateSources(sources, interceptors, current, profiles,
builder);
List<ConfigSource> configSources = getSources(sourcesWithPriorities);

// Rebuild the chain with the late sources and new instances of the interceptors
// The new instance will ensure that we get rid of references to factories and other stuff and keep only
// the resolved final source or interceptor to use.
current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null);
current = new SmallRyeConfigSourceInterceptorContext(new SmallRyeConfigSources(sourcesWithPriorities), current);
PropertyNamesConfigSourceInterceptor propertyNamesInterceptor = new PropertyNamesConfigSourceInterceptor(current,
configSources);
current = new SmallRyeConfigSourceInterceptorContext(propertyNamesInterceptor, current);
for (ConfigSourceInterceptor interceptor : interceptors) {
current = new SmallRyeConfigSourceInterceptorContext(interceptor, current);
}

// PropertyNames and generate additional properties
List<ConfigSource> configSources = getSources(sourcesWithPriorities);
PropertyNamesConfigSourceInterceptor propertyNamesInterceptor = new PropertyNamesConfigSourceInterceptor();
current = new SmallRyeConfigSourceInterceptorContext(propertyNamesInterceptor, current);
PropertyNames propertyNames = new PropertyNames(propertyNamesInterceptor);
propertyNames.add(generateDottedProperties(configSources, current));

this.profiles = profiles;
this.sources = configSources;
Expand Down Expand Up @@ -714,63 +711,6 @@ private static List<ConfigurableConfigSource> getConfigurableSources(final List<
return Collections.unmodifiableList(configurableConfigSources);
}

/**
* Generate dotted properties from Env properties.
* <br>
* These are required when a consumer relies on the list of properties to find additional
* configurations. The list of properties is not normalized due to environment variables, which follow specific
* naming rules. The MicroProfile Config specification defines a set of conversion rules to look up and find
* values from environment variables even when using their dotted version, but this does not apply to the
* properties list.
* <br>
* Because an environment variable name may only be represented by a subset of characters, it is not possible
* to represent exactly a dotted version name from an environment variable name. Additional dotted properties
* mapped from environment variables are only added if a relationship cannot be found between all properties
* using the conversions look up rules of the MicroProfile Config specification. Example:
* <br>
* If <code>foo.bar</code> is present and <code>FOO_BAR</code> is also present, no property is required.
* If <code>foo-bar</code> is present and <code>FOO_BAR</code> is also present, no property is required.
* If <code>FOO_BAR</code> is present a property <code>foo.bar</code> is required.
*/
private static Set<String> generateDottedProperties(final List<ConfigSource> sources,
final SmallRyeConfigSourceInterceptorContext current) {
// Collect all known properties
Set<String> properties = new HashSet<>();
Iterator<String> iterateNames = current.iterateNames();
while (iterateNames.hasNext()) {
properties.add(iterateNames.next());
}

// Collect only properties from the EnvSources
Set<String> envProperties = new HashSet<>();
for (ConfigSource source : sources) {
if (source instanceof EnvConfigSource) {
envProperties.addAll(source.getPropertyNames());
}
}
properties.removeAll(envProperties);

// Collect properties that have the same semantic meaning
Set<String> overrides = new HashSet<>();
for (String property : properties) {
String semanticProperty = replaceNonAlphanumericByUnderscores(property);
for (String envProperty : envProperties) {
if (envProperty.equalsIgnoreCase(semanticProperty)) {
overrides.add(envProperty);
break;
}
}
}

// Remove them - Remaining properties can only be found in the EnvSource - generate a dotted version
envProperties.removeAll(overrides);
Set<String> dottedProperties = new HashSet<>();
for (String envProperty : envProperties) {
dottedProperties.add(toLowerCaseAndDotted(envProperty));
}
return dottedProperties;
}

List<String> getProfiles() {
return profiles;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2307,4 +2307,26 @@ void mapKeyQuotes() {
interface MapKeyQuotes {
Map<String, String> values();
}

@Test
void mapWithEnvVarsOnlyInProfile() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(config("map.env.one", "one", "%dev.map.env.two", "two"))
.withSources(new EnvConfigSource(Map.of("_DEV_MAP_ENV_THREE", "3"), 100))
.withMapping(MapWithEnvVarsOnlyInProfile.class)
.withProfile("dev")
.build();

MapWithEnvVarsOnlyInProfile mapping = config.getConfigMapping(MapWithEnvVarsOnlyInProfile.class);

assertEquals("one", mapping.map().get("one"));
assertEquals("two", mapping.map().get("two"));
assertEquals("3", mapping.map().get("three"));
}

@ConfigMapping(prefix = "map.env")
interface MapWithEnvVarsOnlyInProfile {
@WithParentName
Map<String, String> map();
}
}

0 comments on commit c519eb7

Please sign in to comment.