diff --git a/SpongeAPI b/SpongeAPI index a6c456a3df8..7dbc81a31b2 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit a6c456a3df850597e120c3e67361a75e9608b179 +Subproject commit 7dbc81a31b2cdfe5a865bc4174dbd22c8eacd86a diff --git a/src/main/java/org/spongepowered/common/service/server/permission/DataFactoryCollection.java b/src/main/java/org/spongepowered/common/service/server/permission/DataFactoryCollection.java index 893e24eb91e..c42bd52dcca 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/DataFactoryCollection.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/DataFactoryCollection.java @@ -27,7 +27,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import org.spongepowered.api.event.Cause; -import org.spongepowered.api.service.permission.MemorySubjectData; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; diff --git a/src/main/java/org/spongepowered/common/service/server/permission/GlobalMemorySubjectData.java b/src/main/java/org/spongepowered/common/service/server/permission/GlobalMemorySubjectData.java index 602103c5df6..2d9a8dbb75e 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/GlobalMemorySubjectData.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/GlobalMemorySubjectData.java @@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableMap; import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.MemorySubjectData; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectData; import org.spongepowered.api.service.permission.SubjectReference; diff --git a/src/main/java/org/spongepowered/common/service/server/permission/MemorySubjectData.java b/src/main/java/org/spongepowered/common/service/server/permission/MemorySubjectData.java new file mode 100644 index 00000000000..3ab37f3c37f --- /dev/null +++ b/src/main/java/org/spongepowered/common/service/server/permission/MemorySubjectData.java @@ -0,0 +1,506 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * 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 org.spongepowered.common.service.server.permission; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.concurrent.ConcurrentHashMap; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.api.event.permission.SubjectDataUpdateEvent; +import org.spongepowered.api.service.context.Context; +import org.spongepowered.api.service.permission.NodeTree; +import org.spongepowered.api.service.permission.Subject; +import org.spongepowered.api.service.permission.SubjectData; +import org.spongepowered.api.service.permission.SubjectReference; +import org.spongepowered.api.service.permission.TransferMethod; +import org.spongepowered.api.util.Tristate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentMap; + +/** + * A subject data implementation storing all contained data in memory. + * + *

Users of this class who wish to correctly implement the + * {@link SubjectDataUpdateEvent} should do so by overriding the + * {@link #onUpdate()} method.

+ * + *

This class is thread-safe.

+ */ +public class MemorySubjectData implements SubjectData { + + protected final Subject subject; + protected final ConcurrentMap, Map> options = new ConcurrentHashMap<>(); + protected final ConcurrentMap, NodeTree> permissions = new ConcurrentHashMap<>(); + protected final ConcurrentMap, List> parents = new ConcurrentHashMap<>(); + + /** + * Creates a new subject data instance, using the provided service to + * request instances of permission subjects. + * + * @param subject The subject this data belongs to + */ + public MemorySubjectData(final Subject subject) { + this.subject = Objects.requireNonNull(subject, "subject"); + } + + /** + * Called each time the data in this {@link MemorySubjectData} is mutated + * in some way. + */ + protected void onUpdate() { + // Do nothing - users of the class will override. + } + + @Override + public Subject subject() { + return this.subject; + } + + @Override + public boolean isTransient() { + return true; + } + + @Override + public Map, Map> allPermissions() { + final ImmutableMap.Builder, Map> ret = ImmutableMap.builder(); + for (final Map.Entry, NodeTree> ent : this.permissions.entrySet()) { + ret.put(ent.getKey(), ent.getValue().asMap()); + } + return ret.build(); + } + + /** + * Gets the calculated node tree representation of the permissions for this + * subject data instance. If no data is present for the given context, + * returns null. + * + * @param contexts The contexts to get a node tree for + * @return The node tree + */ + public NodeTree nodeTree(final Set contexts) { + final NodeTree perms = this.permissions.get(Objects.requireNonNull(contexts, "contexts")); + return perms == null ? NodeTree.of(Collections.emptyMap()) : perms; + } + + @Override + public Map permissions(final Set contexts) { + final NodeTree perms = this.permissions.get(Objects.requireNonNull(contexts, "contexts")); + return perms == null ? ImmutableMap.of() : perms.asMap(); + } + + @Override + public CompletableFuture setPermission(Set contexts, final String permission, final Tristate value) { + Objects.requireNonNull(contexts, "contexts"); + Objects.requireNonNull(permission, "permission"); + Objects.requireNonNull(value, "value"); + contexts = ImmutableSet.copyOf(contexts); + while (true) { + final NodeTree oldTree = this.permissions.get(contexts); + if (oldTree != null && oldTree.get(permission) == value) { + return CompletableFuture.completedFuture(false); + } + + if (oldTree == null && value != Tristate.UNDEFINED) { + if (this.permissions.putIfAbsent(contexts, NodeTree.of(Collections.singletonMap(permission, value.asBoolean()))) == null) { + break; + } + } else { + if (oldTree == null || this.permissions.replace(contexts, oldTree, oldTree.withValue(permission, value))) { + break; + } + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture setPermissions(Set contexts, final @Nullable Map permissions, final TransferMethod method) { + contexts = ImmutableSet.copyOf(Objects.requireNonNull(contexts, "contexts")); + Objects.requireNonNull(method, "method"); + + outer: while (true) { + final NodeTree oldTree = this.permissions.get(contexts); + switch (method) { + case MERGE: + if (permissions == null) { + break outer; + } + + final NodeTree newTree; + if (oldTree != null) { + newTree = oldTree.withAll(permissions); + } else { + newTree = NodeTree.of(permissions); + } + if (this.updateCollection(this.permissions, contexts, oldTree, newTree)) { + break outer; + } + break; + case OVERWRITE: + if (this.updateCollection(this.permissions, contexts, oldTree, NodeTree.of(permissions == null ? ImmutableMap.of() : permissions))) { + break outer; + } + + break; + default: + throw new IllegalStateException("Unhandled enum state " + method); + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public Tristate fallbackPermissionValue(final Set contexts) { + final NodeTree tree = this.permissions.get(Objects.requireNonNull(contexts, "contexts")); + return tree == null ? Tristate.UNDEFINED : tree.rootValue(); + } + + @Override + public Map, Tristate> allFallbackPermissionValues() { + final ImmutableMap.Builder, Tristate> builder = ImmutableMap.builder(); + + for (final Map.Entry, NodeTree> entry : this.permissions.entrySet()) { + builder.put(entry.getKey(), entry.getValue().rootValue()); + } + return builder.build(); + } + + @Override + public CompletableFuture setFallbackPermissionValue(Set contexts, final Tristate fallback) { + contexts = ImmutableSet.copyOf(Objects.requireNonNull(contexts, "contexts")); + Objects.requireNonNull(fallback, "fallback"); + + while (true) { + final NodeTree oldTree = this.permissions.get(contexts); + if (oldTree != null && oldTree.rootValue() == fallback) { + return CompletableFuture.completedFuture(false); + } + + if (oldTree == null && fallback != Tristate.UNDEFINED) { + if (this.permissions.putIfAbsent(contexts, NodeTree.of(ImmutableMap.of(), fallback)) == null) { + break; + } + } else { + if (oldTree == null || this.permissions.replace(contexts, oldTree, oldTree.withRootValue(fallback))) { + break; + } + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture clearFallbackPermissionValues() { + boolean anyUpdated = false; + for (final Set key : this.permissions.keySet()) { + while (true) { + final NodeTree oldTree = this.permissions.get(key); + if (oldTree == null || oldTree.rootValue() == Tristate.UNDEFINED) { + continue; + } + + if (this.updateCollection(this.permissions, key, oldTree, oldTree.withRootValue(Tristate.UNDEFINED))) { + anyUpdated = true; + break; + } + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(anyUpdated); + } + + @Override + public CompletableFuture clearPermissions() { + final boolean wasEmpty = this.permissions.isEmpty(); + this.permissions.clear(); + if (!wasEmpty) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(!wasEmpty); + } + + @Override + public CompletableFuture clearPermissions(final Set context) { + final boolean changed = this.permissions.remove(Objects.requireNonNull(context, "context")) != null; + if (changed) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(changed); + } + + @Override + public Map, List> allParents() { + return ImmutableMap.copyOf(this.parents); + } + + @Override + public List parents(final Set contexts) { + return this.parents.getOrDefault(Objects.requireNonNull(contexts, "contexts"), ImmutableList.of()); + } + + @Override + public CompletableFuture setParents(Set contexts, final List parents, final TransferMethod method) { + contexts = ImmutableSet.copyOf(Objects.requireNonNull(contexts, "contexts")); + Objects.requireNonNull(method, "method"); + + outer: while (true) { + final List oldParents = this.parents.get(contexts); + switch (method) { + case MERGE: + if (oldParents != null) { + final List newParents = new ArrayList<>(oldParents); + newParents.removeAll(parents); + newParents.addAll(parents); + if (this.parents.replace(contexts, oldParents, newParents)) { + break outer; + } + } else { + if (this.parents.putIfAbsent(contexts, ImmutableList.copyOf(parents)) == null) { + break outer; + } + } + break; + case OVERWRITE: + if (this.parents.replace(contexts, oldParents, ImmutableList.copyOf(parents))) { + break outer; + } + break; + default: + throw new IllegalStateException("Unhandled enum state " + method); + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture addParent(Set contexts, final SubjectReference parent) { + Objects.requireNonNull(contexts, "contexts"); + Objects.requireNonNull(parent, "parent"); + contexts = ImmutableSet.copyOf(contexts); + while (true) { + final List oldParents = this.parents.get(contexts); + if (oldParents != null && oldParents.contains(parent)) { + return CompletableFuture.completedFuture(false); + } + + final List newParents = ImmutableList.builder() + .addAll(oldParents == null ? ImmutableList.of() : oldParents) + .add(parent) + .build(); + + if (this.updateCollection(this.parents, contexts, oldParents, newParents)) { + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + } + } + + private boolean updateCollection(final ConcurrentMap collection, final K key, final @Nullable V oldValue, final @Nullable V newValue) { + if (newValue == null) { + return oldValue == null ? !collection.containsKey(key) : collection.remove(key, oldValue); + } else if (oldValue == null) { + return collection.putIfAbsent(key, newValue) == null; + } else { + return collection.replace(key, oldValue, newValue); + } + } + + @Override + public CompletableFuture removeParent(Set contexts, final SubjectReference parent) { + Objects.requireNonNull(contexts, "contexts"); + Objects.requireNonNull(parent, "parent"); + contexts = ImmutableSet.copyOf(contexts); + while (true) { + final List oldParents = this.parents.get(contexts); + if (oldParents == null || !oldParents.contains(parent)) { + return CompletableFuture.completedFuture(false); + } + + final List newParents = new ArrayList<>(oldParents); + newParents.remove(parent); + + if (this.updateCollection(this.parents, contexts, oldParents, ImmutableList.copyOf(newParents))) { + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + } + } + + @Override + public CompletableFuture clearParents() { + final boolean wasEmpty = this.parents.isEmpty(); + this.parents.clear(); + if (!wasEmpty) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(!wasEmpty); + } + + @Override + public CompletableFuture clearParents(final Set contexts) { + final boolean changed = this.parents.remove(Objects.requireNonNull(contexts, "contexts")) != null; + if (changed) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(changed); + } + + @Override + public Map, Map> allOptions() { + return ImmutableMap.copyOf(this.options); + } + + @Override + public Map options(final Set contexts) { + return this.options.getOrDefault(Objects.requireNonNull(contexts, "contexts"), Collections.emptyMap()); + } + + @Override + public CompletableFuture setOption(Set contexts, final String key, final @Nullable String value) { + contexts = ImmutableSet.copyOf(Objects.requireNonNull(contexts, "contexts")); + Objects.requireNonNull(key, "key"); + @Nullable Map origMap = this.options.get(contexts); + Map newMap; + + if (origMap == null) { + if (value == null) { + return CompletableFuture.completedFuture(false); + } + + if ((origMap = this.options.putIfAbsent(contexts, Collections.singletonMap(key.toLowerCase(), value))) == null) { + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + } + do { + if (value == null) { + if (!origMap.containsKey(key)) { + return CompletableFuture.completedFuture(false); + } + newMap = new HashMap<>(origMap); + newMap.remove(key); + } else { + newMap = new HashMap<>(origMap); + newMap.put(key, value); + } + newMap = ImmutableMap.copyOf(newMap); + } while (!this.options.replace(contexts, origMap, newMap)); + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture setOptions(Set contexts, final @Nullable Map options, final TransferMethod method) { + contexts = ImmutableSet.copyOf(Objects.requireNonNull(contexts, "contexts")); + Objects.requireNonNull(method, "method"); + + outer: while (true) { + final Map oldOptions = this.options.get(contexts); + switch (method) { + case MERGE: + if (options == null) { + break outer; + } + + final Map newOptions = oldOptions == null ? new HashMap<>() : new HashMap<>(oldOptions); + newOptions.putAll(options); + if (this.updateCollection(this.options, contexts, oldOptions, newOptions)) { + break outer; + } + break; + case OVERWRITE: + if (this.updateCollection(this.options, contexts, oldOptions, options == null ? null : ImmutableMap.copyOf(options))) { + break outer; + } + + break; + default: + throw new IllegalStateException("Unhandled enum state " + method); + } + } + this.onUpdate(); + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture clearOptions() { + final boolean wasEmpty = this.options.isEmpty(); + this.options.clear(); + if (!wasEmpty) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(!wasEmpty); + } + + @Override + public CompletableFuture clearOptions(final Set contexts) { + final boolean ret = this.options.remove(Objects.requireNonNull(contexts, "contexts")) != null; + if (ret) { + this.onUpdate(); + } + return CompletableFuture.completedFuture(ret); + } + + @Override + public CompletableFuture copyFrom(final SubjectData other, final TransferMethod method) { + Objects.requireNonNull(other, "other"); + Objects.requireNonNull(method, "method"); + final Map, Map> otherPerms = other.allPermissions(); + final Map, Map> otherOptions = other.allOptions(); + final Map, ? extends List> otherParents = other.allParents(); + + if (method == TransferMethod.OVERWRITE) { + this.permissions.clear(); + this.parents.clear(); + this.options.clear(); + } + + otherPerms.forEach((ctx, permissions) -> this.setPermissions(ctx, permissions, method)); + otherOptions.forEach((ctx, options) -> this.setOptions(ctx, options, method)); + otherParents.forEach((ctx, parents) -> this.setParents(ctx, parents, method)); + + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture moveFrom(final SubjectData other, final TransferMethod method) { + return this.copyFrom(other, method).thenCompose(res -> + CompletableFuture.allOf(other.clearOptions(), other.clearParents(), other.clearPermissions()).thenApply(x -> res)); + } +} diff --git a/src/main/java/org/spongepowered/common/service/server/permission/OpLevelCollection.java b/src/main/java/org/spongepowered/common/service/server/permission/OpLevelCollection.java index af4033709c7..0994d18d9ba 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/OpLevelCollection.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/OpLevelCollection.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.MemorySubjectData; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; diff --git a/src/main/java/org/spongepowered/common/service/server/permission/SpongeBaseSubject.java b/src/main/java/org/spongepowered/common/service/server/permission/SpongeBaseSubject.java index cddf1e57918..1bac00ffc13 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/SpongeBaseSubject.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/SpongeBaseSubject.java @@ -27,7 +27,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.api.event.Cause; import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.MemorySubjectData; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectData; diff --git a/src/main/java/org/spongepowered/common/service/server/permission/SpongeSubject.java b/src/main/java/org/spongepowered/common/service/server/permission/SpongeSubject.java index d5736128818..69349b2e5e9 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/SpongeSubject.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/SpongeSubject.java @@ -24,8 +24,6 @@ */ package org.spongepowered.common.service.server.permission; -import org.spongepowered.api.service.permission.MemorySubjectData; - public abstract class SpongeSubject extends SpongeBaseSubject { @Override diff --git a/src/main/java/org/spongepowered/common/service/server/permission/UserSubject.java b/src/main/java/org/spongepowered/common/service/server/permission/UserSubject.java index dc10fd56871..048825dbca4 100644 --- a/src/main/java/org/spongepowered/common/service/server/permission/UserSubject.java +++ b/src/main/java/org/spongepowered/common/service/server/permission/UserSubject.java @@ -29,7 +29,6 @@ import net.minecraft.server.players.ServerOpListEntry; import org.spongepowered.api.Sponge; import org.spongepowered.api.event.Cause; -import org.spongepowered.api.service.permission.MemorySubjectData; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.permission.SubjectReference;