Track individual changes to users/groups instead of writing in full on each save (#2767)

This commit is contained in:
lucko 2020-12-13 13:08:15 +00:00 committed by GitHub
parent 97d1deec9c
commit 478fddc486
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1551 additions and 1234 deletions

View File

@ -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();

View File

@ -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);

View File

@ -84,7 +84,7 @@ public class GroupListMembers extends ChildCommand<Group> {
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));
}
}

View File

@ -1,467 +0,0 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.model;
import 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}.
*
* <p>Permissions are stored in Multimaps, with the context of the node being the key, and the actual Node object being
* the value. The keys (context sets) are ordered according to their weight {@link ContextSetComparator}, and the values
* are ordered according to the priority of the node, according to {@link NodeComparator}.</p>
*
* <p>Each holder has two of these maps, one for enduring and transient nodes.</p>
*/
public final class NodeMap {
private static final Function<ImmutableContextSet, SortedSet<Node>> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse());
private static final Function<ImmutableContextSet, SortedSet<InheritanceNode>> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse());
/**
* The holder which this map is for
*/
private final PermissionHolder holder;
/**
* The backing data map.
*
* <p>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}.</p>
*/
private final SortedMap<ImmutableContextSet, SortedSet<Node>> map = new ConcurrentSkipListMap<>(ContextSetComparator.reverse());
/**
* Copy of {@link #map} which only contains group nodes
* @see InheritanceNode
*/
private final SortedMap<ImmutableContextSet, SortedSet<InheritanceNode>> 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<Node> values : this.map.values()) {
size += values.size();
}
return size;
}
public List<Node> asList() {
List<Node> list = new ArrayList<>();
copyTo(list);
return list;
}
public LinkedHashSet<Node> asSet() {
LinkedHashSet<Node> set = new LinkedHashSet<>();
copyTo(set);
return set;
}
public SortedSet<Node> asSortedSet() {
SortedSet<Node> set = new TreeSet<>(NodeWithContextComparator.reverse());
copyTo(set);
return set;
}
public ImmutableSet<Node> asImmutableSet() {
ImmutableSet.Builder<Node> builder = ImmutableSet.builder();
for (SortedSet<Node> values : this.map.values()) {
builder.addAll(values);
}
return builder.build();
}
public Map<ImmutableContextSet, Collection<Node>> asMap() {
Map<ImmutableContextSet, Collection<Node>> map = new HashMap<>();
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> e : this.map.entrySet()) {
map.put(e.getKey(), new ArrayList<>(e.getValue()));
}
return map;
}
public List<InheritanceNode> inheritanceAsList() {
List<InheritanceNode> set = new ArrayList<>();
copyInheritanceNodesTo(set);
return set;
}
public LinkedHashSet<InheritanceNode> inheritanceAsSet() {
LinkedHashSet<InheritanceNode> set = new LinkedHashSet<>();
copyInheritanceNodesTo(set);
return set;
}
public SortedSet<InheritanceNode> inheritanceAsSortedSet() {
SortedSet<InheritanceNode> set = new TreeSet<>(NodeWithContextComparator.reverse());
copyInheritanceNodesTo(set);
return set;
}
public Map<ImmutableContextSet, Collection<InheritanceNode>> inheritanceAsMap() {
Map<ImmutableContextSet, Collection<InheritanceNode>> map = new HashMap<>();
for (Map.Entry<ImmutableContextSet, SortedSet<InheritanceNode>> 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<? super Node> consumer) {
for (SortedSet<Node> values : this.map.values()) {
values.forEach(consumer);
}
}
public void forEach(QueryOptions filter, Consumer<? super Node> consumer) {
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> 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<InheritanceNode> inheritanceNodes = this.inheritanceMap.get(e.getKey());
if (inheritanceNodes != null) {
inheritanceNodes.forEach(consumer);
}
}
} else {
e.getValue().forEach(consumer);
}
}
}
public void copyTo(Collection<? super Node> collection) {
for (SortedSet<Node> values : this.map.values()) {
collection.addAll(values);
}
}
public void copyTo(Collection<? super Node> collection, QueryOptions filter) {
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> 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<InheritanceNode> inheritanceNodes = this.inheritanceMap.get(e.getKey());
if (inheritanceNodes != null) {
collection.addAll(inheritanceNodes);
}
}
} else {
collection.addAll(e.getValue());
}
}
}
public <T extends Node> void copyTo(Collection<? super T> collection, NodeType<T> type, QueryOptions filter) {
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> 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<InheritanceNode> 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<? super InheritanceNode> collection) {
for (SortedSet<InheritanceNode> values : this.inheritanceMap.values()) {
collection.addAll(values);
}
}
public void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection, QueryOptions filter) {
for (Map.Entry<ImmutableContextSet, SortedSet<InheritanceNode>> e : this.inheritanceMap.entrySet()) {
if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) {
continue;
}
if (inheritanceNodesIncludeTest(filter, e.getKey())) {
collection.addAll(e.getValue());
}
}
}
public Collection<Node> nodesInContext(ContextSet context) {
final SortedSet<Node> values = this.map.get(context.immutableCopy());
if (values == null) {
return ImmutableSet.of();
}
return new ArrayList<>(values);
}
public Collection<InheritanceNode> inheritanceNodesInContext(ContextSet context) {
final SortedSet<InheritanceNode> values = this.inheritanceMap.get(context.immutableCopy());
if (values == null) {
return ImmutableSet.of();
}
return new ArrayList<>(values);
}
private Node localise(Node node) {
Optional<InheritanceOriginMetadata> 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<Node> 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<InheritanceNode> 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<Node> 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<InheritanceNode> 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<Node> nodesInContext = this.map.get(context);
if (nodesInContext != null) {
nodesInContext.remove(node);
}
if (node instanceof InheritanceNode && node.getValue()) {
SortedSet<InheritanceNode> 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<? extends Node> set) {
this.map.clear();
this.inheritanceMap.clear();
mergeContent(set);
}
void setContent(Stream<? extends Node> stream) {
this.map.clear();
this.inheritanceMap.clear();
mergeContent(stream);
}
void mergeContent(Iterable<? extends Node> set) {
for (Node n : set) {
add(n);
}
}
void mergeContent(Stream<? extends Node> stream) {
stream.forEach(this::add);
}
boolean removeIf(Predicate<? super Node> predicate) {
boolean success = false;
for (SortedSet<Node> valueSet : this.map.values()) {
if (valueSet.removeIf(predicate)) {
success = true;
}
}
for (SortedSet<InheritanceNode> valueSet : this.inheritanceMap.values()) {
valueSet.removeIf(predicate);
}
return success;
}
boolean removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
ImmutableContextSet context = contextSet.immutableCopy();
boolean success = false;
SortedSet<Node> nodesInContext = this.map.get(context);
if (nodesInContext != null) {
success = nodesInContext.removeIf(predicate);
}
SortedSet<InheritanceNode> inheritanceNodesInContext = this.inheritanceMap.get(context);
if (inheritanceNodesInContext != null) {
inheritanceNodesInContext.removeIf(predicate);
}
return success;
}
boolean auditTemporaryNodes(@Nullable Set<? super Node> removed) {
boolean work = false;
for (SortedSet<Node> valueSet : this.map.values()) {
Iterator<Node> 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<InheritanceNode> inheritanceNodesInContext = this.inheritanceMap.get(entry.getContexts());
if (inheritanceNodesInContext != null) {
inheritanceNodesInContext.remove(entry);
}
}
it.remove();
work = true;
}
}
return work;
}
}

