All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.config.Config Maven / Gradle / Ivy

Go to download

An intelligent way of booting complex applications and services according to their dependencies

There is a newer version: 3.1.0
Show newest version
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