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 { + } +}