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