io.jstach.kiwi.boot.KiwiConfig Maven / Gradle / Ivy
The newest version!
package io.jstach.kiwi.boot;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.jspecify.annotations.Nullable;
import io.jstach.kiwi.kvs.KeyValue;
import io.jstach.kiwi.kvs.KeyValues;
import io.jstach.kiwi.kvs.KeyValuesEnvironment;
import io.jstach.kiwi.kvs.KeyValuesEnvironment.Logger;
import io.jstach.kiwi.kvs.KeyValuesSystem;
import io.jstach.kiwi.kvs.Variables;
/**
* Kiwi Config is tiny opinionated configuration framework modeled after Spring Boot that
* uses Kiwi KVS system to load key values into a Map. It is made to be a slightly better
* {@link System#getProperties()}.
*
* Features are purposely very limited.
*
* The loading is as follows:
*
* - System.getProperties and System.getEnv are load for interpolation.
* - Resource
classpath:/application.properties
is loaded (but not
* required)
* - Resource
profile.classpath:/application__PROFILE__.properties
is
* loaded (but not required)
*
* Example Usage
* {@snippet :
* KiwiConfig.of().getProperty(key); // Use instead of System.getProperty
* }
*
*/
public sealed interface KiwiConfig {
/**
* Analogous to {@link System#getProperty(String)}.
* @param key to use for lookup for matching value.
* @return value or null
if not found.
*/
@Nullable
String getProperty(String key);
/**
* Set default properties which will be used in addition to those in the found on
* loading. This is analogous to Spring Boots
* SpringApplication.setDefaultProperties
.
* @param properties the additional properties to set
*/
public static void setDefaultProperties(Map properties) {
Holder.PROPERTIES = KeyValues.builder().add(properties.entrySet()).build();
}
/**
* Gets the global config singleton. Analogous to {@link System#getProperties()}.
* @return global config that is reloadable.
*/
public static ReloadableConfig of() {
return Holder.Hidden.CONFIG;
}
/**
* Creates a stable config based on the opinionated loading. Usually {@link #of()} is
* the more desirable call.
* @return a new loaded stable config.
* @see #of()
*/
public static StableConfig create() {
return Holder.load();
}
/**
* A reloadable config allows reloading but does not guarantee that repeated calls of
* {@link #getProperty(String)} and similar will return the same results. Notice that
* there is no event propagation or watching of resources. That is am exercise for the
* consumer of the library.
*/
sealed interface ReloadableConfig extends KiwiConfig {
/**
* The current backing stable config (might be generated).
* @return stable config.
*/
public StableConfig stableConfig();
/**
* Will reload the config and return the previous config.
* @return previous config.
*/
public KiwiConfig reload();
}
/**
* A stable config guarantees that repeated calls of {@link #getProperty(String)} and
* similar will return the same result.
*/
non-sealed interface StableConfig extends KiwiConfig {
/**
* Describes the key like what resource it came from. Stable Config has this
* method because calling describe on reloadable config may generate different
* results.
* @param key to describe.
* @return description.
* @throws NoSuchElementException if the key is missing.
*/
String describe(String key) throws NoSuchElementException;
/**
* Generates a map from the config.
* @return key value map of config.
*/
Map toMap();
}
}
class QueueLogger implements Logger {
private final ConcurrentLinkedQueue events = new ConcurrentLinkedQueue<>();
record Event(System.Logger.Level level, String message) {
}
@Override
public void init(KeyValuesSystem system) {
events.add(new Event(System.Logger.Level.INFO, "Initializing"));
}
@Override
public void debug(String message) {
events.add(new Event(System.Logger.Level.DEBUG, message));
}
@Override
public void info(String message) {
events.add(new Event(System.Logger.Level.INFO, message));
}
@Override
public void warn(String message) {
events.add(new Event(System.Logger.Level.WARNING, message));
}
@Override
public void closed(KeyValuesSystem system) {
System.Logger logger = System.getLogger("io.jstach.kiwi.boot.KiwiConfig");
Event e;
while ((e = events.poll()) != null) {
logger.log(e.level, e.message);
}
}
@Override
public void fatal(Exception exception) {
var err = System.err;
Event e;
while ((e = events.poll()) != null) {
err.println("[" + Logger.formatLevel(e.level) + "] io.jstach.kiwi.boot.KiwiConfig - " + e.message);
}
err.println("[ERROR] io.jstach.kiwi.boot.KiwiConfig - " + exception.getMessage());
exception.printStackTrace(err);
}
}
final class Holder {
static volatile @Nullable KeyValues PROPERTIES;
static class Hidden {
final static KiwiConfig.ReloadableConfig CONFIG = new WrapperKiwiConfig(load());
}
static KiwiConfig.StableConfig load() {
record BootEnvironment(QueueLogger logger) implements KeyValuesEnvironment {
@Override
public Logger getLogger() {
return this.logger;
}
}
var logger = new QueueLogger();
try (var system = KeyValuesSystem.builder() //
.useServiceLoader()
.environment(new BootEnvironment(logger))
.build()) {
var properties = Holder.PROPERTIES;
var loader = system //
.loader();
if (properties != null) {
loader.add("setDefaultProperties", properties);
}
var kvs = loader.variables(Variables::ofSystemProperties)
.variables(Variables::ofSystemEnv)
.variables(RandomVariables::of)
.add("classpath:/application.properties", b -> b.name("application").noRequire(true))
.add("profile.classpath:/application-__PROFILE__.properties", b -> b.name("profiles").noRequire(true))
.load();
Map m = new LinkedHashMap<>();
for (var kv : kvs) {
m.put(kv.key(), kv);
}
var config = new DefaultKiwiConfig("application.properties", m);
config.setSystemProperties(system);
return config;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
final class WrapperKiwiConfig implements KiwiConfig.ReloadableConfig {
volatile KiwiConfig.StableConfig config;
public WrapperKiwiConfig(KiwiConfig.StableConfig config) {
super();
this.config = config;
}
@Override
public @Nullable String getProperty(String key) {
return config.getProperty(key);
}
@Override
public StableConfig stableConfig() {
return config;
}
@Override
public KiwiConfig reload() {
var c = config;
config = Holder.load();
return c;
}
}
final class DefaultKiwiConfig implements KiwiConfig.StableConfig {
private final String description;
private final Map keyValues;
public DefaultKiwiConfig(String description, Map keyValues) {
super();
this.description = description;
this.keyValues = keyValues;
}
@Override
public @Nullable String getProperty(String key) {
var kv = keyValues.get(key);
if (kv != null) {
return kv.expanded();
}
return null;
}
@Override
public String describe(String key) {
return this.description;
}
@Override
public Map toMap() {
return toMap(keyValues);
}
private static Map toMap(Map kvs) {
Map m = new LinkedHashMap<>();
for (var e : kvs.entrySet()) {
m.put(e.getKey(), e.getValue().value());
}
return Map.copyOf(m);
}
void setSystemProperties(KeyValuesSystem system) {
var logger = system.environment().getLogger();
Map properties = toMap();
Map propertiesToBeSet = new LinkedHashMap<>();
final Set commandLineProperties = getJvmCommandLinePropertyNames();
for (Entry e : properties.entrySet()) {
if (commandLineProperties.contains(e.getKey())) {
logger.info("Not overriding command line property: " + e.getKey());
}
else if (denyPropertyToBeOverridden(e.getKey())) {
logger.info("Not overriding restricted property: " + e.getKey());
}
else {
if (Objects.isNull(e.getValue())) {
logger.warn("Property key: " + e.getKey() + " is null");
}
else {
propertiesToBeSet.put(e.getKey(), e.getValue());
}
}
}
propertiesToBeSet.put("SYSTEM_PROPERTIES_LOADED", "true");
var systemProperties = system.environment().getSystemProperties();
for (var e : propertiesToBeSet.entrySet()) {
systemProperties.setProperty(e.getKey(), e.getValue());
}
}
static boolean denyPropertyToBeOverridden(@Nullable String property) {
if (property == null)
return true;
return (property.startsWith("java.") || property.startsWith("sun.") || property.equals("user.dir")
|| property.equals("user.name") || property.equals("path.separator") || property.startsWith("os."));
}
Set getJvmCommandLinePropertyNames() {
if (!isJmxModuleAvailable()) {
return Collections.emptySet();
}
try {
List args = ManagementFactory.getRuntimeMXBean().getInputArguments();
if (args.isEmpty()) {
return Collections.emptySet();
}
Set props = new HashSet<>(args.size());
for (String s : args) {
s = s.trim();
if (!s.startsWith("-D")) {
continue;
}
int end = s.indexOf("=");
props.add(s.substring(2, end > 3 ? end : s.length()));
}
return props;
}
catch (Throwable e) {
return Collections.emptySet();
}
}
private static boolean isJmxModuleAvailable() {
ModuleLayer bootLayer = ModuleLayer.boot();
return bootLayer.findModule("java.management").isPresent();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy