modifiedKeys() {
29 | return modifiedKeys;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/ConfigEnvMain.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | public class ConfigEnvMain {
4 |
5 | public static void main(String[] args) {
6 |
7 | // // initialise it
8 | // Config.init();
9 | //
10 | // final String appName = System.getProperty("appName");
11 | // final String appInstanceId = System.getProperty("appInstanceId");
12 |
13 | System.out.println("--- 2");
14 | System.out.println("appName=" + Config.getNullable("appName"));
15 | System.out.println("appInstanceId=" + Config.getNullable("appInstanceId"));
16 | System.out.println("appEnvironment=" + Config.getNullable("appEnvironment"));
17 | System.out.println("appVersion=" + Config.getNullable("appVersion"));
18 | System.out.println("appIp=" + Config.getNullable("appIp"));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | org.avaje
6 | java11-oss
7 | 5.1
8 |
9 |
10 |
11 | io.avaje
12 | tests-config-reactor
13 | 1
14 | pom
15 |
16 |
17 | avaje-config
18 | avaje-config-toml
19 | avaje-aws-appconfig
20 | avaje-dynamic-logback
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ${{ matrix.os }}
9 | permissions:
10 | contents: read
11 | packages: write
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | java_version: [11,17,21]
16 | os: [ubuntu-latest]
17 |
18 | steps:
19 | - uses: actions/checkout@v6
20 | - name: Set up Java
21 | uses: actions/setup-java@v5
22 | with:
23 | java-version: ${{ matrix.java_version }}
24 | distribution: 'zulu'
25 | - name: Maven cache
26 | uses: actions/cache@v5
27 | env:
28 | cache-name: maven-cache
29 | with:
30 | path:
31 | ~/.m2
32 | key: build-${{ env.cache-name }}
33 | - name: Build with Maven
34 | run: mvn clean test
35 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ResourceLoader.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.io.InputStream;
4 |
5 | import org.jspecify.annotations.Nullable;
6 |
7 | /**
8 | * Plugin API for loading resources typically from the classpath or module path.
9 | *
10 | * When not specified Avaje Config provides a default implementation that looks
11 | * to find resources using the class loader associated with the ResourceLoader.
12 | *
13 | * Note there is a fallback to use {@link ClassLoader#getSystemResourceAsStream(String)}
14 | * if the ResourceLoader returns null.
15 | */
16 | public interface ResourceLoader extends ConfigExtension {
17 |
18 | /**
19 | * Return the InputStream for the given resource or null if it can not be found.
20 | */
21 | @Nullable
22 | InputStream getResourceAsStream(String resourcePath);
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/jdk-ea-stable.yml:
--------------------------------------------------------------------------------
1 |
2 | name: JDK EA Stable
3 |
4 | on:
5 | push:
6 | pull_request:
7 | workflow_dispatch:
8 | schedule:
9 | - cron: '39 1 * * 1,3,5'
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | packages: write
17 |
18 | steps:
19 | - uses: actions/checkout@v6
20 | - name: Set up Java
21 | uses: oracle-actions/setup-java@v1
22 | with:
23 | website: jdk.java.net
24 | release: ea
25 | version: stable
26 | - name: Maven cache
27 | uses: actions/cache@v5
28 | env:
29 | cache-name: maven-cache
30 | with:
31 | path:
32 | ~/.m2
33 | key: build-${{ env.cache-name }}
34 | - name: Maven version
35 | run: mvn --version
36 | - name: Build with Maven
37 | run: mvn package
38 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ModificationEventRunner.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | /**
4 | * Run the event listener notifications.
5 | *
6 | * Supply this using service loading to for example run the event listener notification
7 | * in the background using an {@link java.util.concurrent.ExecutorService}.
8 | *
9 | * The default is for event listener notification to be executed using the same thread
10 | * that is making the modifications to the configuration.
11 | */
12 | public interface ModificationEventRunner extends ConfigExtension {
13 |
14 | /**
15 | * Run the task of notifying all the event listeners of a modification event
16 | * to the configuration.
17 | *
18 | * @param onChangeNotifyTask The task to be executed notifying listeners of changes
19 | * to the configuration.
20 | */
21 | void run(Runnable onChangeNotifyTask);
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' }}
12 | steps:
13 | - name: Dependabot metadata
14 | id: metadata
15 | uses: dependabot/fetch-metadata@v2
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 | - name: Approve a PR
19 | run: gh pr review --approve "$PR_URL"
20 | env:
21 | PR_URL: ${{github.event.pull_request.html_url}}
22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
23 | - name: Enable auto-merge for Dependabot PRs
24 | run: gh pr merge --auto --merge "$PR_URL"
25 | env:
26 | PR_URL: ${{github.event.pull_request.html_url}}
27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
28 |
--------------------------------------------------------------------------------
/.github/workflows/jdk-ea.yml:
--------------------------------------------------------------------------------
1 |
2 | name: avaje-config EA
3 |
4 | on:
5 | pull_request:
6 | workflow_dispatch:
7 | schedule:
8 | - cron: '48 0 * * 6'
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ${{ matrix.os }}
14 | permissions:
15 | contents: read
16 | packages: write
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | java_version: [GA,EA]
21 | os: [ubuntu-latest]
22 |
23 | steps:
24 | - uses: actions/checkout@v6
25 | - name: Set up Java
26 | uses: oracle-actions/setup-java@v1
27 | with:
28 | website: jdk.java.net
29 | release: ${{ matrix.java_version }}
30 | - name: Maven cache
31 | uses: actions/cache@v5
32 | env:
33 | cache-name: maven-cache
34 | with:
35 | path:
36 | ~/.m2
37 | key: build-${{ env.cache-name }}
38 | - name: Build with Maven
39 | run: mvn clean test
40 |
41 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ConfigParser.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.io.InputStream;
4 | import java.io.Reader;
5 | import java.util.Map;
6 |
7 | import org.jspecify.annotations.NullMarked;
8 |
9 | /**
10 | * Load a config file into a flattened map.
11 | */
12 | @NullMarked
13 | public interface ConfigParser extends ConfigExtension {
14 |
15 | /**
16 | * File extensions Supported by this parser
17 | */
18 | String[] supportedExtensions();
19 |
20 | /**
21 | * Parse content into key value pairs.
22 | *
23 | * @param reader configuration contents
24 | * @return Key-Value pairs of all the configs
25 | */
26 | Map load(Reader reader);
27 |
28 | /**
29 | * Parse content into key value pairs.
30 | *
31 | * @param is configuration contents
32 | * @return Key-Value pairs of all the configs
33 | */
34 | Map load(InputStream is);
35 | }
36 |
--------------------------------------------------------------------------------
/avaje-dynamic-logback/README.md:
--------------------------------------------------------------------------------
1 | # avaje-dynamic-logback
2 |
3 | A plugin for avaje-config that dynamically changes the logging levels.
4 |
5 |
6 | ## Configuring logging levels
7 |
8 | Logging levels are configured by using `log.level.` as prefix, for example:
9 |
10 | This plugin registers to listen to configuration changes the logging
11 | level for configuration changes that start with `log.level`
12 |
13 | When configuration is first loaded and then whenever it is changed
14 | the logging level is changed.
15 |
16 |
17 | #### yaml
18 | ```yaml
19 | log.level:
20 | com.example: INFO
21 | com.example.sample: TRACE
22 | ```
23 |
24 | #### properties
25 | ```properties
26 | log.level.com.example=INFO
27 | log.level.com.example.sample=TRACE
28 | ```
29 |
30 |
31 | ## Steps to use
32 |
33 | ### Add dependency
34 |
35 | ```xml
36 |
37 | io.avaje
38 | avaje-dynamic-logback
39 | ...
40 |
41 | ```
42 |
--------------------------------------------------------------------------------
/.github/workflows/native-image.yml:
--------------------------------------------------------------------------------
1 | name: native image build
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '40 1 1 1 6'
7 |
8 | jobs:
9 | build:
10 | runs-on: ${{ matrix.os }}
11 | permissions:
12 | contents: read
13 | packages: write
14 | strategy:
15 | fail-fast: true
16 | matrix:
17 | os: [ubuntu-latest]
18 |
19 | steps:
20 | - uses: actions/checkout@v6
21 | - uses: graalvm/setup-graalvm@v1
22 | with:
23 | java-version: '21'
24 | distribution: 'graalvm'
25 | cache: 'maven'
26 | github-token: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Versions
29 | run: |
30 | echo "GRAALVM_HOME: $GRAALVM_HOME"
31 | echo "JAVA_HOME: $JAVA_HOME"
32 | java --version
33 | native-image --version
34 | - name: Build with Maven
35 | run: |
36 | mvn clean install -DskipTests
37 | cd tests/test-native-image
38 | mvn clean package -Pnative
39 | ./target/test-native-image -Davaje.profiles=admin
40 |
--------------------------------------------------------------------------------
/avaje-aws-appconfig/src/main/java/io/avaje/config/appconfig/AppConfigFetcher.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.appconfig;
2 |
3 | import java.net.ConnectException;
4 | import java.net.URI;
5 |
6 | interface AppConfigFetcher {
7 |
8 | static AppConfigFetcher.Builder builder() {
9 | return new DAppConfigFetcher.Builder();
10 | }
11 |
12 | Result fetch() throws ConnectException, FetchException;
13 |
14 | URI uri();
15 |
16 | class FetchException extends Exception {
17 |
18 | public FetchException(Exception e) {
19 | super(e);
20 | }
21 | }
22 |
23 | interface Result {
24 |
25 | String version();
26 |
27 | String contentType();
28 |
29 | String body();
30 | }
31 |
32 | interface Builder {
33 |
34 | AppConfigFetcher.Builder application(String application);
35 |
36 | AppConfigFetcher.Builder environment(String environment);
37 |
38 | AppConfigFetcher.Builder configuration(String configuration);
39 |
40 | AppConfigFetcher.Builder port(int port);
41 |
42 | AppConfigFetcher build();
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/org/example/MyExternalLoader.java:
--------------------------------------------------------------------------------
1 | package org.example;
2 |
3 | import io.avaje.config.Configuration;
4 | import io.avaje.config.ConfigurationSource;
5 |
6 | public class MyExternalLoader implements ConfigurationSource {
7 |
8 | static boolean refreshCalled;
9 |
10 | @Override
11 | public void load(Configuration configuration) {
12 |
13 | // we can read properties that have been already
14 | // loaded from files/resources if desired
15 | configuration.getOptional("myExternalLoader.location");
16 |
17 | // add set properties (kind of the point)
18 | configuration.setProperty("myExternalLoader", "wasExecuted");
19 |
20 | // schedule a task if we like
21 | configuration.schedule(500, 500, () -> System.out.println("MyExternalLoader task .."));
22 | }
23 |
24 | @Override
25 | public void reload() {
26 | refreshCalled = true;
27 | }
28 |
29 | public static boolean refreshCalled() {
30 | return refreshCalled;
31 | }
32 |
33 | public static void reset() {
34 | refreshCalled = false;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/avaje-config/test-config-main.sh:
--------------------------------------------------------------------------------
1 | export POD_NAME=five-7d6d5bdf8-bsvpl
2 | export POD_NAMESPACE=development
3 | export POD_IP=1.2.3.4
4 | export POD_VERSION=5.23
5 |
6 | java -classpath target/test-classes:target/classes:/home/rob/.m2/repository-pv/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar io.avaje.config.ConfigEnvMain
7 |
8 | #/home/rob/.m2/repository-pv/org/yaml/snakeyaml/1.25/snakeyaml-1.25.jar:/home/rob/.m2/repository-pv/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar:/home/rob/.m2/repository-pv/org/avaje/composite/junit/1.1/junit-1.1.jar:/home/rob/.m2/repository-pv/junit/junit/4.12/junit-4.12.jar:/home/rob/.m2/repository-pv/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar:/home/rob/.m2/repository-pv/org/assertj/assertj-core/3.10.0/assertj-core-3.10.0.jar:/home/rob/.m2/repository-pv/com/h2database/h2/1.4.197/h2-1.4.197.jar:/home/rob/.m2/repository-pv/org/mockito/mockito-core/2.18.3/mockito-core-2.18.3.jar:/home/rob/.m2/repository-pv/net/bytebuddy/byte-buddy/1.8.5/byte-buddy-1.8.5.jar:/home/rob/.m2/repository-pv/net/bytebuddy/byte-buddy-agent/1.8.5/byte-buddy-agent-1.8.5.jar:/home/rob/.m2/repository-pv/org/objenesis/objenesis/2.6/objenesis-2.6.jar:/home/rob/apps/idea-IU-193.5233.102/lib/idea_rt.jar com.intellij.rt.execution.application.AppMainV2
9 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreListener.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.function.Consumer;
4 |
5 | import static java.lang.System.Logger.Level.ERROR;
6 |
7 | /**
8 | * Wraps the listener taking the interesting keys into account.
9 | */
10 | final class CoreListener {
11 |
12 | private final ConfigurationLog log;
13 | private final Consumer listener;
14 | private final String[] keys;
15 |
16 | CoreListener(ConfigurationLog log, Consumer listener, String[] keys) {
17 | this.log = log;
18 | this.listener = listener;
19 | this.keys = keys;
20 | }
21 |
22 | void accept(CoreModificationEvent event) {
23 | if (keys == null || keys.length == 0 || containsKey(event)) {
24 | try {
25 | listener.accept(event);
26 | } catch (Exception e) {
27 | log.log(ERROR, "Error during onChange notification", e);
28 | }
29 | }
30 | }
31 |
32 | private boolean containsKey(CoreModificationEvent event) {
33 | final var modifiedKeys = event.modifiedKeys();
34 | for (String key : keys) {
35 | if (modifiedKeys.contains(key)) {
36 | return true;
37 | }
38 | }
39 | return false;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ConfigurationLog.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.lang.System.Logger.Level;
4 |
5 | /**
6 | * Configuration events are sent to this event log.
7 | *
8 | * The EventLog implementation can be provided by ServiceLoader and then can
9 | * control how the events are logged. For example, it might delay logging messages
10 | * until logging implementation has finished configuration.
11 | */
12 | public interface ConfigurationLog extends ConfigExtension {
13 |
14 | /**
15 | * Invoked when the configuration is being initialised.
16 | */
17 | default void preInitialisation() {
18 | // do nothing by default
19 | }
20 |
21 | /**
22 | * Invoked when the initialisation of configuration has been completed.
23 | */
24 | default void postInitialisation() {
25 | // do nothing by default
26 | }
27 |
28 | /**
29 | * Log an event with the given level, message, and thrown exception.
30 | */
31 | void log(Level level, String message, Throwable thrown);
32 |
33 | /**
34 | * Log an event with the given level, formatted message, and arguments.
35 | *
36 | * The message format is as per {@link java.text.MessageFormat#format(String, Object...)}.
37 | */
38 | void log(Level level, String message, Object... args);
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ConfigurationSource.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | /**
4 | * Additional source to load and update configuration.
5 | */
6 | public interface ConfigurationSource extends ConfigExtension {
7 |
8 | /**
9 | * Load additional configuration.
10 | *
11 | * At this load time the configuration has already loaded properties
12 | * from files and resources and configuration can be read provide
13 | * configuration to the source like URL's to load more configuration
14 | * from etc.
15 | *
16 | * The {@link Configuration#setProperty(String, String)} method is
17 | * used when loading the additional properties from the source.
18 | *
19 | * Also note that the source can additionally use
20 | * {@link Configuration#schedule(long, long, Runnable)} to schedule
21 | * a period task to for example refresh data etc.
22 | *
23 | * @param configuration The configuration with initially properties.
24 | */
25 | void load(Configuration configuration);
26 |
27 | /**
28 | * Explicitly reload the configuration source.
29 | *
30 | * Generally the configuration source will schedule a periodic refresh of its
31 | * configuration but there are cases like Lambda where it can be useful to
32 | * trigger a refresh explicitly and manually (e.g. on Lambda invocation).
33 | */
34 | default void reload() {
35 | // do nothing by default
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreComponents.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.Collections;
4 | import java.util.List;
5 |
6 | final class CoreComponents {
7 |
8 | private final ModificationEventRunner runner;
9 | private final ConfigurationLog log;
10 | private final Parsers parsers;
11 | private final List sources;
12 | private final List plugins;
13 |
14 | CoreComponents(
15 | ModificationEventRunner runner,
16 | ConfigurationLog log,
17 | Parsers parsers,
18 | List sources,
19 | List plugins) {
20 | this.runner = runner;
21 | this.log = log;
22 | this.parsers = parsers;
23 | this.sources = sources;
24 | this.plugins = plugins;
25 | }
26 |
27 | /** For testing only */
28 | CoreComponents() {
29 | this.runner = new CoreConfiguration.ForegroundEventRunner();
30 | this.log = new DefaultConfigurationLog();
31 | this.parsers = new Parsers(Collections.emptyList());
32 | this.sources = Collections.emptyList();
33 | this.plugins = Collections.emptyList();
34 | }
35 |
36 | Parsers parsers() {
37 | return parsers;
38 | }
39 |
40 | ConfigurationLog log() {
41 | return log;
42 | }
43 |
44 | ModificationEventRunner runner() {
45 | return runner;
46 | }
47 |
48 | List sources() {
49 | return sources;
50 | }
51 |
52 | List plugins() {
53 | return plugins;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/PropertiesParser.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.io.Reader;
6 | import java.io.UncheckedIOException;
7 | import java.util.LinkedHashMap;
8 | import java.util.Map;
9 | import java.util.Properties;
10 | import java.util.Set;
11 |
12 | import org.jspecify.annotations.NullMarked;
13 |
14 | @NullMarked
15 | final class PropertiesParser implements ConfigParser {
16 |
17 | private static final String[] extensions = {"properties"};
18 |
19 | @Override
20 | public String[] supportedExtensions() {
21 | return extensions;
22 | }
23 |
24 | @Override
25 | public Map load(Reader reader) {
26 | try {
27 | Properties p = new Properties();
28 | p.load(reader);
29 | return toMap(p);
30 | } catch (IOException e) {
31 | throw new UncheckedIOException(e);
32 | }
33 | }
34 |
35 | @Override
36 | public Map load(InputStream is) {
37 | try {
38 | Properties p = new Properties();
39 | p.load(is);
40 | return toMap(p);
41 | } catch (IOException e) {
42 | throw new UncheckedIOException(e);
43 | }
44 | }
45 |
46 | private static Map toMap(Properties p) {
47 | Map result = new LinkedHashMap<>();
48 | Set> entries = p.entrySet();
49 | for (Map.Entry entry : entries) {
50 | Object value = entry.getValue();
51 | if (value != null) {
52 | result.put(entry.getKey().toString(), entry.getValue().toString());
53 | }
54 | }
55 | return result;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/PropertiesParserTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.io.ByteArrayInputStream;
6 | import java.io.StringReader;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.Map;
9 |
10 | import static org.assertj.core.api.Assertions.assertThat;
11 |
12 | class PropertiesParserTest {
13 |
14 | @Test
15 | void supportedExtensions() {
16 | var parser = new PropertiesParser();
17 | assertThat(parser.supportedExtensions()).isEqualTo(new String[]{"properties"});
18 | }
19 |
20 | private static String input() {
21 | return "one.key=a\n" +
22 | "one.key2=b\n" +
23 | "## comment\n" +
24 | "key3 = c\n";
25 | }
26 |
27 | @Test
28 | void load_reader() {
29 | var parser = new PropertiesParser();
30 | Map map = parser.load(new StringReader(input()));
31 |
32 | assertThat(map).hasSize(3);
33 | assertThat(map).containsOnlyKeys("one.key", "one.key2", "key3");
34 | assertThat(map).containsEntry("one.key", "a");
35 | assertThat(map).containsEntry("one.key2", "b");
36 | assertThat(map).containsEntry("key3", "c");
37 | }
38 |
39 | @Test
40 | void load_inputStream() {
41 | var parser = new PropertiesParser();
42 | Map map = parser.load(new ByteArrayInputStream(input().getBytes(StandardCharsets.UTF_8)));
43 |
44 | assertThat(map).hasSize(3);
45 | assertThat(map).containsOnlyKeys("one.key", "one.key2", "key3");
46 | assertThat(map).containsEntry("one.key", "a");
47 | assertThat(map).containsEntry("one.key2", "b");
48 | assertThat(map).containsEntry("key3", "c");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Application configuration based on loading properties and yaml files.
3 | *
4 | * Examples
5 | * {@code
6 | *
7 | * int port = Config.getInt("app.port", 8090);
8 | *
9 | * String topicName = Config.get("app.topic.name");
10 | *
11 | * List codes = Config.getList().ofInt("my.codes", 42, 54);
12 | *
13 | * }
14 | *
15 | * Loading into System properties
16 | *
17 | * If config.load.systemProperties is set to true
18 | * then all the properties are loaded into System properties.
19 | *
20 | *
21 | * File watching and reloading
22 | *
23 | * We can enable watching configuration files by setting
24 | * config.watch.enabled=true. With this enabled
25 | * config will watch for modifications to the configuration files
26 | * and reload the configuration.
27 | *
28 | * By default the files are checked every 60 seconds. We can
29 | * change this by setting the config.watch.period
30 | * (which is in seconds). For example setting
31 | * config.watch.period=10 means the files are
32 | * checked every every 10 seconds.
33 | *
34 | * By default there is an initial delay of 60 seconds. We can
35 | * change this by setting config.watch.delay.
36 | *
37 | * This can provide us a simple "feature toggle" mechanism.
38 | *
39 | *
{@code
40 | *
41 | * // we can toggle this on/off by editing the
42 | * // appropriate property in the configuration file
43 | * if (Config.enabled("feature.cleanup", false)) {
44 | * ...
45 | * }
46 | *
47 | * }
48 | */
49 | package io.avaje.config;
--------------------------------------------------------------------------------
/.github/workflows/milestone.yml:
--------------------------------------------------------------------------------
1 | # Trigger the workflow when a release is published
2 | name: Close Milestone on Release
3 |
4 | on:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | close_milestone:
10 | # Use the latest Ubuntu runner
11 | runs-on: ubuntu-latest
12 | # We only want to proceed if the release tag matches the milestone title
13 | steps:
14 | - name: Extract Milestone Title
15 | id: extract
16 | run: |
17 | RELEASE_TAG="${{ github.event.release.tag_name }}"
18 | MILESTONE_TITLE="${RELEASE_TAG#v}"
19 | echo "MILESTONE_TITLE=$MILESTONE_TITLE" >> $GITHUB_OUTPUT
20 |
21 | - name: Close Corresponding Milestone
22 | uses: cli/cli-action@v2
23 | with:
24 | github-token: ${{ secrets.GITHUB_TOKEN }}
25 | args: |
26 | # Search for an open milestone with a title matching the release tag
27 | MILESTONE_ID=$(gh api \
28 | -H "Accept: application/vnd.github+json" \
29 | /repos/${{ github.repository }}/milestones \
30 | -f state='open' \
31 | -f sort='due_on' \
32 | -f direction='asc' \
33 | -f per_page='100' \
34 | --jq '.[] | select(.title=="${{ steps.extract.outputs.MILESTONE_TITLE }}") | .number' \
35 | | head -n 1)
36 | if [ -z "$MILESTONE_ID" ]; then
37 | echo "No open milestone found"
38 | else
39 | gh api \
40 | --method PATCH \
41 | -H "Accept: application/vnd.github+json" \
42 | /repos/${{ github.repository }}/milestones/$MILESTONE_ID \
43 | -f state='closed'
44 | fi
45 |
--------------------------------------------------------------------------------
/avaje-aws-appconfig/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.avaje
8 | java11-oss
9 | 5.1
10 |
11 |
12 |
13 | io.avaje
14 | avaje-aws-appconfig
15 | avaje-aws-appconfig
16 | 1.6
17 | AWS AppConfig plugin for avaje-config
18 |
19 |
20 | false
21 | 2025-12-16T08:12:58Z
22 |
23 |
24 |
25 |
26 | io.avaje
27 | avaje-applog
28 | 1.2
29 |
30 |
31 |
32 | io.avaje
33 | avaje-config
34 | 4.4
35 | provided
36 |
37 |
38 |
39 | io.avaje
40 | avaje-spi-service
41 | 2.14
42 | true
43 |
44 |
45 |
46 | io.avaje
47 | junit
48 | 1.7
49 | test
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/avaje-config-toml/src/main/java/io/avaje/config/toml/TomlParser.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.toml;
2 |
3 | import io.avaje.config.ConfigParser;
4 | import org.jspecify.annotations.NullMarked;
5 | import org.tomlj.Toml;
6 | import org.tomlj.TomlArray;
7 |
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.io.Reader;
11 | import java.io.UncheckedIOException;
12 | import java.util.Map;
13 | import java.util.stream.Collectors;
14 |
15 | @NullMarked
16 | public final class TomlParser implements ConfigParser {
17 |
18 | private static final String[] extensions = {"toml"};
19 |
20 | @Override
21 | public String[] supportedExtensions() {
22 | return extensions;
23 | }
24 |
25 | @Override
26 | public Map load(Reader reader) {
27 | try {
28 | return Toml.parse(reader).dottedEntrySet()
29 | .stream()
30 | .collect(Collectors.toMap(Map.Entry::getKey, entry -> readTomlValue(entry.getValue())));
31 | } catch (IOException exception) {
32 | throw new UncheckedIOException(exception);
33 | }
34 | }
35 |
36 | @Override
37 | public Map load(InputStream is) {
38 | try {
39 | return Toml.parse(is).dottedEntrySet()
40 | .stream()
41 | .collect(Collectors.toMap(Map.Entry::getKey, entry -> readTomlValue(entry.getValue())));
42 | } catch (IOException exception) {
43 | throw new UncheckedIOException(exception);
44 | }
45 | }
46 |
47 | private static String readTomlValue(Object object) {
48 | if (object instanceof TomlArray) {
49 | TomlArray array = (TomlArray) object;
50 | return array.toList().stream()
51 | .map(TomlParser::readTomlValue)
52 | .collect(Collectors.joining(";"));
53 | }
54 | return String.valueOf(object);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/DefaultValues.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import io.avaje.config.Configuration.Entry;
4 |
5 | import java.util.Optional;
6 |
7 | /**
8 | * Override and fallback values.
9 | */
10 | final class DefaultValues {
11 |
12 | /**
13 | * Return the key as an environment variable name using the standard conventions.
14 | */
15 | static String toEnvKey(String key) {
16 | return key.replace('.', '_').replace("-", "").toUpperCase();
17 | }
18 |
19 | /**
20 | * Return an Entry overriding first by system property and then by environment variable.
21 | *
22 | * If the key is not overridden then it is returned as the given value and source.
23 | */
24 | static CoreEntry overrideValue(String key, String value, String source) {
25 | String propertyValue = System.getProperty(key);
26 | if (propertyValue != null) {
27 | // overridden by a system property
28 | return CoreEntry.of(propertyValue, Constants.SYSTEM_PROPS);
29 | }
30 | String envValue = System.getenv(toEnvKey(key));
31 | if (envValue != null) {
32 | // overridden by an environment variable
33 | return CoreEntry.of(envValue, Constants.ENV_VARIABLES);
34 | }
35 | // not overridden, return as given
36 | return CoreEntry.of(value, source);
37 | }
38 |
39 | static Optional fallbackValue(String key) {
40 | String propertyValue = System.getProperty(key);
41 | if (propertyValue != null) {
42 | // overridden by a system property
43 | return Optional.of(CoreEntry.of(propertyValue, Constants.SYSTEM_PROPS));
44 | }
45 | String envValue = System.getenv(toEnvKey(key));
46 | if (envValue != null) {
47 | // overridden by an environment variable
48 | return Optional.of(CoreEntry.of(envValue, Constants.ENV_VARIABLES));
49 | }
50 | return Optional.empty();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/avaje-aws-appconfig/README.md:
--------------------------------------------------------------------------------
1 | # avaje-aws-appconfig
2 |
3 | This is a avaje-config ConfigurationSource that uses
4 | [AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html)
5 | as a source for configuration.
6 |
7 | The AWS AppConfig content can either be in `yaml` or `properties`
8 | format.
9 |
10 | This plugin will load configuration from the AWS AppConfig agent on initialisation
11 | and then additionally by polling the AppConfig agent (for changes to the config)
12 | and also when `Configuration.refresh()` is called.
13 |
14 | ## Configuration
15 |
16 | - aws.appconfig.enabled - defaults to `true`
17 | - aws.appconfig.application - required
18 | - aws.appconfig.environment - required
19 | - aws.appconfig.configuration - defaults to `"default"`
20 | - aws.appconfig.pollingEnabled - defaults to `true`
21 | - aws.appconfig.pollingSeconds - defaults to `45` seconds
22 | - aws.appconfig.refreshSeconds - defaults to `(pollingSeconds - 1)`
23 |
24 |
25 | ## Steps to use
26 |
27 | ### Add dependency
28 |
29 | ```xml
30 |
31 | io.avaje
32 | avaje-aws-appconfig
33 | ...
34 |
35 | ```
36 |
37 | ### Add to src/main/application.yaml
38 |
39 | ```yaml
40 | aws.appconfig:
41 | application: my-application
42 | environment: ${ENVIRONMENT:dev}
43 | configuration: default
44 | ```
45 |
46 | Or with more parameters like:
47 |
48 | ```yaml
49 | aws.appconfig:
50 | enabled: true
51 | application: my-application
52 | environment: ${ENVIRONMENT:dev}
53 | configuration: default
54 | pollingEnabled: true
55 | pollingSeconds: 60
56 | ```
57 |
58 |
59 | ### Add to src/test/application-test.yml
60 |
61 | Turn it off for testing. When running tests we generally don't wish to
62 | pull configuration from AWS AppConfig.
63 |
64 | ```yaml
65 | aws.appconfig.enabled: false
66 | ```
67 |
--------------------------------------------------------------------------------
/avaje-dynamic-logback/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.avaje
8 | java11-oss
9 | 5.1
10 |
11 |
12 |
13 | io.avaje
14 | avaje-dynamic-logback
15 | avaje-dynamic-logback
16 | 2.0
17 |
18 | https://github.com/avaje/avaje-config/tree/master/avaje-dynamic-logback
19 |
20 |
21 | false
22 | 2025-12-16T08:12:58Z
23 |
24 |
25 |
26 |
27 |
28 | io.avaje
29 | avaje-config
30 | 5.0
31 |
32 |
33 |
34 | io.avaje
35 | avaje-spi-service
36 | 2.14
37 | true
38 |
39 |
40 |
41 | ch.qos.logback
42 | logback-classic
43 | 1.5.22
44 |
45 |
46 |
47 | io.avaje
48 | avaje-applog-slf4j
49 | 1.0
50 |
51 |
52 |
53 | io.avaje
54 | junit
55 | 1.7
56 | test
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/avaje-config-toml/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | org.avaje
6 | java11-oss
7 | 5.1
8 |
9 |
10 |
11 | Avaje Config Toml
12 | io.avaje
13 | avaje-config-toml
14 | 5.0
15 |
16 |
17 | scm:git:git@github.com:avaje/avaje-config.git
18 | scm:git:git@github.com:avaje/avaje-config.git
19 | HEAD
20 |
21 |
22 |
23 | false
24 | 2025-12-16T08:12:58Z
25 |
26 |
27 |
28 |
29 | io.avaje
30 | avaje-config
31 | ${project.version}
32 |
33 |
34 | org.tomlj
35 | tomlj
36 | 1.1.1
37 |
38 |
39 |
40 | io.avaje
41 | junit
42 | 1.7
43 | test
44 |
45 |
46 |
47 | ch.qos.logback
48 | logback-classic
49 | 1.5.22
50 | test
51 |
52 |
53 |
54 | io.avaje
55 | avaje-applog-slf4j
56 | 1.0
57 | test
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/tests/test-native-image/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.avaje
8 | java11-oss
9 | 3.12
10 |
11 |
12 |
13 | org.example
14 | test-native-image
15 | 1.0-SNAPSHOT
16 |
17 |
18 | 21
19 | UTF-8
20 | 0.9.27
21 | org.example.Main
22 |
23 |
24 |
25 |
26 | io.avaje
27 | avaje-config
28 | 4.2
29 |
30 |
31 |
32 |
33 |
34 | native
35 |
36 |
37 |
38 | org.graalvm.buildtools
39 | native-maven-plugin
40 | ${version.plugin.nativeimage}
41 |
42 |
43 | build-native
44 |
45 | build
46 |
47 | package
48 |
49 |
50 | --no-fallback
51 | --allow-incomplete-classpath
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ModificationEvent.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.Map;
4 | import java.util.Properties;
5 | import java.util.Set;
6 | import java.util.function.Consumer;
7 |
8 | /**
9 | * The event that occurs on configuration changes. Register to listen for these events
10 | * via {@link Configuration#onChange(Consumer, String...)}.
11 | *
12 | * @see Configuration#eventBuilder(String)
13 | * @see Configuration#onChange(Consumer, String...)
14 | */
15 | public interface ModificationEvent {
16 |
17 | /**
18 | * Return the name of the event (e.g "reload").
19 | */
20 | String name();
21 |
22 | /**
23 | * Return the updated configuration.
24 | */
25 | Configuration configuration();
26 |
27 | /**
28 | * Return the set of keys where the properties where modified.
29 | */
30 | Set modifiedKeys();
31 |
32 | /**
33 | * Build and publish modifications to the configuration.
34 | * {@code
35 | *
36 | * configuration.eventBuilder("MyChanges")
37 | * .put("someKey", "val0")
38 | * .put("someOther.key", "42")
39 | * .remove("foo")
40 | * .publish();
41 | *
42 | * }
43 | *
44 | * @see Configuration#eventBuilder(String)
45 | * @see Configuration#onChange(Consumer, String...)
46 | */
47 | interface Builder {
48 |
49 | /**
50 | * Set a property value.
51 | *
52 | * @param key The property key
53 | * @param value The new value of the property
54 | */
55 | Builder put(String key, String value);
56 |
57 | /**
58 | * Set all the properties from the map.
59 | */
60 | Builder putAll(Map map);
61 |
62 | /**
63 | * Set all the properties from the Properties object.
64 | */
65 | Builder putAll(Properties properties);
66 |
67 | /**
68 | * Remove a property from the configuration.
69 | */
70 | Builder remove(String key);
71 |
72 | /**
73 | * Publish the changes. Listeners registered via {@link Configuration#onChange(Consumer, String...)}
74 | * will be notified on the changes.
75 | */
76 | void publish();
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreEventBuilder.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.LinkedHashMap;
4 | import java.util.Map;
5 | import java.util.Properties;
6 | import java.util.function.BiConsumer;
7 |
8 | import static java.util.Objects.requireNonNull;
9 |
10 | final class CoreEventBuilder implements ModificationEvent.Builder {
11 |
12 | private final String name;
13 | private final CoreConfiguration origin;
14 | private final CoreEntry.CoreMap snapshot;
15 | private final Map changes = new LinkedHashMap<>();
16 |
17 |
18 | CoreEventBuilder(String name, CoreConfiguration origin, CoreEntry.CoreMap snapshot) {
19 | this.name = name;
20 | this.origin = origin;
21 | this.snapshot = snapshot; // at the moment we don't mutate the snapshot so could just use the original map
22 | }
23 |
24 | @Override
25 | public ModificationEvent.Builder putAll(Properties properties) {
26 | properties.forEach((key, value) -> {
27 | requireNonNull(value);
28 | put(key.toString(), value.toString());
29 | });
30 | return this;
31 | }
32 |
33 | @Override
34 | public ModificationEvent.Builder putAll(Map map) {
35 | map.forEach((key, value) -> {
36 | requireNonNull(value);
37 | put(key, value.toString());
38 | });
39 | return this;
40 | }
41 |
42 | @Override
43 | public ModificationEvent.Builder put(String key, String value) {
44 | requireNonNull(key);
45 | requireNonNull(value);
46 | value = origin.eval(value);
47 | if (snapshot.isChanged(key, value)) {
48 | changes.put(key, value);
49 | }
50 | return this;
51 | }
52 |
53 | @Override
54 | public ModificationEvent.Builder remove(String key) {
55 | requireNonNull(key);
56 | if (snapshot.containsKey(key)) {
57 | changes.put(key, null);
58 | }
59 | return this;
60 | }
61 |
62 | @Override
63 | public void publish() {
64 | origin.publishEvent(this);
65 | }
66 |
67 | boolean hasChanges() {
68 | return !changes.isEmpty();
69 | }
70 |
71 | void forEachPut(BiConsumer consumer) {
72 | changes.forEach(consumer);
73 | }
74 |
75 | String name() {
76 | return name;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/Parsers.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.HashMap;
4 | import java.util.List;
5 | import java.util.Map;
6 | import java.util.Set;
7 |
8 | /**
9 | * Holds the non-properties ConfigParsers.
10 | */
11 | final class Parsers {
12 |
13 | private final Map parserMap = new HashMap<>();
14 |
15 | Parsers(List otherParsers) {
16 | parserMap.put("properties", new PropertiesParser());
17 | if (!"true".equals(System.getProperty("skipYaml"))) {
18 | initYamlParser();
19 | }
20 | if (!"true".equals(System.getProperty("skipCustomParsing"))) {
21 | initParsers(otherParsers);
22 | }
23 | }
24 |
25 | private void initYamlParser() {
26 | var modules = ModuleLayer.boot();
27 | YamlLoader yamlLoader =
28 | modules
29 | .findModule("io.avaje.config")
30 | .filter(m -> modules.findModule("org.yaml.snakeyaml").isPresent())
31 | .map(m -> (YamlLoader) new YamlLoaderSnake())
32 | .orElseGet(
33 | () -> {
34 | try {
35 | return new YamlLoaderSnake();
36 | } catch (Throwable e) {
37 | return new YamlLoaderSimple();
38 | }
39 | });
40 |
41 | parserMap.put("yml", yamlLoader);
42 | parserMap.put("yaml", yamlLoader);
43 | }
44 |
45 | private void initParsers(List otherParsers) {
46 | for (ConfigParser parser : otherParsers) {
47 | for (var ext : parser.supportedExtensions()) {
48 | parserMap.put(ext, parser);
49 | }
50 | }
51 | }
52 |
53 | /**
54 | * Return the extension ConfigParser pairs.
55 | */
56 | Set> entrySet() {
57 | return parserMap.entrySet();
58 | }
59 |
60 | /**
61 | * Return the ConfigParser for the given extension.
62 | */
63 | ConfigParser get(String extension) {
64 | return parserMap.get(extension.toLowerCase());
65 | }
66 |
67 | /**
68 | * Return true if the extension has a matching parser.
69 | */
70 | boolean supportsExtension(String extension) {
71 | return parserMap.containsKey(extension.toLowerCase());
72 | }
73 |
74 | /**
75 | * Return the set of supported extensions.
76 | */
77 | Set supportedExtensions() {
78 | return parserMap.keySet();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/YamlLoaderSnake.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.io.InputStream;
4 | import java.io.Reader;
5 | import java.util.Collection;
6 | import java.util.LinkedHashMap;
7 | import java.util.Map;
8 |
9 | import org.jspecify.annotations.NullMarked;
10 | import org.yaml.snakeyaml.Yaml;
11 |
12 | /**
13 | * Loads configuration from Yml into the load context.
14 | *
15 | * Note that this ignores 'lists' so just reads 'maps' and scalar values.
16 | */
17 | @NullMarked
18 | final class YamlLoaderSnake implements YamlLoader {
19 |
20 | private final Yaml yaml;
21 |
22 | YamlLoaderSnake() {
23 | this.yaml = new Yaml();
24 | }
25 |
26 | @Override
27 | public Map load(Reader reader) {
28 | return load(yaml.loadAll(reader));
29 | }
30 |
31 | @Override
32 | public Map load(InputStream is) {
33 | return load(yaml.loadAll(is));
34 | }
35 |
36 | @SuppressWarnings("unchecked")
37 | private Map load(Iterable source) {
38 | Load load = new Load();
39 | for (Object map : source) {
40 | load.loadMap((Map) map, null);
41 | }
42 | return load.map();
43 | }
44 |
45 | private static class Load {
46 |
47 | private final Map map = new LinkedHashMap<>();
48 |
49 | void add(String key, String val) {
50 | map.put(key, val);
51 | }
52 |
53 | @SuppressWarnings("unchecked")
54 | private void loadMap(Map map, String path) {
55 | for (Map.Entry entry : map.entrySet()) {
56 | String key = entry.getKey();
57 | if (path != null) {
58 | key = path + "." + key;
59 | }
60 | Object val = entry.getValue();
61 | if (val instanceof Map) {
62 | loadMap((Map) val, key);
63 | } else {
64 | addScalar(key, val);
65 | }
66 | }
67 | }
68 |
69 | private void addScalar(String key, Object val) {
70 | if (val instanceof String) {
71 | add(key, (String) val);
72 | } else if (val instanceof Number || val instanceof Boolean) {
73 | add(key, val.toString());
74 | } else if (val instanceof Collection>) {
75 | add(key, String.join(",", (Iterable) val));
76 | }
77 | }
78 |
79 | private Map map() {
80 | return map;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/avaje-dynamic-logback/src/main/java/io/avaje/config/dynamiclogback/LogbackPlugin.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.dynamiclogback;
2 |
3 | import static java.lang.System.Logger.Level.DEBUG;
4 | import static java.lang.System.Logger.Level.TRACE;
5 |
6 | import org.slf4j.LoggerFactory;
7 |
8 | import ch.qos.logback.classic.Level;
9 | import ch.qos.logback.classic.Logger;
10 | import ch.qos.logback.classic.LoggerContext;
11 | import io.avaje.applog.AppLog;
12 | import io.avaje.config.Configuration;
13 | import io.avaje.config.ConfigurationPlugin;
14 | import io.avaje.config.ModificationEvent;
15 | import io.avaje.spi.ServiceProvider;
16 |
17 | /**
18 | * Plugin to dynamically adjust the log levels via configuration changes.
19 | */
20 | @ServiceProvider
21 | public final class LogbackPlugin implements ConfigurationPlugin {
22 |
23 | static {
24 | // see #204. Without this called statically, the same call in the constructor
25 | // may return SubstituteLoggerFactory and fail to cast.
26 | LoggerFactory.getILoggerFactory();
27 | }
28 |
29 | private static final System.Logger log = AppLog.getLogger(LogbackPlugin.class);
30 |
31 | @Override
32 | public void apply(Configuration configuration) {
33 | final var loggerContext = loggerContext();
34 | final var config = configuration.forPath("log.level");
35 | for (String key : config.keys()) {
36 | String rawLevel = config.getNullable(key);
37 | setLogLevel(key, loggerContext, rawLevel);
38 | log.log(TRACE, "log level {0} for {1}", rawLevel, key);
39 | }
40 | configuration.onChange(this::onChangeAny);
41 | }
42 |
43 | private static void setLogLevel(String key, LoggerContext loggerContext, String level) {
44 | Logger logger = loggerContext.getLogger(key);
45 | if (logger != null && level != null) {
46 | logger.setLevel(Level.toLevel(level));
47 | }
48 | }
49 |
50 | private void onChangeAny(ModificationEvent modificationEvent) {
51 | final var loggerContext = loggerContext();
52 | final var config = modificationEvent.configuration();
53 | modificationEvent.modifiedKeys().stream()
54 | .filter(key -> key.startsWith("log.level."))
55 | .forEach(key -> {
56 | String logKey = key.substring(10);
57 | String rawLevel = config.getNullable(key);
58 | setLogLevel(logKey, loggerContext, rawLevel);
59 | log.log(DEBUG, "set log level {0} for {1}", rawLevel, logKey);
60 | });
61 | }
62 |
63 | private LoggerContext loggerContext() {
64 | return (LoggerContext) LoggerFactory.getILoggerFactory();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/ConfigServiceLoader.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.ServiceLoader;
6 |
7 | /**
8 | * Load all the avaje-config extensions via ServiceLoader using the single
9 | * common ConfigExtension interface.
10 | */
11 | final class ConfigServiceLoader {
12 |
13 | private static final ConfigServiceLoader INSTANCE = new ConfigServiceLoader();
14 |
15 | static ConfigServiceLoader get() {
16 | return INSTANCE;
17 | }
18 |
19 | private final ConfigurationLog log;
20 | private final ResourceLoader resourceLoader;
21 | private final ModificationEventRunner eventRunner;
22 | private final List sources = new ArrayList<>();
23 | private final List plugins = new ArrayList<>();
24 | private final Parsers parsers;
25 |
26 | ConfigServiceLoader() {
27 | ModificationEventRunner _eventRunner = null;
28 | ConfigurationLog _log = null;
29 | ResourceLoader _resourceLoader = null;
30 | List otherParsers = new ArrayList<>();
31 |
32 | for (var spi : ServiceLoader.load(ConfigExtension.class)) {
33 | if (spi instanceof ConfigurationSource) {
34 | sources.add((ConfigurationSource) spi);
35 | } else if (spi instanceof ConfigurationPlugin) {
36 | plugins.add((ConfigurationPlugin) spi);
37 | } else if (spi instanceof ConfigParser) {
38 | otherParsers.add((ConfigParser) spi);
39 | } else if (spi instanceof ConfigurationLog) {
40 | _log = (ConfigurationLog) spi;
41 | } else if (spi instanceof ResourceLoader) {
42 | _resourceLoader = (ResourceLoader) spi;
43 | } else if (spi instanceof ModificationEventRunner) {
44 | _eventRunner = (ModificationEventRunner) spi;
45 | }
46 | }
47 |
48 | this.log = _log == null ? new DefaultConfigurationLog() : _log;
49 | this.resourceLoader = _resourceLoader == null ? new DefaultResourceLoader() : _resourceLoader;
50 | this.eventRunner = _eventRunner == null ? new CoreConfiguration.ForegroundEventRunner() : _eventRunner;
51 | this.parsers = new Parsers(otherParsers);
52 | }
53 |
54 | Parsers parsers() {
55 | return parsers;
56 | }
57 |
58 | ConfigurationLog log() {
59 | return log;
60 | }
61 |
62 | ResourceLoader resourceLoader() {
63 | return resourceLoader;
64 | }
65 |
66 | ModificationEventRunner eventRunner() {
67 | return eventRunner;
68 | }
69 |
70 | List sources() {
71 | return sources;
72 | }
73 |
74 | List plugins() {
75 | return plugins;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/avaje-aws-appconfig/src/main/java/io/avaje/config/appconfig/DAppConfigFetcher.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.appconfig;
2 |
3 | import java.net.ConnectException;
4 | import java.net.URI;
5 | import java.net.http.HttpClient;
6 | import java.net.http.HttpRequest;
7 | import java.net.http.HttpResponse;
8 |
9 | final class DAppConfigFetcher implements AppConfigFetcher {
10 |
11 | private final URI uri;
12 | private final HttpClient httpClient;
13 |
14 | DAppConfigFetcher(String uri) {
15 | this.uri = URI.create(uri);
16 | this.httpClient = HttpClient.newBuilder()
17 | .build();
18 | }
19 |
20 | @Override
21 | public URI uri() {
22 | return uri;
23 | }
24 |
25 | @Override
26 | public AppConfigFetcher.Result fetch() throws ConnectException, FetchException {
27 | HttpRequest request = HttpRequest.newBuilder()
28 | .uri(uri)
29 | .GET()
30 | .build();
31 |
32 | try {
33 | HttpResponse res = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
34 | String version = res.headers().firstValue("Configuration-Version").orElse(null);
35 | String contentType = res.headers().firstValue("Content-Type").orElse("unknown");
36 | String body = res.body();
37 | return new DResult(version, contentType, body);
38 |
39 | } catch (ConnectException e) {
40 | throw e; // expected on shutdown
41 | } catch (Exception e) {
42 | throw new FetchException(e);
43 | }
44 | }
45 |
46 | static class Builder implements AppConfigFetcher.Builder {
47 |
48 | private int port = 2772;
49 | private String application;
50 | private String environment;
51 | private String configuration;
52 |
53 | @Override
54 | public Builder application(String application) {
55 | this.application = application;
56 | return this;
57 | }
58 |
59 | @Override
60 | public Builder environment(String environment) {
61 | this.environment = environment;
62 | return this;
63 | }
64 |
65 | @Override
66 | public Builder configuration(String configuration) {
67 | this.configuration = configuration;
68 | return this;
69 | }
70 |
71 | @Override
72 | public Builder port(int port) {
73 | this.port = port;
74 | return this;
75 | }
76 |
77 | @Override
78 | public AppConfigFetcher build() {
79 | return new DAppConfigFetcher(uri());
80 | }
81 |
82 | private String uri() {
83 | if (configuration == null) {
84 | configuration = environment + "-" + application;
85 | }
86 | return "http://localhost:" + port + "/applications/"
87 | + application + "/environments/"
88 | + environment + "/configurations/"
89 | + configuration;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/avaje-config-toml/src/test/java/io/avaje/config/toml/TomlParserTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.toml;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.io.ByteArrayInputStream;
6 | import java.io.StringReader;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.Map;
9 |
10 | import static org.assertj.core.api.Assertions.*;
11 |
12 | class TomlParserTest {
13 |
14 | @Test
15 | void supportedExtensions() {
16 | var parser = new TomlParser();
17 | assertThat(parser.supportedExtensions()).isEqualTo(new String[]{"toml"});
18 | }
19 |
20 | private static String input() {
21 | return "key = \"c\"\n" +
22 | "\n" +
23 | "[one]\n" +
24 | "key1 = \"a\"\n" +
25 | "key2 = \"b\"\n" +
26 | "key3 = [\"a\", \"b\", \"c\"]\n" +
27 | "[two]\n" +
28 | "local_datetime = 2024-09-09T15:30:00\n" +
29 | "local_date = 2024-09-09\n" +
30 | "local_time = 15:30:00\n" +
31 | "offset_datetime = 2024-09-09T15:30:00+02:00";
32 | }
33 |
34 | @Test
35 | void load_reader() {
36 | var parser = new TomlParser();
37 | Map map = parser.load(new StringReader(input()));
38 |
39 | assertThat(map).hasSize(8);
40 | assertThat(map).containsOnlyKeys("key",
41 | "one.key1", "one.key2", "one.key3",
42 | "two.local_datetime", "two.local_date", "two.local_time", "two.offset_datetime");
43 |
44 | assertThat(map).containsEntry("key", "c");
45 |
46 | assertThat(map).containsEntry("one.key1", "a");
47 | assertThat(map).containsEntry("one.key2", "b");
48 | assertThat(map).containsEntry("one.key3", "a;b;c");
49 |
50 | assertThat(map).containsEntry("two.local_datetime", "2024-09-09T15:30");
51 | assertThat(map).containsEntry("two.local_date", "2024-09-09");
52 | assertThat(map).containsEntry("two.local_time", "15:30");
53 | assertThat(map).containsEntry("two.offset_datetime", "2024-09-09T15:30+02:00");
54 | }
55 |
56 | @Test
57 | void load_inputStream() {
58 | var parser = new TomlParser();
59 | Map map = parser.load(new ByteArrayInputStream(input().getBytes(StandardCharsets.UTF_8)));
60 |
61 | assertThat(map).hasSize(8);
62 | assertThat(map).containsOnlyKeys("key",
63 | "one.key1", "one.key2", "one.key3",
64 | "two.local_datetime", "two.local_date", "two.local_time", "two.offset_datetime");
65 |
66 | assertThat(map).containsEntry("key", "c");
67 |
68 | assertThat(map).containsEntry("one.key1", "a");
69 | assertThat(map).containsEntry("one.key2", "b");
70 | assertThat(map).containsEntry("one.key3", "a;b;c");
71 |
72 | assertThat(map).containsEntry("two.local_datetime", "2024-09-09T15:30");
73 | assertThat(map).containsEntry("two.local_date", "2024-09-09");
74 | assertThat(map).containsEntry("two.local_time", "15:30");
75 | assertThat(map).containsEntry("two.offset_datetime", "2024-09-09T15:30+02:00");
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreListValue.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.Collections;
6 | import java.util.List;
7 | import java.util.function.Function;
8 |
9 | final class CoreListValue implements Configuration.ListValue {
10 |
11 | private final CoreConfiguration config;
12 |
13 | public CoreListValue(CoreConfiguration config) {
14 | this.config = config;
15 | }
16 |
17 | @Override
18 | public List of(String key) {
19 | final String val = config.value(key);
20 | return val == null ? Collections.emptyList() : split(val);
21 | }
22 |
23 | @Override
24 | public List of(String key, String... defaultValues) {
25 | final String val = config.value(key);
26 | return val == null ? Arrays.asList(defaultValues) : split(val);
27 | }
28 |
29 | @Override
30 | public List ofInt(String key) {
31 | return splitInt(config.value(key));
32 | }
33 |
34 | @Override
35 | public List ofInt(String key, int... defaultValues) {
36 | final String val = config.value(key);
37 | return val == null ? intDefaults(defaultValues) : splitInt(val);
38 | }
39 |
40 | private static List intDefaults(int[] defaultValues) {
41 | final List ints = new ArrayList<>(defaultValues.length);
42 | for (final int defaultVal : defaultValues) {
43 | ints.add(defaultVal);
44 | }
45 | return ints;
46 | }
47 |
48 | @Override
49 | public List ofLong(String key) {
50 | return splitLong(config.value(key));
51 | }
52 |
53 | @Override
54 | public List ofLong(String key, long... defaultValues) {
55 | final String val = config.value(key);
56 | return val == null ? longDefaults(defaultValues) : splitLong(val);
57 | }
58 |
59 | private static List longDefaults(long[] defaultValues) {
60 | final List ints = new ArrayList<>(defaultValues.length);
61 | for (final long defaultVal : defaultValues) {
62 | ints.add(defaultVal);
63 | }
64 | return ints;
65 | }
66 |
67 | @Override
68 | public List ofType(String key, Function function) {
69 | final String val = config.value(key);
70 | try {
71 | return splitAs(val, function);
72 | } catch (final Exception e) {
73 | throw new IllegalStateException("Failed to convert key: " + key + " with the provided function", e);
74 | }
75 | }
76 |
77 | List split(String allValues) {
78 | return Arrays.asList(allValues.split(","));
79 | }
80 |
81 | List splitInt(String allValues) {
82 | return splitAs(allValues, Integer::parseInt);
83 | }
84 |
85 | List splitLong(String allValues) {
86 | return splitAs(allValues, Long::parseLong);
87 | }
88 |
89 | List splitAs(String allValues, Function function) {
90 | if (allValues == null) {
91 | return Collections.emptyList();
92 | }
93 | final List list = new ArrayList<>();
94 | for (final var value : allValues.split(",")) {
95 | list.add(function.apply(value));
96 | }
97 | return list;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreSetValue.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.util.Collections;
4 | import java.util.LinkedHashSet;
5 | import java.util.Set;
6 | import java.util.function.Function;
7 |
8 | final class CoreSetValue implements Configuration.SetValue {
9 |
10 | private final CoreConfiguration config;
11 |
12 | CoreSetValue(CoreConfiguration config) {
13 | this.config = config;
14 | }
15 |
16 | @Override
17 | public Set of(String key) {
18 | final String val = config.value(key);
19 | return val == null ? Collections.emptySet() : split(val);
20 | }
21 |
22 | @Override
23 | public Set of(String key, String... defaultValues) {
24 | final String val = config.value(key);
25 | return val == null ? stringDefaults(defaultValues) : split(val);
26 | }
27 |
28 | private static Set stringDefaults(String[] defaultValues) {
29 | final Set values = new LinkedHashSet<>();
30 | Collections.addAll(values, defaultValues);
31 | return values;
32 | }
33 |
34 | @Override
35 | public Set ofInt(String key) {
36 | return splitInt(config.value(key));
37 | }
38 |
39 | @Override
40 | public Set ofInt(String key, int... defaultValues) {
41 | final String val = config.value(key);
42 | return val == null ? intDefaults(defaultValues) : splitInt(val);
43 | }
44 |
45 | private static Set intDefaults(int[] defaultValues) {
46 | final Set ints = new LinkedHashSet<>();
47 | for (final int defaultVal : defaultValues) {
48 | ints.add(defaultVal);
49 | }
50 | return ints;
51 | }
52 |
53 | @Override
54 | public Set ofLong(String key) {
55 | return splitLong(config.value(key));
56 | }
57 |
58 | @Override
59 | public Set ofLong(String key, long... defaultValues) {
60 | final String val = config.value(key);
61 | return val == null ? longDefaults(defaultValues) : splitLong(val);
62 | }
63 |
64 | private static Set longDefaults(long[] defaultValues) {
65 | final Set ints = new LinkedHashSet<>();
66 | for (final long defaultVal : defaultValues) {
67 | ints.add(defaultVal);
68 | }
69 | return ints;
70 | }
71 |
72 | @Override
73 | public Set ofType(String key, Function function) {
74 | final String val = config.value(key);
75 | try {
76 | return splitAs(val, function);
77 | } catch (final Exception e) {
78 | throw new IllegalStateException("Failed to convert key: " + key + " with the provided function", e);
79 | }
80 | }
81 |
82 | Set split(String allValues) {
83 | return stringDefaults(allValues.split(","));
84 | }
85 |
86 | Set splitInt(String allValues) {
87 | return splitAs(allValues, Integer::parseInt);
88 | }
89 |
90 | Set splitLong(String allValues) {
91 | return splitAs(allValues, Long::parseLong);
92 | }
93 |
94 | Set splitAs(String allValues, Function function) {
95 | if (allValues == null) {
96 | return Collections.emptySet();
97 | }
98 | final Set set = new LinkedHashSet<>();
99 | for (final var value : allValues.split(",")) {
100 | set.add(function.apply(value));
101 | }
102 | return set;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/avaje-config/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | org.avaje
6 | java11-oss
7 | 5.1
8 |
9 |
10 |
11 | Avaje Config
12 | io.avaje
13 | avaje-config
14 | 5.0
15 |
16 |
17 | scm:git:git@github.com:avaje/avaje-config.git
18 | scm:git:git@github.com:avaje/avaje-config.git
19 | HEAD
20 |
21 |
22 |
23 | 2.5
24 | true
25 | false
26 | 2025-12-16T08:12:58Z
27 |
28 |
29 |
30 |
31 |
32 | org.jspecify
33 | jspecify
34 | 1.0.0
35 |
36 |
37 |
38 | io.avaje
39 | avaje-spi-service
40 | 2.14
41 | true
42 |
43 |
44 |
45 | io.avaje
46 | avaje-applog
47 | 1.2
48 |
49 |
50 |
51 |
52 | org.yaml
53 | snakeyaml
54 | ${snakeyaml.version}
55 | true
56 |
57 |
58 |
59 | io.avaje
60 | junit
61 | 1.7
62 | test
63 |
64 |
65 |
66 | ch.qos.logback
67 | logback-classic
68 | 1.5.22
69 | test
70 |
71 |
72 |
73 | io.avaje
74 | avaje-applog-slf4j
75 | 1.0
76 | test
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | org.apache.maven.plugins
85 | maven-repository-plugin
86 | 2.4
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/CoreExpressionEvalTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.Properties;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 | import static org.junit.jupiter.api.Assertions.assertEquals;
9 | import static org.junit.jupiter.api.Assertions.assertNull;
10 |
11 | class CoreExpressionEvalTest {
12 |
13 | @Test
14 | void eval_null() {
15 | assertNull(eval(null));
16 | }
17 |
18 | @Test
19 | void eval_empty() {
20 | assertEquals("", eval(""));
21 | }
22 |
23 | @Test
24 | void eval_noExpressions() {
25 | assertEquals("basic", eval("basic"));
26 | assertEquals("{basic}", eval("{basic}"));
27 | }
28 |
29 | @Test
30 | void eval_singleExpression() {
31 | System.setProperty("foo", "Hello");
32 | assertEquals("Hello", eval("${foo}"));
33 | assertEquals("preHello", eval("pre${foo}"));
34 | assertEquals("HelloPost", eval("${foo}Post"));
35 | assertEquals("beforeHelloAfter", eval("before${foo}After"));
36 | System.clearProperty("foo");
37 | }
38 |
39 | @Test
40 | void eval_singleExpression_withDefault() {
41 | System.setProperty("foo", "Hello");
42 | assertEquals("Hello", eval("${foo:bart}"));
43 | assertEquals("beforeHelloAfter", eval("before${foo:bart}After"));
44 | assertEquals("preHello", eval("pre${foo:bart}"));
45 | assertEquals("HelloPost", eval("${foo:bart}Post"));
46 |
47 | System.clearProperty("foo");
48 | assertEquals("bart", eval("${foo:bart}"));
49 | assertEquals("before-bart-after", eval("before-${foo:bart}-after"));
50 | assertEquals("pre-bart", eval("pre-${foo:bart}"));
51 | assertEquals("bart-post", eval("${foo:bart}-post"));
52 | }
53 |
54 | @Test
55 | void eval_singleExpression_withDefaultIncludesColons() {
56 | assertEquals("jdbc:postgresql://localhost:7432/myapp", eval("${db.url:jdbc:postgresql://localhost:7432/myapp}"));
57 |
58 | System.setProperty("db.url", "jdbc:postgresql://foo:7432/bar");
59 | assertEquals("jdbc:postgresql://foo:7432/bar", eval("${db.url:jdbc:postgresql://localhost:7432/myapp}"));
60 |
61 | System.clearProperty("db.url");
62 | }
63 |
64 | @Test
65 | void eval_multiExpression_withDefault() {
66 | assertEquals("num1num2", eval("${one:num1}${two:num2}"));
67 | assertEquals("num1-num2", eval("${one:num1}-${two:num2}"));
68 | assertEquals("num1abnum2", eval("${one:num1}ab${two:num2}"));
69 | assertEquals("anum1bcnum2d", eval("a${one:num1}bc${two:num2}d"));
70 |
71 | System.setProperty("one", "first");
72 | System.setProperty("two", "second");
73 |
74 | assertEquals("firstsecond", eval("${one:num1}${two:num2}"));
75 | assertEquals("first-second", eval("${one:num1}-${two:num2}"));
76 | assertEquals("pre-first-second-post", eval("pre-${one:num1}-${two:num2}-post"));
77 | assertEquals("AfirstBCsecondD", eval("A${one:num1}BC${two:num2}D"));
78 |
79 |
80 | System.clearProperty("one");
81 | System.clearProperty("two");
82 | }
83 |
84 | @Test
85 | void eval_withSourceMap() {
86 | CoreEntry.CoreMap source = CoreEntry.newMap();
87 | source.put("one", "1","");
88 | source.put("two", "2","");
89 | final CoreExpressionEval exprEval = new CoreExpressionEval(source);
90 |
91 | assertThat(exprEval.eval("foo${one}bar${two}baz${one}")).isEqualTo("foo1bar2baz1");
92 | assertThat(exprEval.eval("foo{one}bar{two}")).isEqualTo("foo{one}bar{two}");
93 | assertThat(exprEval.eval("${one}${two}${one}")).isEqualTo("121");
94 | }
95 |
96 | @Test
97 | void eval_withSourceProperties() {
98 | Properties source = new Properties();
99 | source.put("one", "1");
100 | source.put("two", "2");
101 | final CoreExpressionEval exprEval = new CoreExpressionEval(source);
102 |
103 | assertThat(exprEval.eval("foo${one}bar${two}baz${one}")).isEqualTo("foo1bar2baz1");
104 | assertThat(exprEval.eval("foo{one}bar{two}")).isEqualTo("foo{one}bar{two}");
105 | assertThat(exprEval.eval("${one}${two}${one}")).isEqualTo("121");
106 | }
107 |
108 | private String eval(String key) {
109 | return new CoreExpressionEval(CoreEntry.newMap()).eval(key);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/FileWatch.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.io.FileNotFoundException;
6 | import java.io.InputStream;
7 | import java.lang.System.Logger.Level;
8 | import java.util.*;
9 |
10 | final class FileWatch {
11 |
12 | private final ConfigurationLog log;
13 | private final Configuration configuration;
14 | private final Parsers parsers;
15 | private final List files;
16 | private final long delay;
17 | private final long period;
18 |
19 | FileWatch(CoreConfiguration configuration, List loadedFiles, Parsers parsers) {
20 | this.log = configuration.log();
21 | this.configuration = configuration;
22 | this.delay = configuration.getLong("config.watch.delay", 60);
23 | this.period = configuration.getLong("config.watch.period", 10);
24 | this.parsers = parsers;
25 | this.files = initFiles(loadedFiles);
26 | if (files.isEmpty()) {
27 | log.log(Level.ERROR, "No files to watch?");
28 | } else {
29 | configuration.schedule(delay * 1000, period * 1000, this::check);
30 | }
31 | }
32 |
33 | @Override
34 | public String toString() {
35 | return "Watch[period:" + period + " delay:" + delay + " files:" + files + "]";
36 | }
37 |
38 | private List initFiles(List loadedFiles) {
39 | List entries = new ArrayList<>(loadedFiles.size());
40 | for (File loadedFile : loadedFiles) {
41 | entries.add(new Entry(loadedFile));
42 | }
43 | return entries;
44 | }
45 |
46 | boolean changed() {
47 | for (Entry file : files) {
48 | if (file.changed()) {
49 | return true;
50 | }
51 | }
52 | return false;
53 | }
54 |
55 | void check() {
56 | final Map keyValues = new LinkedHashMap<>();
57 | for (Entry file : files) {
58 | if (file.reload()) {
59 | log.log(Level.DEBUG, "reloading configuration from {0}", file);
60 | if (file.isCustom()) {
61 | reloadYaml(file, keyValues);
62 | } else {
63 | reloadProps(file, keyValues);
64 | }
65 | }
66 | }
67 | final var builder = configuration.eventBuilder("reload");
68 | keyValues.forEach(builder::put);
69 | builder.publish();
70 | }
71 |
72 | private void reloadProps(Entry file, Map keyValues) {
73 | try (InputStream is = file.inputStream()) {
74 | final var properties = new Properties();
75 | properties.load(is);
76 | Enumeration> enumeration = properties.propertyNames();
77 | while (enumeration.hasMoreElements()) {
78 | final String key = (String) enumeration.nextElement();
79 | keyValues.put(key, properties.getProperty(key));
80 | }
81 | } catch (Exception e) {
82 | log.log(Level.ERROR, "Unexpected error reloading config file " + file, e);
83 | }
84 | }
85 |
86 | private void reloadYaml(Entry file, Map keyValues) {
87 | var parser = parsers.get(file.extension);
88 | if (parser == null) {
89 | log.log(Level.ERROR, "Unexpected - no parser to reload config file " + file);
90 | } else {
91 | try (InputStream is = file.inputStream()) {
92 | keyValues.putAll(parser.load(is));
93 | } catch (Exception e) {
94 | log.log(Level.ERROR, "Unexpected error reloading config file " + file, e);
95 | }
96 | }
97 | }
98 |
99 | private static class Entry {
100 | private final File file;
101 | private final boolean customExtension;
102 | private final String extension;
103 | private long lastMod;
104 | private long lastLength;
105 |
106 | Entry(File file) {
107 | this.file = file;
108 | this.lastMod = file.lastModified();
109 | this.lastLength = file.length();
110 | var name = file.getName();
111 | this.extension = name.substring(name.lastIndexOf(".") + 1);
112 | this.customExtension = !"properties".equals(extension);
113 | }
114 |
115 | @Override
116 | public String toString() {
117 | return file.toString();
118 | }
119 |
120 | boolean isCustom() {
121 | return customExtension;
122 | }
123 |
124 | boolean reload() {
125 | if (!changed()) {
126 | return false;
127 | }
128 | lastMod = file.lastModified();
129 | lastLength = file.length();
130 | return true;
131 | }
132 |
133 | boolean changed() {
134 | return file.lastModified() > lastMod || file.length() != lastLength;
135 | }
136 |
137 | InputStream inputStream() {
138 | try {
139 | return new FileInputStream(file);
140 | } catch (FileNotFoundException e) {
141 | throw new RuntimeException(e);
142 | }
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://discord.gg/Qcqf9R27BR)
2 | [](https://mvnrepository.com/artifact/io.avaje/avaje-config)
3 | [](https://javadoc.io/doc/io.avaje/avaje-config/latest/io.avaje.config/io/avaje/config/package-summary.html)
4 | [](https://github.com/avaje/avaje-config/blob/master/LICENSE)
5 | [](https://github.com/avaje/avaje-config/actions/workflows/build.yml)
6 | [](https://github.com/avaje/avaje-config/actions/workflows/native-image.yml)
7 |
8 | # [Avaje Config](https://avaje.io/config/)
9 |
10 | This library loads properties files that can be used to configure
11 | an application including "testing" and "local development" and
12 | dynamic configuration (changes to configuration properties at runtime).
13 |
14 | ```xml
15 |
16 | io.avaje
17 | avaje-config
18 | ${avaje.config.version}
19 |
20 | ```
21 |
22 | ## Typical use
23 |
24 | - Put application.yaml into src/main/resources for properties that have reasonable defaults
25 | - Put application-test.yaml into src/test/resources for properties used when running tests
26 | - Specify external properties via command line arguments. These effectively override application.yaml properties.
27 |
28 |
29 | ## Config use
30 |
31 | Getting property values
32 | ```java
33 |
34 | // get a String property
35 | String value = Config.get("myapp.foo");
36 |
37 | // with a default value
38 | String value = Config.get("myapp.foo", "withDefaultValue");
39 |
40 | // also int, long and boolean with and without default values
41 | int intVal = Config.getInt("bar");
42 | long longVal = Config.getLong("bar");
43 | boolean booleanVal = Config.getBool("bar");
44 |
45 | ```
46 | Register callback on property change.
47 | ```java
48 |
49 | Config.onChange("myapp.foo", newValue -> {
50 | // do something ...
51 | });
52 |
53 | Config.onChangeInt("myapp.foo", newIntValue -> {
54 | // do something ...
55 | });
56 |
57 | Config.onChangeLong("myapp.foo", newLongValue -> {
58 | // do something ...
59 | });
60 |
61 | Config.onChangeBool("myapp.foo", newBooleanValue -> {
62 | // do something ...
63 | });
64 |
65 | ```
66 |
67 | ## Loading properties
68 |
69 | Config loads properties from expected locations as well as via command line arguments.
70 | Below is the how it looks for configuration properties.
71 |
72 | - loads from main resources (if they exist)
73 | - application.yaml
74 | - application.properties
75 |
76 | - loads files from the current working directory (if they exist)
77 | - application.yaml
78 | - application.properties
79 |
80 | - loads via system property `props.file` or environment variable `PROPS_FILE` (if defined)
81 |
82 | - loads via system property `avaje.profiles` or environment variable `AVAJE_PROFILES` (if defined).
83 |
84 | Setting the `config.profiles` or environment variable `CONFIG_PROFILES` will cause avaje config to load the property files in the form `application-${profile}.properties` (will also work for yml/yaml files).
85 |
86 | For example, if you set the `config.profiles` to `dev,docker` it will attempt to load `application-dev.properties` and `application-docker.properties`.
87 |
88 | - loads via `load.properties` property.
89 |
90 | We can define a `load.properties` property which has name of property file in resource folder, or path locations for other properties/yaml files to load.
91 |
92 | `load.properties` is pretty versatile and can even be chained. For example, in your main application properties, you can have `load.properties=application-${profile:local}.properties` to load based on another property, and in the loaded properties you can add `load.properties` there to load more properties, and so on.
93 |
94 | Example application.properties:
95 | ```
96 | common.property=value
97 | load.properties=application-${profile:local}.properties,path/to/prop/application-extra2.properties
98 | ```
99 |
100 |
101 | - loads test resources (if they exist, nb: Test resources are only visible when running tests)
102 | - application-test.properties
103 | - application-test.yaml
104 |
105 |
106 | If no test resources were loaded then it additionally loads from "local dev" and command line:
107 |
108 | - loads from "local dev".
109 |
110 | We can specify an `app.name` property and then put a properties/yaml file at: `${user.home}/.localdev/{appName}.yaml`
111 | We do this to set/override properties when we want to run the application locally (aka main method)
112 |
113 | - loads from command line arguments
114 |
115 | Command line arguments starting with `-P` can specify properties/yaml files to load
116 |
117 |
118 | When properties are loaded they are merged/overlayed.
119 |
120 | ### config.load.systemProperties
121 | If we set `config.load.systemProperties` to true then all the properties that have been loaded are then set into system properties.
122 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreConfigurationBuilder.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import org.jspecify.annotations.NullMarked;
4 | import org.jspecify.annotations.Nullable;
5 |
6 | import java.io.File;
7 | import java.io.FileReader;
8 | import java.io.IOException;
9 | import java.io.UncheckedIOException;
10 | import java.util.Map;
11 | import java.util.Properties;
12 |
13 | import static java.lang.System.Logger.Level.DEBUG;
14 | import static java.lang.System.Logger.Level.INFO;
15 | import static java.util.Objects.requireNonNull;
16 |
17 | @NullMarked
18 | final class CoreConfigurationBuilder implements Configuration.Builder {
19 |
20 | private final CoreEntry.CoreMap sourceMap = CoreEntry.newMap();
21 | private final ConfigServiceLoader serviceLoader = ConfigServiceLoader.get();
22 | private final Parsers parsers = serviceLoader.parsers();
23 | private ConfigurationLog log = serviceLoader.log();
24 | private ResourceLoader resourceLoader = serviceLoader.resourceLoader();
25 | private ModificationEventRunner eventRunner = serviceLoader.eventRunner();
26 | private boolean includeResourceLoading;
27 | private @Nullable InitialLoader initialLoader;
28 |
29 | @Override
30 | public Configuration.Builder eventRunner(ModificationEventRunner eventRunner) {
31 | this.eventRunner = requireNonNull(eventRunner);
32 | return this;
33 | }
34 |
35 | @Override
36 | public Configuration.Builder log(ConfigurationLog log) {
37 | this.log = requireNonNull(log);
38 | return this;
39 | }
40 |
41 | @Override
42 | public Configuration.Builder resourceLoader(ResourceLoader resourceLoader) {
43 | this.resourceLoader = requireNonNull(resourceLoader);
44 | return this;
45 | }
46 |
47 | @Override
48 | public Configuration.Builder put(String key, String value) {
49 | put(requireNonNull(key), requireNonNull(value), "initial");
50 | return this;
51 | }
52 |
53 | @Override
54 | public Configuration.Builder putAll(Map source) {
55 | requireNonNull(source);
56 | source.forEach((key, value) -> {
57 | if (key != null && value != null) {
58 | put(key, value.toString(), "initial");
59 | }
60 | });
61 | return this;
62 | }
63 |
64 | @Override
65 | public Configuration.Builder putAll(Properties source) {
66 | requireNonNull(source);
67 | source.forEach((key, value) -> {
68 | if (key != null && value != null) {
69 | put(key.toString(), value.toString(), "initial");
70 | }
71 | });
72 | return this;
73 | }
74 |
75 | @Override
76 | public Configuration.Builder load(String resource) {
77 | final var configParser = parser(resource);
78 | try {
79 | try (var inputStream = resourceLoader.getResourceAsStream(resource)) {
80 | if (inputStream == null) {
81 | log.log(INFO, "Configuration resource:{0} not found", resource);
82 | } else {
83 | var source = "resource:" + resource;
84 | configParser.load(inputStream).forEach((k, v) -> put(k, v, source));
85 | log.log(DEBUG, "loaded {0}", source);
86 | }
87 | return this;
88 | }
89 | } catch (IOException e) {
90 | throw new UncheckedIOException(e);
91 | }
92 | }
93 |
94 | @Override
95 | public Configuration.Builder load(File file) {
96 | if (!file.exists()) {
97 | log.log(INFO, "Configuration file:{0} not found", file);
98 | return this;
99 | }
100 | final var configParser = parser(file.getName());
101 | try {
102 | try (var reader = new FileReader(file)) {
103 | var source = "file:" + file.getName();
104 | configParser.load(reader).forEach((k, v) -> put(k, v, source));
105 | log.log(DEBUG, "loaded {0}", source);
106 | return this;
107 | }
108 | } catch (IOException e) {
109 | throw new UncheckedIOException(e);
110 | }
111 | }
112 |
113 | private void put(String key, String value, String source) {
114 | sourceMap.put(key, DefaultValues.overrideValue(key, value, source));
115 | }
116 |
117 | private ConfigParser parser(String name) {
118 | int pos = name.lastIndexOf('.');
119 | if (pos == -1) {
120 | throw new IllegalArgumentException("Unable to determine the extension for " + name);
121 | }
122 | var extension = name.substring(pos + 1);
123 | ConfigParser configParser = parsers.get(extension);
124 | if (configParser == null) {
125 | throw new IllegalArgumentException("No parser registered for extension " + extension);
126 | }
127 | return configParser;
128 | }
129 |
130 | @Override
131 | public Configuration.Builder includeResourceLoading() {
132 | this.includeResourceLoading = true;
133 | return this;
134 | }
135 |
136 | @Override
137 | public Configuration build() {
138 | var components = new CoreComponents(
139 | eventRunner,
140 | log,
141 | parsers,
142 | serviceLoader.sources(),
143 | serviceLoader.plugins()
144 | );
145 | if (includeResourceLoading) {
146 | log.preInitialisation();
147 | initialLoader = new InitialLoader(components, resourceLoader);
148 | }
149 | return new CoreConfiguration(components, initEntries()).postLoad(initialLoader);
150 | }
151 |
152 | private CoreEntry.CoreMap initEntries() {
153 | final var entries = initEntryMap();
154 | entries.addAll(sourceMap);
155 | return CoreExpressionEval.evalFor(entries);
156 | }
157 |
158 | private CoreEntry.CoreMap initEntryMap() {
159 | return initialLoader == null ? CoreEntry.newMap() : initialLoader.load();
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreEntry.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import static java.util.Objects.requireNonNull;
4 |
5 | import java.util.*;
6 | import java.util.concurrent.ConcurrentHashMap;
7 | import java.util.function.BiConsumer;
8 |
9 | import org.jspecify.annotations.NullMarked;
10 | import org.jspecify.annotations.Nullable;
11 |
12 | /**
13 | * Configuration entry.
14 | */
15 | @NullMarked
16 | final class CoreEntry implements Configuration.Entry {
17 |
18 | /**
19 | * Entry used to represent no entry / null.
20 | */
21 | static final CoreEntry NULL_ENTRY = new CoreEntry();
22 |
23 | private @Nullable final String value;
24 | private final boolean boolValue;
25 | private @Nullable final String source;
26 |
27 | /**
28 | * Return a new empty entryMap for entries.
29 | */
30 | static CoreMap newMap() {
31 | return new CoreEntry.CoreMap();
32 | }
33 |
34 | /**
35 | * Return a copy of the entryMap given the source.
36 | */
37 | static CoreMap newMap(CoreMap source) {
38 | return new CoreEntry.CoreMap(source);
39 | }
40 |
41 | /**
42 | * Return a new entryMap populated from the given Properties.
43 | *
44 | * @param propSource where these properties came from
45 | */
46 | static CoreMap newMap(Properties source, String propSource) {
47 | return new CoreEntry.CoreMap(source, propSource);
48 | }
49 |
50 | /**
51 | * Return an entry for the given value.
52 | */
53 | static CoreEntry of(@Nullable String val, String source) {
54 | return val == null ? NULL_ENTRY : new CoreEntry(val, source);
55 | }
56 |
57 | /**
58 | * Construct for our special NULL entry.
59 | */
60 | private CoreEntry() {
61 | this.value = null;
62 | this.boolValue = false;
63 | this.source = null;
64 | }
65 |
66 | private CoreEntry(String value, String source) {
67 | requireNonNull(value);
68 | this.value = value;
69 | this.boolValue = Boolean.parseBoolean(value);
70 | this.source = source;
71 | }
72 |
73 | @Override
74 | public String toString() {
75 | return '{' + value + " source:" + source + '}';
76 | }
77 |
78 | boolean needsEvaluation() {
79 | return value != null && value.contains("${");
80 | }
81 |
82 | @Override
83 | @Nullable
84 | public String value() {
85 | return value;
86 | }
87 |
88 | boolean boolValue() {
89 | return boolValue;
90 | }
91 |
92 | @Override
93 | @Nullable
94 | public String source() {
95 | return source;
96 | }
97 |
98 | boolean isNull() {
99 | return value == null;
100 | }
101 |
102 | /**
103 | * A entryMap like container of CoreEntry entries.
104 | */
105 | static class CoreMap {
106 |
107 | private final Map entryMap = new ConcurrentHashMap<>();
108 |
109 | CoreMap() {
110 | }
111 |
112 | CoreMap(CoreMap source) {
113 | entryMap.putAll(source.entryMap);
114 | }
115 |
116 | CoreMap(Properties source, String sourceName) {
117 | source.forEach((key, value) -> {
118 | if (value != null) {
119 | entryMap.put(key.toString(), CoreEntry.of(value.toString(), sourceName));
120 | }
121 | });
122 | }
123 |
124 | /**
125 | * Add all the entries from another source.
126 | */
127 | void addAll(CoreMap source) {
128 | entryMap.putAll(source.entryMap);
129 | }
130 |
131 | int size() {
132 | return entryMap.size();
133 | }
134 |
135 | @Nullable
136 | CoreEntry get(String key) {
137 | return entryMap.get(key);
138 | }
139 |
140 | /**
141 | * Apply changes returning the set of modified keys.
142 | */
143 | Set applyChanges(CoreEventBuilder eventBuilder) {
144 | Set modifiedKeys = new HashSet<>();
145 | final var sourceName = "event:" + eventBuilder.name();
146 | eventBuilder.forEachPut((key, value) -> {
147 | if (value == null) {
148 | if (entryMap.remove(key) != null) {
149 | modifiedKeys.add(key);
150 | }
151 | } else if (putIfChanged(key, value, sourceName)) {
152 | modifiedKeys.add(key);
153 | }
154 | });
155 | return modifiedKeys;
156 | }
157 |
158 | /**
159 | * Return true if this is a change in value.
160 | */
161 | boolean isChanged(String key, String value) {
162 | final CoreEntry entry = entryMap.get(key);
163 | return entry == null || !Objects.equals(entry.value, value);
164 | }
165 |
166 | /**
167 | * Return true if this put resulted in a modification.
168 | */
169 | private boolean putIfChanged(String key, String value, String source) {
170 | final CoreEntry entry = entryMap.get(key);
171 | if (entry == null) {
172 | entryMap.put(key, CoreEntry.of(value, source));
173 | return true;
174 | } else if (!Objects.equals(entry.value, value)) {
175 | entryMap.put(key, CoreEntry.of(value, source + " <- " + entry.source));
176 | return true;
177 | }
178 | return false;
179 | }
180 |
181 | Set keys() {
182 | return entryMap.keySet();
183 | }
184 |
185 | boolean containsKey(String key) {
186 | return entryMap.containsKey(key);
187 | }
188 |
189 | void put(String key, CoreEntry value) {
190 | entryMap.put(key, value);
191 | }
192 |
193 | void put(String key, @Nullable String value, String source) {
194 | entryMap.put(key, CoreEntry.of(value, source));
195 | }
196 |
197 | @Nullable
198 | String raw(String key) {
199 | final var entry = entryMap.get(key);
200 | return entry == null ? null : entry.value();
201 | }
202 |
203 | void forEach(BiConsumer consumer) {
204 | entryMap.forEach(consumer);
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/InitialLoadContext.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import io.avaje.config.CoreEntry.CoreMap;
4 | import org.jspecify.annotations.Nullable;
5 |
6 | import java.io.*;
7 | import java.lang.System.Logger.Level;
8 | import java.util.*;
9 |
10 | /**
11 | * Manages the underlying map of properties we are gathering.
12 | */
13 | final class InitialLoadContext {
14 |
15 | private final ConfigurationLog log;
16 | private final ResourceLoader resourceLoader;
17 | /**
18 | * CoreMap we are loading the properties into.
19 | */
20 | private final CoreEntry.CoreMap map = CoreEntry.newMap();
21 |
22 | /**
23 | * Names of resources/files that were loaded.
24 | */
25 | private final Set loadedResources = new LinkedHashSet<>();
26 | private final List loadedFiles = new ArrayList<>();
27 | private final CoreExpressionEval exprEval;
28 | private final Set loadCheck = new HashSet<>();
29 | private int recursiveLoadCount;
30 |
31 | InitialLoadContext(ConfigurationLog log, ResourceLoader resourceLoader) {
32 | this.log = log;
33 | this.resourceLoader = resourceLoader;
34 | this.exprEval = new CoreExpressionEval(map);
35 | }
36 |
37 | Set loadedFrom() {
38 | return loadedResources;
39 | }
40 |
41 | List loadedFiles() {
42 | return loadedFiles;
43 | }
44 |
45 | String eval(String expression) {
46 | return exprEval.eval(expression);
47 | }
48 |
49 | /**
50 | * If we are in Kubernetes and expose environment variables
51 | * POD_NAMESPACE, POD_NAME, POD_VERSION, POD_ID we can use these to set
52 | * app.environment, app.name, app.instanceId, app.version and app.ipAddress.
53 | */
54 | void loadEnvironmentVars() {
55 | final String podName = System.getenv("POD_NAME");
56 | initSystemProperty(podName, "app.instanceId");
57 | initSystemProperty(podService(podName), "app.name");
58 | initSystemProperty(System.getenv("POD_NAMESPACE"), "app.environment");
59 | initSystemProperty(System.getenv("POD_VERSION"), "app.version");
60 | initSystemProperty(System.getenv("POD_IP"), "app.ipAddress");
61 | initSystemProperty(System.getenv("CONFIG_PROFILES"), "config.profiles");
62 | initSystemProperty(System.getenv("AVAJE_PROFILES"), "avaje.profiles");
63 | }
64 |
65 | private void initSystemProperty(String envValue, String key) {
66 | if (envValue != null && System.getProperty(key) == null) {
67 | map.put(key, envValue, Constants.ENV_VARIABLES);
68 | }
69 | }
70 |
71 | static String podService(String podName) {
72 | if (podName != null && podName.length() > 16) {
73 | int p0 = podName.lastIndexOf('-', podName.length() - 16);
74 | if (p0 > -1) {
75 | return podName.substring(0, p0);
76 | }
77 | }
78 | return null;
79 | }
80 |
81 | /**
82 | * Return the input stream (maybe null) for the given source.
83 | */
84 | @Nullable
85 | InputStream resource(String resourcePath, InitialLoader.Source source) {
86 | InputStream is = null;
87 | if (source == InitialLoader.Source.RESOURCE) {
88 | is = resourceStream(resourcePath);
89 | if (is != null) {
90 | loadedResources.add(source.key(resourcePath));
91 | loadCheck.add(resourcePath);
92 | }
93 | } else {
94 | File file = new File(resourcePath);
95 | if (file.exists()) {
96 | try {
97 | is = new FileInputStream(file);
98 | loadedResources.add(source.key(resourcePath));
99 | loadCheck.add(resourcePath);
100 | loadedFiles.add(file);
101 | } catch (FileNotFoundException e) {
102 | throw new UncheckedIOException(e);
103 | }
104 | }
105 | }
106 | return is;
107 | }
108 |
109 | private InputStream resourceStream(String resourcePath) {
110 | return resourceLoader.getResourceAsStream(resourcePath);
111 | }
112 |
113 | void put(String key, String val, String source) {
114 | if (val != null) {
115 | val = val.trim();
116 | }
117 | map.put(key, DefaultValues.overrideValue(key, val, source));
118 | }
119 |
120 | /**
121 | * Evaluate all the expressions and return as a Properties object.
122 | */
123 | CoreMap entryMap() {
124 | log.log(Level.TRACE, "load from {0}", loadedResources);
125 | return map;
126 | }
127 |
128 | /**
129 | * Read the special properties that can point to an external properties source.
130 | */
131 | String indirectLocation() {
132 | String location = System.getProperty("load.properties");
133 | if (location != null) {
134 | return location;
135 | }
136 | var indirectLocation = map.get("load.properties");
137 | if (indirectLocation == null) {
138 | indirectLocation = map.get("load.properties.override");
139 | }
140 | return indirectLocation == null ? null : indirectLocation.value();
141 | }
142 |
143 | String profiles() {
144 | final var configEntry = map.get("config.profiles");
145 | final var configProfile = configEntry == null ? System.getProperty("config.profiles") : configEntry.value();
146 | if (configProfile != null) {
147 | return configProfile;
148 | }
149 |
150 | final var avajeProfile = map.get("avaje.profiles");
151 | return avajeProfile == null ? System.getProperty("avaje.profiles") : avajeProfile.value();
152 | }
153 |
154 | /**
155 | * Return the number of properties resources loaded.
156 | */
157 | int size() {
158 | return loadedResources.size();
159 | }
160 |
161 | String getAppName() {
162 | final var appName = map.get("app.name");
163 | return (appName != null) ? appName.value() : System.getProperty("app.name");
164 | }
165 |
166 | boolean alreadyLoaded(String fileName) {
167 | return !loadCheck.add(fileName);
168 | }
169 |
170 | boolean allowRecursiveLoad() {
171 | if (recursiveLoadCount++ > 100) {
172 | throw new IllegalStateException("Recursive loading exceeded 100. Check your load.properties entries");
173 | }
174 | return true;
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/CoreExpressionEval.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import org.jspecify.annotations.Nullable;
4 |
5 | import java.util.Properties;
6 |
7 | import static java.util.Objects.requireNonNull;
8 |
9 | /**
10 | * Helper used to evaluate expressions such as ${CATALINA_HOME}.
11 | *
12 | * The expressions can contain environment variables or system properties.
13 | */
14 | final class CoreExpressionEval implements Configuration.ExpressionEval {
15 |
16 | /**
17 | * Used to detect the start of an expression.
18 | */
19 | private static final String START = "${";
20 |
21 | /**
22 | * Used to detect the end of an expression.
23 | */
24 | private static final String END = "}";
25 | private static final String DOCKER_HOST = "docker.host";
26 |
27 | private CoreEntry.CoreMap sourceMap;
28 | private Properties sourceProperties;
29 |
30 | /**
31 | * Create with source map that can use used to eval expressions.
32 | */
33 | CoreExpressionEval(CoreEntry.CoreMap sourceMap) {
34 | this.sourceMap = sourceMap;
35 | }
36 |
37 | /**
38 | * Create with source properties that can be used to eval expressions.
39 | */
40 | CoreExpressionEval(Properties sourceProperties) {
41 | this.sourceProperties = sourceProperties;
42 | }
43 |
44 | /**
45 | * Evaluate all the entries until no more can be resolved.
46 | *
47 | * @param map The source map which is copied and resolved
48 | * @return A copy of the source map will all entries resolved if possible
49 | */
50 | static CoreEntry.CoreMap evalFor(CoreEntry.CoreMap map) {
51 | final var copy = CoreEntry.newMap(map);
52 | return new CoreExpressionEval(copy).evalAll();
53 | }
54 |
55 | private CoreEntry.CoreMap evalAll() {
56 | sourceMap.forEach((key, entry) -> {
57 | if (entry.needsEvaluation()) {
58 | sourceMap.put(key, eval(entry.value()), requireNonNull(entry.source()));
59 | }
60 | });
61 | return sourceMap;
62 | }
63 |
64 | @Override
65 | @Nullable
66 | public String eval(@Nullable String val) {
67 | return val == null ? null : evalRecurse(val);
68 | }
69 |
70 | private String evalRecurse(String input) {
71 | final String resolved = evalInput(input);
72 | if (resolved.contains(START) && !resolved.equals(input)) {
73 | return evalRecurse(resolved);
74 | } else {
75 | return resolved;
76 | }
77 | }
78 |
79 | private String evalInput(String input) {
80 | final int start = input.indexOf(START);
81 | if (start == -1) {
82 | return input;
83 | }
84 | final int end = input.indexOf(END, start + 1);
85 | return end == -1 ? input : eval(input, start, end);
86 | }
87 |
88 | /**
89 | * Convert the expression usingEnvironment variables, System Properties or an existing property.
90 | */
91 | private String evaluateExpression(String exp) {
92 | String val = System.getProperty(exp);
93 | if (val == null) {
94 | val = System.getenv(exp);
95 | if (val == null) {
96 | val = localLookup(exp);
97 | }
98 | }
99 | return val;
100 | }
101 |
102 | private String localLookup(String exp) {
103 | if (sourceMap != null) {
104 | return sourceMap.raw(exp);
105 | } else if (sourceProperties != null) {
106 | return sourceProperties.getProperty(exp);
107 | }
108 | return null;
109 | }
110 |
111 | private String eval(String val, int start, int end) {
112 | return new EvalBuffer(val, start, end).process();
113 | }
114 |
115 | private final class EvalBuffer {
116 |
117 | private final StringBuilder buf = new StringBuilder();
118 | private final String original;
119 | private int position;
120 | private int start;
121 | private int end;
122 | private String expression;
123 | private String defaultValue;
124 |
125 | EvalBuffer(String val, int start, int end) {
126 | this.original = val;
127 | this.start = start;
128 | this.end = end;
129 | this.position = 0;
130 | moveToStart();
131 | }
132 |
133 | void moveToStart() {
134 | if (start > position) {
135 | buf.append(original, position, start);
136 | position = start;
137 | }
138 | }
139 |
140 | void parseForDefault() {
141 | int colonPos = original.indexOf(':', start);
142 | if (colonPos > start && colonPos < end) {
143 | expression = original.substring(start + START.length(), colonPos);
144 | defaultValue = original.substring(colonPos + 1, end);
145 | } else {
146 | expression = original.substring(start + START.length(), end);
147 | }
148 | }
149 |
150 | void evaluate() {
151 | String eval = evaluateExpression(expression);
152 | if (eval != null) {
153 | buf.append(eval);
154 | } else {
155 | if (defaultValue != null) {
156 | buf.append(defaultValue);
157 | } else if (DOCKER_HOST.equals(expression)) {
158 | final String dockerHost = DockerHost.host();
159 | buf.append(dockerHost);
160 | System.setProperty("docker.host", dockerHost);
161 | } else {
162 | buf.append(START).append(expression).append(END);
163 | }
164 | }
165 | }
166 |
167 | String end() {
168 | if (end < original.length() - 1) {
169 | buf.append(original.substring(end + 1));
170 | }
171 | return buf.toString();
172 | }
173 |
174 | boolean next() {
175 | if (end < original.length()) {
176 | int startPos = original.indexOf(START, end + 1);
177 | if (startPos > -1) {
178 | int endPos = original.indexOf(END, startPos + 1);
179 | if (endPos > -1) {
180 | if (startPos > end + 1) {
181 | buf.append(original, end + 1, startPos);
182 | }
183 | this.start = startPos;
184 | this.end = endPos;
185 | return true;
186 | }
187 | }
188 | }
189 | return false;
190 | }
191 |
192 | private void evalNext() {
193 | parseForDefault();
194 | evaluate();
195 | }
196 |
197 | String process() {
198 | evalNext();
199 | while (next()) {
200 | evalNext();
201 | }
202 | return end();
203 | }
204 | }
205 |
206 | }
207 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/FileWatchTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 |
4 | import org.junit.jupiter.api.Test;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.io.File;
9 | import java.io.FileWriter;
10 | import java.io.IOException;
11 | import java.util.ArrayList;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.Properties;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.assertj.core.api.Assertions.fail;
18 |
19 | class FileWatchTest {
20 |
21 | Logger log = LoggerFactory.getLogger("FileWatchTest");
22 |
23 | @Test
24 | void test_when_notChanged() {
25 |
26 | CoreConfiguration config = newConfig();
27 | List files = files();
28 | final FileWatch watch = fileWatch(config, files);
29 |
30 | assertThat(config.size()).isEqualTo(2);
31 | // not touched
32 | watch.check();
33 | // no reload
34 | assertThat(config.size()).isEqualTo(2);
35 | }
36 |
37 | @Test
38 | void test_check_whenTouched_expect_loaded() {
39 |
40 | CoreConfiguration config = newConfig();
41 | List files = files();
42 | final FileWatch watch = fileWatch(config, files);
43 |
44 | assertThat(config.size()).isEqualTo(2);
45 | assertThat(config.getOptional("one")).isEmpty();
46 |
47 | touchFiles(files);
48 | // check after touch means files loaded
49 | watch.check();
50 |
51 | // properties loaded as expected
52 | final int size0 = config.size();
53 | assertThat(size0).isGreaterThan(2);
54 | assertThat(config.get("one")).isEqualTo("a");
55 | assertThat(config.getInt("my.size", 42)).isEqualTo(17);
56 | assertThat(config.getBool("c.active", false)).isTrue();
57 | assertThat(config.enabled("c.active", false)).isTrue();
58 | }
59 |
60 | @Test
61 | void test_check_whenTouchedScheduled_expect_loaded() {
62 |
63 | CoreConfiguration config = newConfig();
64 | List files = files();
65 | for (File file : files) {
66 | if (!file.exists()) {
67 | fail("File " + file.getAbsolutePath() + " does not exist?");
68 | }
69 | }
70 | final FileWatch watch = fileWatch(config, files);
71 | System.out.println(watch);
72 |
73 | // assert not loaded
74 | assertThat(config.size()).isEqualTo(2);
75 | if (isGithubActions()) {
76 | log.info("file change not detected in GithubActions");
77 | return;
78 | }
79 | // touch but scheduled check not run yet
80 | touchFiles(files);
81 | // wait until scheduled check has been run
82 | sleep(3000);
83 |
84 | // properties loaded as expected
85 | assertThat(config.size()).isGreaterThan(2);
86 | assertThat(config.get("one")).isEqualTo("a");
87 | assertThat(config.getInt("my.size", 42)).isEqualTo(17);
88 | assertThat(config.getBool("c.active", false)).isTrue();
89 | }
90 |
91 | @Test
92 | void test_check_whenFileWritten() throws Exception {
93 | log.info("test_check_whenFileWritten");
94 | CoreConfiguration config = newConfig();
95 | List files = files();
96 |
97 | final FileWatch watch = fileWatch(config, files);
98 |
99 | if (isGithubActions()) {
100 | File aFile = files.get(0);
101 | log.info("file change detection in GithubActions via change in length from {}", aFile.length());
102 | assertThat(config.getOptional("one")).isEmpty();
103 |
104 | writeContent("one=NotAReally");
105 | sleep(20);
106 | log.info("file length now {}", aFile.length());
107 | watch.check();
108 | assertThat(config.get("one")).isEqualTo("NotAReally");
109 |
110 | writeContent("one=a");
111 | sleep(20);
112 | log.info("file length now {}", aFile.length());
113 | watch.check();
114 | assertThat(config.get("one")).isEqualTo("a");
115 | return;
116 | }
117 |
118 | touchFiles(files);
119 | watch.check();
120 |
121 | // properties loaded as expected
122 | final int size0 = config.size();
123 | assertThat(size0).isGreaterThan(2);
124 | assertThat(config.get("one")).isEqualTo("a");
125 |
126 | writeContent("one=NotA");
127 | sleep(20);
128 | //assertThat(watch.changed()).isTrue();
129 | watch.check();
130 | assertThat(watch.changed()).isFalse();
131 |
132 | assertThat(config.get("one")).isEqualTo("NotA");
133 | writeContent("one=a");
134 | sleep(20);
135 | watch.check();
136 | assertThat(config.get("one")).isEqualTo("a");
137 | }
138 |
139 | private static FileWatch fileWatch(CoreConfiguration config, List files) {
140 | return new FileWatch(config, files, new Parsers(Collections.emptyList()));
141 | }
142 |
143 | private void writeContent(String content) throws IOException {
144 | sleep(20);
145 | File aProps = new File("./src/test/resources/watch/a.properties");
146 | if (!aProps.exists()) {
147 | throw new IllegalStateException("a.properties does not exist?");
148 | }
149 | FileWriter fw = new FileWriter(aProps);
150 | fw.write(content);
151 | fw.close();
152 | }
153 |
154 | private void sleep(int millis) {
155 | try {
156 | Thread.sleep(millis);
157 | } catch (InterruptedException e) {
158 | Thread.currentThread().interrupt();
159 | throw new RuntimeException(e);
160 | }
161 | }
162 |
163 | private CoreConfiguration newConfig() {
164 | final Properties properties = new Properties();
165 | properties.setProperty("config.watch.delay", "1");
166 | properties.setProperty("config.watch.period", "1");
167 | return new CoreConfiguration(CoreEntry.newMap(properties, "newConfig"));
168 | }
169 |
170 | private List files() {
171 | List files = new ArrayList<>();
172 | files.add(new File("./src/test/resources/watch/a.properties"));
173 | files.add(new File("./src/test/resources/watch/b.yaml"));
174 | files.add(new File("./src/test/resources/watch/c.yml"));
175 | return files;
176 | }
177 |
178 | private void touchFiles(List files) {
179 | sleep(50);
180 | for (File file : files) {
181 | if (!file.setLastModified(System.currentTimeMillis())) {
182 | System.err.println("touch setLastModified not successful");
183 | }
184 | }
185 | }
186 |
187 | private boolean isGithubActions() {
188 | return "true".equals(System.getenv("GITHUB_ACTIONS"));
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/InitialLoaderTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static io.avaje.config.CoreExpressionEval.evalFor;
6 | import static io.avaje.config.InitialLoader.Source.RESOURCE;
7 | import static org.assertj.core.api.Assertions.assertThat;
8 | import static org.junit.jupiter.api.Assertions.assertEquals;
9 |
10 | class InitialLoaderTest {
11 |
12 | private static InitialLoader newInitialLoader() {
13 | return new InitialLoader(new CoreComponents(), new DefaultResourceLoader());
14 | }
15 |
16 | @Test
17 | void load() {
18 | String userName = System.getProperty("user.name");
19 | String userHome = System.getProperty("user.home");
20 |
21 | InitialLoader loader = newInitialLoader();
22 | loader.loadProperties("test-properties/application.properties", RESOURCE);
23 | loader.loadWithExtensionCheck("test-properties/application.yaml");
24 |
25 | loader.loadProperties("test-properties/one.properties", RESOURCE);
26 | loader.loadWithExtensionCheck("test-properties/foo.yml");
27 |
28 | var properties = evalFor(loader.entryMap());
29 |
30 | assertEquals("fromProperties", properties.get("app.fromProperties").value());
31 | assertEquals("Two", properties.get("app.two").value());
32 |
33 | assertEquals("bart", properties.get("eval.withDefault").value());
34 | assertEquals(userName, properties.get("eval.name").value());
35 | assertEquals(userHome + "/after", properties.get("eval.home").value());
36 |
37 | assertEquals("before|Beta|after", properties.get("someOne").value());
38 | assertEquals("before|Two|after", properties.get("someTwo").value());
39 | }
40 |
41 | @Test
42 | void loadWithExtensionCheck() {
43 | InitialLoader loader = newInitialLoader();
44 | loader.loadWithExtensionCheck("test-dummy.properties");
45 | loader.loadWithExtensionCheck("test-dummy.yml");
46 | loader.loadWithExtensionCheck("test-dummy2.yaml");
47 |
48 | var properties = evalFor(loader.entryMap());
49 | assertThat(properties.get("dummy.yaml.bar").value()).isEqualTo("baz");
50 | assertThat(properties.get("dummy.yml.foo").value()).isEqualTo("bar");
51 | assertThat(properties.get("dummy.properties.foo").value()).isEqualTo("bar");
52 | assertThat(properties.get("dummy.properties.a").value()).isEqualTo("fromResource");
53 | }
54 |
55 | @Test
56 | void loadYaml() {
57 | InitialLoader loader = newInitialLoader();
58 | loader.loadWithExtensionCheck("test-properties/foo.yml");
59 | var properties = evalFor(loader.entryMap());
60 |
61 | assertThat(properties.get("Some.Other.pass").value()).isEqualTo("someDefault");
62 | }
63 |
64 | @Test
65 | void loadProperties() {
66 | System.setProperty("eureka.instance.hostname", "host1");
67 | System.setProperty("server.port", "9876");
68 |
69 | InitialLoader loader = newInitialLoader();
70 | loader.loadProperties("test-properties/one.properties", RESOURCE);
71 | var properties = evalFor(loader.entryMap());
72 |
73 | assertThat(properties.get("hello").source()).isEqualTo("resource:test-properties/one.properties");
74 | assertThat(properties.get("hello").value()).isEqualTo("there");
75 | assertThat(properties.get("name").value()).isEqualTo("Rob");
76 | assertThat(properties.get("statusPageUrl").value()).isEqualTo("https://host1:9876/status");
77 | assertThat(properties.get("statusPageUrl2").value()).isEqualTo("https://aaa:9876/status2");
78 | assertThat(properties.get("statusPageUrl3").value()).isEqualTo("https://aaa:89/status3");
79 | assertThat(properties.get("statusPageUrl4").value()).isEqualTo("https://there:9876/name/Rob");
80 | assertThat(properties.get("sameFileEval.4").value()).isEqualTo("somethin1-2-3-4");
81 | assertThat(properties.get("sameFileEval.3").value()).isEqualTo("somethin1-2-3");
82 | assertThat(properties.get("sameFileEval.2").value()).isEqualTo("somethin1-2");
83 | assertThat(properties.get("sameFileEval.1").value()).isEqualTo("somethin1");
84 | assertThat(properties.get("asameFileEval.0").value()).isEqualTo("somethin1-2-3-afour");
85 | assertThat(properties.get("zsameFileEval.0").value()).isEqualTo("somethin1-2-3-zfour");
86 | assertThat(properties.get("zsameFileCombo").value()).isEqualTo("A|somethin1-2|somethin1-2-3|B");
87 | assertThat(properties.get("someOne").value()).isEqualTo("before|${app.one}|after");
88 | assertThat(properties.get("someOne2").value()).isEqualTo("Bef|before|${app.one}|after|Aft");
89 | }
90 |
91 | @Test
92 | void splitPaths() {
93 | InitialLoader loader = newInitialLoader();
94 | assertThat(loader.splitPaths("one two three")).contains("one", "two", "three");
95 | assertThat(loader.splitPaths("one,two,three")).contains("one", "two", "three");
96 | assertThat(loader.splitPaths("one;two;three")).contains("one", "two", "three");
97 | assertThat(loader.splitPaths("one two,three;four,five six")).contains("one", "two", "three", "four", "five", "six");
98 | }
99 |
100 | @Test
101 | void loadViaCommandLine_whenNotValid() {
102 | InitialLoader loader = newInitialLoader();
103 | loader.loadViaCommandLine(new String[]{"-p", "8765"});
104 | assertEquals(0, loader.size());
105 | loader.loadViaCommandLine(new String[]{"-port", "8765"});
106 | assertEquals(0, loader.size());
107 |
108 | loader.loadViaCommandLine(new String[]{"-port"});
109 | loader.loadViaCommandLine(new String[]{"-p", "ort"});
110 | assertEquals(0, loader.size());
111 |
112 | loader.loadViaCommandLine(new String[]{"-p", "doesNotExist.yaml"});
113 | assertEquals(0, loader.size());
114 | }
115 |
116 | @Test
117 | void loadViaCommandLine_localFile() {
118 | InitialLoader loader = newInitialLoader();
119 | loader.loadViaCommandLine(new String[]{"-p", "test-dummy2.yaml"});
120 | assertEquals(1, loader.size());
121 | }
122 |
123 | @Test
124 | void load_withSuppressTestResource() {
125 | //application-test.yaml is loaded when suppressTestResource is not set to true
126 | try {
127 | System.setProperty("suppressTestResource", "");
128 | InitialLoader loader = newInitialLoader();
129 | var properties = evalFor(loader.load());
130 | assertThat(properties.get("myapp.activateFoo").value()).isEqualTo("true");
131 |
132 | //application-test.yaml is not loaded when suppressTestResource is set to true
133 | System.setProperty("suppressTestResource", "true");
134 | InitialLoader loaderWithSuppressTestResource = newInitialLoader();
135 | var propertiesWithoutTestResource = evalFor(loaderWithSuppressTestResource.load());
136 | assertThat(propertiesWithoutTestResource.get("myapp.activateFoo")).isNull();
137 | } finally {
138 | System.clearProperty("suppressTestResource");
139 | }
140 | }
141 |
142 | @Test
143 | void load_withLoadPropertyChain() {
144 | InitialLoader loader = newInitialLoader();
145 | loader.loadWithExtensionCheck("test-properties/chain/main.properties");
146 | var properties = evalFor(loader.load());
147 | assertThat(properties.get("value.main").value()).isEqualTo("main");
148 | assertThat(properties.get("value.a").value()).isEqualTo("true");
149 | assertThat(properties.get("value.b").value()).isEqualTo("true");
150 | assertThat(properties.get("value.c").value()).isEqualTo("true");
151 | assertThat(properties.get("value.d").value()).isEqualTo("true");
152 | assertThat(properties.get("value.e").value()).isEqualTo("true");
153 | assertThat(properties.get("override").value()).isEqualTo("e");
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/avaje-aws-appconfig/src/main/java/io/avaje/config/appconfig/AppConfigPlugin.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config.appconfig;
2 |
3 | import static java.lang.System.Logger.Level.DEBUG;
4 | import static java.lang.System.Logger.Level.ERROR;
5 | import static java.lang.System.Logger.Level.INFO;
6 | import static java.lang.System.Logger.Level.TRACE;
7 | import static java.lang.System.Logger.Level.WARNING;
8 |
9 | import java.io.StringReader;
10 | import java.lang.System.Logger.Level;
11 | import java.net.ConnectException;
12 | import java.time.Instant;
13 | import java.util.Map;
14 | import java.util.concurrent.atomic.AtomicInteger;
15 | import java.util.concurrent.atomic.AtomicReference;
16 | import java.util.concurrent.locks.LockSupport;
17 | import java.util.concurrent.locks.ReentrantLock;
18 |
19 | import io.avaje.applog.AppLog;
20 | import io.avaje.config.ConfigParser;
21 | import io.avaje.config.Configuration;
22 | import io.avaje.config.ConfigurationSource;
23 | import io.avaje.spi.ServiceProvider;
24 |
25 | /**
26 | * Plugin that loads AWS AppConfig as Yaml or Properties.
27 | *
28 | * By default, will periodically reload the configuration if it has changed.
29 | */
30 | @ServiceProvider
31 | public final class AppConfigPlugin implements ConfigurationSource {
32 |
33 | private static final System.Logger log = AppLog.getLogger("io.avaje.config.AwsAppConfig");
34 |
35 | private Loader loader;
36 |
37 | @Override
38 | public void load(Configuration configuration) {
39 | if (!configuration.enabled("aws.appconfig.enabled", true)) {
40 | log.log(INFO, "AwsAppConfig plugin is disabled");
41 | return;
42 | }
43 | loader = new Loader(configuration);
44 | int attempts = loader.initialLoad();
45 | if (attempts > 1){
46 | log.log(INFO, "AwsAppConfig loaded after {0} attempts", attempts + 1);
47 | }
48 | }
49 |
50 | @Override
51 | public void reload() {
52 | if (loader != null) {
53 | loader.reload();
54 | }
55 | }
56 |
57 | static final class Loader {
58 |
59 | private final Configuration configuration;
60 | private final AppConfigFetcher fetcher;
61 | private final ConfigParser yamlParser;
62 | private final ConfigParser propertiesParser;
63 | private final AtomicInteger connectErrorCount = new AtomicInteger();
64 | private final ReentrantLock lock = new ReentrantLock();
65 | private final AtomicReference validUntil;
66 | private final long nextRefreshSeconds;
67 |
68 | private String currentVersion = "none";
69 |
70 | Loader(Configuration configuration) {
71 | this.validUntil = new AtomicReference<>(Instant.now().minusSeconds(1));
72 | this.configuration = configuration;
73 | this.propertiesParser = configuration.parser("properties").orElseThrow();
74 | this.yamlParser = configuration.parser("yaml").orElse(null);
75 | if (yamlParser == null) {
76 | log.log(WARNING, "No Yaml parser registered");
77 | }
78 |
79 | var app = configuration.get("aws.appconfig.application");
80 | var env = configuration.get("aws.appconfig.environment");
81 | var con = configuration.get("aws.appconfig.configuration", "default");
82 |
83 | this.fetcher = AppConfigFetcher.builder()
84 | .application(app)
85 | .environment(env)
86 | .configuration(con)
87 | .build();
88 |
89 | log.log(DEBUG, "AwsAppConfig uri {0}", fetcher.uri());
90 |
91 | boolean pollEnabled = configuration.enabled("aws.appconfig.pollingEnabled", true);
92 | long pollSeconds = configuration.getLong("aws.appconfig.pollingSeconds", 45L);
93 | this.nextRefreshSeconds = configuration.getLong("aws.appconfig.refreshSeconds", pollSeconds - 1);
94 | if (pollEnabled) {
95 | configuration.schedule(pollSeconds * 1000L, pollSeconds * 1000L, this::reload);
96 | }
97 | }
98 |
99 | /**
100 | * Potential race conditional with AWS AppConfig sidecar so use simple retry loop.
101 | */
102 | int initialLoad() {
103 | lock.lock();
104 | try {
105 | Exception lastAttempt = null;
106 | for (int i = 1; i < 11; i++) {
107 | try {
108 | loadAndPublish();
109 | return i;
110 | } catch (Exception e) {
111 | // often seeing this with apps that start quickly (and AppConfig sidecar not up yet)
112 | lastAttempt = e;
113 | log.log(DEBUG, "retrying, load attempt {0} got {1}", i, e.getMessage());
114 | LockSupport.parkNanos(250_000_000); // 250 millis
115 | }
116 | }
117 | log.log(ERROR, "Failed initial AwsAppConfig load", lastAttempt);
118 | return -1;
119 |
120 | } finally{
121 | lock.unlock();
122 | }
123 | }
124 |
125 | void reload() {
126 | if (reloadRequired()) {
127 | performReload();
128 | }
129 | }
130 |
131 | private boolean reloadRequired() {
132 | return validUntil.get().isBefore(Instant.now());
133 | }
134 |
135 | private void performReload() {
136 | lock.lock();
137 | try {
138 | if (!reloadRequired()) {
139 | return;
140 | }
141 | loadAndPublish();
142 |
143 | } catch (ConnectException e) {
144 | // expected during shutdown when AppConfig sidecar shuts down before the app
145 | int errCount = connectErrorCount.incrementAndGet();
146 | Level level = errCount > 1 ? WARNING : INFO;
147 | log.log(level, "Failed to fetch from AwsAppConfig - likely shutdown in progress");
148 | } catch (Exception e) {
149 | log.log(ERROR, "Error fetching or processing AwsAppConfig", e);
150 | } finally {
151 | lock.unlock();
152 | }
153 | }
154 |
155 | /**
156 | * Load and publish the configuration from AWS AppConfig.
157 | */
158 | private void loadAndPublish() throws AppConfigFetcher.FetchException, ConnectException {
159 | AppConfigFetcher.Result result = fetcher.fetch();
160 | if (currentVersion.equals(result.version())) {
161 | log.log(TRACE, "AwsAppConfig unchanged, version {0}", currentVersion);
162 | } else {
163 | String contentType = result.contentType();
164 | if (log.isLoggable(TRACE)) {
165 | int contentLength = result.body().length();
166 | log.log(TRACE, "AwsAppConfig fetched version:{0} contentType:{1} contentLength:{2,number,#}", result.version(), contentType, contentLength);
167 | }
168 | Map keyValues = parse(result);
169 | configuration.eventBuilder("AwsAppConfig")
170 | .putAll(keyValues)
171 | .publish();
172 | currentVersion = result.version();
173 | debugLog(result, keyValues.size());
174 | }
175 | // move the next valid until time
176 | validUntil.set(Instant.now().plusSeconds(nextRefreshSeconds));
177 | }
178 |
179 | private Map parse(AppConfigFetcher.Result result) {
180 | ConfigParser parser = parser(result.contentType());
181 | return parser.load(new StringReader(result.body()));
182 | }
183 |
184 | private ConfigParser parser(String contentType) {
185 | if (contentType.endsWith("yaml")) {
186 | return yamlParser;
187 | } else {
188 | return propertiesParser;
189 | }
190 | }
191 |
192 | private static void debugLog(AppConfigFetcher.Result result, int size) {
193 | if (log.isLoggable(DEBUG)) {
194 | log.log(DEBUG, "AwsAppConfig loaded version {0} with {1} properties", result.version(), size);
195 | }
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/YamlParserTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
5 |
6 | import java.io.InputStream;
7 | import java.util.Map;
8 |
9 | import org.junit.jupiter.api.Test;
10 |
11 | class YamlParserTest {
12 |
13 | private final YamlLoaderSnake load = new YamlLoaderSnake();
14 |
15 | @Test
16 | void simpleYamlParserMultiLoad() {
17 | YamlLoaderSimple parser = new YamlLoaderSimple();
18 | Map load1 = parser.load(res("/yaml/basic.yaml"));
19 | assertThat(load1).hasSize(6);
20 | basic(load1);
21 |
22 | Map load2 = parser.load(res("/yaml/key-comment.yaml"));
23 | assertThat(load2).hasSize(4);
24 | assertThat(load2).containsOnlyKeys("k1","k2","k3", "k4");
25 | }
26 |
27 | @Test
28 | void basic() {
29 | basic(parseYaml2("/yaml/basic.yaml"));
30 | basic(parseYaml("/yaml/basic.yaml"));
31 | }
32 |
33 | void basic(final Map map) {
34 | assertThat(map)
35 | .containsOnlyKeys(
36 | "name",
37 | "properties.key1",
38 | "properties.key2",
39 | "sorted.1",
40 | "sorted.2",
41 | "root.directories");
42 | assertThat(map.get("name")).isEqualTo("Name123");
43 | assertThat(map.get("properties.key1")).isEqualTo("value1");
44 | assertThat(map.get("properties.key2")).isEqualTo("value2");
45 | assertThat(map.get("sorted.1")).isEqualTo("one");
46 | assertThat(map.get("root.directories")).isEqualTo("/one/two/three,/four/five/six");
47 | }
48 |
49 | @Test
50 | void parse_singleDoc() {
51 | parse_singleDoc(parseYaml2("/yaml/single-doc.yaml"));
52 | parse_singleDoc(parseYaml("/yaml/single-doc.yaml"));
53 | }
54 |
55 | void parse_singleDoc(Map map) {
56 | assertThat(map).containsOnlyKeys("name.a.e", "name.b.t", "int", "float");
57 | assertThat(map.get("name.a.e")).isEqualTo("e1");
58 | assertThat(map.get("name.b.t")).isEqualTo("t1");
59 | assertThat(map.get("int")).isEqualTo("42");
60 | assertThat(map.get("float")).isEqualTo("3.14159");
61 | }
62 |
63 | @Test
64 | void parse_multipleDocs() {
65 | parse_multipleDocs(parseYaml2("/yaml/multiple-docs.yaml"));
66 | parse_multipleDocs(parseYaml("/yaml/multiple-docs.yaml"));
67 | }
68 |
69 | private void parse_multipleDocs(Map map) {
70 | assertThat(map).containsOnlyKeys("name", "some.key1", "other.key1", "other.key2");
71 | assertThat(map.get("name")).isEqualTo("Name123");
72 | assertThat(map.get("some.key1")).isEqualTo("value1");
73 | assertThat(map.get("other.key1")).isEqualTo("o1");
74 | assertThat(map.get("other.key2")).isEqualTo("o2");
75 | }
76 |
77 | @Test
78 | void parse_quotedValues() {
79 | parse_quotedValues(parseYaml2("/yaml/quoted-values.yaml"));
80 | parse_quotedValues(parseYaml("/yaml/quoted-values.yaml"));
81 | }
82 |
83 | void parse_quotedValues(Map map) {
84 | assertThat(map).containsOnlyKeys("a", "a1", "b", "b1", "c", "c1", "e", "e1", "d", "d1");
85 | assertThat(map.get("a")).isEqualTo("unquoted value");
86 | assertThat(map.get("a1")).isEqualTo("unquoted value with comment");
87 | assertThat(map.get("b")).isEqualTo("single quote");
88 | assertThat(map.get("b1")).isEqualTo("single quote with comment");
89 | assertThat(map.get("c")).isEqualTo("double quote");
90 | assertThat(map.get("c1")).isEqualTo("double quote with comment");
91 | assertThat(map.get("d")).isEqualTo(" single quote with spaces ");
92 | assertThat(map.get("d1")).isEqualTo(" single quote with spaces + comment ");
93 | assertThat(map.get("e")).isEqualTo(" double quote with spaces ");
94 | assertThat(map.get("e1")).isEqualTo(" double quote with spaces + comment ");
95 | }
96 |
97 | @Test
98 | void parse_quotedKeys() {
99 | parse_quotedKeys(parseYaml2("/yaml/quoted-keys.yaml"));
100 | parse_quotedKeys(parseYaml("/yaml/quoted-keys.yaml"));
101 | }
102 |
103 | private void parse_quotedKeys(Map map) {
104 | assertThat(map).containsOnlyKeys("a-1", "a-2", "b.a-1", "b.a-2");
105 | assertThat(map.get("a-1")).isEqualTo("v0");
106 | assertThat(map.get("a-2")).isEqualTo("v1");
107 | assertThat(map.get("b.a-1")).isEqualTo("v2");
108 | assertThat(map.get("b.a-2")).isEqualTo("v3");
109 | }
110 |
111 | @Test
112 | void parse_multiLine() {
113 | parse_multiLine(parseYaml2("/yaml/multi-line.yaml"));
114 | parse_multiLine(parseYaml("/yaml/multi-line.yaml"));
115 | }
116 |
117 | private void parse_multiLine(Map map) {
118 | assertThat(map).containsOnlyKeys("a0", "a1", "a2", "a3", "a4", "n1.k0", "n1.k1");
119 | assertThat(map.get("n1.k1")).isEqualTo("kv1");
120 | assertThat(map.get("n1.k0")).isEqualTo("a line0\n" +
121 | "b line1\n" +
122 | "a other\n");
123 | assertThat(map.get("a0")).isEqualTo("a line0\n" +
124 | "b line1\n");
125 | assertThat(map.get("a1")).isEqualTo("a line0\n" +
126 | "b line1\n");
127 | assertThat(map.get("a2")).isEqualTo("a line0\n" +
128 | "b line1");
129 | assertThat(map.get("a3")).isEqualTo("a line0\n" +
130 | "\n" +
131 | "b line1");
132 | assertThat(map.get("a4")).isEqualTo("a line0\n" +
133 | "b line1\n" +
134 | "\n" +
135 | "c line1\n" +
136 | "\n");
137 | }
138 |
139 | @Test
140 | void parse_multiLine_empty() {
141 | parse_multiLine_empty(parseYaml2("/yaml/multi-line-empty.yaml"));
142 | parse_multiLine_empty(parseYaml("/yaml/multi-line-empty.yaml"));
143 | }
144 |
145 | private void parse_multiLine_empty(Map map) {
146 | assertThat(map).containsOnlyKeys("a0", "a1", "a2", "a3", "a4");
147 | assertThat(map.get("a0")).isEqualTo("");
148 | assertThat(map.get("a1")).isEqualTo("");
149 | assertThat(map.get("a2")).isEqualTo("");
150 | assertThat(map.get("a3")).isEqualTo("");
151 | assertThat(map.get("a4")).isEqualTo("\n" +
152 | "\n" +
153 | "\n");
154 | }
155 |
156 | @Test
157 | void parse_keyComments() {
158 | parse_keyComments(parseYaml2("/yaml/key-comment.yaml"));
159 | parse_keyComments(parseYaml("/yaml/key-comment.yaml"));
160 | }
161 |
162 | private void parse_keyComments(Map map) {
163 | assertThat(map).containsOnlyKeys("k1", "k2", "k3", "k4");
164 | assertThat(map.get("k1")).isEqualTo("v1");
165 | assertThat(map.get("k2")).isEqualTo("v2");
166 | assertThat(map.get("k3")).isEqualTo("v3");
167 | assertThat(map.get("k4")).isEqualTo("v4");
168 | }
169 |
170 | @Test
171 | void parse_multi_line_implicit() {
172 | parse_multi_line_implicit(parseYaml2("/yaml/multi-line-implicit.yaml"));
173 | parse_multi_line_implicit(parseYaml("/yaml/multi-line-implicit.yaml"));
174 | }
175 |
176 | private void parse_multi_line_implicit(Map map) {
177 | assertThat(map).containsOnlyKeys("k0.a", "k1", "k2");
178 | assertThat(map.get("k0.a")).isEqualTo("v0");
179 | assertThat(map.get("k1")).isEqualTo("aa bb cc");
180 | assertThat(map.get("k2")).isEqualTo("dd ee ff");
181 | }
182 |
183 | @Test
184 | void parse_top_vals() {
185 | assertThatThrownBy(() -> parseYaml("/yaml/err-top-vals.yaml"))
186 | .hasMessageContaining("line: 2");
187 | assertThatThrownBy(() -> parseYaml2("/yaml/err-top-vals.yaml"))
188 | .hasMessageContaining("line 4");
189 | }
190 |
191 | @Test
192 | void parse_err_req_key2() {
193 | assertThatThrownBy(() -> parseYaml2("/yaml/err-require-topkey.yaml"))
194 | .hasMessageContaining("line 5");
195 | assertThatThrownBy(() -> parseYaml("/yaml/err-require-topkey.yaml"))
196 | .hasMessageContaining("line:5");
197 | }
198 |
199 | @Test
200 | void parse_list() {
201 | var list =
202 | Map.of(
203 | "keyValueList",
204 | "apple:10,pear:15,cheese:10",
205 | "keyValueList1",
206 | "apple:10,pear:15,cheese:10");
207 | assertThat(parseYaml2("/yaml/list.yaml")).isEqualTo(list);
208 | assertThat(parseYaml("/yaml/list.yaml")).isEqualTo(list);
209 | }
210 |
211 | private Map parseYaml2(String s) {
212 | return load.load(res(s));
213 | }
214 |
215 | private Map parseYaml(String s) {
216 | YamlLoaderSimple parser = new YamlLoaderSimple();
217 | return parser.load(res(s));
218 | }
219 |
220 | private InputStream res(String path) {
221 | return YamlLoaderSimple.class.getResourceAsStream(path);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/YamlLoaderSimple.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import static java.util.stream.Collectors.joining;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.InputStreamReader;
8 | import java.io.LineNumberReader;
9 | import java.io.Reader;
10 | import java.io.UncheckedIOException;
11 | import java.util.ArrayDeque;
12 | import java.util.ArrayList;
13 | import java.util.Deque;
14 | import java.util.Iterator;
15 | import java.util.LinkedHashMap;
16 | import java.util.List;
17 | import java.util.Map;
18 | import java.util.StringJoiner;
19 |
20 | import org.jspecify.annotations.NullMarked;
21 |
22 | /**
23 | * Simple YAML parser for loading yaml based config.
24 | */
25 | @NullMarked
26 | final class YamlLoaderSimple implements YamlLoader {
27 |
28 | @Override
29 | public Map load(Reader reader) {
30 | return new Load().load(reader);
31 | }
32 |
33 | @Override
34 | public Map load(InputStream is) {
35 | return new Load().load(is);
36 | }
37 |
38 | private static class Load {
39 | enum MultiLineTrim {
40 | Clip,
41 | Strip,
42 | Keep,
43 | Implicit
44 | }
45 |
46 | enum State {
47 | RequireKey,
48 | MultiLine,
49 | List,
50 | KeyOrValue,
51 | RequireTopKey
52 | }
53 |
54 | private final Map keyValues = new LinkedHashMap<>();
55 | private final Deque keyStack = new ArrayDeque<>();
56 | private final List multiLines = new ArrayList<>();
57 |
58 | private State state = State.RequireKey;
59 | private MultiLineTrim multiLineTrim = MultiLineTrim.Clip;
60 | private int currentLine;
61 | private int currentIndent;
62 | private int multiLineIndent;
63 |
64 | private Map load(InputStream is) {
65 | return load(new InputStreamReader(is));
66 | }
67 |
68 | private Map load(Reader reader) {
69 | try (LineNumberReader lineReader = new LineNumberReader(reader)) {
70 | String line;
71 | do {
72 | line = lineReader.readLine();
73 | processLine(line);
74 | } while (line != null);
75 |
76 | return keyValues;
77 | } catch (IOException e) {
78 | throw new UncheckedIOException(e);
79 | }
80 | }
81 |
82 | private void processLine(String line) {
83 | if (line == null) {
84 | checkFinalMultiLine();
85 | } else {
86 | currentLine++;
87 | readIndent(line);
88 | if (state == State.MultiLine || state == State.List) {
89 | processMultiLine(line);
90 | } else {
91 | processNext(line);
92 | }
93 | }
94 | }
95 |
96 | private void checkFinalMultiLine() {
97 | if (state == State.MultiLine) {
98 | addKeyVal(multiLineValue());
99 | }
100 | if (state == State.List) {
101 | addKeyVal(listValue());
102 | }
103 | }
104 |
105 | private void processMultiLine(String line) {
106 | if (multiLineIndent == 0) {
107 | if (currentIndent == 0 && !line.trim().isEmpty()) {
108 | multiLineEnd(line);
109 | return;
110 | }
111 | // first multiLine
112 | multiLineIndent = currentIndent;
113 | multiLines.add(line);
114 | } else if (currentIndent >= multiLineIndent || line.trim().isEmpty()) {
115 | multiLines.add(line);
116 | } else {
117 | // end of multiLine
118 | multiLineEnd(line);
119 | }
120 | }
121 |
122 | private void multiLineEnd(String line) {
123 | if (state == State.MultiLine) {
124 | addKeyVal(multiLineValue());
125 | } else {
126 | addKeyVal(listValue());
127 | }
128 | processNext(line);
129 | }
130 |
131 | private String listValue() {
132 | if (multiLines.isEmpty()) {
133 | return "";
134 | }
135 | multiLineTrimTrailing();
136 | var result =
137 | multiLines.stream().map(s -> s.trim().substring(1).stripLeading()).collect(joining(","));
138 | multiLineEnd();
139 | return result;
140 | }
141 |
142 | private String multiLineValue() {
143 | if (multiLines.isEmpty()) {
144 | return "";
145 | }
146 | if (multiLineTrim != MultiLineTrim.Keep) {
147 | multiLineTrimTrailing();
148 | }
149 | String join = multiLineTrim == MultiLineTrim.Implicit ? " " : "\n";
150 | StringBuilder sb = new StringBuilder();
151 | int lastIndex = multiLines.size() - 1;
152 | for (int i = 0; i <= lastIndex; i++) {
153 | String line = multiLines.get(i);
154 | if (line.length() < multiLineIndent) {
155 | // empty line whitespace
156 | sb.append("\n");
157 | } else {
158 | line = line.substring(multiLineIndent);
159 | if (i == lastIndex && (multiLineTrim == MultiLineTrim.Strip || multiLineTrim == MultiLineTrim.Implicit)) {
160 | sb.append(line);
161 | } else {
162 | sb.append(line).append(join);
163 | }
164 | }
165 | }
166 | multiLineEnd();
167 | return sb.toString();
168 | }
169 |
170 | private void multiLineTrimTrailing() {
171 | for (int i = multiLines.size(); i-- > 0; ) {
172 | if (!multiLines.get(i).trim().isEmpty()) {
173 | break;
174 | }
175 | multiLines.remove(i);
176 | }
177 | }
178 |
179 | void readIndent(String line) {
180 | currentIndent = indent(line);
181 | }
182 |
183 | private void processNext(String line) {
184 | if (newDocument(line) || ignoreLine(line)) {
185 | return;
186 | }
187 |
188 | final int pos = line.indexOf(':');
189 | var list = line.stripLeading().charAt(0) == '-';
190 | if (pos == -1 || list) {
191 | // value on another line
192 | processNonKey(line, list);
193 | return;
194 | }
195 | if (state == State.RequireTopKey && currentIndent > 0) {
196 | throw new IllegalStateException("Require top level key at line:" + currentLine + " [" + line + "]");
197 | }
198 |
199 | // must be a key - would expect explicit multiline otherwise
200 | final Key key = new Key(currentIndent, trimKey(line.substring(0, pos)));
201 | popKeys(currentIndent);
202 | keyStack.push(key);
203 |
204 | // look at the remainder of the line
205 | final String remaining = line.substring(pos + 1);
206 | final String trimmedValue = remaining.trim();
207 | if (trimmedValue.startsWith("|")) {
208 | multilineStart(multiLineTrimMode(trimmedValue));
209 |
210 | } else if (trimmedValue.startsWith("-")) {
211 | listStart(multiLineTrimMode(trimmedValue));
212 | } else if (trimmedValue.isEmpty() || trimmedValue.startsWith("#")) {
213 | // empty or comment
214 | state = State.KeyOrValue;
215 | } else {
216 | // simple key value
217 | addKeyVal(trimValue(remaining.trim()));
218 | }
219 | }
220 |
221 | private MultiLineTrim multiLineTrimMode(String trimmedValue) {
222 | if (trimmedValue.length() == 1) {
223 | return MultiLineTrim.Clip;
224 | }
225 | final char ch = trimmedValue.charAt(1);
226 | switch (ch) {
227 | case '-':
228 | // the final line break and any trailing empty lines are excluded
229 | return MultiLineTrim.Strip;
230 | case '+':
231 | // the final line break and any trailing empty lines are included
232 | return MultiLineTrim.Keep;
233 | default:
234 | // the final line break character is included and trailing empty lines are excluded
235 | return MultiLineTrim.Clip;
236 | }
237 | }
238 |
239 | private void addKeyVal(String value) {
240 | keyValues.put(fullKey(), value);
241 | keyStack.pop();
242 | state = State.RequireKey;
243 | }
244 |
245 | private void processNonKey(String line, boolean list) {
246 | if (state == State.RequireKey) {
247 | state = State.RequireTopKey;
248 | // drop this value line
249 | return;
250 | }
251 | if (keyStack.isEmpty()) {
252 | throw new IllegalStateException("Reading a value but no key at line: " + currentLine + " line[" + line + "]");
253 | }
254 | final int keyIndent = keyStack.peek().indent;
255 | if (currentIndent <= keyIndent) {
256 | throw new IllegalStateException("Value not indented enough for key " + fullKey() + " at line: " + currentLine + " line[" + line + "]");
257 | }
258 | if (list) {
259 | listStart(MultiLineTrim.Implicit);
260 | } else {
261 | multilineStart(MultiLineTrim.Implicit);
262 | }
263 | multiLineIndent = currentIndent;
264 | multiLines.add(line);
265 | }
266 |
267 | private void multilineStart(MultiLineTrim trim) {
268 | state = State.MultiLine;
269 | multiLineIndent = 0;
270 | multiLineTrim = trim;
271 | }
272 |
273 | private void listStart(MultiLineTrim trim) {
274 | state = State.List;
275 | multiLineIndent = 0;
276 | multiLineTrim = trim;
277 | }
278 |
279 | private void multiLineEnd() {
280 | state = State.RequireKey;
281 | multiLineIndent = 0;
282 | multiLines.clear();
283 | }
284 |
285 | private boolean newDocument(String line) {
286 | if (line.startsWith("---")) {
287 | keyStack.clear();
288 | return true;
289 | }
290 | return false;
291 | }
292 |
293 | private boolean ignoreLine(String line) {
294 | final String trimmed = line.trim();
295 | return trimmed.isEmpty() || trimmed.startsWith("#");
296 | }
297 |
298 | private String trimValue(String value) {
299 | if (value.startsWith("'")) {
300 | return unquoteValue('\'', value);
301 | }
302 | if (value.startsWith("\"")) {
303 | return unquoteValue('"', value);
304 | }
305 | int commentPos = value.indexOf('#');
306 | if (commentPos > -1) {
307 | return value.substring(0, commentPos).trim();
308 | }
309 | return value;
310 | }
311 |
312 | private String unquoteValue(char quoteChar, String value) {
313 | final int pos = value.lastIndexOf(quoteChar);
314 | return value.substring(1, pos);
315 | }
316 |
317 | private String fullKey() {
318 | StringJoiner fullKey = new StringJoiner(".");
319 | Iterator it = keyStack.descendingIterator();
320 | while (it.hasNext()) {
321 | fullKey.add(it.next().key());
322 | }
323 | return fullKey.toString();
324 | }
325 |
326 | private void popKeys(int indent) {
327 | while (!keyStack.isEmpty()) {
328 | if (keyStack.peek().indent() < indent) {
329 | break;
330 | }
331 | keyStack.pop();
332 | }
333 | }
334 |
335 | private String trimKey(String indentKey) {
336 | return unquoteKey(indentKey.trim());
337 | }
338 |
339 | private String unquoteKey(String value) {
340 | if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) {
341 | return value.substring(1, value.length() - 1);
342 | }
343 | return value;
344 | }
345 |
346 | private int indent(String line) {
347 | final char[] chars = line.toCharArray();
348 | for (int i = 0; i < chars.length; i++) {
349 | if (!Character.isWhitespace(chars[i])) {
350 | return i;
351 | }
352 | }
353 | return 0;
354 | }
355 |
356 | private static class Key {
357 | private final int indent;
358 | private final String key;
359 |
360 | Key(int indent, String key) {
361 | this.indent = indent;
362 | this.key = key;
363 | }
364 |
365 | int indent() {
366 | return indent;
367 | }
368 |
369 | String key() {
370 | return key;
371 | }
372 | }
373 | }
374 | }
375 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/avaje-config/src/main/java/io/avaje/config/InitialLoader.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import static io.avaje.config.InitialLoader.Source.FILE;
4 | import static io.avaje.config.InitialLoader.Source.RESOURCE;
5 | import static java.lang.System.Logger.Level.WARNING;
6 |
7 | import java.io.File;
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.io.UncheckedIOException;
11 | import java.util.*;
12 | import java.util.regex.Pattern;
13 |
14 | import org.jspecify.annotations.NullMarked;
15 | import org.jspecify.annotations.Nullable;
16 |
17 | import io.avaje.config.CoreEntry.CoreMap;
18 |
19 | /**
20 | * Loads the configuration from known/expected locations.
21 | *
22 | * Defines the loading order of resources and files.
23 | */
24 | @NullMarked
25 | final class InitialLoader {
26 |
27 | private static final Pattern SPLIT_PATHS = Pattern.compile("[\\s,;]+");
28 |
29 | /**
30 | * Return the Expression evaluator using the given properties.
31 | */
32 | static Configuration.ExpressionEval evalFor(Properties properties) {
33 | return new CoreExpressionEval(properties);
34 | }
35 |
36 | enum Source {
37 | RESOURCE,
38 | FILE;
39 | String key(String path) {
40 | return name().toLowerCase() + ':' + path;
41 | }
42 | }
43 |
44 | private final ConfigurationLog log;
45 | private final InitialLoadContext loadContext;
46 | private final Set profileResourceLoaded = new HashSet<>();
47 | private final Parsers parsers;
48 |
49 | InitialLoader(CoreComponents components, ResourceLoader resourceLoader) {
50 | this.parsers = components.parsers();
51 | this.log = components.log();
52 | this.loadContext = new InitialLoadContext(log, resourceLoader);
53 | }
54 |
55 | Set loadedFrom() {
56 | return loadContext.loadedFrom();
57 | }
58 |
59 | /**
60 | * Provides properties by reading known locations.
61 | *
62 | *
Main configuration
63 | *
64 | *
Firstly loads from main resources
65 | *
66 | * - application.properties
67 | * - application.yaml
68 | *
69 | *
70 | *
Then loads from local files
71 | *
72 | * - application.properties
73 | * - application.yaml
74 | *
75 | *
76 | *
Then loads from environment variable PROPS_FILE
77 | * Then loads from system property props.file
78 | * Then loads from load.properties
79 | *
80 | *
Test configuration
81 | *
82 | * Once the main configuration is read it will try to read common test configuration.
83 | * This will only be successful if the test resources are available (i.e. running tests).
84 | *
85 | * Loads from test resources
86 | *
87 | * - application-test.properties
88 | * - application-test.yaml
89 | *
90 | */
91 | CoreMap load() {
92 | loadEnvironmentVars();
93 | loadLocalFiles();
94 | return entryMap();
95 | }
96 |
97 | void initWatcher(CoreConfiguration configuration) {
98 | if (configuration.getBool("config.watch.enabled", false)) {
99 | configuration.setWatcher(new FileWatch(configuration, loadContext.loadedFiles(), parsers));
100 | }
101 | }
102 |
103 | void loadEnvironmentVars() {
104 | loadContext.loadEnvironmentVars();
105 | }
106 |
107 | /**
108 | * Load from local files and resources.
109 | */
110 | void loadLocalFiles() {
111 | loadMain(RESOURCE);
112 | loadViaProfiles(RESOURCE);
113 | // external file configuration overrides the resources configuration
114 | loadMain(FILE);
115 | // load additional profile RESOURCE(s) if added via loadMain()
116 | loadViaProfiles(RESOURCE);
117 | loadViaProfiles(FILE);
118 | loadViaSystemProperty();
119 | loadViaIndirection();
120 | // test configuration (if found) overrides main configuration
121 | // we should only find these resources when running tests
122 | if (!loadTest()) {
123 | loadLocalDev();
124 | }
125 | loadViaCommandLineArgs();
126 | }
127 |
128 | private void loadViaCommandLineArgs() {
129 | final String rawArgs = System.getProperty("sun.java.command");
130 | if (rawArgs != null) {
131 | loadViaCommandLine(rawArgs.split(" "));
132 | }
133 | }
134 |
135 | void loadViaCommandLine(String[] args) {
136 | for (int i = 0; i < args.length; i++) {
137 | String arg = args[i];
138 | if (arg.startsWith("-P") || arg.startsWith("-p")) {
139 | if (arg.length() == 2 && i < args.length - 1) {
140 | // next argument expected to be a properties file paths
141 | i++;
142 | loadCommandLineArg(args[i]);
143 | } else {
144 | // no space between -P and properties file paths
145 | loadCommandLineArg(arg.substring(2));
146 | }
147 | }
148 | }
149 | }
150 |
151 | private void loadCommandLineArg(String arg) {
152 | if (isValidExtension(arg)) {
153 | loadViaPaths(arg);
154 | }
155 | }
156 |
157 | private boolean isValidExtension(String arg) {
158 | var extension = arg.substring(arg.lastIndexOf(".") + 1);
159 | return "properties".equals(extension) || parsers.supportsExtension(extension);
160 | }
161 |
162 | /**
163 | * Provides a way to override properties when running via main() locally.
164 | */
165 | private void loadLocalDev() {
166 | File localDev = new File(System.getProperty("user.home"), ".localdev");
167 | if (localDev.exists()) {
168 | final String appName = loadContext.getAppName();
169 | if (appName != null) {
170 | final String prefix = localDev.getAbsolutePath() + File.separator + appName;
171 | load(prefix, FILE);
172 | }
173 | }
174 | }
175 |
176 | /**
177 | * Load test configuration.
178 | *
179 | * @return true if test properties have been loaded.
180 | */
181 | private boolean loadTest() {
182 | if (Boolean.getBoolean("suppressTestResource")) {
183 | return false;
184 | }
185 | int before = loadContext.size();
186 | load("application-test", RESOURCE);
187 | if (loadProperties("test-ebean.properties", RESOURCE)) {
188 | log.log(WARNING, "Loading properties from test-ebean.properties is deprecated. Please migrate to application-test.yaml or application-test.properties instead.");
189 | }
190 | return loadContext.size() > before;
191 | }
192 |
193 | /**
194 | * Load configuration defined by a load.properties entry in properties file.
195 | */
196 | private void loadViaIndirection() {
197 | String paths = loadContext.indirectLocation();
198 | if (paths != null) {
199 | loadViaPaths(paths);
200 | }
201 | }
202 |
203 | private String@Nullable[] profiles() {
204 | final String paths = loadContext.profiles();
205 | return paths == null ? null : splitPaths(paths);
206 | }
207 |
208 | /**
209 | * Load configuration defined by a config.profiles property.
210 | */
211 | private void loadViaProfiles(Source source) {
212 | final var profiles = profiles();
213 | if (profiles != null) {
214 | for (final String path : profiles) {
215 | final var profile = loadContext.eval(path);
216 | if ((source != RESOURCE || !profileResourceLoaded.contains(profile)) && load("application-" + profile, source)) {
217 | profileResourceLoaded.add(profile);
218 | }
219 | }
220 | }
221 | }
222 |
223 | private void loadViaPaths(String paths) {
224 | for (String rawPath : splitPaths(paths)) {
225 | String path = loadContext.eval(rawPath);
226 | if (!loadContext.alreadyLoaded(path)) {
227 | if (loadWithExtensionCheck(path) && loadContext.allowRecursiveLoad()) {
228 | // depth first recursively load via load.properties entry
229 | loadViaIndirection();
230 | }
231 | }
232 | }
233 | }
234 |
235 | int size() {
236 | return loadContext.size();
237 | }
238 |
239 | String[] splitPaths(String location) {
240 | return SPLIT_PATHS.split(location);
241 | }
242 |
243 | /**
244 | * Load the main configuration for the given source.
245 | */
246 | private void loadMain(Source source) {
247 | load("application", source);
248 | if (loadProperties("ebean.properties", source)) {
249 | log.log(WARNING, "Loading properties from ebean.properties is deprecated. Please migrate to use application.yaml or application.properties instead.");
250 | }
251 | }
252 |
253 | private void loadViaSystemProperty() {
254 | String fileName = System.getenv("PROPS_FILE");
255 | if (fileName == null) {
256 | fileName = System.getProperty("props.file");
257 | if (fileName != null && !loadWithExtensionCheck(fileName)) {
258 | log.log(WARNING, "Unable to find file {0} to load properties", fileName);
259 | }
260 | }
261 | }
262 |
263 | boolean loadWithExtensionCheck(String fileName) {
264 | var extension = fileName.substring(fileName.lastIndexOf(".") + 1);
265 | if ("properties".equals(extension)) {
266 | return loadProperties(fileName, RESOURCE) | loadProperties(fileName, FILE);
267 | } else {
268 | var parser = parsers.get(extension);
269 | if (parser == null) {
270 | throw new IllegalArgumentException(
271 | "Expecting only properties or "
272 | + parsers.supportedExtensions()
273 | + " file extensions but got ["
274 | + fileName
275 | + "]");
276 | }
277 |
278 | return loadCustomExtension(fileName, parser, RESOURCE)
279 | | loadCustomExtension(fileName, parser, FILE);
280 | }
281 | }
282 |
283 | /**
284 | * Evaluate all the configuration entries and return as properties.
285 | */
286 | CoreMap entryMap() {
287 | return loadContext.entryMap();
288 | }
289 |
290 | /**
291 | * Attempt to load a properties and yaml/yml file. Return true if at least one was loaded.
292 | */
293 | boolean load(String resourcePath, Source source) {
294 | return loadProperties(resourcePath + ".properties", source) || loadCustom(resourcePath, source);
295 | }
296 |
297 | private boolean loadCustom(String resourcePath, Source source) {
298 | for (var entry : parsers.entrySet()) {
299 | var extension = entry.getKey();
300 | if (loadCustomExtension(resourcePath + "." + extension, entry.getValue(), source)) {
301 | return true;
302 | }
303 | }
304 | return false;
305 | }
306 |
307 | boolean loadCustomExtension(String resourcePath, ConfigParser parser, Source source) {
308 | try (InputStream is = resource(resourcePath, source)) {
309 | if (is != null) {
310 | var sourceName = source.key(resourcePath);
311 | parser.load(is).forEach((k, v) -> loadContext.put(k, v, sourceName));
312 | return true;
313 | }
314 | } catch (Exception e) {
315 | throw new IllegalStateException("Error loading properties - " + resourcePath, e);
316 | }
317 | return false;
318 | }
319 |
320 | boolean loadProperties(String resourcePath, Source source) {
321 | try (InputStream is = resource(resourcePath, source)) {
322 | if (is != null) {
323 | loadProperties(is, source.key(resourcePath));
324 | return true;
325 | }
326 | } catch (IOException e) {
327 | throw new UncheckedIOException("Error loading properties - " + resourcePath, e);
328 | }
329 | return false;
330 | }
331 |
332 | @Nullable
333 | private InputStream resource(String resourcePath, Source source) {
334 | return loadContext.resource(resourcePath, source);
335 | }
336 |
337 | private void loadProperties(InputStream is, String source) throws IOException {
338 | Properties properties = new Properties();
339 | properties.load(is);
340 | put(properties, source);
341 | }
342 |
343 | private void put(Properties properties, String source) {
344 | Enumeration> enumeration = properties.propertyNames();
345 | while (enumeration.hasMoreElements()) {
346 | String key = (String) enumeration.nextElement();
347 | String val = properties.getProperty(key);
348 | loadContext.put(key, val, source);
349 | }
350 | }
351 |
352 | }
353 |
--------------------------------------------------------------------------------
/avaje-config/src/test/java/io/avaje/config/ConfigTest.java:
--------------------------------------------------------------------------------
1 | package io.avaje.config;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.junit.jupiter.api.Assertions.assertThrows;
5 |
6 | import java.net.URI;
7 | import java.time.Duration;
8 | import java.time.LocalDate;
9 | import java.util.Map;
10 | import java.util.Optional;
11 | import java.util.Properties;
12 | import java.util.concurrent.atomic.AtomicReference;
13 |
14 | import org.junit.jupiter.api.Disabled;
15 | import org.junit.jupiter.api.Test;
16 |
17 | class ConfigTest {
18 |
19 | @Test
20 | void fallbackToSystemProperty_initial() {
21 | System.setProperty("MySystemProp0", "bar");
22 | assertThat(Config.get("MySystemProp0", "foo")).isEqualTo("bar");
23 |
24 | var entry = Config.asConfiguration().entry("MySystemProp0");
25 | assertThat(entry).isPresent().get().satisfies(e -> {
26 | assertThat(e.source()).isEqualTo("SystemProperty");
27 | assertThat(e.value()).isEqualTo("bar");
28 | });
29 |
30 | // cached the initial value, still bar even when system property changed
31 | System.setProperty("MySystemProp0", "bazz");
32 | assertThat(Config.get("MySystemProp0")).isEqualTo("bar");
33 |
34 | // mutate via Config.setProperty()
35 | Config.setProperty("MySystemProp0", "caz");
36 | assertThat(Config.get("MySystemProp0")).isEqualTo("caz");
37 |
38 | assertThat(Config.list().of("MySystemProp0")).contains("caz");
39 | assertThat(Config.set().of("MySystemProp0")).contains("caz");
40 | }
41 |
42 | @Test
43 | void getNullable() {
44 | assertThat(Config.getNullable("IDoNotExist0")).isNull();
45 | assertThat(Config.getNullable("IDoNotExist0", System.getenv("AlsoDoNotExist"))).isNull();
46 |
47 | var entry = Config.asConfiguration().entry("IDoNotExist0");
48 | assertThat(entry).isEmpty();
49 | }
50 |
51 | @Test
52 | void getSnakeList() {
53 | assertThat(Config.list().of("metal.gear")).contains("snake?", "Snake!?", "SNAAAAKE!!");
54 | }
55 |
56 | @Test
57 | void getNullableExists() {
58 | assertThat(Config.getNullable("IDoNotExist1", "SomeVal")).isEqualTo("SomeVal");
59 | System.setProperty("MyRareSystemProp", "hello");
60 | assertThat(Config.getNullable("MyRareSystemProp", System.getenv("AlsoDoNotExist"))).isEqualTo("hello");
61 | }
62 |
63 | @Test
64 | void getOptionalWithNullDefault() {
65 | assertThat(Config.getOptional("IDoNotExist2", System.getenv("AlsoDoNotExist"))).isEmpty();
66 | }
67 |
68 | @Test
69 | void getOptionalWithDefault() {
70 | assertThat(Config.getOptional("IDoNotExist3", System.getProperty("user.home"))).isNotEmpty();
71 | }
72 |
73 | @Test
74 | void fallbackToSystemProperty_cacheInitialNullValue() {
75 | assertThat(Config.getOptional("MySystemProp")).isEmpty();
76 | System.setProperty("MySystemProp", "hello");
77 | // cached the initial null so still null
78 | assertThat(Config.getOptional("MySystemProp")).isEmpty();
79 | }
80 |
81 | @Test
82 | void fallbackToSystemProperty_cacheInitialValue() {
83 | assertThat(Config.get("MySystemProp2", "foo")).isEqualTo("foo");
84 | System.setProperty("MySystemProp2", "notFoo");
85 | // cached the initial value foo so still foo
86 | assertThat(Config.get("MySystemProp2")).isEqualTo("foo");
87 | Config.clearProperty("MySystemProp2");
88 | }
89 |
90 | @Test
91 | void setProperty() {
92 | assertThat(Config.getOptional("MySystemProp3")).isEmpty();
93 | Config.setProperty("MySystemProp3", "hello2");
94 |
95 | assertThat(Config.get("MySystemProp3")).isEqualTo("hello2");
96 | Config.clearProperty("MySystemProp3");
97 | }
98 |
99 | @Test
100 | void eventBuilderPublish() {
101 | assertThat(Config.getOptional("MySystemProp4")).isEmpty();
102 | Config.eventBuilder("MyChange").put("MySystemProp4", "hello4").publish();
103 |
104 | assertThat(Config.get("MySystemProp4")).isEqualTo("hello4");
105 | Config.clearProperty("MySystemProp4");
106 | }
107 |
108 | @Test
109 | void onChangeEventListener() {
110 | assertThat(Config.getOptional("MySystemProp5")).isEmpty();
111 | AtomicReference capturedEvent = new AtomicReference<>();
112 | Config.onChange(capturedEvent::set);
113 | Config.setProperty("MySystemProp5", "hi5");
114 |
115 | ModificationEvent event = capturedEvent.get();
116 |
117 | assertThat(event.name()).isEqualTo("SetProperty");
118 | assertThat(event.modifiedKeys()).containsExactly("MySystemProp5");
119 | assertThat(Config.get("MySystemProp5")).isEqualTo("hi5");
120 |
121 | Config.clearProperty("MySystemProp5");
122 | }
123 |
124 | @Test
125 | void asProperties() {
126 | String home = System.getProperty("user.home");
127 |
128 | final Properties properties = Config.asProperties();
129 | assertThat(properties.getProperty("myapp.fooName")).isEqualTo("Hello");
130 | assertThat(properties.getProperty("myapp.fooHome")).isEqualTo(home + "/config");
131 |
132 | var entry = Config.asConfiguration().entry("myapp.fooName");
133 | assertThat(entry).isPresent().get().satisfies(e -> {
134 | assertThat(e.source()).isEqualTo("resource:application-test.yaml");
135 | assertThat(e.value()).isEqualTo("Hello");
136 | });
137 |
138 | assertThat(Config.get("myExternalLoader")).isEqualTo("wasExecuted");
139 |
140 | assertThat(properties.getProperty("config.load.systemProperties")).isEqualTo("true");
141 | assertThat(System.getProperty("config.load.systemProperties")).isEqualTo("true");
142 | assertThat(System.getProperty("myExternalLoader")).isEqualTo("wasExecuted");
143 | assertThat(Config.getBool("config.load.systemProperties")).isTrue();
144 | assertThat(System.getProperty("myapp.fooName")).isNull();
145 | assertThat(System.getProperty("myapp.bar.barRules")).isNull();
146 | assertThat(System.getProperty("myapp.bar.barDouble")).isEqualTo("33.3");
147 |
148 | assertThat(properties).containsKeys("config.load.systemProperties", "config.watch.enabled", "myExternalLoader", "myapp.activateFoo", "myapp.bar.barDouble", "myapp.bar.barRules", "myapp.fooHome", "myapp.fooName", "system.excluded.properties");
149 | assertThat(properties).hasSize(13);
150 | }
151 |
152 | @Test
153 | void asConfiguration() {
154 | String home = System.getProperty("user.home");
155 |
156 | final Configuration configuration = Config.asConfiguration();
157 | assertThat(configuration.get("myapp.fooName")).isEqualTo("Hello");
158 | assertThat(configuration.get("myapp.fooHome")).isEqualTo(home + "/config");
159 | }
160 |
161 | @Test
162 | void putAll() {
163 | Map extra = Map.of("myTempKey", "foo");
164 | Config.putAll(extra);
165 |
166 | Map extra2 = Map.of("myTempKey2", "foo2");
167 | Config.putAll(extra2);
168 |
169 | final Configuration configuration = Config.asConfiguration();
170 | assertThat(configuration.get("myTempKey")).isEqualTo("foo");
171 | assertThat(configuration.get("myTempKey2")).isEqualTo("foo2");
172 |
173 | configuration.clearProperty("myTempKey");
174 | configuration.clearProperty("myTempKey2");
175 | }
176 |
177 | @Disabled
178 | @Test
179 | void load() {
180 | assertThat(System.getProperty("myapp.fooName")).isNull();
181 |
182 | assertThat(Config.get("myapp.fooName")).isEqualTo("Hello");
183 | assertThat(System.getProperty("myapp.fooName")).isNull();
184 |
185 | Config.loadIntoSystemProperties();
186 | assertThat(System.getProperty("myapp.fooName")).isEqualTo("Hello");
187 | }
188 |
189 | @Test
190 | void get() {
191 | assertThat(Config.get("myapp.fooName", "junk")).isEqualTo("Hello");
192 | }
193 |
194 | @Test
195 | void get_withEval() {
196 | String home = System.getProperty("user.home");
197 | assertThat(Config.get("myapp.fooHome")).isEqualTo(home + "/config");
198 | }
199 |
200 | @Test
201 | void get_dockerHost() {
202 | Optional dockerHost = Config.getOptional("myapp.testHost");
203 | assertThat(dockerHost).isPresent();
204 | String testHost = dockerHost.get();
205 | assertThat(testHost).isNotEqualTo("${docker.host}");
206 | assertThat(System.getProperty("docker.host")).isEqualTo(testHost);
207 | }
208 |
209 | @Test
210 | public void get_default() {
211 | assertThat(Config.get("myapp.doesNotExist2", "MyDefault")).isEqualTo("MyDefault");
212 | assertThat(Config.get("myapp.doesNotExist2")).isEqualTo("MyDefault");
213 | }
214 |
215 | @Test
216 | void get_default_repeated_expect_returnDefaultValue() {
217 | assertThat(Config.getOptional("myapp.doesNotExist3")).isEmpty();
218 | assertThat(Config.get("myapp.doesNotExist3", "other")).isEqualTo("other");
219 | assertThat(Config.get("myapp.doesNotExist3", "foo")).isEqualTo("other"); // No longer "foo" to be consistent with getBool treatment
220 | assertThat(Config.get("myapp.doesNotExist3", "junk")).isEqualTo("other");
221 | }
222 |
223 | @Test
224 | void get_optional() {
225 | assertThat(Config.getOptional("myapp.doesNotExist")).isEmpty();
226 | assertThat(Config.getOptional("myapp.fooName")).isNotEmpty();
227 | }
228 |
229 | @Test
230 | void getBool_required_missing() {
231 | Config.clearProperty("myapp.doesNotExist");
232 | assertThrows(IllegalStateException.class, () -> Config.getBool("myapp.doesNotExist"));
233 | }
234 |
235 | @Test
236 | void enabled_required_missing() {
237 | Config.clearProperty("myapp.doesNotExist");
238 | assertThrows(IllegalStateException.class, () -> Config.enabled("myapp.doesNotExist"));
239 | }
240 |
241 | @Test
242 | void getBool_required_set() {
243 | assertThat(Config.getBool("myapp.activateFoo")).isTrue();
244 | assertThat(Config.enabled("myapp.activateFoo")).isTrue();
245 | }
246 |
247 | @Test
248 | void getBool() {
249 | assertThat(Config.getBool("myapp.activateFoo", false)).isTrue();
250 | assertThat(Config.enabled("myapp.activateFoo", false)).isTrue();
251 | }
252 |
253 | @Test
254 | void getBool_default() {
255 | assertThat(Config.getBool("myapp.doesNotExist", false)).isFalse();
256 | // default value is cached, still false
257 | assertThat(Config.getBool("myapp.doesNotExist", true)).isFalse();
258 | // can dynamically change
259 | Config.setProperty("myapp.doesNotExist", "true");
260 | assertThat(Config.getBool("myapp.doesNotExist", true)).isTrue();
261 | }
262 |
263 | @Test
264 | void enabled_default() {
265 | assertThat(Config.enabled("myapp.en.doesNotExist", false)).isFalse();
266 | // default value is cached, still false
267 | assertThat(Config.enabled("myapp.en.doesNotExist", true)).isFalse();
268 | // can dynamically change
269 | Config.setProperty("myapp.en.doesNotExist", "true");
270 | assertThat(Config.enabled("myapp.en.doesNotExist", true)).isTrue();
271 | Config.clearProperty("myapp.en.doesNotExist");
272 | }
273 |
274 | @Test
275 | void getInt_required_missing() {
276 | assertThrows(IllegalStateException.class, () -> Config.getInt("myapp.doesNotExist"));
277 | }
278 |
279 | @Test
280 | void getInt_required_set() {
281 | assertThat(Config.getInt("myapp.bar.barRules")).isEqualTo(42);
282 | }
283 |
284 | @Test
285 | void getInt() {
286 | assertThat(Config.getInt("myapp.bar.barRules", 99)).isEqualTo(42);
287 | }
288 |
289 | @Test
290 | void getInt_default() {
291 | assertThat(Config.getInt("myapp.bar.doesNotExist", 99)).isEqualTo(99);
292 | }
293 |
294 | @Test
295 | void getLong_required_missing() {
296 | assertThrows(IllegalStateException.class, () -> Config.getLong("myapp.bar.doesNotExist"));
297 | }
298 |
299 | @Test
300 | void getLong_required_set() {
301 | assertThat(Config.getLong("myapp.bar.barRules")).isEqualTo(42L);
302 | }
303 |
304 | @Test
305 | void getLong() {
306 | assertThat(Config.getLong("myapp.bar.barRules", 99)).isEqualTo(42L);
307 | }
308 |
309 | @Test
310 | void getLong_default() {
311 | assertThat(Config.getLong("myapp.bar.doesNotExist", 99)).isEqualTo(99L);
312 | }
313 |
314 | @Test
315 | void getDecimal_doesNotExist() {
316 | assertThrows(IllegalStateException.class, () -> Config.getDecimal("myTestDecimal.doesNotExist"));
317 | }
318 |
319 | @Test
320 | void getDecimal_default() {
321 | assertThat(Config.getDecimal("myTestDecimal.doesNotExist", "10.4")).isEqualByComparingTo("10.4");
322 | Config.clearProperty("myTestDecimal.doesNotExist");
323 | }
324 |
325 | @Test
326 | void getDecimal() {
327 | Config.setProperty("myTestDecimal", "14.3");
328 | assertThat(Config.getDecimal("myTestDecimal")).isEqualByComparingTo("14.3");
329 | assertThat(Config.getDecimal("myTestDecimal", "10.4")).isEqualByComparingTo("14.3");
330 | Config.clearProperty("myTestDecimal");
331 | }
332 |
333 | @Test
334 | void getURI() {
335 | Config.setProperty("myConfigUrl", "http://bana");
336 | assertThat(Config.getURI("myConfigUrl")).isEqualTo(URI.create("http://bana"));
337 | assertThat(Config.getURI("myConfigUrl", "http://two")).isEqualTo(URI.create("http://bana"));
338 | Config.clearProperty("myConfigUrl");
339 | }
340 |
341 | @Test
342 | void getDuration() {
343 | Config.setProperty("myConfigDuration", "PT10H");
344 | assertThat(Config.getDuration("myConfigDuration")).isEqualTo(Duration.parse("PT10H"));
345 | assertThat(Config.getDuration("myConfigDuration", "PT10H")).isEqualTo(Duration.parse("PT10H"));
346 | Config.clearProperty("myConfigDuration");
347 | }
348 |
349 | @Test
350 | void getEnum_doesNotExist() {
351 | assertThrows(IllegalStateException.class, () -> Config.getEnum(MyTestEnum.class, "myTestEnum.doesNotExist"));
352 | }
353 |
354 | @Test
355 | void getEnum_default() {
356 | assertThat(Config.getEnum(MyTestEnum.class, "myTestEnum.doesNotExist2", MyTestEnum.C)).isEqualTo(MyTestEnum.C);
357 | Config.clearProperty("myTestEnum.doesNotExist2");
358 | }
359 |
360 | @Test
361 | void getEnum() {
362 | Config.setProperty("myTestEnum", "B");
363 | assertThat(Config.getEnum(MyTestEnum.class, "myTestEnum")).isEqualTo(MyTestEnum.B);
364 | assertThat(Config.getEnum(MyTestEnum.class, "myTestEnum", MyTestEnum.C)).isEqualTo(MyTestEnum.B);
365 | Config.clearProperty("myTestEnum");
366 | }
367 |
368 | @Test
369 | void getAs_example() {
370 | Config.setProperty("func", "2023-11-03");
371 |
372 | LocalDate asLocalDate = Config.getAs("func", LocalDate::parse);
373 | assertThat(asLocalDate).isEqualTo(LocalDate.of(2023, 11, 3));
374 |
375 | Config.clearProperty("func");
376 | }
377 |
378 | @Test
379 | void getAs_func() {
380 | Config.setProperty("func", "amogus");
381 | final var result =
382 | Config.getAs(
383 | "func",
384 | x -> {
385 | assertThat(x).isEqualTo("amogus");
386 | return "sus";
387 | });
388 | assertThat(result).isEqualTo("sus");
389 |
390 | assertThrows(
391 | IllegalStateException.class,
392 | () ->
393 | Config.getAs(
394 | "func",
395 | x -> {
396 | throw new RuntimeException("broke");
397 | }));
398 | Config.clearProperty("func");
399 | }
400 |
401 | @Test
402 | void getAsOptional_func() {
403 | Config.setProperty("func", "fire");
404 | var result =
405 | Config.getAsOptional(
406 | "func",
407 | x -> {
408 | assertThat(x).isEqualTo("fire");
409 | return "sus";
410 | });
411 |
412 | assertThat(result.orElseThrow()).isEqualTo("sus");
413 |
414 | assertThrows(
415 | IllegalStateException.class,
416 | () ->
417 | Config.getAsOptional(
418 | "func",
419 | x -> {
420 | throw new RuntimeException("broke");
421 | }));
422 | Config.clearProperty("func");
423 |
424 | result =
425 | Config.getAsOptional(
426 | "func",
427 | x -> {
428 | assertThat(x).isEqualTo("fire");
429 | return null;
430 | });
431 |
432 | assertThat(result.isEmpty()).isEqualTo(true);
433 | }
434 |
435 | enum MyTestEnum {
436 | A, B, C
437 | }
438 |
439 | @Test
440 | void onChange() {
441 |
442 | AtomicReference ref = new AtomicReference<>();
443 | ref.set("initialValue");
444 |
445 | Config.onChange("some.key", ref::set);
446 |
447 | assertThat(ref.get()).isEqualTo("initialValue");
448 | Config.setProperty("some.key", "val1");
449 | assertThat(ref.get()).isEqualTo("val1");
450 |
451 | Config.setProperty("some.key", "val2");
452 | assertThat(ref.get()).isEqualTo("val2");
453 | }
454 |
455 | @Test
456 | void onChangeInt() {
457 |
458 | AtomicReference ref = new AtomicReference<>();
459 | ref.set(1);
460 |
461 | Config.onChangeInt("some.intKey", ref::set);
462 |
463 | assertThat(ref.get()).isEqualTo(1);
464 |
465 | Config.setProperty("some.intKey", "2");
466 | assertThat(ref.get()).isEqualTo(2);
467 |
468 | Config.setProperty("some.intKey", "42");
469 | assertThat(ref.get()).isEqualTo(42);
470 | }
471 |
472 | @Test
473 | void onChangeLong() {
474 |
475 | AtomicReference ref = new AtomicReference<>();
476 | ref.set(1L);
477 |
478 | Config.onChangeLong("some.longKey", ref::set);
479 |
480 | assertThat(ref.get()).isEqualTo(1);
481 |
482 | Config.setProperty("some.longKey", "2");
483 | assertThat(ref.get()).isEqualTo(2);
484 |
485 | Config.setProperty("some.longKey", "42");
486 | assertThat(ref.get()).isEqualTo(42);
487 | }
488 |
489 | @Test
490 | void onChangeBool() {
491 |
492 | AtomicReference ref = new AtomicReference<>();
493 | ref.set(false);
494 |
495 | Config.onChangeBool("some.boolKey", ref::set);
496 |
497 | assertThat(ref.get()).isFalse();
498 |
499 | Config.setProperty("some.boolKey", "true");
500 | assertThat(ref.get()).isTrue();
501 |
502 | Config.setProperty("some.boolKey", "false");
503 | assertThat(ref.get()).isFalse();
504 | }
505 | }
506 |
--------------------------------------------------------------------------------