diff --git a/avaje-aws-appconfig/pom.xml b/avaje-aws-appconfig/pom.xml
new file mode 100644
index 0000000..9744e7c
--- /dev/null
+++ b/avaje-aws-appconfig/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ org.avaje
+ java11-oss
+ 4.0
+
+
+
+ avaje-aws-appconfig
+ 0.1-SNAPSHOT
+
+
+ false
+
+
+
+
+ io.avaje
+ avaje-config
+ 3.11-SNAPSHOT
+ provided
+
+
+
+ io.avaje
+ junit
+ 1.3
+ test
+
+
+
+
diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java
new file mode 100644
index 0000000..1b2d447
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java
@@ -0,0 +1,40 @@
+package io.avaje.aws.appconfig;
+
+public interface AppConfigFetcher {
+
+ static AppConfigFetcher.Builder builder() {
+ return new DAppConfigFetcher.Builder();
+ }
+
+ Result fetch() throws FetchException;
+
+ class FetchException extends Exception {
+
+ public FetchException(Exception e) {
+ super(e);
+ }
+ }
+
+ interface Result {
+
+ String version();
+
+ String contentType();
+
+ String body();
+ }
+
+ interface Builder {
+
+ AppConfigFetcher.Builder application(String application);
+
+ AppConfigFetcher.Builder environment(String environment);
+
+ AppConfigFetcher.Builder configuration(String configuration);
+
+ AppConfigFetcher.Builder port(int port);
+
+ AppConfigFetcher build();
+ }
+
+}
diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java
new file mode 100644
index 0000000..5096053
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java
@@ -0,0 +1,103 @@
+package io.avaje.aws.appconfig;
+
+import io.avaje.config.ConfigParser;
+import io.avaje.config.Configuration;
+import io.avaje.config.ConfigurationSource;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Map;
+import java.util.Properties;
+
+import static java.lang.System.Logger.Level.*;
+
+public final class AppConfigPlugin implements ConfigurationSource {
+
+ private static final System.Logger log = System.getLogger("io.avaje.config.AppConfigPlugin");
+
+ @Override
+ public void load(Configuration configuration) {
+ if (!configuration.getBool("aws.appconfig.enabled", true)) {
+ log.log(INFO, "AppConfigPlugin is not enabled");
+ }
+
+ var loader = new Loader(configuration);
+ loader.schedule();
+ loader.reload();
+ }
+
+
+ static final class Loader {
+
+ private final Configuration configuration;
+ private final AppConfigFetcher fetcher;
+ private final ConfigParser yamlParser;
+ private final long frequency;
+
+ private String currentVersion = "";
+
+ Loader(Configuration configuration) {
+ this.configuration = configuration;
+ this.frequency = configuration.getLong("aws.appconfig.frequency", 60L);
+ this.yamlParser = configuration.parser("yaml").orElse(null);
+
+ var app = configuration.get("aws.appconfig.application");
+ var env = configuration.get("aws.appconfig.environment");
+ var con = configuration.get("aws.appconfig.configuration");
+
+ this.fetcher = AppConfigFetcher.builder()
+ .application(app)
+ .environment(env)
+ .configuration(con)
+ .build();
+ }
+
+ void schedule() {
+ configuration.schedule(frequency * 1000L, frequency * 1000L, this::reload);
+ }
+
+ void reload() {
+ try {
+ AppConfigFetcher.Result result = fetcher.fetch();
+ if (currentVersion.equals(result.version())) {
+ log.log(TRACE, "AwsAppConfig unchanged, version", 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());
+ }
+ if (contentType.endsWith("yaml")) {
+ if (yamlParser == null) {
+ log.log(ERROR, "No Yaml Parser registered to parse AWS AppConfig");
+ } else {
+ Map keyValues = yamlParser.load(new StringReader(result.body()));
+ configuration.eventBuilder("AwsAppConfig")
+ .putAll(keyValues)
+ .publish();
+ currentVersion = result.version();
+ debugLog(result, keyValues.size());
+ }
+ } else {
+ // assuming properties content
+ Properties properties = new Properties();
+ properties.load(new StringReader(result.body()));
+ configuration.eventBuilder("AwsAppConfig")
+ .putAll(properties)
+ .publish();
+ currentVersion = result.version();
+ debugLog(result, properties.size());
+ }
+ }
+
+ } catch (AppConfigFetcher.FetchException | IOException e) {
+ log.log(ERROR, "Error fetching or processing AppConfig", e);
+ }
+ }
+
+ 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/aws/appconfig/DAppConfigFetcher.java
new file mode 100644
index 0000000..215ffb2
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DAppConfigFetcher.java
@@ -0,0 +1,85 @@
+package io.avaje.aws.appconfig;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+final class DAppConfigFetcher implements AppConfigFetcher {
+
+ private final URI uri;
+ private final HttpClient httpClient;
+
+ DAppConfigFetcher(String uri) {
+ this.uri = URI.create(uri);
+ this.httpClient = HttpClient.newBuilder()
+ .build();
+ }
+
+ @Override
+ public AppConfigFetcher.Result fetch() throws FetchException {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(uri)
+ .GET()
+ .build();
+
+ try {
+ HttpResponse res = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ String version = res.headers().firstValue("Configuration-Version").orElse(null);
+ String contentType = res.headers().firstValue("Content-Type").orElse("unknown");
+ String body = res.body();
+ return new DResult(version, contentType, body);
+
+ } catch (IOException | InterruptedException e) {
+ throw new FetchException(e);
+ }
+ }
+
+ static class Builder implements AppConfigFetcher.Builder {
+
+ private int port = 2772;
+ private String application;
+ private String environment;
+ private String configuration;
+
+ @Override
+ public Builder application(String application) {
+ this.application = application;
+ return this;
+ }
+
+ @Override
+ public Builder environment(String environment) {
+ this.environment = environment;
+ return this;
+ }
+
+ @Override
+ public Builder configuration(String configuration) {
+ this.configuration = configuration;
+ return this;
+ }
+
+ @Override
+ public Builder port(int port) {
+ this.port = port;
+ return this;
+ }
+
+ @Override
+ public AppConfigFetcher build() {
+ return new DAppConfigFetcher(uri());
+ }
+
+ private String uri() {
+ if (configuration == null) {
+ configuration = environment + "-" + application;
+ }
+ return "http://localhost:" + port + "/applications/"
+ + application + "/environments/"
+ + environment + "/configurations/"
+ + configuration;
+ }
+ }
+}
diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java
new file mode 100644
index 0000000..5b85817
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/DResult.java
@@ -0,0 +1,28 @@
+package io.avaje.aws.appconfig;
+
+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) {
+ this.version = version;
+ this.contentType = contentType;
+ this.body = body;
+ }
+
+ @Override
+ public String version() {
+ return version;
+ }
+
+ @Override
+ public String contentType() {
+ return contentType;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+}
diff --git a/avaje-aws-appconfig/src/main/java/module-info.java b/avaje-aws-appconfig/src/main/java/module-info.java
new file mode 100644
index 0000000..89a56e9
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/java/module-info.java
@@ -0,0 +1,10 @@
+import io.avaje.aws.appconfig.AppConfigPlugin;
+
+module io.avaje.aws.appconfig {
+
+ exports io.avaje.aws.appconfig;
+
+ requires io.avaje.config;
+ requires java.net.http;
+ provides io.avaje.config.ConfigurationSource with AppConfigPlugin;
+}
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
new file mode 100644
index 0000000..fd4b517
--- /dev/null
+++ b/avaje-aws-appconfig/src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource
@@ -0,0 +1 @@
+io.avaje.aws.appconfig.AppConfigPlugin
diff --git a/pom.xml b/pom.xml
index 76070ea..8f80dcd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
avaje-config
+ avaje-aws-appconfig