diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9e235c..0f9b50fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Core: Allow for setting a custom `CaptionVariableReplacementHandler` on the command manager ([#352](https://github.com/Incendo/cloud/pull/352)) - Core: Add `DurationArgument` for parsing `java.time.Duration` ([#330](https://github.com/Incendo/cloud/pull/330)) +- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363)) +- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363)) - Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353)) ### Fixed diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index fe83e816..523629a5 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -996,6 +996,16 @@ public class Command { ); } + /** + * Returns the current command execution handler. + * + * @return the current handler + * @since 1.7.0 + */ + public @NonNull CommandExecutionHandler handler() { + return this.commandExecutionHandler; + } + /** * Specify a required sender type * diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java index da682dce..5d3c567f 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/CommandExecutionHandler.java @@ -25,7 +25,10 @@ package cloud.commandframework.execution; import cloud.commandframework.Command; import cloud.commandframework.context.CommandContext; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -38,6 +41,35 @@ import org.checkerframework.checker.nullness.qual.Nullable; @FunctionalInterface public interface CommandExecutionHandler { + /** + * Returns a {@link CommandExecutionHandler} that does nothing (no-op). + * + * @param Command sender type + * @return command execution handler that does nothing + * @since 1.7.0 + */ + static @NonNull CommandExecutionHandler noOpCommandExecutionHandler() { + return new NullCommandExecutionHandler<>(); + } + + /** + * Returns a {@link CommandExecutionHandler} that delegates the given + * {@code handlers} in sequence. + *

+ * If any handler in the chain throws an exception, then no subsequent + * handlers will be invoked. + * + * @param handlers The handlers to delegate to + * @param Command sender type + * @return multicast-delegate command execution handler + * @since 1.7.0 + */ + static @NonNull CommandExecutionHandler delegatingExecutionHandler( + final List> handlers + ) { + return new MulticastDelegateFutureCommandExecutionHandler<>(handlers); + } + /** * Handle command execution * @@ -101,4 +133,46 @@ public interface CommandExecutionHandler { } + /** + * Delegates to other handlers. + * + * @param Command sender type + * @see #delegatingExecutionHandler(List) + * @since 1.7.0 + */ + final class MulticastDelegateFutureCommandExecutionHandler implements FutureCommandExecutionHandler { + + private final List> handlers; + + private MulticastDelegateFutureCommandExecutionHandler( + final @NonNull List<@NonNull CommandExecutionHandler> handlers + ) { + this.handlers = Collections.unmodifiableList(handlers); + } + + @Override + public CompletableFuture<@Nullable Void> executeFuture( + @NonNull final CommandContext commandContext + ) { + @MonotonicNonNull CompletableFuture<@Nullable Void> composedHandler = null; + + if (this.handlers.isEmpty()) { + composedHandler = CompletableFuture.completedFuture(null); + } else { + for (final CommandExecutionHandler handler : this.handlers) { + if (composedHandler == null) { + composedHandler = handler.executeFuture(commandContext); + } else { + composedHandler = composedHandler.thenCompose( + ignore -> handler.executeFuture(commandContext) + ); + } + } + } + + return composedHandler; + } + + } + } diff --git a/cloud-core/src/test/java/cloud/commandframework/execution/MulticastDelegateFutureCommandExecutionHandlerTest.java b/cloud-core/src/test/java/cloud/commandframework/execution/MulticastDelegateFutureCommandExecutionHandlerTest.java new file mode 100644 index 00000000..16be013b --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/execution/MulticastDelegateFutureCommandExecutionHandlerTest.java @@ -0,0 +1,110 @@ +// +// MIT License +// +// Copyright (c) 2021 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.execution; + +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.context.CommandContext; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +@ExtendWith(MockitoExtension.class) +class MulticastDelegateFutureCommandExecutionHandlerTest { + + @Mock + private CommandContext context; + + @Test + void ExecuteFuture_HappyFlow_Success() { + // Arrange + final CommandExecutionHandler handlerA = mock(CommandExecutionHandler.class); + when(handlerA.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final CommandExecutionHandler handlerB = mock(CommandExecutionHandler.class); + when(handlerB.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final CommandExecutionHandler handlerC = mock(CommandExecutionHandler.class); + when(handlerC.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final CommandExecutionHandler delegatingHandler = CommandExecutionHandler.delegatingExecutionHandler( + Arrays.asList(handlerA, handlerB, handlerC) + ); + + // Act + delegatingHandler.executeFuture(this.context).join(); + + // Assert + final InOrder inOrder = Mockito.inOrder(handlerA, handlerB, handlerC); + inOrder.verify(handlerA).executeFuture(notNull()); + inOrder.verify(handlerB).executeFuture(notNull()); + inOrder.verify(handlerC).executeFuture(notNull()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + void ExecuteFuture_FailedFuture_StopsDelegating() { + // Arrange + final CommandExecutionHandler handlerA = mock(CommandExecutionHandler.class); + when(handlerA.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + final CommandExecutionHandler handlerB = mock(CommandExecutionHandler.class); + final CompletableFuture futureB = new CompletableFuture<>(); + futureB.completeExceptionally(new RuntimeException()); + when(handlerB.executeFuture(any())).thenReturn(futureB); + + final CommandExecutionHandler handlerC = mock(CommandExecutionHandler.class); + + final CommandExecutionHandler delegatingHandler = CommandExecutionHandler.delegatingExecutionHandler( + Arrays.asList(handlerA, handlerB, handlerC) + ); + + // Act + final CompletableFuture future = delegatingHandler.executeFuture(this.context); + assertThrows( + CompletionException.class, + future::join + ); + + // Assert + final InOrder inOrder = Mockito.inOrder(handlerA, handlerB, handlerC); + inOrder.verify(handlerA).executeFuture(notNull()); + inOrder.verify(handlerB).executeFuture(notNull()); + inOrder.verify(handlerC, never()).executeFuture(notNull()); + inOrder.verifyNoMoreInteractions(); + } +}