Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ static ConfigServiceLoader get() {
private final List<ConfigurationSource> sources = new ArrayList<>();
private final List<ConfigurationPlugin> plugins = new ArrayList<>();
private final Parsers parsers;
private final ConfigurationFallback fallback;

ConfigServiceLoader() {
ModificationEventRunner _eventRunner = null;
ConfigurationLog _log = null;
ResourceLoader _resourceLoader = null;
ConfigurationFallback _fallback = null;
List<ConfigParser> otherParsers = new ArrayList<>();

for (var spi : ServiceLoader.load(ConfigExtension.class)) {
Expand All @@ -42,13 +44,16 @@ static ConfigServiceLoader get() {
_resourceLoader = (ResourceLoader) spi;
} else if (spi instanceof ModificationEventRunner) {
_eventRunner = (ModificationEventRunner) spi;
} else if (spi instanceof ConfigurationFallback) {
_fallback = (ConfigurationFallback) spi;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a good idea to either fail if there are two instances of ConfigurationFallback configured; otherwise the behaviour may become unpredictable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a use case where for Tests, it would be useful to use an alternative ConfigurationFallback.

In this case the test configuration would have values like ssm://test/myapp/some_key_of_secret

... and if a value had that special prefix it would load the value from the AWS secret store. You need aws single signon to be able to do this of course. This is to assist 2 scenarios which is (1) running locally against Dev/Test and (2) Integration tests

I think the override feature would actually support this and ideally this is only done for Testing, so the override implementation is only in maven test scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rbygrave ; I think it may be good to look into throwing in this scenario because otherwise the behaviour could be confusing and difficult to track down.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a use case where for Tests, it would be useful to use an alternative ConfigurationFallback.

So in this case there would be an instance of ConfigurationFallback from the main classpath and an instance from the test classpath? It may be worth logging to stderr (at least) that this is happening because in other situation a clash will be hard to comprehend.

}
}

this.log = _log == null ? new DefaultConfigurationLog() : _log;
this.resourceLoader = _resourceLoader == null ? new DefaultResourceLoader() : _resourceLoader;
this.eventRunner = _eventRunner == null ? new CoreConfiguration.ForegroundEventRunner() : _eventRunner;
this.parsers = new Parsers(otherParsers);
this.fallback = _fallback == null ? new DefaultFallback() : _fallback;
}

Parsers parsers() {
Expand All @@ -67,6 +72,10 @@ ModificationEventRunner eventRunner() {
return eventRunner;
}

ConfigurationFallback fallback() {
return fallback;
}

List<ConfigurationSource> sources() {
return sources;
}
Expand Down
29 changes: 29 additions & 0 deletions avaje-config/src/main/java/io/avaje/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,13 @@ interface Builder {
*/
Builder includeResourceLoading();

/**
* Defines the means by which the configuration is able to maybe fallback to a value if there is no configured value
* for a given key.
*/

Builder fallback(ConfigurationFallback fallback);

/**
* Build and return the Configuration.
* <p>
Expand All @@ -816,6 +823,13 @@ interface Builder {
*/
interface Entry {

/**
* Return an entry given the value and source.
*/
static Entry of(@Nullable String val, String source) {
return CoreEntry.of(val, source);
}

/**
* Return the source of the entry.
*/
Expand All @@ -827,5 +841,20 @@ interface Entry {
*/
@Nullable
String value();

/**
* Return true if the entry needs evaluation.
*/
boolean needsEvaluation();

/**
* Return true if the entry represents a null.
*/
boolean isNull();

/**
* Return the boolean value for the entry.
*/
boolean boolValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.avaje.config;

import io.avaje.config.Configuration.Entry;

import java.util.Optional;

/**
* Implementations of this class are able to define a fallback value when there is no
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if the name ConfigurationFallback is the best since it has a dual purpose of providing a fallback and an overrride. Maybe ConfigurationValueInterceptor or ConfigurationOverrideAndFallback might be clearer?

Even if this name is retained as-is, the class comments would be good to update so that they describe the role of the SPI interface.

* value available for a configuration key.
* <p>
* The default implementation uses System Properties and Environment variables.
*/
public interface ConfigurationFallback extends ConfigExtension {

/**
* Provides the ability to override the value that is going to be set.
* <p>
* By default, this just returns the value that is passed in and does not
* override the value.
*
* @param key The property key
* @param value The value that can be overridden by the returned value
* @param source The source of the key value pair
*/
default Entry overrideValue(String key, String value, String source) {
return Entry.of(value, source);
}

/**
* Return a value for the supplied key or {@code null} if there is no value.
*
* @param key The configuration key to get a fallback value for.
*/
default Optional<Entry> fallbackValue(String key) {
return Optional.empty();
}

}
15 changes: 14 additions & 1 deletion avaje-config/src/main/java/io/avaje/config/CoreComponents.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ final class CoreComponents {
private final ModificationEventRunner runner;
private final ConfigurationLog log;
private final Parsers parsers;
private final ConfigurationFallback fallback;
private final List<ConfigurationSource> sources;
private final List<ConfigurationPlugin> plugins;

CoreComponents(ModificationEventRunner runner, ConfigurationLog log, Parsers parsers, List<ConfigurationSource> sources, List<ConfigurationPlugin> plugins) {
CoreComponents(
ModificationEventRunner runner,
ConfigurationLog log,
Parsers parsers,
ConfigurationFallback fallback,
List<ConfigurationSource> sources,
List<ConfigurationPlugin> plugins) {
this.runner = runner;
this.log = log;
this.parsers = parsers;
this.sources = sources;
this.plugins = plugins;
this.fallback = fallback;
}

/** For testing only */
Expand All @@ -26,6 +34,7 @@ final class CoreComponents {
this.parsers = new Parsers(Collections.emptyList());
this.sources = Collections.emptyList();
this.plugins = Collections.emptyList();
this.fallback = new DefaultFallback();
}

Parsers parsers() {
Expand All @@ -40,6 +49,10 @@ ModificationEventRunner runner() {
return runner;
}

ConfigurationFallback fallback() {
return fallback;
}

List<ConfigurationSource> sources() {
return sources;
}
Expand Down
47 changes: 16 additions & 31 deletions avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.avaje.config;

import static io.avaje.config.Constants.SYSTEM_PROPS;
import static io.avaje.config.Constants.USER_PROVIDED_DEFAULT;
import static java.lang.System.Logger.Level.ERROR;
import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -50,7 +49,7 @@ final class CoreConfiguration implements Configuration {
this.log = components.log();
this.sources = components.sources();
this.plugins = components.plugins();
this.properties = new ModifyAwareProperties(entries);
this.properties = new ModifyAwareProperties(entries, components.fallback());
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
this.pathPrefix = "";
Expand All @@ -62,7 +61,7 @@ final class CoreConfiguration implements Configuration {
this.log = parent.log;
this.sources = parent.sources;
this.plugins = parent.plugins;
this.properties = new ModifyAwareProperties(entries);
this.properties = new ModifyAwareProperties(entries, parent.properties.fallback);
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
this.pathPrefix = prefix;
Expand Down Expand Up @@ -487,18 +486,16 @@ void fireOnChange(String value) {
}
}

static String toEnvKey(String key) {
return key.replace('.', '_').replace("-", "").toUpperCase();
}

private static class ModifyAwareProperties {

private final CoreEntry.CoreMap entries;
private final Configuration.ExpressionEval eval;
private final ConfigurationFallback fallback;

ModifyAwareProperties(CoreEntry.CoreMap entries) {
ModifyAwareProperties(CoreEntry.CoreMap entries, ConfigurationFallback fallback) {
this.entries = entries;
this.eval = new CoreExpressionEval(entries);
this.fallback = fallback;
}

int size() {
Expand All @@ -512,7 +509,7 @@ String eval(String value) {

@Nullable
String valueOrNull(String key) {
CoreEntry entry = entries.get(key);
Entry entry = entries.get(key);
return entry == null ? null : entry.value();
}

Expand All @@ -525,11 +522,11 @@ boolean getBool(String key, boolean defaultValue) {
return entry(key, String.valueOf(defaultValue)).boolValue();
}

CoreEntry entry(String key) {
Entry entry(String key) {
return _entry(key, null);
}

CoreEntry entry(String key, @Nullable String defaultValue) {
Entry entry(String key, @Nullable String defaultValue) {
return _entry(key, defaultValue);
}

Expand All @@ -539,18 +536,18 @@ CoreEntry entry(String key, @Nullable String defaultValue) {
*/
Optional<Entry> optionalEntry(String key) {
return Optional.ofNullable(entries.get(key))
.filter(entry -> !entry.isNull())
.map(entry -> entry);
.filter(entry -> !entry.isNull());
}

/**
* Get property with caching taking into account defaultValue and "null".
*/
private CoreEntry _entry(String key, @Nullable String defaultValue) {
CoreEntry value = entries.get(key);
private Entry _entry(String key, @Nullable String defaultValue) {
Entry value = entries.get(key);
if (value == null) {
// defining property at runtime with System property/ENV backing
value = defaultEntry(defaultValue, systemValue(key));
value = fallback.fallbackValue(key)
.or(() -> asDefault(defaultValue))
.orElse(CoreEntry.NULL_ENTRY);
entries.put(key, value);
} else if (value.isNull() && defaultValue != null) {
value = CoreEntry.of(defaultValue, USER_PROVIDED_DEFAULT);
Expand All @@ -559,20 +556,8 @@ private CoreEntry _entry(String key, @Nullable String defaultValue) {
return value;
}

private static CoreEntry defaultEntry(@Nullable String defaultValue, @Nullable String systemValue) {
if (systemValue != null) {
return CoreEntry.of(systemValue, SYSTEM_PROPS);
} else if (defaultValue != null) {
return CoreEntry.of(defaultValue, USER_PROVIDED_DEFAULT);
} else {
return CoreEntry.NULL_ENTRY;
}
}

@Nullable
private static String systemValue(String key) {
final String val = System.getProperty(key, System.getenv(key));
return val != null ? val : System.getenv(toEnvKey(key));
private Optional<Entry> asDefault(@Nullable String defaultValue) {
return defaultValue == null ? Optional.empty() : Optional.of(CoreEntry.of(defaultValue, USER_PROVIDED_DEFAULT));
}

void loadIntoSystemProperties(Set<String> excludedSet) {
Expand Down
Loading