diff --git a/alpine-common/pom.xml b/alpine-common/pom.xml
index 906eeda3..cf44e287 100644
--- a/alpine-common/pom.xml
+++ b/alpine-common/pom.xml
@@ -35,6 +35,10 @@
org.apache.commonscommons-lang3
+
+ io.smallrye.config
+ smallrye-config-core
+ com.fasterxml.jackson.corejackson-annotations
diff --git a/alpine-common/src/main/java/alpine/Config.java b/alpine-common/src/main/java/alpine/Config.java
index ca67eb99..d88b1096 100644
--- a/alpine-common/src/main/java/alpine/Config.java
+++ b/alpine-common/src/main/java/alpine/Config.java
@@ -22,22 +22,36 @@
import alpine.common.util.ByteFormat;
import alpine.common.util.PathUtil;
import alpine.common.util.SystemUtil;
+import io.smallrye.config.ConfigSourceInterceptor;
+import io.smallrye.config.ConfigSourceInterceptorContext;
+import io.smallrye.config.ConfigSourceInterceptorFactory;
+import io.smallrye.config.ExpressionConfigSourceInterceptor;
+import io.smallrye.config.Priorities;
+import io.smallrye.config.ProfileConfigSourceInterceptor;
+import io.smallrye.config.RelocateConfigSourceInterceptor;
+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;
import java.nio.file.Files;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.OptionalInt;
import java.util.Properties;
import java.util.UUID;
-
+import java.util.stream.Collectors;
+
+import static io.smallrye.config.PropertiesConfigSourceLoader.inFileSystem;
+import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOCATIONS;
+import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_LOG_VALUES;
+import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN;
+import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE;
+import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_PROFILE_PARENT;
import static java.util.function.Predicate.not;
/**
@@ -50,14 +64,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));
@@ -218,40 +231,71 @@ 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.2/config/getting-started/#config-sources
+ .addDefaultSources()
+ // Support expressions.
+ // https://smallrye.io/smallrye-config/3.10.2/config/expressions/
+ .withInterceptors(new ExpressionConfigSourceInterceptor())
+ // Support profiles.
+ // https://smallrye.io/smallrye-config/3.10.2/config/profiles/
+ .withInterceptors(new ProfileConfigSourceInterceptor(List.of("prod", "dev", "test")))
+ // Relocate SmallRye properties to the Alpine prefix for better framework "immersion".
+ // https://smallrye.io/smallrye-config/3.10.2/extensions/relocate/
+ .withInterceptorFactories(new ConfigSourceInterceptorFactory() {
+
+ @Override
+ public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) {
+ // Properties to be relocated are documented here:
+ // https://smallrye.io/smallrye-config/3.10.2/config/configuration/
+ return new RelocateConfigSourceInterceptor(Map.ofEntries(
+ Map.entry(SMALLRYE_CONFIG_PROFILE, "alpine.config.profile"),
+ Map.entry(SMALLRYE_CONFIG_PROFILE_PARENT, "alpine.config.profile.parent"),
+ Map.entry(SMALLRYE_CONFIG_LOCATIONS, "alpine.config.locations"),
+ Map.entry(SMALLRYE_CONFIG_LOG_VALUES, "alpine.config.log.values"),
+ Map.entry(SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, "alpine.config.mapping.validate-unknown")));
+ }
+
+ @Override
+ public OptionalInt getPriority() {
+ // Priority must be higher than that of ProfileConfigSourceInterceptor
+ // in order for profile property relocations to work.
+ return OptionalInt.of(Priorities.LIBRARY + 210);
+ }
+
+ })
+ .withDefaultValue("alpine.profile", "prod")
+ // Allow applications to customize the Config via SPI.
+ // https://smallrye.io/smallrye-config/3.10.2/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);
@@ -313,6 +357,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
@@ -432,15 +490,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);
}
/**
@@ -455,21 +508,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);
}
/**
@@ -520,15 +569,10 @@ public boolean getPropertyAsBoolean(Key key) {
* @since 2.2.5
*/
public List getPropertyAsList(Key key) {
- String property = getProperty(key);
- if (property == null) {
- return Collections.emptyList();
- } else {
- return Arrays.stream(property.split(","))
- .map(String::trim)
- .filter(not(String::isEmpty))
- .toList();
- }
+ return delegateConfig.getValues(key.getPropertyName(), String.class).stream()
+ .map(String::trim)
+ .filter(not(String::isEmpty))
+ .collect(Collectors.toList());
}
/**
@@ -538,9 +582,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.
@@ -551,33 +592,25 @@ 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