From ecdc70029b3861c9566cbd72259130fa3b921c60 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sat, 19 Oct 2024 22:19:13 +0200 Subject: [PATCH] Adopt SmallRye Config Closes #695 Signed-off-by: nscuro --- alpine-common/pom.xml | 4 + .../src/main/java/alpine/Config.java | 176 ++++++++---------- .../src/test/java/alpine/ConfigTest.java | 66 ++++++- .../alpine/common/util/ThreadUtilTest.java | 36 +++- ...ig_testGetPassThroughProperties.properties | 3 +- ...rye.config.SmallRyeConfigBuilderCustomizer | 1 + pom.xml | 7 + 7 files changed, 184 insertions(+), 109 deletions(-) create mode 100644 alpine-common/src/test/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer diff --git a/alpine-common/pom.xml b/alpine-common/pom.xml index e05f2144..0bde487e 100644 --- a/alpine-common/pom.xml +++ b/alpine-common/pom.xml @@ -35,6 +35,10 @@ org.apache.commons commons-lang3 + + io.smallrye.config + smallrye-config-core + com.fasterxml.jackson.core jackson-annotations diff --git a/alpine-common/src/main/java/alpine/Config.java b/alpine-common/src/main/java/alpine/Config.java index c8d26d60..8af8005e 100644 --- a/alpine-common/src/main/java/alpine/Config.java +++ b/alpine-common/src/main/java/alpine/Config.java @@ -22,10 +22,12 @@ import alpine.common.util.ByteFormat; import alpine.common.util.PathUtil; import alpine.common.util.SystemUtil; +import io.smallrye.config.ExpressionConfigSourceInterceptor; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; import org.apache.commons.lang3.StringUtils; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,6 +39,8 @@ import java.util.Properties; import java.util.UUID; +import static io.smallrye.config.PropertiesConfigSourceLoader.inFileSystem; + /** * The Config class is responsible for reading the application.properties file. * @@ -47,14 +51,13 @@ public class Config { private static final Logger LOGGER = Logger.getLogger(Config.class); private static final String ALPINE_APP_PROP = "alpine.application.properties"; - private static final String PROP_FILE = "application.properties"; private static final String ALPINE_VERSION_PROP_FILE = "alpine.version"; private static final String APPLICATION_VERSION_PROP_FILE = "application.version"; private static final Config INSTANCE; - private static Properties properties; private static Properties alpineVersionProperties; private static Properties applicationVersionProperties; private static String systemId; + private static org.eclipse.microprofile.config.Config delegateConfig; static { LOGGER.info(StringUtils.repeat("-", 80)); @@ -215,40 +218,43 @@ public static Config getInstance() { * Initialize the Config object. This method should only be called once. */ void init() { - if (properties != null) { + if (delegateConfig != null) { return; } LOGGER.info("Initializing Configuration"); - properties = new Properties(); - + final SmallRyeConfigBuilder configBuilder = new SmallRyeConfigBuilder() + .forClassLoader(Thread.currentThread().getContextClassLoader()) + // Enable default config sources: + // + // | Source | Priority | + // | :--------------------------------------------------- | :------- | + // | System properties | 400 | + // | Environment variables | 300 | + // | ${pwd}/.env file | 295 | + // | ${pwd}/config/application.properties | 260 | + // | ${classpath}/application.properties | 250 | + // | ${classpath}/META-INF/microprofile-config.properties | 100 | + // + // https://smallrye.io/smallrye-config/3.10.0/config/getting-started/#config-sources + .addDefaultSources() + // Support expressions. + // https://smallrye.io/smallrye-config/3.10.0/config/expressions/ + .withInterceptors(new ExpressionConfigSourceInterceptor()) + // Allow applications to customize the Config via SPI. + // https://smallrye.io/smallrye-config/3.10.0/config/customizer/ + .addDiscoveredCustomizers(); + + // If a custom properties file is specified via "alpine.application.properties" system property, + // register it as additional config source. The file has a higher priority than any of the default + // properties sources. final String alpineAppProp = PathUtil.resolve(System.getProperty(ALPINE_APP_PROP)); if (StringUtils.isNotBlank(alpineAppProp)) { - LOGGER.info("Loading application properties from " + alpineAppProp); - try (InputStream fileInputStream = Files.newInputStream((new File(alpineAppProp)).toPath())) { - properties.load(fileInputStream); - } catch (FileNotFoundException e) { - LOGGER.error("Could not find property file " + alpineAppProp); - } catch (IOException e) { - LOGGER.error("Unable to load " + alpineAppProp); - } - } else { - LOGGER.info("System property " + ALPINE_APP_PROP + " not specified"); - LOGGER.info("Loading " + PROP_FILE + " from classpath"); - try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROP_FILE)) { - if (in != null) { - properties.load(in); - } else { - LOGGER.error("Unable to load (resourceStream is null) " + PROP_FILE); - } - } catch (IOException e) { - LOGGER.error("Unable to load " + PROP_FILE); - } - } - if (properties.size() == 0) { - LOGGER.error("A fatal error occurred loading application properties. Please correct the issue and restart the application."); + configBuilder.withSources(inFileSystem(alpineAppProp, 275, Thread.currentThread().getContextClassLoader())); } + delegateConfig = configBuilder.build(); + alpineVersionProperties = new Properties(); try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(ALPINE_VERSION_PROP_FILE)) { alpineVersionProperties.load(in); @@ -310,6 +316,20 @@ void init() { } } + /** + * @since 3.2.0 + */ + public org.eclipse.microprofile.config.Config getDelegate() { + return delegateConfig; + } + + /** + * @since 3.2.0 + */ + public T getMapping(final Class mappingClass) { + return delegateConfig.unwrap(SmallRyeConfig.class).getConfigMapping(mappingClass); + } + /** * Retrieves the path where the system.id is stored * @return a File representing the path to the system.id @@ -429,15 +449,10 @@ public File getDataDirectorty() { * @since 1.0.0 */ public String getProperty(Key key) { - final String envVariable = getPropertyFromEnvironment(key); - if (envVariable != null) { - return envVariable; - } - if (key.getDefaultValue() == null) { - return properties.getProperty(key.getPropertyName()); - } else { - return properties.getProperty(key.getPropertyName(), String.valueOf(key.getDefaultValue())); - } + return delegateConfig.getOptionalValue(key.getPropertyName(), String.class) + .orElseGet(() -> key.getDefaultValue() != null + ? String.valueOf(key.getDefaultValue()) + : null); } /** @@ -452,21 +467,17 @@ public String getProperty(Key key) { * @since 1.7.0 */ public String getPropertyOrFile(AlpineKey key) { - final AlpineKey fileKey = AlpineKey.valueOf(key.toString()+"_FILE"); - final String filePath = getProperty(fileKey); - final String prop = getProperty(key); - if (StringUtils.isNotBlank(filePath)) { - if (prop != null && !prop.equals(String.valueOf(key.getDefaultValue()))) { - LOGGER.warn(fileKey.getPropertyName() + " overrides value from property " + key.getPropertyName()); - } - try { - return new String(Files.readAllBytes(new File(PathUtil.resolve(filePath)).toPath())).replaceAll("\\s+", ""); - } catch (IOException e) { - LOGGER.error(filePath + " file doesn't exist or not readable."); - return null; - } - } - return prop; + return delegateConfig.getOptionalValue(key.getPropertyName() + ".file", String.class) + .map(filePath -> { + try { + return new String(Files.readAllBytes(new File(PathUtil.resolve(filePath)).toPath())).replaceAll("\\s+", ""); + } catch (IOException e) { + LOGGER.error(filePath + " file doesn't exist or not readable.", e); + return null; + } + }) + .or(() -> delegateConfig.getOptionalValue(key.getPropertyName(), String.class)) + .orElse(null); } /** @@ -532,9 +543,6 @@ public List getPropertyAsList(Key key) { * Their main use-case is to allow users to configure certain aspects of libraries and frameworks used by Alpine, * without Alpine having to introduce {@link AlpineKey}s for every single option. *

