diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java index 6787a87cc..8a7ed90d1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java @@ -94,7 +94,7 @@ public class ParentClearTrack extends GenericChildCommand { target.removeIf(DataType.NORMAL, context.isEmpty() ? null : context, NodeType.INHERITANCE.predicate(n -> track.containsGroup(n.getGroupName())), false); if (target.getType() == HolderType.USER) { - plugin.getUserManager().giveDefaultIfNeeded(((User) target), false); + plugin.getUserManager().giveDefaultIfNeeded(((User) target)); } int changed = before - target.normalData().size(); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java index 8727fd716..ced77c54c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java @@ -101,7 +101,7 @@ public class ParentRemove extends GenericChildCommand { .build().submit(plugin, sender); if (target.getType() == HolderType.USER) { - plugin.getUserManager().giveDefaultIfNeeded(((User) target), false); + plugin.getUserManager().giveDefaultIfNeeded(((User) target)); } StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java index 10c41fee3..c58a13011 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java @@ -84,7 +84,7 @@ public class GroupListMembers extends ChildCommand { if (target.getName().equals(GroupManager.DEFAULT_GROUP_NAME)) { // include all non-saved online players in the results for (User user : plugin.getUserManager().getAll().values()) { - if (!plugin.getUserManager().shouldSave(user)) { + if (!plugin.getUserManager().isNonDefaultUser(user)) { matchedUsers.add(NodeEntry.of(user.getUniqueId(), node)); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java deleted file mode 100644 index 165e93e99..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * 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 com.google.common.collect.ImmutableSet; - -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextSetComparator; -import me.lucko.luckperms.common.node.comparator.NodeComparator; -import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; - -import net.luckperms.api.context.ContextSatisfyMode; -import net.luckperms.api.context.ContextSet; -import net.luckperms.api.context.DefaultContextKeys; -import net.luckperms.api.context.ImmutableContextSet; -import net.luckperms.api.node.Node; -import net.luckperms.api.node.NodeEqualityPredicate; -import net.luckperms.api.node.NodeType; -import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; -import net.luckperms.api.node.types.InheritanceNode; -import net.luckperms.api.query.Flag; -import net.luckperms.api.query.QueryOptions; - -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * A map of nodes held by a {@link PermissionHolder}. - * - *

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}.

- * - *

Each holder has two of these maps, one for enduring and transient nodes.

- */ -public final class NodeMap { - private static final Function> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); - private static final Function> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); - - /** - * The holder which this map is for - */ - private final PermissionHolder holder; - - /** - * The backing data map. - * - *

Nodes are mapped by the result of {@link Node#getContexts()}, 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}.

- */ - private final SortedMap> map = new ConcurrentSkipListMap<>(ContextSetComparator.reverse()); - - /** - * Copy of {@link #map} which only contains group nodes - * @see InheritanceNode - */ - private final SortedMap> inheritanceMap = new ConcurrentSkipListMap<>(ContextSetComparator.reverse()); - - NodeMap(PermissionHolder holder) { - this.holder = holder; - } - - public boolean isEmpty() { - return this.map.isEmpty(); - } - - public int size() { - int size = 0; - for (SortedSet values : this.map.values()) { - size += values.size(); - } - return size; - } - - public List asList() { - List list = new ArrayList<>(); - copyTo(list); - return list; - } - - public LinkedHashSet asSet() { - LinkedHashSet set = new LinkedHashSet<>(); - copyTo(set); - return set; - } - - public SortedSet asSortedSet() { - SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); - copyTo(set); - return set; - } - - public ImmutableSet asImmutableSet() { - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (SortedSet values : this.map.values()) { - builder.addAll(values); - } - return builder.build(); - } - - public Map> asMap() { - Map> map = new HashMap<>(); - for (Map.Entry> e : this.map.entrySet()) { - map.put(e.getKey(), new ArrayList<>(e.getValue())); - } - return map; - } - - public List inheritanceAsList() { - List set = new ArrayList<>(); - copyInheritanceNodesTo(set); - return set; - } - - public LinkedHashSet inheritanceAsSet() { - LinkedHashSet set = new LinkedHashSet<>(); - copyInheritanceNodesTo(set); - return set; - } - - public SortedSet inheritanceAsSortedSet() { - SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); - copyInheritanceNodesTo(set); - return set; - } - - public Map> inheritanceAsMap() { - Map> map = new HashMap<>(); - for (Map.Entry> e : this.inheritanceMap.entrySet()) { - map.put(e.getKey(), new ArrayList<>(e.getValue())); - } - return map; - } - - private static boolean flagExcludeTest(Flag flag, String contextKey, QueryOptions filter, ImmutableContextSet contextSet) { - // return true (negative result) if the explicit *include* flag is not set, and if the context set doesn't contain the required context key. - return !filter.flag(flag) && !contextSet.containsKey(contextKey); - } - - private static boolean normalNodesExcludeTest(QueryOptions filter, ImmutableContextSet contextSet) { - // return true (negative result) if normal nodes should not be included due to the lack of a server/world context. - return flagExcludeTest(Flag.INCLUDE_NODES_WITHOUT_SERVER_CONTEXT, DefaultContextKeys.SERVER_KEY, filter, contextSet) || - flagExcludeTest(Flag.INCLUDE_NODES_WITHOUT_WORLD_CONTEXT, DefaultContextKeys.WORLD_KEY, filter, contextSet); - } - - private static boolean inheritanceNodesIncludeTest(QueryOptions filter, ImmutableContextSet contextSet) { - // return true (positive result) if inheritance nodes should be included, due to the lack of any flags preventing their inclusion. - return !flagExcludeTest(Flag.APPLY_INHERITANCE_NODES_WITHOUT_SERVER_CONTEXT, DefaultContextKeys.SERVER_KEY, filter, contextSet) && - !flagExcludeTest(Flag.APPLY_INHERITANCE_NODES_WITHOUT_WORLD_CONTEXT, DefaultContextKeys.WORLD_KEY, filter, contextSet); - } - - private ContextSatisfyMode defaultSatisfyMode() { - return this.holder.getPlugin().getConfiguration().get(ConfigKeys.CONTEXT_SATISFY_MODE); - } - - public void forEach(Consumer consumer) { - for (SortedSet values : this.map.values()) { - values.forEach(consumer); - } - } - - public void forEach(QueryOptions filter, Consumer consumer) { - for (Map.Entry> e : this.map.entrySet()) { - if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { - continue; - } - - if (normalNodesExcludeTest(filter, e.getKey())) { - if (inheritanceNodesIncludeTest(filter, e.getKey())) { - // only copy inheritance nodes. - SortedSet inheritanceNodes = this.inheritanceMap.get(e.getKey()); - if (inheritanceNodes != null) { - inheritanceNodes.forEach(consumer); - } - } - } else { - e.getValue().forEach(consumer); - } - } - } - - public void copyTo(Collection collection) { - for (SortedSet values : this.map.values()) { - collection.addAll(values); - } - } - - public void copyTo(Collection collection, QueryOptions filter) { - for (Map.Entry> e : this.map.entrySet()) { - if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { - continue; - } - - if (normalNodesExcludeTest(filter, e.getKey())) { - if (inheritanceNodesIncludeTest(filter, e.getKey())) { - // only copy inheritance nodes. - SortedSet inheritanceNodes = this.inheritanceMap.get(e.getKey()); - if (inheritanceNodes != null) { - collection.addAll(inheritanceNodes); - } - } - } else { - collection.addAll(e.getValue()); - } - } - } - - public void copyTo(Collection collection, NodeType type, QueryOptions filter) { - for (Map.Entry> e : this.map.entrySet()) { - if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { - continue; - } - - if (normalNodesExcludeTest(filter, e.getKey())) { - if (inheritanceNodesIncludeTest(filter, e.getKey())) { - // only copy inheritance nodes. - if (type == NodeType.INHERITANCE) { - SortedSet inheritanceNodes = this.inheritanceMap.get(e.getKey()); - if (inheritanceNodes != null) { - for (InheritanceNode node : inheritanceNodes) { - collection.add(type.cast(node)); - } - } - } - } - } else { - for (Node node : e.getValue()) { - if (type.matches(node)) { - collection.add(type.cast(node)); - } - } - } - } - } - - public void copyInheritanceNodesTo(Collection collection) { - for (SortedSet values : this.inheritanceMap.values()) { - collection.addAll(values); - } - } - - public void copyInheritanceNodesTo(Collection collection, QueryOptions filter) { - for (Map.Entry> e : this.inheritanceMap.entrySet()) { - if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { - continue; - } - - if (inheritanceNodesIncludeTest(filter, e.getKey())) { - collection.addAll(e.getValue()); - } - } - } - - public Collection nodesInContext(ContextSet context) { - final SortedSet values = this.map.get(context.immutableCopy()); - if (values == null) { - return ImmutableSet.of(); - } - return new ArrayList<>(values); - } - - public Collection inheritanceNodesInContext(ContextSet context) { - final SortedSet values = this.inheritanceMap.get(context.immutableCopy()); - if (values == null) { - return ImmutableSet.of(); - } - return new ArrayList<>(values); - } - - private Node localise(Node node) { - Optional metadata = node.getMetadata(InheritanceOriginMetadata.KEY); - if (metadata.isPresent() && metadata.get().getOrigin().equals(this.holder.getIdentifier())) { - return node; - } - - return node.toBuilder().withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin(this.holder.getIdentifier())).build(); - } - - void add(Node node) { - ImmutableContextSet context = node.getContexts(); - Node n = localise(node); - - SortedSet nodesInContext = this.map.computeIfAbsent(context, VALUE_SET_SUPPLIER); - nodesInContext.removeIf(e -> e.equals(node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); - nodesInContext.add(n); - - if (n instanceof InheritanceNode) { - SortedSet inheritanceNodesInContext = this.inheritanceMap.computeIfAbsent(context, INHERITANCE_VALUE_SET_SUPPLIER); - inheritanceNodesInContext.removeIf(e -> e.equals(node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); - if (n.getValue()) { - inheritanceNodesInContext.add((InheritanceNode) n); - } - } - } - - void remove(Node node) { - ImmutableContextSet context = node.getContexts(); - SortedSet nodesInContext = this.map.get(context); - if (nodesInContext != null) { - nodesInContext.removeIf(e -> e.equals(node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); - } - - if (node instanceof InheritanceNode) { - SortedSet inheritanceNodesInContext = this.inheritanceMap.get(context); - if (inheritanceNodesInContext != null) { - inheritanceNodesInContext.removeIf(e -> e.equals(node, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); - } - } - } - - private void removeExact(Node node) { - ImmutableContextSet context = node.getContexts(); - SortedSet nodesInContext = this.map.get(context); - if (nodesInContext != null) { - nodesInContext.remove(node); - } - - if (node instanceof InheritanceNode && node.getValue()) { - SortedSet inheritanceNodesInContext = this.inheritanceMap.get(context); - if (inheritanceNodesInContext != null) { - inheritanceNodesInContext.remove(node); - } - } - } - - void replace(Node node, Node previous) { - removeExact(previous); - add(node); - } - - void clear() { - this.map.clear(); - this.inheritanceMap.clear(); - } - - void clear(ContextSet contextSet) { - ImmutableContextSet context = contextSet.immutableCopy(); - this.map.remove(context); - this.inheritanceMap.remove(context); - } - - void setContent(Iterable set) { - this.map.clear(); - this.inheritanceMap.clear(); - mergeContent(set); - } - - void setContent(Stream stream) { - this.map.clear(); - this.inheritanceMap.clear(); - mergeContent(stream); - } - - void mergeContent(Iterable set) { - for (Node n : set) { - add(n); - } - } - - void mergeContent(Stream stream) { - stream.forEach(this::add); - } - - boolean removeIf(Predicate predicate) { - boolean success = false; - for (SortedSet valueSet : this.map.values()) { - if (valueSet.removeIf(predicate)) { - success = true; - } - } - for (SortedSet valueSet : this.inheritanceMap.values()) { - valueSet.removeIf(predicate); - } - return success; - } - - boolean removeIf(ContextSet contextSet, Predicate predicate) { - ImmutableContextSet context = contextSet.immutableCopy(); - - boolean success = false; - - SortedSet nodesInContext = this.map.get(context); - if (nodesInContext != null) { - success = nodesInContext.removeIf(predicate); - } - - SortedSet inheritanceNodesInContext = this.inheritanceMap.get(context); - if (inheritanceNodesInContext != null) { - inheritanceNodesInContext.removeIf(predicate); - } - - return success; - } - - boolean auditTemporaryNodes(@Nullable Set removed) { - boolean work = false; - - for (SortedSet valueSet : this.map.values()) { - Iterator it = valueSet.iterator(); - while (it.hasNext()) { - Node entry = it.next(); - if (!entry.hasExpired()) { - continue; - } - - // remove - if (removed != null) { - removed.add(entry); - } - if (entry instanceof InheritanceNode && entry.getValue()) { - SortedSet inheritanceNodesInContext = this.inheritanceMap.get(entry.getContexts()); - if (inheritanceNodesInContext != null) { - inheritanceNodesInContext.remove(entry); - } - } - it.remove(); - work = true; - } - } - - return work; - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java index 61d642b9e..0f0836cc8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java @@ -32,6 +32,10 @@ import me.lucko.luckperms.common.cacheddata.HolderCachedDataManager; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.inheritance.InheritanceComparator; import me.lucko.luckperms.common.inheritance.InheritanceGraph; +import me.lucko.luckperms.common.model.nodemap.MutateResult; +import me.lucko.luckperms.common.model.nodemap.NodeMap; +import me.lucko.luckperms.common.model.nodemap.NodeMapMutable; +import me.lucko.luckperms.common.model.nodemap.RecordedNodeMap; import me.lucko.luckperms.common.node.NodeEquality; import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; @@ -59,18 +63,13 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.OptionalInt; -import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.IntFunction; import java.util.function.Predicate; -import java.util.stream.Stream; /** * Represents an object that can hold permissions, (a user or group) @@ -112,7 +111,7 @@ public abstract class PermissionHolder { * * @see #normalData() */ - private final NodeMap normalNodes = new NodeMap(this); + private final RecordedNodeMap normalNodes = new RecordedNodeMap(new NodeMapMutable(this)); /** * The holders transient nodes. @@ -124,13 +123,7 @@ public abstract class PermissionHolder { * * @see #transientData() */ - private final NodeMap transientNodes = new NodeMap(this); - - /** - * Lock used by Storage implementations to prevent concurrent read/writes - * @see #getIoLock() - */ - private final Lock ioLock = new ReentrantLock(); + private final NodeMap transientNodes = new NodeMapMutable(this); /** * Comparator used to ordering groups when calculating inheritance @@ -152,10 +145,6 @@ public abstract class PermissionHolder { return this.plugin; } - public Lock getIoLock() { - return this.ioLock; - } - public Comparator getInheritanceComparator() { return this.inheritanceComparator; } @@ -171,7 +160,7 @@ public abstract class PermissionHolder { } } - public NodeMap normalData() { + public RecordedNodeMap normalData() { return this.normalNodes; } @@ -237,18 +226,20 @@ public abstract class PermissionHolder { getPlugin().getEventDispatcher().dispatchDataRecalculate(this); } + public void loadNodesFromStorage(Iterable set) { + // TODO: should we attempt to "replay" existing changes on top of the new data? + normalData().discardChanges(); + normalData().bypass().setContent(set); + invalidateCache(); + } + public void setNodes(DataType type, Iterable set) { getData(type).setContent(set); invalidateCache(); } - public void setNodes(DataType type, Stream stream) { - getData(type).setContent(stream); - invalidateCache(); - } - public void mergeNodes(DataType type, Iterable set) { - getData(type).mergeContent(set); + getData(type).addAll(set); invalidateCache(); } @@ -436,20 +427,19 @@ public abstract class PermissionHolder { private boolean auditTemporaryNodes(DataType dataType) { ImmutableSet before = getData(dataType).asImmutableSet(); - Set removed = new HashSet<>(); - boolean work = getData(dataType).auditTemporaryNodes(removed); - if (work) { + MutateResult result = getData(dataType).removeIf(Node::hasExpired); + if (!result.isEmpty()) { // call event ImmutableSet after = getData(dataType).asImmutableSet(); - for (Node r : removed) { + for (Node r : result.getRemoved()) { this.plugin.getEventDispatcher().dispatchNodeRemove(r, this, dataType, before, after); } // invalidate invalidateCache(); } - return work; + return !result.isEmpty(); } public Tristate hasNode(DataType type, Node node, NodeEqualityPredicate equalityPredicate) { @@ -522,7 +512,7 @@ public abstract class PermissionHolder { if (newNode != null) { // Remove the old Node & add the new one. ImmutableSet before = data.asImmutableSet(); - data.replace(newNode, otherMatch); + data.removeThenAdd(otherMatch, newNode); ImmutableSet after = data.asImmutableSet(); this.plugin.getEventDispatcher().dispatchNodeAdd(newNode, this, dataType, before, after); @@ -572,7 +562,7 @@ public abstract class PermissionHolder { // Remove the old Node & add the new one. ImmutableSet before = data.asImmutableSet(); - data.replace(newNode, otherMatch); + data.removeThenAdd(otherMatch, newNode); ImmutableSet after = data.asImmutableSet(); this.plugin.getEventDispatcher().dispatchNodeRemove(otherMatch, this, dataType, before, after); @@ -594,17 +584,17 @@ public abstract class PermissionHolder { ImmutableSet before = data.asImmutableSet(); if (contextSet == null) { - if (!data.removeIf(predicate)) { + if (data.removeIf(predicate).isEmpty()) { return false; } } else { - if (!data.removeIf(contextSet, predicate)) { + if (data.removeIf(contextSet, predicate).isEmpty()) { return false; } } if (getType() == HolderType.USER && giveDefault) { - getPlugin().getUserManager().giveDefaultIfNeeded((User) this, false); + getPlugin().getUserManager().giveDefaultIfNeeded((User) this); } ImmutableSet after = data.asImmutableSet(); @@ -626,7 +616,7 @@ public abstract class PermissionHolder { } if (getType() == HolderType.USER && giveDefault) { - getPlugin().getUserManager().giveDefaultIfNeeded((User) this, false); + getPlugin().getUserManager().giveDefaultIfNeeded((User) this); } ImmutableSet after = data.asImmutableSet(); diff --git a/common/src/main/java/me/lucko/luckperms/common/model/Track.java b/common/src/main/java/me/lucko/luckperms/common/model/Track.java index a9a894e09..5683bcdc9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/Track.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/Track.java @@ -47,8 +47,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -61,8 +59,6 @@ public final class Track { private final LuckPermsPlugin plugin; - private final Lock ioLock = new ReentrantLock(); - /** * The groups within this track */ @@ -79,10 +75,6 @@ public final class Track { return this.name; } - public Lock getIoLock() { - return this.ioLock; - } - public ApiTrack getApiProxy() { return this.apiProxy; } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java index f32c76a58..c25c76798 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java @@ -79,8 +79,8 @@ public abstract class AbstractUserManager extends AbstractManage } @Override - public boolean giveDefaultIfNeeded(User user, boolean save) { - boolean work = false; + public boolean giveDefaultIfNeeded(User user) { + boolean requireSave = false; Collection globalGroups = user.normalData().inheritanceNodesInContext(ImmutableContextSetImpl.EMPTY); @@ -106,7 +106,7 @@ public abstract class AbstractUserManager extends AbstractManage // if the group is null, it'll be resolved in the next step if (group != null) { user.getPrimaryGroup().setStoredValue(group); - work = true; + requireSave = true; } } } @@ -120,14 +120,39 @@ public abstract class AbstractUserManager extends AbstractManage if (!hasGroup) { user.getPrimaryGroup().setStoredValue(GroupManager.DEFAULT_GROUP_NAME); user.setNode(DataType.NORMAL, Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build(), false); - work = true; + requireSave = true; } - if (work && save) { - this.plugin.getStorage().saveUser(user); + return requireSave; + } + + @Override + public boolean isNonDefaultUser(User user) { + if (user.normalData().size() != 1) { + return true; } - return work; + List nodes = user.normalData().asList(); + if (nodes.size() != 1) { + return true; + } + + Node onlyNode = nodes.iterator().next(); + if (!isDefaultNode(onlyNode)) { + return true; + } + + // Not in the default primary group + return !user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME).equalsIgnoreCase(GroupManager.DEFAULT_GROUP_NAME); + } + + @Override + public boolean isDefaultNode(Node node) { + return node instanceof InheritanceNode && + node.getValue() && + !node.hasExpiry() && + node.getContexts().isEmpty() && + ((InheritanceNode) node).getGroupName().equalsIgnoreCase(GroupManager.DEFAULT_GROUP_NAME); } @Override @@ -157,39 +182,4 @@ public abstract class AbstractUserManager extends AbstractManage getAll().values().forEach(u -> u.getCachedData().invalidatePermissionCalculators()); } - /** - * Check whether the user's state indicates that they should be persisted to storage. - * - * @param user the user to check - * @return true if the user should be saved - */ - @Override - public boolean shouldSave(User user) { - if (user.normalData().size() != 1) { - return true; - } - - List nodes = user.normalData().asList(); - if (nodes.size() != 1) { - return true; - } - - Node onlyNode = nodes.iterator().next(); - if (!(onlyNode instanceof InheritanceNode)) { - return true; - } - - if (onlyNode.hasExpiry() || !onlyNode.getContexts().isEmpty()) { - return true; - } - - if (!((InheritanceNode) onlyNode).getGroupName().equalsIgnoreCase(GroupManager.DEFAULT_GROUP_NAME)) { - // The user's only node is not the default group one. - return true; - } - - - // Not in the default primary group - return !user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME).equalsIgnoreCase(GroupManager.DEFAULT_GROUP_NAME); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java index 8b9c02c44..7b64a2561 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java @@ -29,6 +29,8 @@ import me.lucko.luckperms.common.calculator.PermissionCalculator; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.Manager; +import net.luckperms.api.node.Node; + import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -49,7 +51,7 @@ public interface UserManager extends Manager { * * @param user the user to give to */ - boolean giveDefaultIfNeeded(User user, boolean save); + boolean giveDefaultIfNeeded(User user); /** * Check whether the user's state indicates that they should be persisted to storage. @@ -57,7 +59,15 @@ public interface UserManager extends Manager { * @param user the user to check * @return true if the user should be saved */ - boolean shouldSave(User user); + boolean isNonDefaultUser(User user); + + /** + * Gets whether the given node is a default node given by {@link #giveDefaultIfNeeded(User)}. + * + * @param node the node + * @return true if it is the default node + */ + boolean isDefaultNode(Node node); /** * Gets the instance responsible for unloading unneeded users. diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java new file mode 100644 index 000000000..aeb554c1f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java @@ -0,0 +1,152 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.nodemap; + +import net.luckperms.api.node.Node; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Records a log of the changes that occur as a result of a {@link NodeMap} mutation(s). + */ +public class MutateResult { + private final LinkedHashSet changes = new LinkedHashSet<>(); + + public Set getChanges() { + return this.changes; + } + + public Set getChanges(ChangeType type) { + Set changes = new LinkedHashSet<>(this.changes.size()); + for (Change change : this.changes) { + if (change.getType() == type) { + changes.add(change.getNode()); + } + } + return changes; + } + + void clear() { + this.changes.clear(); + } + + public boolean isEmpty() { + return this.changes.isEmpty(); + } + + public Set getAdded() { + return getChanges(ChangeType.ADD); + } + + public Set getRemoved() { + return getChanges(ChangeType.REMOVE); + } + + private void recordChange(Change change) { + // This method is the magic of this class. + // When tracking, we want to ignore changes that cancel each other out, and only + // keep track of the net difference. + // e.g. adding then removing the same node = zero net change, so ignore it. + + if (this.changes.remove(change.inverse())) { + return; + } + this.changes.add(change); + } + + public void recordChange(ChangeType type, Node node) { + recordChange(new Change(type, node)); + } + + public void recordChanges(ChangeType type, Iterable nodes) { + for (Node node : nodes) { + recordChange(new Change(type, node)); + } + } + + public MutateResult mergeFrom(MutateResult other) { + for (Change change : other.changes) { + recordChange(change); + } + return this; + } + + @Override + public String toString() { + return "MutateResult{changes=" + this.changes + '}'; + } + + public static final class Change { + private final ChangeType type; + private final Node node; + + public Change(ChangeType type, Node node) { + this.type = type; + this.node = node; + } + + public ChangeType getType() { + return this.type; + } + + public Node getNode() { + return this.node; + } + + public Change inverse() { + return new Change(this.type.inverse(), this.node); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Change change = (Change) o; + return this.type == change.type && this.node.equals(change.node); + } + + @Override + public int hashCode() { + return Objects.hash(this.type, this.node); + } + + @Override + public String toString() { + return "Change{type=" + this.type + ", node=" + this.node + '}'; + } + } + + public enum ChangeType { + ADD, REMOVE; + + public ChangeType inverse() { + return this == ADD ? REMOVE : ADD; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java new file mode 100644 index 000000000..117045cac --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java @@ -0,0 +1,161 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.nodemap; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; + +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; + +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * A map of nodes held by a {@link PermissionHolder}. + */ +public interface NodeMap { + + boolean isEmpty(); + + int size(); + + default List asList() { + List list = new ArrayList<>(); + copyTo(list); + return list; + } + + default LinkedHashSet asSet() { + LinkedHashSet set = new LinkedHashSet<>(); + copyTo(set); + return set; + } + + default SortedSet asSortedSet() { + SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); + copyTo(set); + return set; + } + + default ImmutableSet asImmutableSet() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + copyTo(builder); + return builder.build(); + } + + Map> asMap(); + + default List inheritanceAsList() { + List set = new ArrayList<>(); + copyInheritanceNodesTo(set); + return set; + } + + default LinkedHashSet inheritanceAsSet() { + LinkedHashSet set = new LinkedHashSet<>(); + copyInheritanceNodesTo(set); + return set; + } + + default SortedSet inheritanceAsSortedSet() { + SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); + copyInheritanceNodesTo(set); + return set; + } + + default ImmutableSet inheritanceAsImmutableSet() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + copyInheritanceNodesTo(builder); + return builder.build(); + } + + Map> inheritanceAsMap(); + + void forEach(Consumer consumer); + + void forEach(QueryOptions filter, Consumer consumer); + + void copyTo(Collection collection); + + void copyTo(ImmutableCollection.Builder collection); + + void copyTo(Collection collection, QueryOptions filter); + + void copyTo(Collection collection, NodeType type, QueryOptions filter); + + void copyInheritanceNodesTo(Collection collection); + + void copyInheritanceNodesTo(ImmutableCollection.Builder collection); + + void copyInheritanceNodesTo(Collection collection, QueryOptions filter); + + Collection nodesInContext(ContextSet context); + + Collection inheritanceNodesInContext(ContextSet context); + + // mutate methods + + MutateResult add(Node nodeWithoutInheritanceOrigin); + + MutateResult remove(Node node); + + MutateResult removeExact(Node node); + + MutateResult removeIf(Predicate predicate); + + MutateResult removeIf(ContextSet contextSet, Predicate predicate); + + MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd); + + MutateResult clear(); + + MutateResult clear(ContextSet contextSet); + + MutateResult setContent(Iterable set); + + MutateResult setContent(Stream stream); + + MutateResult addAll(Iterable set); + + MutateResult addAll(Stream stream); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java new file mode 100644 index 000000000..3274e8a01 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java @@ -0,0 +1,243 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.nodemap; + +import com.google.common.collect.ImmutableCollection; + +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.Flag; +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.function.Consumer; + +/** + * Base implementation of {@link NodeMap} query methods. + */ +abstract class NodeMapBase implements NodeMap { + + NodeMapBase() { + + } + + protected abstract SortedMap> map(); + + protected abstract SortedMap> inheritanceMap(); + + protected abstract ContextSatisfyMode defaultSatisfyMode(); + + @Override + public boolean isEmpty() { + return map().isEmpty(); + } + + @Override + public int size() { + int size = 0; + for (SortedSet values : map().values()) { + size += values.size(); + } + return size; + } + + @Override + public Map> asMap() { + Map> map = new HashMap<>(); + for (Map.Entry> e : map().entrySet()) { + map.put(e.getKey(), new ArrayList<>(e.getValue())); + } + return map; + } + + @Override + public Map> inheritanceAsMap() { + Map> map = new HashMap<>(); + for (Map.Entry> e : inheritanceMap().entrySet()) { + map.put(e.getKey(), new ArrayList<>(e.getValue())); + } + return map; + } + + @Override + public void forEach(Consumer consumer) { + for (SortedSet values : map().values()) { + values.forEach(consumer); + } + } + + @Override + public void forEach(QueryOptions filter, Consumer consumer) { + for (Map.Entry> e : map().entrySet()) { + if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { + continue; + } + + if (normalNodesExcludeTest(filter, e.getKey())) { + if (inheritanceNodesIncludeTest(filter, e.getKey())) { + SortedSet inheritanceNodes = inheritanceMap().get(e.getKey()); + if (inheritanceNodes != null) { + inheritanceNodes.forEach(consumer); + } + } + } else { + e.getValue().forEach(consumer); + } + } + } + + @Override + public void copyTo(Collection collection) { + for (SortedSet values : map().values()) { + collection.addAll(values); + } + } + + @Override + public void copyTo(ImmutableCollection.Builder collection) { + for (SortedSet values : map().values()) { + collection.addAll(values); + } + } + + @Override + public void copyTo(Collection collection, QueryOptions filter) { + for (Map.Entry> e : map().entrySet()) { + if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { + continue; + } + + if (normalNodesExcludeTest(filter, e.getKey())) { + if (inheritanceNodesIncludeTest(filter, e.getKey())) { + SortedSet inheritanceNodes = inheritanceMap().get(e.getKey()); + if (inheritanceNodes != null) { + collection.addAll(inheritanceNodes); + } + } + } else { + collection.addAll(e.getValue()); + } + } + } + + @Override + public void copyTo(Collection collection, NodeType type, QueryOptions filter) { + if (type == NodeType.INHERITANCE) { + //noinspection unchecked + copyInheritanceNodesTo((Collection) collection, filter); + return; + } + + for (Map.Entry> e : map().entrySet()) { + if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { + continue; + } + + if (normalNodesExcludeTest(filter, e.getKey())) { + continue; + } + + for (Node node : e.getValue()) { + if (type.matches(node)) { + collection.add(type.cast(node)); + } + } + } + } + + @Override + public void copyInheritanceNodesTo(Collection collection) { + for (SortedSet values : inheritanceMap().values()) { + collection.addAll(values); + } + } + + @Override + public void copyInheritanceNodesTo(ImmutableCollection.Builder collection) { + for (SortedSet values : inheritanceMap().values()) { + collection.addAll(values); + } + } + + @Override + public void copyInheritanceNodesTo(Collection collection, QueryOptions filter) { + for (Map.Entry> e : inheritanceMap().entrySet()) { + if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) { + continue; + } + + if (inheritanceNodesIncludeTest(filter, e.getKey())) { + collection.addAll(e.getValue()); + } + } + } + + @Override + public Collection nodesInContext(ContextSet context) { + return copy(map().get(context.immutableCopy())); + } + + @Override + public Collection inheritanceNodesInContext(ContextSet context) { + return copy(inheritanceMap().get(context.immutableCopy())); + } + + private static Collection copy(Collection collection) { + if (collection == null) { + return Collections.emptySet(); + } + return new ArrayList<>(collection); + } + + private static boolean flagExcludeTest(Flag flag, String contextKey, QueryOptions filter, ImmutableContextSet contextSet) { + // return true (negative result) if the explicit *include* flag is not set, and if the context set doesn't contain the required context key. + return !filter.flag(flag) && !contextSet.containsKey(contextKey); + } + + private static boolean normalNodesExcludeTest(QueryOptions filter, ImmutableContextSet contextSet) { + // return true (negative result) if normal nodes should not be included due to the lack of a server/world context. + return flagExcludeTest(Flag.INCLUDE_NODES_WITHOUT_SERVER_CONTEXT, DefaultContextKeys.SERVER_KEY, filter, contextSet) || + flagExcludeTest(Flag.INCLUDE_NODES_WITHOUT_WORLD_CONTEXT, DefaultContextKeys.WORLD_KEY, filter, contextSet); + } + + private static boolean inheritanceNodesIncludeTest(QueryOptions filter, ImmutableContextSet contextSet) { + // return true (positive result) if inheritance nodes should be included, due to the lack of any flags preventing their inclusion. + return !flagExcludeTest(Flag.APPLY_INHERITANCE_NODES_WITHOUT_SERVER_CONTEXT, DefaultContextKeys.SERVER_KEY, filter, contextSet) && + !flagExcludeTest(Flag.APPLY_INHERITANCE_NODES_WITHOUT_WORLD_CONTEXT, DefaultContextKeys.WORLD_KEY, filter, contextSet); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java new file mode 100644 index 000000000..829fd6633 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java @@ -0,0 +1,404 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.nodemap; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ContextSetComparator; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.nodemap.MutateResult.ChangeType; +import me.lucko.luckperms.common.node.comparator.NodeComparator; + +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeEqualityPredicate; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import net.luckperms.api.node.types.InheritanceNode; + +import java.util.Iterator; +import java.util.Optional; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public class NodeMapMutable extends NodeMapBase { + + // Used in calls to Map#computeIfAbsent to make them behave like a LoadingMap/Cache + // The key (ImmutableContextSet) isn't actually used - these are more like suppliers than functions + private static final Function> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); + private static final Function> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); + + // Creates the Map instances used by this.map and this.inheritanceMap + private static SortedMap> createMap() { + return new ConcurrentSkipListMap<>(ContextSetComparator.reverse()); + } + + /* + * Nodes are inserted into the maps using Node#getContexts() as the key. + * The context set keys are ordered according to the rules of ContextSetComparator. + * The node values are ordered according to the priority rules defined in NodeComparator. + * + * We use our own "multimap"-like implementation here because guava's is not thread safe. + * + * The map fields aren't final because they are replaced when large updates (e.g. clear) + * are performed. We do this so there's no risk that the read methods will see an inconsistent + * state in the middle of an update from the DB. (see below comment about locking - we don't + * lock for reads!) + */ + private SortedMap> map = createMap(); + private SortedMap> inheritanceMap = createMap(); + + /** + * This lock is used whilst performing mutations, but *not* reads. + * + * The maps themselves are thread safe, so for querying, we just allow + * the read methods to do whatever they want without any locking. + * However, we want mutations to be atomic, so we use the lock to ensure that happens. + */ + private final Lock lock = new ReentrantLock(); + + protected final PermissionHolder holder; + + public NodeMapMutable(PermissionHolder holder) { + this.holder = holder; + } + + @Override + protected SortedMap> map() { + return this.map; + } + + @Override + protected SortedMap> inheritanceMap() { + return this.inheritanceMap; + } + + @Override + protected ContextSatisfyMode defaultSatisfyMode() { + return this.holder.getPlugin().getConfiguration().get(ConfigKeys.CONTEXT_SATISFY_MODE); + } + + private Node addInheritanceOrigin(Node node) { + Optional metadata = node.getMetadata(InheritanceOriginMetadata.KEY); + if (metadata.isPresent() && metadata.get().getOrigin().equals(this.holder.getIdentifier())) { + return node; + } + + return node.toBuilder().withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin(this.holder.getIdentifier())).build(); + } + + @Override + public MutateResult add(Node nodeWithoutInheritanceOrigin) { + Node node = addInheritanceOrigin(nodeWithoutInheritanceOrigin); + + ImmutableContextSet context = node.getContexts(); + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + SortedSet nodes = this.map.computeIfAbsent(context, VALUE_SET_SUPPLIER); + + // add the new node to the set - if it was already there, return + if (!nodes.add(node)) { + return result; + } + + // mark that we added the node in the results + result.recordChange(ChangeType.ADD, node); + + // remove any others that were in the set already with a different value/expiry time + removeMatching(nodes.iterator(), node, result); + + // update the inheritanceMap too if necessary + if (node instanceof InheritanceNode) { + SortedSet inhNodes = this.inheritanceMap.computeIfAbsent(context, INHERITANCE_VALUE_SET_SUPPLIER); + // remove existing.. + inhNodes.removeIf(el -> node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); + // .. & add + if (node.getValue()) { + inhNodes.add((InheritanceNode) node); + } + } + + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult remove(Node node) { + ImmutableContextSet context = node.getContexts(); + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + SortedSet nodes = this.map.get(context); + if (nodes == null) { + return result; + } + + // remove any nodes that match, record to results + removeMatching(nodes.iterator(), node, result); + + // update inheritance map too + if (node instanceof InheritanceNode) { + SortedSet inhNodes = this.inheritanceMap.get(context); + if (inhNodes != null) { + inhNodes.removeIf(el -> node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)); + } + } + + } finally { + this.lock.unlock(); + } + + return result; + } + + private static void removeMatching(Iterator it, Node node, MutateResult result) { + while (it.hasNext()) { + Node el = it.next(); + if (el != node && node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)) { + it.remove(); + result.recordChange(ChangeType.REMOVE, el); + } + } + } + + @Override + public MutateResult removeExact(Node node) { + ImmutableContextSet context = node.getContexts(); + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + SortedSet nodes = this.map.get(context); + if (nodes == null) { + return result; + } + + // try to remove an exact match + if (nodes.remove(node)) { + // if we removed something, record to results + result.recordChange(ChangeType.REMOVE, node); + + // update inheritance map too if necessary + if (node instanceof InheritanceNode && node.getValue()) { + SortedSet inhNodes = this.inheritanceMap.get(context); + if (inhNodes != null) { + inhNodes.remove(node); + } + } + } + + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult removeIf(Predicate predicate) { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + for (SortedSet nodes : this.map.values()) { + removeMatching(nodes.iterator(), predicate, result); + } + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult removeIf(ContextSet contextSet, Predicate predicate) { + ImmutableContextSet context = contextSet.immutableCopy(); + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + SortedSet nodes = this.map.get(context); + if (nodes == null) { + return result; + } + removeMatching(nodes.iterator(), predicate, result); + } finally { + this.lock.unlock(); + } + + return result; + } + + private void removeMatching(Iterator it, Predicate predicate, MutateResult result) { + while (it.hasNext()) { + Node node = it.next(); + + // if the predicate passes, remove the node from the set & record to results + if (predicate.test(node)) { + it.remove(); + result.recordChange(ChangeType.REMOVE, node); + + // update inheritance map too if necessary + if (node instanceof InheritanceNode && node.getValue()) { + SortedSet inhNodes = this.inheritanceMap.get(node.getContexts()); + if (inhNodes != null) { + inhNodes.remove(node); + } + } + } + } + } + + @Override + public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) { + if (nodeToAdd.equals(nodeToRemove)) { + return new MutateResult(); + } + + this.lock.lock(); + try { + return removeExact(nodeToRemove).mergeFrom(add(nodeToAdd)); + } finally { + this.lock.unlock(); + } + } + + @Override + public MutateResult clear() { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + // log removals + for (SortedSet nodes : this.map.values()) { + result.recordChanges(ChangeType.REMOVE, nodes); + } + + // replace the map - this means any client reading async won't be affected + // by any race conditions between this call to clear and any subsequent call to setContent + this.map = createMap(); + this.inheritanceMap = createMap(); + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult clear(ContextSet contextSet) { + ImmutableContextSet context = contextSet.immutableCopy(); + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + SortedSet removed = this.map.remove(context); + if (removed != null) { + result.recordChanges(ChangeType.REMOVE, removed); + this.inheritanceMap.remove(context); + } + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult setContent(Iterable set) { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + result.mergeFrom(clear()); + result.mergeFrom(addAll(set)); + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult setContent(Stream stream) { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + result.mergeFrom(clear()); + result.mergeFrom(addAll(stream)); + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult addAll(Iterable set) { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + for (Node n : set) { + result.mergeFrom(add(n)); + } + } finally { + this.lock.unlock(); + } + + return result; + } + + @Override + public MutateResult addAll(Stream stream) { + MutateResult result = new MutateResult(); + + this.lock.lock(); + try { + stream.forEach(n -> result.mergeFrom(add(n))); + } finally { + this.lock.unlock(); + } + + return result; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java new file mode 100644 index 000000000..10d24cad9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java @@ -0,0 +1,187 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.nodemap; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; + +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryOptions; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * A forwarding {@link NodeMap} that records all mutations and keeps them in a log. + */ +public class RecordedNodeMap implements NodeMap { + + private final NodeMap delegate; + private final Lock lock = new ReentrantLock(); + private MutateResult changes = new MutateResult(); + + public RecordedNodeMap(NodeMap delegate) { + this.delegate = delegate; + } + + public NodeMap bypass() { + return this.delegate; + } + + public void discardChanges() { + this.lock.lock(); + try { + this.changes.clear(); + } finally { + this.lock.unlock(); + } + } + + public MutateResult exportChanges(Predicate onlyIf) { + this.lock.lock(); + try { + MutateResult existing = this.changes; + if (onlyIf.test(existing)) { + this.changes = new MutateResult(); + return existing; + } + return null; + } finally { + this.lock.unlock(); + } + } + + private MutateResult record(MutateResult result) { + this.lock.lock(); + try { + this.changes.mergeFrom(result); + } finally { + this.lock.unlock(); + } + return result; + } + + // delegate, but pass the result through #record(MutateResult) + + @Override + public MutateResult add(Node nodeWithoutInheritanceOrigin) { + return record(this.delegate.add(nodeWithoutInheritanceOrigin)); + } + + @Override + public MutateResult remove(Node node) { + return record(this.delegate.remove(node)); + } + + @Override + public MutateResult removeExact(Node node) { + return record(this.delegate.removeExact(node)); + } + + @Override + public MutateResult removeIf(Predicate predicate) { + return record(this.delegate.removeIf(predicate)); + } + + @Override + public MutateResult removeIf(ContextSet contextSet, Predicate predicate) { + return record(this.delegate.removeIf(contextSet, predicate)); + } + + @Override + public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) { + return record(this.delegate.removeThenAdd(nodeToRemove, nodeToAdd)); + } + + @Override + public MutateResult clear() { + return record(this.delegate.clear()); + } + + @Override + public MutateResult clear(ContextSet contextSet) { + return record(this.delegate.clear(contextSet)); + } + + @Override + public MutateResult setContent(Iterable set) { + return record(this.delegate.setContent(set)); + } + + @Override + public MutateResult setContent(Stream stream) { + return record(this.delegate.setContent(stream)); + } + + @Override + public MutateResult addAll(Iterable set) { + return record(this.delegate.addAll(set)); + } + + @Override + public MutateResult addAll(Stream stream) { + return record(this.delegate.addAll(stream)); + } + + // just plain delegation + + @Override public boolean isEmpty() { return this.delegate.isEmpty(); } + @Override public int size() { return this.delegate.size(); } + @Override public List asList() { return this.delegate.asList(); } + @Override public LinkedHashSet asSet() { return this.delegate.asSet(); } + @Override public SortedSet asSortedSet() { return this.delegate.asSortedSet(); } + @Override public ImmutableSet asImmutableSet() { return this.delegate.asImmutableSet(); } + @Override public Map> asMap() { return this.delegate.asMap(); } + @Override public List inheritanceAsList() { return this.delegate.inheritanceAsList(); } + @Override public LinkedHashSet inheritanceAsSet() { return this.delegate.inheritanceAsSet(); } + @Override public SortedSet inheritanceAsSortedSet() { return this.delegate.inheritanceAsSortedSet(); } + @Override public ImmutableSet inheritanceAsImmutableSet() { return this.delegate.inheritanceAsImmutableSet(); } + @Override public Map> inheritanceAsMap() { return this.delegate.inheritanceAsMap(); } + @Override public void forEach(Consumer consumer) { this.delegate.forEach(consumer); } + @Override public void forEach(QueryOptions filter, Consumer consumer) { this.delegate.forEach(filter, consumer); } + @Override public void copyTo(Collection collection) { this.delegate.copyTo(collection); } + @Override public void copyTo(ImmutableCollection.Builder collection) { this.delegate.copyTo(collection); } + @Override public void copyTo(Collection collection, QueryOptions filter) { this.delegate.copyTo(collection, filter); } + @Override public void copyTo(Collection collection, NodeType type, QueryOptions filter) { this.delegate.copyTo(collection, type, filter); } + @Override public void copyInheritanceNodesTo(Collection collection) { this.delegate.copyInheritanceNodesTo(collection); } + @Override public void copyInheritanceNodesTo(ImmutableCollection.Builder collection) { this.delegate.copyInheritanceNodesTo(collection); } + @Override public void copyInheritanceNodesTo(Collection collection, QueryOptions filter) { this.delegate.copyInheritanceNodesTo(collection, filter); } + @Override public Collection nodesInContext(ContextSet context) { return this.delegate.nodesInContext(context); } + @Override public Collection inheritanceNodesInContext(ContextSet context) { return this.delegate.inheritanceNodesInContext(context); } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java index f1e352a95..c268fa031 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java @@ -55,7 +55,6 @@ import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.PlayerSaveResult; -import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; @@ -193,44 +192,38 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio @Override public User loadUser(UUID uniqueId, String username) { User user = this.plugin.getUserManager().getOrMake(uniqueId, username); - user.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.USER, uniqueId.toString()); if (object != null) { String name = object.getNode("name").getString(); user.getPrimaryGroup().setStoredValue(object.getNode(this.loader instanceof JsonLoader ? "primaryGroup" : "primary-group").getString()); - - user.setNodes(DataType.NORMAL, readNodes(object)); user.setUsername(name, true); - boolean save = this.plugin.getUserManager().giveDefaultIfNeeded(user, false); - if (user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name))) { - save = true; - } + user.loadNodesFromStorage(readNodes(object)); + this.plugin.getUserManager().giveDefaultIfNeeded(user); - if (save | user.auditTemporaryNodes()) { + boolean updatedUsername = user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name)); + if (updatedUsername | user.auditTemporaryNodes()) { saveUser(user); } } else { - if (this.plugin.getUserManager().shouldSave(user)) { - user.clearNodes(DataType.NORMAL, null, true); + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + user.loadNodesFromStorage(Collections.emptyList()); user.getPrimaryGroup().setStoredValue(null); - this.plugin.getUserManager().giveDefaultIfNeeded(user, false); + this.plugin.getUserManager().giveDefaultIfNeeded(user); } } } catch (Exception e) { throw reportException(uniqueId.toString(), e); - } finally { - user.getIoLock().unlock(); } return user; } @Override public void saveUser(User user) { - user.getIoLock().lock(); + user.normalData().discardChanges(); try { - if (!this.plugin.getUserManager().shouldSave(user)) { + if (!this.plugin.getUserManager().isNonDefaultUser(user)) { saveFile(StorageLocation.USER, user.getUniqueId().toString(), null); } else { ConfigurationNode data = SimpleConfigurationNode.root(); @@ -245,20 +238,17 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio } } catch (Exception e) { throw reportException(user.getUniqueId().toString(), e); - } finally { - user.getIoLock().unlock(); } } @Override public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); - group.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.GROUP, name); if (object != null) { - group.setNodes(DataType.NORMAL, readNodes(object)); + group.loadNodesFromStorage(readNodes(object)); } else { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateStorage) { @@ -270,19 +260,12 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio } } catch (Exception e) { throw reportException(name, e); - } finally { - group.getIoLock().unlock(); } return group; } @Override public Optional loadGroup(String name) { - Group group = this.plugin.getGroupManager().getIfLoaded(name); - if (group != null) { - group.getIoLock().lock(); - } - try { ConfigurationNode object = readFile(StorageLocation.GROUP, name); @@ -290,26 +273,17 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio return Optional.empty(); } - if (group == null) { - group = this.plugin.getGroupManager().getOrMake(name); - group.getIoLock().lock(); - } - - group.setNodes(DataType.NORMAL, readNodes(object)); - + Group group = this.plugin.getGroupManager().getOrMake(name); + group.loadNodesFromStorage(readNodes(object)); + return Optional.of(group); } catch (Exception e) { throw reportException(name, e); - } finally { - if (group != null) { - group.getIoLock().unlock(); - } } - return Optional.of(group); } @Override public void saveGroup(Group group) { - group.getIoLock().lock(); + group.normalData().discardChanges(); try { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateStorage) { @@ -320,20 +294,15 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio saveFile(StorageLocation.GROUP, group.getName(), data); } catch (Exception e) { throw reportException(group.getName(), e); - } finally { - group.getIoLock().unlock(); } } @Override public void deleteGroup(Group group) { - group.getIoLock().lock(); try { saveFile(StorageLocation.GROUP, group.getName(), null); } catch (Exception e) { throw reportException(group.getName(), e); - } finally { - group.getIoLock().unlock(); } this.plugin.getGroupManager().unload(group.getName()); } @@ -341,7 +310,6 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio @Override public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); try { ConfigurationNode object = readFile(StorageLocation.TRACK, name); @@ -362,19 +330,12 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio } catch (Exception e) { throw reportException(name, e); - } finally { - track.getIoLock().unlock(); } return track; } @Override public Optional loadTrack(String name) { - Track track = this.plugin.getTrackManager().getIfLoaded(name); - if (track != null) { - track.getIoLock().lock(); - } - try { ConfigurationNode object = readFile(StorageLocation.TRACK, name); @@ -382,30 +343,19 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio return Optional.empty(); } - if (track == null) { - track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - } - + Track track = this.plugin.getTrackManager().getOrMake(name); List groups = object.getNode("groups").getChildrenList().stream() .map(ConfigurationNode::getString) .collect(ImmutableCollectors.toList()); - track.setGroups(groups); - + return Optional.of(track); } catch (Exception e) { throw reportException(name, e); - } finally { - if (track != null) { - track.getIoLock().unlock(); - } } - return Optional.of(track); } @Override public void saveTrack(Track track) { - track.getIoLock().lock(); try { ConfigurationNode data = SimpleConfigurationNode.root(); if (this instanceof SeparatedConfigurateStorage) { @@ -415,20 +365,15 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio saveFile(StorageLocation.TRACK, track.getName(), data); } catch (Exception e) { throw reportException(track.getName(), e); - } finally { - track.getIoLock().unlock(); } } @Override public void deleteTrack(Track track) { - track.getIoLock().lock(); try { saveFile(StorageLocation.TRACK, track.getName(), null); } catch (Exception e) { throw reportException(track.getName(), e); - } finally { - track.getIoLock().unlock(); } this.plugin.getTrackManager().unload(track.getName()); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java index 51ad65bbd..71fe6a4a4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java @@ -64,7 +64,6 @@ import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.PlayerSaveResult; -import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; @@ -72,6 +71,7 @@ import org.bson.Document; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -298,53 +298,43 @@ public class MongoStorage implements StorageImplementation { @Override public User loadUser(UUID uniqueId, String username) { User user = this.plugin.getUserManager().getOrMake(uniqueId, username); - user.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "users"); - try (MongoCursor cursor = c.find(new Document("_id", user.getUniqueId())).iterator()) { - if (cursor.hasNext()) { - // User exists, let's load. - Document d = cursor.next(); + MongoCollection c = this.database.getCollection(this.prefix + "users"); + try (MongoCursor cursor = c.find(new Document("_id", user.getUniqueId())).iterator()) { + if (cursor.hasNext()) { + // User exists, let's load. + Document d = cursor.next(); + String name = d.getString("name"); - String name = d.getString("name"); - user.getPrimaryGroup().setStoredValue(d.getString("primaryGroup")); - user.setNodes(DataType.NORMAL, nodesFromDoc(d)); - user.setUsername(name, true); + user.getPrimaryGroup().setStoredValue(d.getString("primaryGroup")); + user.setUsername(name, true); - boolean save = this.plugin.getUserManager().giveDefaultIfNeeded(user, false); - if (user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name))) { - save = true; - } + user.loadNodesFromStorage(nodesFromDoc(d)); + this.plugin.getUserManager().giveDefaultIfNeeded(user); - if (save | user.auditTemporaryNodes()) { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user)); - } - } else { - if (this.plugin.getUserManager().shouldSave(user)) { - user.clearNodes(DataType.NORMAL, null, true); - user.getPrimaryGroup().setStoredValue(null); - this.plugin.getUserManager().giveDefaultIfNeeded(user, false); - } + + boolean updatedUsername = user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name)); + if (updatedUsername | user.auditTemporaryNodes()) { + c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user)); + } + } else { + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + user.loadNodesFromStorage(Collections.emptyList()); + user.getPrimaryGroup().setStoredValue(null); + this.plugin.getUserManager().giveDefaultIfNeeded(user); } } - } finally { - user.getIoLock().unlock(); } return user; } @Override public void saveUser(User user) { - user.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "users"); - if (!this.plugin.getUserManager().shouldSave(user)) { - c.deleteOne(new Document("_id", user.getUniqueId())); - } else { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user), new ReplaceOptions().upsert(true)); - } - } finally { - user.getIoLock().unlock(); + MongoCollection c = this.database.getCollection(this.prefix + "users"); + user.normalData().discardChanges(); + if (!this.plugin.getUserManager().isNonDefaultUser(user)) { + c.deleteOne(new Document("_id", user.getUniqueId())); + } else { + c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user), new ReplaceOptions().upsert(true)); } } @@ -388,50 +378,31 @@ public class MongoStorage implements StorageImplementation { @Override public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); - group.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", group.getName())).iterator()) { - if (cursor.hasNext()) { - Document d = cursor.next(); - group.setNodes(DataType.NORMAL, nodesFromDoc(d)); - } else { - c.insertOne(groupToDoc(group)); - } + MongoCollection c = this.database.getCollection(this.prefix + "groups"); + try (MongoCursor cursor = c.find(new Document("_id", group.getName())).iterator()) { + if (cursor.hasNext()) { + Document d = cursor.next(); + group.loadNodesFromStorage(nodesFromDoc(d)); + } else { + c.insertOne(groupToDoc(group)); } - } finally { - group.getIoLock().unlock(); } return group; } @Override public Optional loadGroup(String name) { - Group group = this.plugin.getGroupManager().getIfLoaded(name); - if (group != null) { - group.getIoLock().lock(); - } - try { - MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { - if (!cursor.hasNext()) { - return Optional.empty(); - } - - if (group == null) { - group = this.plugin.getGroupManager().getOrMake(name); - group.getIoLock().lock(); - } - - Document d = cursor.next(); - group.setNodes(DataType.NORMAL, nodesFromDoc(d)); - } - } finally { - if (group != null) { - group.getIoLock().unlock(); + MongoCollection c = this.database.getCollection(this.prefix + "groups"); + try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + if (!cursor.hasNext()) { + return Optional.empty(); } + + Group group = this.plugin.getGroupManager().getOrMake(name); + Document d = cursor.next(); + group.loadNodesFromStorage(nodesFromDoc(d)); + return Optional.of(group); } - return Optional.of(group); } @Override @@ -454,24 +425,15 @@ public class MongoStorage implements StorageImplementation { @Override public void saveGroup(Group group) { - group.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "groups"); - c.replaceOne(new Document("_id", group.getName()), groupToDoc(group), new ReplaceOptions().upsert(true)); - } finally { - group.getIoLock().unlock(); - } + MongoCollection c = this.database.getCollection(this.prefix + "groups"); + group.normalData().discardChanges(); + c.replaceOne(new Document("_id", group.getName()), groupToDoc(group), new ReplaceOptions().upsert(true)); } @Override public void deleteGroup(Group group) { - group.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "groups"); - c.deleteOne(new Document("_id", group.getName())); - } finally { - group.getIoLock().unlock(); - } + MongoCollection c = this.database.getCollection(this.prefix + "groups"); + c.deleteOne(new Document("_id", group.getName())); } @Override @@ -498,53 +460,33 @@ public class MongoStorage implements StorageImplementation { @Override public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", track.getName())).iterator()) { - if (!cursor.hasNext()) { - c.insertOne(trackToDoc(track)); - } else { - Document d = cursor.next(); - //noinspection unchecked - track.setGroups((List) d.get("groups")); - } + MongoCollection c = this.database.getCollection(this.prefix + "tracks"); + try (MongoCursor cursor = c.find(new Document("_id", track.getName())).iterator()) { + if (!cursor.hasNext()) { + c.insertOne(trackToDoc(track)); + } else { + Document d = cursor.next(); + //noinspection unchecked + track.setGroups((List) d.get("groups")); } - } finally { - track.getIoLock().unlock(); } return track; } @Override public Optional loadTrack(String name) { - Track track = this.plugin.getTrackManager().getIfLoaded(name); - if (track != null) { - track.getIoLock().lock(); - } - - try { - MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { - if (!cursor.hasNext()) { - return Optional.empty(); - } - - if (track == null) { - track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - } - - Document d = cursor.next(); - //noinspection unchecked - track.setGroups((List) d.get("groups")); - } - } finally { - if (track != null) { - track.getIoLock().unlock(); + MongoCollection c = this.database.getCollection(this.prefix + "tracks"); + try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + if (!cursor.hasNext()) { + return Optional.empty(); } + + Track track = this.plugin.getTrackManager().getOrMake(name); + Document d = cursor.next(); + //noinspection unchecked + track.setGroups((List) d.get("groups")); + return Optional.of(track); } - return Optional.of(track); } @Override @@ -567,24 +509,14 @@ public class MongoStorage implements StorageImplementation { @Override public void saveTrack(Track track) { - track.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.replaceOne(new Document("_id", track.getName()), trackToDoc(track)); - } finally { - track.getIoLock().unlock(); - } + MongoCollection c = this.database.getCollection(this.prefix + "tracks"); + c.replaceOne(new Document("_id", track.getName()), trackToDoc(track)); } @Override public void deleteTrack(Track track) { - track.getIoLock().lock(); - try { - MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.deleteOne(new Document("_id", track.getName())); - } finally { - track.getIoLock().unlock(); - } + MongoCollection c = this.database.getCollection(this.prefix + "tracks"); + c.deleteOne(new Document("_id", track.getName())); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java deleted file mode 100644 index 484a9611f..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlNode.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * 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.storage.implementation.sql; - -import com.google.common.base.Strings; - -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; -import me.lucko.luckperms.common.node.factory.NodeBuilders; -import me.lucko.luckperms.common.util.gson.GsonProvider; - -import net.luckperms.api.context.ContextSet; -import net.luckperms.api.context.DefaultContextKeys; -import net.luckperms.api.context.ImmutableContextSet; -import net.luckperms.api.context.MutableContextSet; -import net.luckperms.api.node.Node; - -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -/** - * A version of {@link Node}, more closely following the model used by the SQL - * datastore. - * - * All values are non-null. - */ -public final class SqlNode { - - public static final int NULL_ID = -1; - - public static SqlNode fromNode(Node node) { - ContextSet contexts = node.getContexts(); - - Set servers = contexts.getValues(DefaultContextKeys.SERVER_KEY); - Optional firstServer = servers.stream().sorted().findFirst(); - - String server; - if (firstServer.isPresent()) { - server = firstServer.get(); - MutableContextSet mutableContextSet = contexts.mutableCopy(); - mutableContextSet.remove(DefaultContextKeys.SERVER_KEY, server); - contexts = mutableContextSet; - } else { - server = "global"; - } - - Set worlds = contexts.getValues(DefaultContextKeys.WORLD_KEY); - Optional firstWorld = worlds.stream().sorted().findFirst(); - - String world; - if (firstWorld.isPresent()) { - world = firstWorld.get(); - MutableContextSet mutableContextSet = contexts instanceof MutableContextSet ? (MutableContextSet) contexts : contexts.mutableCopy(); - mutableContextSet.remove(DefaultContextKeys.WORLD_KEY, world); - contexts = mutableContextSet; - } else { - world = "global"; - } - - - long expiry = node.hasExpiry() ? node.getExpiry().getEpochSecond() : 0L; - return new SqlNode(node.getKey(), node.getValue(), server, world, expiry, contexts.immutableCopy(), NULL_ID); - } - - public static SqlNode fromSqlFields(long sqlId, String permission, boolean value, String server, String world, long expiry, String contexts) { - if (Strings.isNullOrEmpty(server)) { - server = "global"; - } - if (Strings.isNullOrEmpty(world)) { - world = "global"; - } - - return new SqlNode(permission, value, server, world, expiry, ContextSetJsonSerializer.deserialize(GsonProvider.normal(), contexts).immutableCopy(), sqlId); - } - - private final String permission; - private final boolean value; - private final String server; - private final String world; - private final long expiry; - private final ImmutableContextSet contexts; - private final long sqlId; - - private SqlNode(String permission, boolean value, String server, String world, long expiry, ImmutableContextSet contexts, long sqlId) { - this.permission = Objects.requireNonNull(permission, "permission"); - this.value = value; - this.server = Objects.requireNonNull(server, "server"); - this.world = Objects.requireNonNull(world, "world"); - this.expiry = expiry; - this.contexts = Objects.requireNonNull(contexts, "contexts"); - this.sqlId = sqlId; - } - - public Node toNode() { - return NodeBuilders.determineMostApplicable(this.permission) - .value(this.value) - .withContext(DefaultContextKeys.SERVER_KEY, this.server) - .withContext(DefaultContextKeys.WORLD_KEY, this.world) - .expiry(this.expiry) - .withContext(this.contexts) - .build(); - } - - public String getPermission() { - return this.permission; - } - - public boolean getValue() { - return this.value; - } - - public String getServer() { - return this.server; - } - - public String getWorld() { - return this.world; - } - - public long getExpiry() { - return this.expiry; - } - - public ImmutableContextSet getContexts() { - return this.contexts; - } - - public long getSqlId() { - if (this.sqlId == NULL_ID) { - throw new IllegalStateException("sql id not set"); - } - return this.sqlId; - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof SqlNode)) return false; - final SqlNode other = (SqlNode) o; - - return this.getPermission().equals(other.getPermission()) && - this.getValue() == other.getValue() && - this.getServer().equals(other.getServer()) && - this.getWorld().equals(other.getWorld()) && - this.getExpiry() == other.getExpiry() && - this.getContexts().equals(other.getContexts()); - } - - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.getPermission().hashCode(); - result = result * PRIME + Boolean.hashCode(this.getValue()); - result = result * PRIME + this.getServer().hashCode(); - result = result * PRIME + this.getWorld().hashCode(); - result = result * PRIME + Long.hashCode(this.getExpiry()); - result = result * PRIME + this.getContexts().hashCode(); - return result; - } - - @Override - public String toString() { - return "NodeModel(" + - "permission=" + this.getPermission() + ", " + - "value=" + this.getValue() + ", " + - "server=" + this.getServer() + ", " + - "world=" + this.getWorld() + ", " + - "expiry=" + this.getExpiry() + ", " + - "contexts=" + this.getContexts() + ")"; - } -} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlRowId.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlRowId.java new file mode 100644 index 000000000..9e1bc01cb --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlRowId.java @@ -0,0 +1,57 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * 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.storage.implementation.sql; + +import net.luckperms.api.node.metadata.NodeMetadataKey; + +public final class SqlRowId { + public static final NodeMetadataKey KEY = NodeMetadataKey.of("sqlrowid", SqlRowId.class); + + private final long rowId; + + public SqlRowId(long rowId) { + this.rowId = rowId; + } + + public long getRowId() { + return this.rowId; + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof SqlRowId && this.rowId == ((SqlRowId) o).rowId); + } + + @Override + public int hashCode() { + return Long.hashCode(this.rowId); + } + + @Override + public String toString() { + return "SqlRowId{rowId=" + this.rowId + '}'; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java index 0110cf72f..8ae7a7bec 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.storage.implementation.sql; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gson.reflect.TypeToken; @@ -38,6 +39,8 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.model.nodemap.MutateResult; +import me.lucko.luckperms.common.node.factory.NodeBuilders; import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; @@ -49,8 +52,9 @@ import me.lucko.luckperms.common.util.gson.GsonProvider; import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.PlayerSaveResult; -import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import java.io.IOException; @@ -80,6 +84,7 @@ public class SqlStorage implements StorageImplementation { private static final String USER_PERMISSIONS_SELECT = "SELECT id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE uuid=?"; private static final String USER_PERMISSIONS_DELETE_SPECIFIC = "DELETE FROM '{prefix}user_permissions' WHERE id=?"; + private static final String USER_PERMISSIONS_DELETE_SPECIFIC_PROPS = "DELETE FROM '{prefix}user_permissions' WHERE uuid=? AND permission=? AND value=? AND server=? AND world=? AND expiry=? AND contexts=?"; private static final String USER_PERMISSIONS_DELETE = "DELETE FROM '{prefix}user_permissions' WHERE uuid=?"; private static final String USER_PERMISSIONS_INSERT = "INSERT INTO '{prefix}user_permissions' (uuid, permission, value, server, world, expiry, contexts) VALUES(?, ?, ?, ?, ?, ?, ?)"; private static final String USER_PERMISSIONS_SELECT_DISTINCT = "SELECT DISTINCT uuid FROM '{prefix}user_permissions'"; @@ -99,6 +104,7 @@ public class SqlStorage implements StorageImplementation { private static final String GROUP_PERMISSIONS_SELECT = "SELECT id, permission, value, server, world, expiry, contexts FROM '{prefix}group_permissions' WHERE name=?"; private static final String GROUP_PERMISSIONS_SELECT_ALL = "SELECT name, id, permission, value, server, world, expiry, contexts FROM '{prefix}group_permissions'"; private static final String GROUP_PERMISSIONS_DELETE_SPECIFIC = "DELETE FROM '{prefix}group_permissions' WHERE id=?"; + private static final String GROUP_PERMISSIONS_DELETE_SPECIFIC_PROPS = "DELETE FROM '{prefix}group_permissions' WHERE name=? AND permission=? AND value=? AND server=? AND world=? AND expiry=? AND contexts=?"; private static final String GROUP_PERMISSIONS_DELETE = "DELETE FROM '{prefix}group_permissions' WHERE name=?"; private static final String GROUP_PERMISSIONS_INSERT = "INSERT INTO '{prefix}group_permissions' (name, permission, value, server, world, expiry, contexts) VALUES(?, ?, ?, ?, ?, ?, ?)"; private static final String GROUP_PERMISSIONS_SELECT_PERMISSION = "SELECT name, id, permission, value, server, world, expiry, contexts FROM '{prefix}group_permissions' WHERE "; @@ -313,82 +319,64 @@ public class SqlStorage implements StorageImplementation { @Override public User loadUser(UUID uniqueId, String username) throws SQLException { User user = this.plugin.getUserManager().getOrMake(uniqueId, username); - user.getIoLock().lock(); - try { - List nodes; - String primaryGroup = null; - String savedUsername = null; - try (Connection c = this.connectionFactory.getConnection()) { - nodes = selectUserPermissions(new ArrayList<>(), c, user.getUniqueId()); + List nodes; + SqlPlayerData playerData; - SqlPlayerData playerData = selectPlayerData(c, user.getUniqueId()); - if (playerData != null) { - primaryGroup = playerData.primaryGroup; - savedUsername = playerData.username; - } - } - - // update username & primary group - if (primaryGroup == null) { - primaryGroup = GroupManager.DEFAULT_GROUP_NAME; - } - user.getPrimaryGroup().setStoredValue(primaryGroup); - - // Update their username to what was in the storage if the one in the local instance is null - user.setUsername(savedUsername, true); - - if (!nodes.isEmpty()) { - user.setNodes(DataType.NORMAL, nodes.stream().map(SqlNode::toNode)); - - // Save back to the store if data they were given any defaults or had permissions expire - if (this.plugin.getUserManager().giveDefaultIfNeeded(user, false) | user.auditTemporaryNodes()) { - // This should be fine, as the lock will be acquired by the same thread. - saveUser(user); - } - - } else { - if (this.plugin.getUserManager().shouldSave(user)) { - user.clearNodes(DataType.NORMAL, null, true); - user.getPrimaryGroup().setStoredValue(null); - this.plugin.getUserManager().giveDefaultIfNeeded(user, false); - } - } - } finally { - user.getIoLock().unlock(); + try (Connection c = this.connectionFactory.getConnection()) { + nodes = selectUserPermissions(c, user.getUniqueId()); + playerData = selectPlayerData(c, user.getUniqueId()); } + + if (playerData != null) { + if (playerData.primaryGroup != null) { + user.getPrimaryGroup().setStoredValue(playerData.primaryGroup); + } else { + user.getPrimaryGroup().setStoredValue(GroupManager.DEFAULT_GROUP_NAME); + } + + user.setUsername(playerData.username, true); + } + + user.loadNodesFromStorage(nodes); + this.plugin.getUserManager().giveDefaultIfNeeded(user); + + if (user.auditTemporaryNodes()) { + saveUser(user); + } + return user; } @Override public void saveUser(User user) throws SQLException { - user.getIoLock().lock(); - try { - if (!this.plugin.getUserManager().shouldSave(user)) { - try (Connection c = this.connectionFactory.getConnection()) { - deleteUser(c, user.getUniqueId()); - } - return; + MutateResult changes = user.normalData().exportChanges(results -> { + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + return true; } - Set remote; + // if the only change is adding the default node, we don't need to export + if (results.getChanges().size() == 1) { + MutateResult.Change onlyChange = results.getChanges().iterator().next(); + return !(onlyChange.getType() == MutateResult.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.getNode())); + } + + return true; + }); + + if (changes == null) { try (Connection c = this.connectionFactory.getConnection()) { - remote = selectUserPermissions(new HashSet<>(), c, user.getUniqueId()); + deleteUser(c, user.getUniqueId()); } + return; + } - Set local = user.normalData().asList().stream().map(SqlNode::fromNode).collect(Collectors.toSet()); - Set missingFromRemote = getMissingFromRemote(local, remote); - Set missingFromLocal = getMissingFromLocal(local, remote); - - try (Connection c = this.connectionFactory.getConnection()) { - updateUserPermissions(c, user.getUniqueId(), missingFromRemote, missingFromLocal); - insertPlayerData(c, user.getUniqueId(), new SqlPlayerData( - user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME), - user.getUsername().orElse("null").toLowerCase() - )); - } - } finally { - user.getIoLock().unlock(); + try (Connection c = this.connectionFactory.getConnection()) { + updateUserPermissions(c, user.getUniqueId(), changes.getAdded(), changes.getRemoved()); + insertPlayerData(c, user.getUniqueId(), new SqlPlayerData( + user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME), + user.getUsername().orElse("null").toLowerCase() + )); } } @@ -421,7 +409,7 @@ public class SqlStorage implements StorageImplementation { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UUID holder = UUID.fromString(rs.getString("uuid")); - Node node = readNode(rs).toNode(); + Node node = readNode(rs); N match = constraint.filterConstraintMatch(node); if (match != null) { @@ -460,45 +448,27 @@ public class SqlStorage implements StorageImplementation { } Group group = this.plugin.getGroupManager().getOrMake(name); - group.getIoLock().lock(); - try { - List nodes; - try (Connection c = this.connectionFactory.getConnection()) { - nodes = selectGroupPermissions(new ArrayList<>(), c, group.getName()); - } - - if (!nodes.isEmpty()) { - group.setNodes(DataType.NORMAL, nodes.stream().map(SqlNode::toNode)); - } else { - group.clearNodes(DataType.NORMAL, null, false); - } - } finally { - group.getIoLock().unlock(); + List nodes; + try (Connection c = this.connectionFactory.getConnection()) { + nodes = selectGroupPermissions(c, group.getName()); } + + group.loadNodesFromStorage(nodes); return Optional.of(group); } @Override public void loadAllGroups() throws SQLException { - Map> groups = new HashMap<>(); + Map> groups = new HashMap<>(); try (Connection c = this.connectionFactory.getConnection()) { selectGroups(c).forEach(name -> groups.put(name, new ArrayList<>())); selectAllGroupPermissions(groups, c); } - for (Map.Entry> entry : groups.entrySet()) { + for (Map.Entry> entry : groups.entrySet()) { Group group = this.plugin.getGroupManager().getOrMake(entry.getKey()); - group.getIoLock().lock(); - try { - Collection nodes = entry.getValue(); - if (!nodes.isEmpty()) { - group.setNodes(DataType.NORMAL, nodes.stream().map(SqlNode::toNode)); - } else { - group.clearNodes(DataType.NORMAL, null, false); - } - } finally { - group.getIoLock().unlock(); - } + Collection nodes = entry.getValue(); + group.loadNodesFromStorage(nodes); } this.plugin.getGroupManager().retainAll(groups.keySet()); @@ -506,48 +476,24 @@ public class SqlStorage implements StorageImplementation { @Override public void saveGroup(Group group) throws SQLException { - group.getIoLock().lock(); - try { - if (group.normalData().isEmpty()) { - try (Connection c = this.connectionFactory.getConnection()) { - deleteGroupPermissions(c, group.getName()); - } - return; - } + MutateResult changes = group.normalData().exportChanges(c -> true); - Set remote; + if (!changes.isEmpty()) { try (Connection c = this.connectionFactory.getConnection()) { - remote = selectGroupPermissions(new HashSet<>(), c, group.getName()); + updateGroupPermissions(c, group.getName(), changes.getAdded(), changes.getRemoved()); } - - Set local = group.normalData().asList().stream().map(SqlNode::fromNode).collect(Collectors.toSet()); - Set missingFromRemote = getMissingFromRemote(local, remote); - Set missingFromLocal = getMissingFromLocal(local, remote); - - if (!missingFromLocal.isEmpty() || !missingFromRemote.isEmpty()) { - try (Connection c = this.connectionFactory.getConnection()) { - updateGroupPermissions(c, group.getName(), missingFromRemote, missingFromLocal); - } - } - } finally { - group.getIoLock().unlock(); } } @Override public void deleteGroup(Group group) throws SQLException { - group.getIoLock().lock(); - try { - try (Connection c = this.connectionFactory.getConnection()) { - deleteGroupPermissions(c, group.getName()); + try (Connection c = this.connectionFactory.getConnection()) { + deleteGroupPermissions(c, group.getName()); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_DELETE))) { - ps.setString(1, group.getName()); - ps.execute(); - } + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_DELETE))) { + ps.setString(1, group.getName()); + ps.execute(); } - } finally { - group.getIoLock().unlock(); } this.plugin.getGroupManager().unload(group.getName()); @@ -564,7 +510,7 @@ public class SqlStorage implements StorageImplementation { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); - Node node = readNode(rs).toNode(); + Node node = readNode(rs); N match = constraint.filterConstraintMatch(node); if (match != null) { @@ -580,22 +526,17 @@ public class SqlStorage implements StorageImplementation { @Override public Track createAndLoadTrack(String name) throws SQLException { Track track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - try { - List groups; - try (Connection c = this.connectionFactory.getConnection()) { - groups = selectTrack(c, track.getName()); - } + List groups; + try (Connection c = this.connectionFactory.getConnection()) { + groups = selectTrack(c, track.getName()); + } - if (groups != null) { - track.setGroups(groups); - } else { - try (Connection c = this.connectionFactory.getConnection()) { - insertTrack(c, track.getName(), track.getGroups()); - } + if (groups != null) { + track.setGroups(groups); + } else { + try (Connection c = this.connectionFactory.getConnection()) { + insertTrack(c, track.getName(), track.getGroups()); } - } finally { - track.getIoLock().unlock(); } return track; } @@ -612,17 +553,12 @@ public class SqlStorage implements StorageImplementation { } Track track = this.plugin.getTrackManager().getOrMake(name); - track.getIoLock().lock(); - try { - List groups; - try (Connection c = this.connectionFactory.getConnection()) { - groups = selectTrack(c, name); - } - - track.setGroups(groups); - } finally { - track.getIoLock().unlock(); + List groups; + try (Connection c = this.connectionFactory.getConnection()) { + groups = selectTrack(c, name); } + + track.setGroups(groups); return Optional.of(track); } @@ -634,13 +570,8 @@ public class SqlStorage implements StorageImplementation { for (String trackName : tracks) { Track track = this.plugin.getTrackManager().getOrMake(trackName); - track.getIoLock().lock(); - try { - List groups = selectTrack(c, trackName); - track.setGroups(groups); - } finally { - track.getIoLock().unlock(); - } + List groups = selectTrack(c, trackName); + track.setGroups(groups); } } @@ -649,28 +580,18 @@ public class SqlStorage implements StorageImplementation { @Override public void saveTrack(Track track) throws SQLException { - track.getIoLock().lock(); - try { - try (Connection c = this.connectionFactory.getConnection()) { - updateTrack(c, track.getName(), track.getGroups()); - } - } finally { - track.getIoLock().unlock(); + try (Connection c = this.connectionFactory.getConnection()) { + updateTrack(c, track.getName(), track.getGroups()); } } @Override public void deleteTrack(Track track) throws SQLException { - track.getIoLock().lock(); - try { - try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_DELETE))) { - ps.setString(1, track.getName()); - ps.execute(); - } + try (Connection c = this.connectionFactory.getConnection()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_DELETE))) { + ps.setString(1, track.getName()); + ps.execute(); } - } finally { - track.getIoLock().unlock(); } this.plugin.getTrackManager().unload(track.getName()); @@ -797,7 +718,7 @@ public class SqlStorage implements StorageImplementation { .build(); } - private static SqlNode readNode(ResultSet rs) throws SQLException { + private static Node readNode(ResultSet rs) throws SQLException { long id = rs.getLong("id"); String permission = rs.getString("permission"); boolean value = rs.getBoolean("value"); @@ -805,33 +726,105 @@ public class SqlStorage implements StorageImplementation { String world = rs.getString("world"); long expiry = rs.getLong("expiry"); String contexts = rs.getString("contexts"); - return SqlNode.fromSqlFields(id, permission, value, server, world, expiry, contexts); + + if (Strings.isNullOrEmpty(server)) { + server = "global"; + } + if (Strings.isNullOrEmpty(world)) { + world = "global"; + } + + return NodeBuilders.determineMostApplicable(permission) + .value(value) + .withContext(DefaultContextKeys.SERVER_KEY, server) + .withContext(DefaultContextKeys.WORLD_KEY, world) + .expiry(expiry) + .withContext(ContextSetJsonSerializer.deserialize(GsonProvider.normal(), contexts).immutableCopy()) + .withMetadata(SqlRowId.KEY, new SqlRowId(id)) + .build(); } - private static void writeNode(SqlNode nd, PreparedStatement ps) throws SQLException { - ps.setString(2, nd.getPermission()); - ps.setBoolean(3, nd.getValue()); - ps.setString(4, nd.getServer()); - ps.setString(5, nd.getWorld()); - ps.setLong(6, nd.getExpiry()); - ps.setString(7, GsonProvider.normal().toJson(ContextSetJsonSerializer.serialize(nd.getContexts()))); + private static String getFirstContextValue(MutableContextSet set, String key) { + Set values = set.getValues(key); + String value = values.stream().sorted().findFirst().orElse(null); + if (value != null) { + set.remove(key, value); + } else { + value = "global"; + } + return value; } - private static Set getMissingFromRemote(Set local, Set remote) { - // entries in local but not remote need to be added - Set missingFromRemote = new HashSet<>(local); - missingFromRemote.removeAll(remote); - return missingFromRemote; + private static void writeNode(Node node, PreparedStatement ps) throws SQLException { + MutableContextSet contexts = node.getContexts().mutableCopy(); + String server = getFirstContextValue(contexts, DefaultContextKeys.SERVER_KEY); + String world = getFirstContextValue(contexts, DefaultContextKeys.WORLD_KEY); + long expiry = node.hasExpiry() ? node.getExpiry().getEpochSecond() : 0L; + + ps.setString(2, node.getKey()); + ps.setBoolean(3, node.getValue()); + ps.setString(4, server); + ps.setString(5, world); + ps.setLong(6, expiry); + ps.setString(7, GsonProvider.normal().toJson(ContextSetJsonSerializer.serialize(contexts))); } - private static Set getMissingFromLocal(Set local, Set remote) { - // entries in remote but not local need to be removed - Set missingFromLocal = new HashSet<>(remote); - missingFromLocal.removeAll(local); - return missingFromLocal; + private void updateUserPermissions(Connection c, UUID user, Set add, Set delete) throws SQLException { + updatePermissions(c, user.toString(), add, delete, USER_PERMISSIONS_DELETE_SPECIFIC, USER_PERMISSIONS_DELETE_SPECIFIC_PROPS, USER_PERMISSIONS_INSERT); } - private > T selectUserPermissions(T nodes, Connection c, UUID user) throws SQLException { + private void updateGroupPermissions(Connection c, String group, Set add, Set delete) throws SQLException { + updatePermissions(c, group, add, delete, GROUP_PERMISSIONS_DELETE_SPECIFIC, GROUP_PERMISSIONS_DELETE_SPECIFIC_PROPS, GROUP_PERMISSIONS_INSERT); + } + + private void updatePermissions(Connection c, String holder, Set add, Set delete, String deleteSpecificQuery, String deleteQuery, String insertQuery) throws SQLException { + if (!delete.isEmpty()) { + List deleteRows = new ArrayList<>(delete.size()); + List deleteNodes = new ArrayList<>(delete.size()); + for (Node node : delete) { + SqlRowId rowId = node.getMetadata(SqlRowId.KEY).orElse(null); + if (rowId != null) { + deleteRows.add(rowId.getRowId()); + } else { + deleteNodes.add(node); + } + } + + if (!deleteRows.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(deleteSpecificQuery))) { + for (Long id : deleteRows) { + ps.setLong(1, id); + ps.addBatch(); + } + ps.executeBatch(); + } + } + if (!deleteNodes.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(deleteQuery))) { + for (Node node : deleteNodes) { + ps.setString(1, holder); + writeNode(node, ps); + ps.addBatch(); + } + ps.executeBatch(); + } + } + } + + if (!add.isEmpty()) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(insertQuery))) { + for (Node node : add) { + ps.setString(1, holder); + writeNode(node, ps); + ps.addBatch(); + } + ps.executeBatch(); + } + } + } + + private List selectUserPermissions(Connection c, UUID user) throws SQLException { + List nodes = new ArrayList<>(); try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT))) { ps.setString(1, user.toString()); try (ResultSet rs = ps.executeQuery()) { @@ -868,28 +861,6 @@ public class SqlStorage implements StorageImplementation { } } - private void updateUserPermissions(Connection c, UUID user, Set add, Set delete) throws SQLException { - if (!delete.isEmpty()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE_SPECIFIC))) { - for (SqlNode node : delete) { - ps.setLong(1, node.getSqlId()); - ps.addBatch(); - } - ps.executeBatch(); - } - } - if (!add.isEmpty()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_INSERT))) { - for (SqlNode node : add) { - ps.setString(1, user.toString()); - writeNode(node, ps); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } - private void insertPlayerData(Connection c, UUID user, SqlPlayerData data) throws SQLException { boolean hasPrimaryGroupSaved; try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) { @@ -929,7 +900,8 @@ public class SqlStorage implements StorageImplementation { return groups; } - private > T selectGroupPermissions(T nodes, Connection c, String group) throws SQLException { + private List selectGroupPermissions(Connection c, String group) throws SQLException { + List nodes = new ArrayList<>(); try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT))) { ps.setString(1, group); try (ResultSet rs = ps.executeQuery()) { @@ -941,12 +913,12 @@ public class SqlStorage implements StorageImplementation { return nodes; } - private void selectAllGroupPermissions(Map> nodes, Connection c) throws SQLException { + private void selectAllGroupPermissions(Map> nodes, Connection c) throws SQLException { try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT_ALL))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); - Collection list = nodes.get(holder); + Collection list = nodes.get(holder); if (list != null) { list.add(readNode(rs)); } @@ -962,28 +934,6 @@ public class SqlStorage implements StorageImplementation { } } - private void updateGroupPermissions(Connection c, String group, Set add, Set delete) throws SQLException { - if (!delete.isEmpty()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE_SPECIFIC))) { - for (SqlNode node : delete) { - ps.setLong(1, node.getSqlId()); - ps.addBatch(); - } - ps.executeBatch(); - } - } - if (!add.isEmpty()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_INSERT))) { - for (SqlNode node : add) { - ps.setString(1, group); - writeNode(node, ps); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } - private List selectTrack(Connection c, String name) throws SQLException { String groups; try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) { diff --git a/common/src/main/java/me/lucko/luckperms/common/tasks/ExpireTemporaryTask.java b/common/src/main/java/me/lucko/luckperms/common/tasks/ExpireTemporaryTask.java index 628f26783..1558521df 100644 --- a/common/src/main/java/me/lucko/luckperms/common/tasks/ExpireTemporaryTask.java +++ b/common/src/main/java/me/lucko/luckperms/common/tasks/ExpireTemporaryTask.java @@ -26,12 +26,9 @@ package me.lucko.luckperms.common.tasks; import me.lucko.luckperms.common.model.Group; -import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.util.concurrent.locks.Lock; - public class ExpireTemporaryTask implements Runnable { private final LuckPermsPlugin plugin; @@ -43,9 +40,6 @@ public class ExpireTemporaryTask implements Runnable { public void run() { boolean groupChanges = false; for (Group group : this.plugin.getGroupManager().getAll().values()) { - if (shouldSkip(group)) { - continue; - } if (group.auditTemporaryNodes()) { this.plugin.getStorage().saveGroup(group); groupChanges = true; @@ -53,9 +47,6 @@ public class ExpireTemporaryTask implements Runnable { } for (User user : this.plugin.getUserManager().getAll().values()) { - if (shouldSkip(user)) { - continue; - } if (user.auditTemporaryNodes()) { this.plugin.getStorage().saveUser(user); } @@ -67,19 +58,4 @@ public class ExpireTemporaryTask implements Runnable { } } - // return true if the holder's io lock is currently held, false otherwise - private static boolean shouldSkip(PermissionHolder holder) { - Lock lock = holder.getIoLock(); - - // if the holder is currently being manipulated by the storage impl, - // don't attempt to audit temporary permissions - if (!lock.tryLock()) { - // if #tryLock returns false, it means it's held by something else - return true; - } - - // immediately release the lock & return false - lock.unlock(); - return false; - } } \ No newline at end of file diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java index 08044f1ef..161ee9ef5 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java @@ -77,11 +77,6 @@ public class SpongeGroupManager extends AbstractGroupManager implem .build(s -> { SpongeGroup group = getIfLoaded(s); if (group != null) { - // they're already loaded, but the data might not actually be there yet - // if stuff is being loaded, then the user's i/o lock will be locked by the storage impl - group.getIoLock().lock(); - group.getIoLock().unlock(); - return group.sponge(); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java index d408329b0..fe200f929 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java @@ -83,11 +83,6 @@ public class SpongeUserManager extends AbstractUserManager implement // check if the user instance is already loaded. SpongeUser user = getIfLoaded(u); if (user != null) { - // they're already loaded, but the data might not actually be there yet - // if stuff is being loaded, then the user's i/o lock will be locked by the storage impl - user.getIoLock().lock(); - user.getIoLock().unlock(); - return user.sponge(); }