View File

@ -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<? super PermissionHolder> 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<? extends Node> 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<? extends Node> set) {
getData(type).setContent(set);
invalidateCache();
}
public void setNodes(DataType type, Stream<? extends Node> stream) {
getData(type).setContent(stream);
invalidateCache();
}
public void mergeNodes(DataType type, Iterable<? extends Node> set) {
getData(type).mergeContent(set);
getData(type).addAll(set);
invalidateCache();
}
@ -436,20 +427,19 @@ public abstract class PermissionHolder {
private boolean auditTemporaryNodes(DataType dataType) {
ImmutableSet<Node> before = getData(dataType).asImmutableSet();
Set<Node> removed = new HashSet<>();
boolean work = getData(dataType).auditTemporaryNodes(removed);
if (work) {
MutateResult result = getData(dataType).removeIf(Node::hasExpired);
if (!result.isEmpty()) {
// call event
ImmutableSet<Node> 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<Node> before = data.asImmutableSet();
data.replace(newNode, otherMatch);
data.removeThenAdd(otherMatch, newNode);
ImmutableSet<Node> 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<Node> before = data.asImmutableSet();
data.replace(newNode, otherMatch);
data.removeThenAdd(otherMatch, newNode);
ImmutableSet<Node> after = data.asImmutableSet();
this.plugin.getEventDispatcher().dispatchNodeRemove(otherMatch, this, dataType, before, after);
@ -594,17 +584,17 @@ public abstract class PermissionHolder {
ImmutableSet<Node> 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<Node> 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<Node> after = data.asImmutableSet();

View File

@ -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;
}

View File

@ -79,8 +79,8 @@ public abstract class AbstractUserManager<T extends User> extends AbstractManage
}
@Override
public boolean giveDefaultIfNeeded(User user, boolean save) {
boolean work = false;
public boolean giveDefaultIfNeeded(User user) {
boolean requireSave = false;
Collection<InheritanceNode> globalGroups = user.normalData().inheritanceNodesInContext(ImmutableContextSetImpl.EMPTY);
@ -106,7 +106,7 @@ public abstract class AbstractUserManager<T extends User> 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<T extends User> 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<Node> 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<T extends User> 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<Node> 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);
}
}

View File

@ -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<T extends User> extends Manager<UUID, User, T> {
*
* @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<T extends User> extends Manager<UUID, User, T> {
* @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.

View File

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

View File

@ -0,0 +1,161 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.model.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<Node> asList() {
List<Node> list = new ArrayList<>();
copyTo(list);
return list;
}
default LinkedHashSet<Node> asSet() {
LinkedHashSet<Node> set = new LinkedHashSet<>();
copyTo(set);
return set;
}
default SortedSet<Node> asSortedSet() {
SortedSet<Node> set = new TreeSet<>(NodeWithContextComparator.reverse());
copyTo(set);
return set;
}
default ImmutableSet<Node> asImmutableSet() {
ImmutableSet.Builder<Node> builder = ImmutableSet.builder();
copyTo(builder);
return builder.build();
}
Map<ImmutableContextSet, Collection<Node>> asMap();
default List<InheritanceNode> inheritanceAsList() {
List<InheritanceNode> set = new ArrayList<>();
copyInheritanceNodesTo(set);
return set;
}
default LinkedHashSet<InheritanceNode> inheritanceAsSet() {
LinkedHashSet<InheritanceNode> set = new LinkedHashSet<>();
copyInheritanceNodesTo(set);
return set;
}
default SortedSet<InheritanceNode> inheritanceAsSortedSet() {
SortedSet<InheritanceNode> set = new TreeSet<>(NodeWithContextComparator.reverse());
copyInheritanceNodesTo(set);
return set;
}
default ImmutableSet<InheritanceNode> inheritanceAsImmutableSet() {
ImmutableSet.Builder<InheritanceNode> builder = ImmutableSet.builder();
copyInheritanceNodesTo(builder);
return builder.build();
}
Map<ImmutableContextSet, Collection<InheritanceNode>> inheritanceAsMap();
void forEach(Consumer<? super Node> consumer);
void forEach(QueryOptions filter, Consumer<? super Node> consumer);
void copyTo(Collection<? super Node> collection);
void copyTo(ImmutableCollection.Builder<? super Node> collection);
void copyTo(Collection<? super Node> collection, QueryOptions filter);
<T extends Node> void copyTo(Collection<? super T> collection, NodeType<T> type, QueryOptions filter);
void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection);
void copyInheritanceNodesTo(ImmutableCollection.Builder<? super InheritanceNode> collection);
void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection, QueryOptions filter);
Collection<Node> nodesInContext(ContextSet context);
Collection<InheritanceNode> inheritanceNodesInContext(ContextSet context);
// mutate methods
MutateResult add(Node nodeWithoutInheritanceOrigin);
MutateResult remove(Node node);
MutateResult removeExact(Node node);
MutateResult removeIf(Predicate<? super Node> predicate);
MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> predicate);
MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd);
MutateResult clear();
MutateResult clear(ContextSet contextSet);
MutateResult setContent(Iterable<? extends Node> set);
MutateResult setContent(Stream<? extends Node> stream);
MutateResult addAll(Iterable<? extends Node> set);
MutateResult addAll(Stream<? extends Node> stream);
}

