From a68bc0bea7634aff1f46da032fe221f51f748cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Sat, 24 Oct 2020 19:28:56 +0200 Subject: [PATCH] :sparkles: Allow for recursive annotations (#97) Co-authored-by: Mariell --- CHANGELOG.md | 3 +- .../annotations/AnnotationParser.java | 36 ++++++++-- .../annotations/AnnotationParserTest.java | 66 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ec6b86..72968b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Allow for combined presence flags, such that `-a -b -c` is equivalent to `-abc` - - Allow for class annotations as a default for when an annotation is not present on a method. + - Allow for class annotations as a default for when an annotation is not present on a method + - Allow for annotated annotations ### Fixed - Fix arguments with no required children not being executors (cloud-brigadier) 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 7acdf825..2ff29100 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/AnnotationParser.java @@ -51,11 +51,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; +import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; @@ -106,16 +108,40 @@ public final class AnnotationParser { )); } + @SuppressWarnings("unchecked") + static @Nullable A getAnnotationRecursively( + final @NonNull Annotation[] annotations, + final @NonNull Class clazz, + final @NonNull Set> checkedAnnotations + ) { + A innerCandidate = null; + for (final Annotation annotation : annotations) { + if (!checkedAnnotations.add(annotation.annotationType())) { + continue; + } + if (annotation.annotationType().equals(clazz)) { + return (A) annotation; + } + if (annotation.annotationType().getPackage().getName().startsWith("java.lang")) { + continue; + } + final A inner = getAnnotationRecursively(annotation.annotationType().getAnnotations(), clazz, checkedAnnotations); + if (inner != null) { + innerCandidate = inner; + } + } + return innerCandidate; + } + static @Nullable A getMethodOrClassAnnotation( final @NonNull Method method, final @NonNull Class clazz ) { - if (method.isAnnotationPresent(clazz)) { - return method.getAnnotation(clazz); - } else if (method.getDeclaringClass().isAnnotationPresent(clazz)) { - return method.getDeclaringClass().getAnnotation(clazz); + A annotation = getAnnotationRecursively(method.getAnnotations(), clazz, new HashSet<>()); + if (annotation == null) { + annotation = getAnnotationRecursively(method.getDeclaringClass().getAnnotations(), clazz, new HashSet<>()); } - return null; + return annotation; } static boolean methodOrClassHasAnnotation( 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 519b7bec..3c00dea2 100644 --- a/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java +++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/AnnotationParserTest.java @@ -32,6 +32,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -76,6 +81,34 @@ class AnnotationParserTest { Assertions.assertEquals(NAMED_SUGGESTIONS, manager.suggest(new TestCommandSender(), "namedsuggestions ")); } + @Test + void testAnnotationResolver() throws Exception { + final Class annotatedClass = AnnotatedClass.class; + final Method annotatedMethod = annotatedClass.getDeclaredMethod("annotatedMethod"); + + System.out.println("Looking for @CommandDescription"); + final CommandDescription commandDescription = AnnotationParser.getMethodOrClassAnnotation(annotatedMethod, + CommandDescription.class); + Assertions.assertNotNull(commandDescription); + Assertions.assertEquals("Hello World!", commandDescription.value()); + + System.out.println("Looking for @CommandPermission"); + final CommandPermission commandPermission = AnnotationParser.getMethodOrClassAnnotation(annotatedMethod, + CommandPermission.class); + Assertions.assertNotNull(commandPermission); + Assertions.assertEquals("some.permission", commandPermission.value()); + + System.out.println("Looking for @CommandMethod"); + final CommandMethod commandMethod = AnnotationParser.getMethodOrClassAnnotation(annotatedMethod, + CommandMethod.class); + Assertions.assertNotNull(commandMethod); + Assertions.assertEquals("method", commandMethod.value()); + + System.out.println("Looking for @Regex"); + @SuppressWarnings("unused") + final Regex regex = AnnotationParser.getMethodOrClassAnnotation(annotatedMethod, Regex.class); + } + @ProxiedBy("proxycommand") @CommandMethod("test|t literal [string]") public void testCommand( @@ -103,4 +136,37 @@ class AnnotationParserTest { ) { } + + @CommandPermission("some.permission") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private @interface AnnotatedAnnotation { + } + + + @Bad1 + @CommandDescription("Hello World!") + private static class AnnotatedClass { + + @CommandMethod("method") + @AnnotatedAnnotation + public static void annotatedMethod() { + } + + } + + + @Bad2 + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + private @interface Bad1 { + } + + + @Bad1 + @Target({ElementType.METHOD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + private @interface Bad2 { + } + }