From 3f852d068efa842b01038505f50946d2b9037491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Fri, 18 Sep 2020 21:30:00 +0200 Subject: [PATCH] Improve the annotated command method code and add more supported annotations --- .../annotations/AnnotationParser.java | 158 +++++++++++++++--- .../commands/annotations/Description.java | 45 +++++ .../annotations/AnnotationParserTest.java | 18 +- .../commands/CommandTree.java | 2 + .../annotations/specifier/Completions.java | 46 +++++ .../commands/arguments/StaticArgument.java | 13 ++ .../arguments/parser/ParserParameters.java | 15 ++ .../arguments/parser/StandardParameters.java | 10 ++ .../parser/StandardParserRegistry.java | 21 ++- .../arguments/standard/IntegerArgument.java | 33 ++++ .../InvalidCommandSenderException.java | 2 +- .../commands/CommandSuggestionsTest.java | 24 ++- cloud-minecraft/cloud-bukkit-test/pom.xml | 5 + .../commands/BukkitTest.java | 44 +++-- .../commands/BukkitCommand.java | 15 +- 15 files changed, 392 insertions(+), 59 deletions(-) create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Description.java create mode 100644 cloud-core/src/main/java/com/intellectualsites/commands/annotations/specifier/Completions.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 0a5624c9..b7026523 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 @@ -30,6 +30,7 @@ import com.intellectualsites.commands.CommandManager; import com.intellectualsites.commands.arguments.CommandArgument; 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.meta.CommandMeta; @@ -42,11 +43,11 @@ import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -58,20 +59,63 @@ import java.util.regex.Pattern; */ public final class AnnotationParser { - private static final Predicate PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9]+)(|([A-Za-z0-9]+))*") + private static final Predicate PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9]+)(|([A-Za-z0-9]+))*") + .asPredicate(); + private static final Predicate PATTERN_ARGUMENT_REQUIRED = Pattern.compile("<([A-Za-z0-9]+)>") + .asPredicate(); + private static final Predicate PATTERN_ARGUMENT_OPTIONAL = Pattern.compile("\\[([A-Za-z0-9]+)]") .asPredicate(); - private static final Predicate PATTERN_ARGUMENT_REQUIRED = Pattern.compile("<([A-Za-z0-9]+)>").asPredicate(); - private static final Predicate PATTERN_ARGUMENT_OPTIONAL = Pattern.compile("\\[([A-Za-z0-9]+)]").asPredicate(); + + private final Function metaMapper; private final CommandManager manager; + private final Map, Function> annotationMappers; + private final Class commandSenderClass; /** * Construct a new annotation parser * - * @param manager Command manager instance + * @param manager Command manager instance + * @param commandSenderClass Command sender class + * @param metaMapper Function that is used to create {@link CommandMeta} instances from annotations on the + * command methods. These annotations will be mapped to + * {@link com.intellectualsites.commands.arguments.parser.ParserParameter}. Mappers for the + * parser parameters can be registered using {@link #registerAnnotationMapper(Class, Function)} */ - public AnnotationParser(@Nonnull final CommandManager manager) { + public AnnotationParser(@Nonnull final CommandManager manager, + @Nonnull final Class commandSenderClass, + @Nonnull final Function metaMapper) { + this.commandSenderClass = commandSenderClass; this.manager = manager; + this.metaMapper = metaMapper; + this.annotationMappers = Maps.newHashMap(); + this.registerAnnotationMapper(Description.class, d -> ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); + } + + /** + * Register an annotation mapper + * + * @param annotation Annotation class + * @param mapper Mapping function + * @param Annotation type + */ + public void registerAnnotationMapper(@Nonnull final Class annotation, + @Nonnull final Function mapper) { + this.annotationMappers.put(annotation, mapper); + } + + @Nonnull + private M createMeta(@Nonnull final Annotation[] annotations) { + final ParserParameters parameters = ParserParameters.empty(); + for (final Annotation annotation : annotations) { + @SuppressWarnings("ALL") final Function function = this.annotationMappers.get(annotation.annotationType()); + if (function == null) { + continue; + } + //noinspection unchecked + parameters.merge((ParserParameters) function.apply(annotation)); + } + return this.metaMapper.apply(parameters); } /** @@ -84,13 +128,16 @@ public final class AnnotationParser { */ @Nonnull public Collection> parse(@Nonnull final T instance) { - final Method[] methods = instance.getClass().getMethods(); + final Method[] methods = instance.getClass().getDeclaredMethods(); final Collection commandMethodPairs = new ArrayList<>(); for (final Method method : methods) { final CommandMethod commandMethod = method.getAnnotation(CommandMethod.class); if (commandMethod == null) { continue; } + if (!method.isAccessible()) { + method.setAccessible(true); + } if (method.getReturnType() != Void.TYPE) { throw new IllegalArgumentException(String.format("@CommandMethod annotated method '%s' has non-void return type", method.getName())); @@ -112,13 +159,13 @@ public final class AnnotationParser { for (final CommandMethodPair commandMethodPair : methodPairs) { final CommandMethod commandMethod = commandMethodPair.getCommandMethod(); final Method method = commandMethodPair.getMethod(); - final LinkedHashMap tokens = this.parseSyntax(commandMethod.value()); + final LinkedHashMap tokens = this.parseSyntax(commandMethod.value()); /* Determine command name */ final String commandToken = commandMethod.value().split(" ")[0].split("\\|")[0]; @SuppressWarnings("ALL") Command.Builder builder = this.manager.commandBuilder(commandToken, - Collections.emptyList(), - manager.createDefaultCommandMeta()); + tokens.get(commandToken).getMinor(), + this.createMeta(method.getAnnotations())); final Collection arguments = this.getArguments(method); final Map> commandArguments = Maps.newHashMap(); /* Go through all annotated parameters and build up the argument tree */ @@ -130,13 +177,13 @@ public final class AnnotationParser { } boolean commandNameFound = false; /* Build the command tree */ - for (final Map.Entry entry : tokens.entrySet()) { + for (final Map.Entry entry : tokens.entrySet()) { if (!commandNameFound) { commandNameFound = true; continue; } - if (entry.getValue() == ArgumentMode.LITERAL) { - builder = builder.literal(entry.getKey()); + if (entry.getValue().getArgumentMode() == ArgumentMode.LITERAL) { + builder = builder.literal(entry.getKey(), entry.getValue().getMinor().toArray(new String[0])); } else { final CommandArgument argument = commandArguments.get(entry.getKey()); if (argument == null) { @@ -148,8 +195,24 @@ public final class AnnotationParser { builder = builder.argument(argument); } } + /* Try to find the command sender type */ + Class senderType = null; + for (final Parameter parameter : method.getParameters()) { + if (parameter.isAnnotationPresent(Argument.class)) { + continue; + } + if (this.commandSenderClass.isAssignableFrom(parameter.getType())) { + senderType = (Class) parameter.getType(); + break; + } + } /* Decorate command data */ - builder = builder.withPermission(commandMethod.permission()).withSenderType(commandMethod.requiredSender()); + builder = builder.withPermission(commandMethod.permission()); + if (commandMethod.requiredSender() != Object.class) { + builder = builder.withSenderType(commandMethod.requiredSender()); + } else if (senderType != null) { + builder = builder.withSenderType(senderType); + } /* Construct the handler */ final CommandExecutionHandler commandExecutionHandler = commandContext -> { final List parameters = new ArrayList<>(method.getParameterCount()); @@ -189,7 +252,7 @@ public final class AnnotationParser { @Nonnull @SuppressWarnings("unchecked") private CommandArgument buildArgument(@Nonnull final Method method, - @Nullable final ArgumentMode argumentMode, + @Nullable final SyntaxFragment syntaxFragment, @Nonnull final ArgumentParameterPair argumentPair) { final Parameter parameter = argumentPair.getParameter(); final Collection annotations = Arrays.asList(parameter.getAnnotations()); @@ -204,16 +267,15 @@ public final class AnnotationParser { + "for that type", parameter.getName(), method.getName(), token.toString()))); - if (argumentMode == null || argumentMode == ArgumentMode.LITERAL) { + if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) { throw new IllegalArgumentException(String.format( "Invalid command argument '%s' in method '%s': " - + "Missing syntax mapping", argumentPair.getArgument().value(), method.getName())); + + "Missing syntax mapping", argumentPair.getArgument().value(), method.getName())); } final Argument argument = argumentPair.getArgument(); - @SuppressWarnings("ALL") - final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(parameter.getType(), - argument.value()); - if (argumentMode == ArgumentMode.OPTIONAL) { + @SuppressWarnings("ALL") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(parameter.getType(), + argument.value()); + if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) { if (argument.defaultValue().isEmpty()) { argumentBuilder.asOptional(); } else { @@ -226,22 +288,30 @@ public final class AnnotationParser { } @Nonnull - LinkedHashMap parseSyntax(@Nonnull final String syntax) { + LinkedHashMap parseSyntax(@Nonnull final String syntax) { final StringTokenizer stringTokenizer = new StringTokenizer(syntax, " "); - final LinkedHashMap map = new LinkedHashMap<>(); + final LinkedHashMap map = new LinkedHashMap<>(); while (stringTokenizer.hasMoreTokens()) { final String token = stringTokenizer.nextToken(); + String major; + List minor = new ArrayList<>(); + ArgumentMode mode; if (PATTERN_ARGUMENT_REQUIRED.test(token)) { - map.put(token.substring(1, token.length() - 1), ArgumentMode.REQUIRED); + major = token.substring(1, token.length() - 1); + mode = ArgumentMode.REQUIRED; } else if (PATTERN_ARGUMENT_OPTIONAL.test(token)) { - map.put(token.substring(1, token.length() - 1), ArgumentMode.OPTIONAL); + major = token.substring(1, token.length() - 1); + mode = ArgumentMode.OPTIONAL; } else if (PATTERN_ARGUMENT_LITERAL.test(token)) { final String[] literals = token.split("\\|"); /* Actually use the other literals as well */ - map.put(literals[0], ArgumentMode.LITERAL); + major = literals[0]; + minor.addAll(Arrays.asList(literals).subList(1, literals.length)); + mode = ArgumentMode.LITERAL; } else { throw new IllegalArgumentException(String.format("Unrecognizable syntax token '%s'", syntax)); } + map.put(major, new SyntaxFragment(major, minor, mode)); } return map; } @@ -306,7 +376,41 @@ public final class AnnotationParser { enum ArgumentMode { - LITERAL, OPTIONAL, REQUIRED + LITERAL, + OPTIONAL, + REQUIRED + } + + + private static final class SyntaxFragment { + + private final String major; + private final List minor; + private final ArgumentMode argumentMode; + + private SyntaxFragment(@Nonnull final String major, + @Nonnull final List minor, + @Nonnull final ArgumentMode argumentMode) { + this.major = major; + this.minor = minor; + this.argumentMode = argumentMode; + } + + @Nonnull + private String getMajor() { + return this.major; + } + + @Nonnull + private List getMinor() { + return this.minor; + } + + @Nonnull + private ArgumentMode getArgumentMode() { + return this.argumentMode; + } + } } diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Description.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Description.java new file mode 100644 index 00000000..7dbe10c5 --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Description.java @@ -0,0 +1,45 @@ +// +// 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; + +/** + * Maps to {@link com.intellectualsites.commands.arguments.parser.StandardParameters#DESCRIPTION} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Description { + + /** + * Command description + * + * @return Command syntax + */ + String value() default ""; + +} diff --git a/cloud-annotations/src/test/java/com/intellectualsites/commands/annotations/AnnotationParserTest.java b/cloud-annotations/src/test/java/com/intellectualsites/commands/annotations/AnnotationParserTest.java index 999e82dc..1db1df1f 100644 --- a/cloud-annotations/src/test/java/com/intellectualsites/commands/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/com/intellectualsites/commands/annotations/AnnotationParserTest.java @@ -23,7 +23,6 @@ // package com.intellectualsites.commands.annotations; -import com.google.common.collect.Maps; import com.intellectualsites.commands.Command; import com.intellectualsites.commands.CommandManager; import com.intellectualsites.commands.annotations.specifier.Range; @@ -34,7 +33,6 @@ import org.junit.jupiter.api.Test; import javax.annotation.Nonnull; import java.util.Collection; -import java.util.Map; import java.util.concurrent.CompletionException; class AnnotationParserTest { @@ -45,18 +43,7 @@ class AnnotationParserTest { @BeforeAll static void setup() { manager = new TestCommandManager(); - annotationParser = new AnnotationParser<>(manager); - } - - @Test - void testSyntaxParsing() { - final String text = "literal [optional]"; - final Map arguments = annotationParser.parseSyntax(text); - final Map map = Maps.newLinkedHashMap(); - map.put("literal", AnnotationParser.ArgumentMode.LITERAL); - map.put("required", AnnotationParser.ArgumentMode.REQUIRED); - map.put("optional", AnnotationParser.ArgumentMode.OPTIONAL); - Assertions.assertEquals(map, arguments); + annotationParser = new AnnotationParser<>(manager, TestCommandSender.class, p -> SimpleCommandMeta.empty()); } @Test @@ -64,11 +51,12 @@ class AnnotationParserTest { final Collection> commands = annotationParser.parse(this); Assertions.assertFalse(commands.isEmpty()); manager.executeCommand(new TestCommandSender(), "test 10").join(); + manager.executeCommand(new TestCommandSender(), "t 10").join(); Assertions.assertThrows(CompletionException.class, () -> manager.executeCommand(new TestCommandSender(), "test 101").join()); } - @CommandMethod("test [string]") + @CommandMethod("test|t [string]") public void testCommand(@Nonnull final TestCommandSender sender, @Argument("int") @Range(max = "100") final int argument, @Nonnull @Argument(value = "string", defaultValue = "potato") final String string) { 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 d4cbbafa..8a28d241 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandTree.java @@ -285,6 +285,8 @@ public final class CommandTree { return child.getValue().getParser().suggestions(commandContext, commandQueue.peek()); } else if (child.isLeaf()) { return Collections.emptyList(); + } else if (commandQueue.peek().isEmpty()) { + return child.getValue().getParser().suggestions(commandContext, commandQueue.remove()); } final ArgumentParseResult result = child.getValue().getParser().parse(commandContext, commandQueue); if (result.getParsedValue().isPresent()) { diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/annotations/specifier/Completions.java b/cloud-core/src/main/java/com/intellectualsites/commands/annotations/specifier/Completions.java new file mode 100644 index 00000000..5bd2b60f --- /dev/null +++ b/cloud-core/src/main/java/com/intellectualsites/commands/annotations/specifier/Completions.java @@ -0,0 +1,46 @@ +// +// 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.specifier; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Command completions, separated by "," or ", " + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Completions { + + /** + * Command completions + * + * @return Command completions + */ + String value() default ""; + +} diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java index f4c6f62f..ffbcdeb8 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/StaticArgument.java @@ -28,6 +28,7 @@ import com.intellectualsites.commands.arguments.parser.ArgumentParser; import com.intellectualsites.commands.context.CommandContext; import javax.annotation.Nonnull; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -93,16 +94,28 @@ public final class StaticArgument extends CommandArgument { return Collections.unmodifiableSet(((StaticArgumentParser) this.getParser()).getAcceptedStrings()); } + /** + * Get an immutable list of all aliases that are not the main literal + * + * @return Immutable view of the optional argument aliases + */ + @Nonnull + public List getAlternativeAliases() { + return Collections.unmodifiableList(new ArrayList<>(((StaticArgumentParser) this.getParser()).acceptedStrings)); + } + private static final class StaticArgumentParser implements ArgumentParser { private final String name; private final Set acceptedStrings = new HashSet<>(); + private final Set alternativeAliases = new HashSet<>(); private StaticArgumentParser(@Nonnull final String name, @Nonnull final String... aliases) { this.name = name; this.acceptedStrings.add(this.name); this.acceptedStrings.addAll(Arrays.asList(aliases)); + this.alternativeAliases.addAll(Arrays.asList(aliases)); } @Nonnull diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/ParserParameters.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/ParserParameters.java index a3f62f27..2c84c2da 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/ParserParameters.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/ParserParameters.java @@ -46,6 +46,21 @@ public final class ParserParameters { return new ParserParameters(); } + /** + * Create a {@link ParserParameters} instance containing a single key-value par + * + * @param parameter Parameter + * @param value Value + * @param Value type + * @return Constructed instance + */ + @Nonnull + public static ParserParameters single(@Nonnull final ParserParameter parameter, @Nonnull final T value) { + final ParserParameters parameters = new ParserParameters(); + parameters.store(parameter, value); + return parameters; + } + /** * Check if this instance contains a parameter-object pair for a given parameter * 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 cab11607..47db61fc 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 @@ -45,6 +45,16 @@ public final class StandardParameters { */ public static final ParserParameter RANGE_MAX = create("max", TypeToken.of(Number.class)); + /** + * Command description + */ + public static final ParserParameter DESCRIPTION = create("description", TypeToken.of(String.class)); + + /** + * Command completions + */ + public static final ParserParameter COMPLETIONS = create("completions", TypeToken.of(String[].class)); + private static ParserParameter create(@Nonnull final String key, @Nonnull final TypeToken expectedType) { return new ParserParameter<>(key, expectedType); } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParserRegistry.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParserRegistry.java index ebda7564..db75babe 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParserRegistry.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/parser/StandardParserRegistry.java @@ -25,6 +25,7 @@ package com.intellectualsites.commands.arguments.parser; import com.google.common.collect.ImmutableMap; import com.google.common.reflect.TypeToken; +import com.intellectualsites.commands.annotations.specifier.Completions; import com.intellectualsites.commands.annotations.specifier.Range; import com.intellectualsites.commands.arguments.standard.BooleanArgument; import com.intellectualsites.commands.arguments.standard.ByteArgument; @@ -38,8 +39,8 @@ import com.intellectualsites.commands.arguments.standard.StringArgument; import javax.annotation.Nonnull; import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -75,6 +76,7 @@ public final class StandardParserRegistry implements ParserRegistry { public StandardParserRegistry() { /* Register standard mappers */ this.registerAnnotationMapper(Range.class, new RangeMapper<>()); + this.registerAnnotationMapper(Completions.class, new CompletionsMapper()); /* Register standard types */ this.registerParserSupplier(TypeToken.of(Byte.class), options -> @@ -95,7 +97,8 @@ public final class StandardParserRegistry implements ParserRegistry { this.registerParserSupplier(TypeToken.of(Character.class), options -> new CharArgument.CharacterParser()); /* Make this one less awful */ this.registerParserSupplier(TypeToken.of(String.class), options -> new StringArgument.StringParser( - StringArgument.StringMode.SINGLE, (context, s) -> Collections.emptyList())); + StringArgument.StringMode.SINGLE, (context, s) -> + Arrays.asList(options.get(StandardParameters.COMPLETIONS, new String[0])))); /* Add options to this */ this.registerParserSupplier(TypeToken.of(Boolean.class), options -> new BooleanArgument.BooleanParser<>(false)); } @@ -229,4 +232,18 @@ public final class StandardParserRegistry implements ParserRegistry { } + + private static final class CompletionsMapper implements BiFunction, ParserParameters> { + + @Override + public ParserParameters apply(final Completions completions, final TypeToken token) { + if (token.getRawType().equals(String.class)) { + final String[] splitCompletions = completions.value().replace(" ", "").split(","); + return ParserParameters.single(StandardParameters.COMPLETIONS, splitCompletions); + } + return ParserParameters.empty(); + } + + } + } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/IntegerArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/IntegerArgument.java index 4dd9b5cf..c156b72c 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/IntegerArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/standard/IntegerArgument.java @@ -30,11 +30,19 @@ import com.intellectualsites.commands.context.CommandContext; import com.intellectualsites.commands.exceptions.parsing.NumberParseException; import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; import java.util.Queue; +import java.util.stream.Collectors; +import java.util.stream.IntStream; @SuppressWarnings("unused") public final class IntegerArgument extends CommandArgument { + private static final int MAX_SUGGESTIONS_INCREMENT = 10; + private static final int NUMBER_SHIFT_MULTIPLIER = 10; + private final int min; private final int max; @@ -224,6 +232,31 @@ public final class IntegerArgument extends CommandArgument { public boolean isContextFree() { return true; } + + @Nonnull + @Override + public List suggestions(@Nonnull final CommandContext commandContext, + @Nonnull final String input) { + if (input.isEmpty()) { + return IntStream.range(0, MAX_SUGGESTIONS_INCREMENT).mapToObj(Integer::toString).collect(Collectors.toList()); + } + try { + final int inputInt = Integer.parseInt(input); + if (inputInt > this.getMax()) { + return Collections.emptyList(); + } else { + final List suggestions = new LinkedList<>(); + suggestions.add(input); /* It's a valid number, so we suggest it */ + for (int i = 0; i < MAX_SUGGESTIONS_INCREMENT + && (inputInt * NUMBER_SHIFT_MULTIPLIER) + i <= this.getMax(); i++) { + suggestions.add(Integer.toString((inputInt * NUMBER_SHIFT_MULTIPLIER) + i)); + } + return suggestions; + } + } catch (final Exception ignored) { + return Collections.emptyList(); /* Invalid input */ + } + } } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidCommandSenderException.java b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidCommandSenderException.java index 662606e2..4920d4db 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidCommandSenderException.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/exceptions/InvalidCommandSenderException.java @@ -62,7 +62,7 @@ public final class InvalidCommandSenderException extends CommandParseException { @Override public String getMessage() { return String.format("%s is not allowed to execute that command. Must be of type %s", - getCommandSender().toString(), + getCommandSender().getClass().getSimpleName(), requiredSender.getSimpleName()); } } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java b/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java index 1d28fb58..86269376 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandSuggestionsTest.java @@ -24,6 +24,7 @@ package com.intellectualsites.commands; import com.intellectualsites.commands.arguments.standard.EnumArgument; +import com.intellectualsites.commands.arguments.standard.IntegerArgument; import com.intellectualsites.commands.arguments.standard.StringArgument; import com.intellectualsites.commands.meta.SimpleCommandMeta; import org.junit.jupiter.api.Assertions; @@ -50,6 +51,14 @@ public class CommandSuggestionsTest { .build()) .argument(EnumArgument.required(TestEnum.class, "enum")) .build()); + manager.command(manager.commandBuilder("test") + .literal("comb") + .argument(StringArgument.newBuilder("str") + .withSuggestionsProvider((c, s) -> Arrays.asList("one", "two")) + .build()) + .argument(IntegerArgument.newBuilder("num") + .withMin(1).withMax(95).asOptional().build()) + .build()); } @Test @@ -59,7 +68,7 @@ public class CommandSuggestionsTest { Assertions.assertTrue(suggestions.isEmpty()); final String input2 = "test "; final List suggestions2 = manager.suggest(new TestCommandSender(), input2); - Assertions.assertEquals(Arrays.asList("one", "two","var"), suggestions2); + Assertions.assertEquals(Arrays.asList("comb", "one", "two","var"), suggestions2); } @Test @@ -85,6 +94,19 @@ public class CommandSuggestionsTest { Assertions.assertTrue(suggestions.isEmpty()); } + @Test + void testComb() { + final String input = "test comb "; + final List suggestions = manager.suggest(new TestCommandSender(), input); + Assertions.assertEquals(Arrays.asList("one", "two"), suggestions); + final String input2 = "test comb one "; + final List suggestions2 = manager.suggest(new TestCommandSender(), input2); + Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions2); + final String input3 = "test comb one 9"; + final List suggestions3 = manager.suggest(new TestCommandSender(), input3); + Assertions.assertEquals(Arrays.asList("9", "90", "91", "92", "93", "94", "95"), suggestions3); + } + public enum TestEnum { FOO, BAR diff --git a/cloud-minecraft/cloud-bukkit-test/pom.xml b/cloud-minecraft/cloud-bukkit-test/pom.xml index a168aab5..60c5f59e 100644 --- a/cloud-minecraft/cloud-bukkit-test/pom.xml +++ b/cloud-minecraft/cloud-bukkit-test/pom.xml @@ -71,5 +71,10 @@ 1.8.8-R0.1-SNAPSHOT provided + + com.intellectualsites + cloud-annotations + 1.0-SNAPSHOT + 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 4fe3d555..18b87084 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 @@ -23,7 +23,14 @@ // 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.Description; +import com.intellectualsites.commands.annotations.specifier.Completions; +import com.intellectualsites.commands.annotations.specifier.Range; import com.intellectualsites.commands.arguments.parser.ArgumentParseResult; +import com.intellectualsites.commands.arguments.parser.StandardParameters; import com.intellectualsites.commands.arguments.standard.BooleanArgument; import com.intellectualsites.commands.arguments.standard.DoubleArgument; import com.intellectualsites.commands.arguments.standard.EnumArgument; @@ -33,17 +40,21 @@ import com.intellectualsites.commands.arguments.standard.StringArgument; import com.intellectualsites.commands.execution.CommandExecutionCoordinator; import com.intellectualsites.commands.parsers.WorldArgument; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; public final class BukkitTest extends JavaPlugin { @@ -54,13 +65,18 @@ public final class BukkitTest extends JavaPlugin { @Override public void onEnable() { try { - final PaperCommandManager mgr = new PaperCommandManager<>( + final PaperCommandManager mgr = new PaperCommandManager<>( this, CommandExecutionCoordinator .simpleCoordinator(), - BukkitCommandSender::of, - BukkitCommandSender::getInternalSender + Function.identity(), + Function.identity() ); + final AnnotationParser annotationParser + = new AnnotationParser<>(mgr, CommandSender.class, p -> + BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION, + "No description")).build()); + annotationParser.parse(this); mgr.registerBrigadier(); mgr.command(mgr.commandBuilder("gamemode", Collections.singleton("gajmöde"), @@ -68,7 +84,7 @@ public final class BukkitTest extends JavaPlugin { .withDescription("Your ugli") .build()) .argument(EnumArgument.required(GameMode.class, "gamemode")) - .argument(StringArgument.newBuilder("player") + .argument(StringArgument.newBuilder("player") .withSuggestionsProvider((v1, v2) -> { final List suggestions = new ArrayList<>( @@ -80,18 +96,17 @@ public final class BukkitTest extends JavaPlugin { suggestions.add("cat"); return suggestions; }).build()) - .handler(c -> c.getSender() - .asPlayer() + .handler(c -> ((Player) c.getSender()) .setGameMode(c.get("gamemode") .orElse(GameMode.SURVIVAL))) .build()) .command(mgr.commandBuilder("kenny") .literal("sux") .argument(IntegerArgument - .newBuilder("perc") + .newBuilder("perc") .withMin(PERC_MIN).withMax(PERC_MAX).build()) .handler(context -> { - context.getSender().asPlayer().sendMessage(String.format( + ((Player) context.getSender()).sendMessage(String.format( "Kenny sux %d%%", context.get("perc").orElse(PERC_MIN) )); @@ -122,13 +137,14 @@ public final class BukkitTest extends JavaPlugin { .sendMessage(String.format("UUID: %s\n", c.get("uuid").orElse(null)))) .build()) .command(mgr.commandBuilder("give") + .withSenderType(Player.class) .argument(EnumArgument.required(Material.class, "material")) .argument(IntegerArgument.required("amount")) .handler(c -> { final Material material = c.getRequired("material"); final int amount = c.getRequired("amount"); final ItemStack itemStack = new ItemStack(material, amount); - c.getSender().asPlayer().getInventory().addItem(itemStack); + ((Player) c.getSender()).getInventory().addItem(itemStack); c.getSender().sendMessage("You've been given stuff, bro."); }) .build()) @@ -138,7 +154,7 @@ public final class BukkitTest extends JavaPlugin { .argument(WorldArgument.required("world")) .handler(c -> { final World world = c.getRequired("world"); - c.getSender().asPlayer().teleport(world.getSpawnLocation()); + ((Player) c.getSender()).teleport(world.getSpawnLocation()); c.getSender().sendMessage("Teleported."); }) .build()) @@ -155,4 +171,12 @@ public final class BukkitTest extends JavaPlugin { } } + @Description("Test cloud command using @CommandMethod") + @CommandMethod(value = "annotation|a [number]", permission = "some.permission.node") + private void annotatedCommand(@Nonnull final Player player, + @Argument("input") @Completions("one,two,duck") @Nonnull final String input, + @Argument("number") @Range(max = "100") final int number) { + player.sendMessage(ChatColor.GOLD + "Your input was: " + ChatColor.AQUA + input + ChatColor.GREEN + " (" + number + ")"); + } + } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java index 8d3775aa..0afbf2cc 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/com/intellectualsites/commands/BukkitCommand.java @@ -24,6 +24,7 @@ package com.intellectualsites.commands; import com.intellectualsites.commands.arguments.CommandArgument; +import com.intellectualsites.commands.arguments.StaticArgument; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginIdentifiableCommand; @@ -32,17 +33,20 @@ import org.bukkit.plugin.Plugin; import javax.annotation.Nonnull; import java.util.List; -final class BukkitCommand - extends org.bukkit.command.Command implements PluginIdentifiableCommand { +final class BukkitCommand extends org.bukkit.command.Command implements PluginIdentifiableCommand { private final CommandArgument command; private final BukkitCommandManager bukkitCommandManager; private final com.intellectualsites.commands.Command cloudCommand; + @SuppressWarnings("unchecked") BukkitCommand(@Nonnull final com.intellectualsites.commands.Command cloudCommand, @Nonnull final CommandArgument command, @Nonnull final BukkitCommandManager bukkitCommandManager) { - super(command.getName()); + super(command.getName(), + cloudCommand.getCommandMeta().getOrDefault("description", ""), + "", + ((StaticArgument) command).getAlternativeAliases()); this.command = command; this.bukkitCommandManager = bukkitCommandManager; this.cloudCommand = cloudCommand; @@ -92,4 +96,9 @@ final class BukkitCommand return this.bukkitCommandManager.getOwningPlugin(); } + @Override + public String getPermission() { + return this.cloudCommand.getCommandPermission(); + } + }