View File

@ -0,0 +1,243 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.model.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<ImmutableContextSet, SortedSet<Node>> map();
protected abstract SortedMap<ImmutableContextSet, SortedSet<InheritanceNode>> inheritanceMap();
protected abstract ContextSatisfyMode defaultSatisfyMode();
@Override
public boolean isEmpty() {
return map().isEmpty();
}
@Override
public int size() {
int size = 0;
for (SortedSet<Node> values : map().values()) {
size += values.size();
}
return size;
}
@Override
public Map<ImmutableContextSet, Collection<Node>> asMap() {
Map<ImmutableContextSet, Collection<Node>> map = new HashMap<>();
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> e : map().entrySet()) {
map.put(e.getKey(), new ArrayList<>(e.getValue()));
}
return map;
}
@Override
public Map<ImmutableContextSet, Collection<InheritanceNode>> inheritanceAsMap() {
Map<ImmutableContextSet, Collection<InheritanceNode>> map = new HashMap<>();
for (Map.Entry<ImmutableContextSet, SortedSet<InheritanceNode>> e : inheritanceMap().entrySet()) {
map.put(e.getKey(), new ArrayList<>(e.getValue()));
}
return map;
}
@Override
public void forEach(Consumer<? super Node> consumer) {
for (SortedSet<Node> values : map().values()) {
values.forEach(consumer);
}
}
@Override
public void forEach(QueryOptions filter, Consumer<? super Node> consumer) {
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> e : map().entrySet()) {
if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) {
continue;
}
if (normalNodesExcludeTest(filter, e.getKey())) {
if (inheritanceNodesIncludeTest(filter, e.getKey())) {
SortedSet<InheritanceNode> inheritanceNodes = inheritanceMap().get(e.getKey());
if (inheritanceNodes != null) {
inheritanceNodes.forEach(consumer);
}
}
} else {
e.getValue().forEach(consumer);
}
}
}
@Override
public void copyTo(Collection<? super Node> collection) {
for (SortedSet<Node> values : map().values()) {
collection.addAll(values);
}
}
@Override
public void copyTo(ImmutableCollection.Builder<? super Node> collection) {
for (SortedSet<Node> values : map().values()) {
collection.addAll(values);
}
}
@Override
public void copyTo(Collection<? super Node> collection, QueryOptions filter) {
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> e : map().entrySet()) {
if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) {
continue;
}
if (normalNodesExcludeTest(filter, e.getKey())) {
if (inheritanceNodesIncludeTest(filter, e.getKey())) {
SortedSet<InheritanceNode> inheritanceNodes = inheritanceMap().get(e.getKey());
if (inheritanceNodes != null) {
collection.addAll(inheritanceNodes);
}
}
} else {
collection.addAll(e.getValue());
}
}
}
@Override
public <T extends Node> void copyTo(Collection<? super T> collection, NodeType<T> type, QueryOptions filter) {
if (type == NodeType.INHERITANCE) {
//noinspection unchecked
copyInheritanceNodesTo((Collection<? super InheritanceNode>) collection, filter);
return;
}
for (Map.Entry<ImmutableContextSet, SortedSet<Node>> 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<? super InheritanceNode> collection) {
for (SortedSet<InheritanceNode> values : inheritanceMap().values()) {
collection.addAll(values);
}
}
@Override
public void copyInheritanceNodesTo(ImmutableCollection.Builder<? super InheritanceNode> collection) {
for (SortedSet<InheritanceNode> values : inheritanceMap().values()) {
collection.addAll(values);
}
}
@Override
public void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection, QueryOptions filter) {
for (Map.Entry<ImmutableContextSet, SortedSet<InheritanceNode>> e : inheritanceMap().entrySet()) {
if (!filter.satisfies(e.getKey(), defaultSatisfyMode())) {
continue;
}
if (inheritanceNodesIncludeTest(filter, e.getKey())) {
collection.addAll(e.getValue());
}
}
}
@Override
public Collection<Node> nodesInContext(ContextSet context) {
return copy(map().get(context.immutableCopy()));
}
@Override
public Collection<InheritanceNode> inheritanceNodesInContext(ContextSet context) {
return copy(inheritanceMap().get(context.immutableCopy()));
}
private static <T> Collection<T> copy(Collection<T> 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);
}
}

