From 3f59a81836db088d087df572c4432659d8b1787e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Tue, 22 Sep 2020 23:03:11 +0200 Subject: [PATCH] Add a confirmation system. --- .../annotations/AnnotationParser.java | 9 +- .../commands/annotations/Confirmation.java | 37 ++++ .../commands/CommandTree.java | 4 +- .../arguments/parser/StandardParameters.java | 4 + .../CommandConfirmationManager.java | 160 ++++++++++++++++++ .../extra/confirmation/package-info.java | 28 +++ .../commands/meta/SimpleCommandMeta.java | 12 ++ .../commands/BukkitTest.java | 15 ++ 8 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Confirmation.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/CommandConfirmationManager.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/package-info.java diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java index 12345b1b..8364ca0e 100644 --- a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java @@ -32,7 +32,9 @@ import com.intellectualsites.commands.arguments.parser.ArgumentParser; import com.intellectualsites.commands.arguments.parser.ParserParameters; import com.intellectualsites.commands.arguments.parser.StandardParameters; import com.intellectualsites.commands.execution.CommandExecutionHandler; +import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager; import com.intellectualsites.commands.meta.CommandMeta; +import com.intellectualsites.commands.meta.SimpleCommandMeta; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -140,10 +142,15 @@ public final class AnnotationParser { /* Determine command name */ final String commandToken = commandMethod.value().split(" ")[0].split("\\|")[0]; @SuppressWarnings("ALL") final CommandManager manager = this.manager; + final SimpleCommandMeta.Builder metaBuilder = SimpleCommandMeta.builder() + .with(this.metaFactory.apply(method.getAnnotations())); + if (method.isAnnotationPresent(Confirmation.class)) { + metaBuilder.with(CommandConfirmationManager.CONFIRMATION_REQUIRED_META, "true"); + } @SuppressWarnings("ALL") Command.Builder builder = manager.commandBuilder(commandToken, tokens.get(commandToken).getMinor(), - this.metaFactory.apply(method.getAnnotations())); + metaBuilder.build()); final Collection arguments = this.argumentExtractor.apply(method); final Map> commandArguments = Maps.newHashMap(); final Map, String> argumentDescriptions = Maps.newHashMap(); diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Confirmation.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Confirmation.java new file mode 100644 index 00000000..bf905dfe --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Confirmation.java @@ -0,0 +1,37 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// 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 com.intellectualsites.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Require confirmation for the command + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Confirmation { +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java index a7bc89aa..373f407c 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -58,7 +58,7 @@ import java.util.stream.Collectors; * These arguments may be {@link StaticArgument literals} or variables. Command may either be required * or optional, with the requirement that no optional argument precedes a required argument. *

