+ * By default, will periodically reload the configuration if it has changed.
+ */
+public final class AwsAppConfigPlugin implements ConfigurationSource {
+
+ private static final System.Logger log = System.getLogger("io.avaje.config.AwsAppConfig");
+
+ private Loader loader;
+
+ @Override
+ public void load(Configuration configuration) {
+ if (!configuration.enabled("aws.appconfig.enabled", true)) {
+ log.log(INFO, "AwsAppConfig plugin is disabled");
+ return;
+ }
+ loader = new Loader(configuration);
+ loader.reload();
+ }
+
+ @Override
+ public void reload() {
+ if (loader != null) {
+ loader.reload();
+ }
+ }
+
+ static final class Loader {
+
+ private final Configuration configuration;
+ private final AppConfigFetcher fetcher;
+ private final ConfigParser yamlParser;
+ private final ConfigParser propertiesParser;
+ private final ReentrantLock lock = new ReentrantLock();
+ private final AtomicReference validUntil;
+ private final long nextRefreshSeconds;
+
+ private String currentVersion = "none";
+
+ Loader(Configuration configuration) {
+ this.validUntil = new AtomicReference<>(Instant.now().minusSeconds(1));
+ this.configuration = configuration;
+ this.propertiesParser = configuration.parser("properties").orElseThrow();
+ this.yamlParser = configuration.parser("yaml").orElse(null);
+ if (yamlParser == null) {
+ log.log(WARNING, "No Yaml parser registered");
+ }
+
+ var app = configuration.get("aws.appconfig.application");
+ var env = configuration.get("aws.appconfig.environment");
+ var con = configuration.get("aws.appconfig.configuration", env + "-" + app);
+
+ this.fetcher = AppConfigFetcher.builder()
+ .application(app)
+ .environment(env)
+ .configuration(con)
+ .build();
+
+ boolean pollEnabled = configuration.enabled("aws.appconfig.poll.enabled", true);
+ long pollSeconds = configuration.getLong("aws.appconfig.poll.seconds", 45L);
+ this.nextRefreshSeconds = configuration.getLong("aws.appconfig.refresh.seconds", pollSeconds - 1);
+ if (pollEnabled) {
+ configuration.schedule(pollSeconds * 1000L, pollSeconds * 1000L, this::reload);
+ }
+ }
+
+ void reload() {
+ if (reloadRequired()) {
+ performReload();
+ }
+ }
+
+ private boolean reloadRequired() {
+ return validUntil.get().isAfter(Instant.now());
+ }
+
+ private void performReload() {
+ lock.lock();
+ try {
+ if (!reloadRequired()) {
+ return;
+ }
+ AppConfigFetcher.Result result = fetcher.fetch();
+ if (currentVersion.equals(result.version())) {
+ log.log(TRACE, "AwsAppConfig unchanged, version {0}", currentVersion);
+ } else {
+ String contentType = result.contentType();
+ if (log.isLoggable(TRACE)) {
+ log.log(TRACE, "AwsAppConfig fetched version:{0} contentType:{1} body:{2}", result.version(), contentType, result.body());
+ }
+ Map keyValues = parse(result);
+ configuration.eventBuilder("AwsAppConfig")
+ .putAll(keyValues)
+ .publish();
+ currentVersion = result.version();
+ debugLog(result, keyValues.size());
+ }
+ // move the next valid until time
+ validUntil.set(Instant.now().plusSeconds(nextRefreshSeconds));
+
+ } catch (Exception e) {
+ log.log(ERROR, "Error fetching or processing AwsAppConfig", e);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private Map parse(AppConfigFetcher.Result result) {
+ ConfigParser parser = parser(result.contentType());
+ return parser.load(new StringReader(result.body()));
+ }
+
+ private ConfigParser parser(String contentType) {
+ if (contentType.endsWith("yaml")) {
+ return yamlParser;
+ } else {
+ return propertiesParser;
+ }
+ }
+
+ private static void debugLog(AppConfigFetcher.Result result, int size) {
+ if (log.isLoggable(DEBUG)) {
+ log.log(DEBUG, "AwsAppConfig loaded version {0} with {1} properties", result.version(), size);
+ }
+ }
+ }
+}
diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DAppConfigFetcher.java
similarity index 98%
rename from avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java
rename to avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DAppConfigFetcher.java
index 215ffb2..8281903 100644
--- a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DAppConfigFetcher.java
@@ -1,4 +1,4 @@
-package io.avaje.aws.appconfig;
+package io.avaje.config.awsappconfig;
import java.io.IOException;
import java.net.URI;
diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DResult.java
similarity index 81%
rename from avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java
rename to avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DResult.java
index 5b85817..f387e75 100644
--- a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/DResult.java
@@ -1,11 +1,12 @@
-package io.avaje.aws.appconfig;
+package io.avaje.config.awsappconfig;
final class DResult implements AppConfigFetcher.Result {
private final String version;
private final String contentType;
private final String body;
- public DResult(String version, String contentType, String body) {
+
+ DResult(String version, String contentType, String body) {
this.version = version;
this.contentType = contentType;
this.body = body;
diff --git a/avaje-aws-appconfig/src/main/java/module-info.java b/avaje-aws-appconfig/src/main/java/module-info.java
index 89a56e9..d30d949 100644
--- a/avaje-aws-appconfig/src/main/java/module-info.java
+++ b/avaje-aws-appconfig/src/main/java/module-info.java
@@ -1,10 +1,10 @@
-import io.avaje.aws.appconfig.AppConfigPlugin;
+import io.avaje.config.awsappconfig.AwsAppConfigPlugin;
-module io.avaje.aws.appconfig {
+module io.avaje.config.awsappconfig {
- exports io.avaje.aws.appconfig;
+ exports io.avaje.config.awsappconfig;
requires io.avaje.config;
requires java.net.http;
- provides io.avaje.config.ConfigurationSource with AppConfigPlugin;
+ provides io.avaje.config.ConfigurationSource with AwsAppConfigPlugin;
}
diff --git a/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource b/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource
index fd4b517..53b7519 100644
--- a/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource
+++ b/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource
@@ -1 +1 @@
-io.avaje.aws.appconfig.AppConfigPlugin
+io.avaje.config.awsappconfig.AwsAppConfigPlugin
diff --git a/avaje-config/src/main/java/io/avaje/config/Configuration.java b/avaje-config/src/main/java/io/avaje/config/Configuration.java
index 0f87fb2..a90196b 100644
--- a/avaje-config/src/main/java/io/avaje/config/Configuration.java
+++ b/avaje-config/src/main/java/io/avaje/config/Configuration.java
@@ -482,6 +482,15 @@ default boolean enabled(String key, boolean enabledDefault) {
*/
void loadIntoSystemProperties();
+ /**
+ * Trigger a {@link ConfigurationSource#reload()} on all the sources.
+ *
+ * Generally configuration sources will schedule a periodic refresh of their
+ * configuration but there are cases like Lambda where it can be useful to
+ * trigger a refresh explicitly (e.g. on Lambda invocation).
+ */
+ void reloadSources();
+
/**
* Return the number of configuration properties.
*/
diff --git a/avaje-config/src/main/java/io/avaje/config/ConfigurationSource.java b/avaje-config/src/main/java/io/avaje/config/ConfigurationSource.java
index 245870f..de0802d 100644
--- a/avaje-config/src/main/java/io/avaje/config/ConfigurationSource.java
+++ b/avaje-config/src/main/java/io/avaje/config/ConfigurationSource.java
@@ -23,4 +23,15 @@ public interface ConfigurationSource {
* @param configuration The configuration with initially properties.
*/
void load(Configuration configuration);
+
+ /**
+ * Explicitly reload the configuration source.
+ *
+ * Generally the configuration source will schedule a periodic refresh of its
+ * configuration but there are cases like Lambda where it can be useful to
+ * trigger a refresh explicitly and manually (e.g. on Lambda invocation).
+ */
+ default void reload() {
+ // do nothing by default
+ }
}
diff --git a/avaje-config/src/main/java/io/avaje/config/CoreComponents.java b/avaje-config/src/main/java/io/avaje/config/CoreComponents.java
new file mode 100644
index 0000000..339b523
--- /dev/null
+++ b/avaje-config/src/main/java/io/avaje/config/CoreComponents.java
@@ -0,0 +1,42 @@
+package io.avaje.config;
+
+import java.util.Collections;
+import java.util.List;
+
+final class CoreComponents {
+
+ private final ModificationEventRunner runner;
+ private final ConfigurationLog log;
+ private final Parsers parsers;
+ private final List sources;
+
+ CoreComponents(ModificationEventRunner runner, ConfigurationLog log, Parsers parsers, List sources) {
+ this.runner = runner;
+ this.log = log;
+ this.parsers = parsers;
+ this.sources = sources;
+ }
+
+ CoreComponents() {
+ this.runner = new CoreConfiguration.ForegroundEventRunner();
+ this.log = new DefaultConfigurationLog();
+ this.parsers = new Parsers();
+ this.sources = Collections.emptyList();
+ }
+
+ Parsers parsers() {
+ return parsers;
+ }
+
+ ConfigurationLog log() {
+ return log;
+ }
+
+ ModificationEventRunner runner() {
+ return runner;
+ }
+
+ List sources() {
+ return sources;
+ }
+}
diff --git a/avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java b/avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java
index 85a455f..5e3bf00 100644
--- a/avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java
+++ b/avaje-config/src/main/java/io/avaje/config/CoreConfiguration.java
@@ -18,6 +18,7 @@
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;
/**
@@ -35,16 +36,18 @@ final class CoreConfiguration implements Configuration {
private final CoreListValue listValue;
private final CoreSetValue setValue;
private final ModificationEventRunner eventRunner;
+ private final List sources;
private boolean loadedSystemProperties;
private FileWatch watcher;
private Timer timer;
private final String pathPrefix;
- CoreConfiguration(Parsers parsers, ModificationEventRunner eventRunner, ConfigurationLog log, CoreEntry.CoreMap entries) {
- this.parsers = parsers;
- this.eventRunner = eventRunner;
- this.log = log;
+ CoreConfiguration(CoreComponents components, CoreEntry.CoreMap entries) {
+ this.parsers = components.parsers();
+ this.eventRunner = components.runner();
+ this.log = components.log();
+ this.sources = components.sources();
this.properties = new ModifyAwareProperties(entries);
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
@@ -55,6 +58,7 @@ final class CoreConfiguration implements Configuration {
this.parsers = parent.parsers;
this.eventRunner = parent.eventRunner;
this.log = parent.log;
+ this.sources = parent.sources;
this.properties = new ModifyAwareProperties(entries);
this.listValue = new CoreListValue(this);
this.setValue = new CoreSetValue(this);
@@ -65,7 +69,7 @@ final class CoreConfiguration implements Configuration {
* For testing purposes.
*/
CoreConfiguration(CoreEntry.CoreMap entries) {
- this(new Parsers(), new ForegroundEventRunner(), new DefaultConfigurationLog(), entries);
+ this(new CoreComponents(), entries);
}
/**
@@ -107,7 +111,7 @@ void initSystemProperties() {
}
private void loadSources(Set names) {
- for (final ConfigurationSource source : ServiceLoader.load(ConfigurationSource.class)) {
+ for (ConfigurationSource source : sources) {
source.load(this);
names.add("ConfigurationSource:" + source.getClass().getCanonicalName());
}
@@ -173,6 +177,13 @@ public void loadIntoSystemProperties() {
loadedSystemProperties = true;
}
+ @Override
+ public void reloadSources() {
+ for (ConfigurationSource source : sources) {
+ source.reload();
+ }
+ }
+
@Override
public Properties asProperties() {
return properties.asProperties();
@@ -384,12 +395,12 @@ private void applyChangesAndPublish(CoreEventBuilder eventBuilder) {
@Override
public void onChange(Consumer eventListener, String... keys) {
- listeners.add(new CoreListener(eventListener, keys));
+ listeners.add(new CoreListener(log, eventListener, keys));
}
private OnChangeListener onChange(String key) {
requireNonNull(key, "key is required");
- return callbacks.computeIfAbsent(key, s -> new OnChangeListener());
+ return callbacks.computeIfAbsent(key, s -> new OnChangeListener(log));
}
@Override
@@ -433,15 +444,24 @@ public void clearProperty(String key) {
private static class OnChangeListener {
+ private final ConfigurationLog log;
private final List> callbacks = new ArrayList<>();
+ OnChangeListener(ConfigurationLog log) {
+ this.log = log;
+ }
+
void register(Consumer callback) {
callbacks.add(callback);
}
void fireOnChange(String value) {
for (Consumer callback : callbacks) {
- callback.accept(value);
+ try {
+ callback.accept(value);
+ } catch (Exception e) {
+ log.log(ERROR, "Error during onChange notification", e);
+ }
}
}
}
diff --git a/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java b/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java
index d16887c..1066fdc 100644
--- a/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java
+++ b/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java
@@ -1,9 +1,7 @@
package io.avaje.config;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Properties;
-import java.util.ServiceLoader;
+import java.util.*;
+import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
@@ -76,11 +74,16 @@ public Configuration build() {
final var runner = initRunner();
final var log = initLog();
final var parsers = new Parsers();
+ final var sources = ServiceLoader.load(ConfigurationSource.class).stream()
+ .map(ServiceLoader.Provider::get)
+ .collect(Collectors.toList());
+
+ var components = new CoreComponents(runner, log, parsers, sources);
if (includeResourceLoading) {
log.preInitialisation();
- initialLoader = new InitialLoader(parsers, log, initResourceLoader());
+ initialLoader = new InitialLoader(components, initResourceLoader());
}
- return new CoreConfiguration(parsers, runner, log, initEntries()).postLoad(initialLoader);
+ return new CoreConfiguration(components, initEntries()).postLoad(initialLoader);
}
private CoreEntry.CoreMap initEntries() {
diff --git a/avaje-config/src/main/java/io/avaje/config/CoreListener.java b/avaje-config/src/main/java/io/avaje/config/CoreListener.java
index 833456b..56b24e4 100644
--- a/avaje-config/src/main/java/io/avaje/config/CoreListener.java
+++ b/avaje-config/src/main/java/io/avaje/config/CoreListener.java
@@ -2,22 +2,30 @@
import java.util.function.Consumer;
+import static java.lang.System.Logger.Level.ERROR;
+
/**
* Wraps the listener taking the interesting keys into account.
*/
final class CoreListener {
+ private final ConfigurationLog log;
private final Consumer listener;
private final String[] keys;
- CoreListener(Consumer listener, String[] keys) {
+ CoreListener(ConfigurationLog log, Consumer listener, String[] keys) {
+ this.log = log;
this.listener = listener;
this.keys = keys;
}
void accept(CoreModificationEvent event) {
if (keys == null || keys.length == 0 || containsKey(event)) {
- listener.accept(event);
+ try {
+ listener.accept(event);
+ } catch (Exception e) {
+ log.log(ERROR, "Error during onChange notification", e);
+ }
}
}
diff --git a/avaje-config/src/main/java/io/avaje/config/InitialLoader.java b/avaje-config/src/main/java/io/avaje/config/InitialLoader.java
index 58d6c30..f6ab1b5 100644
--- a/avaje-config/src/main/java/io/avaje/config/InitialLoader.java
+++ b/avaje-config/src/main/java/io/avaje/config/InitialLoader.java
@@ -41,9 +41,9 @@ enum Source {
private final Set profileResourceLoaded = new HashSet<>();
private final Parsers parsers;
- InitialLoader(Parsers parsers, ConfigurationLog log, ResourceLoader resourceLoader) {
- this.parsers = parsers;
- this.log = log;
+ InitialLoader(CoreComponents components, ResourceLoader resourceLoader) {
+ this.parsers = components.parsers();
+ this.log = components.log();
this.loadContext = new InitialLoadContext(log, resourceLoader);
}
diff --git a/avaje-config/src/main/java/io/avaje/config/Parsers.java b/avaje-config/src/main/java/io/avaje/config/Parsers.java
index 4eb0037..a24dd08 100644
--- a/avaje-config/src/main/java/io/avaje/config/Parsers.java
+++ b/avaje-config/src/main/java/io/avaje/config/Parsers.java
@@ -13,6 +13,7 @@ final class Parsers {
private final Map parserMap = new HashMap<>();
Parsers() {
+ parserMap.put("properties", new PropertiesParser());
if (!"true".equals(System.getProperty("skipYaml"))) {
initYamlParser();
}
diff --git a/avaje-config/src/main/java/io/avaje/config/PropertiesParser.java b/avaje-config/src/main/java/io/avaje/config/PropertiesParser.java
new file mode 100644
index 0000000..1404b7f
--- /dev/null
+++ b/avaje-config/src/main/java/io/avaje/config/PropertiesParser.java
@@ -0,0 +1,54 @@
+package io.avaje.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+final class PropertiesParser implements ConfigParser {
+
+ private static final String[] extensions = new String[]{"properties"};
+
+ @Override
+ public String[] supportedExtensions() {
+ return extensions;
+ }
+
+ @Override
+ public Map load(Reader reader) {
+ try {
+ Properties p = new Properties();
+ p.load(reader);
+ return toMap(p);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public Map load(InputStream is) {
+ try {
+ Properties p = new Properties();
+ p.load(is);
+ return toMap(p);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static Map toMap(Properties p) {
+ Map result = new LinkedHashMap<>();
+ Set> entries = p.entrySet();
+ for (Map.Entry