View File

@ -0,0 +1,404 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.model.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<ImmutableContextSet, SortedSet<Node>> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse());
private static final Function<ImmutableContextSet, SortedSet<InheritanceNode>> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse());
// Creates the Map instances used by this.map and this.inheritanceMap
private static <N extends Node> SortedMap<ImmutableContextSet, SortedSet<N>> 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<ImmutableContextSet, SortedSet<Node>> map = createMap();
private SortedMap<ImmutableContextSet, SortedSet<InheritanceNode>> 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<ImmutableContextSet, SortedSet<Node>> map() {
return this.map;
}
@Override
protected SortedMap<ImmutableContextSet, SortedSet<InheritanceNode>> inheritanceMap() {
return this.inheritanceMap;
}
@Override
protected ContextSatisfyMode defaultSatisfyMode() {
return this.holder.getPlugin().getConfiguration().get(ConfigKeys.CONTEXT_SATISFY_MODE);
}
private Node addInheritanceOrigin(Node node) {
Optional<InheritanceOriginMetadata> 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<Node> 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<InheritanceNode> 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<Node> 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<InheritanceNode> 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<Node> 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<Node> 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<InheritanceNode> inhNodes = this.inheritanceMap.get(context);
if (inhNodes != null) {
inhNodes.remove(node);
}
}
}
} finally {
this.lock.unlock();
}
return result;
}
@Override
public MutateResult removeIf(Predicate<? super Node> predicate) {
MutateResult result = new MutateResult();
this.lock.lock();
try {
for (SortedSet<Node> nodes : this.map.values()) {
removeMatching(nodes.iterator(), predicate, result);
}
} finally {
this.lock.unlock();
}
return result;
}
@Override
public MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> predicate) {
ImmutableContextSet context = contextSet.immutableCopy();
MutateResult result = new MutateResult();
this.lock.lock();
try {
SortedSet<Node> 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<Node> it, Predicate<? super Node> 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<InheritanceNode> 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<Node> 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<Node> 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<? extends Node> 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<? extends Node> 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<? extends Node> 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<? extends Node> stream) {
MutateResult result = new MutateResult();
this.lock.lock();
try {
stream.forEach(n -> result.mergeFrom(add(n)));
} finally {
this.lock.unlock();
}
return result;
}
}

