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}