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 badad3d8..0a5624c9 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 @@ -23,15 +23,20 @@ // package com.intellectualsites.commands.annotations; +import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; import com.intellectualsites.commands.Command; 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.execution.CommandExecutionHandler; import com.intellectualsites.commands.meta.CommandMeta; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; @@ -39,6 +44,7 @@ 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.Predicate; @@ -55,7 +61,7 @@ public final class AnnotationParser { 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_OPTIONAL = Pattern.compile("\\[([A-Za-z0-9]+)]").asPredicate(); private final CommandManager manager; @@ -91,40 +97,134 @@ public final class AnnotationParser { } commandMethodPairs.add(new CommandMethodPair(method, commandMethod)); } - return this.construct(commandMethodPairs); + final Collection> commands = this.construct(instance, commandMethodPairs); + for (final Command command : commands) { + this.manager.command(command); + } + return commands; } @Nonnull - private Collection> construct(@Nonnull final Collection methodPairs) { + @SuppressWarnings("unchecked") + private Collection> construct(@Nonnull final Object instance, + @Nonnull final Collection methodPairs) { final Collection> commands = new ArrayList<>(); for (final CommandMethodPair commandMethodPair : methodPairs) { final CommandMethod commandMethod = commandMethodPair.getCommandMethod(); final Method method = commandMethodPair.getMethod(); - final Map tokens = this.parseSyntax(commandMethod.value()); - Command.Builder builder = this.manager.commandBuilder(commandMethod.value(), - Collections.emptyList(), - manager.createDefaultCommandMeta()); + 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()); final Collection arguments = this.getArguments(method); + final Map> commandArguments = Maps.newHashMap(); + /* Go through all annotated parameters and build up the argument tree */ for (final ArgumentParameterPair argumentPair : arguments) { - final Parameter parameter = argumentPair.getParameter(); - final Collection annotations = Arrays.asList(parameter.getAnnotations()); - final TypeToken token = TypeToken.of(parameter.getParameterizedType()); - final ParserParameters parameters = this.manager.getParserRegistry() - .parseAnnotations(token, annotations); - final ArgumentParser parser = this.manager.getParserRegistry() - .createParser(token, parameters) - .orElseThrow(() -> new IllegalArgumentException( - String.format("Parameter '%s' in method '%s' " - + "has parser '%s' but no parser exists " - + "for that type", - parameter.getName(), method.getName(), - token.toString()))); + final CommandArgument argument = this.buildArgument(method, + tokens.get(argumentPair.getArgument().value()), + argumentPair); + commandArguments.put(argument.getName(), argument); } - + boolean commandNameFound = false; + /* Build the command tree */ + for (final Map.Entry entry : tokens.entrySet()) { + if (!commandNameFound) { + commandNameFound = true; + continue; + } + if (entry.getValue() == ArgumentMode.LITERAL) { + builder = builder.literal(entry.getKey()); + } else { + final CommandArgument argument = commandArguments.get(entry.getKey()); + if (argument == null) { + throw new IllegalArgumentException(String.format( + "Found no mapping for argument '%s' in method '%s'", + entry.getKey(), method.getName() + )); + } + builder = builder.argument(argument); + } + } + /* Decorate command data */ + builder = builder.withPermission(commandMethod.permission()).withSenderType(commandMethod.requiredSender()); + /* Construct the handler */ + final CommandExecutionHandler commandExecutionHandler = commandContext -> { + final List parameters = new ArrayList<>(method.getParameterCount()); + for (final Parameter parameter : method.getParameters()) { + if (parameter.isAnnotationPresent(Argument.class)) { + final Argument argument = parameter.getAnnotation(Argument.class); + final CommandArgument commandArgument = commandArguments.get(argument.value()); + if (commandArgument.isRequired()) { + parameters.add(commandContext.getRequired(argument.value())); + } else { + final Object optional = commandContext.get(argument.value()).orElse(null); + parameters.add(optional); + } + } else { + if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) { + parameters.add(commandContext.getSender()); + } else { + throw new IllegalArgumentException(String.format( + "Unknown command parameter '%s' in method '%s'", + parameter.getName(), method.getName() + )); + } + } + } + try { + method.invoke(instance, parameters.toArray()); + } catch (final IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + }; + builder = builder.handler(commandExecutionHandler); + commands.add(builder.build()); } return commands; } + @Nonnull + @SuppressWarnings("unchecked") + private CommandArgument buildArgument(@Nonnull final Method method, + @Nullable final ArgumentMode argumentMode, + @Nonnull final ArgumentParameterPair argumentPair) { + final Parameter parameter = argumentPair.getParameter(); + final Collection annotations = Arrays.asList(parameter.getAnnotations()); + final TypeToken token = TypeToken.of(parameter.getParameterizedType()); + final ParserParameters parameters = this.manager.getParserRegistry() + .parseAnnotations(token, annotations); + final ArgumentParser parser = this.manager.getParserRegistry() + .createParser(token, parameters) + .orElseThrow(() -> new IllegalArgumentException( + String.format("Parameter '%s' in method '%s' " + + "has parser '%s' but no parser exists " + + "for that type", + parameter.getName(), method.getName(), + token.toString()))); + if (argumentMode == null || argumentMode == ArgumentMode.LITERAL) { + throw new IllegalArgumentException(String.format( + "Invalid command argument '%s' in method '%s': " + + "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) { + if (argument.defaultValue().isEmpty()) { + argumentBuilder.asOptional(); + } else { + argumentBuilder.asOptionalWithDefault(argument.defaultValue()); + } + } else { + argumentBuilder.asRequired(); + } + return argumentBuilder.manager(this.manager).withParser(parser).build(); + } + @Nonnull LinkedHashMap parseSyntax(@Nonnull final String syntax) { final StringTokenizer stringTokenizer = new StringTokenizer(syntax, " "); @@ -135,10 +235,12 @@ public final class AnnotationParser { map.put(token.substring(1, token.length() - 1), ArgumentMode.REQUIRED); } else if (PATTERN_ARGUMENT_OPTIONAL.test(token)) { map.put(token.substring(1, token.length() - 1), ArgumentMode.OPTIONAL); - } else { + } 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); + } else { + throw new IllegalArgumentException(String.format("Unrecognizable syntax token '%s'", syntax)); } } return map; diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java index 8fb2fc4d..2256e4d7 100644 --- a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java @@ -42,13 +42,6 @@ public @interface Argument { */ String value(); - /** - * Whether or not the argument is required - * - * @return {@code true} if the argument is required, else {@code false} - */ - boolean required() default true; - /** * Name of the argument parser * @@ -56,4 +49,11 @@ public @interface Argument { */ String parserName() default ""; + /** + * Get the default value + * + * @return Default value + */ + String defaultValue() 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 6a6db351..999e82dc 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 @@ -24,13 +24,18 @@ 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; import com.intellectualsites.commands.meta.SimpleCommandMeta; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; 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 { @@ -54,4 +59,20 @@ class AnnotationParserTest { Assertions.assertEquals(map, arguments); } + @Test + void testMethodConstruction() { + final Collection> commands = annotationParser.parse(this); + Assertions.assertFalse(commands.isEmpty()); + manager.executeCommand(new TestCommandSender(), "test 10").join(); + Assertions.assertThrows(CompletionException.class, () -> + manager.executeCommand(new TestCommandSender(), "test 101").join()); + } + + @CommandMethod("test [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) { + System.out.printf("Received int: %d and string '%s'\n", argument, string); + } + } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java index 73353caf..cfd9e5da 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/arguments/CommandArgument.java @@ -128,7 +128,7 @@ public class CommandArgument implements Comparable> */ @Nonnull public static CommandArgument.Builder ofType(@Nonnull final Class clazz, - @Nonnull final String name) { + @Nonnull final String name) { return new Builder<>(clazz, name); }