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

Java tutorial

Introduction

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

Source

/*
 * Copyright (c) 2016 Lucko (Luck) <luck@lucko.me>
 *
 *  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.core.model;

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

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;

import me.lucko.luckperms.api.Contexts;
import me.lucko.luckperms.api.LocalizedNode;
import me.lucko.luckperms.api.Node;
import me.lucko.luckperms.api.Tristate;
import me.lucko.luckperms.common.api.delegates.PermissionHolderDelegate;
import me.lucko.luckperms.common.caching.MetaHolder;
import me.lucko.luckperms.common.caching.handlers.CachedStateManager;
import me.lucko.luckperms.common.caching.handlers.GroupReference;
import me.lucko.luckperms.common.caching.handlers.HolderReference;
import me.lucko.luckperms.common.caching.holder.ExportNodesHolder;
import me.lucko.luckperms.common.caching.holder.GetAllNodesRequest;
import me.lucko.luckperms.common.commands.utils.Util;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.core.InheritanceInfo;
import me.lucko.luckperms.common.core.NodeBuilder;
import me.lucko.luckperms.common.core.NodeFactory;
import me.lucko.luckperms.common.core.PriorityComparator;
import me.lucko.luckperms.common.core.TemporaryModifier;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.utils.Cache;
import me.lucko.luckperms.common.utils.ExtractedContexts;
import me.lucko.luckperms.common.utils.ImmutableLocalizedNode;
import me.lucko.luckperms.common.utils.WeightComparator;
import me.lucko.luckperms.exceptions.ObjectAlreadyHasException;
import me.lucko.luckperms.exceptions.ObjectLacksException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * Represents an object that can hold permissions
 * For example a User or a Group
 */
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class PermissionHolder {

    /**
     * The UUID of the user / name of the group.
     * Used to prevent circular inheritance issues
     */
    @Getter
    private final String objectName;

    /**
     * Reference to the main plugin instance
     */
    @Getter(AccessLevel.PROTECTED)
    private final LuckPermsPlugin plugin;

    /**
     * The holders persistent nodes
     */
    private final Set<Node> nodes = new HashSet<>();

    /**
     * The holders transient nodes
     */
    private final Set<Node> transientNodes = new HashSet<>();

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

    @Getter
    private final Set<Runnable> stateListeners = ConcurrentHashMap.newKeySet();

    /*
     * CACHES - cache the result of a number of methods in this class, until they are invalidated.
     */

    /* Internal Caches - only depend on the state of this instance. */

    // Just holds immutable copies of the node sets.
    private Cache<ImmutableSet<Node>> enduringCache = new Cache<>(() -> {
        synchronized (nodes) {
            return ImmutableSet.copyOf(nodes);
        }
    });
    private Cache<ImmutableSet<Node>> transientCache = new Cache<>(() -> {
        synchronized (transientNodes) {
            return ImmutableSet.copyOf(transientNodes);
        }
    });

    // Merges transient and persistent nodes together, and converts Node instances to a localized form.
    private Cache<ImmutableSortedSet<LocalizedNode>> cache = new Cache<>(() -> cacheApply(false));
    // Same as the cache above, except this merges temporary values with any permanent values if any are matching.
    private Cache<ImmutableSortedSet<LocalizedNode>> mergedCache = new Cache<>(() -> cacheApply(true));

    /* External Caches - may depend on the state of other instances. */

    private LoadingCache<GetAllNodesRequest, SortedSet<LocalizedNode>> getAllNodesCache = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(new CacheLoader<GetAllNodesRequest, SortedSet<LocalizedNode>>() {
                @Override
                public SortedSet<LocalizedNode> load(GetAllNodesRequest getAllNodesHolder) {
                    return getAllNodesCacheApply(getAllNodesHolder);
                }
            });
    private LoadingCache<ExtractedContexts, Set<LocalizedNode>> getAllNodesFilteredCache = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(new CacheLoader<ExtractedContexts, Set<LocalizedNode>>() {
                @Override
                public Set<LocalizedNode> load(ExtractedContexts extractedContexts) throws Exception {
                    return getAllNodesFilteredApply(extractedContexts);
                }
            });
    private LoadingCache<ExportNodesHolder, Map<String, Boolean>> exportNodesCache = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(new CacheLoader<ExportNodesHolder, Map<String, Boolean>>() {
                @Override
                public Map<String, Boolean> load(ExportNodesHolder exportNodesHolder) throws Exception {
                    return exportNodesApply(exportNodesHolder);
                }
            });

    /* Caching apply methods. Are just called by the caching instances to gather data about the instance. */

    protected void forceCleanup() {
        getAllNodesCache.cleanUp();
        getAllNodesFilteredCache.cleanUp();
        exportNodesCache.cleanUp();
    }

    private void invalidateCache(boolean enduring) {
        if (enduring) {
            enduringCache.invalidate();
        } else {
            transientCache.invalidate();
        }
        cache.invalidate();
        mergedCache.invalidate();

        // Invalidate inheritance caches
        getAllNodesCache.invalidateAll();
        getAllNodesFilteredCache.invalidateAll();
        exportNodesCache.invalidateAll();

        // Invalidate listeners
        for (Runnable r : stateListeners) {
            try {
                r.run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // Get previous references
        Set<HolderReference> refs = plugin.getCachedStateManager().getInheritances(toReference());

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

        // Add all new references affected by the state change.
        refs.addAll(plugin.getCachedStateManager().getInheritances(toReference()));

        // Invalidate all affected children.
        CachedStateManager.invalidateInheritances(plugin, refs);
    }

    public void invalidateInheritanceCaches() {
        getAllNodesCache.invalidateAll();
        getAllNodesFilteredCache.invalidateAll();
        exportNodesCache.invalidateAll();
        declareState();
    }

    private ImmutableSortedSet<LocalizedNode> cacheApply(boolean mergeTemp) {
        TreeSet<LocalizedNode> combined = new TreeSet<>(PriorityComparator.reverse());
        Set<Node> enduring = getNodes();
        if (!enduring.isEmpty()) {
            combined.addAll(enduring.stream().map(n -> makeLocal(n, getObjectName())).collect(Collectors.toList()));
        }
        Set<Node> tran = getTransientNodes();
        if (!tran.isEmpty()) {
            combined.addAll(tran.stream().map(n -> makeLocal(n, getObjectName())).collect(Collectors.toList()));
        }

        Iterator<LocalizedNode> it = combined.iterator();
        Set<LocalizedNode> higherPriority = new HashSet<>();

        iterate: while (it.hasNext()) {
            LocalizedNode entry = it.next();
            for (LocalizedNode h : higherPriority) {
                if (mergeTemp ? entry.getNode().equalsIgnoringValueOrTemp(h.getNode())
                        : entry.getNode().almostEquals(h.getNode())) {
                    it.remove();
                    continue iterate;
                }
            }
            higherPriority.add(entry);
        }
        return ImmutableSortedSet.copyOfSorted(combined);
    }

    private SortedSet<LocalizedNode> getAllNodesCacheApply(GetAllNodesRequest getAllNodesHolder) {
        // Expand the holder.
        List<String> excludedGroups = new ArrayList<>(getAllNodesHolder.getExcludedGroups());
        ExtractedContexts contexts = getAllNodesHolder.getContexts();

        // Don't register users, as they cannot be inherited.
        if (!(this instanceof User)) {
            excludedGroups.add(getObjectName().toLowerCase());
        }

        // Get the objects base permissions.
        SortedSet<LocalizedNode> all = new TreeSet<>((SortedSet<LocalizedNode>) getPermissions(true));

        Contexts context = contexts.getContexts();
        String server = contexts.getServer();
        String world = contexts.getWorld();

        Set<Node> parents = all.stream().map(LocalizedNode::getNode).filter(Node::getValue)
                .filter(Node::isGroupNode)
                .filter(n -> n.shouldApplyOnServer(server, context.isApplyGlobalGroups(),
                        plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)))
                .filter(n -> n.shouldApplyOnWorld(world, context.isApplyGlobalWorldGroups(),
                        plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)))
                .filter(n -> n.shouldApplyWithContext(contexts.getContextSet(), false)).collect(Collectors.toSet());

        // Resolve & sort parents into order before we apply.
        TreeSet<Map.Entry<Integer, Group>> sortedParents = new TreeSet<>(WeightComparator.INSTANCE.reversed());
        for (Node node : parents) {
            Group group = plugin.getGroupManager().getIfLoaded(node.getGroupName());
            if (group != null) {
                sortedParents.add(Maps.immutableEntry(group.getWeight().orElse(0), group));
            }
        }

        for (Map.Entry<Integer, Group> e : sortedParents) {
            if (excludedGroups.contains(e.getValue().getObjectName().toLowerCase())) {
                continue;
            }

            inherited: for (LocalizedNode inherited : e.getValue().getAllNodes(excludedGroups, contexts)) {
                for (LocalizedNode existing : all) {
                    if (existing.getNode().almostEquals(inherited.getNode())) {
                        continue inherited;
                    }
                }

                all.add(inherited);
            }
        }

        return all;
    }

    private Set<LocalizedNode> getAllNodesFilteredApply(ExtractedContexts contexts) {
        Contexts context = contexts.getContexts();
        String server = contexts.getServer();
        String world = contexts.getWorld();

        SortedSet<LocalizedNode> allNodes;
        if (context.isApplyGroups()) {
            allNodes = getAllNodes(null, contexts);
        } else {
            allNodes = new TreeSet<>((SortedSet<LocalizedNode>) getPermissions(true));
        }

        allNodes.removeIf(node -> !node.isGroupNode() && (!node.shouldApplyOnServer(server,
                context.isIncludeGlobal(), plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))
                || !node.shouldApplyOnWorld(world, context.isIncludeGlobalWorld(),
                        plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))
                || !node.shouldApplyWithContext(contexts.getContextSet(), false)));

        Set<LocalizedNode> perms = ConcurrentHashMap.newKeySet();

        all: for (LocalizedNode ln : allNodes) {
            // Force higher priority nodes to override
            for (LocalizedNode alreadyIn : perms) {
                if (ln.getNode().getPermission().equals(alreadyIn.getNode().getPermission())) {
                    continue all;
                }
            }

            perms.add(ln);
        }

        return perms;
    }

    private Map<String, Boolean> exportNodesApply(ExportNodesHolder exportNodesHolder) {
        Contexts context = exportNodesHolder.getContexts();
        Boolean lowerCase = exportNodesHolder.getLowerCase();

        Map<String, Boolean> perms = new HashMap<>();

        for (LocalizedNode ln : getAllNodesFiltered(ExtractedContexts.generate(context))) {
            Node node = ln.getNode();

            perms.put(lowerCase ? node.getPermission().toLowerCase() : node.getPermission(), node.getValue());

            if (plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND)) {
                List<String> sh = node.resolveShorthand();
                if (!sh.isEmpty()) {
                    sh.stream().map(s -> lowerCase ? s.toLowerCase() : s).filter(s -> !perms.containsKey(s))
                            .forEach(s -> perms.put(s, node.getValue()));
                }
            }
        }

        return ImmutableMap.copyOf(perms);
    }

    protected void declareState() {
        plugin.getCachedStateManager().putAll(toReference(), getGroupReferences());
    }

    public abstract String getFriendlyName();

    public abstract HolderReference<?> toReference();

    public abstract PermissionHolderDelegate getDelegate();

    public Set<Node> getNodes() {
        return enduringCache.get();
    }

    public void setNodes(Map<String, Boolean> nodes) {
        Set<Node> set = nodes.entrySet().stream().map(e -> makeNode(e.getKey(), e.getValue()))
                .collect(Collectors.toSet());

        setNodes(set);
    }

    public void setNodes(Set<Node> set) {
        synchronized (nodes) {
            if (nodes.equals(set)) {
                return;
            }

            nodes.clear();
            nodes.addAll(set);
        }
        invalidateCache(true);
    }

    public Set<Node> getTransientNodes() {
        return transientCache.get();
    }

    public void setTransientNodes(Set<Node> set) {
        synchronized (transientNodes) {
            if (transientNodes.equals(set)) {
                return;
            }

            transientNodes.clear();
            transientNodes.addAll(set);
        }
        invalidateCache(false);
    }

    /**
     * Combines and returns this holders nodes in a priority order.
     *
     * @param mergeTemp if the temporary nodes should be merged together with permanent nodes
     * @return the holders transient and permanent nodes
     */
    public SortedSet<LocalizedNode> getPermissions(boolean mergeTemp) {
        return mergeTemp ? mergedCache.get() : cache.get();
    }

    /**
     * Resolves inherited nodes and returns them
     *
     * @param excludedGroups a list of groups to exclude
     * @param contexts       context to decide if groups should be applied
     * @return a set of nodes
     */
    public SortedSet<LocalizedNode> getAllNodes(List<String> excludedGroups, ExtractedContexts contexts) {
        return getAllNodesCache.getUnchecked(GetAllNodesRequest
                .of(excludedGroups == null ? ImmutableList.of() : ImmutableList.copyOf(excludedGroups), contexts));
    }

    /**
     * Gets all of the nodes that this holder has (and inherits), given the context
     *
     * @param contexts the context for this request
     * @return a map of permissions
     */
    public Set<LocalizedNode> getAllNodesFiltered(ExtractedContexts contexts) {
        return getAllNodesFilteredCache.getUnchecked(contexts);
    }

    /**
     * Converts the output of {@link #getAllNodesFiltered(ExtractedContexts)}, and expands shorthand perms
     *
     * @param context the context for this request
     * @return a map of permissions
     */
    public Map<String, Boolean> exportNodes(Contexts context, boolean lowerCase) {
        return exportNodesCache.getUnchecked(ExportNodesHolder.of(context, lowerCase));
    }

    public MetaHolder accumulateMeta(MetaHolder holder, List<String> excludedGroups, ExtractedContexts contexts) {
        if (holder == null) {
            holder = new MetaHolder(plugin.getConfiguration().get(ConfigKeys.PREFIX_FORMATTING_OPTIONS).copy(),
                    plugin.getConfiguration().get(ConfigKeys.SUFFIX_FORMATTING_OPTIONS).copy());
        }

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

        excludedGroups.add(getObjectName().toLowerCase());

        Contexts context = contexts.getContexts();
        String server = contexts.getServer();
        String world = contexts.getWorld();

        SortedSet<LocalizedNode> all = new TreeSet<>((SortedSet<LocalizedNode>) getPermissions(true));
        for (LocalizedNode ln : all) {
            Node n = ln.getNode();

            if (!n.getValue())
                continue;
            if (!n.isMeta() && !n.isPrefix() && !n.isSuffix())
                continue;
            if (!n.shouldApplyOnServer(server, context.isIncludeGlobal(), false))
                continue;
            if (!n.shouldApplyOnWorld(world, context.isIncludeGlobalWorld(), false))
                continue;
            if (!n.shouldApplyWithContext(contexts.getContextSet(), false))
                continue;

            holder.accumulateNode(ln);
        }

        Set<Node> parents = all.stream().map(LocalizedNode::getNode).filter(Node::getValue)
                .filter(Node::isGroupNode).collect(Collectors.toSet());

        parents.removeIf(node -> !node.shouldApplyOnServer(server, context.isApplyGlobalGroups(),
                plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))
                || !node.shouldApplyOnWorld(world, context.isApplyGlobalWorldGroups(),
                        plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX))
                || !node.shouldApplyWithContext(contexts.getContextSet(), false));

        TreeSet<Map.Entry<Integer, Node>> sortedParents = new TreeSet<>(Util.META_COMPARATOR.reversed());
        for (Node node : parents) {
            Group group = plugin.getGroupManager().getIfLoaded(node.getGroupName());
            if (group != null) {
                OptionalInt weight = group.getWeight();
                if (weight.isPresent()) {
                    sortedParents.add(Maps.immutableEntry(weight.getAsInt(), node));
                    continue;
                }
            }

            sortedParents.add(Maps.immutableEntry(0, node));
        }

        for (Map.Entry<Integer, Node> e : sortedParents) {
            Node parent = e.getValue();
            Group group = plugin.getGroupManager().getIfLoaded(parent.getGroupName());
            if (group == null) {
                continue;
            }

            if (excludedGroups.contains(group.getObjectName().toLowerCase())) {
                continue;
            }

            group.accumulateMeta(holder, excludedGroups, contexts);
        }

        return holder;
    }

    /**
     * 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(getPermissions(true));

        synchronized (nodes) {
            Iterator<Node> it = nodes.iterator();
            while (it.hasNext()) {
                Node entry = it.next();
                if (entry.hasExpired()) {
                    removed.add(entry);
                    work = true;
                    it.remove();
                }
            }
        }

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

        synchronized (transientNodes) {
            Iterator<Node> it = transientNodes.iterator();
            while (it.hasNext()) {
                Node entry = it.next();
                if (entry.hasExpired()) {
                    removed.add(entry);
                    work = true;
                    it.remove();
                }
            }
        }

        if (work) {
            invalidateCache(false);
        }

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

        ImmutableSet<Node> after = ImmutableSet.copyOf(getPermissions(true));

        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() : getNodes()) {
            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 t    whether to check transient nodes
     * @return a tristate
     */
    public Tristate hasPermission(Node node, boolean t) {
        return getAlmostEquals(node, t).map(Node::getTristate).orElse(Tristate.UNDEFINED);
    }

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

    public boolean hasPermission(String node, boolean b) {
        return hasPermission(buildNode(node).setValue(b).build()).asBoolean() == b;
    }

    public boolean hasPermission(String node, boolean b, String server) {
        return hasPermission(buildNode(node).setValue(b).setServer(server).build()).asBoolean() == b;
    }

    public boolean hasPermission(String node, boolean b, String server, String world) {
        return hasPermission(buildNode(node).setValue(b).setServer(server).setWorld(world).build())
                .asBoolean() == b;
    }

    public boolean hasPermission(String node, boolean b, boolean temporary) {
        return hasPermission(buildNode(node).setValue(b).setExpiry(temporary ? 10L : 0L).build()).asBoolean() == b;
    }

    public boolean hasPermission(String node, boolean b, String server, boolean temporary) {
        return hasPermission(buildNode(node).setValue(b).setServer(server).setExpiry(temporary ? 10L : 0L).build())
                .asBoolean() == b;
    }

    public boolean hasPermission(String node, boolean b, String server, String world, boolean temporary) {
        return hasPermission(buildNode(node).setValue(b).setServer(server).setWorld(world)
                .setExpiry(temporary ? 10L : 0L).build()).asBoolean() == b;
    }

    /**
     * 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 : getAllNodes(null, ExtractedContexts.generate(Contexts.allowAll()))) {
            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 b) {
        return inheritsPermission(buildNode(node).setValue(b).build()).asBoolean() == b;
    }

    public boolean inheritsPermission(String node, boolean b, String server) {
        return inheritsPermission(buildNode(node).setValue(b).setServer(server).build()).asBoolean() == b;
    }

    public boolean inheritsPermission(String node, boolean b, String server, String world) {
        return inheritsPermission(buildNode(node).setValue(b).setServer(server).setWorld(world).build())
                .asBoolean() == b;
    }

    public boolean inheritsPermission(String node, boolean b, boolean temporary) {
        return inheritsPermission(buildNode(node).setValue(b).setExpiry(temporary ? 10L : 0L).build())
                .asBoolean() == b;
    }

    public boolean inheritsPermission(String node, boolean b, String server, boolean temporary) {
        return inheritsPermission(
                buildNode(node).setValue(b).setServer(server).setExpiry(temporary ? 10L : 0L).build())
                        .asBoolean() == b;
    }

    public boolean inheritsPermission(String node, boolean b, String server, String world, boolean temporary) {
        return inheritsPermission(buildNode(node).setValue(b).setServer(server).setWorld(world)
                .setExpiry(temporary ? 10L : 0L).build()).asBoolean() == b;
    }

    /**
     * Sets a permission node
     *
     * @param node the node to set
     * @throws ObjectAlreadyHasException if the holder has this permission already
     */
    public void setPermission(Node node) throws ObjectAlreadyHasException {
        if (hasPermission(node, false) != Tristate.UNDEFINED) {
            throw new ObjectAlreadyHasException();
        }

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

        synchronized (nodes) {
            nodes.add(node);
        }
        invalidateCache(true);

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

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

    /**
     * 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
     * @throws ObjectAlreadyHasException if the holder has this permission set already, respective of the modifier
     */
    public Node setPermission(Node node, TemporaryModifier modifier) throws ObjectAlreadyHasException {
        // 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();

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

                    // Remove the old node & add the new one.
                    synchronized (nodes) {
                        nodes.remove(previous);
                        nodes.add(newNode);
                    }

                    invalidateCache(true);
                    ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
                    plugin.getApiProvider().getEventFactory().handleNodeAdd(newNode, this, before, after);
                    return 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()) {

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

                        synchronized (nodes) {
                            nodes.remove(previous);
                            nodes.add(node);
                        }

                        invalidateCache(true);
                        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
                        plugin.getApiProvider().getEventFactory().handleNodeAdd(node, this, before, after);
                        return node;
                    }
                }
            }

            // DENY behaviour is the default anyways.
        }

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

    /**
     * Sets a transient permission node
     *
     * @param node the node to set
     * @throws ObjectAlreadyHasException if the holder has this permission already
     */
    public void setTransientPermission(Node node) throws ObjectAlreadyHasException {
        if (hasPermission(node, true) != Tristate.UNDEFINED) {
            throw new ObjectAlreadyHasException();
        }

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

        synchronized (transientNodes) {
            transientNodes.add(node);
        }
        invalidateCache(false);

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

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

    public void setPermission(String node, boolean value) throws ObjectAlreadyHasException {
        setPermission(buildNode(node).setValue(value).build());
    }

    public void setPermission(String node, boolean value, String server) throws ObjectAlreadyHasException {
        setPermission(buildNode(node).setValue(value).setServer(server).build());
    }

    public void setPermission(String node, boolean value, String server, String world)
            throws ObjectAlreadyHasException {
        setPermission(buildNode(node).setValue(value).setServer(server).setWorld(world).build());
    }

    public void setPermission(String node, boolean value, long expireAt) throws ObjectAlreadyHasException {
        setPermission(buildNode(node).setValue(value).setExpiry(expireAt).build());
    }

    public void setPermission(String node, boolean value, String server, long expireAt)
            throws ObjectAlreadyHasException {
        setPermission(buildNode(node).setValue(value).setServer(server).setExpiry(expireAt).build());
    }

    public void setPermission(String node, boolean value, String server, String world, long expireAt)
            throws ObjectAlreadyHasException {
        setPermission(
                buildNode(node).setValue(value).setServer(server).setWorld(world).setExpiry(expireAt).build());
    }

    /**
     * Unsets a permission node
     *
     * @param node the node to unset
     * @throws ObjectLacksException if the holder doesn't have this node already
     */
    public void unsetPermission(Node node) throws ObjectLacksException {
        if (hasPermission(node, false) == Tristate.UNDEFINED) {
            throw new ObjectLacksException();
        }

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

        synchronized (nodes) {
            nodes.removeIf(e -> e.almostEquals(node));
        }
        invalidateCache(true);

        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeRemove(node, this, before, after);
    }

    /**
     * Unsets a transient permission node
     *
     * @param node the node to unset
     * @throws ObjectLacksException if the holder doesn't have this node already
     */
    public void unsetTransientPermission(Node node) throws ObjectLacksException {
        if (hasPermission(node, true) == Tristate.UNDEFINED) {
            throw new ObjectLacksException();
        }

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

        synchronized (transientNodes) {
            transientNodes.removeIf(e -> e.almostEquals(node));
        }
        invalidateCache(false);

        ImmutableSet<Node> after = ImmutableSet.copyOf(getTransientNodes());
        plugin.getApiProvider().getEventFactory().handleNodeRemove(node, this, before, after);
    }

    public void unsetPermission(String node, boolean temporary) throws ObjectLacksException {
        unsetPermission(buildNode(node).setExpiry(temporary ? 10L : 0L).build());
    }

    public void unsetPermission(String node) throws ObjectLacksException {
        unsetPermission(buildNode(node).build());
    }

    public void unsetPermission(String node, String server) throws ObjectLacksException {
        unsetPermission(buildNode(node).setServer(server).build());
    }

    public void unsetPermission(String node, String server, String world) throws ObjectLacksException {
        unsetPermission(buildNode(node).setServer(server).setWorld(world).build());
    }

    public void unsetPermission(String node, String server, boolean temporary) throws ObjectLacksException {
        unsetPermission(buildNode(node).setServer(server).setExpiry(temporary ? 10L : 0L).build());
    }

    public void unsetPermission(String node, String server, String world, boolean temporary)
            throws ObjectLacksException {
        unsetPermission(buildNode(node).setServer(server).setWorld(world).setExpiry(temporary ? 10L : 0L).build());
    }

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

    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 void setInheritGroup(Group group) throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true);
    }

    public void setInheritGroup(Group group, String server) throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true, server);
    }

    public void setInheritGroup(Group group, String server, String world) throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true, server, world);
    }

    public void setInheritGroup(Group group, long expireAt) throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true, expireAt);
    }

    public void setInheritGroup(Group group, String server, long expireAt) throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true, server, expireAt);
    }

    public void setInheritGroup(Group group, String server, String world, long expireAt)
            throws ObjectAlreadyHasException {
        if (group.getName().equalsIgnoreCase(this.getObjectName())) {
            throw new ObjectAlreadyHasException();
        }

        setPermission("group." + group.getName(), true, server, world, expireAt);
    }

    public void unsetInheritGroup(Group group) throws ObjectLacksException {
        unsetPermission("group." + group.getName());
    }

    public void unsetInheritGroup(Group group, boolean temporary) throws ObjectLacksException {
        unsetPermission("group." + group.getName(), temporary);
    }

    public void unsetInheritGroup(Group group, String server) throws ObjectLacksException {
        unsetPermission("group." + group.getName(), server);
    }

    public void unsetInheritGroup(Group group, String server, String world) throws ObjectLacksException {
        unsetPermission("group." + group.getName(), server, world);
    }

    public void unsetInheritGroup(Group group, String server, boolean temporary) throws ObjectLacksException {
        unsetPermission("group." + group.getName(), server, temporary);
    }

    public void unsetInheritGroup(Group group, String server, String world, boolean temporary)
            throws ObjectLacksException {
        unsetPermission("group." + group.getName(), server, world, temporary);
    }

    /**
     * Clear all of the holders permission nodes
     */
    public void clearNodes() {
        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            nodes.clear();
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearNodes(String server) {
        String finalServer = Optional.ofNullable(server).orElse("global");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            if (!nodes.removeIf(n -> n.getServer().orElse("global").equalsIgnoreCase(finalServer))) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearNodes(String server, String world) {
        String finalServer = Optional.ofNullable(server).orElse("global");
        String finalWorld = Optional.ofNullable(world).orElse("null");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes.removeIf(n -> n.getServer().orElse("global").equalsIgnoreCase(finalServer)
                    && n.getWorld().orElse("null").equalsIgnoreCase(finalWorld));
            if (!b) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearParents() {
        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());

        synchronized (nodes) {
            boolean b = nodes.removeIf(Node::isGroupNode);
            if (!b) {
                return;
            }
        }
        if (this instanceof User) {
            plugin.getUserManager().giveDefaultIfNeeded((User) this, false);
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearParents(String server) {
        String finalServer = Optional.ofNullable(server).orElse("global");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes
                    .removeIf(n -> n.isGroupNode() && n.getServer().orElse("global").equalsIgnoreCase(finalServer));
            if (!b) {
                return;
            }
        }
        if (this instanceof User) {
            plugin.getUserManager().giveDefaultIfNeeded((User) this, false);
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearParents(String server, String world) {
        String finalServer = Optional.ofNullable(server).orElse("global");
        String finalWorld = Optional.ofNullable(world).orElse("null");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes
                    .removeIf(n -> n.isGroupNode() && n.getServer().orElse("global").equalsIgnoreCase(finalServer)
                            && n.getWorld().orElse("null").equalsIgnoreCase(finalWorld));
            if (!b) {
                return;
            }
        }
        if (this instanceof User) {
            plugin.getUserManager().giveDefaultIfNeeded((User) this, false);
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearMeta() {
        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());

        synchronized (nodes) {
            if (!nodes.removeIf(n -> n.isMeta() || n.isPrefix() || n.isSuffix())) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearMeta(String server) {
        String finalServer = Optional.ofNullable(server).orElse("global");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes.removeIf(n -> (n.isMeta() || n.isPrefix() || n.isSuffix())
                    && n.getServer().orElse("global").equalsIgnoreCase(finalServer));
            if (!b) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearMeta(String server, String world) {
        String finalServer = Optional.ofNullable(server).orElse("global");
        String finalWorld = Optional.ofNullable(world).orElse("null");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes.removeIf(n -> (n.isMeta() || n.isPrefix() || n.isSuffix())
                    && (n.getServer().orElse("global").equalsIgnoreCase(finalServer)
                            && n.getWorld().orElse("null").equalsIgnoreCase(finalWorld)));
            if (!b) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearMetaKeys(String key, String server, String world, boolean temp) {
        String finalServer = Optional.ofNullable(server).orElse("global");
        String finalWorld = Optional.ofNullable(world).orElse("null");

        ImmutableSet<Node> before = ImmutableSet.copyOf(getNodes());
        synchronized (nodes) {
            boolean b = nodes.removeIf(
                    n -> n.isMeta() && (n.isTemporary() == temp) && n.getMeta().getKey().equalsIgnoreCase(key)
                            && n.getServer().orElse("global").equalsIgnoreCase(finalServer)
                            && n.getWorld().orElse("null").equalsIgnoreCase(finalWorld));
            if (!b) {
                return;
            }
        }
        invalidateCache(true);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

    public void clearTransientNodes() {
        ImmutableSet<Node> before = ImmutableSet.copyOf(getTransientNodes());

        synchronized (transientNodes) {
            transientNodes.clear();
        }
        invalidateCache(false);
        ImmutableSet<Node> after = ImmutableSet.copyOf(getTransientNodes());
        plugin.getApiProvider().getEventFactory().handleNodeClear(this, before, after);
    }

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

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

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

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

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

    public OptionalInt getWeight() {
        if (this instanceof User)
            return OptionalInt.empty();

        OptionalInt weight = OptionalInt.empty();
        try {
            weight = getNodes().stream().filter(n -> n.getPermission().startsWith("weight."))
                    .map(n -> n.getPermission().substring("weight.".length())).mapToInt(Integer::parseInt).max();
        } catch (Exception ignored) {
        }

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

        return weight;
    }

    /**
     * Get a {@link List} of all of the groups the holder inherits, on all servers
     *
     * @return a {@link List} of group names
     */
    public List<String> getGroupNames() {
        return getNodes().stream().filter(Node::isGroupNode).map(Node::getGroupName).collect(Collectors.toList());
    }

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

    /**
     * Get a {@link List} of the groups the holder inherits on a specific server and world
     *
     * @param server the server to check
     * @param world  the world to check
     * @return a {@link List} of group names
     */
    public List<String> getLocalGroups(String server, String world) {
        return getNodes().stream().filter(Node::isGroupNode).filter(n -> n.shouldApplyOnWorld(world, false, true))
                .filter(n -> n.shouldApplyOnServer(server, false, true)).map(Node::getGroupName)
                .collect(Collectors.toList());
    }

    public List<String> getLocalGroups(String server, String world, boolean includeGlobal) {
        return getNodes().stream().filter(Node::isGroupNode)
                .filter(n -> n.shouldApplyOnWorld(world, includeGlobal, true))
                .filter(n -> n.shouldApplyOnServer(server, includeGlobal, true)).map(Node::getGroupName)
                .collect(Collectors.toList());
    }

    /**
     * Get a {@link List} of the groups the holder inherits on a specific server
     *
     * @param server the server to check
     * @return a {@link List} of group names
     */
    public List<String> getLocalGroups(String server) {
        return getNodes().stream().filter(Node::isGroupNode).filter(n -> n.shouldApplyOnServer(server, false, true))
                .map(Node::getGroupName).collect(Collectors.toList());
    }

    public static Map<String, Boolean> exportToLegacy(Set<Node> nodes) {
        Map<String, Boolean> m = new TreeMap<>((o1, o2) -> PriorityComparator.get().compareStrings(o1, o2));
        for (Node node : nodes) {
            m.put(node.toSerializedNode(), node.getValue());
        }
        return m;
    }

    private static Node.Builder buildNode(String permission) {
        return new NodeBuilder(permission);
    }

    private static ImmutableLocalizedNode makeLocal(Node node, String location) {
        return ImmutableLocalizedNode.of(node, location);
    }

    private static Node makeNode(String s, Boolean b) {
        return NodeFactory.fromSerialisedNode(s, b);
    }
}