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

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

Go to download

A flexible configuration tree abstraction. Supports multiple configuration sources.

There is a newer version: 6.0-rc2
Show newest version
/*
 * 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