View File

@ -0,0 +1,187 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.model.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<MutateResult> 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<? super Node> predicate) {
return record(this.delegate.removeIf(predicate));
}
@Override
public MutateResult removeIf(ContextSet contextSet, Predicate<? super Node> 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<? extends Node> set) {
return record(this.delegate.setContent(set));
}
@Override
public MutateResult setContent(Stream<? extends Node> stream) {
return record(this.delegate.setContent(stream));
}
@Override
public MutateResult addAll(Iterable<? extends Node> set) {
return record(this.delegate.addAll(set));
}
@Override
public MutateResult addAll(Stream<? extends Node> 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<Node> asList() { return this.delegate.asList(); }
@Override public LinkedHashSet<Node> asSet() { return this.delegate.asSet(); }
@Override public SortedSet<Node> asSortedSet() { return this.delegate.asSortedSet(); }
@Override public ImmutableSet<Node> asImmutableSet() { return this.delegate.asImmutableSet(); }
@Override public Map<ImmutableContextSet, Collection<Node>> asMap() { return this.delegate.asMap(); }
@Override public List<InheritanceNode> inheritanceAsList() { return this.delegate.inheritanceAsList(); }
@Override public LinkedHashSet<InheritanceNode> inheritanceAsSet() { return this.delegate.inheritanceAsSet(); }
@Override public SortedSet<InheritanceNode> inheritanceAsSortedSet() { return this.delegate.inheritanceAsSortedSet(); }
@Override public ImmutableSet<InheritanceNode> inheritanceAsImmutableSet() { return this.delegate.inheritanceAsImmutableSet(); }
@Override public Map<ImmutableContextSet, Collection<InheritanceNode>> inheritanceAsMap() { return this.delegate.inheritanceAsMap(); }
@Override public void forEach(Consumer<? super Node> consumer) { this.delegate.forEach(consumer); }
@Override public void forEach(QueryOptions filter, Consumer<? super Node> consumer) { this.delegate.forEach(filter, consumer); }
@Override public void copyTo(Collection<? super Node> collection) { this.delegate.copyTo(collection); }
@Override public void copyTo(ImmutableCollection.Builder<? super Node> collection) { this.delegate.copyTo(collection); }
@Override public void copyTo(Collection<? super Node> collection, QueryOptions filter) { this.delegate.copyTo(collection, filter); }
@Override public <T extends Node> void copyTo(Collection<? super T> collection, NodeType<T> type, QueryOptions filter) { this.delegate.copyTo(collection, type, filter); }
@Override public void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection) { this.delegate.copyInheritanceNodesTo(collection); }
@Override public void copyInheritanceNodesTo(ImmutableCollection.Builder<? super InheritanceNode> collection) { this.delegate.copyInheritanceNodesTo(collection); }
@Override public void copyInheritanceNodesTo(Collection<? super InheritanceNode> collection, QueryOptions filter) { this.delegate.copyInheritanceNodesTo(collection, filter); }
@Override public Collection<Node> nodesInContext(ContextSet context) { return this.delegate.nodesInContext(context); }
@Override public Collection<InheritanceNode> inheritanceNodesInContext(ContextSet context) { return this.delegate.inheritanceNodesInContext(context); }
}

