From 9550dce5e6b50b96553958dc5477172d70e8af64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 14 Jan 2021 10:56:27 +0100 Subject: [PATCH] :sparkles: Add access to parameter annotations in the parameter injector --- CHANGELOG.md | 1 + .../MethodCommandExecutionHandler.java | 2 +- .../annotations/AnnotationAccessor.java | 12 ++++ .../MultiDelegateAnnotationAccessor.java | 64 +++++++++++++++++++ .../injection/ParameterInjector.java | 2 +- .../AnnotationAccessorTest.java | 64 +++++++++++++++++++ .../ParameterInjectorRegistryTest.java | 8 +-- 7 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 cloud-core/src/main/java/cloud/commandframework/annotations/MultiDelegateAnnotationAccessor.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/AnnotationAccessorTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c6437919..73279162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow command argument names to include `_` and `-` ([#186](https://github.com/Incendo/cloud/pull/186)) - Make it easier to use translatable components with MinecraftHelp ([#197](https://github.com/Incendo/cloud/pull/197)) - Show "No result for query" when a multi-help topic is empty + - Use the method+field annotation accessor rather than the method accessor when injecting method parameters ### Deprecated - Description, and everything using Description directly ([#207](https://github.com/Incendo/cloud/pull/207)) diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java index d6108496..c41bb92a 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java @@ -92,7 +92,7 @@ class MethodCommandExecutionHandler implements CommandExecutionHandler { final Optional value = this.injectorRegistry.getInjectable( parameter.getType(), commandContext, - this.annotationAccessor + AnnotationAccessor.of(AnnotationAccessor.of(parameter), this.annotationAccessor) ); if (value.isPresent()) { arguments.add(value.get()); diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/AnnotationAccessor.java b/cloud-core/src/main/java/cloud/commandframework/annotations/AnnotationAccessor.java index 091b137d..419c2d66 100644 --- a/cloud-core/src/main/java/cloud/commandframework/annotations/AnnotationAccessor.java +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/AnnotationAccessor.java @@ -60,6 +60,18 @@ public interface AnnotationAccessor { return new AnnotatedElementAccessor(element); } + /** + * Get a {@link AnnotationAccessor} instance that delegates to multiple {@link AnnotatedElement} instances. + * The first accessor that provides a requested annotation will always be used + * + * @param accessors The accessor to delegate to + * @return Annotation accessor that delegates to the given accessors (using their natural ordering) + * @since 1.4.0 + */ + static @NonNull AnnotationAccessor of(final @NonNull AnnotationAccessor@NonNull... accessors) { + return new MultiDelegateAnnotationAccessor(accessors); + } + /** * Get an annotation instance, if it's present. If the annotation * isn't available, this will return {@code null} diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/MultiDelegateAnnotationAccessor.java b/cloud-core/src/main/java/cloud/commandframework/annotations/MultiDelegateAnnotationAccessor.java new file mode 100644 index 00000000..26ec0557 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/MultiDelegateAnnotationAccessor.java @@ -0,0 +1,64 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework.annotations; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +final class MultiDelegateAnnotationAccessor implements AnnotationAccessor { + + private final AnnotationAccessor[] accessors; + + MultiDelegateAnnotationAccessor(final @NonNull AnnotationAccessor@NonNull... accessors) { + this.accessors = accessors; + } + + @Override + public @Nullable A annotation(@NonNull final Class clazz) { + A instance = null; + for (final AnnotationAccessor annotationAccessor : accessors) { + instance = annotationAccessor.annotation(clazz); + if (instance != null) { + break; + } + } + return instance; + } + + @Override + public @NonNull Collection<@NonNull Annotation> annotations() { + final List annotationList = new LinkedList<>(); + for (final AnnotationAccessor annotationAccessor : accessors) { + annotationList.addAll(annotationAccessor.annotations()); + } + return Collections.unmodifiableCollection(annotationList); + } + +} diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjector.java b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjector.java index 51d97eeb..306348b7 100644 --- a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjector.java +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjector.java @@ -44,7 +44,7 @@ public interface ParameterInjector { * annotated method. If the injector cannot (or shouldn't) create a value, it is free to return {@code null}. * * @param context Command context that is requesting the injection - * @param annotationAccessor Annotation accessor proxying the method which the value is being injected into + * @param annotationAccessor Annotation accessor proxying the method and parameter which the value is being injected into * @return The value, if it could be created. Else {@code null}, in which case no value will be injected * by this particular injector */ diff --git a/cloud-core/src/test/java/cloud/commandframework/AnnotationAccessorTest.java b/cloud-core/src/test/java/cloud/commandframework/AnnotationAccessorTest.java new file mode 100644 index 00000000..6c0ad034 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/AnnotationAccessorTest.java @@ -0,0 +1,64 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// 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 cloud.commandframework; + +import cloud.commandframework.annotations.AnnotationAccessor; +import org.junit.jupiter.api.Assertions; +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.lang.reflect.Parameter; + +public class AnnotationAccessorTest { + + @Qualifier("method") + public static void annotatedMethod(@Qualifier("parameter") final String parameter) { + } + + @Test + void testQualifierResolutionOrder() throws Exception { + final Method method = AnnotationAccessorTest.class.getMethod("annotatedMethod", String.class); + final Parameter parameter = method.getParameters()[0]; + final AnnotationAccessor accessor = AnnotationAccessor.of( + AnnotationAccessor.of(parameter), + AnnotationAccessor.of(method) + ); + final Qualifier qualifier = accessor.annotation(Qualifier.class); + Assertions.assertNotNull(qualifier); + Assertions.assertEquals("parameter", qualifier.value()); + } + + @Target({ElementType.PARAMETER, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + private @interface Qualifier { + + String value(); + + } + +} diff --git a/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java b/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java index d2375f91..d49118e1 100644 --- a/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java @@ -48,7 +48,7 @@ public class ParameterInjectorRegistryTest { private Injector injector; @BeforeEach - public void setup() { + void setup() { this.commandSender = new TestCommandSender(); this.commandManager = new TestCommandManager(); this.commandContextFactory = new StandardCommandContextFactory<>(); @@ -63,7 +63,7 @@ public class ParameterInjectorRegistryTest { } @Test - public void testSimpleInjection() { + void testSimpleInjection() { Assertions.assertEquals(INJECTED_INTEGER, parameterInjectorRegistry.getInjectable( Integer.class, this.createContext(), @@ -72,7 +72,7 @@ public class ParameterInjectorRegistryTest { } @Test - public void testGuiceInjection() { + void testGuiceInjection() { this.parameterInjectorRegistry.registerInjectionService(GuiceInjectionService.create(this.injector)); Assertions.assertEquals(TestModule.INJECTED_INTEGER, parameterInjectorRegistry.getInjectable( Integer.class, @@ -82,7 +82,7 @@ public class ParameterInjectorRegistryTest { } @Test - public void testNonExistentInjection() { + void testNonExistentInjection() { Assertions.assertNull(parameterInjectorRegistry.getInjectable( String.class, this.createContext(),