From ad3ca86f423971d6076061258b07a18cee02d8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Tue, 20 Oct 2020 17:49:50 +0200 Subject: [PATCH] :sparkles: Add a lockable command manager This will be used for Sponge & Fabric, that both require command registration to happen (at latest) in specific events. This way we lock writing after that event has occurred. As a side effect, we're able to collect & merge all commands before registering them to the platform, which means we don't have to hackily force-inject duplicate commands. --- CHANGELOG.md | 1 + .../LockableCommandManager.java | 123 ++++++++++++++++++ .../LockableCommandManagerTest.java | 45 +++++++ .../TestLockableCommandManager.java | 54 ++++++++ 4 files changed, 223 insertions(+) create mode 100644 cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 477f90ef..a272678f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added TextColorArgument to minecraft-extras - Added LocationArgument to cloud-bukkit - Added ServerArgument to cloud-velocity + - Added LockableCommandManager to cloud-core ## [1.0.2] - 2020-10-18 diff --git a/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java new file mode 100644 index 00000000..9f2b170f --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java @@ -0,0 +1,123 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & 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 cloud.commandframework; + +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; +import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.internal.CommandRegistrationHandler; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Function; + +/** + * {@link CommandManager} implementation that allows you to lock command registrations. + * This should be used when the platform limits command registration to a certain point in time. + *

+ * To lock writes, use {@link #lockWrites()}. To check if writing is allowed, use {@link #isCommandRegistrationAllowed()}. + * If {@link #isCommandRegistrationAllowed()} is {@code false} then {@link #command(Command)} will throw + * {@link IllegalStateException}. + * + * @param Command sender type + * @since 1.1.0 + */ +public abstract class LockableCommandManager extends CommandManager { + + private final Object writeLock = new Object(); + private volatile boolean writeLocked = false; + + /** + * Create a new command manager instance + * + * @param commandExecutionCoordinator Execution coordinator instance. The coordinator is in charge of executing incoming + * commands. Some considerations must be made when picking a suitable execution coordinator + * for your platform. For example, an entirely asynchronous coordinator is not suitable + * when the parsers used in that particular platform are not thread safe. If you have + * commands that perform blocking operations, however, it might not be a good idea to + * use a synchronous execution coordinator. In most cases you will want to pick between + * {@link CommandExecutionCoordinator#simpleCoordinator()} and + * {@link AsynchronousCommandExecutionCoordinator} + * @param commandRegistrationHandler Command registration handler. This will get called every time a new command is + * registered to the command manager. This may be used to forward command registration + */ + public LockableCommandManager( + final @NonNull Function<@NonNull CommandTree, @NonNull CommandExecutionCoordinator> commandExecutionCoordinator, + final @NonNull CommandRegistrationHandler commandRegistrationHandler + ) { + super(commandExecutionCoordinator, commandRegistrationHandler); + } + + /** + * {@inheritDoc} + *

+ * This should only be called when {@link #isCommandRegistrationAllowed()} is {@code false}, + * else {@link IllegalStateException} will be called + * + * @param command Command to register + * @return + */ + @Override + public final @NonNull CommandManager command(final @NonNull Command command) { + synchronized (this.writeLock) { + if (!isCommandRegistrationAllowed()) { + throw new IllegalStateException( + "Command registration is not allowed. The command manager has been locked." + ); + } + return super.command(command); + } + } + + /** + * {@inheritDoc} + *

+ * This should only be called when {@link #isCommandRegistrationAllowed()} is {@code false}, + * else {@link IllegalStateException} will be called + * + * @param command Command to register. {@link Command.Builder#build()}} will be invoked. + * @return + */ + @Override + public final @NonNull CommandManager command(final Command.@NonNull Builder command) { + return super.command(command); + } + + /** + * Lock writing. After this, {@link #isCommandRegistrationAllowed()} will return {@code false} + */ + protected final void lockWrites() { + synchronized (this.writeLock) { + this.writeLocked = true; + } + } + + /** + * Check if command registration is allowed + * + * @return {@code true} if the registration is allowed, else {@code false} + */ + public final boolean isCommandRegistrationAllowed() { + return !this.writeLocked; + } + +} diff --git a/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java b/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java new file mode 100644 index 00000000..ad9496ce --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/LockableCommandManagerTest.java @@ -0,0 +1,45 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & 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 cloud.commandframework; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LockableCommandManagerTest { + + @Test + void testLockableCommandManager() { + final LockableCommandManager manager = new TestLockableCommandManager(); + /* Add a command before locking */ + manager.command(manager.commandBuilder("test1")); + /* Lock */ + manager.lockWrites(); + /* Add a command after locking */ + Assertions.assertThrows( + IllegalStateException.class, + () -> manager.command(manager.commandBuilder("test2")) + ); + } + +} diff --git a/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java b/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java new file mode 100644 index 00000000..6a295230 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/TestLockableCommandManager.java @@ -0,0 +1,54 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & 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 cloud.commandframework; + +import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.internal.CommandRegistrationHandler; +import cloud.commandframework.meta.SimpleCommandMeta; + +public class TestLockableCommandManager extends LockableCommandManager { + + /** + * Construct a new test command manager + */ + public TestLockableCommandManager() { + super( + CommandExecutionCoordinator.simpleCoordinator(), + CommandRegistrationHandler.nullCommandRegistrationHandler() + ); + } + + @Override + public final SimpleCommandMeta createDefaultCommandMeta() { + return SimpleCommandMeta.empty(); + } + + @Override + public final boolean hasPermission(final TestCommandSender sender, final String permission) { + System.out.printf("Testing permission: %s\n", permission); + return !permission.equalsIgnoreCase("no"); + } + +} +