View File

@ -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<Group> 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<Track> 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<String> 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());
}

View File

@ -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<Document> c = this.database.getCollection(this.prefix + "users");
try (MongoCursor<Document> cursor = c.find(new Document("_id", user.getUniqueId())).iterator()) {
if (cursor.hasNext()) {
// User exists, let's load.
Document d = cursor.next();
MongoCollection<Document> c = this.database.getCollection(this.prefix + "users");
try (MongoCursor<Document> 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<Document> 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<Document> 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<Document> c = this.database.getCollection(this.prefix + "groups");
try (MongoCursor<Document> 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<Document> c = this.database.getCollection(this.prefix + "groups");
try (MongoCursor<Document> 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<Group> loadGroup(String name) {
Group group = this.plugin.getGroupManager().getIfLoaded(name);
if (group != null) {
group.getIoLock().lock();
}
try {
MongoCollection<Document> c = this.database.getCollection(this.prefix + "groups");
try (MongoCursor<Document> 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<Document> c = this.database.getCollection(this.prefix + "groups");
try (MongoCursor<Document> 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<Document> 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<Document> 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<Document> c = this.database.getCollection(this.prefix + "groups");
c.deleteOne(new Document("_id", group.getName()));
} finally {
group.getIoLock().unlock();
}
MongoCollection<Document> 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<Document> c = this.database.getCollection(this.prefix + "tracks");
try (MongoCursor<Document> 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<String>) d.get("groups"));
}
MongoCollection<Document> c = this.database.getCollection(this.prefix + "tracks");
try (MongoCursor<Document> 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<String>) d.get("groups"));
}
} finally {
track.getIoLock().unlock();
}
return track;
}
@Override
public Optional<Track> loadTrack(String name) {
Track track = this.plugin.getTrackManager().getIfLoaded(name);
if (track != null) {
track.getIoLock().lock();
}
try {
MongoCollection<Document> c = this.database.getCollection(this.prefix + "tracks");
try (MongoCursor<Document> 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<String>) d.get("groups"));
}
} finally {
if (track != null) {
track.getIoLock().unlock();
MongoCollection<Document> c = this.database.getCollection(this.prefix + "tracks");
try (MongoCursor<Document> 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<String>) 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<Document> c = this.database.getCollection(this.prefix + "tracks");
c.replaceOne(new Document("_id", track.getName()), trackToDoc(track));
} finally {
track.getIoLock().unlock();
}
MongoCollection<Document> 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<Document> c = this.database.getCollection(this.prefix + "tracks");
c.deleteOne(new Document("_id", track.getName()));
} finally {
track.getIoLock().unlock();
}
MongoCollection<Document> c = this.database.getCollection(this.prefix + "tracks");
c.deleteOne(new Document("_id", track.getName()));
}
@Override

View File

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

View File

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

