io.activej.config.Config Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of activej-config Show documentation
Show all versions of activej-config Show documentation
A flexible configuration tree abstraction. Supports multiple configuration sources.
/*
* Copyright (C) 2020 ActiveJ LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.activej.config;
import io.activej.config.converter.ConfigConverter;
import io.activej.config.converter.ConfigConverters;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
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.activej.common.Checks.checkArgument;
import static io.activej.common.Utils.nonNullElse;
import static io.activej.common.Utils.nonNullOrException;
import static java.util.Collections.unmodifiableMap;
/**
* 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 @Nullable String getValue(@Nullable String defaultValue) {
return defaultValue;
}
@Override
public String getValue() throws NoSuchElementException {
throw new NoSuchElementException("No value at empty config node");
}
@Override
public Map getChildren() {
return Map.of();
}
};
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);
}
/**
* Returns a value stored in root or a default value
*/
default String getValue(@Nullable String defaultValue) {
return get(THIS, defaultValue);
}
/**
* Returns a 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();
}
/**
* Returns a 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));
}
/**
* Returns a value from this {@link Config} converted using a given config converter
*
* @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);
}
/**
* Returns a 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), "Children already contain key '%s'", 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);
}
};
}
/**
* Returns an empty config
*/
static Config create() {
return EMPTY;
}
/**
* Creates a new config from all system properties
*
* @return new {@code Config}
*/
static Config ofSystemProperties() {
return ofProperties(System.getProperties());
}
/**
* Creates a new config from system properties that start with a prefix
*
* @return new {@code Config}
*/
static Config ofSystemProperties(String prefix) {
//noinspection unchecked - properties are expected to have String keys and values
return ofMap(System.getProperties().entrySet().stream()
.map(e -> (Map.Entry) (Map.Entry, ?>) e)
.filter(entry -> entry.getKey().startsWith(prefix))
.collect(Collectors.toMap(
e -> e.getKey().length() == prefix.length() ?
"" :
e.getKey().substring(prefix.length() + 1),
Map.Entry::getValue)));
}
/**
* Creates a new config from properties
*
* @return new {@code Config}
*/
static Config ofProperties(Properties properties) {
//noinspection unchecked - properties are expected to have String keys and values
return ofMap((Map) (Map, ?>) properties);
}
/**
* 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);
}
/**
* Creates a new config from properties file
*
* @see #ofProperties(Path, boolean)
*/
static Config ofProperties(String fileName, boolean optional) {
return ofProperties(Paths.get(fileName), optional);
}
/**
* Creates a new config from properties file that resides in a class path
*
* @see #ofClassPathProperties(String, ClassLoader, boolean) (Path, boolean)
*/
static Config ofClassPathProperties(String fileName) {
return ofClassPathProperties(fileName, Thread.currentThread().getContextClassLoader(), false);
}
/**
* Creates a new config from properties file that resides in a class path
*
* @see #ofClassPathProperties(String, ClassLoader, boolean) (Path, boolean)
*/
static Config ofClassPathProperties(String fileName, ClassLoader classLoader) {
return ofClassPathProperties(fileName, classLoader, false);
}
/**
* Creates a new config from properties file that resides in a class path
*
* @see #ofClassPathProperties(String, ClassLoader, boolean) (Path, boolean)
*/
static Config ofClassPathProperties(String fileName, boolean optional) {
return ofClassPathProperties(fileName, Thread.currentThread().getContextClassLoader(), optional);
}
/**
* Creates a new config from properties file that resides in a class path
*
* @param fileName a name of a properties file
* @param classLoader a class loader that will be used to load a properties file from a class path
* @param optional whether a properties file is optional
* @return a new {@link Config}
*/
static Config ofClassPathProperties(String fileName, ClassLoader classLoader, boolean optional) {
Properties props = new Properties();
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
try (InputStream resource = classLoader.getResourceAsStream(fileName)) {
if (resource == null) {
throw new FileNotFoundException(fileName);
}
props.load(resource);
} catch (IOException e) {
if (optional) {
logger.warn("Can't load properties file: {}", fileName);
} else {
throw new IllegalArgumentException("Failed to load required properties: " + fileName, e);
}
}
return ofProperties(props);
}
/**
* Creates a new config from properties file
*
* @param file properties file
* @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, 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 (Map.Entry entry : map.entrySet()) {
config = config.with(entry.getKey(), entry.getValue());
}
return config;
}
static Config ofConfigs(Map map) {
Config config = create();
for (Map.Entry entry : map.entrySet()) {
config = config.with(entry.getKey(), entry.getValue());
}
return config;
}
/**
* Returns a new {@code Config} with only one value
*/
static Config ofValue(String value) {
return create().with(THIS, value);
}
/**
* Creates a config out of a given value and a config converter for a 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 volatile Config actualConfig;
private Config ensureConfig() {
if (actualConfig == null) {
synchronized (this) {
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();
}
};
}
/**
* Adds a value to a given path for this {@link Config}
*
* @param path path
* @return new {@code Config} with value in path
*/
default Config with(String path, String value) {
checkPath(path);
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 Map.of();
}
});
}
/**
* Adds a {@link Config} to a given path for this {@link Config}
*
* @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);
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 = Map.of(key, config);
config = new Config() {
@Override
public @Nullable String getValue(@Nullable String defaultValue) {
return defaultValue;
}
@Override
public String getValue() throws NoSuchElementException {
throw new NoSuchElementException("No value at intermediate config node");
}
@Override
public Map getChildren() {
return map;
}
};
}
return overrideWith(config);
}
/**
* Overrides this config with another 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 overrideWith(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::overrideWith));
Map finalChildren = unmodifiableMap(children);
return new Config() {
@Override
public @Nullable String getValue(@Nullable String defaultValue) {
return nonNullElse(value, defaultValue);
}
@Override
public String getValue() throws NoSuchElementException {
return nonNullOrException(value, () -> new NoSuchElementException("No value at config node"));
}
@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 combineWith(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::combineWith));
return Config.EMPTY
.overrideWith(thisValue != null ? Config.ofValue(thisValue) : Config.EMPTY)
.overrideWith(otherValue != null ? Config.ofValue(otherValue) : Config.EMPTY)
.overrideWith(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 (Map.Entry entry : children.entrySet()) {
Map childMap = entry.getValue().toMap();
result.putAll(childMap.entrySet().stream()
.collect(Collectors.toMap(e -> concatPath(entry.getKey(), e.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 - 2025 Weber Informatics LLC | Privacy Policy