/**
* PermissionsEx
* Copyright (C) zml and PermissionsEx contributors
*
* 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 ninja.leaping.permissionsex.util;
import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* An immutable tree structure for determining node data. Any changes will create new copies of the necessary tree objects.
* Keys are case-insensitive.
* Segments of nodes are split by the '.' character
*/
public class NodeTree {
public static final int PERMISSION_UNDEFINED = 0;
private static final Pattern SPLIT_REGEX = Pattern.compile("\\.");
private final Node rootNode;
private NodeTree(int value) {
this.rootNode = new Node(new HashMap<>());
this.rootNode.value = value;
}
private NodeTree(Node rootNode) {
this.rootNode = rootNode;
}
/**
* Create a new node tree with the given values, and a default value of UNDEFINED.
*
* @param values The values to set
* @return The new node tree
*/
public static NodeTree of(Map<String, Integer> values) {
return of(values, PERMISSION_UNDEFINED);
}
/**
* Create a new node tree with the given values, and the specified root fallback value.
*
* @param values The values to be contained in this node tree
* @param defaultValue The fallback value for any completely undefined nodes
* @return The newly created node tree
*/
public static NodeTree of(Map<String, Integer> values, int defaultValue) {
NodeTree newTree = new NodeTree(defaultValue);
for (Map.Entry<String, Integer> value : values.entrySet()) {
String[] parts = SPLIT_REGEX.split(value.getKey().toLowerCase());
Node currentNode = newTree.rootNode;
for (String part : parts) {
if (currentNode.children.containsKey(part)) {
currentNode = currentNode.children.get(part);
} else {
Node newNode = new Node(new HashMap<>());
currentNode.children.put(part, newNode);
currentNode = newNode;
}
}
currentNode.value = value.getValue();
}
return newTree;
}
/**
* Returns the value assigned to a specific node, or the nearest parent value in the tree if the node itself is undefined.
*
* @param node The path to get the node value at
* @return The tristate value for the given node
*/
public int get(String node) {
String[] parts = SPLIT_REGEX.split(node.toLowerCase());
Node currentNode = this.rootNode;
int lastUndefinedVal = this.rootNode.value;
for (String str : parts) {
if (!currentNode.children.containsKey(str)) {
break;
}
currentNode = currentNode.children.get(str);
if (Math.abs(currentNode.value) >= Math.abs(lastUndefinedVal)) {
lastUndefinedVal = currentNode.value;
}
}
return lastUndefinedVal;
}
/**
* Convert this node tree into a map of the defined nodes in this tree.
*
* @return An immutable map representation of the nodes defined in this tree
*/
public Map<String, Integer> asMap() {
ImmutableMap.Builder<String, Integer> ret = ImmutableMap.builder();
for (Map.Entry<String, Node> ent : this.rootNode.children.entrySet()) {
populateMap(ret, ent.getKey(), ent.getValue());
}
return ret.build();
}
private void populateMap(ImmutableMap.Builder<String, Integer> values, String prefix, Node currentNode) {
if (currentNode.value != 0) {
values.put(prefix, currentNode.value);
}
for (Map.Entry<String, Node> ent : currentNode.children.entrySet()) {
populateMap(values, prefix + '.' + ent.getKey(), ent.getValue());
}
}
/**
* Return a new NodeTree instance with a single changed value.
*
* @param node The node path to change the value of
* @param value The value to change, or UNDEFINED to remove
* @return The new, modified node tree
*/
public NodeTree withValue(String node, int value) {
String[] parts = SPLIT_REGEX.split(node.toLowerCase());
Node newRoot = new Node(new HashMap<>(this.rootNode.children));
Node newPtr = newRoot;
Node currentPtr = this.rootNode;
newPtr.value = currentPtr == null ? 0 : currentPtr.value;
for (String part : parts) {
Node oldChild = currentPtr == null ? null : currentPtr.children.get(part);
Node newChild = new Node(oldChild != null ? new HashMap<>(oldChild.children) : new HashMap<String, Node>());
newPtr.children.put(part, newChild);
currentPtr = oldChild;
newPtr = newChild;
}
newPtr.value = value;
return new NodeTree(newRoot);
}
/**
* Return a modified new node tree with the specified values set.
*
* @param values The values to set
* @return The new node tree
*/
public NodeTree withAll(Map<String, Integer> values) {
NodeTree ret = this;
for (Map.Entry<String, Integer> ent : values.entrySet()) {
ret = ret.withValue(ent.getKey(), ent.getValue());
}
return ret;
}
private static class Node {
private final Map<String, Node> children;
private int value = 0;
private Node(Map<String, Node> children) {
this.children = children;
}
}
}