View File

@ -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<SqlNode> nodes;
String primaryGroup = null;
String savedUsername = null;
try (Connection c = this.connectionFactory.getConnection()) {
nodes = selectUserPermissions(new ArrayList<>(), c, user.getUniqueId());
List<Node> 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<SqlNode> 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<SqlNode> local = user.normalData().asList().stream().map(SqlNode::fromNode).collect(Collectors.toSet());
Set<SqlNode> missingFromRemote = getMissingFromRemote(local, remote);
Set<SqlNode> 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<SqlNode> 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<Node> 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<String, Collection<SqlNode>> groups = new HashMap<>();
Map<String, Collection<Node>> groups = new HashMap<>();
try (Connection c = this.connectionFactory.getConnection()) {
selectGroups(c).forEach(name -> groups.put(name, new ArrayList<>()));
selectAllGroupPermissions(groups, c);
}
for (Map.Entry<String, Collection<SqlNode>> entry : groups.entrySet()) {
for (Map.Entry<String, Collection<Node>> entry : groups.entrySet()) {
Group group = this.plugin.getGroupManager().getOrMake(entry.getKey());
group.getIoLock().lock();
try {
Collection<SqlNode> 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<Node> 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<SqlNode> 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<SqlNode> local = group.normalData().asList().stream().map(SqlNode::fromNode).collect(Collectors.toSet());
Set<SqlNode> missingFromRemote = getMissingFromRemote(local, remote);
Set<SqlNode> 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<String> groups;
try (Connection c = this.connectionFactory.getConnection()) {
groups = selectTrack(c, track.getName());
}
List<String> 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<String> groups;
try (Connection c = this.connectionFactory.getConnection()) {
groups = selectTrack(c, name);
}
track.setGroups(groups);
} finally {
track.getIoLock().unlock();
List<String> 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<String> groups = selectTrack(c, trackName);
track.setGroups(groups);
} finally {
track.getIoLock().unlock();
}
List<String> 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<String> 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<SqlNode> getMissingFromRemote(Set<SqlNode> local, Set<SqlNode> remote) {
// entries in local but not remote need to be added
Set<SqlNode> 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<SqlNode> getMissingFromLocal(Set<SqlNode> local, Set<SqlNode> remote) {
// entries in remote but not local need to be removed
Set<SqlNode> missingFromLocal = new HashSet<>(remote);
missingFromLocal.removeAll(local);
return missingFromLocal;
private void updateUserPermissions(Connection c, UUID user, Set<Node> add, Set<Node> delete) throws SQLException {
updatePermissions(c, user.toString(), add, delete, USER_PERMISSIONS_DELETE_SPECIFIC, USER_PERMISSIONS_DELETE_SPECIFIC_PROPS, USER_PERMISSIONS_INSERT);
}
private <T extends Collection<SqlNode>> T selectUserPermissions(T nodes, Connection c, UUID user) throws SQLException {
private void updateGroupPermissions(Connection c, String group, Set<Node> add, Set<Node> 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<Node> add, Set<Node> delete, String deleteSpecificQuery, String deleteQuery, String insertQuery) throws SQLException {
if (!delete.isEmpty()) {
List<Long> deleteRows = new ArrayList<>(delete.size());
List<Node> 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<Node> selectUserPermissions(Connection c, UUID user) throws SQLException {
List<Node> 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<SqlNode> add, Set<SqlNode> 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 extends Collection<SqlNode>> T selectGroupPermissions(T nodes, Connection c, String group) throws SQLException {
private List<Node> selectGroupPermissions(Connection c, String group) throws SQLException {
List<Node> 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<String, Collection<SqlNode>> nodes, Connection c) throws SQLException {
private void selectAllGroupPermissions(Map<String, Collection<Node>> 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<SqlNode> list = nodes.get(holder);
Collection<Node> 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<SqlNode> add, Set<SqlNode> 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<String> selectTrack(Connection c, String name) throws SQLException {
String groups;
try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) {

View File

@ -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;
}
}

View File

@ -77,11 +77,6 @@ public class SpongeGroupManager extends AbstractGroupManager<SpongeGroup> 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();
}

View File

@ -83,11 +83,6 @@ public class SpongeUserManager extends AbstractUserManager<SpongeUser> 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();
}