From c67619e5daa8f9c210296530e7fb471269908eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Fri, 2 Oct 2020 20:52:35 +0200 Subject: [PATCH] :sparkles: Add flag support to the annotation system. --- .../annotations/AnnotationParser.java | 8 ++ .../annotations/ArgumentExtractor.java | 2 +- .../commandframework/annotations/Flag.java | 69 ++++++++++++++ .../annotations/FlagExtractor.java | 91 +++++++++++++++++++ .../MethodCommandExecutionHandler.java | 9 ++ .../annotations/AnnotationParserTest.java | 11 +++ .../java/cloud/commandframework/Command.java | 11 +++ .../arguments/flags/CommandFlag.java | 12 +++ .../arguments/flags/FlagContext.java | 3 +- 9 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java create mode 100644 cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java index 6edc2d92..46832242 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -27,6 +27,7 @@ import cloud.commandframework.Command; import cloud.commandframework.CommandManager; import cloud.commandframework.Description; import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserParameter; import cloud.commandframework.arguments.parser.StandardParameters; @@ -64,6 +65,7 @@ public final class AnnotationParser { private final Map, Function> annotationMappers; private final Class commandSenderClass; private final MetaFactory metaFactory; + private final FlagExtractor flagExtractor; /** * Construct a new annotation parser @@ -82,6 +84,7 @@ public final class AnnotationParser { this.manager = manager; this.metaFactory = new MetaFactory(this, metaMapper); this.annotationMappers = new HashMap<>(); + this.flagExtractor = new FlagExtractor(manager); this.registerAnnotationMapper(CommandDescription.class, d -> ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); } @@ -156,6 +159,7 @@ public final class AnnotationParser { tokens.get(commandToken).getMinor(), metaBuilder.build()); final Collection arguments = this.argumentExtractor.apply(method); + final Collection> flags = this.flagExtractor.apply(method); final Map> commandArguments = new HashMap<>(); final Map, String> argumentDescriptions = new HashMap<>(); /* Go through all annotated parameters and build up the argument tree */ @@ -218,6 +222,10 @@ public final class AnnotationParser { if (method.isAnnotationPresent(Hidden.class)) { builder = builder.hidden(); } + /* Apply flags */ + for (final CommandFlag flag : flags) { + builder = builder.flag(flag); + } /* Construct and register the command */ final Command builtCommand = builder.build(); commands.add(builtCommand); diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentExtractor.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentExtractor.java index cef1d247..eda34fc3 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentExtractor.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentExtractor.java @@ -35,7 +35,7 @@ import java.util.function.Function; * Utility that extract {@link Argument arguments} from * {@link java.lang.reflect.Method method} {@link java.lang.reflect.Parameter parameters} */ -class ArgumentExtractor implements Function<@NonNull Method, Collection<@NonNull ArgumentParameterPair>> { +class ArgumentExtractor implements Function<@NonNull Method, @NonNull Collection<@NonNull ArgumentParameterPair>> { @Override public @NonNull Collection<@NonNull ArgumentParameterPair> apply(@NonNull final Method method) { diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java new file mode 100644 index 00000000..de47def5 --- /dev/null +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/Flag.java @@ -0,0 +1,69 @@ +// +// 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the parameter should be treated like a {@link cloud.commandframework.arguments.flags.CommandFlag}. + * If the parameter is a {@code boolean} then a presence flag will be created, else a value flag will be created + * and the parser will be resolved the same way as it would for a {@link Argument} + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Flag { + + /** + * The flag name + * + * @return Flag name + */ + String value(); + + /** + * Flag aliases + * + * @return Aliases + */ + String[] aliases() default ""; + + /** + * Name of the parser. Leave empty to use + * the default parser for the parameter type + * + * @return Parser name + */ + String parserName() default ""; + + /** + * The argument description + * + * @return Argument description + */ + String description() default ""; + +} diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java new file mode 100644 index 00000000..c50753ec --- /dev/null +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/FlagExtractor.java @@ -0,0 +1,91 @@ +// +// 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.annotations; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.Description; +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.flags.CommandFlag; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.arguments.parser.ParserParameters; +import cloud.commandframework.arguments.parser.ParserRegistry; +import io.leangen.geantyref.TypeToken; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Collection; +import java.util.LinkedList; +import java.util.function.Function; + +final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNull CommandFlag>> { + + private final CommandManager commandManager; + + FlagExtractor(@NonNull final CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public @NonNull Collection<@NonNull CommandFlag> apply(@NonNull final Method method) { + final Collection> flags = new LinkedList<>(); + for (final Parameter parameter : method.getParameters()) { + if (!parameter.isAnnotationPresent(Flag.class)) { + continue; + } + final Flag flag = parameter.getAnnotation(Flag.class); + final CommandFlag.Builder builder = this.commandManager.flagBuilder(flag.value()) + .withDescription(Description.of(flag.description())).withAliases(flag.aliases()); + if (parameter.getType().equals(boolean.class)) { + flags.add(builder.build()); + } else { + final ParserRegistry registry = this.commandManager.getParserRegistry(); + final ArgumentParser parser; + if (flag.parserName().isEmpty()) { + parser = registry.createParser(TypeToken.get(parameter.getType()), ParserParameters.empty()) + .orElse(null); + } else { + parser = registry.createParser(flag.parserName(), ParserParameters.empty()) + .orElse(null); + } + if (parser == null) { + throw new IllegalArgumentException( + String.format("Cannot find parser for type '%s' for flag '%s' in method '%s'", + parameter.getType().getCanonicalName(), flag.value(), method.getName())); + } + @SuppressWarnings("ALL") + final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(parameter.getType(), flag.value()); + @SuppressWarnings("ALL") + final CommandArgument argument = argumentBuilder.asRequired() + .manager(this.commandManager) + .withParser(parser) + .build(); + // noinspection unchecked + flags.add(builder.withArgument(argument).build()); + } + } + return flags; + } + +} diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java index 7c034627..5d042d71 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java @@ -24,6 +24,7 @@ package cloud.commandframework.annotations; import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.flags.FlagContext; import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.CommandExecutionHandler; import org.checkerframework.checker.nullness.qual.NonNull; @@ -55,6 +56,7 @@ class MethodCommandExecutionHandler implements CommandExecutionHandler { @Override public void execute(@NonNull final CommandContext commandContext) { final List arguments = new ArrayList<>(this.parameters.length); + final FlagContext flagContext = commandContext.flags(); /* Bind parameters to context */ for (final Parameter parameter : this.parameters) { @@ -67,6 +69,13 @@ class MethodCommandExecutionHandler implements CommandExecutionHandler { final Object optional = commandContext.getOptional(argument.value()).orElse(null); arguments.add(optional); } + } else if (parameter.isAnnotationPresent(Flag.class)) { + final Flag flag = parameter.getAnnotation(Flag.class); + if (parameter.getType() == boolean.class) { + arguments.add(flagContext.isPresent(flag.value())); + } else { + arguments.add(flagContext.getValue(flag.value(), null)); + } } else { if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) { arguments.add(commandContext.getSender()); diff --git a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java index 0785dc5f..62a0a536 100644 --- a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java @@ -58,6 +58,8 @@ class AnnotationParserTest { manager.executeCommand(new TestCommandSender(), "proxycommand 10").join(); Assertions.assertThrows(CompletionException.class, () -> manager.executeCommand(new TestCommandSender(), "test 101").join()); + manager.executeCommand(new TestCommandSender(), "flagcommand -p").join(); + manager.executeCommand(new TestCommandSender(), "flagcommand --print --word peanut").join(); } @ProxiedBy("proxycommand") @@ -69,4 +71,13 @@ class AnnotationParserTest { System.out.printf("Received int: %d and string '%s'\n", argument, string); } + @CommandMethod("flagcommand") + public void testFlags(final TestCommandSender sender, + @Flag(value = "print", aliases = "p") boolean print, + @Flag(value = "word", aliases = "w") String word) { + if (print) { + System.out.println(word); + } + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index b84e3a65..b2452ae2 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -680,6 +680,17 @@ public class Command { Collections.unmodifiableList(flags)); } + /** + * Register a new command flag + * + * @param builder Flag builder. {@link CommandFlag.Builder#build()} will be invoked. + * @param Flag value type + * @return New builder instance that uses the provided flag + */ + public @NonNull Builder flag(final CommandFlag.@NonNull Builder builder) { + return this.flag(builder.build()); + } + /** * Build a command using the builder instance * diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java index 635d94de..336a6f8f 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java @@ -89,6 +89,7 @@ public final class CommandFlag { /** * Get the flag description *

+ * * @return Flag description */ public @NonNull Description getDescription() { @@ -179,6 +180,17 @@ public final class CommandFlag { return new Builder<>(this.name, this.aliases, this.description, argument); } + /** + * Create a new builder instance using the given command argument + * + * @param builder Command argument builder. {@link CommandArgument.Builder#build()} will be invoked. + * @param New argument type + * @return New builder instance + */ + public Builder withArgument(final CommandArgument.@NonNull Builder builder) { + return this.withArgument(builder.build()); + } + /** * Build a new command flag instance * diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java index 459311d7..984005d5 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java @@ -24,6 +24,7 @@ package cloud.commandframework.arguments.flags; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashMap; import java.util.Map; @@ -94,7 +95,7 @@ public final class FlagContext { * @param Value type * @return Stored value, or the supplied default value */ - public T getValue(@NonNull final String name, @NonNull final T defaultValue) { + public @Nullable T getValue(@NonNull final String name, @Nullable final T defaultValue) { final Object value = this.flagValues.get(name); if (value == null) { return defaultValue;