diff --git a/CHANGELOG.md b/CHANGELOG.md index 85e219b0f..221118dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expose the Command which led to `InvalidCommandSenderException`s - Expose the CommandContext which led to `CommandExecutionException`s - Added helper methods for command flags to MutableCommandBuilder + - Added injection services + - Added a Guice injection service - Add predicate permissions +### Deprecated + - Deprecated ParameterInjectorRegistry#injectors + ## [1.3.0] - 2020-12-18 ### Added @@ -33,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - String keyed command meta + - Deprecated ParameterInjectorRegistry#injectors ### Fixed - Fixed issue with task synchronization diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index e33f3fa54..3b15e61da 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,6 +4,9 @@ object Versions { const val errorprone = "2.4.0" const val errorprone_javac = "9+181-r4173-1" + // INTEGRATION DEPENDENCIES + const val guice = "4.2.3" + // DISCORD DEPENDENCIES const val javacord = "3.1.1" const val jda = "4.2.0_209" 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 57aca59b9..d61084962 100644 --- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java +++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/MethodCommandExecutionHandler.java @@ -23,7 +23,6 @@ // package cloud.commandframework.annotations; -import cloud.commandframework.annotations.injection.ParameterInjector; import cloud.commandframework.annotations.injection.ParameterInjectorRegistry; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.flags.FlagContext; @@ -37,9 +36,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; class MethodCommandExecutionHandler implements CommandExecutionHandler { @@ -90,19 +89,13 @@ public void execute(final @NonNull CommandContext commandContext) { if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) { arguments.add(commandContext.getSender()); } else { - final Collection> injectors = this.injectorRegistry.injectors(parameter.getType()); - Object value = null; - for (final ParameterInjector injector : injectors) { - value = injector.create( - commandContext, - this.annotationAccessor - ); - if (value != null) { - break; - } - } - if (value != null) { - arguments.add(value); + final Optional value = this.injectorRegistry.getInjectable( + parameter.getType(), + commandContext, + this.annotationAccessor + ); + if (value.isPresent()) { + arguments.add(value.get()); } else { throw new IllegalArgumentException(String.format( "Unknown command parameter '%s' in method '%s'", diff --git a/cloud-core/build.gradle.kts b/cloud-core/build.gradle.kts index 3b51433a0..da8813449 100644 --- a/cloud-core/build.gradle.kts +++ b/cloud-core/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { api(project(":cloud-services")) + compileOnly("com.google.inject", "guice", Versions.guice) testImplementation("org.openjdk.jmh", "jmh-core", Versions.jmh) testImplementation("org.openjdk.jmh", "jmh-generator-annprocess", Versions.jmh) + testImplementation("com.google.inject", "guice", Versions.guice) } diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/GuiceInjectionService.java b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/GuiceInjectionService.java new file mode 100644 index 000000000..44eda89e2 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/GuiceInjectionService.java @@ -0,0 +1,69 @@ +// +// 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.injection; + +import cloud.commandframework.annotations.AnnotationAccessor; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.types.tuples.Triplet; +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * {@link InjectionService Injection service} that injects using a Guice {@link Injector} + * + * @param Command sender type + * @since 1.4.0 + */ +public final class GuiceInjectionService implements InjectionService { + + private final Injector injector; + + private GuiceInjectionService(final @NonNull Injector injector) { + this.injector = injector; + } + + /** + * Create a new Guice injection service that wraps the given injector + * + * @param injector Injector to wrap + * @param Command sender type + * @return the created injection service + */ + public static GuiceInjectionService create(final @NonNull Injector injector) { + return new GuiceInjectionService<>(injector); + } + + @Override + @SuppressWarnings("EmptyCatch") + public @Nullable Object handle(final @NonNull Triplet, Class, AnnotationAccessor> triplet) { + try { + return this.injector.getInstance(triplet.getSecond()); + } catch (final ConfigurationException ignored) { + } + return null; + } + +} diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/InjectionService.java b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/InjectionService.java new file mode 100644 index 000000000..a5e85b9e8 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/InjectionService.java @@ -0,0 +1,43 @@ +// +// 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.injection; + +import cloud.commandframework.annotations.AnnotationAccessor; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.services.types.Service; +import cloud.commandframework.types.tuples.Triplet; + +/** + * Service that can be registered to the {@link ParameterInjectorRegistry} in order to provide + * custom injection support. This can be used to integrate the Cloud with existing dependency + * injection frameworks. + * + * @param Command sender type + * @since 1.4.0 + */ +@FunctionalInterface +public interface InjectionService extends + Service, Class, AnnotationAccessor>, Object> { + +} diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjectorRegistry.java b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjectorRegistry.java index 9ae97b9c5..1e0a08269 100644 --- a/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjectorRegistry.java +++ b/cloud-core/src/main/java/cloud/commandframework/annotations/injection/ParameterInjectorRegistry.java @@ -23,7 +23,13 @@ // package cloud.commandframework.annotations.injection; +import cloud.commandframework.annotations.AnnotationAccessor; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.services.ServicePipeline; +import cloud.commandframework.types.tuples.Triplet; +import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -32,6 +38,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; /** * Registry containing mappings between {@link Class classes} and {@link ParameterInjector injectors} @@ -39,10 +46,20 @@ * @param Command sender type * @since 1.2.0 */ -public final class ParameterInjectorRegistry { +@SuppressWarnings("ALL") +public final class ParameterInjectorRegistry implements InjectionService { private volatile int injectorCount = 0; private final Map, List>> injectors = new HashMap<>(); + private final ServicePipeline servicePipeline = ServicePipeline.builder().build(); + + /** + * Create a new parameter injector registry + */ + public ParameterInjectorRegistry() { + servicePipeline.registerServiceType(new TypeToken>() { + }, this); + } /** * Register an injector for a particular type @@ -61,12 +78,16 @@ public synchronized void registerInjector( } /** - * Get a collection of all injectors that could potentially inject a value of the given type + * Get a collection of all injectors that could potentially inject a value of the given type. This + * does not include injectors from external injector services, instead it only uses injectors + * registered using {@link #registerInjector(Class, ParameterInjector)}. * * @param clazz Type to query for * @param Generic type * @return Immutable collection containing all injectors that could potentially inject a value of the given type + * @deprecated Inject directly instead of relying on this list */ + @Deprecated public synchronized @NonNull Collection<@NonNull ParameterInjector> injectors( final @NonNull Class clazz ) { @@ -79,4 +100,55 @@ public synchronized void registerInjector( return Collections.unmodifiableCollection(injectors); } + @Override + public @Nullable Object handle(final @NonNull Triplet, Class, AnnotationAccessor> triplet) { + for (final ParameterInjector injector : this.injectors(triplet.getSecond())) { + final Object value = injector.create(triplet.getFirst(), triplet.getThird()); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Attempt to get an injectable value for the given context. This will consider all registered + * {@link InjectionService injection services}, and not just the {@link ParameterInjector injectors} + * registered using {@link #registerInjector(Class, ParameterInjector)}. + * + * @param clazz Class of the to inject + * @param context The command context that requests the injection + * @param annotationAccessor Annotation accessor for the injection. If the object is requested without access to annotations, + * use {@link AnnotationAccessor#empty()} + * @param Type to inject + * @return The injected value, if an injector was able to provide a value + * @since 1.4.0 + */ + @SuppressWarnings("EmptyCatch") + public <@NonNull T> @NonNull Optional getInjectable( + final @NonNull Class clazz, + final @NonNull CommandContext context, + final @NonNull AnnotationAccessor annotationAccessor + ) { + final Triplet, Class, AnnotationAccessor> triplet = Triplet.of(context, clazz, annotationAccessor); + try { + return Optional.of(clazz.cast(this.servicePipeline.pump(triplet).through(new TypeToken>() { + }).getResult())); + } catch (final IllegalStateException ignored) { + } + return Optional.empty(); + } + + /** + * Register an injection service that will be able to provide injections using + * {@link #getInjectable(Class, CommandContext, AnnotationAccessor)}. + * + * @param service Service implementation + * @since 1.4.0 + */ + public void registerInjectionService(final InjectionService service) { + this.servicePipeline.registerServiceImplementation(new TypeToken>() { + }, service, Collections.emptyList()); + } + } diff --git a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java index 1ab9a2f9a..f38d60c80 100644 --- a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java @@ -25,7 +25,6 @@ import cloud.commandframework.CommandManager; import cloud.commandframework.annotations.AnnotationAccessor; -import cloud.commandframework.annotations.injection.ParameterInjector; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.flags.FlagContext; import cloud.commandframework.captions.Caption; @@ -603,13 +602,7 @@ public void setCurrentArgument(final @Nullable CommandArgument argument) { "Cannot retrieve injectable values from a command context that is not associated with a command manager" ); } - for (final ParameterInjector injector : this.commandManager.parameterInjectorRegistry().injectors(clazz)) { - final Object value = injector.create(this, AnnotationAccessor.empty()); - if (value != null) { - return Optional.of((T) value); - } - } - return Optional.empty(); + return this.commandManager.parameterInjectorRegistry().getInjectable(clazz, this, AnnotationAccessor.empty()); } diff --git a/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java b/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java new file mode 100644 index 000000000..d2375f914 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/ParameterInjectorRegistryTest.java @@ -0,0 +1,104 @@ +// +// 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 cloud.commandframework.annotations.injection.GuiceInjectionService; +import cloud.commandframework.annotations.injection.ParameterInjectorRegistry; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandContextFactory; +import cloud.commandframework.context.StandardCommandContextFactory; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ParameterInjectorRegistryTest { + + private static final int INJECTED_INTEGER = 5; + + private ParameterInjectorRegistry parameterInjectorRegistry; + private TestCommandSender commandSender; + private CommandContextFactory commandContextFactory; + private CommandManager commandManager; + private Injector injector; + + @BeforeEach + public void setup() { + this.commandSender = new TestCommandSender(); + this.commandManager = new TestCommandManager(); + this.commandContextFactory = new StandardCommandContextFactory<>(); + this.parameterInjectorRegistry = new ParameterInjectorRegistry<>(); + this.parameterInjectorRegistry.registerInjector(Integer.class, (context, annotationAccessor) -> INJECTED_INTEGER); + this.commandSender = new TestCommandSender(); + this.injector = Guice.createInjector(new TestModule()); + } + + private @NonNull CommandContext createContext() { + return this.commandContextFactory.create(false, this.commandSender, this.commandManager); + } + + @Test + public void testSimpleInjection() { + Assertions.assertEquals(INJECTED_INTEGER, parameterInjectorRegistry.getInjectable( + Integer.class, + this.createContext(), + AnnotationAccessor.empty() + ).orElse(-1)); + } + + @Test + public void testGuiceInjection() { + this.parameterInjectorRegistry.registerInjectionService(GuiceInjectionService.create(this.injector)); + Assertions.assertEquals(TestModule.INJECTED_INTEGER, parameterInjectorRegistry.getInjectable( + Integer.class, + this.createContext(), + AnnotationAccessor.empty() + ).orElse(-1)); + } + + @Test + public void testNonExistentInjection() { + Assertions.assertNull(parameterInjectorRegistry.getInjectable( + String.class, + this.createContext(), + AnnotationAccessor.empty() + ).orElse(null)); + } + + private static final class TestModule extends AbstractModule { + + private static final int INJECTED_INTEGER = 10; + + @Override + protected void configure() { + bind(Integer.class).toInstance(INJECTED_INTEGER); + } + + } + +}