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..169403c2 100644 --- a/alpine-common/src/main/java/alpine/Config.java +++ b/alpine-common/src/main/java/alpine/Config.java @@ -22,10 +22,11 @@ import alpine.common.util.ByteFormat; import alpine.common.util.PathUtil; import alpine.common.util.SystemUtil; +import io.smallrye.config.ExpressionConfigSourceInterceptor; +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 +38,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 +50,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 +217,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/Main/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 +315,13 @@ void init() { } } + /** + * @since 3.2.0 + */ + public org.eclipse.microprofile.config.Config getDelegate() { + return delegateConfig; + } + /** * Retrieves the path where the system.id is stored * @return a File representing the path to the system.id @@ -429,15 +441,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 +459,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,7 +535,7 @@ 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}. + * Properties are read from both environment variables, and foo. * 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.} @@ -545,33 +548,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 +572,7 @@ static void reset() { */ @Deprecated public String getProperty(String key) { - return properties.getProperty(key); + return delegateConfig.getOptionalValue(key, String.class).orElse(null); } /** @@ -596,31 +585,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..dcef8f38 100644 --- a/alpine-common/src/test/java/alpine/ConfigTest.java +++ b/alpine-common/src/test/java/alpine/ConfigTest.java @@ -8,6 +8,9 @@ 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 static org.assertj.core.api.Assertions.assertThat; @@ -43,11 +46,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,7 +63,11 @@ 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" )); } 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/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