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}