- * The {@link Command commands} are stored in this tree and the nodes of tree consists of the command + * The {@link Command commands} are stored in this tree and the nodes of tree consists of the command * {@link CommandArgument arguments}. Each leaf node of the tree should containing a fully parsed * {@link Command}. It is thus possible to walk the tree and determine whether or not the supplied * input from a command sender constitutes a proper command. @@ -548,7 +548,7 @@ public final class CommandTree { public Node> getNamedNode(@Nullable final String name) { for (final Node> node : this.getRootNodes()) { if (node.getValue() != null && node.getValue() instanceof StaticArgument) { - final StaticArgument staticArgument = (StaticArgument) node.getValue(); + @SuppressWarnings("unchecked") final StaticArgument staticArgument = (StaticArgument) node.getValue(); for (final String alias : staticArgument.getAliases()) { if (alias.equalsIgnoreCase(name)) { return node; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParameters.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParameters.java index 0491331e..efe46e12 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParameters.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParameters.java @@ -44,6 +44,10 @@ public final class StandardParameters { * Command description */ public static final ParserParameter DESCRIPTION = create("description", TypeToken.of(String.class)); + /** + * Command confirmation + */ + public static final ParserParameter CONFIRMATION = create("confirmation", TypeToken.of(Boolean.class)); /** * Command completions */ diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/CommandConfirmationManager.java b/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/CommandConfirmationManager.java new file mode 100644 index 00000000..aff1047c --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/CommandConfirmationManager.java @@ -0,0 +1,160 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// 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 com.intellectualsites.commands.extra.confirmation; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.intellectualsites.commands.CommandManager; +import com.intellectualsites.commands.execution.CommandExecutionHandler; +import com.intellectualsites.commands.execution.postprocessor.CommandPostprocessingContext; +import com.intellectualsites.commands.execution.postprocessor.CommandPostprocessor; +import com.intellectualsites.commands.meta.SimpleCommandMeta; +import com.intellectualsites.services.types.ConsumerService; + +import javax.annotation.Nonnull; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Manager for the command confirmation system that enables the ability to add "confirmation" requirements to commands, + * such that they need to be confirmed in order to be executed. + *

+ * To use the confirmation system, the confirmation post processor needs to be added. To to this, use + * {@link #registerConfirmationProcessor(CommandManager)}. After this is done, a confirmation command has + * been added. To do this, create a command builder and attach {@link #createConfirmationExecutionHandler()}. + *

+ * To require a command to be confirmed, use {@link #decorate(SimpleCommandMeta.Builder)} on the command meta builder. + * + * @param Command sender type + */ +public class CommandConfirmationManager { + + /** + * Meta data stored for commands that require confirmation + */ + public static final String CONFIRMATION_REQUIRED_META = "__REQUIRE_CONFIRMATION__"; + + private final Consumer> notifier; + private final Consumer errorNotifier; + private final Cache> pendingCommands; + + /** + * Create a new confirmation manager instance + * + * @param timeout Timeout value + * @param timeoutTimeUnit Timeout time unit + * @param notifier Notifier that gets called when a command gets added to the queue + * @param errorNotifier Notifier that gets called when someone tries to confirm a command with nothing in the queue + */ + public CommandConfirmationManager(final long timeout, + @Nonnull final TimeUnit timeoutTimeUnit, + @Nonnull final Consumer> notifier, + @Nonnull final Consumer errorNotifier) { + this.notifier = notifier; + this.errorNotifier = errorNotifier; + this.pendingCommands = CacheBuilder.newBuilder().expireAfterWrite(timeout, timeoutTimeUnit).concurrencyLevel(1).build(); + } + + private void notifyConsumer(@Nonnull final CommandPostprocessingContext context) { + this.notifier.accept(context); + } + + private void addPending(@Nonnull final CommandPostprocessingContext context) { + this.pendingCommands.put(context.getCommandContext().getSender(), context); + } + + /** + * Get a pending context if one is stored for the sender + * + * @param sender Sender + * @return Optional containing the post processing context if one has been stored, else {@link Optional#empty()} + */ + @Nonnull + public Optional> getPending(@Nonnull final C sender) { + return Optional.ofNullable(this.pendingCommands.getIfPresent(sender)); + } + + /** + * Decorate a simple command meta builder, to require confirmation for a command + * + * @param builder Command meta builder + * @return Builder instance + */ + @Nonnull + public SimpleCommandMeta.Builder decorate(@Nonnull final SimpleCommandMeta.Builder builder) { + return builder.with(CONFIRMATION_REQUIRED_META, "true"); + } + + /** + * Register the confirmation processor in the command manager + * + * @param manager Command manager + */ + public void registerConfirmationProcessor(@Nonnull final CommandManager manager) { + manager.registerCommandPostProcessor(new CommandConfirmationPostProcessor()); + } + + /** + * Create an execution handler for a confirmation command + * + * @return Handler for a confirmation command + */ + @Nonnull + public CommandExecutionHandler createConfirmationExecutionHandler() { + return context -> { + final Optional> pending = this.getPending(context.getSender()); + if (pending.isPresent()) { + final CommandPostprocessingContext postprocessingContext = pending.get(); + postprocessingContext.getCommand() + .getCommandExecutionHandler() + .execute(postprocessingContext.getCommandContext()); + } else { + this.errorNotifier.accept(context.getSender()); + } + }; + } + + + private final class CommandConfirmationPostProcessor implements CommandPostprocessor { + + @Override + public void accept(@Nonnull final CommandPostprocessingContext context) { + if (!context.getCommand() + .getCommandMeta() + .getOrDefault(CONFIRMATION_REQUIRED_META, "false") + .equals("true")) { + return; + } + /* Add it to the "queue" */ + addPending(context); + /* Notify the consumer that a confirmation is required */ + notifyConsumer(context); + /* Interrupt */ + ConsumerService.interrupt(); + } + + } + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/package-info.java b/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/package-info.java new file mode 100644 index 00000000..c18dc3ff --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/extra/confirmation/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg +// +// 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. +// + +/** + * Confirmation system + */ +package com.intellectualsites.commands.extra.confirmation; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java b/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java index ee04530c..f8ae8df9 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/meta/SimpleCommandMeta.java @@ -107,6 +107,18 @@ public class SimpleCommandMeta extends CommandMeta { private Builder() { } + /** + * Copy all values from another command meta instance + * + * @param commandMeta Existing instance + * @return Builder instance + */ + @Nonnull + public Builder with(@Nonnull final CommandMeta commandMeta) { + commandMeta.getAll().forEach(this::with); + return this; + } + /** * Store a new key-value pair in the meta map * diff --git a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java index b8347acb..1cdfe7ae 100644 --- a/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java +++ b/cloud-minecraft/cloud-bukkit-test/src/main/java/com/intellectualsites/commands/BukkitTest.java @@ -26,6 +26,7 @@ package com.intellectualsites.commands; import com.intellectualsites.commands.annotations.AnnotationParser; import com.intellectualsites.commands.annotations.Argument; import com.intellectualsites.commands.annotations.CommandMethod; +import com.intellectualsites.commands.annotations.Confirmation; import com.intellectualsites.commands.annotations.Description; import com.intellectualsites.commands.annotations.specifier.Completions; import com.intellectualsites.commands.annotations.specifier.Range; @@ -43,6 +44,7 @@ import com.intellectualsites.commands.bukkit.CloudBukkitCapabilities; import com.intellectualsites.commands.bukkit.parsers.WorldArgument; import com.intellectualsites.commands.execution.AsynchronousCommandExecutionCoordinator; import com.intellectualsites.commands.execution.CommandExecutionCoordinator; +import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager; import com.intellectualsites.commands.meta.SimpleCommandMeta; import com.intellectualsites.commands.paper.PaperCommandManager; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -61,6 +63,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -100,6 +103,14 @@ public final class BukkitTest extends JavaPlugin { getLogger().warning("Failed to register asynchronous command completions: " + e.getMessage()); } + final CommandConfirmationManager confirmationManager = new CommandConfirmationManager<>( + 30, + TimeUnit.SECONDS, + c -> c.getCommandContext().getSender().sendMessage(ChatColor.RED + "Oh no. Confirm using /cloud confirm!"), + c -> c.sendMessage(ChatColor.RED + "You don't have any pending commands!") + ); + confirmationManager.registerConfirmationProcessor(mgr); + final AnnotationParser annotationParser = new AnnotationParser<>(mgr, CommandSender.class, p -> BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION, @@ -185,6 +196,9 @@ public final class BukkitTest extends JavaPlugin { .build()) .command(mgr.commandBuilder("annotationass").handler(c -> c.getSender() .sendMessage(ChatColor.YELLOW + "Du e en ananas!")).build()) + .command(mgr.commandBuilder("cloud") + .literal("confirm") + .handler(confirmationManager.createConfirmationExecutionHandler()).build()) .command(mgr.commandBuilder("cloud") .literal("help") .argument(StringArgument.newBuilder("query").greedy() @@ -207,6 +221,7 @@ public final class BukkitTest extends JavaPlugin { player.sendMessage(ChatColor.GOLD + "Your input was: " + ChatColor.AQUA + input + ChatColor.GREEN + " (" + number + ")"); } + @Confirmation @CommandMethod("cloud debug") private void doHelp() { final Set capabilities = this.mgr.queryCapabilities();