001/* 002 * PermissionsEx 003 * Copyright (C) zml and PermissionsEx contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ca.stellardrift.permissionsex.util; 018 019import org.checkerframework.checker.nullness.qual.Nullable; 020 021import java.util.ArrayDeque; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Locale; 025import java.util.Map; 026import java.util.function.IntPredicate; 027import java.util.regex.Pattern; 028 029/** 030 * An immutable tree structure for determining node data. 031 * 032 * <p>Any changes will create new copies of the necessary tree objects.</p> 033 * 034 * <p>Keys are case-insensitive.</p> 035 * 036 * <p>Segments of nodes are split by the '.' character</p> 037 * 038 * @since 2.0.0 039 */ 040public final class NodeTree { 041 public static final int PERMISSION_UNDEFINED = 0; 042 043 private static final Pattern SPLIT_REGEX = Pattern.compile("\\."); 044 private final Node rootNode; 045 046 private NodeTree(int value) { 047 this.rootNode = new Node(new HashMap<>()); 048 this.rootNode.value = value; 049 } 050 051 private NodeTree(Node rootNode) { 052 this.rootNode = rootNode; 053 } 054 055 /** 056 * Create a new node tree with the given values, and a default value of UNDEFINED. 057 * 058 * @param values The values to set 059 * @return The new node tree 060 * @since 2.0.0 061 */ 062 public static NodeTree of(Map<String, Integer> values) { 063 return of(values, PERMISSION_UNDEFINED); 064 } 065 066 /** 067 * Create a new node tree with the given values, and the specified root fallback value. 068 * 069 * @param values The values to be contained in this node tree 070 * @param defaultValue The fallback value for any completely undefined nodes 071 * @return The newly created node tree 072 * @since 2.0.0 073 */ 074 public static NodeTree of(final Map<String, Integer> values, final int defaultValue) { 075 final NodeTree newTree = new NodeTree(defaultValue); 076 for (Map.Entry<String, Integer> value : values.entrySet()) { 077 final String[] parts = SPLIT_REGEX.split(value.getKey().toLowerCase(Locale.ROOT)); 078 Node currentNode = newTree.rootNode; 079 for (String part : parts) { 080 if (currentNode.children.containsKey(part)) { 081 currentNode = currentNode.children.get(part); 082 } else { 083 Node newNode = new Node(); 084 currentNode.putChild(part, newNode); 085 currentNode = newNode; 086 } 087 } 088 currentNode.value = value.getValue(); 089 } 090 return newTree; 091 } 092 093 /** 094 * Returns the value assigned to a specific node, or the nearest parent value in the tree if the node itself is undefined. 095 * 096 * @param node The path to get the node value at 097 * @return The int value for the given node 098 * @since 2.0.0 099 */ 100 public int get(final String node) { 101 final String[] parts = SPLIT_REGEX.split(node.toLowerCase(Locale.ROOT)); 102 Node currentNode = this.rootNode; 103 int lastUndefinedVal = this.rootNode.value; 104 for (final String part : parts) { 105 if (!currentNode.children.containsKey(part)) { 106 break; 107 } 108 currentNode = currentNode.children.get(part); 109 if (Math.abs(currentNode.value) >= Math.abs(lastUndefinedVal)) { 110 lastUndefinedVal = currentNode.value; 111 } 112 } 113 return lastUndefinedVal; 114 } 115 116 /** 117 * Return whether the node {@code prefix} or any of its children match the predicate {@code test}. 118 * 119 * @param prefix the prefix to test 120 * @param test the test function 121 * @return if any values return true 122 */ 123 public boolean anyInPrefixMatching(final String prefix, final IntPredicate test) { 124 final String[] parts = SPLIT_REGEX.split(prefix.toLowerCase(Locale.ROOT)); 125 Node currentNode = this.rootNode; 126 int lastUndefinedVal = this.rootNode.value; 127 128 // Resolve prefix 129 for (final String part : parts) { 130 if (!currentNode.children.containsKey(part)) { 131 return test.test(lastUndefinedVal); 132 } 133 currentNode = currentNode.children.get(part); 134 if (Math.abs(currentNode.value) >= Math.abs(lastUndefinedVal)) { 135 lastUndefinedVal = currentNode.value; 136 } 137 } 138 139 // If there are no children overridden, test on the prefix 140 if (currentNode.children.isEmpty()) { 141 return test.test(lastUndefinedVal); 142 } 143 144 // Now visit all children, stopping on first match 145 // search breadth-first 146 final ArrayDeque<Node> toVisit = new ArrayDeque<>(currentNode.children.size() * 2); 147 toVisit.addAll(currentNode.children.values()); 148 149 @Nullable Node current; 150 while ((current = toVisit.poll()) != null) { 151 // compute the value based on maximum of prefix's value or leaf value 152 if (Math.abs(current.value) >= Math.abs(lastUndefinedVal) && test.test(current.value)) { 153 return true; 154 } 155 156 toVisit.addAll(current.children.values()); 157 } 158 return false; 159 } 160 161 /** 162 * Convert this node tree into a map of the defined nodes in this tree. 163 * 164 * @return An immutable map representation of the nodes defined in this tree 165 * @since 2.0.0 166 */ 167 public Map<String, Integer> asMap() { 168 final Map<String, Integer> ret = new HashMap<>(); 169 for (final Map.Entry<String, Node> ent : this.rootNode.children.entrySet()) { 170 populateMap(ret, ent.getKey(), ent.getValue()); 171 } 172 return Collections.unmodifiableMap(ret); 173 } 174 175 private void populateMap(final Map<String, Integer> values, final String prefix, final Node currentNode) { 176 if (currentNode.value != 0) { 177 values.put(prefix, currentNode.value); 178 } 179 for (final Map.Entry<String, Node> ent : currentNode.children.entrySet()) { 180 populateMap(values, prefix + '.' + ent.getKey(), ent.getValue()); 181 } 182 } 183 184 /** 185 * Return a new NodeTree instance with a single changed value. 186 * 187 * @param node The node path to change the value of 188 * @param value The value to change, or UNDEFINED to remove 189 * @return The new, modified node tree 190 * @since 2.0.0 191 */ 192 public NodeTree withValue(final String node, final int value) { 193 String[] parts = SPLIT_REGEX.split(node.toLowerCase()); 194 Node newRoot = new Node(new HashMap<>(this.rootNode.children)); 195 Node newPtr = newRoot; 196 @Nullable Node currentPtr = this.rootNode; 197 198 newPtr.value = currentPtr.value; 199 for (String part : parts) { 200 final @Nullable Node oldChild = currentPtr == null ? null : currentPtr.children.get(part); 201 final Node newChild = new Node(oldChild != null ? new HashMap<>(oldChild.children) : new HashMap<>()); 202 newPtr.children.put(part, newChild); 203 currentPtr = oldChild; 204 newPtr = newChild; 205 } 206 newPtr.value = value; 207 return new NodeTree(newRoot); 208 } 209 210 /** 211 * Return a modified new node tree with the specified values set. 212 * 213 * @param values The values to set 214 * @return The new node tree 215 * @since 2.0.0 216 */ 217 public NodeTree withAll(Map<String, Integer> values) { 218 NodeTree ret = this; 219 for (Map.Entry<String, Integer> ent : values.entrySet()) { 220 ret = ret.withValue(ent.getKey(), ent.getValue()); 221 } 222 return ret; 223 } 224 225 @Override 226 public String toString() { 227 return "NodeTree{" + this.rootNode + "}"; 228 } 229 230 static class Node { 231 232 private static final Map<String, Node> EMPTY = Collections.emptyMap(); 233 234 Map<String, Node> children; 235 int value = 0; 236 237 Node(Map<String, Node> children) { 238 this.children = children; 239 } 240 241 Node() { 242 this.children = EMPTY; 243 } 244 245 void putChild(final String path, final Node child) { 246 if (this.children == EMPTY) { 247 this.children = new HashMap<>(); 248 } 249 this.children.put(path, child); 250 } 251 252 @Override 253 public String toString() { 254 return "<value: " + this.value + ", children=" + this.children + ">"; 255 } 256 } 257}