me.lucko.luckperms.common.model.PermissionHolder.java Source code

Java tutorial

Introduction

Here is the source code for me.lucko.luckperms.common.model.PermissionHolder.java

Source

/*
 * This file is part of LuckPerms, licensed under the MIT License.
 *
 *  Copyright (c) lucko (Luck) <luck@lucko.me>
 *  Copyright (c) contributors
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 */

package me.lucko.luckperms.common.model;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SortedSetMultimap;

import me.lucko.luckperms.api.Contexts;
import me.lucko.luckperms.api.DataMutateResult;
import me.lucko.luckperms.api.LocalizedNode;
import me.lucko.luckperms.api.Node;
import me.lucko.luckperms.api.Tristate;
import me.lucko.luckperms.api.context.ContextSet;
import me.lucko.luckperms.api.context.ImmutableContextSet;
import me.lucko.luckperms.common.api.delegates.model.ApiPermissionHolder;
import me.lucko.luckperms.common.buffers.BufferedRequest;
import me.lucko.luckperms.common.buffers.Cache;
import me.lucko.luckperms.common.caching.HolderCachedData;
import me.lucko.luckperms.common.caching.handlers.StateListener;
import me.lucko.luckperms.common.caching.type.MetaAccumulator;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.contexts.ContextSetComparator;
import me.lucko.luckperms.common.node.ImmutableLocalizedNode;
import me.lucko.luckperms.common.node.InheritanceInfo;
import me.lucko.luckperms.common.node.MetaType;
import me.lucko.luckperms.common.node.NodeComparator;
import me.lucko.luckperms.common.node.NodeFactory;
import me.lucko.luckperms.common.node.NodeTools;
import me.lucko.luckperms.common.node.NodeWithContextComparator;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.primarygroup.GroupInheritanceComparator;
import me.lucko.luckperms.common.references.GroupReference;
import me.lucko.luckperms.common.references.HolderReference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Represents an object that can hold permissions, (a user or group)
 *
 * <p>Permissions are stored in Multimaps, with the context of the node being the key, and the actual Node object being
 * the value. The keys (context sets) are ordered according to their weight {@link ContextSetComparator}, and the values
 * are ordered according to the priority of the node, according to {@link NodeComparator}.</p>
 *
 * <p>This class also provides a number of methods to perform inheritance lookups. These lookup methods initially use
 * Lists of nodes populated with the inheritance tree. Nodes at the start of this list have priority over nodes at the
 * end. Nodes higher up the tree appear at the end of these lists. In order to remove duplicate elements, the lists are
 * flattened using the methods in {@link NodeTools}. This is significantly faster than trying to prevent duplicates
 * throughout the process of accumulation, and reduces the need for too much caching.</p>
 *
 * <p>Cached state is avoided in these instances to cut down on memory footprint. The nodes are stored indexed to the
 * contexts they apply in, so doing context specific querying should be fast. Caching would be ineffective here, due to
 * the potentially vast amount of contexts being used by nodes, and the potential for very large inheritance trees.</p>
 */
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class PermissionHolder {

    /**
     * The name of this PermissionHolder object.
     *
     * <p>Used to prevent circular inheritance issues.</p>
     *
     * <p>For users, this value is a String representation of their {@link User#getUuid()}. For groups, it's just the
     * {@link Group#getName()}.</p>
     */
    @Getter
    private final String objectName;

    /**
     * Reference to the main plugin instance
     */
    @Getter
    private final LuckPermsPlugin plugin;

    /**
     * The holders persistent nodes.
     *
     * <p>Nodes are mapped by the result of {@link Node#getFullContexts()}, and keys are sorted by the weight of the
     * ContextSet. ContextSets are ordered first by the presence of a server key, then by the presence of a world
     * key, and finally by the overall size of the set. Nodes are ordered according to the priority rules
     * defined in {@link NodeComparator}.</p>
     */
    private final SortedSetMultimap<ImmutableContextSet, Node> nodes = MultimapBuilder
            .treeKeys(ContextSetComparator.reverse()).treeSetValues(NodeComparator.reverse()).build();

    /**
     * Caches an immutable copy of the above nodes multimap
     */
    private final class NodesCache extends Cache<ImmutableSetMultimap<ImmutableContextSet, Node>> {
        @Override
        protected ImmutableSetMultimap<ImmutableContextSet, Node> supply() {
            nodesLock.lock();
            try {
                return ImmutableSetMultimap.copyOf(nodes);
            } finally {
                nodesLock.unlock();
            }
        }
    }

    private final NodesCache nodesCopy = new NodesCache();

    // used to ensure thread safe access to the backing nodes map
    private final ReentrantLock nodesLock = new ReentrantLock();

    /**
     * The holders transient nodes.
     *
     * <p>These are nodes which are never stored or persisted to a file, and only
     * last until the end of the objects lifetime. (for a group, that's when the server stops, and for a user, it's when
     * they log out, or get unloaded.)</p>
     *
     * <p>Nodes are mapped by the result of {@link Node#getFullContexts()}, and keys are sorted by the weight of the
     * ContextSet. ContextSets are ordered first by the presence of a server key, then by the presence of a world
     * key, and finally by the overall size of the set. Nodes are ordered according to the priority rules
     * defined in {@link NodeComparator}.</p>
     */
    private final SortedSetMultimap<ImmutableContextSet, Node> transientNodes = MultimapBuilder
            .treeKeys(ContextSetComparator.reverse()).treeSetValues(NodeComparator.reverse()).build();

    /**
     * Caches an immutable copy of the above transientNodes multimap
     */
    private final class TransientNodesCache extends Cache<ImmutableSetMultimap<ImmutableContextSet, Node>> {
        @Override
        protected ImmutableSetMultimap<ImmutableContextSet, Node> supply() {
            transientNodesLock.lock();
            try {
                return ImmutableSetMultimap.copyOf(transientNodes);
            } finally {
                transientNodesLock.unlock();
            }
        }
    }

    private final TransientNodesCache transientNodesCopy = new TransientNodesCache();

    /**
     * Caches the holders weight lookup
     */
    private final class WeightCache extends Cache<OptionalInt> {
        @Override
        protected OptionalInt supply() {
            return calculateWeight();
        }
    }

    private final WeightCache weightCache = new WeightCache();

    // used to ensure thread safe access to the backing transientNodes map
    private final ReentrantLock transientNodesLock = new ReentrantLock();

    /**
     * Lock used by Storage implementations to prevent concurrent read/writes
     */
    @Getter
    private final Lock ioLock = new ReentrantLock();

    /**
     * Comparator used to ordering groups when calculating inheritance
     */
    private final Comparator<Group> inheritanceComparator = GroupInheritanceComparator.getFor(this);

    /**
     * A set of runnables which are called when this objects state changes.
     */
    @Getter
    private final Set<StateListener> stateListeners = ConcurrentHashMap.newKeySet();

    private void invalidateCache() {
        nodesCopy.invalidate();
        transientNodesCopy.invalidate();
        weightCache.invalidate();

        // Invalidate listeners
        for (StateListener listener : stateListeners) {
            try {
                listener.onStateChange();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // Declare new state to the state manager
        declareState();
    }

    protected void declareState() {
        /* only declare state of groups. the state manager isn't really being used now the caches in this class
           are gone, but it's useful for command output. */
        if (this instanceof Group) {
            plugin.getCachedStateManager().putAll(toReference(), getGroupReferences());
        }
    }

    /**
     * Gets the friendly name of this permission holder (for use in commands, etc)
     *
     * @return the holders "friendly" name
     */
    public abstract String getFriendlyName();

    /**
     * Gets the holders cached data
     *
     * @return the holders cached data
     */
    public abstract HolderCachedData<?> getCachedData();

    public abstract BufferedRequest<Void> getRefreshBuffer();

    /**
     * Forms a HolderReference for this PermissionHolder.
     *
     * @return this holders reference
     */
    public abstract HolderReference<?, ?> toReference();

    /**
     * Gets the API delegate for this instance
     *
     * @return the api delegate
     */
    public abstract ApiPermissionHolder getDelegate();

    /**
     * Returns an immutable copy of this objects nodes
     *
     * @return an immutable copy of the multimap storing this objects nodes
     */
    public ImmutableSetMultimap<ImmutableContextSet, Node> getEnduringNodes() {
        return nodesCopy.get();
    }

    /**
     * Returns an immutable copy of this objects transient nodes
     *
     * @return an immutable copy of the multimap storing this objects transient nodes
     */
    public ImmutableSetMultimap<ImmutableContextSet, Node> getTransientNodes() {
        return transientNodesCopy.get();
    }

    /**
     * Sets this objects nodes to the values in the set
     *
     * @param set the set of nodes to apply to the object
     */
    public void setEnduringNodes(Set<Node> set) {
        nodesLock.lock();
        try {
            nodes.clear();
            for (Node n : set) {
                nodes.put(n.getFullContexts().makeImmutable(), n);
            }
        } finally {
            nodesLock.unlock();
        }
        invalidateCache();
    }

    /**
     * Replaces the multimap backing this object with another
     *
     * @param multimap the replacement multimap
     */
    public void replaceEnduringNodes(Multimap<ImmutableContextSet, Node> multimap) {
        nodesLock.lock();
        try {
            nodes.clear();
            nodes.putAll(multimap);
        } finally {
            nodesLock.unlock();
        }
        invalidateCache();
    }

    public void setTransientNodes(Set<Node> set) {
        transientNodesLock.lock();
        try {
            transientNodes.clear();
            for (Node n : set) {
                transientNodes.put(n.getFullContexts().makeImmutable(), n);
            }
        } finally {
            transientNodesLock.unlock();
        }
        invalidateCache();
    }

    public void replaceTransientNodes(Multimap<ImmutableContextSet, Node> multimap) {
        transientNodesLock.lock();
        try {
            transientNodes.clear();
            transientNodes.putAll(multimap);
        } finally {
            transientNodesLock.unlock();
        }
        invalidateCache();
    }

    /**
     * Merges enduring and transient permissions into one set
     *
     * @return a set containing the holders enduring and transient permissions
     */
    public LinkedHashSet<Node> getOwnNodesSet() {
        LinkedHashSet<Node> ret = new LinkedHashSet<>();

        transientNodesLock.lock();
        try {
            ret.addAll(transientNodes.values());
        } finally {
            transientNodesLock.unlock();
        }

        nodesLock.lock();
        try {
            ret.addAll(nodes.values());
        } finally {
            nodesLock.unlock();
        }

        return ret;
    }

    public List<Node> getOwnNodes() {
        List<Node> ret = new ArrayList<>();

        transientNodesLock.lock();
        try {
            ret.addAll(transientNodes.values());
        } finally {
            transientNodesLock.unlock();
        }

        nodesLock.lock();
        try {
            ret.addAll(nodes.values());
        } finally {
            nodesLock.unlock();
        }

        return ret;
    }

    public SortedSet<LocalizedNode> getOwnNodesSorted() {
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());

        transientNodesLock.lock();
        try {
            for (Node node : transientNodes.values()) {
                ret.add(ImmutableLocalizedNode.of(node, getObjectName()));
            }
        } finally {
            transientNodesLock.unlock();
        }

        nodesLock.lock();
        try {
            for (Node node : nodes.values()) {
                ret.add(ImmutableLocalizedNode.of(node, getObjectName()));
            }
        } finally {
            nodesLock.unlock();
        }

        return ret;
    }

    public List<Node> filterEnduringNodes(ContextSet filter) {
        return filterEnduringNodes(new ArrayList<>(), filter);
    }

    public <C extends Collection<Node>> C filterEnduringNodes(C ret, ContextSet filter) {
        nodesLock.lock();
        try {
            for (Map.Entry<ImmutableContextSet, Collection<Node>> e : nodes.asMap().entrySet()) {
                if (e.getKey().isSatisfiedBy(filter)) {
                    ret.addAll(e.getValue());
                }
            }
        } finally {
            nodesLock.unlock();
        }

        return ret;
    }

    public List<Node> filterTransientNodes(ContextSet filter) {
        return filterTransientNodes(new ArrayList<>(), filter);
    }

    public <C extends Collection<Node>> C filterTransientNodes(C ret, ContextSet filter) {
        transientNodesLock.lock();
        try {
            for (Map.Entry<ImmutableContextSet, Collection<Node>> e : transientNodes.asMap().entrySet()) {
                if (e.getKey().isSatisfiedBy(filter)) {
                    ret.addAll(e.getValue());
                }
            }
        } finally {
            transientNodesLock.unlock();
        }

        return ret;
    }

    public List<Node> filterNodes(ContextSet filter) {
        return filterNodes(new ArrayList<>(), filter);
    }

    public <C extends Collection<Node>> C filterNodes(C ret, ContextSet filter) {
        transientNodesLock.lock();
        try {
            for (Map.Entry<ImmutableContextSet, Collection<Node>> e : transientNodes.asMap().entrySet()) {
                if (e.getKey().isSatisfiedBy(filter)) {
                    ret.addAll(e.getValue());
                }
            }
        } finally {
            transientNodesLock.unlock();
        }

        nodesLock.lock();
        try {
            for (Map.Entry<ImmutableContextSet, Collection<Node>> e : nodes.asMap().entrySet()) {
                if (e.getKey().isSatisfiedBy(filter)) {
                    ret.addAll(e.getValue());
                }
            }
        } finally {
            nodesLock.unlock();
        }

        return ret;
    }

    public boolean removeIf(Predicate<Node> predicate) {
        boolean result;
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            result = nodes.values().removeIf(predicate);
        } finally {
            nodesLock.unlock();
        }

        if (!result) {
            return false;
        }

        invalidateCache();

        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean removeIfTransient(Predicate<Node> predicate) {
        boolean result;

        transientNodesLock.lock();
        try {
            result = transientNodes.values().removeIf(predicate);
        } finally {
            transientNodesLock.unlock();
        }

        if (result) {
            invalidateCache();
        }

        return result;
    }

    /**
     * Resolves inherited nodes and returns them
     *
     * @param excludedGroups a list of groups to exclude
     * @param context       context to decide if groups should be applied
     * @return a set of nodes
     */
    public List<LocalizedNode> resolveInheritances(List<LocalizedNode> accumulator, Set<String> excludedGroups,
            Contexts context) {
        if (accumulator == null) {
            accumulator = new ArrayList<>();
        }

        if (excludedGroups == null) {
            excludedGroups = new HashSet<>();
        }

        if (this instanceof Group) {
            excludedGroups.add(getObjectName().toLowerCase());
        }

        // get and add the objects own nodes
        List<Node> nodes = filterNodes(context.getContexts());
        for (Node node : nodes) {
            ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, getObjectName());
            accumulator.add(localizedNode);
        }

        // resolve and process the objects parents
        List<Group> resolvedGroups = new ArrayList<>();
        Set<String> processedGroups = new HashSet<>();

        for (Node n : nodes) {
            if (!n.isGroupNode())
                continue;
            String groupName = n.getGroupName();

            if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive())
                continue;

            if (!((context.isApplyGlobalGroups() || n.isServerSpecific())
                    && (context.isApplyGlobalWorldGroups() || n.isWorldSpecific()))) {
                continue;
            }

            Group g = plugin.getGroupManager().getIfLoaded(groupName);
            if (g != null) {
                resolvedGroups.add(g);
            }
        }

        // sort the groups according to weight + other factors.
        resolvedGroups.sort(inheritanceComparator);

        for (Group g : resolvedGroups) {
            g.resolveInheritances(accumulator, excludedGroups, context);
        }

        return accumulator;
    }

    public List<LocalizedNode> resolveInheritances(Contexts context) {
        return resolveInheritances(null, null, context);
    }

    public SortedSet<LocalizedNode> resolveInheritancesAlmostEqual(Contexts contexts) {
        List<LocalizedNode> nodes = resolveInheritances(new LinkedList<>(), null, contexts);
        NodeTools.removeAlmostEqual(nodes.iterator());
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());
        ret.addAll(nodes);
        return ret;
    }

    public SortedSet<LocalizedNode> resolveInheritancesMergeTemp(Contexts contexts) {
        List<LocalizedNode> nodes = resolveInheritances(new LinkedList<>(), null, contexts);
        NodeTools.removeIgnoreValueOrTemp(nodes.iterator());
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());
        ret.addAll(nodes);
        return ret;
    }

    /**
     * Resolves inherited nodes and returns them
     *
     * @param excludedGroups a list of groups to exclude
     * @return a set of nodes
     */
    public List<LocalizedNode> resolveInheritances(List<LocalizedNode> accumulator, Set<String> excludedGroups) {
        if (accumulator == null) {
            accumulator = new ArrayList<>();
        }

        if (excludedGroups == null) {
            excludedGroups = new HashSet<>();
        }

        if (this instanceof Group) {
            excludedGroups.add(getObjectName().toLowerCase());
        }

        // get and add the objects own nodes
        List<Node> nodes = getOwnNodes();
        for (Node node : nodes) {
            ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, getObjectName());
            accumulator.add(localizedNode);
        }

        // resolve and process the objects parents
        List<Group> resolvedGroups = new ArrayList<>();
        Set<String> processedGroups = new HashSet<>();

        for (Node n : nodes) {
            if (!n.isGroupNode())
                continue;
            String groupName = n.getGroupName();

            if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive())
                continue;

            Group g = plugin.getGroupManager().getIfLoaded(groupName);
            if (g != null) {
                resolvedGroups.add(g);
            }
        }

        // sort the groups according to weight + other factors.
        resolvedGroups.sort(inheritanceComparator);

        for (Group g : resolvedGroups) {
            g.resolveInheritances(accumulator, excludedGroups);
        }

        return accumulator;
    }

    public List<LocalizedNode> resolveInheritances() {
        return resolveInheritances(null, null);
    }

    public SortedSet<LocalizedNode> resolveInheritancesAlmostEqual() {
        List<LocalizedNode> nodes = resolveInheritances(new LinkedList<>(), null);
        NodeTools.removeAlmostEqual(nodes.iterator());
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());
        ret.addAll(nodes);
        return ret;
    }

    public SortedSet<LocalizedNode> resolveInheritancesMergeTemp() {
        List<LocalizedNode> nodes = resolveInheritances(new LinkedList<>(), null);
        NodeTools.removeIgnoreValueOrTemp(nodes.iterator());
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());
        ret.addAll(nodes);
        return ret;
    }

    public SortedSet<LocalizedNode> getAllNodes(Contexts context) {
        List<LocalizedNode> entries;
        if (context.isApplyGroups()) {
            entries = resolveInheritances(new LinkedList<>(), null, context);
        } else {
            entries = new LinkedList<>();
            for (Node n : filterNodes(context.getContexts())) {
                ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(n, getObjectName());
                entries.add(localizedNode);
            }
        }

        if (!context.isIncludeGlobal()) {
            entries.removeIf(n -> !n.isGroupNode() && !n.isServerSpecific());
        }
        if (!context.isApplyGlobalWorldGroups()) {
            entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific());
        }

        NodeTools.removeSamePermission(entries.iterator());
        SortedSet<LocalizedNode> ret = new TreeSet<>(NodeWithContextComparator.reverse());
        ret.addAll(entries);
        return ret;
    }

    public Map<String, Boolean> exportNodesAndShorthand(Contexts context, boolean lowerCase) {
        List<LocalizedNode> entries;
        if (context.isApplyGroups()) {
            entries = resolveInheritances(new LinkedList<>(), null, context);
        } else {
            entries = new LinkedList<>();
            for (Node n : filterNodes(context.getContexts())) {
                ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(n, getObjectName());
                entries.add(localizedNode);
            }
        }

        if (!context.isIncludeGlobal()) {
            entries.removeIf(n -> !n.isGroupNode() && !n.isServerSpecific());
        }
        if (!context.isApplyGlobalWorldGroups()) {
            entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific());
        }

        Map<String, Boolean> perms = new HashMap<>();
        boolean applyShorthand = plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND);
        for (Node node : entries) {
            String perm = lowerCase ? node.getPermission().toLowerCase() : node.getPermission();

            if (perms.putIfAbsent(perm, node.getValuePrimitive()) == null) {
                if (applyShorthand) {
                    List<String> sh = node.resolveShorthand();
                    if (!sh.isEmpty()) {
                        sh.stream().map(s -> lowerCase ? s.toLowerCase() : s)
                                .forEach(s -> perms.putIfAbsent(s, node.getValuePrimitive()));
                    }
                }
            }
        }

        return ImmutableMap.copyOf(perms);
    }

    public Map<String, Boolean> exportNodesAndShorthand(boolean lowerCase) {
        List<LocalizedNode> entries = resolveInheritances();

        Map<String, Boolean> perms = new HashMap<>();
        boolean applyShorthand = plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND);
        for (Node node : entries) {
            String perm = lowerCase ? node.getPermission().toLowerCase().intern() : node.getPermission();

            if (perms.putIfAbsent(perm, node.getValuePrimitive()) == null && applyShorthand) {
                List<String> shorthand = node.resolveShorthand();
                if (!shorthand.isEmpty()) {
                    for (String s : shorthand) {
                        perms.putIfAbsent((lowerCase ? s.toLowerCase() : s).intern(), node.getValuePrimitive());
                    }
                }
            }
        }

        return ImmutableMap.copyOf(perms);
    }

    public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, Set<String> excludedGroups,
            Contexts context) {
        if (accumulator == null) {
            accumulator = MetaAccumulator.makeFromConfig(plugin);
        }

        if (excludedGroups == null) {
            excludedGroups = new HashSet<>();
        }

        if (this instanceof Group) {
            excludedGroups.add(getObjectName().toLowerCase());
        }

        // get and add the objects own nodes
        List<Node> nodes = filterNodes(context.getContexts());

        for (Node node : nodes) {
            if (!node.getValuePrimitive())
                continue;
            if (!node.isMeta() && !node.isPrefix() && !node.isSuffix())
                continue;

            if (!((context.isIncludeGlobal() || node.isServerSpecific())
                    && (context.isIncludeGlobalWorld() || node.isWorldSpecific()))) {
                continue;
            }

            accumulator.accumulateNode(ImmutableLocalizedNode.of(node, getObjectName()));
        }

        OptionalInt w = getWeight();
        if (w.isPresent()) {
            accumulator.accumulateWeight(w.getAsInt());
        }

        // resolve and process the objects parents
        List<Group> resolvedGroups = new ArrayList<>();
        Set<String> processedGroups = new HashSet<>();

        for (Node n : nodes) {
            if (!n.isGroupNode())
                continue;
            String groupName = n.getGroupName();

            if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive())
                continue;

            if (!((context.isApplyGlobalGroups() || n.isServerSpecific())
                    && (context.isApplyGlobalWorldGroups() || n.isWorldSpecific()))) {
                continue;
            }

            Group g = plugin.getGroupManager().getIfLoaded(groupName);
            if (g != null) {
                resolvedGroups.add(g);
            }
        }

        // sort the groups according to weight + other factors.
        resolvedGroups.sort(inheritanceComparator);

        for (Group g : resolvedGroups) {
            g.accumulateMeta(accumulator, excludedGroups, context);
        }

        return accumulator;
    }

    public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, Set<String> excludedGroups) {
        if (accumulator == null) {
            accumulator = MetaAccumulator.makeFromConfig(plugin);
        }

        if (excludedGroups == null) {
            excludedGroups = new HashSet<>();
        }

        if (this instanceof Group) {
            excludedGroups.add(getObjectName().toLowerCase());
        }

        // get and add the objects own nodes
        List<Node> nodes = getOwnNodes();

        for (Node node : nodes) {
            if (!node.getValuePrimitive())
                continue;
            if (!node.isMeta() && !node.isPrefix() && !node.isSuffix())
                continue;

            accumulator.accumulateNode(ImmutableLocalizedNode.of(node, getObjectName()));
        }

        OptionalInt w = getWeight();
        if (w.isPresent()) {
            accumulator.accumulateWeight(w.getAsInt());
        }

        // resolve and process the objects parents
        List<Group> resolvedGroups = new ArrayList<>();
        Set<String> processedGroups = new HashSet<>();

        for (Node n : nodes) {
            if (!n.isGroupNode())
                continue;
            String groupName = n.getGroupName();

            if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive())
                continue;

            Group g = plugin.getGroupManager().getIfLoaded(groupName);
            if (g != null) {
                resolvedGroups.add(g);
            }
        }

        // sort the groups according to weight + other factors.
        resolvedGroups.sort(inheritanceComparator);

        for (Group g : resolvedGroups) {
            g.accumulateMeta(accumulator, excludedGroups);
        }

        return accumulator;
    }

    /**
     * Removes temporary permissions that have expired
     *
     * @return true if permissions had expired and were removed
     */
    public boolean auditTemporaryPermissions() {
        boolean work = false;
        Set<Node> removed = new HashSet<>();

        ImmutableSet<Node> before = ImmutableSet.copyOf(getOwnNodesSet());

        nodesLock.lock();
        try {
            Iterator<Node> it = nodes.values().iterator();
            while (it.hasNext()) {
                Node entry = it.next();
                if (entry.hasExpired()) {
                    removed.add(entry);
                    work = true;
                    it.remove();
                }
            }
        } finally {
            nodesLock.unlock();
        }

        if (work) {
            invalidateCache();
            work = false;
        }

        transientNodesLock.lock();
        try {
            Iterator<Node> it = transientNodes.values().iterator();
            while (it.hasNext()) {
                Node entry = it.next();
                if (entry.hasExpired()) {
                    removed.add(entry);
                    work = true;
                    it.remove();
                }
            }
        } finally {
            transientNodesLock.unlock();
        }

        if (work) {
            invalidateCache();
        }

        if (removed.isEmpty()) {
            return false;
        }

        ImmutableSet<Node> after = ImmutableSet.copyOf(getOwnNodesSet());

        for (Node r : removed) {
            plugin.getApiProvider().getEventFactory().handleNodeRemove(r, this, before, after);
        }

        return true;
    }

    public Optional<Node> getAlmostEquals(Node node, boolean t) {
        for (Node n : t ? getTransientNodes().values() : getEnduringNodes().values()) {
            if (n.almostEquals(node)) {
                return Optional.of(n);
            }
        }

        return Optional.empty();
    }

    /**
     * Check if the holder has a permission node
     *
     * @param node the node to check
     * @param checkTransient    whether to check transient nodes
     * @return a tristate
     */
    public Tristate hasPermission(Node node, boolean checkTransient) {
        if (this instanceof Group && node.isGroupNode() && node.getGroupName().equalsIgnoreCase(getObjectName())) {
            return Tristate.TRUE;
        }

        return getAlmostEquals(node, checkTransient).map(Node::getTristate).orElse(Tristate.UNDEFINED);
    }

    public Tristate hasPermission(Node node) {
        return hasPermission(node, false);
    }

    public boolean hasPermission(String node, boolean value) {
        return hasPermission(NodeFactory.make(node, value)).asBoolean() == value;
    }

    public boolean hasPermission(String node, boolean value, String server) {
        return hasPermission(NodeFactory.make(node, value, server)).asBoolean() == value;
    }

    public boolean hasPermission(String node, boolean value, String server, String world) {
        return hasPermission(NodeFactory.make(node, value, server, world)).asBoolean() == value;
    }

    public boolean hasPermission(String node, boolean value, boolean temporary) {
        return hasPermission(NodeFactory.make(node, value, temporary)).asBoolean() == value;
    }

    public boolean hasPermission(String node, boolean value, String server, boolean temporary) {
        return hasPermission(NodeFactory.make(node, value, server, temporary)).asBoolean() == value;
    }

    public boolean hasPermission(String node, boolean value, String server, String world, boolean temporary) {
        return hasPermission(NodeFactory.make(node, value, server, world, temporary)).asBoolean() == value;
    }

    /**
     * Check if the holder inherits a node
     *
     * @param node the node to check
     * @return the result of the lookup
     */
    public InheritanceInfo inheritsPermissionInfo(Node node) {
        for (LocalizedNode n : resolveInheritances()) {
            if (n.getNode().almostEquals(node)) {
                return InheritanceInfo.of(n);
            }
        }

        return InheritanceInfo.empty();
    }

    /**
     * Check if the holder inherits a node
     *
     * @param node the node to check
     * @return the Tristate result
     */
    public Tristate inheritsPermission(Node node) {
        return inheritsPermissionInfo(node).getResult();
    }

    public boolean inheritsPermission(String node, boolean value) {
        return inheritsPermission(NodeFactory.make(node, value)).asBoolean() == value;
    }

    public boolean inheritsPermission(String node, boolean value, String server) {
        return inheritsPermission(NodeFactory.make(node, value, server)).asBoolean() == value;
    }

    public boolean inheritsPermission(String node, boolean value, String server, String world) {
        return inheritsPermission(NodeFactory.make(node, value, server, world)).asBoolean() == value;
    }

    public boolean inheritsPermission(String node, boolean value, boolean temporary) {
        return inheritsPermission(NodeFactory.make(node, value, temporary)).asBoolean() == value;
    }

    public boolean inheritsPermission(String node, boolean value, String server, boolean temporary) {
        return inheritsPermission(NodeFactory.make(node, value, server, temporary)).asBoolean() == value;
    }

    public boolean inheritsPermission(String node, boolean value, String server, String world, boolean temporary) {
        return inheritsPermission(NodeFactory.make(node, value, server, world, temporary)).asBoolean() == value;
    }

    /**
     * Sets a permission node
     *
     * @param node the node to set
     */
    public DataMutateResult setPermission(Node node) {
        if (hasPermission(node, false) != Tristate.UNDEFINED) {
            return DataMutateResult.ALREADY_HAS;
        }

        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            nodes.put(node.getFullContexts().makeImmutable(), node);
        } finally {
            nodesLock.unlock();
        }
        invalidateCache();

        ImmutableCollection<Node> after = getEnduringNodes().values();

        plugin.getApiProvider().getEventFactory().handleNodeAdd(node, this, before, after);
        return DataMutateResult.SUCCESS;
    }

    /**
     * Sets a permission node, applying a temporary modifier if the node is temporary.
     * @param node the node to set
     * @param modifier the modifier to use for the operation
     * @return the node that was actually set, respective of the modifier
     */
    public Map.Entry<DataMutateResult, Node> setPermission(Node node, TemporaryModifier modifier) {
        // If the node is temporary, we should take note of the modifier
        if (node.isTemporary()) {
            if (modifier == TemporaryModifier.ACCUMULATE) {
                // Try to accumulate with an existing node
                Optional<Node> existing = getAlmostEquals(node, false);

                // An existing node was found
                if (existing.isPresent()) {
                    Node previous = existing.get();

                    // Create a new node with the same properties, but add the expiry dates together
                    Node newNode = NodeFactory.builderFromExisting(node)
                            .setExpiry(previous.getExpiryUnixTime() + node.getSecondsTilExpiry()).build();

                    ImmutableCollection<Node> before = getEnduringNodes().values();

                    // Remove the old node & add the new one.
                    nodesLock.lock();
                    try {
                        nodes.remove(previous.getFullContexts().makeImmutable(), previous);
                        nodes.put(newNode.getFullContexts().makeImmutable(), newNode);
                    } finally {
                        nodesLock.unlock();
                    }

                    invalidateCache();
                    ImmutableCollection<Node> after = getEnduringNodes().values();
                    plugin.getApiProvider().getEventFactory().handleNodeAdd(newNode, this, before, after);
                    return Maps.immutableEntry(DataMutateResult.SUCCESS, newNode);
                }

            } else if (modifier == TemporaryModifier.REPLACE) {
                // Try to replace an existing node
                Optional<Node> existing = getAlmostEquals(node, false);

                // An existing node was found
                if (existing.isPresent()) {
                    Node previous = existing.get();

                    // Only replace if the new expiry time is greater than the old one.
                    if (node.getExpiryUnixTime() > previous.getExpiryUnixTime()) {

                        ImmutableCollection<Node> before = getEnduringNodes().values();

                        nodesLock.lock();
                        try {
                            nodes.remove(previous.getFullContexts().makeImmutable(), previous);
                            nodes.put(node.getFullContexts().makeImmutable(), node);
                        } finally {
                            nodesLock.unlock();
                        }

                        invalidateCache();
                        ImmutableCollection<Node> after = getEnduringNodes().values();
                        plugin.getApiProvider().getEventFactory().handleNodeAdd(node, this, before, after);
                        return Maps.immutableEntry(DataMutateResult.SUCCESS, node);
                    }
                }
            }

            // DENY behaviour is the default anyways.
        }

        // Fallback to the normal handling.
        return Maps.immutableEntry(setPermission(node), node);
    }

    /**
     * Sets a transient permission node
     *
     * @param node the node to set
     */
    public DataMutateResult setTransientPermission(Node node) {
        if (hasPermission(node, true) != Tristate.UNDEFINED) {
            return DataMutateResult.ALREADY_HAS;
        }

        ImmutableCollection<Node> before = getTransientNodes().values();

        transientNodesLock.lock();
        try {
            transientNodes.put(node.getFullContexts().makeImmutable(), node);
        } finally {
            transientNodesLock.unlock();
        }

        invalidateCache();

        ImmutableCollection<Node> after = getTransientNodes().values();

        plugin.getApiProvider().getEventFactory().handleNodeAdd(node, this, before, after);
        return DataMutateResult.SUCCESS;
    }

    /**
     * Unsets a permission node
     *
     * @param node the node to unset
     */
    public DataMutateResult unsetPermission(Node node) {
        if (hasPermission(node, false) == Tristate.UNDEFINED) {
            return DataMutateResult.LACKS;
        }

        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            nodes.get(node.getFullContexts().makeImmutable()).removeIf(e -> e.almostEquals(node));
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();

        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeRemove(node, this, before, after);
        return DataMutateResult.SUCCESS;
    }

    /**
     * Unsets a permission node
     *
     * @param node the node to unset
     */
    public DataMutateResult unsetPermissionExact(Node node) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            nodes.get(node.getFullContexts().makeImmutable()).removeIf(e -> e.equals(node));
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();

        ImmutableCollection<Node> after = getEnduringNodes().values();

        if (before.size() == after.size()) {
            return DataMutateResult.LACKS;
        }

        plugin.getApiProvider().getEventFactory().handleNodeRemove(node, this, before, after);
        return DataMutateResult.SUCCESS;
    }

    /**
     * Unsets a transient permission node
     *
     * @param node the node to unset
     */
    public DataMutateResult unsetTransientPermission(Node node) {
        if (hasPermission(node, true) == Tristate.UNDEFINED) {
            return DataMutateResult.LACKS;
        }

        ImmutableCollection<Node> before = getTransientNodes().values();

        transientNodesLock.lock();
        try {
            transientNodes.get(node.getFullContexts().makeImmutable()).removeIf(e -> e.almostEquals(node));
        } finally {
            transientNodesLock.unlock();
        }

        invalidateCache();

        ImmutableCollection<Node> after = getTransientNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeRemove(node, this, before, after);
        return DataMutateResult.SUCCESS;
    }

    public boolean inheritsGroup(Group group) {
        return group.getName().equalsIgnoreCase(this.getObjectName())
                || hasPermission("group." + group.getName(), true);
    }

    public boolean inheritsGroup(Group group, ContextSet contextSet) {
        return group.getName().equalsIgnoreCase(this.getObjectName()) || hasPermission(
                NodeFactory.newBuilder("group." + group.getName()).withExtraContext(contextSet).build())
                        .asBoolean();
    }

    public boolean inheritsGroup(Group group, String server) {
        return group.getName().equalsIgnoreCase(this.getObjectName())
                || hasPermission("group." + group.getName(), true, server);
    }

    public boolean inheritsGroup(Group group, String server, String world) {
        return group.getName().equalsIgnoreCase(this.getObjectName())
                || hasPermission("group." + group.getName(), true, server, world);
    }

    public DataMutateResult setInheritGroup(Group group, ContextSet contexts) {
        return setPermission(NodeFactory.newBuilder("group." + group.getName()).withExtraContext(contexts).build());
    }

    public DataMutateResult unsetInheritGroup(Group group, ContextSet contexts) {
        return unsetPermission(
                NodeFactory.newBuilder("group." + group.getName()).withExtraContext(contexts).build());
    }

    /**
     * Clear all of the holders permission nodes
     */
    public boolean clearNodes() {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            nodes.clear();
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();

        if (before.size() == after.size()) {
            return false;
        }

        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearNodes(ContextSet contextSet) {
        ImmutableCollection<Node> before = getEnduringNodes().values();
        nodesLock.lock();
        try {
            nodes.removeAll(contextSet.makeImmutable());
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();

        if (before.size() == after.size()) {
            return false;
        }

        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearParents(boolean giveDefault) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            boolean b = nodes.values().removeIf(Node::isGroupNode);
            if (!b) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        if (this instanceof User && giveDefault) {
            plugin.getUserManager().giveDefaultIfNeeded((User) this, false);
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearParents(ContextSet contextSet, boolean giveDefault) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            SortedSet<Node> nodes = this.nodes.get(contextSet.makeImmutable());
            if (nodes == null) {
                return false;
            }

            boolean b = nodes.removeIf(Node::isGroupNode);
            if (!b) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        if (this instanceof User && giveDefault) {
            plugin.getUserManager().giveDefaultIfNeeded((User) this, false);
        }
        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearMeta(MetaType type) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            if (!nodes.values().removeIf(type::matches)) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearMeta(MetaType type, ContextSet contextSet) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            SortedSet<Node> nodes = this.nodes.get(contextSet.makeImmutable());
            if (nodes == null) {
                return false;
            }

            boolean b = nodes.removeIf(type::matches);
            if (!b) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearMetaKeys(String key, boolean temp) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            boolean b = this.nodes.values().removeIf(
                    n -> n.isMeta() && (n.isTemporary() == temp) && n.getMeta().getKey().equalsIgnoreCase(key));
            if (!b) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearMetaKeys(String key, ContextSet contextSet, boolean temp) {
        ImmutableCollection<Node> before = getEnduringNodes().values();

        nodesLock.lock();
        try {
            SortedSet<Node> nodes = this.nodes.get(contextSet.makeImmutable());
            if (nodes == null) {
                return false;
            }

            boolean b = nodes.removeIf(
                    n -> n.isMeta() && (n.isTemporary() == temp) && n.getMeta().getKey().equalsIgnoreCase(key));
            if (!b) {
                return false;
            }
        } finally {
            nodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getEnduringNodes().values();
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    public boolean clearTransientNodes() {
        ImmutableCollection<Node> before = getTransientNodes().values();

        transientNodesLock.lock();
        try {
            transientNodes.clear();
        } finally {
            transientNodesLock.unlock();
        }

        invalidateCache();
        ImmutableCollection<Node> after = getTransientNodes().values();

        if (before.size() == after.size()) {
            return false;
        }

        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
        return true;
    }

    /**
     * @return The temporary nodes held by the holder
     */
    public Set<Node> getTemporaryNodes() {
        return getOwnNodes().stream().filter(Node::isTemporary).collect(Collectors.toSet());
    }

    /**
     * @return The permanent nodes held by the holder
     */
    public Set<Node> getPermanentNodes() {
        return getOwnNodes().stream().filter(Node::isPermanent).collect(Collectors.toSet());
    }

    public Set<Node> getPrefixNodes() {
        return getOwnNodes().stream().filter(Node::isPrefix).collect(Collectors.toSet());
    }

    public Set<Node> getSuffixNodes() {
        return getOwnNodes().stream().filter(Node::isSuffix).collect(Collectors.toSet());
    }

    public Set<Node> getMetaNodes() {
        return getOwnNodes().stream().filter(Node::isMeta).collect(Collectors.toSet());
    }

    public OptionalInt getWeight() {
        return weightCache.get();
    }

    private OptionalInt calculateWeight() {
        if (this instanceof User)
            return OptionalInt.empty();

        boolean seen = false;
        int best = 0;
        for (Node n : getEnduringNodes().get(ImmutableContextSet.empty())) {
            if (!n.getPermission().startsWith("weight.")) {
                continue;
            }

            String substring = n.getPermission().substring("weight.".length());

            int i;
            try {
                i = Integer.parseInt(substring);
            } catch (NumberFormatException e) {
                continue;
            }

            if (!seen || i > best) {
                seen = true;
                best = i;
            }
        }
        OptionalInt weight = seen ? OptionalInt.of(best) : OptionalInt.empty();

        if (!weight.isPresent()) {
            Integer w = plugin.getConfiguration().get(ConfigKeys.GROUP_WEIGHTS).get(getObjectName().toLowerCase());
            if (w != null) {
                weight = OptionalInt.of(w);
            }
        }

        return weight;
    }

    public Set<HolderReference> getGroupReferences() {
        return getOwnNodes().stream().filter(Node::isGroupNode).map(Node::getGroupName).map(String::toLowerCase)
                .map(GroupReference::of).collect(Collectors.toSet());
    }
}