Skip to content

Commit

Permalink
Adopt SmallRye Config
Browse files Browse the repository at this point in the history
Closes stevespringett#695

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Oct 19, 2024
1 parent 4eb935d commit a97148c
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 107 deletions.
4 changes: 4 additions & 0 deletions alpine-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
Expand Down
167 changes: 66 additions & 101 deletions alpine-common/src/main/java/alpine/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -532,7 +535,7 @@ public List<String> 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.
* <p>
* 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.
* <p>
* Properties <strong>must</strong> be prefixed with {@code ALPINE_} (for environment variables) or {@code alpine.}
Expand All @@ -545,33 +548,19 @@ public List<String> getPropertyAsList(Key key) {
*/
public Map<String, String> getPassThroughProperties(final String prefix) {
final var passThroughProperties = new HashMap<String, String>();
try {
for (final Map.Entry<String, String> 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<Object, Object> 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;
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down
17 changes: 14 additions & 3 deletions alpine-common/src/test/java/alpine/ConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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"
));
}

Expand Down
36 changes: 34 additions & 2 deletions alpine-common/src/test/java/alpine/common/util/ThreadUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Alpine.DataNucleus.From.Props.MixedCase=fromProps9
alpine.datanucleus.expression.from.props=${alpine.datanucleus.foo}
Loading

0 comments on commit a97148c

Please sign in to comment.