io.datakernel.config.Config Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of datakernel-boot Show documentation
Show all versions of datakernel-boot Show documentation
An intelligent way of booting complex applications and services according to their dependencies
package io.datakernel.config;
import io.datakernel.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.datakernel.util.Preconditions.checkArgument;
import static io.datakernel.util.Preconditions.checkNotNull;
import static java.util.Collections.*;
/**
* Interface for interaction with configs.
*/
public interface Config {
Logger logger = LoggerFactory.getLogger(Config.class);
/**
* Empty config with no values, children, etc...
*/
Config EMPTY = new Config() {
@Override
public String getValue(@Nullable String defaultValue) {
return defaultValue;
}
@Override
public String getValue() throws NoSuchElementException {
throw new NoSuchElementException();
}
@Override
public Map getChildren() {
return emptyMap();
}
};
String THIS = "";
String DELIMITER = ".";
Pattern DELIMITER_PATTERN = Pattern.compile(Pattern.quote(DELIMITER));
Pattern PATH_PATTERN = Pattern.compile("([0-9a-zA-Z_-]+(\\.[0-9a-zA-Z_-]+)*)?");
static String concatPath(String prefix, String suffix) {
return prefix.isEmpty() || suffix.isEmpty() ? prefix + suffix : prefix + DELIMITER + suffix;
}
static void checkPath(String path) {
checkArgument(PATH_PATTERN.matcher(path).matches(), "Invalid path %s", path);
}
/**
* @return value stored in root or defaultValue
*/
default String getValue(@Nullable String defaultValue) {
return get(THIS, defaultValue);
}
/**
* @return value stored in root
* @throws NoSuchElementException if there is nothing in root
*/
default String getValue() throws NoSuchElementException {
return get(THIS);
}
Map getChildren();
default boolean hasValue() {
return getValue(null) != null;
}
default boolean hasChildren() {
return !getChildren().isEmpty();
}
default boolean hasChild(String path) {
checkPath(path);
Config config = this;
for (String key : DELIMITER_PATTERN.split(path)) {
if (key.isEmpty()) continue;
Map children = config.getChildren();
if (!children.containsKey(key)) return false;
config = children.get(key);
}
return true;
}
default boolean isEmpty() {
return !hasValue() && !hasChildren();
}
/**
* Throw {@code NoSuchElementException} if there is no value in path.
* @return String value that lays in path
* @see Config#get(ConfigConverter, String, Object)
*/
default String get(String path) throws NoSuchElementException {
checkPath(path);
return getChild(path).getValue();
}
/**
* @return String value that lays in path
* @see Config#get(ConfigConverter, String, Object)
*/
default String get(String path, @Nullable String defaultValue) {
checkPath(path);
return getChild(path).getValue(defaultValue);
}
/**
* Throw {@code NoSuchElementException} if there is no value in path.
* @return value converted to <T>
* @see Config#get(ConfigConverter, String, Object)
*/
default T get(ConfigConverter converter, String path) throws NoSuchElementException {
return converter.get(getChild(path));
}
/**
* @param converter specifies how to convert config string value into <T>
* @param path path to config value. Example: "rpc.server.port" to get port for rpc server.
* @param return type
* @return value from this {@code Config} in path or defaultValue if there is nothing on that path
* @see ConfigConverters
*/
default T get(ConfigConverter converter, String path, @Nullable T defaultValue) {
return converter.get(getChild(path), defaultValue);
}
/**
* @return child {@code Config} if it exists, {@link Config#EMPTY} config otherwise
*/
default Config getChild(String path) {
checkPath(path);
Config config = this;
for (String key : path.split(Pattern.quote(DELIMITER))) {
if (key.isEmpty()) continue;
Map children = config.getChildren();
config = children.containsKey(key) ? children.get(key) : config.provideNoKeyChild(key);
}
return config;
}
default Config provideNoKeyChild(String key) {
checkArgument(!getChildren().containsKey(key));
return EMPTY;
}
/**
* Applies setter to value in path using converter to get it
* @param type of value
*/
default void apply(ConfigConverter converter, String path, Consumer setter) {
checkPath(path);
T value = get(converter, path);
setter.accept(value);
}
/**
* The same as {@link Config#apply(ConfigConverter, String, Consumer)} but with default value
*/
default void apply(ConfigConverter converter, String path, T defaultValue, Consumer setter) {
apply(converter, path, defaultValue, (value, $) -> setter.accept(value));
}
/**
* The same as {@link Config#apply(ConfigConverter, String, Consumer)} but with default value and BiConsumer
* Note that BiConsumer receives value and defaultValue as arguments
*/
default void apply(ConfigConverter converter, String path, T defaultValue, BiConsumer setter) {
checkPath(path);
T value = get(converter, path, defaultValue);
setter.accept(value, defaultValue);
}
static BiConsumer ifNotDefault(Consumer setter) {
return (value, defaultValue) -> {
if (!Objects.equals(value, defaultValue)) {
setter.accept(value);
}
};
}
static Consumer ifNotNull(Consumer setter) {
return (value) -> {
if (value != null) {
setter.accept(value);
}
};
}
static Consumer ifNotDefault(T defaultValue, Consumer setter) {
return value -> {
if (!Objects.equals(value, defaultValue)) {
setter.accept(value);
}
};
}
/**
* @return empty config
*/
static Config create() {
return EMPTY;
}
/**
* Creates new config from properties
* @return new {@code Config}
*/
static Config ofProperties(Properties properties) {
return ofMap(properties.stringPropertyNames().stream()
.collect(Collectors.toMap(k -> k, properties::getProperty,
(u, v) -> {throw new AssertionError();}, LinkedHashMap::new)));
}
/**
* The same as {@link Config#ofProperties(String, boolean)} but with optional=false
* @return new {@code Config}
*/
static Config ofProperties(String fileName) {
return ofProperties(fileName, false);
}
/**
* @see Config#ofProperties(Path, boolean)
*/
static Config ofProperties(String fileName, boolean optional) {
return ofProperties(Paths.get(fileName), optional);
}
/**
* Creates new config from file
* @param file with properties
* @param optional if true will log warning "Can't load..." else throws exception
* @return new {@code Config}
*/
static Config ofProperties(Path file, boolean optional) {
Properties props = new Properties();
try (InputStream is = Files.newInputStream(file)) {
props.load(is);
} catch (IOException e) {
if (optional) {
logger.warn("Can't load properties file: {}", file);
} else {
throw new IllegalArgumentException("Failed to load required properties: " + file.toString(), e);
}
}
return ofProperties(props);
}
/**
* Creates config from Map
* @param map of path, value pairs
* @return new {@code Config}
*/
static Config ofMap(Map map) {
Config config = create();
for (String path : map.keySet()) {
String value = map.get(path);
config = config.with(path, value);
}
return config;
}
static Config ofConfigs(Map map) {
Config config = create();
for (String path : map.keySet()) {
Config childConfig = map.get(path);
config = config.with(path, childConfig);
}
return config;
}
/**
* @return new {@code Config} with only one value
*/
static Config ofValue(String value) {
return create().with(THIS, value);
}
/**
* @param configConverter specifies converter for <T>
* @param value of type <T>
* @return new {@code Config} with given value
*/
static Config ofValue(ConfigConverter configConverter, T value) {
EffectiveConfig effectiveConfig = EffectiveConfig.wrap(Config.create());
configConverter.get(effectiveConfig, value);
return ofMap(effectiveConfig.getEffectiveDefaults());
}
static Config lazyConfig(Supplier configSupplier) {
return new Config() {
private Config actualConfig;
private synchronized Config ensureConfig() {
if (actualConfig == null) {
actualConfig = configSupplier.get();
}
return actualConfig;
}
@Override
public String getValue(@Nullable String defaultValue) {
return ensureConfig().getValue(defaultValue);
}
@Override
public String getValue() throws NoSuchElementException {
return ensureConfig().getValue();
}
@Override
public Map getChildren() {
return ensureConfig().getChildren();
}
};
}
/**
* @param path path
* @return new {@code Config} with value in path
*/
default Config with(String path, String value) {
checkPath(path);
checkNotNull(value);
return with(path, new Config() {
@Override
public String getValue(@Nullable String defaultValue) {
return value;
}
@Override
public String getValue() throws NoSuchElementException {
return value;
}
@Override
public Map getChildren() {
return emptyMap();
}
});
}
/**
* @param path path
* @param config holds one value at root
* @return new {@code Config} with overridden value in path
* this method returns new config instead of changing the old one.
*/
default Config with(String path, Config config) {
checkPath(path);
checkNotNull(config);
String value = config.getValue(null);
String[] keys = path.split(Pattern.quote(DELIMITER));
for (int i = keys.length - 1; i >= 0; i--) {
String key = keys[i];
if (key.isEmpty()) continue;
Map map = singletonMap(key, config);
config = new Config() {
@Override
public String getValue(@Nullable String defaultValue) {
return defaultValue;
}
@Override
public String getValue() throws NoSuchElementException {
throw new NoSuchElementException();
}
@Override
public Map getChildren() {
return map;
}
};
}
return override(config);
}
/**
* @param other config with values
* @return new {@code Config} with values from this config overridden by values from other
* this method returns new config instead of changing the old one.
*/
default Config override(Config other) {
String otherValue = other.getValue(null);
Map otherChildren = other.getChildren();
if (otherValue == null && otherChildren.isEmpty()) {
return this;
}
String value = otherValue != null ? otherValue : getValue(null);
Map children = new LinkedHashMap<>(getChildren());
otherChildren.forEach((key, otherChild) -> children.merge(key, otherChild, Config::override));
Map finalChildren = unmodifiableMap(children);
return new Config() {
@Override
public String getValue(@Nullable String defaultValue) {
return value != null ? value : defaultValue;
}
@Override
public String getValue() throws NoSuchElementException {
if (value != null) return value;
throw new NoSuchElementException();
}
@Override
public Map getChildren() {
return finalChildren;
}
};
}
/**
* Tries to merge two configs into one. Throws {@code IllegalArgumentException} if there are conflicts.
* @param other config to merge with
* @return new merged {@code Config}
* this method returns new config instead of changing the old one.
*/
default Config combine(Config other) {
String thisValue = getValue(null);
String otherValue = other.getValue(null);
if (thisValue != null && otherValue != null) {
throw new IllegalArgumentException("Duplicate values\n" + this.toMap() + "\n" + other.toMap());
}
Map children = new LinkedHashMap<>(getChildren());
other.getChildren().forEach((key, otherChild) -> children.merge(key, otherChild, Config::combine));
return Config.EMPTY
.override(thisValue != null ? Config.ofValue(thisValue) : Config.EMPTY)
.override(otherValue != null ? Config.ofValue(otherValue) : Config.EMPTY)
.override(Config.ofConfigs(children));
}
/**
* Converts this config to {@code Map}
* @return new {@code Map} where path and value are Strings
*/
default Map toMap() {
Map result = new LinkedHashMap<>();
if (hasValue()) {
result.put(THIS, getValue());
}
Map children = getChildren();
for (String key : children.keySet()) {
Map childMap = children.get(key).toMap();
result.putAll(childMap.entrySet().stream()
.collect(Collectors.toMap(entry -> concatPath(key, entry.getKey()), Map.Entry::getValue)));
}
return result;
}
/**
* Converts this config to {@code Properties}
* @return Properties with config values
*/
default Properties toProperties() {
Properties properties = new Properties();
toMap().forEach(properties::setProperty);
return properties;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy