diff --git a/avaje-aws-appconfig/pom.xml b/avaje-aws-appconfig/pom.xml index 01a81c0..f0445e0 100644 --- a/avaje-aws-appconfig/pom.xml +++ b/avaje-aws-appconfig/pom.xml @@ -10,7 +10,8 @@ - avaje-aws-appconfig + io.avaje + avaje-config-aws-appconfig 0.3-SNAPSHOT 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 deleted file mode 100644 index dffed85..0000000 --- a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigPlugin.java +++ /dev/null @@ -1,104 +0,0 @@ -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"); - return; - } - - 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/AppConfigFetcher.java b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AppConfigFetcher.java similarity index 90% rename from avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java rename to avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AppConfigFetcher.java index 1b2d447..29e147f 100644 --- a/avaje-aws-appconfig/src/main/java/io/avaje/aws/appconfig/AppConfigFetcher.java +++ b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AppConfigFetcher.java @@ -1,6 +1,6 @@ -package io.avaje.aws.appconfig; +package io.avaje.config.awsappconfig; -public interface AppConfigFetcher { +interface AppConfigFetcher { static AppConfigFetcher.Builder builder() { return new DAppConfigFetcher.Builder(); diff --git a/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AwsAppConfigPlugin.java b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AwsAppConfigPlugin.java new file mode 100644 index 0000000..b873ae9 --- /dev/null +++ b/avaje-aws-appconfig/src/main/java/io/avaje/config/awsappconfig/AwsAppConfigPlugin.java @@ -0,0 +1,142 @@ +package io.avaje.config.awsappconfig; + +import io.avaje.config.ConfigParser; +import io.avaje.config.Configuration; +import io.avaje.config.ConfigurationSource; + +import java.io.StringReader; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import static java.lang.System.Logger.Level.*; + +/** + * Plugin that loads AWS AppConfig as Yaml or Properties. + *

+ * 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 entry : entries) { + Object value = entry.getValue(); + if (value != null) { + result.put(entry.getKey().toString(), entry.getValue().toString()); + } + } + return result; + } +} diff --git a/avaje-config/src/test/java/io/avaje/config/CoreConfigurationTest.java b/avaje-config/src/test/java/io/avaje/config/CoreConfigurationTest.java index f5223c0..b9d3f0d 100644 --- a/avaje-config/src/test/java/io/avaje/config/CoreConfigurationTest.java +++ b/avaje-config/src/test/java/io/avaje/config/CoreConfigurationTest.java @@ -1,6 +1,7 @@ package io.avaje.config; import io.avaje.config.CoreEntry.CoreMap; +import org.example.MyExternalLoader; import org.junit.jupiter.api.Test; import java.io.StringReader; @@ -177,6 +178,10 @@ void builder_withResources() { String userHome = System.getProperty("user.home"); assertThat(conf.get("myHome")).isEqualTo("my/" + userHome + "/home"); + + MyExternalLoader.reset(); + conf.reloadSources(); + assertThat(MyExternalLoader.refreshCalled()).isTrue(); } @Test diff --git a/avaje-config/src/test/java/io/avaje/config/InitialLoaderTest.java b/avaje-config/src/test/java/io/avaje/config/InitialLoaderTest.java index 6a7299e..11cc2f1 100644 --- a/avaje-config/src/test/java/io/avaje/config/InitialLoaderTest.java +++ b/avaje-config/src/test/java/io/avaje/config/InitialLoaderTest.java @@ -10,7 +10,7 @@ class InitialLoaderTest { private static InitialLoader newInitialLoader() { - return new InitialLoader(new Parsers(), new DefaultConfigurationLog(), new DefaultResourceLoader()); + return new InitialLoader(new CoreComponents(), new DefaultResourceLoader()); } @Test diff --git a/avaje-config/src/test/java/io/avaje/config/PropertiesParserTest.java b/avaje-config/src/test/java/io/avaje/config/PropertiesParserTest.java new file mode 100644 index 0000000..94c4bd3 --- /dev/null +++ b/avaje-config/src/test/java/io/avaje/config/PropertiesParserTest.java @@ -0,0 +1,50 @@ +package io.avaje.config; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertiesParserTest { + + @Test + void supportedExtensions() { + var parser = new PropertiesParser(); + assertThat(parser.supportedExtensions()).isEqualTo(new String[]{"properties"}); + } + + private static String input() { + return "one.key=a\n" + + "one.key2=b\n" + + "## comment\n" + + "key3 = c\n"; + } + + @Test + void load_reader() { + var parser = new PropertiesParser(); + Map map = parser.load(new StringReader(input())); + + assertThat(map).hasSize(3); + assertThat(map).containsOnlyKeys("one.key", "one.key2", "key3"); + assertThat(map).containsEntry("one.key", "a"); + assertThat(map).containsEntry("one.key2", "b"); + assertThat(map).containsEntry("key3", "c"); + } + + @Test + void load_inputStream() { + var parser = new PropertiesParser(); + Map map = parser.load(new ByteArrayInputStream(input().getBytes(StandardCharsets.UTF_8))); + + assertThat(map).hasSize(3); + assertThat(map).containsOnlyKeys("one.key", "one.key2", "key3"); + assertThat(map).containsEntry("one.key", "a"); + assertThat(map).containsEntry("one.key2", "b"); + assertThat(map).containsEntry("key3", "c"); + } +} diff --git a/avaje-config/src/test/java/org/example/MyExternalLoader.java b/avaje-config/src/test/java/org/example/MyExternalLoader.java index 0682ba2..bb4601b 100644 --- a/avaje-config/src/test/java/org/example/MyExternalLoader.java +++ b/avaje-config/src/test/java/org/example/MyExternalLoader.java @@ -5,6 +5,8 @@ public class MyExternalLoader implements ConfigurationSource { + static boolean refreshCalled; + @Override public void load(Configuration configuration) { @@ -19,4 +21,16 @@ public void load(Configuration configuration) { configuration.schedule(500, 500, () -> System.out.println("MyExternalLoader task ..")); } + @Override + public void reload() { + refreshCalled = true; + } + + public static boolean refreshCalled() { + return refreshCalled; + } + + public static void reset() { + refreshCalled = false; + } }