From 667dfec904555cddd4a84fb4ccc3650f99304e9b Mon Sep 17 00:00:00 2001 From: Tim Hagemann Date: Tue, 11 Jun 2019 16:39:35 +0200 Subject: [PATCH] Support simple meta-annotations This adds basic support for meta-annotations for commands and parameters and allows users to create their own custom annotations combining certain existing annotations and values. For example, a user can now define their own `CustomAnnotation` which combines a condition and a flag: ```java @Conditions("conditionname:confitionconfig") @Flags("flagconfig=flagvalue") public @interface CustomAnnotation {} ``` (Necessary `@Retention` and `@Target` ignore for brevity) And use it just like they would normally. This works by recursively going through the annotations, instead of just looking at the root level. The reason most existing annotations had to be touched is for them to be allowed on other annotations as most of them were restricted to method or fields. Currently there's no limit on nesting because if the user wants to nest it to obscure levels - so be it. We could decide to limit this at some point to prevent users from shooting themselves in the foot if that's necessary. `@Dependency` and was specifically ignored as I don't think it makes sense for that to be supported in this use case. Relates to: #89 --- .../java/co/aikar/commands/Annotations.java | 16 ++++- .../commands/annotation/CatchUnknown.java | 2 +- .../commands/annotation/CommandAlias.java | 2 +- .../annotation/CommandCompletion.java | 2 +- .../annotation/CommandPermission.java | 2 +- .../aikar/commands/annotation/Conditions.java | 2 +- .../co/aikar/commands/annotation/Default.java | 2 +- .../commands/annotation/Description.java | 2 +- .../co/aikar/commands/annotation/Flags.java | 2 +- .../commands/annotation/HelpCommand.java | 2 +- .../commands/annotation/HelpSearchTags.java | 2 +- .../aikar/commands/annotation/Optional.java | 2 +- .../aikar/commands/annotation/PreCommand.java | 2 +- .../co/aikar/commands/annotation/Private.java | 2 +- .../co/aikar/commands/annotation/Single.java | 2 +- .../co/aikar/commands/annotation/Split.java | 2 +- .../aikar/commands/annotation/Subcommand.java | 2 +- .../co/aikar/commands/annotation/Syntax.java | 2 +- .../co/aikar/commands/annotation/Values.java | 2 +- .../co/aikar/commands/AnnotationTest.java | 70 +++++++++++++++++++ 20 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 core/src/test/java/co/aikar/commands/AnnotationTest.java diff --git a/core/src/main/java/co/aikar/commands/Annotations.java b/core/src/main/java/co/aikar/commands/Annotations.java index 6e377f88..736fd511 100644 --- a/core/src/main/java/co/aikar/commands/Annotations.java +++ b/core/src/main/java/co/aikar/commands/Annotations.java @@ -50,7 +50,7 @@ class Annotations extends AnnotationLookups { } String getAnnotationValue(AnnotatedElement object, Class annoClass, int options) { - Annotation annotation = object.getAnnotation(annoClass); + Annotation annotation = getAnnotationRecursive(object, annoClass); String value = null; if (annotation != null) { @@ -103,6 +103,20 @@ class Annotations extends AnnotationLookups { return value; } + private static Annotation getAnnotationRecursive(AnnotatedElement object, Class annoClass) { + if (object.isAnnotationPresent(annoClass)) { + return object.getAnnotation(annoClass); + } else { + for (Annotation otherAnnotation : object.getDeclaredAnnotations()) { + final Annotation foundAnnotation = getAnnotationRecursive(otherAnnotation.annotationType(), annoClass); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + } + return null; + } + private static boolean hasOption(int options, int option) { return (options & option) == option; } diff --git a/core/src/main/java/co/aikar/commands/annotation/CatchUnknown.java b/core/src/main/java/co/aikar/commands/annotation/CatchUnknown.java index f861c023..15656ca1 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CatchUnknown.java +++ b/core/src/main/java/co/aikar/commands/annotation/CatchUnknown.java @@ -38,6 +38,6 @@ import java.lang.annotation.Target; * Only one instance of this annotation can be used per root command. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface CatchUnknown { } diff --git a/core/src/main/java/co/aikar/commands/annotation/CommandAlias.java b/core/src/main/java/co/aikar/commands/annotation/CommandAlias.java index 54b6c6dd..43b7a957 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CommandAlias.java +++ b/core/src/main/java/co/aikar/commands/annotation/CommandAlias.java @@ -37,7 +37,7 @@ import java.lang.annotation.Target; * Used on a method, defines a root command alias to that specific command */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) public @interface CommandAlias { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java index 8ad0142e..fbd94116 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java +++ b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java @@ -39,7 +39,7 @@ import java.lang.annotation.Target; * @see {@link co.aikar.commands.CommandCompletions} */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface CommandCompletion { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/CommandPermission.java b/core/src/main/java/co/aikar/commands/annotation/CommandPermission.java index b41bafb5..c4b7335f 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CommandPermission.java +++ b/core/src/main/java/co/aikar/commands/annotation/CommandPermission.java @@ -34,7 +34,7 @@ import java.lang.annotation.Target; * Permission format will vary based on implementation platform */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface CommandPermission { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Conditions.java b/core/src/main/java/co/aikar/commands/annotation/Conditions.java index 00d202c3..1548acb9 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Conditions.java +++ b/core/src/main/java/co/aikar/commands/annotation/Conditions.java @@ -37,7 +37,7 @@ import java.lang.annotation.Target; * @see {@link co.aikar.commands.CommandConditions} */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) public @interface Conditions { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Default.java b/core/src/main/java/co/aikar/commands/annotation/Default.java index 93be2219..f346717e 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Default.java +++ b/core/src/main/java/co/aikar/commands/annotation/Default.java @@ -33,7 +33,7 @@ import java.lang.annotation.Target; * If used on a parameter, sets the value to be used for context resolution */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.PARAMETER}) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Default { String value() default ""; } diff --git a/core/src/main/java/co/aikar/commands/annotation/Description.java b/core/src/main/java/co/aikar/commands/annotation/Description.java index 5d690cdf..201f95fb 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Description.java +++ b/core/src/main/java/co/aikar/commands/annotation/Description.java @@ -33,7 +33,7 @@ import java.lang.annotation.Target; * This is used in the help menus. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) public @interface Description { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Flags.java b/core/src/main/java/co/aikar/commands/annotation/Flags.java index b93d9b6f..76ff489d 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Flags.java +++ b/core/src/main/java/co/aikar/commands/annotation/Flags.java @@ -36,7 +36,7 @@ import java.lang.annotation.Target; * If you want to restrict if an issuer can use the command, please use {@link co.aikar.commands.CommandConditions.Condition} instead. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER}) +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Flags { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/HelpCommand.java b/core/src/main/java/co/aikar/commands/annotation/HelpCommand.java index 45e8c73f..fb84fd73 100644 --- a/core/src/main/java/co/aikar/commands/annotation/HelpCommand.java +++ b/core/src/main/java/co/aikar/commands/annotation/HelpCommand.java @@ -35,7 +35,7 @@ import java.lang.annotation.Target; * a method marked with this annotation should also use a {@link co.aikar.commands.CommandHelp} context parameter to show help. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface HelpCommand { /** * The value to forward to the @Subcommand annotation. Lists which subcommands to register to trigger help diff --git a/core/src/main/java/co/aikar/commands/annotation/HelpSearchTags.java b/core/src/main/java/co/aikar/commands/annotation/HelpSearchTags.java index 0973a0b3..64b297c3 100644 --- a/core/src/main/java/co/aikar/commands/annotation/HelpSearchTags.java +++ b/core/src/main/java/co/aikar/commands/annotation/HelpSearchTags.java @@ -35,7 +35,7 @@ import java.lang.annotation.Target; * be used for help in discovering the correct command, then you can add it as a tag. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface HelpSearchTags { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Optional.java b/core/src/main/java/co/aikar/commands/annotation/Optional.java index b7a96ead..6da11e3d 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Optional.java +++ b/core/src/main/java/co/aikar/commands/annotation/Optional.java @@ -36,6 +36,6 @@ import java.lang.annotation.Target; * you will need to allow for a nullable value. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER}) +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Optional { } diff --git a/core/src/main/java/co/aikar/commands/annotation/PreCommand.java b/core/src/main/java/co/aikar/commands/annotation/PreCommand.java index 3fa16168..feebee93 100644 --- a/core/src/main/java/co/aikar/commands/annotation/PreCommand.java +++ b/core/src/main/java/co/aikar/commands/annotation/PreCommand.java @@ -33,5 +33,5 @@ import java.lang.annotation.Target; * This runs before any other command method each time it is invoked. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface PreCommand {} diff --git a/core/src/main/java/co/aikar/commands/annotation/Private.java b/core/src/main/java/co/aikar/commands/annotation/Private.java index 29a8b0d9..331883c6 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Private.java +++ b/core/src/main/java/co/aikar/commands/annotation/Private.java @@ -32,6 +32,6 @@ import java.lang.annotation.Target; * Marks a command to not be included in stuff like tab completion and help pages */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) public @interface Private { } diff --git a/core/src/main/java/co/aikar/commands/annotation/Single.java b/core/src/main/java/co/aikar/commands/annotation/Single.java index 5d061ef1..1a7bafd7 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Single.java +++ b/core/src/main/java/co/aikar/commands/annotation/Single.java @@ -32,5 +32,5 @@ import java.lang.annotation.Target; * Don't join remaining arguments. Used on String parameters, which normally would combine the remaining arguments */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER}) +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Single {} diff --git a/core/src/main/java/co/aikar/commands/annotation/Split.java b/core/src/main/java/co/aikar/commands/annotation/Split.java index 994e4de2..e79b7748 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Split.java +++ b/core/src/main/java/co/aikar/commands/annotation/Split.java @@ -34,7 +34,7 @@ import java.lang.annotation.Target; * For array based parameters, defines the regex pattern to split on */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER}) +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Split { String value() default ","; } diff --git a/core/src/main/java/co/aikar/commands/annotation/Subcommand.java b/core/src/main/java/co/aikar/commands/annotation/Subcommand.java index 6cb89aee..54335247 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Subcommand.java +++ b/core/src/main/java/co/aikar/commands/annotation/Subcommand.java @@ -36,7 +36,7 @@ import java.lang.annotation.Target; * Defines the part after root command like so: "/rootcommand {@link #value()}". */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) public @interface Subcommand { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Syntax.java b/core/src/main/java/co/aikar/commands/annotation/Syntax.java index 2f5072e4..82d9354d 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Syntax.java +++ b/core/src/main/java/co/aikar/commands/annotation/Syntax.java @@ -38,7 +38,7 @@ import java.lang.annotation.Target; * Use {@link Description} together with the help menu for that purpose. **/ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.PARAMETER}) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Syntax { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Values.java b/core/src/main/java/co/aikar/commands/annotation/Values.java index c6cf8194..5b7931ed 100644 --- a/core/src/main/java/co/aikar/commands/annotation/Values.java +++ b/core/src/main/java/co/aikar/commands/annotation/Values.java @@ -34,7 +34,7 @@ import java.lang.annotation.Target; * You may also use {@link CommandCompletion} handler codes here to feed dynamic values and avoid repetition. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PARAMETER}) +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface Values { String value(); } diff --git a/core/src/test/java/co/aikar/commands/AnnotationTest.java b/core/src/test/java/co/aikar/commands/AnnotationTest.java new file mode 100644 index 00000000..364a50b3 --- /dev/null +++ b/core/src/test/java/co/aikar/commands/AnnotationTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2019 Daniel Ennis (Aikar) - MIT License + * + * 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 co.aikar.commands; + +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Description; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AnnotationTest { + + private final CommandManager manager = Mockito.mock(CommandManager.class); + private Annotations annotations = new Annotations<>(this.manager); + + @Test + public void testAnnotationsSimple() { + final String aliasAnnotation = this.annotations.getAnnotationValue(TestClass.class, CommandAlias.class, Annotations.NOTHING); + assertEquals("msg", aliasAnnotation); + final String permissionAnnotation = this.annotations.getAnnotationValue(TestClass.class, CommandPermission.class, Annotations.NOTHING); + assertEquals("test.test", permissionAnnotation); + final String descriptionAnnotation = this.annotations.getAnnotationValue(TestClass.class, Description.class, Annotations.NOTHING); + assertEquals("Just a test command", descriptionAnnotation); + + + final String aliasAnnotationRoot = this.annotations.getAnnotationValue(TestWithRootAnnotation.class, CommandAlias.class, Annotations.NOTHING); + assertEquals("test", aliasAnnotationRoot); + } + + @Retention(RetentionPolicy.RUNTIME) + @CommandAlias("msg") + @CommandPermission("test.test") + @Description("Just a test command") + private @interface TestAnnotation { + } + + @TestAnnotation + private static final class TestClass { + } + + @CommandAlias("test") + private static final class TestWithRootAnnotation { + } +}