package org.netbeans.gradle.project.api.config; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.jtrim.collections.CollectionsEx; import org.jtrim.utils.ExceptionHelper; /** * Defines a tree based configuration store. The edges in the tree are identified * by a string and also each node may store a string value (there is no * restriction on what characters the strings may contain). In most practical * use-cases, you can think of edges as {@literal XML} tags and values as their * {@literal CDATA} content. All strings are treated as case sensitive. * <P> * It is possible to have multiple edges with the same name with the given * parent, in this case all the subtrees can be requested as a list * (order is maintained). * <P> * Instances of {@code ConfigTree} can be created through its {@link Builder}. * <P> * Instances of this class are immutable and as such can be shared without any * further synchronization. */ public final class ConfigTree { private static final int DEFAULT_LIST_SIZE = 10; /** * Defines an empty tree with a single node with no value. */ public static final ConfigTree EMPTY = new Builder().create(); /** * Defines a builder to create {@code ConfigTree} instances. */ public static final class Builder { private String value; private Map<String, List<TreeOrBuilder>> childTrees; private ConfigTree cachedBuilt; /** * Creates a new {@code Builder} with the given configuration tree as * its initial value. That is, if you call {@link #create() create} right * after creating the new {@code Builder}, you will get effectively the * same {@code ConfigTree} what was specified in the argument. * * @param initialValue the base configuration tree of this {@code Builder}. * This argument cannot be {@code null}. */ public Builder(@Nonnull ConfigTree initialValue) { this(); value = initialValue.value; if (!initialValue.childTrees.isEmpty()) { Map<String, List<TreeOrBuilder>> children = CollectionsEx.newHashMap(initialValue.childTrees.size()); childTrees = children; for (Map.Entry<String, List<ConfigTree>> entry: initialValue.childTrees.entrySet()) { List<ConfigTree> entryValue = entry.getValue(); List<TreeOrBuilder> valueList = createList(entryValue.size()); children.put(entry.getKey(), valueList); for (ConfigTree child: entryValue) { valueList.add(new TreeOrBuilder(child)); } } } } /** * Creates a new {@code Builder} with only a single root node without * any value. */ public Builder() { this.value = null; this.childTrees = null; this.cachedBuilt = null; } private static <E> List<E> createList() { return createList(DEFAULT_LIST_SIZE); } private static <E> List<E> createList(int expectedSize) { return new ArrayList<>(Math.max(expectedSize, DEFAULT_LIST_SIZE)); } private static <K, E> List<E> getList(Map<K, List<E>> map, K key) { List<E> result = map.get(key); if (result == null) { result = createList(); map.put(key, result); } return result; } private static <K, E> List<E> getEmptyList(Map<K, List<E>> map, K key) { List<E> result = createList(); map.put(key, result); return result; } private List<TreeOrBuilder> getChildTreeList(String key) { return getList(getChildTrees(), key); } private Map<String, List<TreeOrBuilder>> getChildTrees() { Map<String, List<TreeOrBuilder>> result = childTrees; if (result == null) { result = new HashMap<>(); childTrees = result; } return result; } /** * Sets the string value associated with the root node. Note that, * usually it is not recommended (though it is possible) for non-leaf * nodes to have a value. * * @param value the value of the root node. This argument can be * {@code null}, meaning that this node has no value. */ public void setValue(@Nullable String value) { cachedBuilt = null; this.value = value; } /** * Connects the root node of this {@code Builder} with the specied * configuration tree with the given edge. * <P> * <B>Note</B>: This method will effectievly detach all subtrees along * the given edge. So builders previously created with the given edge * will no longer affect this builder. * * @param key the string identifying the edge connecting the given * subtree with the root. This argument cannot be {@code null}. * @param tree the subtree to be connected with the root node. This * argument cannot be {@code null}. */ public void setChildTree(@Nonnull String key, @Nonnull ConfigTree tree) { ExceptionHelper.checkNotNullArgument(key, "key"); ExceptionHelper.checkNotNullArgument(tree, "tree"); cachedBuilt = null; List<TreeOrBuilder> valueList = getEmptyList(getChildTrees(), key); valueList.add(new TreeOrBuilder(tree)); } /** * Returns a {@code Builder} which can be used to build subtree of this * {@code Builder}. Non-existent subtrees in the path will be created, * however note that already created subtree builders will be reused * (will not add new subtrees). * <P> * <B>Note</B>: The returned {@code Builder} will keep affecting * its parent {@code Builder} instances as long as * {@link #detachChildTreeBuilders() detachChildTreeBuilders} is not * called. Once {@code detachChildTreeBuilders} gets called on a tree, * none of its previously created subtree builders can affect that parent. * <P> * If you need multiple subtrees connected with an edge with the same * name, you have to use the {@link #addChildBuilder(String) addChildBuilder} * method instead. * <P> * This method is effectively the same as repeatedly calling * {@link #getChildBuilder(String) getChildBuilder} on each returned * {@code Builder} with the elements of the given array. * * @param keyNames the names of the edges identifying the subtree to * be edited. This argument cannot be {@code null} and neither of its * elements can be {@code null}. * @return the {@code Builder} which can be used to edit the given * subtree. This method never returns {@code null}. */ @Nonnull public Builder getDeepChildBuilder(@Nonnull String... keyNames) { Builder result = this; for (String keyName: keyNames) { result = result.getChildBuilder(keyName); } return result; } /** * Returns a {@code Builder} which can be used to build subtree of this * {@code Builder}. Non-existent subtrees in the path will be created, * however note that already created subtree builders will be reused * (will not add new subtrees). * <P> * <B>Note</B>: The returned {@code Builder} will keep affecting * its parent {@code Builder} instances as long as * {@link #detachChildTreeBuilders() detachChildTreeBuilders} is not * called. Once {@code detachChildTreeBuilders} gets called on a tree, * none of its previously created subtree builders can affect that parent. * <P> * If you need multiple subtrees connected with an edge with the same * name, you have to use the {@link #addChildBuilder(String) addChildBuilder} * method instead. * <P> * This method is effectively the same as repeatedly calling * {@link #getChildBuilder(String) getChildBuilder} on each returned * {@code Builder} with the keys of the given {@code ConfigPath}. * * @param path the path identifying the subtree to be edited. This * argument cannot be {@code null}. * @return the {@code Builder} which can be used to edit the given * subtree. This method never returns {@code null}. */ @Nonnull public Builder getDeepChildBuilder(@Nonnull ConfigPath path) { Builder result = this; for (String key: path.getKeys()) { result = result.getChildBuilder(key); } return result; } /** * Returns a {@code Builder} which can be used to build subtree of this * {@code Builder}. If a subtree identified by the given edge does not * exist it will be created. If it already exist, the first builder * with the given name will be returned. * <P> * <B>Note</B>: The returned {@code Builder} will keep affecting * its parent {@code Builder} instances as long as * {@link #detachChildTreeBuilders() detachChildTreeBuilders} is not * called. Once {@code detachChildTreeBuilders} gets called on a tree, * none of its previously created subtree builders can affect that parent. * <P> * If you need multiple subtrees connected with an edge with the same * name, you have to use the {@link #addChildBuilder(String) addChildBuilder} * method instead. * * @param key the key identifying the subtree to be edited. This * argument cannnot be {@code null}. * @return the {@code Builder} which can be used to edit the given * subtree. This method never returns {@code null}. */ @Nonnull public Builder getChildBuilder(@Nonnull String key) { ExceptionHelper.checkNotNullArgument(key, "key"); List<TreeOrBuilder> valueList = getChildTreeList(key); if (valueList.isEmpty()) { cachedBuilt = null; Builder result = new Builder(); valueList.add(new TreeOrBuilder(result)); return result; } else { return valueList.get(0).getBuilder(); } } /** * Adds a subtree builder connected to the root node with the given name. * If there is no subtree connected with this name, a new one is created. * If there is already a subtree connected with this name, a new one * is added with the same name. * <P> * <B>Note</B>: {@code getChildBuilder} and similar methods will only * use the first child added and will ignore subsequently added subtrees * with the same name. * * @param key the key identifying the subtree to be edited. This * argument cannnot be {@code null}. * @return the {@code Builder} which can be used to edit the given * subtree. This method never returns {@code null}. */ @Nonnull public Builder addChildBuilder(@Nonnull String key) { ExceptionHelper.checkNotNullArgument(key, "key"); cachedBuilt = null; Builder result = new Builder(); getChildTreeList(key).add(new TreeOrBuilder(result)); return result; } /** * Remove all subtrees connected to the root node with an edge with the * given name. Further editing child subtrees connected with this name * will no longer affect this builder. * * @param key the string identifying the edge connecting the subtrees * to be removed. This argument cannot be {@code null}. */ public void removeChild(@Nonnull String key) { ExceptionHelper.checkNotNullArgument(key, "key"); if (childTrees == null) { return; } if (childTrees.remove(key) != null) { cachedBuilt = null; } } /** * Makes so the previously created subtree builders no longer affect * this {@code Builder}. Note that this method does not remove child * trees, simply makes so that subsequent edits to <I>previously</I> * created builders can no longer affect this builder. */ public void detachChildTreeBuilders() { if (childTrees == null || childTrees.isEmpty()) { return; } Map<String, List<TreeOrBuilder>> children = childTrees; for (List<TreeOrBuilder> valueList: children.values()) { for (TreeOrBuilder child: valueList) { child.makeTree(); } } cachedBuilt = create(); } /** * Creates a new immutable snapshot of the configuration tree built * by this builder. * * @return a new immutable snapshot of the configuration tree built * by this builder. This method never returns {@code null}. */ @Nonnull public ConfigTree create() { ConfigTree result = cachedBuilt; if (result == null) { result = new ConfigTree(this); } return result; } private Map<String, List<ConfigTree>> getChildTreesSnapshot() { Map<String, List<TreeOrBuilder>> children = childTrees; int childTreeCount = children != null ? children.size() : 0; if (childTreeCount == 0) { return Collections.emptyMap(); } Map<String, List<ConfigTree>> result = CollectionsEx.newHashMap(childTreeCount); if (children != null) { for (Map.Entry<String, List<TreeOrBuilder>> entry: children.entrySet()) { String entryKey = entry.getKey(); List<TreeOrBuilder> entryValue = entry.getValue(); int entryValueSize = entryValue.size(); if (entryValueSize == 1) { // A common special case ConfigTree child = entryValue.get(0).createTreeIfHasValue(); if (child != null) { result.put(entryKey, Collections.singletonList(child)); } } else if (entryValueSize > 0) { List<ConfigTree> resultChild = new ArrayList<>(entryValueSize); for (TreeOrBuilder child: entryValue) { ConfigTree builtTree = child.createTreeIfHasValue(); if (builtTree != null) { resultChild.add(builtTree); } } result.put(entryKey, Collections.unmodifiableList(resultChild)); } } } return Collections.unmodifiableMap(result); } } private final String value; private final Map<String, List<ConfigTree>> childTrees; private int hash; // Only works if we rely on the default value: 0 private ConfigTree(Builder builder) { this(builder.value, builder.getChildTreesSnapshot()); } private ConfigTree(String value, Map<String, List<ConfigTree>> childTrees) { this.value = value; this.childTrees = childTrees; } /** * Creates a configuration tree with only a single node with the given * value. If the specified value is {@code null}, the return value is the * {@link #EMPTY empty tree}. * * @param value the value of the root (and only) node of the returned * configuration tree. * @return a configuration tree with only a single node with the given * value. This method never returns {@code null}. */ @Nonnull public static ConfigTree singleValue(@Nullable String value) { return value != null ? new ConfigTree(value, Collections.<String, List<ConfigTree>>emptyMap()) : EMPTY; } /** * Checks whether this node has any child or value at all. That is, this * method returns {@code true}, if and only, if this configuration tree * is equivalent of the {@link #EMPTY empty tree}. * * @return {@code true} if this node is equivalent of the {@link #EMPTY empty tree}, * {@code false} otherwise */ public boolean hasValues() { // We don't store childtrees with no values at all. return value != null || !childTrees.isEmpty(); } /** * Returns the value of this node or the speficied default value if this * node has no associate value. * * @param defaultValue the value to return if the root node has no associated * value. This argument can be {@code null}. * @return the value of this node or the speficied default value if this * node has no associate value. Notice that this method may only return * {@code null}, if the specified default value is {@code null}. */ public String getValue(@Nullable String defaultValue) { return value != null ? value : defaultValue; } /** * Returns all the child trees as a map where keys are the name of the * edges connecting the subtrees (values). * <P> * This method returns a normalized value in such way that none of the * returned {@code ConfigTree} instances are equivalent to the * {@link #EMPTY empty tree}. * * @return all the child trees as a map where keys are the name of the * edges connecting the subtrees (values). This method never returns * {@code null}. */ @Nonnull public Map<String, List<ConfigTree>> getChildTrees() { return childTrees; } /** * Returns the subtree along the given path. This method never fails, if * this configuration tree does not contain any subtree along this path, * this method returns an empty tree. * <P> * If the given path contains no keys, this {@code ConfigTree} is returned. * * @param path the {@code ConfigPath} identifying the subtree to be * returned. This argument cannot be {@code null}. * @return the subtree along the given path. This method never returns * {@code null}. */ @Nonnull public ConfigTree getDeepChildTree(@Nonnull ConfigPath path) { ExceptionHelper.checkNotNullArgument(path, "path"); return getDeepChildTree(path.getKeys().iterator()); } /** * Returns the subtree along the given path. This method never fails, if * this configuration tree does not contain any subtree along this path, * this method returns an empty tree. * <P> * If the given path contains no keys, this {@code ConfigTree} is returned. * * @param keys the name of edges identifying the subtree to be * returned. This argument cannot be {@code null} and the array cannot * contain {@code null} elements. * @return the subtree along the given path. This method never returns * {@code null}. */ @Nonnull public ConfigTree getDeepChildTree(@Nonnull String... keys) { ExceptionHelper.checkNotNullElements(keys, "keys"); ConfigTree result = this; for (String key: keys) { // Minor optimization if (result == EMPTY) { return EMPTY; } result = result.getChildTree(key); } return result; } private ConfigTree getDeepChildTree(Iterator<String> keys) { ConfigTree result = this; while (keys.hasNext()) { String key = keys.next(); // Minor optimization if (result == EMPTY) { return EMPTY; } result = result.getChildTree(key); } return result; } /** * Returns all the child trees connected with an edge with the given name. * If there are no such subtrees, an empty list is returned. * <P> * This method returns a normalized value in such way that none of the * returned {@code ConfigTree} instances are equivalent to the * {@link #EMPTY empty tree}. * * @param key the name of the edge connecting the subtrees to be returned. * This argument cannot be {@code null}. * @return all the child trees connected with an edge with the given name. * This method never returns {@code null} and none of the elements of * list are {@code null}. */ @Nonnull public List<ConfigTree> getChildTrees(@Nonnull String key) { ExceptionHelper.checkNotNullArgument(key, "key"); List<ConfigTree> result = childTrees.get(key); return result != null ? result : Collections.<ConfigTree>emptyList(); } /** * Returns the first child tree connected with an edge with the given name. * If there are no such subtree, an {@link #EMPTY empty tree} is returned. * * @param key the name of the edge connecting the subtree to be returned. * This argument cannot be {@code null}. * @return the first child tree connected with an edge with the given name. * This method never returns {@code null}. */ @Nonnull public ConfigTree getChildTree(@Nonnull String key) { ExceptionHelper.checkNotNullArgument(key, "key"); List<ConfigTree> result = childTrees.get(key); return result != null ? result.get(0) : EMPTY; } private static final class TreeOrBuilder { private ConfigTree tree; private Builder builder; public TreeOrBuilder(ConfigTree tree) { assert tree != null; this.tree = tree; this.builder = null; } public TreeOrBuilder(Builder builder) { assert builder != null; this.tree = null; this.builder = builder; } public ConfigTree createTreeIfHasValue() { ConfigTree result = createTree(); return result.hasValues() ? result : null; } public ConfigTree createTree() { ConfigTree result = tree; if (result == null) { result = builder.create(); } return result; } public void makeTree() { if (builder != null) { tree = builder.create(); builder = null; } } public Builder getBuilder() { Builder result = builder; if (result == null) { result = new Builder(tree); builder = result; tree = null; } return result; } } @Override public int hashCode() { int result = hash; if (result == 0) { result = 3; result = 83 * result + Objects.hashCode(value); result = 83 * result + Objects.hashCode(childTrees); hash = result; } return hash; } @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj == this) return true; if (getClass() != obj.getClass()) return false; final ConfigTree other = (ConfigTree)obj; return Objects.equals(this.value, other.value) && Objects.equals(this.childTrees, other.childTrees); } private static void appendIndent(StringBuilder result, int indent) { final String indentStr = " "; for (int i = 0; i < indent; i++) { result.append(indentStr); } } private void toString(String prefix, int indent, StringBuilder result) { appendIndent(result, indent); result.append(prefix); result.append("ConfigTree"); if (value != null) { result.append(" ("); result.append(value); result.append(")"); } for (Map.Entry<String, List<ConfigTree>> entry: childTrees.entrySet()) { String key = entry.getKey(); for (ConfigTree tree: entry.getValue()) { result.append('\n'); tree.toString(key + " -> ", indent + 1, result); } } } @Override public String toString() { StringBuilder result = new StringBuilder(); toString("", 0, result); return result.toString(); } }