From 9bdd50b5c54a460ea7c6cd51175cdca0a8ca6338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Fri, 28 Jul 2023 11:22:29 +0200 Subject: [PATCH] feat: introduce ArgumentContext to replace ArgumentTiming (#461) --- .../cloud/commandframework/CommandTree.java | 30 +++- .../context/ArgumentContext.java | 149 ++++++++++++++++++ .../context/CommandContext.java | 108 +++++++++++-- .../CommandPerformanceTest.java | 5 +- .../context/ArgumentContextTest.java | 93 +++++++++++ .../bukkit/BukkitCommand.java | 4 +- 6 files changed, 370 insertions(+), 19 deletions(-) create mode 100644 cloud-core/src/main/java/cloud/commandframework/context/ArgumentContext.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/context/ArgumentContextTest.java diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 41b789a13..8caa710b4 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -28,6 +28,7 @@ import cloud.commandframework.arguments.compound.CompoundArgument; import cloud.commandframework.arguments.compound.FlagArgument; import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.context.ArgumentContext; import cloud.commandframework.context.CommandContext; import cloud.commandframework.exceptions.AmbiguousNodeException; import cloud.commandframework.exceptions.ArgumentParseException; @@ -235,12 +236,20 @@ private CommandTree(final @NonNull CommandManager commandManager) { final Node> child = childIterator.next(); if (child.getValue() != null) { final CommandArgument argument = child.getValue(); - final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); + final ArgumentContext argumentContext = commandContext.createArgumentContext(argument); - argumentTiming.setStart(System.nanoTime()); + // Copy the current queue so that we can deduce the captured input. + final List currentQueue = new LinkedList<>(commandQueue); + + argumentContext.markStart(); commandContext.setCurrentArgument(argument); + final ArgumentParseResult result = argument.getParser().parse(commandContext, commandQueue); - argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); + argumentContext.markEnd(); + argumentContext.success(!result.getFailure().isPresent()); + + currentQueue.removeAll(commandQueue); + argumentContext.consumedInput(currentQueue); if (result.getParsedValue().isPresent()) { parsedArguments.add(child.getValue()); @@ -424,10 +433,10 @@ private CommandTree(final @NonNull CommandManager commandManager) { } final CommandArgument argument = child.getValue(); - final CommandContext.ArgumentTiming argumentTiming = commandContext.createTiming(argument); + final ArgumentContext argumentContext = commandContext.createArgumentContext(argument); // START: Parsing - argumentTiming.setStart(System.nanoTime()); + argumentContext.markStart(); final ArgumentParseResult result; final ArgumentParseResult preParseResult = child.getValue().preprocess( commandContext, @@ -435,11 +444,20 @@ private CommandTree(final @NonNull CommandManager commandManager) { ); if (!preParseResult.getFailure().isPresent() && preParseResult.getParsedValue().orElse(false)) { commandContext.setCurrentArgument(argument); + + // Copy the current queue so that we can deduce the captured input. + final List currentQueue = new LinkedList<>(commandQueue); + result = argument.getParser().parse(commandContext, commandQueue); + + // We remove all remaining queue, and then we'll have a list of the captured input. + currentQueue.removeAll(commandQueue); + argumentContext.consumedInput(currentQueue); } else { result = preParseResult; } - argumentTiming.setEnd(System.nanoTime(), result.getFailure().isPresent()); + argumentContext.markEnd(); + argumentContext.success(!result.getFailure().isPresent()); // END: Parsing if (result.getParsedValue().isPresent()) { diff --git a/cloud-core/src/main/java/cloud/commandframework/context/ArgumentContext.java b/cloud-core/src/main/java/cloud/commandframework/context/ArgumentContext.java new file mode 100644 index 000000000..215822fba --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/context/ArgumentContext.java @@ -0,0 +1,149 @@ +// +// MIT License +// +// Copyright (c) 2022 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.context; + +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.StaticArgument; +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +@API(status = API.Status.MAINTAINED, since = "1.9.0") +public final class ArgumentContext { + + private final CommandArgument<@NonNull C, @NonNull T> argument; + private final List consumedInput = new LinkedList<>(); + + /** + * Construct an ArgumentContext object with the given argument. + * + * @param argument the command argument to be assigned to the ArgumentContext + */ + public ArgumentContext(final @NonNull CommandArgument<@NonNull C, @NonNull T> argument) { + this.argument = argument; + } + + private long startTime = -1; + private long endTime = -1; + + private boolean success; + + /** + * Return the associated argument. + * + * @return the argument + */ + public @NonNull CommandArgument<@NonNull C, @NonNull T> argument() { + return this.argument; + } + + /** + * Return the duration taken to parse the argument. + * + * @return the argument parse duration + */ + public @NonNull Duration parseDuration() { + if (this.startTime < 0) { + throw new IllegalStateException("No start time has been registered"); + } else if (this.endTime < 0) { + throw new IllegalStateException("No end time has been registered"); + } + return Duration.ofNanos(this.endTime - this.startTime); + } + + /** + * Set the start time. + */ + public void markStart() { + this.startTime = System.nanoTime(); + } + + /** + * Set the end time. + */ + public void markEnd() { + this.endTime = System.nanoTime(); + } + + long startTime() { + return this.startTime; + } + + long endTime() { + return this.endTime; + } + + /** + * Return whether the argument was parsed successfully. + * + * @return {@code true} if the value was parsed successfully, {@code false} if not + */ + public boolean success() { + return this.success; + } + + /** + * Set whether the argument was parsed successfully. + * + * @param success {@code true} if the value was parsed successfully, {@code false} if not + */ + public void success(final boolean success) { + this.success = success; + } + + /** + * Add the given input to the list of consumed input. + * + * @param consumedInput the consumed input + */ + public void consumedInput(final @NonNull List<@NonNull String> consumedInput) { + this.consumedInput.addAll(consumedInput); + } + + /** + * Return the list of consumed input. + * + * @return the list of consumed input + */ + public @NonNull List<@NonNull String> consumedInput() { + return Collections.unmodifiableList(this.consumedInput); + } + + /** + * Return the exact alias used, if the argument was static. If no alias was consumed + * then {@code null} is returned. + * + * @return the exact alias, or {@code null} + */ + public @Nullable String exactAlias() { + if (!this.success || !(this.argument instanceof StaticArgument)) { + return null; + } + return this.consumedInput.get(0); + } +} diff --git a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java index 9109b472e..ef714cd88 100644 --- a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java @@ -36,13 +36,17 @@ import cloud.commandframework.keys.CloudKeyHolder; import cloud.commandframework.keys.SimpleCloudKey; import cloud.commandframework.permission.CommandPermission; +import cloud.commandframework.types.tuples.Pair; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -56,7 +60,7 @@ public class CommandContext { private final CaptionVariableReplacementHandler captionVariableReplacementHandler; - private final Map, ArgumentTiming> argumentTimings = new HashMap<>(); + private final List> argumentContexts = new LinkedList<>(); private final FlagContext flagContext = FlagContext.create(); private final Map, Object> internalStorage = new HashMap<>(); private final C commandSender; @@ -603,20 +607,103 @@ public T computeIfAbsent( * * @param argument Argument * @return Created timing instance + * + * @deprecated This has been replaced by {@link #createArgumentContext(CommandArgument)} */ + @API(status = API.Status.DEPRECATED, since = "1.9.0") + @Deprecated public @NonNull ArgumentTiming createTiming(final @NonNull CommandArgument argument) { - final ArgumentTiming argumentTiming = new ArgumentTiming(); - this.argumentTimings.put(argument, argumentTiming); - return argumentTiming; + return new ArgumentTiming(); } /** * Get an immutable view of the argument timings map * * @return Argument timings + * @deprecated Replaced with {@link #argumentContexts()} */ + @API(status = API.Status.DEPRECATED, since = "1.9.0") + @Deprecated public @NonNull Map, ArgumentTiming> getArgumentTimings() { - return Collections.unmodifiableMap(this.argumentTimings); + return this.argumentContexts.stream() + .map(context -> Pair.of( + context.argument(), + new ArgumentTiming( + context.startTime(), + context.endTime(), + context.success() + ) + ) + ).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); + } + + /** + * Create an argument context instance for the given argument + * + * @param argument the argument + * @return the created context + * @param the type of the argument + * @since 1.9.0 + */ + @API(status = API.Status.MAINTAINED, since = "1.9.0") + public @NonNull ArgumentContext createArgumentContext(final @NonNull CommandArgument argument) { + final ArgumentContext argumentContext = new ArgumentContext<>(argument); + this.argumentContexts.add(argumentContext); + return argumentContext; + } + + /** + * Returns the context for the given argument + * + * @param argument the argument + * @return the context + * @param the type of the argument + * @since 1.9.0 + */ + @API(status = API.Status.MAINTAINED, since = "1.9.0") + @SuppressWarnings("unchecked") + public @NonNull ArgumentContext argumentContext(final @NonNull CommandArgument argument) { + return this.argumentContexts.stream().filter(context -> context.argument().equals(argument)) + .findFirst() + .map(context -> (ArgumentContext) context) + .orElseThrow(NoSuchElementException::new); + } + + /** + * Returns the context for the argument at the given position + * + * @param position the position + * @return the context + * @since 1.9.0 + */ + @API(status = API.Status.MAINTAINED, since = "1.9.0") + public @NonNull ArgumentContext argumentContext(final int position) { + return this.argumentContexts.get(position); + } + + /** + * Return the context for the argument with the given name. + * + * @param name the name + * @return the context + * @since 1.9.0 + */ + @API(status = API.Status.MAINTAINED, since = "1.9.0") + public @NonNull ArgumentContext argumentContext(final String name) { + return this.argumentContexts.stream().filter(context -> context.argument().getName().equals(name)) + .findFirst() + .orElseThrow(NoSuchElementException::new); + } + + /** + * Return an unmodifiable view of the stored argument contexts + * + * @return the contexts + * @since 1.9.0 + */ + @API(status = API.Status.MAINTAINED, since = "1.9.0") + public @NonNull List<@NonNull ArgumentContext<@NonNull C, @NonNull ?>> argumentContexts() { + return Collections.unmodifiableList(this.argumentContexts); } /** @@ -680,8 +767,11 @@ public void setCurrentArgument(final @Nullable CommandArgument argument) { * parsed. *

* The times are measured in nanoseconds. + * + * @deprecated Superseded by {@link ArgumentContext} */ - @API(status = API.Status.STABLE) + @Deprecated + @API(status = API.Status.DEPRECATED, since = "1.9.0") public static final class ArgumentTiming { private long start; @@ -689,7 +779,7 @@ public static final class ArgumentTiming { private boolean success; /** - * Created a new argument timing instance + * Creates a new argument timing instance * * @param start Start time (in nanoseconds) * @param end End time (in nanoseconds) @@ -702,7 +792,7 @@ public ArgumentTiming(final long start, final long end, final boolean success) { } /** - * Created a new argument timing instance without an end time + * Creates a new argument timing instance without an end time * * @param start Start time (in nanoseconds) */ @@ -712,7 +802,7 @@ public ArgumentTiming(final long start) { } /** - * Created a new argument timing instance + * Creates a new argument timing instance */ public ArgumentTiming() { this(-1, -1, false); diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandPerformanceTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandPerformanceTest.java index 9ea664bb2..221059ce8 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandPerformanceTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandPerformanceTest.java @@ -23,6 +23,7 @@ // package cloud.commandframework; +import cloud.commandframework.context.ArgumentContext; import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.CommandResult; import java.util.Collection; @@ -65,8 +66,8 @@ void testLiterals() { long elapsedTime = 0L; int amount = 0; for (int i = 0; i < 100000; i++) { - for (final CommandContext.ArgumentTiming argumentTiming : result.getCommandContext().getArgumentTimings().values()) { - elapsedTime += argumentTiming.getElapsedTime(); + for (final ArgumentContext argumentContext : result.getCommandContext().argumentContexts()) { + elapsedTime += argumentContext.parseDuration().toNanos(); amount += 1; } } diff --git a/cloud-core/src/test/java/cloud/commandframework/context/ArgumentContextTest.java b/cloud-core/src/test/java/cloud/commandframework/context/ArgumentContextTest.java new file mode 100644 index 000000000..b700120c8 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/context/ArgumentContextTest.java @@ -0,0 +1,93 @@ +// +// MIT License +// +// Copyright (c) 2022 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.context; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.standard.IntegerArgument; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.execution.CommandResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static cloud.commandframework.util.TestUtils.createManager; +import static com.google.common.truth.Truth.assertThat; + +class ArgumentContextTest { + + private CommandManager commandManager; + + @BeforeEach + void setup() { + this.commandManager = createManager(); + } + + @Test + void testConsumedInput() throws Exception { + // Arrange + this.commandManager.command( + this.commandManager.commandBuilder("test", "t").argument( + IntegerArgument.builder("int") + ).argument( + StringArgument.greedy("string") + ) + ); + final String commandInput = "t 1337 roflmao xd"; + + // Act + final CommandResult result = this.commandManager.executeCommand( + new TestCommandSender(), + commandInput + ).get(); + + // Assert + final CommandContext context = result.getCommandContext(); + assertThat(context.argumentContext("test").consumedInput()).containsExactly("t"); + assertThat(context.argumentContext("int").consumedInput()).containsExactly("1337"); + assertThat(context.argumentContext("string").consumedInput()).containsExactly("roflmao", "xd"); + } + + @Test + void testExactAlias() throws Exception { + // Arrange + this.commandManager.command( + this.commandManager.commandBuilder("test", "t") + .literal("foo", "f") + .literal("bar", "b") + ); + final String commandInput = "t f bar"; + + // Act + final CommandResult result = this.commandManager.executeCommand( + new TestCommandSender(), + commandInput + ).get(); + + // Assert + final CommandContext context = result.getCommandContext(); + assertThat(context.argumentContext("test").exactAlias()).isEqualTo("t"); + assertThat(context.argumentContext("foo").exactAlias()).isEqualTo("f"); + assertThat(context.argumentContext("bar").exactAlias()).isEqualTo("bar"); + } +} diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java index def80dcd1..64d7a9e82 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java @@ -104,11 +104,11 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi @Override public boolean execute( final @NonNull CommandSender commandSender, - final @NonNull String s, + final @NonNull String commandLabel, final @NonNull String @NonNull [] strings ) { /* Join input */ - final StringBuilder builder = new StringBuilder(this.command.getName()); + final StringBuilder builder = new StringBuilder(commandLabel); for (final String string : strings) { builder.append(" ").append(string); }