From 9ed40a698ab1bc2a2c96d1d266d6634507cd4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Fri, 18 Dec 2020 18:59:59 +0100 Subject: [PATCH] :sparkles: Allow interception of command builders based on annotations in AnnotationParser Fixes #179 --- CHANGELOG.md | 1 + .../annotations/AnnotationParser.java | 36 +++++++++++++++++-- .../annotations/AnnotationParserTest.java | 34 ++++++++++++++++-- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15fd5dd5..8d6e1c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `@Suggestions` annotated methods - Added `@Parser` annotated methods - Type safe meta system + - Allow interception of command builders based on annotations in AnnotationParser ### Changed - Moved the parser injector registry into CommandManager and added injection to CommandContext 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 d4bd060f..0e0e754c 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -83,6 +83,8 @@ public final class AnnotationParser { private final Map, Function> annotationMappers; private final Map, Function, @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult>>> preprocessorMappers; + private final Map, BiFunction, Command.Builder>> + builderModifiers; private final Class commandSenderClass; private final MetaFactory metaFactory; private final FlagExtractor flagExtractor; @@ -107,6 +109,7 @@ public final class AnnotationParser { this.metaFactory = new MetaFactory(this, metaMapper); this.annotationMappers = new HashMap<>(); this.preprocessorMappers = new HashMap<>(); + this.builderModifiers = new HashMap<>(); this.flagExtractor = new FlagExtractor(manager); this.registerAnnotationMapper(CommandDescription.class, d -> ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); @@ -177,6 +180,25 @@ public final class AnnotationParser { return getMethodOrClassAnnotation(method, clazz) != null; } + /** + * Register a builder modifier for a specific annotation. The builder modifiers are + * allowed to act on a {@link Command.Builder} after all arguments have been added + * to the builder. This allows for modifications of the builder instance before + * the command is registered to the command manager. + * + * @param annotation Annotation (class) that the builder modifier reacts to + * @param builderModifier Modifier that acts on the given annotation and the incoming builder. Command builders + * are immutable, so the modifier should return the instance of the command builder that is + * returned as a result of any operation on the builder + * @param Annotation type + */ + public void registerBuilderModifier( + final @NonNull Class annotation, + final @NonNull BiFunction, Command.Builder> builderModifier + ) { + this.builderModifiers.put(annotation, builderModifier); + } + /** * Register an annotation mapper * @@ -275,9 +297,9 @@ public final class AnnotationParser { method.setAccessible(true); } if (method.getParameterCount() != 2 - || !method.getReturnType().equals(List.class) - || !method.getParameters()[0].getType().equals(CommandContext.class) - || !method.getParameters()[1].getType().equals(String.class) + || !method.getReturnType().equals(List.class) + || !method.getParameters()[0].getType().equals(CommandContext.class) + || !method.getParameters()[1].getType().equals(String.class) ) { throw new IllegalArgumentException(String.format( "@Suggestions annotated method '%s' in class '%s' does not have the correct signature", @@ -457,6 +479,14 @@ public final class AnnotationParser { for (final CommandFlag flag : flags) { builder = builder.flag(flag); } + for (final Annotation annotation : method.getDeclaredAnnotations()) { + @SuppressWarnings("ALL") + final BiFunction builderModifier = this.builderModifiers.get(annotation.annotationType()); + if (builderModifier == null) { + continue; + } + builder = (Command.Builder) builderModifier.apply(annotation, builder); + } /* Construct and register the command */ final Command builtCommand = builder.build(); commands.add(builtCommand); 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 5a3c49ce..3828626d 100644 --- a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java @@ -30,6 +30,7 @@ import cloud.commandframework.annotations.specifier.Range; import cloud.commandframework.annotations.suggestions.Suggestions; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserParameters; +import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.context.CommandContext; import cloud.commandframework.meta.SimpleCommandMeta; @@ -78,7 +79,11 @@ class AnnotationParserTest { InjectableValue.class, (context, annotations) -> new InjectableValue("Hello World!") ); - + /* Register a builder modifier */ + annotationParser.registerBuilderModifier( + IntegerArgumentInjector.class, + (injector, builder) -> builder.argument(IntegerArgument.of(injector.value())) + ); /* Parse the class. Required for both testMethodConstruction() and testNamedSuggestionProvider() */ commands = annotationParser.parse(this); } @@ -135,7 +140,7 @@ class AnnotationParserTest { @Test void testParameterInjection() { - manager.executeCommand(new TestCommandSender(), "inject").join(); + manager.executeCommand(new TestCommandSender(), "injected 10").join(); } @Test @@ -167,6 +172,11 @@ class AnnotationParserTest { ).contains("Stella")); } + @Test + void testInjectedCommand() { + manager.executeCommand(new TestCommandSender(), "injected 10").join(); + } + @Suggestions("cows") public List cowSuggestions(final CommandContext context, final String input) { return Arrays.asList("Stella", "Bella", "Agda"); @@ -177,6 +187,12 @@ class AnnotationParserTest { return new CustomType("yay"); } + @IntegerArgumentInjector + @CommandMethod("injected") + public void injectedCommand(final CommandContext context) { + System.out.printf("Got an integer: %d\n", context.get("number")); + } + @ProxiedBy("proxycommand") @CommandMethod("test|t literal [string]") public void testCommand( @@ -278,4 +294,18 @@ class AnnotationParserTest { } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private @interface IntegerArgumentInjector { + + /** + * The name of the integer argument to insert + * + * @return Integer argument name + */ + String value() default "number"; + + } + }