- * Properties are read from both environment variables, and {@link #PROP_FILE}. - * When a property is defined in both environment and {@code application.properties}, environment takes precedence. - *

* Properties must be prefixed with {@code ALPINE_} (for environment variables) or {@code alpine.} * (for {@code application.properties}) respectively. The Alpine prefix will be removed in keys of the returned * {@link Map}, but the given {@code prefix} will be retained. @@ -545,33 +553,19 @@ public List getPropertyAsList(Key key) { */ public Map getPassThroughProperties(final String prefix) { final var passThroughProperties = new HashMap(); - try { - for (final Map.Entry envVar : System.getenv().entrySet()) { - if (envVar.getKey().startsWith("ALPINE_%s_".formatted(prefix.toUpperCase().replace(".", "_")))) { - final String key = envVar.getKey().replaceFirst("^ALPINE_", "").toLowerCase().replace("_", "."); - passThroughProperties.put(key, envVar.getValue()); - } - } - } catch (SecurityException e) { - LOGGER.warn(""" - Unable to retrieve pass-through properties for prefix "%s" \ - from environment variables. Using defaults.""".formatted(prefix), e); - } - for (final Map.Entry property : properties.entrySet()) { - if (property.getKey() instanceof String key - && key.startsWith("alpine.%s.".formatted(prefix)) - && property.getValue() instanceof final String value) { - key = key.replaceFirst("^alpine\\.", ""); - if (!passThroughProperties.containsKey(key)) { // Environment variables take precedence - passThroughProperties.put(key, value); - } + for (final String propertyName : delegateConfig.getPropertyNames()) { + if (!propertyName.startsWith("alpine.%s.".formatted(prefix))) { + continue; } + + final String key = propertyName.replaceFirst("^alpine\\.", ""); + passThroughProperties.put(key, delegateConfig.getValue(propertyName, String.class)); } return passThroughProperties; } static void reset() { - properties = null; + delegateConfig = null; } /** @@ -583,7 +577,7 @@ static void reset() { */ @Deprecated public String getProperty(String key) { - return properties.getProperty(key); + return delegateConfig.getOptionalValue(key, String.class).orElse(null); } /** @@ -596,31 +590,7 @@ public String getProperty(String key) { */ @Deprecated public String getProperty(String key, String defaultValue) { - return properties.getProperty(key, defaultValue); - } - - /** - * Attempts to retrieve the key via environment variable. Property names are - * always upper case with periods replaced with underscores. - * - * alpine.worker.threads - * becomes - * ALPINE_WORKER_THREADS - * - * @param key the key to retrieve from environment - * @return the value of the key (if set), null otherwise. - * @since 1.4.3 - */ - private String getPropertyFromEnvironment(Key key) { - final String envVariable = key.getPropertyName().toUpperCase().replace(".", "_"); - try { - return StringUtils.trimToNull(System.getenv(envVariable)); - } catch (SecurityException e) { - LOGGER.warn("A security exception prevented access to the environment variable. Using defaults."); - } catch (NullPointerException e) { - // Do nothing. The key was not specified in an environment variable. Continue along. - } - return null; + return delegateConfig.getOptionalValue(key, String.class).orElse(defaultValue); } /** diff --git a/alpine-common/src/test/java/alpine/ConfigTest.java b/alpine-common/src/test/java/alpine/ConfigTest.java index 5ea32280..f289b6cd 100644 --- a/alpine-common/src/test/java/alpine/ConfigTest.java +++ b/alpine-common/src/test/java/alpine/ConfigTest.java @@ -1,5 +1,9 @@ package alpine; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; +import io.smallrye.config.WithDefault; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -8,7 +12,11 @@ import org.junitpioneer.jupiter.SetEnvironmentVariable; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Map; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -43,11 +51,15 @@ public void testGetPassThroughPropertiesEmpty() { @SetEnvironmentVariable(key = "ALPINE_DATANUCLEUS_FROM_ENV", value = "fromEnv7") @SetEnvironmentVariable(key = "alpine_datanucleus_from_env_lowercase", value = "fromEnv8") @SetEnvironmentVariable(key = "Alpine_DataNucleus_From_Env_MixedCase", value = "fromEnv9") - public void testGetPassThroughProperties() { + @SetEnvironmentVariable(key = "ALPINE_DATANUCLEUS_EXPRESSION_FROM_ENV", value = "${alpine.datanucleus.from.env}") + public void testGetPassThroughProperties() throws Exception { final URL propertiesUrl = ConfigTest.class.getResource("/Config_testGetPassThroughProperties.properties"); assertThat(propertiesUrl).isNotNull(); - System.setProperty("alpine.application.properties", propertiesUrl.getPath()); + final Path tmpPropertiesFile = Files.createTempFile(null, ".properties"); + Files.copy(propertiesUrl.openStream(), tmpPropertiesFile, StandardCopyOption.REPLACE_EXISTING); + + System.setProperty("alpine.application.properties", tmpPropertiesFile.toUri().toString()); Config.getInstance().init(); @@ -56,8 +68,56 @@ public void testGetPassThroughProperties() { "datanucleus.foo", "fromEnv3", // ENV takes precedence over properties "datanucleus.foo.bar", "fromEnv4", // ENV takes precedence over properties "datanucleus.from.env", "fromEnv7", - "datanucleus.from.props", "fromProps7" + "datanucleus.from.props", "fromProps7", + "datanucleus.from.env.lowercase", "fromEnv8", + "datanucleus.from.env.mixedcase", "fromEnv9", + "datanucleus.expression.from.props", "fromEnv3", + "datanucleus.expression.from.env", "fromEnv7" )); } + @ConfigMapping(prefix = "alpine") + public interface TestConfig { + + DatabaseConfig database(); + + interface DatabaseConfig { + + Optional url(); + + @WithDefault("testUser") + String username(); + + Map pool(); + + } + + } + + public static class ConfigBuilderCustomizer implements SmallRyeConfigBuilderCustomizer { + + @Override + public void configBuilder(final SmallRyeConfigBuilder configBuilder) { + configBuilder + .withMapping(TestConfig.class) + .withValidateUnknown(false); + } + + } + + @Test + @RestoreEnvironmentVariables + @SetEnvironmentVariable(key = "ALPINE_DATABASE_URL", value = "jdbc:h2:mem:alpine") + @SetEnvironmentVariable(key = "ALPINE_DATABASE_POOL_MAX_SIZE", value = "666") + void testGetMapping() { + Config.getInstance().init(); + + final var testConfig = Config.getInstance().getMapping(TestConfig.class); + assertThat(testConfig).isNotNull(); + assertThat(testConfig.database().url()).contains("jdbc:h2:mem:alpine"); + assertThat(testConfig.database().username()).isEqualTo("testUser"); + assertThat(testConfig.database().pool()) + .containsExactlyInAnyOrderEntriesOf(Map.of("max.size", "666")); + } + } \ No newline at end of file diff --git a/alpine-common/src/test/java/alpine/common/util/ThreadUtilTest.java b/alpine-common/src/test/java/alpine/common/util/ThreadUtilTest.java index 29f56a9a..d16990db 100644 --- a/alpine-common/src/test/java/alpine/common/util/ThreadUtilTest.java +++ b/alpine-common/src/test/java/alpine/common/util/ThreadUtilTest.java @@ -18,24 +18,56 @@ */ package alpine.common.util; +import alpine.Config; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RestoreEnvironmentVariables; import org.junitpioneer.jupiter.SetEnvironmentVariable; +import java.lang.reflect.Method; + class ThreadUtilTest { + private static Method configInitMethod; + private static Method configResetMethod; + + @BeforeAll + public static void setUp() throws Exception { + configInitMethod = Config.class.getDeclaredMethod("init"); + configInitMethod.setAccessible(true); + + configResetMethod = Config.class.getDeclaredMethod("reset"); + configResetMethod.setAccessible(true); + } + + @AfterEach + public void tearDown() throws Exception { + configResetMethod.invoke(Config.getInstance()); + } + + @AfterAll + public static void tearDownClass() throws Exception { + configResetMethod.invoke(Config.getInstance()); // Ensure we're not affecting other tests. + } + @Test @RestoreEnvironmentVariables @SetEnvironmentVariable(key = "ALPINE_WORKER_THREADS", value = "10") - void determineNumberOfWorkerThreadsStaticTest() { + void determineNumberOfWorkerThreadsStaticTest() throws Exception { + configInitMethod.invoke(Config.getInstance()); + Assertions.assertEquals(10, ThreadUtil.determineNumberOfWorkerThreads()); } @Test @RestoreEnvironmentVariables @SetEnvironmentVariable(key = "ALPINE_WORKER_THREADS", value = "0") - void determineNumberOfWorkerThreadsDynamicTest() { + void determineNumberOfWorkerThreadsDynamicTest() throws Exception { + configInitMethod.invoke(Config.getInstance()); + Assertions.assertTrue(ThreadUtil.determineNumberOfWorkerThreads() > 0); } diff --git a/alpine-common/src/test/resources/Config_testGetPassThroughProperties.properties b/alpine-common/src/test/resources/Config_testGetPassThroughProperties.properties index 17591ad7..432931dd 100644 --- a/alpine-common/src/test/resources/Config_testGetPassThroughProperties.properties +++ b/alpine-common/src/test/resources/Config_testGetPassThroughProperties.properties @@ -6,4 +6,5 @@ alpine.data.nucleus.foo=fromProps5 datanucleus.foo=fromProps6 alpine.datanucleus.from.props=fromProps7 ALPINE.DATANUCLEUS.FROM.PROPS.UPPERCASE=fromProps8 -Alpine.DataNucleus.From.Props.MixedCase=fromProps9 \ No newline at end of file +Alpine.DataNucleus.From.Props.MixedCase=fromProps9 +alpine.datanucleus.expression.from.props=${alpine.datanucleus.foo} \ No newline at end of file diff --git a/alpine-common/src/test/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer b/alpine-common/src/test/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer new file mode 100644 index 00000000..804cf05c --- /dev/null +++ b/alpine-common/src/test/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer @@ -0,0 +1 @@ +alpine.ConfigTest$ConfigBuilderCustomizer \ No newline at end of file diff --git a/pom.xml b/pom.xml index 752774e5..e598233e 100644 --- a/pom.xml +++ b/pom.xml @@ -191,6 +191,7 @@ 1.1.7 1.1.7 2.0.12 + 3.10.0 2.2.25 5.11.2 @@ -228,6 +229,12 @@ jersey-client ${lib.jersey.version} + + + io.smallrye.config + smallrye-config-core + ${lib.smallrye-config.version} + jakarta.servlet