ConfigTree.java
package net.finmath.util.config;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import net.finmath.util.config.nodes.ConfigNode;
import net.finmath.util.config.nodes.Node;
import net.finmath.util.config.nodes.SpecialNodes;
import net.finmath.util.config.nodes.ValueNode;
/**
* Config Tree: A tree based representation of configurations that can be selected with a key-value map describing the selector.
*
* <h2>Selector ↦ Configuration</h2>
*
* <dl>
* <dt>configuration</dt>
* <dd>
* A configuration is a value (Object) assigned to a selector (Map<String, Object>).
* </dd>
*
* <dt>selector</dt>
* <dd>
* A selector is a key-value map where certain properties (String) have certain values (Object).
* Properties of the selector are checked in a fixed order,
* such that a tree is formed, where each property corresponds to a level (depth) in the tree.
* </dd>
*
* <dt>default values</dt>
* <dd>
* Each level has a special branch for DEFAULT VALUES, if the given selector value does not match any value of other nodes.
* </dd>
* </dl>
*
* <h2>Selector Key Order</h2>
*
* The selector is matched against the configurtion list by checking the keys in a certain order. This oder does not matter if there is a unique value
* for the given selector, but it may matter if default values have to be assign. See the following example.
*
* <h2>Example</h2>
*
* The configuration is determied by the value of two properties ("prop1" and "prop2") (the keys). The correspondig configuration value is
*
* <p></p>
* <table border="1">
* <tr><th style="text-align: center;">prop1</th><th style="text-align: center;">prop2</th><th style="text-align: center;">value</th></tr>
* <tr><td style="text-align: center;">A</td><td style="text-align: center;">1</td><td style="text-align: center;">42.0</td></tr>
* <tr><td style="text-align: center;">A</td><td style="text-align: center;">2</td><td style="text-align: center;">3141</td></tr>
* <tr><td style="text-align: center;">B</td><td style="text-align: center;">1</td><td style="text-align: center;">1</td></tr>
* <tr><td style="text-align: center;">B</td><td style="text-align: center;">2</td><td style="text-align: center;">2</td></tr>
* <tr><td style="text-align: center;">A</td><td style="text-align: center;">DEFAULT_VALUE</td><td style="text-align: center;">12</td></tr>
* <tr><td style="text-align: center;">DEFAULT_VALUE</td><td style="text-align: center;">2</td><td style="text-align: center;">13</td></tr>
* <tr><td style="text-align: center;">DEFAULT_VALUE</td><td style="text-align: center;">DEFAULT_VALUE</td><td style="text-align: center;">66</td></tr>
* <caption>The list of maps that defines all configurations.</caption>
* </table>
*
* <p></p>
*
* Initialising the class with this configuration we have:
* <ul>
* <li>
* If the class is initialized with keyOder = { "prop1" , "prop2" }
* <ul>
* <li>The <i>selector</i> { "prop1" = "A", "prop2" = 2 } results in the value 3141.</li>
* <li>The <i>selector</i> { "prop1" = "C", "prop2" = 2 } results in the value 13 (the default branch for prop1 is selected first, under that branch value 2 for "prop2" exists).</li>
* <li>The <i>selector</i> { "prop1" = "C", "prop2" = 1 } results in the value 66 (the default branch for prop1 is selected first, under that branch no value 1 for "prop2" exists).</li>
* </ul>
* </li>
* <li>
* If the class is initialized with keyOder = { "prop2" , "prop1" }
* <ul>
* <li>The <i>selector</i> { "prop1" = "A", "prop2" = 2 } results in the value 3141.</li>
* <li>The <i>selector</i> { "prop1" = "C", "prop2" = 2 } results in the value 13 (the branch "2" for prop2 is selected first, under that branch no value C for "prop1" exists).</li>
* <li>The <i>selector</i> { "prop1" = "C", "prop2" = 1 } results in an exception that no configuration is found (the branch 1 for prop2 is selected first, under that branch no value C for "prop1" exists).</li>
* </ul>
* </li>
* </ul>
*
* @author Christian Fries
*/
public class ConfigTree {
private final Node root;
/**
* Construct the tree.
*
* @param keyOrder Order in which the string keys of a selector define the levels of the tree.
* @param configs A list, where each element is map of keys to object. The key "value" is interpreted as the configuration value. All other keys are interpreted as configuration properties.
*/
public ConfigTree(List<String> keyOrder, List<Map<String, Object>> configs) {
this.root = group(keyOrder, configs);
}
/**
* Get the configuration for a given specification of the properties (selector).
*
* The configutation tree is traversed by selecting each route though the value of a specific key in the selector,
* until the lead node is reached.
* If keys are missing in the selector of if values do not match a predefined route, a default route is used.
*
* @param selector Maps the name (String) of a property to its value (Object).
* @return The configuration value for the given selector.
*/
public Object getConfig(Map<String, Object> selector) {
Node node = this.root;
// Traverse the tree where each route is selected though the value of a specific key in the selector.
while(node instanceof ConfigNode) {
final ConfigNode configNode = (ConfigNode)node;
if(selector.containsKey(configNode.getKey()) && configNode.getValueToConfig().keySet().contains(selector.get(configNode.getKey()))) {
node = configNode.getValueToConfig().get(selector.get(configNode.getKey()));
}
else {
node = configNode.getValueToConfig().get(SpecialNodes.DEFAULT_VALUE);
if(Objects.isNull(node)) {
throw new IllegalArgumentException("Neither a value nor a default branch exists in the config tree for " + configNode.getKey() + " at the current location. " + selector);
}
}
}
// Having reached the value node, return it.
if(node instanceof ValueNode) {
final ValueNode valueNode = (ValueNode)node;
return valueNode.getValue();
}
else {
throw new IllegalArgumentException("Unable to resolve configuration from the given properties. " + selector);
}
}
/**
* Helper for the constructor. Recursive contruction of the tree.
*
* @param keyOrder Given key order.
* @param configs List of configs.
* @return Node of the (sub-)tree for the given config key.
*/
private Node group(List<String> keyOrder, List<Map<String, Object>> configs) {
if(keyOrder.size() > 0) {
// Group all elements by the first key in keyOrder....
final String key = keyOrder.get(0);
final Map<Object, List<Map<String, Object>>> grouped = configs.stream().collect(Collectors.groupingBy(map -> map.get(key)));
// ...call group (recursive) for all values below this key taking the remainder of keyOrder...
final List<String> keyOrderRemain = keyOrder.subList(1, keyOrder.size());
final Map<Object, Node> valueToConfig = grouped.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey, entry -> group(keyOrderRemain, entry.getValue())));
// ...create a ConfigNode for this key.
return new ConfigNode(key, valueToConfig);
}
else {
// If no keys are left in key order, create the leaf node
if(configs.size() == 1) {
final Map<String, Object> config = configs.get(0);
final Object value = config.get("value");
return new ValueNode( value);
}
else {
throw new IllegalArgumentException("Multiple configs for the same selector values. " + Arrays.deepToString(configs.toArray()));
}
}
}
}