From e43a3c7194261e70e1f5450c482d1160e3297b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 17 Sep 2020 22:34:58 +0200 Subject: [PATCH] Start working on the annotated command method system --- cloud-annotations/pom.xml | 47 ++++ .../annotations/AnnotationParser.java | 210 ++++++++++++++++++ .../commands/annotations/Argument.java | 59 +++++ .../commands/annotations/CommandMethod.java | 59 +++++ .../commands/annotations/package-info.java | 28 +++ .../commands/CommandManager.java | 4 +- pom.xml | 1 + 7 files changed, 406 insertions(+), 2 deletions(-) create mode 100644 cloud-annotations/pom.xml create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/CommandMethod.java create mode 100644 cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/package-info.java diff --git a/cloud-annotations/pom.xml b/cloud-annotations/pom.xml new file mode 100644 index 00000000..fb988a60 --- /dev/null +++ b/cloud-annotations/pom.xml @@ -0,0 +1,47 @@ + + + + + + cloud + com.intellectualsites + 1.0-SNAPSHOT + + 4.0.0 + + cloud-annotations + + + com.intellectualsites + cloud-core + 1.0-SNAPSHOT + + + + 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 new file mode 100644 index 00000000..12af8e52 --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/AnnotationParser.java @@ -0,0 +1,210 @@ +// +// 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 com.google.common.reflect.TypeToken; +import com.intellectualsites.commands.Command; +import com.intellectualsites.commands.CommandManager; +import com.intellectualsites.commands.arguments.parser.ArgumentParser; +import com.intellectualsites.commands.arguments.parser.ParserParameters; +import com.intellectualsites.commands.meta.CommandMeta; + +import javax.annotation.Nonnull; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +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.Map; +import java.util.StringTokenizer; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Parser that parses class instances {@link Command commands} + * + * @param Command sender type + * @param Command meta type + */ +public 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 final CommandManager manager; + + /** + * Construct a new annotation parser + * + * @param manager Command manager instance + */ + public AnnotationParser(@Nonnull final CommandManager manager) { + this.manager = manager; + } + + /** + * Scan a class instance of {@link CommandMethod} annotations and attempt to + * compile them into {@link Command} instances + * + * @param instance Instance to scan + * @param Type of the instance + * @return Collection of parsed annotations + */ + @Nonnull + public Collection> parse(@Nonnull final T instance) { + final Method[] methods = instance.getClass().getMethods(); + final Collection commandMethodPairs = new ArrayList<>(); + for (final Method method : methods) { + final CommandMethod commandMethod = method.getAnnotation(CommandMethod.class); + if (commandMethod == null) { + continue; + } + if (method.getReturnType() != Void.TYPE) { + throw new IllegalArgumentException(String.format("@CommandMethod annotated method '%s' has non-void return type", + method.getName())); + } + commandMethodPairs.add(new CommandMethodPair(method, commandMethod)); + } + return this.construct(commandMethodPairs); + } + + @Nonnull + private Collection> construct(@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 Collection arguments = this.getArguments(method); + 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()))); + } + + } + return commands; + } + + @Nonnull + private LinkedHashMap parseSyntax(@Nonnull final String syntax) { + final StringTokenizer stringTokenizer = new StringTokenizer(syntax, " "); + final LinkedHashMap map = new LinkedHashMap<>(); + while (stringTokenizer.hasMoreTokens()) { + final String token = stringTokenizer.nextToken(); + if (PATTERN_ARGUMENT_REQUIRED.test(token)) { + 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 { + final String[] literals = token.split("\\|"); + /* Actually use the other literals as well */ + map.put(literals[0], ArgumentMode.LITERAL); + } + } + return map; + } + + @Nonnull + private Collection getArguments(@Nonnull final Method method) { + final Collection arguments = new ArrayList<>(); + for (final Parameter parameter : method.getParameters()) { + if (!parameter.isAnnotationPresent(Argument.class)) { + continue; + } + arguments.add(new ArgumentParameterPair(parameter, parameter.getAnnotation(Argument.class))); + } + return arguments; + } + + + private static final class CommandMethodPair { + + private final Method method; + private final CommandMethod commandMethod; + + private CommandMethodPair(@Nonnull final Method method, @Nonnull final CommandMethod commandMethod) { + this.method = method; + this.commandMethod = commandMethod; + } + + @Nonnull + private Method getMethod() { + return this.method; + } + + @Nonnull + private CommandMethod getCommandMethod() { + return this.commandMethod; + } + + } + + + private static final class ArgumentParameterPair { + + private final Parameter parameter; + private final Argument argument; + + private ArgumentParameterPair(@Nonnull final Parameter parameter, @Nonnull final Argument argument) { + this.parameter = parameter; + this.argument = argument; + } + + @Nonnull + private Parameter getParameter() { + return this.parameter; + } + + @Nonnull + private Argument getArgument() { + return this.argument; + } + + } + + + private enum ArgumentMode { + LITERAL, OPTIONAL, REQUIRED + } + +} 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 new file mode 100644 index 00000000..8fb2fc4d --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/Argument.java @@ -0,0 +1,59 @@ +// +// 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; + +/** + * Annotation used to indicate that a method parameter is a command argument + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Argument { + + /** + * Argument name + * + * @return Argument name + */ + 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 + * + * @return Argument name + */ + String parserName() default ""; + +} diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/CommandMethod.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/CommandMethod.java new file mode 100644 index 00000000..b9746b24 --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/CommandMethod.java @@ -0,0 +1,59 @@ +// +// 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; + +/** + * Used to declare a class method as a command method + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CommandMethod { + + /** + * Command syntax + * + * @return Command syntax + */ + String value(); + + /** + * The command permission node + * + * @return Command permission node + */ + String permission() default ""; + + /** + * The required sender + * + * @return Required sender + */ + Class requiredSender() default Object.class; + +} diff --git a/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/package-info.java b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/package-info.java new file mode 100644 index 00000000..fb11c77f --- /dev/null +++ b/cloud-annotations/src/main/java/com/intellectualsites/commands/annotations/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. +// + +/** + * Annotation parsing related classes + */ +package com.intellectualsites.commands.annotations; diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java index c19bb20e..9bcd5cce 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandManager.java @@ -45,10 +45,10 @@ import com.intellectualsites.services.ServicePipeline; import com.intellectualsites.services.State; import javax.annotation.Nonnull; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -212,7 +212,7 @@ public abstract class CommandManager { */ @Nonnull public Command.Builder commandBuilder(@Nonnull final String name, - @Nonnull final Set aliases, + @Nonnull final Collection aliases, @Nonnull final M meta) { return Command.newBuilder(name, meta, aliases.toArray(new String[0])); } diff --git a/pom.xml b/pom.xml index 89243148..8a6ef31e 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ cloud-minecraft/cloud-bukkit cloud-minecraft/cloud-paper cloud-minecraft/cloud-brigadier + cloud-annotations pom 2020