/*
* Copyright (C) 2015 SoftIndex 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.datakernel.config;
import io.datakernel.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import static io.datakernel.util.Preconditions.checkNotNull;
import static io.datakernel.util.Preconditions.checkState;
import static java.util.Arrays.asList;
/**
* Represents properties in tree form and contains methods that allow to work
* with config tree. Provides convenient static factory methods for
* instantiating, persisting and modifying a {@code Config}. The {@code Config}
* can be instantiated with a given {@link Properties} object, file or path to
* file with properties.
* <p>
* The property consists of a key and a value. The tree path can represent
* either a whole key or several chunks of some property's key. Key parts must
* be separated using "." delimiter. The value of the property is stored in the
* last node of the tree path, represented by a whole key.
* <p>
* In addition, there is a helpful {@link ConfigConverter} interface along with
* {@link ConfigConverters} class, which provides a lot of handy converter
* implementations.
* <p>
* For example, consider a config instance, which stores several properties:
* <pre><code>
* Config config = Config.create(); //create a config
*
* String key1 = "socket_addr";
* String key2 = "is_available";
* String key3 = "connections_count";
*
* config.set(property1Key, "250.200.100.50:10000");
* config.set(property2Key, "true");
* config.set(property3Key, "10");
* </code></pre>
* Next code snippet demonstrates the convenience of config converters:
* <pre><code>
* //instantiating converters (static import may be used instead)
* ConfigConverter<InetSocketAddress> isac = ConfigConverters.ofInetSocketAddress();
* ConfigConverter<Boolean> bc = ConfigConverters.ofBoolean();
* ConfigConverter<Integer> ic= ConfigConverters.ofInteger();
*
* //will return an InetSocketAddress object (250.200.100.50:10000)
* config.get(isac, property1Key);
*
* //will return Boolean true
* config.get(bc, property2Key);
*
* //will return Integer 10
* config.get(ic, property3Key);
* </code></pre>
*
* @see ConfigConverter
* @see ConfigConverters
*/
@SuppressWarnings("unchecked")
public final class Config {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static final String ROOT = "";
public static final char SEPARATOR = '.';
private final Map<String, Config> children = new LinkedHashMap<>();
private final Config parent;
private String value;
private String defaultValue;
private boolean accessed;
private boolean modified;
private Config() {
this.parent = null;
}
public static Config create() {
return new Config();
}
private Config(Config parent) {
this.parent = parent;
}
/**
* Creates a config with properties, stored in specified object.
*
* @param properties config properties
* @return config with given properties
*/
public static Config ofProperties(Properties properties) {
Config root = new Config(null);
for (String key : properties.stringPropertyNames()) {
Config entry = root.ensureChild(key);
entry.value = properties.getProperty(key);
}
return root;
}
/**
* Creates a config with properties contained in the file.
*
* @param propertiesFile file with properties
* @return config with properties from file
* @throws RuntimeException if an input or output exception occures
*/
public static Config ofProperties(File propertiesFile) {
final Properties properties = new Properties();
try (InputStream fis = new FileInputStream(propertiesFile)) {
properties.load(fis);
} catch (Exception e) {
throw new RuntimeException(e);
}
return ofProperties(properties);
}
/**
* Creates a config with properties from a file on the specified path.
*
* @param propertiesFile path to properties file
* @return config with properties from file
* @throws RuntimeException if an input or output exception occured
*/
public static Config ofProperties(String propertiesFile) {
return ofProperties(propertiesFile, false);
}
/**
* Creates a config with properties from a file on the specified
* filepath.
*
* @param propertiesFile path to properties file
* @param optional
* @return config with properties from file
* @throws RuntimeException if an input or output exception occured
*/
public static Config ofProperties(String propertiesFile, boolean optional) {
final File file = new File(propertiesFile);
return ofProperties(file, optional);
}
/**
* Creates a config from a given file if file exists. In case of file
* absence returns blank config if optional is true or throws
* {@code RuntimeException} otherwise.
*
* @param file file containing properties
* @param optional defines behaviour if file doesn't exist
* @return config with properties from file or blank config
* @throws RuntimeException if an input or output exception occured
*/
public static Config ofProperties(File file, boolean optional) {
if (!file.exists() && optional) {
return new Config(null);
}
return ofProperties(file);
}
/**
* Creates a single config, consisting of specified configs.
*
* @param configs set of configs to unite
* @return single config
*/
public static Config union(Config... configs) {
return doUnion(null, asList(configs));
}
/**
* Creates a single config object, consisting of configs, contained
* in specified collection.
*
* @param configs collection of configs
* @return single config
*/
public static Config union(Collection<Config> configs) {
if (configs.size() == 1)
return configs.iterator().next();
return doUnion(null, configs);
}
private static Config doUnion(Config parent, Collection<Config> configs) {
Config result = new Config(parent);
Map<String, List<Config>> childrenList = new LinkedHashMap<>();
for (Config config : configs) {
if (config.value != null) {
if (result.value != null) {
throw new IllegalStateException("Multiple values for " + config.getKey());
}
result.value = config.value;
}
for (String key : config.children.keySet()) {
Config child = config.children.get(key);
List<Config> list = childrenList.get(key);
if (list == null) {
list = new ArrayList<>();
childrenList.put(key, list);
}
list.add(child);
}
}
for (String key : childrenList.keySet()) {
List<Config> childConfigs = childrenList.get(key);
Config joined = doUnion(result, childConfigs);
result.children.put(key, joined);
}
return result;
}
private static String propertiesFileEncode(String string, boolean escapeKey) {
StringBuilder sb = new StringBuilder(string.length() * 2);
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
if ((c > 61) && (c < 127)) {
if (c == '\\') {
sb.append('\\');
sb.append('\\');
continue;
}
sb.append(c);
continue;
}
switch (c) {
case ' ':
if (i == 0 || escapeKey)
sb.append('\\');
sb.append(' ');
break;
case '\t':
sb.append('\\');
sb.append('t');
break;
case '\n':
sb.append('\\');
sb.append('n');
break;
case '\r':
sb.append('\\');
sb.append('r');
break;
case '\f':
sb.append('\\');
sb.append('f');
break;
case '=':
case ':':
case '#':
case '!':
sb.append('\\');
sb.append(c);
break;
default:
sb.append(c);
}
}
return sb.toString();
}
synchronized private boolean saveToPropertiesFile(String prefix, Writer writer) throws IOException {
boolean rootLevel = prefix.isEmpty();
StringBuilder sb = new StringBuilder();
if (value != null || defaultValue != null) {
if (!accessed) {
assert defaultValue == null;
sb.append("# Unused: ");
sb.append(propertiesFileEncode(prefix, true));
sb.append(" = ");
sb.append(propertiesFileEncode(value, false));
} else {
if (value != null && !value.equals(defaultValue)) {
sb.append(propertiesFileEncode(prefix, true));
sb.append(" = ");
sb.append(propertiesFileEncode(value, false));
} else { // defaultValue != null
sb.append("#");
sb.append(propertiesFileEncode(prefix, true));
sb.append(" = ");
if (defaultValue != null) {
sb.append(propertiesFileEncode(defaultValue, false));
}
}
}
}
boolean saved = false;
String line = sb.toString();
if (!line.isEmpty()) {
writer.write(line + '\n');
saved = true;
}
for (String key : children.keySet()) {
Config child = children.get(key);
boolean savedByChild = child.saveToPropertiesFile(rootLevel ? key : (prefix + "." + key), writer);
if (rootLevel && savedByChild) {
writer.write('\n');
}
saved |= savedByChild;
}
return saved;
}
/**
* Saves config to the specified file using {@code UTF-8} charset
*
* @param file the file to be opened for saving
* @throws IOException if an I/O error occurs
*/
public void saveToPropertiesFile(File file) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
saveToPropertiesFile("", writer);
}
}
synchronized public Config ensureChild(String path) {
if (path.isEmpty())
return this;
Config result = this;
for (String key : StringUtils.splitToList(SEPARATOR, path)) {
checkState(!key.isEmpty(), "Child path must not be empty: %s", path);
Config child = result.children.get(key);
if (child == null) {
child = new Config(result);
result.children.put(key, child);
}
result = child;
}
return result;
}
/**
* Returns a config tree node which corresponds to the specified path.
*
* @param path path in the config tree
* @return config tree node
*/
synchronized public Config getChild(String path) {
final Config config = ensureChild(path);
config.accessed = true;
return config;
}
synchronized public boolean isAccessed() {
return accessed;
}
synchronized public boolean isModified() {
return modified;
}
/**
* Returns read-only map of current config's children.
*
* @return unmodifiable map of config's child nodes
*/
synchronized public Map<String, Config> getChildren() {
return Collections.unmodifiableMap(children);
}
/**
* Returns a tree path to this config. Returned path's chunks are separated
* by "."
*
* @return key of config property
*/
synchronized public String getKey() {
if (parent == null)
return "";
for (String childKey : parent.children.keySet()) {
Config child = parent.children.get(childKey);
if (child == this) {
String childRootKey = parent.getKey();
return childRootKey.isEmpty() ? childKey : childRootKey + "." + childKey;
}
}
throw new IllegalStateException();
}
/**
* Returns a value of the property and marks it as accessed.
*
* @return value of the property
*/
synchronized public String get() {
accessed = true;
return value;
}
/**
* Assigns the default value of the property. Returns either value
* or default value of the property.
*
* @param defaultValue default value of a property
* @return value if it is not null; default value otherwise
* @throws IllegalArgumentException in attempt to override default value
*/
synchronized public String get(String defaultValue) {
this.defaultValue = defaultValue;
String result = get();
if (result == null) {
logger.info("Using default value for '{}'", getKey());
result = defaultValue;
}
return result;
}
/**
* Sets value of a property.
*
* @param value value of a property
*/
synchronized public void set(String value) {
modified = true;
this.value = value;
}
/**
* Sets property's value on given path and marks it as modified.
* Creates non-existent nodes on the specified tree path.
*
* @param path path to property's value in config tree
* @param value value of config property
*/
synchronized public void set(String path, String value) {
ensureChild(path).set(value);
}
/**
* Returns a value of tree node on specified path and converts it using
* converter. A {@link ConfigConverters} class has a lot of handy
* implementations of converters.
*
* @param converter config converter
* @param path path to property's value in config tree
* @param <T> a type of property value
* @return a value of property on the tree path
*
* @see ConfigConverter
* @see ConfigConverters
*/
synchronized public <T> T get(ConfigConverter<T> converter, String path) {
checkNotNull(converter);
return converter.get(ensureChild(path));
}
/**
* Acts like {@link #get(String)} and converts result using specified
* converter.
* <p>
* {@link ConfigConverters} class has a lot of handy
* implementations of converters.
*
* @param converter config converter
* @param path path to property's value in config tree
* @param <T> a type of property value
* @return a value of property on the tree path
*
* @see ConfigConverter
* @see ConfigConverters
*/
synchronized public <T> T get(ConfigConverter<T> converter, String path, T defaultValue) {
checkNotNull(converter);
return converter.get(ensureChild(path), defaultValue);
}
/**
* Checks if there is a value assigned to a node. Creates non-existent nodes
* of the specified tree path.
*
* @param path path to required {@code Config} node
* @return true if value is not null; false otherwise
*/
synchronized public boolean hasValue(String path) {
Config child = ensureChild(path);
child.accessed = true;
return child.value != null;
}
/**
* Checks for existence of subsequent parts of property's key.
* <p>
* For the given {@link Config config} object:
* <pre><code>
* composite.property.example.integer=0;
* composite.property.example.character=c;
* composite.property.value=0;
* </code></pre>
* The results of invocation will be the following:
* <pre><code>
* config.hasSection("composite.property"); //true
* config.hasSection("composite.property.example"); //true
* config.hasSection("composite.property.value"); //false
* </code></pre>
* The result of the last method invocation is false because reached leaf
* node has assigned value.
*
* @param path path to required {@code Config} node
* @return true if node on the specified path has children with null
* value; false otherwise
*/
synchronized public boolean hasSection(String path) {
Config child = ensureChild(path);
child.accessed = true;
return child.children.size() > 0 && child.value == null;
}
@Override
public String toString() {
return getKey();
}
@Override
public synchronized boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Config config = (Config) o;
return Objects.equals(this.children, config.children) &&
Objects.equals(this.value, config.value);
}
}