From d2b5290dc4b4f7277e41f302f0893b0fb01c4850 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Wed, 6 Apr 2016 17:08:39 +0300 Subject: [PATCH 01/26] Remove empty lines. --- .../org/spine3/server/command/CommandBus.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index b004d363cb8..c3057be9c14 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -131,19 +131,15 @@ public void unregister(CommandHandler handler) { public Response validate(Message command) { checkNotNull(command); final CommandClass commandClass = CommandClass.of(command); - if (dispatcherRegistered(commandClass)) { return Responses.ok(); } - if (handlerRegistered(commandClass)) { return Responses.ok(); } - //TODO:2015-12-16:alexander.yevsyukov: Implement command validation for completeness of commands. // Presumably, it would be CommandValidator which would be exposed by // corresponding Aggregates or ProcessManagers, and then contributed to validator registry. - return CommandValidation.unsupportedCommand(command); } @@ -156,25 +152,18 @@ public Response validate(Message command) { * the class of the passed command */ public List post(Command request) { - //TODO:2016-01-24:alexander.yevsyukov: Do not return value. - checkNotNull(request); - store(request); - final CommandClass commandClass = CommandClass.of(request); - if (dispatcherRegistered(commandClass)) { return dispatch(request); } - if (handlerRegistered(commandClass)) { final Message command = getMessage(request); final CommandContext context = request.getContext(); return invokeHandler(command, context); } - throw new UnsupportedCommandException(getMessage(request)); } @@ -197,9 +186,7 @@ private List invokeHandler(Message msg, CommandContext context) { List result = Collections.emptyList(); try { result = method.invoke(msg, context); - commandStatus.setOk(context.getCommandId()); - } catch (InvocationTargetException e) { final CommandId commandId = context.getCommandId(); final Throwable cause = e.getCause(); @@ -217,7 +204,6 @@ private List invokeHandler(Message msg, CommandContext context) { commandStatus.setToError(commandId, Errors.fromThrowable(cause)); } } - return result; } @@ -258,6 +244,7 @@ private List invokeHandler(Message msg, CommandContext context) { * The helper class for updating command status. */ private static class CommandStatusHelper { + private final CommandStore commandStore; private CommandStatusHelper(CommandStore commandStore) { @@ -312,7 +299,6 @@ public void close() throws Exception { private enum LogSingleton { INSTANCE; - @SuppressWarnings("NonSerializableFieldInSerializableClass") private final Logger value = LoggerFactory.getLogger(CommandBus.class); } From f4dc5ff0acc49e6c5f2cdc1a0a121ec21163aadb Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Wed, 6 Apr 2016 17:14:34 +0300 Subject: [PATCH 02/26] Use `checkNotDefault()` method. --- .../main/java/org/spine3/server/command/CommandBus.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index c3057be9c14..5327f953221 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -44,6 +44,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.spine3.base.Commands.*; +import static org.spine3.validate.Validate.checkNotDefault; /** * Dispatches the incoming commands to the corresponding handler. @@ -129,7 +130,7 @@ public void unregister(CommandHandler handler) { * {@link CommandValidation#unsupportedCommand(Message)} otherwise */ public Response validate(Message command) { - checkNotNull(command); + checkNotDefault(command); final CommandClass commandClass = CommandClass.of(command); if (dispatcherRegistered(commandClass)) { return Responses.ok(); @@ -153,7 +154,9 @@ public Response validate(Message command) { */ public List post(Command request) { //TODO:2016-01-24:alexander.yevsyukov: Do not return value. - checkNotNull(request); + checkNotDefault(request); + + store(request); final CommandClass commandClass = CommandClass.of(request); if (dispatcherRegistered(commandClass)) { From 18b04beda80cf23dde7a839b07a0a6c9c7e07e45 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Thu, 7 Apr 2016 13:50:08 +0300 Subject: [PATCH 03/26] Add command scheduler to command bus. --- .../main/java/org/spine3/base/Commands.java | 29 +++++ .../src/main/proto/spine/base/command.proto | 9 ++ .../java/org/spine3/base/CommandsShould.java | 28 +++++ .../org/spine3/server/command/CommandBus.java | 45 +++++--- .../server/command/CommandScheduler.java | 47 ++++++++ .../command/DefaultCommandScheduler.java | 103 ++++++++++++++++++ .../server/command/CommandBusShould.java | 41 ++++++- .../DefaultCommandSchedulerShould.java | 95 ++++++++++++++++ 8 files changed, 379 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/org/spine3/server/command/CommandScheduler.java create mode 100644 server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java create mode 100644 server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java diff --git a/client/src/main/java/org/spine3/base/Commands.java b/client/src/main/java/org/spine3/base/Commands.java index 869ea079732..007d2560f9c 100644 --- a/client/src/main/java/org/spine3/base/Commands.java +++ b/client/src/main/java/org/spine3/base/Commands.java @@ -21,6 +21,7 @@ package org.spine3.base; import com.google.common.base.Predicate; +import com.google.protobuf.Duration; import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import org.spine3.protobuf.Messages; @@ -37,6 +38,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.protobuf.util.TimeUtil.getCurrentTime; +import static org.spine3.protobuf.Timestamps.isAfter; +import static org.spine3.validate.Validate.isNotDefault; /** * Client-side utilities for working with commands. @@ -179,4 +182,30 @@ public static String formatMessageTypeAndId(String format, Message commandMessag final String result = String.format(format, commandType, id); return result; } + + /** + * Checks if the command is scheduled to be sent later. + * + * @param command a command to check + * @return {@code true} if the command context has valid delay or sending time set, {@code false} otherwise + */ + public static boolean isScheduled(Command command) { + final CommandContext context = command.getContext(); + final Duration delay = context.getDelay(); + if (isNotDefault(delay)) { + checkArgument(delay.getSeconds() > 0, "Command delay seconds must be a positive value."); + return true; + } + final Timestamp sendingTime = context.getSendingTime(); + if (isNotDefault(sendingTime)) { + checkIsAfterNow(sendingTime); + return true; + } + return false; + } + + private static void checkIsAfterNow(Timestamp sendingTime) { + final Timestamp now = getCurrentTime(); + checkArgument(isAfter(sendingTime, /*than*/ now), "Sending time must be after the current time."); + } } diff --git a/client/src/main/proto/spine/base/command.proto b/client/src/main/proto/spine/base/command.proto index 6c07c3a03cf..967890559b0 100644 --- a/client/src/main/proto/spine/base/command.proto +++ b/client/src/main/proto/spine/base/command.proto @@ -27,6 +27,7 @@ option java_multiple_files = true; option java_generate_equals_and_hash = true; import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/any.proto"; import "spine/base/user_id.proto"; @@ -77,6 +78,14 @@ message CommandContext { // The `namespace` attribute must be defined for commands in multitenant applications. Namespace namespace = 5; + + oneof time_opts { + // The delay to wait before sending the command. + google.protobuf.Duration delay = 6; + + // The time when the command should be sent. + google.protobuf.Timestamp sending_time = 7; + } } // Enumeration of possible failures when validating a command. diff --git a/client/src/test/java/org/spine3/base/CommandsShould.java b/client/src/test/java/org/spine3/base/CommandsShould.java index 622fd107e33..f9d0bb8a3dd 100644 --- a/client/src/test/java/org/spine3/base/CommandsShould.java +++ b/client/src/test/java/org/spine3/base/CommandsShould.java @@ -36,7 +36,10 @@ import java.util.Collection; import java.util.List; +import static com.google.protobuf.util.TimeUtil.add; +import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static org.junit.Assert.*; +import static org.spine3.protobuf.Durations.seconds; import static org.spine3.protobuf.Timestamps.minutesAgo; import static org.spine3.protobuf.Timestamps.secondsAgo; import static org.spine3.test.Tests.hasPrivateUtilityConstructor; @@ -120,4 +123,29 @@ public void create_logging_message_for_command_with_type_and_id() { assertTrue(string.contains(typeName)); assertTrue(string.contains(commandId)); } + + @Test + public void when_command_delay_is_set_then_return_true() { + final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() + .setDelay(seconds(10)) + .build()); + + assertTrue(Commands.isScheduled(cmd)); + } + + @Test + public void when_command_sending_time_is_set_then_return_true() { + final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() + .setSendingTime(add(getCurrentTime(), seconds(10))) + .build()); + + assertTrue(Commands.isScheduled(cmd)); + } + + @Test + public void when_command_is_not_scheduled_then_return_false() { + final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.getDefaultInstance()); + + assertFalse(Commands.isScheduled(cmd)); + } } diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index 5327f953221..c36fefd45ce 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -39,10 +39,10 @@ import javax.annotation.CheckReturnValue; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.emptyList; import static org.spine3.base.Commands.*; import static org.spine3.validate.Validate.checkNotDefault; @@ -61,10 +61,13 @@ public class CommandBus implements AutoCloseable { private final CommandStatusHelper commandStatus; private final ProblemLog problemLog = new ProblemLog(); + private CommandScheduler scheduler; @CheckReturnValue public static CommandBus create(CommandStore store) { - return new CommandBus(checkNotNull(store)); + final CommandBus commandBus = new CommandBus(checkNotNull(store)); + commandBus.setScheduler(new DefaultCommandScheduler(commandBus)); + return commandBus; } protected CommandBus(CommandStore commandStore) { @@ -115,6 +118,13 @@ public void unregister(CommandHandler handler) { handlerRegistry.unregister(handler); } + /** + * Sets a command scheduler. + */ + public void setScheduler(CommandScheduler scheduler) { + this.scheduler = scheduler; + } + /** * Verifies if the command can be posted to this {@code CommandBus}. * @@ -147,33 +157,35 @@ public Response validate(Message command) { /** * Directs a command request to the corresponding handler. * - * @param request the command request to be processed + * @param command the command request to be processed * @return a list of the events as the result of handling the command * @throws UnsupportedCommandException if there is neither handler nor dispatcher registered for * the class of the passed command */ - public List post(Command request) { + public List post(Command command) { //TODO:2016-01-24:alexander.yevsyukov: Do not return value. - checkNotDefault(request); - - - store(request); - final CommandClass commandClass = CommandClass.of(request); + checkNotDefault(command); + store(command); + if (isScheduled(command)) { + scheduler.schedule(command); + return emptyList(); + } + final CommandClass commandClass = CommandClass.of(command); if (dispatcherRegistered(commandClass)) { - return dispatch(request); + return dispatch(command); } if (handlerRegistered(commandClass)) { - final Message command = getMessage(request); - final CommandContext context = request.getContext(); - return invokeHandler(command, context); + final Message message = getMessage(command); + final CommandContext context = command.getContext(); + return invokeHandler(message, context); } - throw new UnsupportedCommandException(getMessage(request)); + throw new UnsupportedCommandException(getMessage(command)); } private List dispatch(Command command) { final CommandClass commandClass = CommandClass.of(command); final CommandDispatcher dispatcher = getDispatcher(commandClass); - List result = Collections.emptyList(); + List result = emptyList(); try { result = dispatcher.dispatch(command); } catch (Exception e) { @@ -186,7 +198,7 @@ private List dispatch(Command command) { private List invokeHandler(Message msg, CommandContext context) { final CommandClass commandClass = CommandClass.of(msg); final CommandHandlerMethod method = getHandler(commandClass); - List result = Collections.emptyList(); + List result = emptyList(); try { result = method.invoke(msg, context); commandStatus.setOk(context.getCommandId()); @@ -298,6 +310,7 @@ public void close() throws Exception { dispatcherRegistry.unregisterAll(); handlerRegistry.unregisterAll(); commandStore.close(); + scheduler.shutdown(); } private enum LogSingleton { diff --git a/server/src/main/java/org/spine3/server/command/CommandScheduler.java b/server/src/main/java/org/spine3/server/command/CommandScheduler.java new file mode 100644 index 00000000000..3003ef96089 --- /dev/null +++ b/server/src/main/java/org/spine3/server/command/CommandScheduler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.command; + +import org.spine3.base.Command; + +/** + * Schedules commands sending them after the given delay (or at the given time). + * + * @author Alexander Litus + */ +public interface CommandScheduler { + + /** + * Schedule a command and send it after the given delay (or at the given time). + * + * @param command a command to send later + * @throws IllegalStateException if the scheduler is shut down + */ + void schedule(Command command); + + /** + * Initiates an orderly shutdown in which previously scheduled commands will be sent later, + * but no new commands will be accepted. + * + *

Invocation has no effect if the scheduler is already shut down. + */ + void shutdown(); +} diff --git a/server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java b/server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java new file mode 100644 index 00000000000..6e227c95e71 --- /dev/null +++ b/server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.command; + +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import org.spine3.base.Command; +import org.spine3.base.CommandContext; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.protobuf.util.TimeUtil.getCurrentTime; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.spine3.validate.Validate.isNotDefault; + +/** + * The default command scheduler implementation. + * + * @author Alexander Litus + */ +public class DefaultCommandScheduler implements CommandScheduler { + + private static final int MIN_THEAD_POOL_SIZE = 5; + + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(MIN_THEAD_POOL_SIZE); + + private boolean isShutdown = false; + + private final CommandBus commandBus; + + /** + * Creates a new instance. + * + * @param commandBus a command bus used to send scheduled commands + */ + public DefaultCommandScheduler(CommandBus commandBus) { + this.commandBus = commandBus; + } + + @Override + public void schedule(Command command) { + checkState(!isShutdown, "Scheduler is shut down."); + final long delaySec = getDelaySeconds(command); + final Command commandUpdated = removeSchedulingOptions(command); + executorService.schedule(new Runnable() { + @Override + public void run() { + commandBus.post(commandUpdated); + } + }, delaySec, SECONDS); + } + + private static long getDelaySeconds(Command command) { + final CommandContext context = command.getContext(); + final long delaySec; + final Duration delay = context.getDelay(); + if (isNotDefault(delay)) { + delaySec = delay.getSeconds(); + } else { + final Timestamp sendingTime = context.getSendingTime(); + final Timestamp now = getCurrentTime(); + delaySec = sendingTime.getSeconds() - now.getSeconds(); + } + return delaySec; + } + + private static Command removeSchedulingOptions(Command command) { + final CommandContext contextUpdated = command.getContext().toBuilder() + .clearDelay() + .clearSendingTime() + .build(); + final Command result = command.toBuilder() + .setContext(contextUpdated) + .build(); + return result; + } + + @Override + public void shutdown() { + executorService.shutdown(); + isShutdown = true; + } +} diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index 4b59e2459f0..e2727a4f57c 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -49,9 +49,12 @@ import java.util.List; import java.util.Set; +import static com.google.protobuf.util.TimeUtil.add; +import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; +import static org.spine3.protobuf.Durations.minutes; import static org.spine3.server.command.CommandValidation.isUnsupportedCommand; import static org.spine3.testdata.TestCommands.*; @@ -61,12 +64,14 @@ public class CommandBusShould { private CommandBus commandBus; private CommandStore commandStore; private CommandFactory commandFactory; + private DefaultCommandScheduler scheduler; @Before public void setUp() { commandStore = mock(CommandStore.class); - + scheduler = mock(DefaultCommandScheduler.class); commandBus = CommandBus.create(commandStore); + commandBus.setScheduler(scheduler); commandFactory = TestCommandFactory.newInstance(CommandBusShould.class); } @@ -398,7 +403,7 @@ public void set_command_status_to_failure_when_handler_throws_failure() throws T // Verify we logged the failure. verify(log, atMost(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId)); } - +// TODO:2016-04-07:alexander.litus: check all atMost() @Test public void set_command_status_to_failure_when_handler_throws_exception() throws TestFailure, TestThrowable { final CreateProjectHandler handler = mock(CreateProjectHandler.class); @@ -444,4 +449,36 @@ public void set_command_status_to_failure_when_handler_throws_unknown_Throwable( // Verify we logged the failure. verify(log, atMost(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId)); } + + @Test + public void schedule_command_if_delay_is_set() { + final Command cmd = Commands.create(createProject(newUuid()), CommandContext.newBuilder() + .setDelay(minutes(1)) + .build()); + + commandBus.post(cmd); + + verify(scheduler, times(1)).schedule(cmd); + } + + @Test + public void schedule_command_if_sending_time_is_set() { + final Command cmd = Commands.create(createProject(newUuid()), CommandContext.newBuilder() + .setSendingTime(add(getCurrentTime(), minutes(1))) + .build()); + + commandBus.post(cmd); + + verify(scheduler, times(1)).schedule(cmd); + } + + @Test + public void do_not_schedule_command_if_no_scheduling_options_are_set() { + commandBus.register(new CreateProjectHandler()); + final Command cmd = commandFactory.create(createProject(newUuid())); + + commandBus.post(cmd); + + verify(scheduler, times(0)).schedule(cmd); + } } diff --git a/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java new file mode 100644 index 00000000000..aa07ebf91f8 --- /dev/null +++ b/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.command; + +import com.google.protobuf.Duration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.spine3.base.Command; +import org.spine3.base.CommandContext; +import org.spine3.base.Commands; +import org.spine3.test.project.command.CreateProject; + +import static com.google.protobuf.util.TimeUtil.add; +import static com.google.protobuf.util.TimeUtil.getCurrentTime; +import static org.mockito.Mockito.*; +import static org.spine3.base.Identifiers.newUuid; +import static org.spine3.protobuf.Durations.seconds; +import static org.spine3.testdata.TestCommands.createProject; + +/** + * @author Alexander Litus + */ +@SuppressWarnings("InstanceMethodNamingConvention") +public class DefaultCommandSchedulerShould { + + private static final Duration DELAY = seconds(1); + private static final int CHECK_OFFSET_MS = 50; + + private CommandBus commandBus; + private CommandScheduler scheduler; + + @Before + public void setUpTest() { + this.commandBus = mock(CommandBus.class); + this.scheduler = new DefaultCommandScheduler(commandBus); + } + + @After + public void tearDownTest() { + scheduler.shutdown(); + } + + @Test + public void schedule_command_if_delay_is_set() throws InterruptedException { + final CreateProject msg = createProject(newUuid()); + final Command cmdPrimary = Commands.create(msg, CommandContext.newBuilder() + .setDelay(DELAY) + .build()); + final Command cmdExpected = Commands.create(msg, CommandContext.getDefaultInstance()); + + scheduler.schedule(cmdPrimary); + + assertCommandSent(cmdExpected, DELAY); + } + + @Test + public void schedule_command_if_sending_time_is_set() throws InterruptedException { + final CreateProject msg = createProject(newUuid()); + final Command cmdPrimary = Commands.create(msg, CommandContext.newBuilder() + .setSendingTime(add(getCurrentTime(), DELAY)) + .build()); + final Command cmdExpected = Commands.create(msg, CommandContext.getDefaultInstance()); + + scheduler.schedule(cmdPrimary); + + assertCommandSent(cmdExpected, DELAY); + } + + private void assertCommandSent(Command cmd, Duration delay) throws InterruptedException { + verify(commandBus, never()).post(any(Command.class)); + + Thread.sleep((delay.getSeconds() * 1000) + CHECK_OFFSET_MS); + + verify(commandBus, times(1)).post(cmd); + } +} From e0e8ecc6bfda431a4569f82a8f6022a88e2db97d Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Thu, 7 Apr 2016 16:03:25 +0300 Subject: [PATCH 04/26] Fix command bus tests. --- .../org/spine3/server/command/CommandBus.java | 6 +- .../server/command/CommandBusShould.java | 139 +++++++++--------- 2 files changed, 74 insertions(+), 71 deletions(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index c36fefd45ce..f0a3b8d8d53 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -60,7 +60,7 @@ public class CommandBus implements AutoCloseable { private final CommandStore commandStore; private final CommandStatusHelper commandStatus; - private final ProblemLog problemLog = new ProblemLog(); + private ProblemLog problemLog = new ProblemLog(); private CommandScheduler scheduler; @CheckReturnValue @@ -251,8 +251,8 @@ private List invokeHandler(Message msg, CommandContext context) { } @VisibleForTesting - /* package */ ProblemLog getProblemLog() { - return problemLog; + /* package */ void setProblemLog(ProblemLog problemLog) { + this.problemLog = problemLog; } /** diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index e2727a4f57c..3592d8b32b2 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -65,13 +65,16 @@ public class CommandBusShould { private CommandStore commandStore; private CommandFactory commandFactory; private DefaultCommandScheduler scheduler; + private CommandBus.ProblemLog log; @Before public void setUp() { commandStore = mock(CommandStore.class); - scheduler = mock(DefaultCommandScheduler.class); commandBus = CommandBus.create(commandStore); + scheduler = mock(DefaultCommandScheduler.class); commandBus.setScheduler(scheduler); + log = mock(CommandBus.ProblemLog.class); + commandBus.setProblemLog(log); commandFactory = TestCommandFactory.newInstance(CommandBusShould.class); } @@ -216,8 +219,7 @@ private static class CreateProjectHandler implements CommandHandler { private boolean handlerInvoked = false; @Assign - public ProjectCreated handle(CreateProject command, CommandContext ctx) - throws TestFailure, TestThrowable { + public ProjectCreated handle(CreateProject command, CommandContext ctx) throws TestFailure, TestThrowable { handlerInvoked = true; return ProjectCreated.getDefaultInstance(); } @@ -242,7 +244,7 @@ public List dispatch(Command request) throws Exception { @Test public void unregister_handler() { - final CreateProjectHandler handler = new CreateProjectHandler(); + final CommandHandler handler = new CreateProjectHandler(); commandBus.register(handler); commandBus.unregister(handler); final String projectId = newUuid(); @@ -251,8 +253,8 @@ public void unregister_handler() { @Test public void validate_commands_both_dispatched_and_handled() { - final CreateProjectHandler handler = new CreateProjectHandler(); - final AddTaskDispatcher dispatcher = new AddTaskDispatcher(); + final CommandHandler handler = new CreateProjectHandler(); + final CommandDispatcher dispatcher = new AddTaskDispatcher(); commandBus.register(handler); commandBus.register(dispatcher); @@ -291,12 +293,19 @@ public void have_log() { public void close_CommandStore_when_closed() throws Exception { commandBus.close(); - verify(commandStore, atMost(1)).close(); + verify(commandStore, times(1)).close(); + } + + @Test + public void shutdown_CommandScheduler_when_closed() throws Exception { + commandBus.close(); + + verify(scheduler, times(1)).shutdown(); } @Test public void remove_all_handlers_on_close() throws Exception { - final CreateProjectHandler handler = new CreateProjectHandler(); + final CommandHandler handler = new CreateProjectHandler(); commandBus.register(handler); commandBus.close(); @@ -342,112 +351,92 @@ public void set_command_status_to_OK_when_handler_returns() { commandBus.post(command); // See that we called CommandStore only once with the right command ID. - verify(commandStore, atMost(1)).setCommandStatusOk(command.getContext() - .getCommandId()); + verify(commandStore, times(1)).setCommandStatusOk(command.getContext() + .getCommandId()); } @Test public void set_command_status_to_error_when_dispatcher_throws() throws Exception { - final CommandDispatcher throwingDispatcher = mock(CommandDispatcher.class); - when(throwingDispatcher.getCommandClasses()).thenReturn(CommandClass.setOf(CreateProject.class)); - final IOException exception = new IOException("Unable to dispatch"); - doThrow(exception).when(throwingDispatcher) - .dispatch(any(Command.class)); - final CommandBus.ProblemLog log = spy(commandBus.getProblemLog()); - - commandBus.register(throwingDispatcher); + final IOException exception = givenThrowingDispatcher(); final Command command = commandFactory.create(createProject(newUuid())); commandBus.post(command); // Verify we updated the status. - verify(commandStore, atMost(1)).updateStatus(eq(command.getContext() - .getCommandId()), eq(exception)); + verify(commandStore, times(1)).updateStatus(eq(command.getContext() + .getCommandId()), eq(exception)); // Verify we logged the error. verify(log, atMost(1)).errorDispatching(eq(exception), eq(command)); } - private static class TestFailure extends FailureThrowable { - private static final long serialVersionUID = 1L; - - private TestFailure() { - super(Failures.UnableToHandle.newBuilder() - .setMessage(TestFailure.class.getName()) - .build()); - } - } - - @SuppressWarnings("serial") - private static class TestThrowable extends Throwable { - } - @Test public void set_command_status_to_failure_when_handler_throws_failure() throws TestFailure, TestThrowable { - final CreateProjectHandler handler = mock(CreateProjectHandler.class); - final FailureThrowable failure = new TestFailure(); - doThrow(failure).when(handler) - .handle(any(CreateProject.class), any(CommandContext.class)); - final CommandBus.ProblemLog log = spy(commandBus.getProblemLog()); - - commandBus.register(handler); + final TestFailure failure = new TestFailure(); + givenThrowingHandler(failure); final Command command = commandFactory.create(createProject(newUuid())); - final CommandId commandId = command.getContext() - .getCommandId(); + final CommandId commandId = command.getContext().getCommandId(); final Message commandMessage = Commands.getMessage(command); commandBus.post(command); // Verify we updated the status. - verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(failure.toMessage())); + verify(commandStore, times(1)).updateStatus(eq(commandId), eq(failure.toMessage())); // Verify we logged the failure. - verify(log, atMost(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId)); + verify(log, times(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId)); } -// TODO:2016-04-07:alexander.litus: check all atMost() + @Test public void set_command_status_to_failure_when_handler_throws_exception() throws TestFailure, TestThrowable { - final CreateProjectHandler handler = mock(CreateProjectHandler.class); final RuntimeException exception = new IllegalStateException("handler throws"); - doThrow(exception).when(handler) - .handle(any(CreateProject.class), any(CommandContext.class)); - final CommandBus.ProblemLog log = spy(commandBus.getProblemLog()); - - commandBus.register(handler); + givenThrowingHandler(exception); final Command command = commandFactory.create(createProject(newUuid())); - final CommandId commandId = command.getContext() - .getCommandId(); + final CommandId commandId = command.getContext().getCommandId(); final Message commandMessage = Commands.getMessage(command); commandBus.post(command); // Verify we updated the status. - - verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(exception)); + verify(commandStore, times(1)).updateStatus(eq(commandId), eq(exception)); // Verify we logged the failure. - verify(log, atMost(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId)); + verify(log, times(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId)); } @Test public void set_command_status_to_failure_when_handler_throws_unknown_Throwable() throws TestFailure, TestThrowable { - final CreateProjectHandler handler = mock(CreateProjectHandler.class); final Throwable throwable = new TestThrowable(); - doThrow(throwable).when(handler) - .handle(any(CreateProject.class), any(CommandContext.class)); - final CommandBus.ProblemLog log = spy(commandBus.getProblemLog()); - - commandBus.register(handler); + givenThrowingHandler(throwable); final Command command = commandFactory.create(createProject(newUuid())); - final CommandId commandId = command.getContext() - .getCommandId(); + final CommandId commandId = command.getContext().getCommandId(); final Message commandMessage = Commands.getMessage(command); commandBus.post(command); // Verify we updated the status. - - verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable))); + verify(commandStore, times(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable))); // Verify we logged the failure. - verify(log, atMost(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId)); + verify(log, times(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId)); + } + + private void givenThrowingHandler(E throwable) throws TestThrowable, TestFailure { + final CreateProjectHandler handler = mock(CreateProjectHandler.class); + doThrow(throwable) + .when(handler) + .handle(any(CreateProject.class), any(CommandContext.class)); + commandBus.register(handler); + } + + private E givenThrowingDispatcher() throws Exception { + final CommandDispatcher throwingDispatcher = mock(CommandDispatcher.class); + when(throwingDispatcher.getCommandClasses()).thenReturn(CommandClass.setOf(CreateProject.class)); + final IOException exception = new IOException("Unable to dispatch"); + doThrow(exception) + .when(throwingDispatcher) + .dispatch(any(Command.class)); + commandBus.register(throwingDispatcher); + @SuppressWarnings("unchecked") + final E throwable = (E) exception; + return throwable; } @Test @@ -481,4 +470,18 @@ public void do_not_schedule_command_if_no_scheduling_options_are_set() { verify(scheduler, times(0)).schedule(cmd); } + + private static class TestFailure extends FailureThrowable { + private static final long serialVersionUID = 1L; + + private TestFailure() { + super(Failures.UnableToHandle.newBuilder() + .setMessage(TestFailure.class.getName()) + .build()); + } + } + + @SuppressWarnings("serial") + private static class TestThrowable extends Throwable { + } } From c4a6e1bb1d8abe0d7a0b5dd66792f8f212c10508 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Thu, 7 Apr 2016 16:20:12 +0300 Subject: [PATCH 05/26] Add more tests. --- .../java/org/spine3/base/CommandsShould.java | 23 +++++++++++++++++-- .../DefaultCommandSchedulerShould.java | 13 +++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/client/src/test/java/org/spine3/base/CommandsShould.java b/client/src/test/java/org/spine3/base/CommandsShould.java index f9d0bb8a3dd..0245f2faa62 100644 --- a/client/src/test/java/org/spine3/base/CommandsShould.java +++ b/client/src/test/java/org/spine3/base/CommandsShould.java @@ -29,6 +29,7 @@ import com.google.protobuf.StringValue; import org.junit.Test; import org.spine3.client.test.TestCommandFactory; +import org.spine3.protobuf.Timestamps; import org.spine3.test.RunTest; import org.spine3.type.TypeName; @@ -127,8 +128,8 @@ public void create_logging_message_for_command_with_type_and_id() { @Test public void when_command_delay_is_set_then_return_true() { final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() - .setDelay(seconds(10)) - .build()); + .setDelay(seconds(10)) + .build()); assertTrue(Commands.isScheduled(cmd)); } @@ -148,4 +149,22 @@ public void when_command_is_not_scheduled_then_return_false() { assertFalse(Commands.isScheduled(cmd)); } + + @Test(expected = IllegalArgumentException.class) + public void when_set_negative_delay_then_throw_exception() { + final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() + .setDelay(seconds(-10)) + .build()); + + Commands.isScheduled(cmd); + } + + @Test(expected = IllegalArgumentException.class) + public void when_set_past_sending_time_then_throw_exception() { + final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() + .setSendingTime(Timestamps.secondsAgo(10)) + .build()); + + Commands.isScheduled(cmd); + } } diff --git a/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java index aa07ebf91f8..28229f2a89e 100644 --- a/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java @@ -31,6 +31,7 @@ import static com.google.protobuf.util.TimeUtil.add; import static com.google.protobuf.util.TimeUtil.getCurrentTime; +import static org.junit.Assert.fail; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.protobuf.Durations.seconds; @@ -92,4 +93,16 @@ private void assertCommandSent(Command cmd, Duration delay) throws InterruptedEx verify(commandBus, times(1)).post(cmd); } + + @Test + public void throw_exception_if_is_shutdown() { + scheduler.shutdown(); + try { + scheduler.schedule(createProject()); + } catch (IllegalStateException expected) { + // is OK as it is shutdown + return; + } + fail("Must throw an exception as it is shutdown."); + } } From 637183f87d7ddcc434d364b97dc80732030265fa Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Thu, 7 Apr 2016 17:47:33 +0300 Subject: [PATCH 06/26] Update protobuf plugin dependency. --- client/build.gradle | 4 ++-- examples/build.gradle | 4 ++-- server/build.gradle | 4 ++-- values/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/build.gradle b/client/build.gradle index 3c9e14c1a9f..7491242a5e7 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -4,7 +4,7 @@ buildscript { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } dependencies { - classpath group: 'org.spine3.tools', name: 'proto-lookup-plugin', version: '1.1', changing: true + classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.2', changing: true } } @@ -13,7 +13,7 @@ dependencies { testCompile project (path: ":testutil", configuration: 'testArtifacts') } -apply plugin: 'org.spine3.tools.protolookup'; +apply plugin: 'org.spine3.tools.protobuf-plugin'; sourceSets { main { diff --git a/examples/build.gradle b/examples/build.gradle index da482493b4d..9758e243473 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath "org.spine3.tools:proto-lookup-plugin:1.1" + classpath "org.spine3.tools:protobuf-plugin:1.2" } } @@ -13,5 +13,5 @@ dependencies { compile project(path: ':server'); } -apply plugin: 'org.spine3.tools.protolookup' +apply plugin: 'org.spine3.tools.protobuf-plugin' diff --git a/server/build.gradle b/server/build.gradle index f47f10080d7..1cf8134453f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -3,7 +3,7 @@ buildscript { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } dependencies { - classpath group: 'org.spine3.tools', name: 'proto-lookup-plugin', version: '1.1', changing: true + classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.2', changing: true } } @@ -14,7 +14,7 @@ dependencies { compile project(path: ':client'); } -apply plugin: 'org.spine3.tools.protolookup'; +apply plugin: 'org.spine3.tools.protobuf-plugin'; sourceSets { main { diff --git a/values/build.gradle b/values/build.gradle index 1083a42de55..205c7bd4370 100644 --- a/values/build.gradle +++ b/values/build.gradle @@ -4,7 +4,7 @@ buildscript { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } dependencies { - classpath group: 'org.spine3.tools', name: 'proto-lookup-plugin', version: '1.1', changing: true + classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.2', changing: true } } @@ -14,7 +14,7 @@ dependencies { compile project(path: ':client'); } -apply plugin: 'org.spine3.tools.protolookup'; +apply plugin: 'org.spine3.tools.protobuf-plugin'; sourceSets { main { From 82894c4629cac8fd10816be82fbfc062bfacecc8 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Fri, 8 Apr 2016 14:08:54 +0300 Subject: [PATCH 07/26] Make CommandScheduler an abstract class; add docs, tests; add a builder to CommandBus. --- .../main/java/org/spine3/base/Commands.java | 21 ++--- .../src/main/proto/spine/base/command.proto | 20 +++-- .../java/org/spine3/base/CommandsShould.java | 39 +++++---- .../spine3/testdata/TestContextFactory.java | 47 ++++++++++- .../aggregate/server/Application.java | 10 ++- .../org/spine3/server/command/CommandBus.java | 81 ++++++++++++++----- .../server/command/CommandScheduler.java | 63 +++++++++++++-- ...ler.java => ExecutorCommandScheduler.java} | 47 ++++------- .../server/BoundedContextBuilderShould.java | 25 +++--- .../spine3/server/BoundedContextShould.java | 6 -- .../server/BoundedContextTestStubs.java | 23 +++--- .../server/command/CommandBusShould.java | 51 +++++++++--- ...va => ExecutorCommandSchedulerShould.java} | 51 +++++++----- .../org/spine3/testdata/TestCommands.java | 22 ++++- 14 files changed, 344 insertions(+), 162 deletions(-) rename {server => client}/src/test/java/org/spine3/testdata/TestContextFactory.java (73%) rename server/src/main/java/org/spine3/server/command/{DefaultCommandScheduler.java => ExecutorCommandScheduler.java} (64%) rename server/src/test/java/org/spine3/server/command/{DefaultCommandSchedulerShould.java => ExecutorCommandSchedulerShould.java} (64%) diff --git a/client/src/main/java/org/spine3/base/Commands.java b/client/src/main/java/org/spine3/base/Commands.java index 549b9943dda..ca78ec9f9d4 100644 --- a/client/src/main/java/org/spine3/base/Commands.java +++ b/client/src/main/java/org/spine3/base/Commands.java @@ -221,28 +221,31 @@ public static boolean isEntityFile(FileDescriptor file) { } /** - * Checks if the command is scheduled to be sent later. + * Checks if the command is scheduled to be delivered later. * * @param command a command to check - * @return {@code true} if the command context has valid delay or sending time set, {@code false} otherwise + * @return {@code true} if the command context has scheduling options set, {@code false} otherwise */ public static boolean isScheduled(Command command) { - final CommandContext context = command.getContext(); - final Duration delay = context.getDelay(); + final Schedule schedule = command.getContext().getSchedule(); + if (schedule.getInTime()) { + return false; + } + final Duration delay = schedule.getDelay(); if (isNotDefault(delay)) { checkArgument(delay.getSeconds() > 0, "Command delay seconds must be a positive value."); return true; } - final Timestamp sendingTime = context.getSendingTime(); - if (isNotDefault(sendingTime)) { - checkIsAfterNow(sendingTime); + final Timestamp deliveryTime = schedule.getDeliveryTime(); + if (isNotDefault(deliveryTime)) { + checkIsAfterNow(deliveryTime); return true; } return false; } - private static void checkIsAfterNow(Timestamp sendingTime) { + private static void checkIsAfterNow(Timestamp deliveryTime) { final Timestamp now = getCurrentTime(); - checkArgument(isAfter(sendingTime, /*than*/ now), "Sending time must be after the current time."); + checkArgument(isAfter(deliveryTime, /*than*/ now), "Delivery time must be after the current time."); } } diff --git a/client/src/main/proto/spine/base/command.proto b/client/src/main/proto/spine/base/command.proto index 46b3565d9ca..45bb5757a55 100644 --- a/client/src/main/proto/spine/base/command.proto +++ b/client/src/main/proto/spine/base/command.proto @@ -79,13 +79,23 @@ message CommandContext { // The `namespace` attribute must be defined for commands in multitenant applications. Namespace namespace = 5; - oneof time_opts { - // The delay to wait before sending the command. - google.protobuf.Duration delay = 6; + // The command scheduling options. + Schedule schedule = 6; +} + +message Schedule { + oneof options { + // The amount of time between the current moment and a command delivering to the target. + google.protobuf.Duration delay = 1; - // The time when the command should be sent. - google.protobuf.Timestamp sending_time = 7; + // The time when the command should be delivered to the target. + google.protobuf.Timestamp delivery_time = 2; } + + // `true` if the command should be delivered to the target right now, despite the scheduling options set; + // `false` otherwise. + // This flag is needed to specify that a scheduled command arrived in time, and to retain the scheduling options. + bool in_time = 3; } // Enumeration of possible failures when validating a command. diff --git a/client/src/test/java/org/spine3/base/CommandsShould.java b/client/src/test/java/org/spine3/base/CommandsShould.java index 909dc544cda..55e6672cc64 100644 --- a/client/src/test/java/org/spine3/base/CommandsShould.java +++ b/client/src/test/java/org/spine3/base/CommandsShould.java @@ -27,9 +27,9 @@ import com.google.protobuf.Int64Value; import com.google.protobuf.Message; import com.google.protobuf.StringValue; +import com.google.protobuf.Timestamp; import org.junit.Test; import org.spine3.client.test.TestCommandFactory; -import org.spine3.protobuf.Timestamps; import org.spine3.test.RunTest; import org.spine3.test.TestCommand; import org.spine3.type.TypeName; @@ -38,15 +38,16 @@ import java.util.Collection; import java.util.List; +import static com.google.protobuf.Descriptors.FileDescriptor; import static com.google.protobuf.util.TimeUtil.add; import static com.google.protobuf.util.TimeUtil.getCurrentTime; -import static com.google.protobuf.Descriptors.FileDescriptor; import static org.junit.Assert.*; import static org.spine3.protobuf.Durations.seconds; import static org.spine3.protobuf.Timestamps.minutesAgo; import static org.spine3.protobuf.Timestamps.secondsAgo; import static org.spine3.protobuf.Values.newStringValue; import static org.spine3.test.Tests.hasPrivateUtilityConstructor; +import static org.spine3.testdata.TestContextFactory.createCommandContext; @SuppressWarnings({"InstanceMethodNamingConvention", "MagicNumber"}) public class CommandsShould { @@ -152,44 +153,48 @@ public void return_false_if_file_does_not_belong_to_entity() { } @Test - public void when_command_delay_is_set_then_return_true() { - final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() - .setDelay(seconds(10)) - .build()); + public void when_command_delay_is_set_then_consider_it_scheduled() { + final CommandContext context = createCommandContext(/*delay=*/seconds(10)); + final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); assertTrue(Commands.isScheduled(cmd)); } @Test - public void when_command_sending_time_is_set_then_return_true() { - final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() - .setSendingTime(add(getCurrentTime(), seconds(10))) - .build()); + public void when_command_sending_time_is_set_then_consider_it_scheduled() { + final Timestamp deliveryTime = add(getCurrentTime(), seconds(10)); + final Command cmd = Commands.create(StringValue.getDefaultInstance(), createCommandContext(deliveryTime)); assertTrue(Commands.isScheduled(cmd)); } @Test - public void when_command_is_not_scheduled_then_return_false() { + public void when_no_scheduling_options_then_consider_command_not_scheduled() { final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.getDefaultInstance()); assertFalse(Commands.isScheduled(cmd)); } + @Test + public void when_command_is_in_time_then_consider_it_not_scheduled() { + final CommandContext context = createCommandContext(/*delay=*/seconds(10), /*in time=*/true); + final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); + + assertFalse(Commands.isScheduled(cmd)); + } + @Test(expected = IllegalArgumentException.class) public void when_set_negative_delay_then_throw_exception() { - final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() - .setDelay(seconds(-10)) - .build()); + final CommandContext context = createCommandContext(/*delay=*/seconds(-10)); + final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); Commands.isScheduled(cmd); } @Test(expected = IllegalArgumentException.class) public void when_set_past_sending_time_then_throw_exception() { - final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.newBuilder() - .setSendingTime(Timestamps.secondsAgo(10)) - .build()); + final CommandContext context = createCommandContext(/*delivery time=*/secondsAgo(10)); + final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); Commands.isScheduled(cmd); } diff --git a/server/src/test/java/org/spine3/testdata/TestContextFactory.java b/client/src/test/java/org/spine3/testdata/TestContextFactory.java similarity index 73% rename from server/src/test/java/org/spine3/testdata/TestContextFactory.java rename to client/src/test/java/org/spine3/testdata/TestContextFactory.java index e16bf6b3ccc..bc48315f2af 100644 --- a/server/src/test/java/org/spine3/testdata/TestContextFactory.java +++ b/client/src/test/java/org/spine3/testdata/TestContextFactory.java @@ -21,6 +21,7 @@ package org.spine3.testdata; import com.google.protobuf.Any; +import com.google.protobuf.Duration; import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import org.spine3.base.CommandContext; @@ -29,6 +30,7 @@ import org.spine3.base.EventContext; import org.spine3.base.EventId; import org.spine3.base.Events; +import org.spine3.base.Schedule; import org.spine3.base.UserId; import org.spine3.time.ZoneOffset; @@ -36,7 +38,7 @@ import static org.spine3.base.Identifiers.newUuid; import static org.spine3.client.UserUtil.newUserId; import static org.spine3.protobuf.Messages.toAny; -import static org.spine3.testdata.TestAggregateIdFactory.newProjectId; +import static org.spine3.protobuf.Values.newStringValue; /** @@ -47,7 +49,7 @@ @SuppressWarnings({"UtilityClass", "OverloadedMethodsWithSameNumberOfParameters"}) public class TestContextFactory { - private static final Any AGGREGATE_ID = toAny(newProjectId()); + private static final Any AGGREGATE_ID = toAny(newStringValue(newUuid())); private TestContextFactory() { } @@ -74,6 +76,47 @@ public static CommandContext createCommandContext(UserId userId, CommandId comma return builder.build(); } + /** + * Creates a new context with the given delay before the delivery time. + */ + public static CommandContext createCommandContext(Duration delay) { + final Schedule schedule = Schedule.newBuilder() + .setDelay(delay) + .build(); + return createCommandContext(schedule); + } + + /** + * Creates a new context with the given scheduling options. + */ + public static CommandContext createCommandContext(Duration delay, boolean isInTime) { + final Schedule schedule = Schedule.newBuilder() + .setDelay(delay) + .setInTime(isInTime) + .build(); + return createCommandContext(schedule); + } + + /** + * Creates a new context with the given delivery time. + */ + public static CommandContext createCommandContext(Timestamp deliveryTime) { + final Schedule schedule = Schedule.newBuilder() + .setDeliveryTime(deliveryTime) + .build(); + return createCommandContext(schedule); + } + + public static CommandContext createCommandContext(Schedule schedule) { + final CommandContext.Builder builder = TestContextFactory.createCommandContext().toBuilder() + .setSchedule(schedule); + return builder.build(); + } + + /* + * Event context factory methods. + */ + /** * Creates a new {@link EventContext} with default properties. */ diff --git a/examples/src/main/java/org/spine3/examples/aggregate/server/Application.java b/examples/src/main/java/org/spine3/examples/aggregate/server/Application.java index 4da11e27229..dc54f547380 100644 --- a/examples/src/main/java/org/spine3/examples/aggregate/server/Application.java +++ b/examples/src/main/java/org/spine3/examples/aggregate/server/Application.java @@ -28,6 +28,7 @@ import org.spine3.examples.aggregate.Client; import org.spine3.examples.aggregate.OrderId; import org.spine3.server.BoundedContext; +import org.spine3.server.EventHandler; import org.spine3.server.command.CommandBus; import org.spine3.server.command.CommandStore; import org.spine3.server.event.EventBus; @@ -53,7 +54,7 @@ public class Application implements AutoCloseable { private final StorageFactory storageFactory; private final BoundedContext boundedContext; - private final EventLogger eventLogger = new EventLogger(); + private final EventHandler eventLogger = new EventLogger(); /** * Creates a new sample with the specified storage factory. @@ -70,7 +71,11 @@ public Application(StorageFactory storageFactory) { } private static CommandBus createCommandBus() { - return CommandBus.create(new CommandStore(InMemoryStorageFactory.getInstance().createCommandStorage())); + final CommandStore store = new CommandStore(InMemoryStorageFactory.getInstance().createCommandStorage()); + final CommandBus commandBus = CommandBus.newBuilder() + .setCommandStore(store) + .build(); + return commandBus; } private static EventBus createEventBus(StorageFactory storageFactory) { @@ -79,7 +84,6 @@ private static EventBus createEventBus(StorageFactory storageFactory) { .setStorage(storageFactory.createEventStorage()) .setLogger(EventStore.log()) .build(); - return EventBus.newInstance(eventStore); } diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index 9b5b45bb612..b980f86c67d 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -39,7 +39,7 @@ import org.spine3.type.CommandClass; import org.spine3.validation.options.ConstraintViolation; -import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.util.List; @@ -55,6 +55,7 @@ * * @author Alexander Yevsyukov * @author Mikhail Melnik + * @author Alexander Litus */ public class CommandBus implements AutoCloseable { @@ -63,20 +64,23 @@ public class CommandBus implements AutoCloseable { private final CommandStore commandStore; + @Nullable + private final CommandScheduler scheduler; + private final CommandStatusHelper commandStatus; private ProblemLog problemLog = new ProblemLog(); - private CommandScheduler scheduler; - @CheckReturnValue - public static CommandBus create(CommandStore store) { - final CommandBus commandBus = new CommandBus(checkNotNull(store)); - commandBus.setScheduler(new DefaultCommandScheduler(commandBus)); - return commandBus; + private CommandBus(Builder builder) { + this.commandStore = builder.getCommandStore(); + this.scheduler = builder.getScheduler(); + this.commandStatus = new CommandStatusHelper(commandStore); } - protected CommandBus(CommandStore commandStore) { - this.commandStore = commandStore; - this.commandStatus = new CommandStatusHelper(commandStore); + /** + * Creates a new builder for command bus. + */ + public static Builder newBuilder() { + return new Builder(); } /** @@ -122,13 +126,6 @@ public void unregister(CommandHandler handler) { handlerRegistry.unregister(handler); } - /** - * Sets a command scheduler. - */ - public void setScheduler(CommandScheduler scheduler) { - this.scheduler = scheduler; - } - /** * Verifies if the command can be posted to this {@code CommandBus}. * @@ -173,11 +170,11 @@ private boolean isUnsupportedCommand(CommandClass commandClass) { public List post(Command command) { //TODO:2016-01-24:alexander.yevsyukov: Do not return value. checkNotDefault(command); - store(command); if (isScheduled(command)) { - scheduler.schedule(command); + schedule(command); return emptyList(); } + store(command); final CommandClass commandClass = CommandClass.of(command); if (isDispatcherRegistered(commandClass)) { return dispatch(command); @@ -230,6 +227,15 @@ private List invokeHandler(Message msg, CommandContext context) { return result; } + private void schedule(Command command) { + if (scheduler != null) { + scheduler.schedule(command); + } else { + throw new IllegalStateException( + "Scheduled commands are not supported by this command bus (scheduler is not set)."); + } + } + /** * Convenience wrapper for logging errors and warnings. */ @@ -318,7 +324,9 @@ public void close() throws Exception { dispatcherRegistry.unregisterAll(); handlerRegistry.unregisterAll(); commandStore.close(); - scheduler.shutdown(); + if (scheduler != null) { + scheduler.shutdown(); + } } private enum LogSingleton { @@ -334,4 +342,37 @@ private enum LogSingleton { /* package */ static Logger log() { return LogSingleton.INSTANCE.value; } + + /** + * Constructs a command bus. + */ + public static class Builder { + + private CommandStore commandStore; + private CommandScheduler scheduler; + + public CommandBus build() { + checkNotNull(commandStore, "Command store must be set."); + final CommandBus commandBus = new CommandBus(this); + return commandBus; + } + + public Builder setCommandStore(CommandStore commandStore) { + this.commandStore = commandStore; + return this; + } + + public CommandStore getCommandStore() { + return commandStore; + } + + public Builder setScheduler(CommandScheduler scheduler) { + this.scheduler = scheduler; + return this; + } + + public CommandScheduler getScheduler() { + return scheduler; + } + } } diff --git a/server/src/main/java/org/spine3/server/command/CommandScheduler.java b/server/src/main/java/org/spine3/server/command/CommandScheduler.java index 3003ef96089..2dd74163fa6 100644 --- a/server/src/main/java/org/spine3/server/command/CommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/CommandScheduler.java @@ -20,28 +20,77 @@ package org.spine3.server.command; +import com.google.common.annotations.VisibleForTesting; import org.spine3.base.Command; +import org.spine3.base.CommandContext; +import org.spine3.base.Schedule; + +import static com.google.common.base.Preconditions.checkState; /** - * Schedules commands sending them after the given delay (or at the given time). + * Schedules commands delivering them to the target according to the scheduling options. * * @author Alexander Litus */ -public interface CommandScheduler { +public abstract class CommandScheduler { + + private final CommandBus commandBus; + + private boolean isActive = true; + + /** + * Creates a new instance. + * + * @param commandBus a command bus used to send scheduled commands + */ + protected CommandScheduler(CommandBus commandBus) { + this.commandBus = commandBus; + } /** - * Schedule a command and send it after the given delay (or at the given time). + * Schedule a command and deliver it to the target according to the scheduling options. * - * @param command a command to send later + * @param command a command to deliver later * @throws IllegalStateException if the scheduler is shut down + * @see #post(Command) */ - void schedule(Command command); + public void schedule(Command command) { + checkState(isActive, "Scheduler is shut down."); + } /** - * Initiates an orderly shutdown in which previously scheduled commands will be sent later, + * Initiates an orderly shutdown in which previously scheduled commands will be delivered later, * but no new commands will be accepted. * *

Invocation has no effect if the scheduler is already shut down. */ - void shutdown(); + public void shutdown() { + isActive = false; + } + + /** + * Delivers a scheduled command to a target. + * + * @param command a command to deliver + */ + protected void post(Command command) { + final Command commandUpdated = setIsInTime(command); + commandBus.post(commandUpdated); + } + + @VisibleForTesting + /* package */ static Command setIsInTime(Command command) { + final CommandContext contextPrimary = command.getContext(); + final Schedule scheduleUpdated = contextPrimary.getSchedule() + .toBuilder() + .setInTime(true) + .build(); + final CommandContext contextUpdated = contextPrimary.toBuilder() + .setSchedule(scheduleUpdated) + .build(); + final Command cmdUpdated = command.toBuilder() + .setContext(contextUpdated) + .build(); + return cmdUpdated; + } } diff --git a/server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java similarity index 64% rename from server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java rename to server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java index 6e227c95e71..a38ae0768cf 100644 --- a/server/src/main/java/org/spine3/server/command/DefaultCommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java @@ -23,81 +23,68 @@ import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import org.spine3.base.Command; -import org.spine3.base.CommandContext; +import org.spine3.base.Schedule; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import static com.google.common.base.Preconditions.checkState; import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static java.util.concurrent.TimeUnit.SECONDS; import static org.spine3.validate.Validate.isNotDefault; /** - * The default command scheduler implementation. + * The command scheduler implementation which uses basic Java task scheduling features. * + *

NOTE: please use another implementation + * in applications running under the Google App Engine. + * + * @see ScheduledExecutorService * @author Alexander Litus */ -public class DefaultCommandScheduler implements CommandScheduler { +public class ExecutorCommandScheduler extends CommandScheduler { private static final int MIN_THEAD_POOL_SIZE = 5; private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(MIN_THEAD_POOL_SIZE); - private boolean isShutdown = false; - - private final CommandBus commandBus; - /** * Creates a new instance. * * @param commandBus a command bus used to send scheduled commands */ - public DefaultCommandScheduler(CommandBus commandBus) { - this.commandBus = commandBus; + public ExecutorCommandScheduler(CommandBus commandBus) { + super(commandBus); } @Override - public void schedule(Command command) { - checkState(!isShutdown, "Scheduler is shut down."); + public void schedule(final Command command) { + super.schedule(command); final long delaySec = getDelaySeconds(command); - final Command commandUpdated = removeSchedulingOptions(command); executorService.schedule(new Runnable() { @Override public void run() { - commandBus.post(commandUpdated); + post(command); } }, delaySec, SECONDS); } private static long getDelaySeconds(Command command) { - final CommandContext context = command.getContext(); + final Schedule schedule = command.getContext().getSchedule(); final long delaySec; - final Duration delay = context.getDelay(); + final Duration delay = schedule.getDelay(); if (isNotDefault(delay)) { delaySec = delay.getSeconds(); } else { - final Timestamp sendingTime = context.getSendingTime(); + final Timestamp deliveryTime = schedule.getDeliveryTime(); final Timestamp now = getCurrentTime(); - delaySec = sendingTime.getSeconds() - now.getSeconds(); + delaySec = deliveryTime.getSeconds() - now.getSeconds(); } return delaySec; } - private static Command removeSchedulingOptions(Command command) { - final CommandContext contextUpdated = command.getContext().toBuilder() - .clearDelay() - .clearSendingTime() - .build(); - final Command result = command.toBuilder() - .setContext(contextUpdated) - .build(); - return result; - } - @Override public void shutdown() { + super.shutdown(); executorService.shutdown(); - isShutdown = true; } } diff --git a/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java b/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java index 8ca78e80a7e..173412cbd5e 100644 --- a/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java +++ b/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java @@ -25,13 +25,13 @@ import org.junit.Before; import org.junit.Test; import org.spine3.server.command.CommandBus; -import org.spine3.server.command.CommandStore; import org.spine3.server.event.EventBus; import org.spine3.server.event.EventStore; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; import static org.junit.Assert.*; +import static org.spine3.testdata.TestCommands.newCommandBus; @SuppressWarnings("InstanceMethodNamingConvention") public class BoundedContextBuilderShould { @@ -48,17 +48,6 @@ public void tearDown() throws Exception { storageFactory.close(); } - private static CommandBus newCommandDispatcher(StorageFactory storageFactory) { - return CommandBus.create(new CommandStore(storageFactory.createCommandStorage())); - } - - private static EventBus newEventBus(StorageFactory storageFactory) { - return EventBus.newInstance(EventStore.newBuilder() - .setStreamExecutor(MoreExecutors.directExecutor()) - .setStorage(storageFactory.createEventStorage()) - .build()); - } - @Test(expected = NullPointerException.class) public void do_not_accept_null_StorageFactory() { //noinspection ConstantConditions @@ -80,7 +69,7 @@ public void do_not_accept_null_CommandDispatcher() { @Test public void return_CommandDispatcher_from_builder() { - final CommandBus expected = newCommandDispatcher(storageFactory); + final CommandBus expected = newCommandBus(storageFactory); final BoundedContext.Builder builder = BoundedContext.newBuilder().setCommandBus(expected); assertEquals(expected, builder.getCommandBus()); } @@ -101,7 +90,8 @@ public void return_if_multitenant_from_builder() { @Test public void be_not_multitenant_by_default() { - assertFalse(BoundedContext.newBuilder().isMultitenant()); + assertFalse(BoundedContext.newBuilder() + .isMultitenant()); } @Test(expected = NullPointerException.class) @@ -109,4 +99,11 @@ public void do_not_accept_null_EventBus() { //noinspection ConstantConditions BoundedContext.newBuilder().setEventBus(null); } + + private static EventBus newEventBus(StorageFactory storageFactory) { + return EventBus.newInstance(EventStore.newBuilder() + .setStreamExecutor(MoreExecutors.directExecutor()) + .setStorage(storageFactory.createEventStorage()) + .build()); + } } diff --git a/server/src/test/java/org/spine3/server/BoundedContextShould.java b/server/src/test/java/org/spine3/server/BoundedContextShould.java index 1c2f382d7df..390458b1fea 100644 --- a/server/src/test/java/org/spine3/server/BoundedContextShould.java +++ b/server/src/test/java/org/spine3/server/BoundedContextShould.java @@ -41,8 +41,6 @@ import org.spine3.server.aggregate.Aggregate; import org.spine3.server.aggregate.AggregateRepository; import org.spine3.server.aggregate.Apply; -import org.spine3.server.command.CommandBus; -import org.spine3.server.command.CommandStore; import org.spine3.server.entity.IdFunction; import org.spine3.server.error.UnsupportedCommandException; import org.spine3.server.event.EventBus; @@ -108,10 +106,6 @@ private static EventBus newEventBus(StorageFactory storageFactory) { .build()); } - private static CommandBus newCommandBus(StorageFactory storageFactory) { - return CommandBus.create(new CommandStore(storageFactory.createCommandStorage())); - } - @After public void tearDown() throws Exception { if (handlersRegistered) { diff --git a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java index 28a7b9971cc..37a4ed47157 100644 --- a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java +++ b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java @@ -22,12 +22,13 @@ import com.google.common.util.concurrent.MoreExecutors; import org.spine3.server.command.CommandBus; -import org.spine3.server.command.CommandStore; import org.spine3.server.event.EventBus; import org.spine3.server.event.EventStore; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import static org.spine3.testdata.TestCommands.newCommandBus; + /** * Creates stubs with instances of {@link BoundedContext} for testing purposes. * @@ -41,21 +42,17 @@ public static BoundedContext create() { } public static BoundedContext create(StorageFactory storageFactory) { - final CommandBus commandBus = CommandBus.create( - new CommandStore(storageFactory.createCommandStorage())); - + final CommandBus commandBus = newCommandBus(storageFactory); final EventBus eventBus = EventBus.newInstance(EventStore.newBuilder() - .setStreamExecutor(MoreExecutors.directExecutor()) - .setStorage(storageFactory.createEventStorage()) - .build()); - - return BoundedContext.newBuilder() + .setStreamExecutor(MoreExecutors.directExecutor()) + .setStorage(storageFactory.createEventStorage()) + .build()); + final BoundedContext.Builder builder = BoundedContext.newBuilder() .setStorageFactory(storageFactory) .setCommandBus(commandBus) - .setEventBus(eventBus) - .build(); + .setEventBus(eventBus); + return builder.build(); } - private BoundedContextTestStubs() { - } + private BoundedContextTestStubs() {} } diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index 3592d8b32b2..83e936a9299 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -20,6 +20,7 @@ package org.spine3.server.command; +import com.google.protobuf.Duration; import com.google.protobuf.Message; import org.junit.Before; import org.junit.Test; @@ -57,6 +58,7 @@ import static org.spine3.protobuf.Durations.minutes; import static org.spine3.server.command.CommandValidation.isUnsupportedCommand; import static org.spine3.testdata.TestCommands.*; +import static org.spine3.testdata.TestContextFactory.createCommandContext; @SuppressWarnings({"InstanceMethodNamingConvention", "ClassWithTooManyMethods"}) public class CommandBusShould { @@ -64,15 +66,17 @@ public class CommandBusShould { private CommandBus commandBus; private CommandStore commandStore; private CommandFactory commandFactory; - private DefaultCommandScheduler scheduler; + private ExecutorCommandScheduler scheduler; private CommandBus.ProblemLog log; @Before public void setUp() { commandStore = mock(CommandStore.class); - commandBus = CommandBus.create(commandStore); - scheduler = mock(DefaultCommandScheduler.class); - commandBus.setScheduler(scheduler); + scheduler = mock(ExecutorCommandScheduler.class); + commandBus = CommandBus.newBuilder() + .setCommandStore(commandStore) + .setScheduler(scheduler) + .build(); log = mock(CommandBus.ProblemLog.class); commandBus.setProblemLog(log); commandFactory = TestCommandFactory.newInstance(CommandBusShould.class); @@ -81,7 +85,9 @@ public void setUp() { @Test(expected = NullPointerException.class) public void do_not_accept_null_CommandStore_on_construction() { //noinspection ConstantConditions,ResultOfMethodCallIgnored - CommandBus.create(null); + CommandBus.newBuilder() + .setCommandStore(null) + .build(); } // @@ -441,9 +447,7 @@ private E givenThrowingDispatcher() throws Exception { @Test public void schedule_command_if_delay_is_set() { - final Command cmd = Commands.create(createProject(newUuid()), CommandContext.newBuilder() - .setDelay(minutes(1)) - .build()); + final Command cmd = newCommand(/*delay=*/minutes(1)); commandBus.post(cmd); @@ -452,9 +456,8 @@ public void schedule_command_if_delay_is_set() { @Test public void schedule_command_if_sending_time_is_set() { - final Command cmd = Commands.create(createProject(newUuid()), CommandContext.newBuilder() - .setSendingTime(add(getCurrentTime(), minutes(1))) - .build()); + final CommandContext context = createCommandContext(/*delivery time=*/add(getCurrentTime(), minutes(1))); + final Command cmd = Commands.create(createProject(newUuid()), context); commandBus.post(cmd); @@ -468,7 +471,31 @@ public void do_not_schedule_command_if_no_scheduling_options_are_set() { commandBus.post(cmd); - verify(scheduler, times(0)).schedule(cmd); + verify(scheduler, never()).schedule(cmd); + } + + @Test + public void do_not_store_command_if_command_is_scheduled() { + final Command cmd = newCommand(/*delay=*/minutes(1)); + + commandBus.post(cmd); + + verify(commandStore, never()).store(cmd); + } + + @Test(expected = IllegalStateException.class) + public void throw_exception_if_post_scheduled_cmd_and_no_scheduler_is_set() { + final CommandBus commandBus = CommandBus.newBuilder() + .setCommandStore(commandStore) + .build(); + final Command cmd = newCommand(/*delay=*/minutes(1)); + + commandBus.post(cmd); + } + + private static Command newCommand(Duration delay) { + final CommandContext context = createCommandContext(delay); + return Commands.create(createProject(newUuid()), context); } private static class TestFailure extends FailureThrowable { diff --git a/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java similarity index 64% rename from server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java rename to server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index 28229f2a89e..e0caa79b580 100644 --- a/server/src/test/java/org/spine3/server/command/DefaultCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -27,21 +27,23 @@ import org.spine3.base.Command; import org.spine3.base.CommandContext; import org.spine3.base.Commands; -import org.spine3.test.project.command.CreateProject; import static com.google.protobuf.util.TimeUtil.add; import static com.google.protobuf.util.TimeUtil.getCurrentTime; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.protobuf.Durations.seconds; import static org.spine3.testdata.TestCommands.createProject; +import static org.spine3.testdata.TestContextFactory.createCommandContext; /** * @author Alexander Litus */ @SuppressWarnings("InstanceMethodNamingConvention") -public class DefaultCommandSchedulerShould { +public class ExecutorCommandSchedulerShould { private static final Duration DELAY = seconds(1); private static final int CHECK_OFFSET_MS = 50; @@ -52,7 +54,7 @@ public class DefaultCommandSchedulerShould { @Before public void setUpTest() { this.commandBus = mock(CommandBus.class); - this.scheduler = new DefaultCommandScheduler(commandBus); + this.scheduler = new ExecutorCommandScheduler(commandBus); } @After @@ -62,36 +64,45 @@ public void tearDownTest() { @Test public void schedule_command_if_delay_is_set() throws InterruptedException { - final CreateProject msg = createProject(newUuid()); - final Command cmdPrimary = Commands.create(msg, CommandContext.newBuilder() - .setDelay(DELAY) - .build()); - final Command cmdExpected = Commands.create(msg, CommandContext.getDefaultInstance()); + final CommandContext context = createCommandContext(DELAY); + final Command cmd = Commands.create(createProject(newUuid()), context); - scheduler.schedule(cmdPrimary); + scheduler.schedule(cmd); - assertCommandSent(cmdExpected, DELAY); + assertCommandSent(cmd, DELAY); } @Test public void schedule_command_if_sending_time_is_set() throws InterruptedException { - final CreateProject msg = createProject(newUuid()); - final Command cmdPrimary = Commands.create(msg, CommandContext.newBuilder() - .setSendingTime(add(getCurrentTime(), DELAY)) - .build()); - final Command cmdExpected = Commands.create(msg, CommandContext.getDefaultInstance()); + final CommandContext context = createCommandContext(/*delivery time=*/add(getCurrentTime(), DELAY)); + final Command cmd = Commands.create(createProject(newUuid()), context); - scheduler.schedule(cmdPrimary); + scheduler.schedule(cmd); - assertCommandSent(cmdExpected, DELAY); + assertCommandSent(cmd, DELAY); + } + + @Test + public void set_command_is_in_time() { + final Command cmd = createProject(); + assertFalse(cmd.getContext() + .getSchedule() + .getInTime()); + + final Command updatedCmd = CommandScheduler.setIsInTime(cmd); + assertTrue(updatedCmd.getContext() + .getSchedule() + .getInTime()); } private void assertCommandSent(Command cmd, Duration delay) throws InterruptedException { - verify(commandBus, never()).post(any(Command.class)); + verify(commandBus, times(0)).post(any(Command.class)); - Thread.sleep((delay.getSeconds() * 1000) + CHECK_OFFSET_MS); + final long delayMs = delay.getSeconds() * 1000; + Thread.sleep(delayMs + CHECK_OFFSET_MS); - verify(commandBus, times(1)).post(cmd); + final Command updatedCmd = CommandScheduler.setIsInTime(cmd); + verify(commandBus, times(1)).post(updatedCmd); } @Test diff --git a/server/src/test/java/org/spine3/testdata/TestCommands.java b/server/src/test/java/org/spine3/testdata/TestCommands.java index 3380aa76067..dcfaba22832 100644 --- a/server/src/test/java/org/spine3/testdata/TestCommands.java +++ b/server/src/test/java/org/spine3/testdata/TestCommands.java @@ -28,6 +28,9 @@ import org.spine3.base.CommandId; import org.spine3.base.Commands; import org.spine3.base.UserId; +import org.spine3.server.command.CommandBus; +import org.spine3.server.command.CommandStore; +import org.spine3.server.storage.StorageFactory; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; import org.spine3.test.project.command.CreateProject; @@ -112,8 +115,8 @@ public static CreateProject createProject(String projectId) { return CreateProject.newBuilder() .setProjectId( ProjectId.newBuilder() - .setId(projectId) - .build()) + .setId(projectId) + .build()) .build(); } @@ -148,8 +151,19 @@ public static StartProject startProject(ProjectId id) { public static StartProject startProject(String projectId) { return StartProject.newBuilder() .setProjectId(ProjectId.newBuilder() - .setId(projectId) - .build()) + .setId(projectId) + .build()) + .build(); + } + + /** + * Creates a new command bus. + */ + public static CommandBus newCommandBus(StorageFactory storageFactory) { + final CommandStore store = new CommandStore(storageFactory.createCommandStorage()); + final CommandBus commandBus = CommandBus.newBuilder() + .setCommandStore(store) .build(); + return commandBus; } } From 35984074ff449ab97c01cce1a6f6d0ed9ba23d99 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 11:51:21 +0300 Subject: [PATCH 08/26] Remove `delivery_time` field in command Schedule options, rename other fields. --- .../main/java/org/spine3/base/Commands.java | 17 +++--------- .../src/main/proto/spine/base/command.proto | 15 ++++------- .../java/org/spine3/base/CommandsShould.java | 19 -------------- .../spine3/testdata/TestContextFactory.java | 18 +++---------- .../server/command/CommandScheduler.java | 12 ++++++--- .../command/ExecutorCommandScheduler.java | 14 +--------- .../server/command/CommandBusShould.java | 10 ------- .../ExecutorCommandSchedulerShould.java | 26 +++++-------------- 8 files changed, 28 insertions(+), 103 deletions(-) diff --git a/client/src/main/java/org/spine3/base/Commands.java b/client/src/main/java/org/spine3/base/Commands.java index ca78ec9f9d4..fac0493944b 100644 --- a/client/src/main/java/org/spine3/base/Commands.java +++ b/client/src/main/java/org/spine3/base/Commands.java @@ -21,8 +21,8 @@ package org.spine3.base; import com.google.common.base.Predicate; -import com.google.protobuf.Duration; import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Duration; import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import org.spine3.protobuf.EntityPackagesMap; @@ -40,7 +40,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.protobuf.util.TimeUtil.getCurrentTime; -import static org.spine3.protobuf.Timestamps.isAfter; import static org.spine3.validate.Validate.isNotDefault; /** @@ -228,24 +227,14 @@ public static boolean isEntityFile(FileDescriptor file) { */ public static boolean isScheduled(Command command) { final Schedule schedule = command.getContext().getSchedule(); - if (schedule.getInTime()) { + if (schedule.getIgnoreDelay()) { return false; } - final Duration delay = schedule.getDelay(); + final Duration delay = schedule.getAfter(); if (isNotDefault(delay)) { checkArgument(delay.getSeconds() > 0, "Command delay seconds must be a positive value."); return true; } - final Timestamp deliveryTime = schedule.getDeliveryTime(); - if (isNotDefault(deliveryTime)) { - checkIsAfterNow(deliveryTime); - return true; - } return false; } - - private static void checkIsAfterNow(Timestamp deliveryTime) { - final Timestamp now = getCurrentTime(); - checkArgument(isAfter(deliveryTime, /*than*/ now), "Delivery time must be after the current time."); - } } diff --git a/client/src/main/proto/spine/base/command.proto b/client/src/main/proto/spine/base/command.proto index 45bb5757a55..3c785dcb9a0 100644 --- a/client/src/main/proto/spine/base/command.proto +++ b/client/src/main/proto/spine/base/command.proto @@ -84,18 +84,13 @@ message CommandContext { } message Schedule { - oneof options { - // The amount of time between the current moment and a command delivering to the target. - google.protobuf.Duration delay = 1; + // The delay between the moment of receiving a command at the server and its delivery to the target. + google.protobuf.Duration after = 1; - // The time when the command should be delivered to the target. - google.protobuf.Timestamp delivery_time = 2; - } - - // `true` if the command should be delivered to the target right now, despite the scheduling options set; + // `true` if the command should be delivered to the target right now, despite the delay set; // `false` otherwise. - // This flag is needed to specify that a scheduled command arrived in time, and to retain the scheduling options. - bool in_time = 3; + // This flag is needed to specify that the delay is passed, and to retain the info about the delay. + bool ignore_delay = 2; } // Enumeration of possible failures when validating a command. diff --git a/client/src/test/java/org/spine3/base/CommandsShould.java b/client/src/test/java/org/spine3/base/CommandsShould.java index 55e6672cc64..87e9b097875 100644 --- a/client/src/test/java/org/spine3/base/CommandsShould.java +++ b/client/src/test/java/org/spine3/base/CommandsShould.java @@ -27,7 +27,6 @@ import com.google.protobuf.Int64Value; import com.google.protobuf.Message; import com.google.protobuf.StringValue; -import com.google.protobuf.Timestamp; import org.junit.Test; import org.spine3.client.test.TestCommandFactory; import org.spine3.test.RunTest; @@ -39,8 +38,6 @@ import java.util.List; import static com.google.protobuf.Descriptors.FileDescriptor; -import static com.google.protobuf.util.TimeUtil.add; -import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static org.junit.Assert.*; import static org.spine3.protobuf.Durations.seconds; import static org.spine3.protobuf.Timestamps.minutesAgo; @@ -160,14 +157,6 @@ public void when_command_delay_is_set_then_consider_it_scheduled() { assertTrue(Commands.isScheduled(cmd)); } - @Test - public void when_command_sending_time_is_set_then_consider_it_scheduled() { - final Timestamp deliveryTime = add(getCurrentTime(), seconds(10)); - final Command cmd = Commands.create(StringValue.getDefaultInstance(), createCommandContext(deliveryTime)); - - assertTrue(Commands.isScheduled(cmd)); - } - @Test public void when_no_scheduling_options_then_consider_command_not_scheduled() { final Command cmd = Commands.create(StringValue.getDefaultInstance(), CommandContext.getDefaultInstance()); @@ -190,12 +179,4 @@ public void when_set_negative_delay_then_throw_exception() { Commands.isScheduled(cmd); } - - @Test(expected = IllegalArgumentException.class) - public void when_set_past_sending_time_then_throw_exception() { - final CommandContext context = createCommandContext(/*delivery time=*/secondsAgo(10)); - final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); - - Commands.isScheduled(cmd); - } } diff --git a/client/src/test/java/org/spine3/testdata/TestContextFactory.java b/client/src/test/java/org/spine3/testdata/TestContextFactory.java index bc48315f2af..3fe63a99d76 100644 --- a/client/src/test/java/org/spine3/testdata/TestContextFactory.java +++ b/client/src/test/java/org/spine3/testdata/TestContextFactory.java @@ -81,7 +81,7 @@ public static CommandContext createCommandContext(UserId userId, CommandId comma */ public static CommandContext createCommandContext(Duration delay) { final Schedule schedule = Schedule.newBuilder() - .setDelay(delay) + .setAfter(delay) .build(); return createCommandContext(schedule); } @@ -89,20 +89,10 @@ public static CommandContext createCommandContext(Duration delay) { /** * Creates a new context with the given scheduling options. */ - public static CommandContext createCommandContext(Duration delay, boolean isInTime) { + public static CommandContext createCommandContext(Duration delay, boolean ignoreDelay) { final Schedule schedule = Schedule.newBuilder() - .setDelay(delay) - .setInTime(isInTime) - .build(); - return createCommandContext(schedule); - } - - /** - * Creates a new context with the given delivery time. - */ - public static CommandContext createCommandContext(Timestamp deliveryTime) { - final Schedule schedule = Schedule.newBuilder() - .setDeliveryTime(deliveryTime) + .setAfter(delay) + .setIgnoreDelay(ignoreDelay) .build(); return createCommandContext(schedule); } diff --git a/server/src/main/java/org/spine3/server/command/CommandScheduler.java b/server/src/main/java/org/spine3/server/command/CommandScheduler.java index 2dd74163fa6..681da86ca62 100644 --- a/server/src/main/java/org/spine3/server/command/CommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/CommandScheduler.java @@ -74,16 +74,22 @@ public void shutdown() { * @param command a command to deliver */ protected void post(Command command) { - final Command commandUpdated = setIsInTime(command); + final Command commandUpdated = setIgnoreDelay(command); commandBus.post(commandUpdated); } + /** + * Sets {@code ignoreDelay} option to true in {@link Schedule} command option. + * + * @param command the command to update + * @return the updated command + */ @VisibleForTesting - /* package */ static Command setIsInTime(Command command) { + /* package */ static Command setIgnoreDelay(Command command) { final CommandContext contextPrimary = command.getContext(); final Schedule scheduleUpdated = contextPrimary.getSchedule() .toBuilder() - .setInTime(true) + .setIgnoreDelay(true) .build(); final CommandContext contextUpdated = contextPrimary.toBuilder() .setSchedule(scheduleUpdated) diff --git a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java index a38ae0768cf..59c249c5e6c 100644 --- a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java @@ -20,17 +20,13 @@ package org.spine3.server.command; -import com.google.protobuf.Duration; -import com.google.protobuf.Timestamp; import org.spine3.base.Command; import org.spine3.base.Schedule; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.spine3.validate.Validate.isNotDefault; /** * The command scheduler implementation which uses basic Java task scheduling features. @@ -70,15 +66,7 @@ public void run() { private static long getDelaySeconds(Command command) { final Schedule schedule = command.getContext().getSchedule(); - final long delaySec; - final Duration delay = schedule.getDelay(); - if (isNotDefault(delay)) { - delaySec = delay.getSeconds(); - } else { - final Timestamp deliveryTime = schedule.getDeliveryTime(); - final Timestamp now = getCurrentTime(); - delaySec = deliveryTime.getSeconds() - now.getSeconds(); - } + final long delaySec = schedule.getAfter().getSeconds(); return delaySec; } diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index 83e936a9299..ff7a75449d8 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -454,16 +454,6 @@ public void schedule_command_if_delay_is_set() { verify(scheduler, times(1)).schedule(cmd); } - @Test - public void schedule_command_if_sending_time_is_set() { - final CommandContext context = createCommandContext(/*delivery time=*/add(getCurrentTime(), minutes(1))); - final Command cmd = Commands.create(createProject(newUuid()), context); - - commandBus.post(cmd); - - verify(scheduler, times(1)).schedule(cmd); - } - @Test public void do_not_schedule_command_if_no_scheduling_options_are_set() { commandBus.register(new CreateProjectHandler()); diff --git a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index e0caa79b580..742b9bedad0 100644 --- a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -28,11 +28,7 @@ import org.spine3.base.CommandContext; import org.spine3.base.Commands; -import static com.google.protobuf.util.TimeUtil.add; -import static com.google.protobuf.util.TimeUtil.getCurrentTime; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.protobuf.Durations.seconds; @@ -73,26 +69,16 @@ public void schedule_command_if_delay_is_set() throws InterruptedException { } @Test - public void schedule_command_if_sending_time_is_set() throws InterruptedException { - final CommandContext context = createCommandContext(/*delivery time=*/add(getCurrentTime(), DELAY)); - final Command cmd = Commands.create(createProject(newUuid()), context); - - scheduler.schedule(cmd); - - assertCommandSent(cmd, DELAY); - } - - @Test - public void set_command_is_in_time() { + public void set_ignore_command_delay_to_true() { final Command cmd = createProject(); assertFalse(cmd.getContext() .getSchedule() - .getInTime()); + .getIgnoreDelay()); - final Command updatedCmd = CommandScheduler.setIsInTime(cmd); + final Command updatedCmd = CommandScheduler.setIgnoreDelay(cmd); assertTrue(updatedCmd.getContext() .getSchedule() - .getInTime()); + .getIgnoreDelay()); } private void assertCommandSent(Command cmd, Duration delay) throws InterruptedException { @@ -101,7 +87,7 @@ private void assertCommandSent(Command cmd, Duration delay) throws InterruptedEx final long delayMs = delay.getSeconds() * 1000; Thread.sleep(delayMs + CHECK_OFFSET_MS); - final Command updatedCmd = CommandScheduler.setIsInTime(cmd); + final Command updatedCmd = CommandScheduler.setIgnoreDelay(cmd); verify(commandBus, times(1)).post(updatedCmd); } From 970ff8f1c1068f4e9c9270c3341f8a34739d8353 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 11:57:22 +0300 Subject: [PATCH 09/26] Improve an error msg. --- server/src/main/java/org/spine3/server/command/CommandBus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index b980f86c67d..784ac011e11 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -232,7 +232,7 @@ private void schedule(Command command) { scheduler.schedule(command); } else { throw new IllegalStateException( - "Scheduled commands are not supported by this command bus (scheduler is not set)."); + "Scheduled commands are not supported by this command bus: scheduler is not set."); } } From 292f893b103be75ffdd1e94a92e36a12cad88ba8 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 12:02:21 +0300 Subject: [PATCH 10/26] Rename a constant. --- client/src/main/java/org/spine3/base/Commands.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/org/spine3/base/Commands.java b/client/src/main/java/org/spine3/base/Commands.java index fac0493944b..73dfee958c2 100644 --- a/client/src/main/java/org/spine3/base/Commands.java +++ b/client/src/main/java/org/spine3/base/Commands.java @@ -52,7 +52,7 @@ public class Commands { /** * A substring which the {@code .proto} file containing commands must have in its name. */ - public static final String COMMANDS_FILE_SUBSTRING = "commands"; + public static final String COMMANDS = "commands"; private static final char PROTO_FILE_SEPARATOR = '/'; @@ -195,13 +195,13 @@ public static String formatMessageTypeAndId(String format, Message commandMessag * Checks if the file is for commands. * * @param file a descriptor of a {@code .proto} file to check - * @return {@code true} if the file name contains {@link #COMMANDS_FILE_SUBSTRING} substring, {@code false} otherwise + * @return {@code true} if the file name contains {@link #COMMANDS} substring, {@code false} otherwise */ public static boolean isCommandsFile(FileDescriptor file) { final String fqn = file.getName(); final int startIndexOfFileName = fqn.lastIndexOf(PROTO_FILE_SEPARATOR) + 1; final String fileName = fqn.substring(startIndexOfFileName); - final boolean isCommandsFile = fileName.contains(COMMANDS_FILE_SUBSTRING); + final boolean isCommandsFile = fileName.contains(COMMANDS); return isCommandsFile; } From 96ade6ed5ae9bf1f4c412e66d1e6f3ce6c1d2092 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 12:36:25 +0300 Subject: [PATCH 11/26] Avoid circular dependency between CommandBus and CommandScheduler. --- .../org/spine3/server/command/CommandBus.java | 26 ++++++++++++++++--- .../server/command/CommandScheduler.java | 21 +++++++-------- .../command/ExecutorCommandScheduler.java | 9 ------- .../ExecutorCommandSchedulerShould.java | 24 ++++++++++++----- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index 784ac011e11..002d65f2fdf 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -20,6 +20,7 @@ package org.spine3.server.command; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.protobuf.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,9 +72,12 @@ public class CommandBus implements AutoCloseable { private ProblemLog problemLog = new ProblemLog(); private CommandBus(Builder builder) { - this.commandStore = builder.getCommandStore(); - this.scheduler = builder.getScheduler(); - this.commandStatus = new CommandStatusHelper(commandStore); + commandStore = builder.getCommandStore(); + scheduler = builder.getScheduler(); + if (scheduler != null) { + scheduler.setPostFunction(newPostFunction()); + } + commandStatus = new CommandStatusHelper(commandStore); } /** @@ -319,6 +323,21 @@ private CommandHandlerMethod getHandler(CommandClass cls) { return handlerRegistry.getHandler(cls); } + private Function newPostFunction() { + final Function function = new Function() { + @Nullable + @Override + public Command apply(@Nullable Command command) { + if (command == null) { + return null; + } + post(command); + return command; + } + }; + return function; + } + @Override public void close() throws Exception { dispatcherRegistry.unregisterAll(); @@ -371,6 +390,7 @@ public Builder setScheduler(CommandScheduler scheduler) { return this; } + @Nullable public CommandScheduler getScheduler() { return scheduler; } diff --git a/server/src/main/java/org/spine3/server/command/CommandScheduler.java b/server/src/main/java/org/spine3/server/command/CommandScheduler.java index 681da86ca62..c7e07beee65 100644 --- a/server/src/main/java/org/spine3/server/command/CommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/CommandScheduler.java @@ -21,6 +21,7 @@ package org.spine3.server.command; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import org.spine3.base.Command; import org.spine3.base.CommandContext; import org.spine3.base.Schedule; @@ -34,18 +35,9 @@ */ public abstract class CommandScheduler { - private final CommandBus commandBus; - private boolean isActive = true; - /** - * Creates a new instance. - * - * @param commandBus a command bus used to send scheduled commands - */ - protected CommandScheduler(CommandBus commandBus) { - this.commandBus = commandBus; - } + private Function postFunction; /** * Schedule a command and deliver it to the target according to the scheduling options. @@ -75,7 +67,14 @@ public void shutdown() { */ protected void post(Command command) { final Command commandUpdated = setIgnoreDelay(command); - commandBus.post(commandUpdated); + postFunction.apply(commandUpdated); + } + + /** + * Sets a function used to post scheduled commands. + */ + /* package */ void setPostFunction(Function postFunction) { + this.postFunction = postFunction; } /** diff --git a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java index 59c249c5e6c..17d1082b25f 100644 --- a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java @@ -43,15 +43,6 @@ public class ExecutorCommandScheduler extends CommandScheduler { private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(MIN_THEAD_POOL_SIZE); - /** - * Creates a new instance. - * - * @param commandBus a command bus used to send scheduled commands - */ - public ExecutorCommandScheduler(CommandBus commandBus) { - super(commandBus); - } - @Override public void schedule(final Command command) { super.schedule(command); diff --git a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index 742b9bedad0..3a18664fd55 100644 --- a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -20,6 +20,7 @@ package org.spine3.server.command; +import com.google.common.base.Function; import com.google.protobuf.Duration; import org.junit.After; import org.junit.Before; @@ -28,8 +29,9 @@ import org.spine3.base.CommandContext; import org.spine3.base.Commands; +import javax.annotation.Nullable; + import static org.junit.Assert.*; -import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.protobuf.Durations.seconds; import static org.spine3.testdata.TestCommands.createProject; @@ -44,13 +46,21 @@ public class ExecutorCommandSchedulerShould { private static final Duration DELAY = seconds(1); private static final int CHECK_OFFSET_MS = 50; - private CommandBus commandBus; private CommandScheduler scheduler; + private Command postedCommand; + @Before public void setUpTest() { - this.commandBus = mock(CommandBus.class); - this.scheduler = new ExecutorCommandScheduler(commandBus); + this.scheduler = new ExecutorCommandScheduler(); + scheduler.setPostFunction(new Function() { + @Nullable + @Override + public Command apply(@Nullable Command input) { + postedCommand = input; + return input; + } + }); } @After @@ -82,13 +92,13 @@ public void set_ignore_command_delay_to_true() { } private void assertCommandSent(Command cmd, Duration delay) throws InterruptedException { - verify(commandBus, times(0)).post(any(Command.class)); + assertNull(postedCommand); final long delayMs = delay.getSeconds() * 1000; Thread.sleep(delayMs + CHECK_OFFSET_MS); - final Command updatedCmd = CommandScheduler.setIgnoreDelay(cmd); - verify(commandBus, times(1)).post(updatedCmd); + final Command expectedCommand = CommandScheduler.setIgnoreDelay(cmd); + assertEquals(expectedCommand, postedCommand); } @Test From bfdcb38d63dbe6812933d03195ffabf74aad2dcb Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 13:35:33 +0300 Subject: [PATCH 12/26] Use mockito spy instead of mock to improve code coverage. --- .../java/org/spine3/server/command/CommandBusShould.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index ff7a75449d8..b2d3368039c 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -50,8 +50,6 @@ import java.util.List; import java.util.Set; -import static com.google.protobuf.util.TimeUtil.add; -import static com.google.protobuf.util.TimeUtil.getCurrentTime; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; @@ -77,7 +75,7 @@ public void setUp() { .setCommandStore(commandStore) .setScheduler(scheduler) .build(); - log = mock(CommandBus.ProblemLog.class); + log = spy(CommandBus.ProblemLog.class); commandBus.setProblemLog(log); commandFactory = TestCommandFactory.newInstance(CommandBusShould.class); } @@ -372,7 +370,7 @@ public void set_command_status_to_error_when_dispatcher_throws() throws Exceptio verify(commandStore, times(1)).updateStatus(eq(command.getContext() .getCommandId()), eq(exception)); // Verify we logged the error. - verify(log, atMost(1)).errorDispatching(eq(exception), eq(command)); + verify(log, times(1)).errorDispatching(eq(exception), eq(command)); } @Test From 2b42bcfebc7c3aef54b786132b9386ea96d43019 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 16:34:59 +0300 Subject: [PATCH 13/26] Add test API to aggregate. --- .../spine3/server/aggregate/Aggregate.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/spine3/server/aggregate/Aggregate.java b/server/src/main/java/org/spine3/server/aggregate/Aggregate.java index 70412bbb85a..598fa72037b 100644 --- a/server/src/main/java/org/spine3/server/aggregate/Aggregate.java +++ b/server/src/main/java/org/spine3/server/aggregate/Aggregate.java @@ -48,6 +48,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Throwables.propagate; +import static java.util.Collections.singletonList; import static org.spine3.base.Identifiers.idToAny; /** @@ -276,7 +277,7 @@ private void updateState() { * which is called automatically by {@link AggregateRepository}. */ @VisibleForTesting - protected final void testDispatch(Message command, CommandContext context) { + public final void testDispatch(Message command, CommandContext context) { dispatch(command, context); } @@ -365,6 +366,33 @@ private void apply(Iterable messages, CommandContext commandC } } + /** + * This method is provided only for the purpose of testing event appliers + * of an aggregate and must not be called from the production code. + * + *

Calls {@link #apply(Iterable, CommandContext)}. + */ + @VisibleForTesting + public final void testApply(Message message, CommandContext commandContext) { + init(); + try { + apply(singletonList(message), commandContext); + } catch (InvocationTargetException e) { + throw propagate(e); + } + } + + /** + * This method is provided only for the purpose of testing an aggregate and + * must not be called from the production code. + * + *

Calls {@link #incrementState(Message)}. + */ + @VisibleForTesting + public final void testIncrementState(S newState) { + incrementState(newState); + } + /** * Applies an event to the aggregate. * From 754a16114faf922e17be25e9599d05a3ecb18cab Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Mon, 18 Apr 2016 17:55:54 +0300 Subject: [PATCH 14/26] Track commands which are scheduled already in scheduler. --- .../main/java/org/spine3/base/Commands.java | 21 +++++---- .../src/main/proto/spine/base/command.proto | 5 --- .../java/org/spine3/base/CommandsShould.java | 8 ---- .../spine3/testdata/TestContextFactory.java | 12 +---- .../server/command/CommandScheduler.java | 30 ++----------- .../command/ExecutorCommandScheduler.java | 15 +++++++ .../ExecutorCommandSchedulerShould.java | 44 ++++++++++--------- 7 files changed, 54 insertions(+), 81 deletions(-) diff --git a/client/src/main/java/org/spine3/base/Commands.java b/client/src/main/java/org/spine3/base/Commands.java index 73dfee958c2..561e2d858cd 100644 --- a/client/src/main/java/org/spine3/base/Commands.java +++ b/client/src/main/java/org/spine3/base/Commands.java @@ -50,11 +50,12 @@ public class Commands { /** - * A substring which the {@code .proto} file containing commands must have in its name. + * A suffix which the {@code .proto} file containing commands must have in its name. */ - public static final String COMMANDS = "commands"; + public static final String FILE_NAME_SUFFIX = "commands"; - private static final char PROTO_FILE_SEPARATOR = '/'; + private static final char FILE_PATH_SEPARATOR = '/'; + private static final char FILE_EXTENSION_SEPARATOR = '.'; private Commands() {} @@ -195,13 +196,14 @@ public static String formatMessageTypeAndId(String format, Message commandMessag * Checks if the file is for commands. * * @param file a descriptor of a {@code .proto} file to check - * @return {@code true} if the file name contains {@link #COMMANDS} substring, {@code false} otherwise + * @return {@code true} if the file name ends with the {@link #FILE_NAME_SUFFIX}, {@code false} otherwise */ public static boolean isCommandsFile(FileDescriptor file) { final String fqn = file.getName(); - final int startIndexOfFileName = fqn.lastIndexOf(PROTO_FILE_SEPARATOR) + 1; - final String fileName = fqn.substring(startIndexOfFileName); - final boolean isCommandsFile = fileName.contains(COMMANDS); + final int startIndexOfFileName = fqn.lastIndexOf(FILE_PATH_SEPARATOR) + 1; + final int endIndexOfFileName = fqn.lastIndexOf(FILE_EXTENSION_SEPARATOR); + final String fileName = fqn.substring(startIndexOfFileName, endIndexOfFileName); + final boolean isCommandsFile = fileName.endsWith(FILE_NAME_SUFFIX); return isCommandsFile; } @@ -223,13 +225,10 @@ public static boolean isEntityFile(FileDescriptor file) { * Checks if the command is scheduled to be delivered later. * * @param command a command to check - * @return {@code true} if the command context has scheduling options set, {@code false} otherwise + * @return {@code true} if the command context has a scheduling option set, {@code false} otherwise */ public static boolean isScheduled(Command command) { final Schedule schedule = command.getContext().getSchedule(); - if (schedule.getIgnoreDelay()) { - return false; - } final Duration delay = schedule.getAfter(); if (isNotDefault(delay)) { checkArgument(delay.getSeconds() > 0, "Command delay seconds must be a positive value."); diff --git a/client/src/main/proto/spine/base/command.proto b/client/src/main/proto/spine/base/command.proto index 3c785dcb9a0..87289eaff88 100644 --- a/client/src/main/proto/spine/base/command.proto +++ b/client/src/main/proto/spine/base/command.proto @@ -86,11 +86,6 @@ message CommandContext { message Schedule { // The delay between the moment of receiving a command at the server and its delivery to the target. google.protobuf.Duration after = 1; - - // `true` if the command should be delivered to the target right now, despite the delay set; - // `false` otherwise. - // This flag is needed to specify that the delay is passed, and to retain the info about the delay. - bool ignore_delay = 2; } // Enumeration of possible failures when validating a command. diff --git a/client/src/test/java/org/spine3/base/CommandsShould.java b/client/src/test/java/org/spine3/base/CommandsShould.java index 87e9b097875..cdf2333d00a 100644 --- a/client/src/test/java/org/spine3/base/CommandsShould.java +++ b/client/src/test/java/org/spine3/base/CommandsShould.java @@ -164,14 +164,6 @@ public void when_no_scheduling_options_then_consider_command_not_scheduled() { assertFalse(Commands.isScheduled(cmd)); } - @Test - public void when_command_is_in_time_then_consider_it_not_scheduled() { - final CommandContext context = createCommandContext(/*delay=*/seconds(10), /*in time=*/true); - final Command cmd = Commands.create(StringValue.getDefaultInstance(), context); - - assertFalse(Commands.isScheduled(cmd)); - } - @Test(expected = IllegalArgumentException.class) public void when_set_negative_delay_then_throw_exception() { final CommandContext context = createCommandContext(/*delay=*/seconds(-10)); diff --git a/client/src/test/java/org/spine3/testdata/TestContextFactory.java b/client/src/test/java/org/spine3/testdata/TestContextFactory.java index 3fe63a99d76..370e15000ab 100644 --- a/client/src/test/java/org/spine3/testdata/TestContextFactory.java +++ b/client/src/test/java/org/spine3/testdata/TestContextFactory.java @@ -89,17 +89,9 @@ public static CommandContext createCommandContext(Duration delay) { /** * Creates a new context with the given scheduling options. */ - public static CommandContext createCommandContext(Duration delay, boolean ignoreDelay) { - final Schedule schedule = Schedule.newBuilder() - .setAfter(delay) - .setIgnoreDelay(ignoreDelay) - .build(); - return createCommandContext(schedule); - } - public static CommandContext createCommandContext(Schedule schedule) { - final CommandContext.Builder builder = TestContextFactory.createCommandContext().toBuilder() - .setSchedule(schedule); + final CommandContext.Builder builder = createCommandContext().toBuilder() + .setSchedule(schedule); return builder.build(); } diff --git a/server/src/main/java/org/spine3/server/command/CommandScheduler.java b/server/src/main/java/org/spine3/server/command/CommandScheduler.java index c7e07beee65..45d4d23d4a5 100644 --- a/server/src/main/java/org/spine3/server/command/CommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/CommandScheduler.java @@ -20,11 +20,8 @@ package org.spine3.server.command; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import org.spine3.base.Command; -import org.spine3.base.CommandContext; -import org.spine3.base.Schedule; import static com.google.common.base.Preconditions.checkState; @@ -42,6 +39,8 @@ public abstract class CommandScheduler { /** * Schedule a command and deliver it to the target according to the scheduling options. * + *

NOTE: check if the command is scheduled already. + * * @param command a command to deliver later * @throws IllegalStateException if the scheduler is shut down * @see #post(Command) @@ -66,8 +65,7 @@ public void shutdown() { * @param command a command to deliver */ protected void post(Command command) { - final Command commandUpdated = setIgnoreDelay(command); - postFunction.apply(commandUpdated); + postFunction.apply(command); } /** @@ -76,26 +74,4 @@ protected void post(Command command) { /* package */ void setPostFunction(Function postFunction) { this.postFunction = postFunction; } - - /** - * Sets {@code ignoreDelay} option to true in {@link Schedule} command option. - * - * @param command the command to update - * @return the updated command - */ - @VisibleForTesting - /* package */ static Command setIgnoreDelay(Command command) { - final CommandContext contextPrimary = command.getContext(); - final Schedule scheduleUpdated = contextPrimary.getSchedule() - .toBuilder() - .setIgnoreDelay(true) - .build(); - final CommandContext contextUpdated = contextPrimary.toBuilder() - .setSchedule(scheduleUpdated) - .build(); - final Command cmdUpdated = command.toBuilder() - .setContext(contextUpdated) - .build(); - return cmdUpdated; - } } diff --git a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java index 17d1082b25f..21c2fcde3c2 100644 --- a/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java +++ b/server/src/main/java/org/spine3/server/command/ExecutorCommandScheduler.java @@ -21,11 +21,14 @@ package org.spine3.server.command; import org.spine3.base.Command; +import org.spine3.base.CommandId; import org.spine3.base.Schedule; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import static com.google.common.collect.Sets.newHashSet; import static java.util.concurrent.TimeUnit.SECONDS; /** @@ -41,11 +44,16 @@ public class ExecutorCommandScheduler extends CommandScheduler { private static final int MIN_THEAD_POOL_SIZE = 5; + private static final Set SCHEDULED_COMMAND_IDS = newHashSet(); + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(MIN_THEAD_POOL_SIZE); @Override public void schedule(final Command command) { super.schedule(command); + if (isScheduledAlready(command)) { + return; + } final long delaySec = getDelaySeconds(command); executorService.schedule(new Runnable() { @Override @@ -53,6 +61,13 @@ public void run() { post(command); } }, delaySec, SECONDS); + SCHEDULED_COMMAND_IDS.add(command.getContext().getCommandId()); + } + + private static boolean isScheduledAlready(Command command) { + final CommandId id = command.getContext().getCommandId(); + final boolean isScheduledAlready = SCHEDULED_COMMAND_IDS.contains(id); + return isScheduledAlready; } private static long getDelaySeconds(Command command) { diff --git a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index 3a18664fd55..b64b328a988 100644 --- a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -31,9 +31,11 @@ import javax.annotation.Nullable; +import static com.google.common.base.Throwables.propagate; import static org.junit.Assert.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.protobuf.Durations.seconds; +import static org.spine3.testdata.TestCommands.addTask; import static org.spine3.testdata.TestCommands.createProject; import static org.spine3.testdata.TestContextFactory.createCommandContext; @@ -44,6 +46,7 @@ public class ExecutorCommandSchedulerShould { private static final Duration DELAY = seconds(1); + private static final long DELAY_MS = DELAY.getSeconds() * 1000; private static final int CHECK_OFFSET_MS = 50; private CommandScheduler scheduler; @@ -69,36 +72,29 @@ public void tearDownTest() { } @Test - public void schedule_command_if_delay_is_set() throws InterruptedException { + public void schedule_command_if_delay_is_set() { final CommandContext context = createCommandContext(DELAY); final Command cmd = Commands.create(createProject(newUuid()), context); scheduler.schedule(cmd); - assertCommandSent(cmd, DELAY); + assertNull(postedCommand); + sleep(DELAY_MS + CHECK_OFFSET_MS); + assertEquals(cmd, postedCommand); } @Test - public void set_ignore_command_delay_to_true() { - final Command cmd = createProject(); - assertFalse(cmd.getContext() - .getSchedule() - .getIgnoreDelay()); - - final Command updatedCmd = CommandScheduler.setIgnoreDelay(cmd); - assertTrue(updatedCmd.getContext() - .getSchedule() - .getIgnoreDelay()); - } - - private void assertCommandSent(Command cmd, Duration delay) throws InterruptedException { - assertNull(postedCommand); + public void not_schedule_command_with_same_id_twice() { + final CommandContext context = createCommandContext(DELAY); + final String id = newUuid(); + final Command expectedCmd = Commands.create(createProject(id), context); + final Command extraCmd = Commands.create(addTask(id), context); - final long delayMs = delay.getSeconds() * 1000; - Thread.sleep(delayMs + CHECK_OFFSET_MS); + scheduler.schedule(expectedCmd); + scheduler.schedule(extraCmd); - final Command expectedCommand = CommandScheduler.setIgnoreDelay(cmd); - assertEquals(expectedCommand, postedCommand); + sleep(DELAY_MS + CHECK_OFFSET_MS); + assertEquals(expectedCmd, postedCommand); } @Test @@ -112,4 +108,12 @@ public void throw_exception_if_is_shutdown() { } fail("Must throw an exception as it is shutdown."); } + + private static void sleep(long delayMs) { + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + throw propagate(e); + } + } } From b1f115d3aab9e6abe5ea6b2ba756509b79b75378 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 13:14:58 +0300 Subject: [PATCH 15/26] Improve command schedule tests. --- .../java/org/spine3/protobuf/Durations.java | 106 ++++++++++++------ .../org/spine3/protobuf/DurationsShould.java | 23 +++- .../ExecutorCommandSchedulerShould.java | 48 +++----- 3 files changed, 108 insertions(+), 69 deletions(-) diff --git a/client/src/main/java/org/spine3/protobuf/Durations.java b/client/src/main/java/org/spine3/protobuf/Durations.java index 27ea85ce63e..6556e08e8d1 100644 --- a/client/src/main/java/org/spine3/protobuf/Durations.java +++ b/client/src/main/java/org/spine3/protobuf/Durations.java @@ -19,7 +19,7 @@ /** * Utility class for working with durations in addition to those available from {@link TimeUtil}. - * + *

*

Use {@code import static org.spine3.protobuf.Durations.*} for compact initialization like this: *

  *      Duration d = add(hours(2), minutes(30));
@@ -41,6 +41,17 @@ private Durations() {}
      * using com.google.protobuf.Duration just by adding 's' before the dot.
      */
 
+    /**
+     * Obtains an instance of {@code Duration} representing the passed number of milliseconds.
+     *
+     * @param milliseconds the number of milliseconds, positive or negative
+     * @return a non-null {@code Duration}
+     */
+    public static Duration ofMilliseconds(long milliseconds) {
+        final Duration result = createDurationFromMillis(milliseconds);
+        return result;
+    }
+
     /**
      * Obtains an instance of {@code Duration} from the number of seconds.
      *
@@ -48,10 +59,10 @@ private Durations() {}
      * @return a non-null {@code Duration}
      */
     public static Duration ofSeconds(long seconds) {
-        return createDurationFromMillis(safeMultiply(seconds, MILLIS_PER_SECOND));
+        final Duration result = createDurationFromMillis(safeMultiply(seconds, MILLIS_PER_SECOND));
+        return result;
     }
 
-
     /**
      * Obtains an instance of {@code Duration} representing the passed number of minutes.
      *
@@ -59,29 +70,48 @@ public static Duration ofSeconds(long seconds) {
      * @return a non-null {@code Duration}
      */
     public static Duration ofMinutes(long minutes) {
-        return ofSeconds(safeMultiply(minutes, SECONDS_PER_MINUTE));
+        final Duration duration = ofSeconds(safeMultiply(minutes, SECONDS_PER_MINUTE));
+        return duration;
     }
 
     /**
      * Obtains an instance of {@code Duration} representing the passed number of hours.
+     *
      * @param hours the number of hours, positive or negative
      * @return a non-null {@code Duration}
      */
     public static Duration ofHours(long hours) {
-        return ofMinutes(safeMultiply(hours, MINUTES_PER_HOUR));
+        final Duration duration = ofMinutes(safeMultiply(hours, MINUTES_PER_HOUR));
+        return duration;
     }
 
+    // Methods for brief computations with Durations like
+    //       add(hours(2), minutes(30));
+
     /**
      * Obtains an instance of {@code Duration} representing the passed number of nanoseconds.
+     *
      * @param nanos the number of nanoseconds, positive or negative
      * @return a non-null {@code Duration}
      */
     public static Duration nanos(long nanos) {
-        return TimeUtil.createDurationFromNanos(nanos);
+        final Duration duration = TimeUtil.createDurationFromNanos(nanos);
+        return duration;
+    }
+
+    /**
+     * Obtains an instance of {@code Duration} representing the passed number of milliseconds.
+     *
+     * @param milliseconds the number of milliseconds, positive or negative
+     * @return a non-null {@code Duration}
+     */
+    public static Duration milliseconds(long milliseconds) {
+        return ofMilliseconds(milliseconds);
     }
 
     /**
      * Obtains an instance of {@code Duration} representing the passed number of seconds.
+     *
      * @param seconds the number of seconds, positive or negative
      * @return a non-null {@code Duration}
      */
@@ -89,9 +119,6 @@ public static Duration seconds(long seconds) {
         return ofSeconds(seconds);
     }
 
-    // Methods for brief computations with Durations like
-    //       add(hours(2), minutes(30));
-
     /**
      * This method allows for more compact code of creation of {@code Duration} instance with minutes.
      */
@@ -108,30 +135,27 @@ public static Duration hours(long hours) {
 
     /**
      * Adds two durations one of which or both can be {@code null}.
+     *
      * @param d1 a duration to add, could be {@code null}
      * @param d2 another duration to add, could be {@code null}
-     * @return
-     *      
    - *
  • sum of two durations if both of them are {@code non-null}
  • - *
  • another {@code non-null} value, if one is {@code null}
  • - *
  • {@link #ZERO} if both values are {@code null}
  • - *
- * + * @return
    + *
  • sum of two durations if both of them are {@code non-null}
  • + *
  • another {@code non-null} value, if one is {@code null}
  • + *
  • {@link #ZERO} if both values are {@code null}
  • + *
*/ public static Duration add(@Nullable Duration d1, @Nullable Duration d2) { if (d1 == null && d2 == null) { return ZERO; } - if (d1 == null) { return d2; } - if (d2 == null) { return d1; } - - return TimeUtil.add(d1, d2); + final Duration result = TimeUtil.add(d1, d2); + return result; } @@ -141,14 +165,16 @@ public static Duration add(@Nullable Duration d1, @Nullable Duration d2) { public static Duration subtract(Duration d1, Duration d2) { /* The sole purpose of this method is minimize the dependencies of the classes working with durations. */ - return TimeUtil.subtract(d1, d2); + final Duration result = TimeUtil.subtract(d1, d2); + return result; } /** * This method allows for more compact code of creation of {@code Duration} instance with hours and minutes. */ public static Duration hoursAndMinutes(long hours, long minutes) { - return add(hours(hours), minutes(minutes)); + final Duration result = add(hours(hours), minutes(minutes)); + return result; } /** @@ -157,14 +183,17 @@ public static Duration hoursAndMinutes(long hours, long minutes) { public static long toNanos(Duration duration) { /* The sole purpose of this method is minimize the dependencies of the classes working with durations. */ - return TimeUtil.toNanos(duration); + final long result = TimeUtil.toNanos(duration); + return result; } /** * Convert a duration to the number of seconds. */ public static long toSeconds(Duration duration) { - return floorDiv(TimeUtil.toMillis(duration), MILLIS_PER_SECOND); + final long millis = TimeUtil.toMillis(duration); + final long seconds = floorDiv(millis, MILLIS_PER_SECOND); + return seconds; } /** @@ -201,7 +230,9 @@ public static long getHours(Duration value) { public static int getMinutes(Duration value) { final long allMinutes = toMinutes(value); final long remainder = allMinutes % MINUTES_PER_HOUR; - return Long.valueOf(remainder).intValue(); + final int result = Long.valueOf(remainder) + .intValue(); + return result; } /** @@ -209,14 +240,18 @@ public static int getMinutes(Duration value) { */ public static boolean isPositiveOrZero(Duration value) { final long millis = toMillis(value); - return millis >= 0; + final boolean result = millis >= 0; + return result; } /** * @return {@code true} if the passed value is greater than zero, {@code false} otherwise */ public static boolean isPositive(DurationOrBuilder value) { - return value.getSeconds() > 0 || value.getNanos() > 0; + final boolean secondsPositive = value.getSeconds() > 0; + final boolean nanosPositive = value.getNanos() > 0; + final boolean result = secondsPositive || nanosPositive; + return result; } @@ -224,7 +259,10 @@ public static boolean isPositive(DurationOrBuilder value) { * @return {@code true} if the passed value is zero, {@code false} otherwise */ public static boolean isZero(DurationOrBuilder value) { - return value.getSeconds() == 0 && value.getNanos() == 0; + final boolean noSeconds = value.getSeconds() == 0; + final boolean noNanos = value.getNanos() == 0; + final boolean result = noSeconds && noNanos; + return result; } /** @@ -233,7 +271,8 @@ public static boolean isZero(DurationOrBuilder value) { public static boolean isGreaterThan(Duration value, Duration another) { final long nanos = toNanos(value); final long anotherNanos = toNanos(another); - return nanos > anotherNanos; + final boolean isGreater = nanos > anotherNanos; + return isGreater; } /** @@ -242,7 +281,8 @@ public static boolean isGreaterThan(Duration value, Duration another) { public static boolean isLessThan(Duration value, Duration another) { final long nanos = toNanos(value); final long anotherNanos = toNanos(another); - return nanos < anotherNanos; + final boolean isLessThan = nanos < anotherNanos; + return isLessThan; } /** @@ -251,7 +291,8 @@ public static boolean isLessThan(Duration value, Duration another) { public static int compare(Duration d1, Duration d2) { final long nanos = toNanos(d1); final long otherNanos = toNanos(d2); - return Long.compare(nanos, otherNanos); + final int result = Long.compare(nanos, otherNanos); + return result; } /** @@ -259,6 +300,7 @@ public static int compare(Duration d1, Duration d2) { */ public static boolean isNegative(Duration value) { final long nanos = toNanos(value); - return nanos < 0; + final boolean isNegative = nanos < 0; + return isNegative; } } diff --git a/client/src/test/java/org/spine3/protobuf/DurationsShould.java b/client/src/test/java/org/spine3/protobuf/DurationsShould.java index dad06a894b1..34eb0fa4624 100644 --- a/client/src/test/java/org/spine3/protobuf/DurationsShould.java +++ b/client/src/test/java/org/spine3/protobuf/DurationsShould.java @@ -21,9 +21,9 @@ import com.google.protobuf.Duration; +import com.google.protobuf.util.TimeUtil; import org.junit.Test; -import static com.google.protobuf.Duration.newBuilder; import static org.junit.Assert.*; import static org.spine3.protobuf.Durations.*; import static org.spine3.test.Tests.hasPrivateUtilityConstructor; @@ -46,6 +46,25 @@ public void have_ZERO_constant() { } + @Test + public void convert_milliseconds_to_duration() { + convertMillisecondsToDurationTest(0); + convertMillisecondsToDurationTest(27); + convertMillisecondsToDurationTest(-384); + } + + private static void convertMillisecondsToDurationTest(long millis) { + final Duration expected = TimeUtil.createDurationFromMillis(millis); + assertEquals(expected, ofMilliseconds(millis)); + assertEquals(expected, milliseconds(millis)); + } + + @Test(expected = IllegalArgumentException.class) + public void fail_to_convert_milliseconds_to_duration_if_input_is_too_big() { + ofMilliseconds(Long.MAX_VALUE); + } + + @Test public void convert_seconds_to_duration() { convertSecondsToDurationTest(0); @@ -384,6 +403,6 @@ private static long minutesToSeconds(long minutes) { } private static Duration durationFromSec(long seconds) { - return newBuilder().setSeconds(seconds).build(); + return Duration.newBuilder().setSeconds(seconds).build(); } } diff --git a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index b64b328a988..deac3a17df0 100644 --- a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -20,7 +20,6 @@ package org.spine3.server.command; -import com.google.common.base.Function; import com.google.protobuf.Duration; import org.junit.After; import org.junit.Before; @@ -29,12 +28,10 @@ import org.spine3.base.CommandContext; import org.spine3.base.Commands; -import javax.annotation.Nullable; - -import static com.google.common.base.Throwables.propagate; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; -import static org.spine3.protobuf.Durations.seconds; +import static org.spine3.protobuf.Durations.milliseconds; import static org.spine3.testdata.TestCommands.addTask; import static org.spine3.testdata.TestCommands.createProject; import static org.spine3.testdata.TestContextFactory.createCommandContext; @@ -45,25 +42,17 @@ @SuppressWarnings("InstanceMethodNamingConvention") public class ExecutorCommandSchedulerShould { - private static final Duration DELAY = seconds(1); - private static final long DELAY_MS = DELAY.getSeconds() * 1000; - private static final int CHECK_OFFSET_MS = 50; + private static final long DELAY_MS = 1100; - private CommandScheduler scheduler; + private static final Duration DELAY = milliseconds(DELAY_MS); - private Command postedCommand; + private CommandScheduler scheduler; + private CommandContext context; @Before public void setUpTest() { - this.scheduler = new ExecutorCommandScheduler(); - scheduler.setPostFunction(new Function() { - @Nullable - @Override - public Command apply(@Nullable Command input) { - postedCommand = input; - return input; - } - }); + this.scheduler = spy(ExecutorCommandScheduler.class); + this.context = createCommandContext(DELAY); } @After @@ -73,19 +62,16 @@ public void tearDownTest() { @Test public void schedule_command_if_delay_is_set() { - final CommandContext context = createCommandContext(DELAY); final Command cmd = Commands.create(createProject(newUuid()), context); scheduler.schedule(cmd); - assertNull(postedCommand); - sleep(DELAY_MS + CHECK_OFFSET_MS); - assertEquals(cmd, postedCommand); + verify(scheduler, never()).post(cmd); + verify(scheduler, after(DELAY_MS).times(1)).post(cmd); } @Test public void not_schedule_command_with_same_id_twice() { - final CommandContext context = createCommandContext(DELAY); final String id = newUuid(); final Command expectedCmd = Commands.create(createProject(id), context); final Command extraCmd = Commands.create(addTask(id), context); @@ -93,8 +79,8 @@ public void not_schedule_command_with_same_id_twice() { scheduler.schedule(expectedCmd); scheduler.schedule(extraCmd); - sleep(DELAY_MS + CHECK_OFFSET_MS); - assertEquals(expectedCmd, postedCommand); + verify(scheduler, after(DELAY_MS).times(1)).post(expectedCmd); + verify(scheduler, never()).post(extraCmd); } @Test @@ -108,12 +94,4 @@ public void throw_exception_if_is_shutdown() { } fail("Must throw an exception as it is shutdown."); } - - private static void sleep(long delayMs) { - try { - Thread.sleep(delayMs); - } catch (InterruptedException e) { - throw propagate(e); - } - } } From 951b8b52ba0436358b32cea38fb3c13289651473 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 13:15:17 +0300 Subject: [PATCH 16/26] Rename aggregate methods for tests. --- .../spine3/server/aggregate/Aggregate.java | 6 +-- .../server/aggregate/AggregateShould.java | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/org/spine3/server/aggregate/Aggregate.java b/server/src/main/java/org/spine3/server/aggregate/Aggregate.java index 598fa72037b..d05f9c1d684 100644 --- a/server/src/main/java/org/spine3/server/aggregate/Aggregate.java +++ b/server/src/main/java/org/spine3/server/aggregate/Aggregate.java @@ -277,7 +277,7 @@ private void updateState() { * which is called automatically by {@link AggregateRepository}. */ @VisibleForTesting - public final void testDispatch(Message command, CommandContext context) { + public final void dispatchForTest(Message command, CommandContext context) { dispatch(command, context); } @@ -373,7 +373,7 @@ private void apply(Iterable messages, CommandContext commandC *

Calls {@link #apply(Iterable, CommandContext)}. */ @VisibleForTesting - public final void testApply(Message message, CommandContext commandContext) { + public final void applyForTest(Message message, CommandContext commandContext) { init(); try { apply(singletonList(message), commandContext); @@ -389,7 +389,7 @@ public final void testApply(Message message, CommandContext commandContext) { *

Calls {@link #incrementState(Message)}. */ @VisibleForTesting - public final void testIncrementState(S newState) { + public final void incrementStateForTest(S newState) { incrementState(newState); } diff --git a/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java b/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java index 205f3af2525..a1e5b485cb9 100644 --- a/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java +++ b/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java @@ -146,7 +146,7 @@ public void not_accept_to_constructor_id_of_unsupported_type() { @Test public void handle_one_command_and_apply_appropriate_event() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); assertTrue(aggregate.isCreateProjectCommandHandled); assertTrue(aggregate.isProjectCreatedEventApplied); @@ -154,7 +154,7 @@ public void handle_one_command_and_apply_appropriate_event() { @Test public void handle_only_appropriate_command() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); assertTrue(aggregate.isCreateProjectCommandHandled); assertTrue(aggregate.isProjectCreatedEventApplied); @@ -168,15 +168,15 @@ public void handle_only_appropriate_command() { @Test public void handle_appropriate_commands_sequentially() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); assertTrue(aggregate.isCreateProjectCommandHandled); assertTrue(aggregate.isProjectCreatedEventApplied); - aggregate.testDispatch(addTask, COMMAND_CONTEXT); + aggregate.dispatchForTest(addTask, COMMAND_CONTEXT); assertTrue(aggregate.isAddTaskCommandHandled); assertTrue(aggregate.isTaskAddedEventApplied); - aggregate.testDispatch(startProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(startProject, COMMAND_CONTEXT); assertTrue(aggregate.isStartProjectCommandHandled); assertTrue(aggregate.isProjectStartedEventApplied); } @@ -186,7 +186,7 @@ public void throw_exception_if_missing_command_handler() { final TestAggregateForCaseMissingHandlerOrApplier aggregate = new TestAggregateForCaseMissingHandlerOrApplier(ID); - aggregate.testDispatch(addTask, COMMAND_CONTEXT); + aggregate.dispatchForTest(addTask, COMMAND_CONTEXT); } @Test(expected = IllegalStateException.class) @@ -194,7 +194,7 @@ public void throw_exception_if_missing_event_applier_for_non_state_neutral_event final TestAggregateForCaseMissingHandlerOrApplier aggregate = new TestAggregateForCaseMissingHandlerOrApplier(ID); try { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); } catch (IllegalStateException e) { // expected exception assertTrue(aggregate.isCreateProjectCommandHandled); throw e; @@ -228,7 +228,7 @@ public void return_default_state_by_default() { @Test public void return_current_state_after_dispatch() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); final Project state = aggregate.getState(); @@ -238,10 +238,10 @@ public void return_current_state_after_dispatch() { @Test public void return_current_state_after_several_dispatches() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); assertEquals(TestAggregate.STATUS_NEW, aggregate.getState().getStatus()); - aggregate.testDispatch(startProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(startProject, COMMAND_CONTEXT); assertEquals(TestAggregate.STATUS_STARTED, aggregate.getState().getStatus()); } @@ -253,7 +253,7 @@ public void return_non_null_time_when_was_last_modified() { @Test public void return_time_when_was_last_modified() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); final long expectedTimeSec = currentTimeSeconds(); final Timestamp whenLastModified = aggregate.whenModified(); @@ -272,11 +272,11 @@ public void play_events() { @Test public void play_snapshot_event_and_restore_state() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); final Snapshot snapshotNewProject = aggregate.toSnapshot(); - aggregate.testDispatch(startProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(startProject, COMMAND_CONTEXT); assertEquals(TestAggregate.STATUS_STARTED, aggregate.getState().getStatus()); final List events = newArrayList(snapshotToEvent(snapshotNewProject)); @@ -332,7 +332,7 @@ public void clear_event_records_when_commit_after_dispatch() { @Test public void transform_current_state_to_snapshot_event() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); final Snapshot snapshot = aggregate.toSnapshot(); final Project state = fromAny(snapshot.getState()); @@ -344,11 +344,11 @@ public void transform_current_state_to_snapshot_event() { @Test public void restore_state_from_snapshot() { - aggregate.testDispatch(createProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(createProject, COMMAND_CONTEXT); final Snapshot snapshotNewProject = aggregate.toSnapshot(); - aggregate.testDispatch(startProject, COMMAND_CONTEXT); + aggregate.dispatchForTest(startProject, COMMAND_CONTEXT); assertEquals(TestAggregate.STATUS_STARTED, aggregate.getState().getStatus()); aggregate.restore(snapshotNewProject); @@ -460,7 +460,7 @@ public void dispatchCommands(Message... commands) { final UserId userId = UserUtil.newUserId("aggregate_should@spine3.org"); for (Message cmd : commands) { final CommandContext ctx = Commands.createContext(userId, ZoneOffsets.UTC); - testDispatch(cmd, ctx); + dispatchForTest(cmd, ctx); } } } @@ -582,7 +582,7 @@ public void propagate_RuntimeException_when_handler_throws() { final Command command = createProject(); try { - faultyAggregate.testDispatch(command.getMessage(), command.getContext()); + faultyAggregate.dispatchForTest(command.getMessage(), command.getContext()); } catch (RuntimeException e) { @SuppressWarnings("ThrowableResultOfMethodCallIgnored") // We need it for checking. final Throwable cause = Throwables.getRootCause(e); @@ -597,7 +597,7 @@ public void propagate_RuntimeException_when_applier_throws() { final Command command = createProject(); try { - faultyAggregate.testDispatch(command.getMessage(), command.getContext()); + faultyAggregate.dispatchForTest(command.getMessage(), command.getContext()); } catch (RuntimeException e) { @SuppressWarnings("ThrowableResultOfMethodCallIgnored") // ... because we need it for checking. final Throwable cause = Throwables.getRootCause(e); From 4aeca107fd421000c3aaaac7fb383005f2b8736f Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 13:33:07 +0300 Subject: [PATCH 17/26] Fix tests. --- .../ExecutorCommandSchedulerShould.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java index deac3a17df0..e325da9cab7 100644 --- a/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java +++ b/server/src/test/java/org/spine3/server/command/ExecutorCommandSchedulerShould.java @@ -20,6 +20,7 @@ package org.spine3.server.command; +import com.google.common.base.Function; import com.google.protobuf.Duration; import org.junit.After; import org.junit.Before; @@ -28,6 +29,8 @@ import org.spine3.base.CommandContext; import org.spine3.base.Commands; +import javax.annotation.Nullable; + import static org.junit.Assert.fail; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; @@ -51,8 +54,9 @@ public class ExecutorCommandSchedulerShould { @Before public void setUpTest() { - this.scheduler = spy(ExecutorCommandScheduler.class); - this.context = createCommandContext(DELAY); + scheduler = spy(ExecutorCommandScheduler.class); + scheduler.setPostFunction(newStubPostFunction()); + context = createCommandContext(DELAY); } @After @@ -94,4 +98,14 @@ public void throw_exception_if_is_shutdown() { } fail("Must throw an exception as it is shutdown."); } + + private static Function newStubPostFunction() { + return new Function() { + @Nullable + @Override + public Command apply(@Nullable Command input) { + return input; + } + }; + } } From 1b2ecf69f576f9d3532284cc91752b15f3f261ad Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 17:29:22 +0300 Subject: [PATCH 18/26] Merge branch 'repos-post-events' into schedule-commands Conflicts: server/src/main/java/org/spine3/server/command/CommandBus.java server/src/test/java/org/spine3/server/BoundedContextShould.java server/src/test/java/org/spine3/server/BoundedContextTestStubs.java server/src/test/java/org/spine3/server/command/CommandBusShould.java --- .idea/codeStyleSettings.xml | 3 +- .../src/main/java/org/spine3/base/Events.java | 10 -- .../main/java/org/spine3/base/Responses.java | 16 ++- .../java/org/spine3/validate/Validate.java | 2 +- client/src/main/proto/spine/validation.proto | 20 +-- .../java/org/spine3/base/ResponsesShould.java | 24 +++- .../org/spine3/validate/ValidateShould.java | 2 +- examples/build.gradle | 14 +- .../aggregate/server/Application.java | 46 +++---- .../aggregate/server/EventLogger.java | 6 +- .../aggregate/server/OrderAggregate.java | 2 +- .../aggregate/server/OrderRepository.java | 25 ++++ .../examples/failure/TaskAggregate.java | 2 +- .../failures/CannotCancelTaskInProgress.java | 2 +- server/build.gradle | 9 +- .../org/spine3/server/BoundedContext.java | 79 ++--------- .../spine3/server/aggregate/Aggregate.java | 74 ++--------- .../server/aggregate/AggregateRepository.java | 54 +++++--- .../spine3/server/aggregate/EventApplier.java | 50 +++++-- .../org/spine3/server/aggregate/Registry.java | 79 ----------- .../error/DuplicateApplierException.java | 2 +- .../spine3/server/{ => command}/Assign.java | 4 +- .../org/spine3/server/command/CommandBus.java | 102 ++++++-------- .../{ => command}/CommandDispatcher.java | 11 +- .../spine3/server/command/CommandHandler.java | 125 ++++++++++++++++++ .../CommandStatusService.java} | 51 +++---- .../server/command/CommandValidation.java | 10 +- .../server/command/DispatcherRegistry.java | 6 +- .../server/command/HandlerRegistry.java | 57 ++++---- .../spine3/server/command/RegistryUtil.java | 2 +- .../server/entity/EntityEventDispatcher.java | 4 +- .../GetTargetIdFromCommand.java | 4 +- ...mandHandlerAlreadyRegisteredException.java | 2 +- .../server/event/DispatcherRegistry.java | 3 +- .../org/spine3/server/event/EventBus.java | 80 ++++------- .../server/{ => event}/EventDispatcher.java | 4 +- .../org/spine3/server/event/EventHandler.java | 50 +++++++ .../spine3/server/event/HandlerRegistry.java | 61 ++++----- .../spine3/server/{ => event}/Subscribe.java | 4 +- .../{ => failure}/FailureThrowable.java | 3 +- .../package-info.java} | 19 +-- .../spine3/server/procman/ProcessManager.java | 115 +++------------- .../procman/ProcessManagerRepository.java | 23 +++- .../spine3/server/projection/Projection.java | 63 +-------- .../projection/ProjectionRepository.java | 4 +- .../org/spine3/server/reflect/Classes.java | 2 +- .../CommandHandlerMethod.java | 90 +++++++------ .../EventHandlerMethod.java | 74 ++++++----- .../HandlerMethod.java} | 102 +++++++++----- .../org/spine3/server/reflect/MethodMap.java | 115 ++++++++-------- .../spine3/server/reflect/MethodRegistry.java | 114 ++++++++++++++++ .../org/spine3/server/reflect/Methods.java | 96 -------------- .../spine3/server/storage/CommandStorage.java | 3 +- .../org/spine3/server}/type/CommandClass.java | 4 +- .../org/spine3/server}/type/EventClass.java | 4 +- .../org/spine3/server/type/MessageClass.java | 10 +- .../org/spine3/server/type/package-info.java | 28 ++++ .../validate/ByteStringFieldValidator.java | 2 +- .../server/validate/FieldValidator.java | 8 +- .../validate/FloatFieldValidatorBase.java | 2 +- .../validate/MessageFieldValidator.java | 18 +-- .../server/validate/MessageValidator.java | 2 +- .../server/validate/NumberFieldValidator.java | 24 ++-- .../server/validate/StringFieldValidator.java | 8 +- .../spine3/server/BoundedContextShould.java | 114 +++++++--------- .../server/BoundedContextTestStubs.java | 10 +- .../aggregate/AggregateRepositoryShould.java | 11 +- .../server/aggregate/AggregateShould.java | 2 +- .../server/command/CommandBusShould.java | 100 +++++++------- .../spine3/server/event/EventBusShould.java | 14 +- .../ProcessManagerRepositoryShould.java | 40 +++--- .../server/procman/ProcessManagerShould.java | 4 +- .../ProjectionRepositoryShould.java | 4 +- .../server/projection/ProjectionShould.java | 2 +- .../MessageHandlerMethodShould.java | 43 +++--- .../server/reflect/MethodMapShould.java | 5 +- .../server/reflect/UtilitiesShould.java | 1 - .../spine3/server}/type/EventClassShould.java | 2 +- .../server/type/MessageClassShould.java | 16 ++- .../validate/FieldValidatorFactoryShould.java | 6 +- .../validate/MessageValidatorShould.java | 54 ++++---- .../org/spine3/testdata/ProjectAggregate.java | 2 +- .../msg/commands.proto | 4 +- .../msg/messages.proto | 4 +- .../msg/stub_aggregate.proto | 4 +- values/build.gradle | 9 +- 86 files changed, 1220 insertions(+), 1265 deletions(-) delete mode 100644 server/src/main/java/org/spine3/server/aggregate/Registry.java rename server/src/main/java/org/spine3/server/{ => command}/Assign.java (96%) rename server/src/main/java/org/spine3/server/{ => command}/CommandDispatcher.java (82%) create mode 100644 server/src/main/java/org/spine3/server/command/CommandHandler.java rename server/src/main/java/org/spine3/server/{CommandHandler.java => command/CommandStatusService.java} (51%) rename server/src/main/java/org/spine3/server/{command => entity}/GetTargetIdFromCommand.java (93%) rename server/src/main/java/org/spine3/server/{ => event}/EventDispatcher.java (95%) create mode 100644 server/src/main/java/org/spine3/server/event/EventHandler.java rename server/src/main/java/org/spine3/server/{ => event}/Subscribe.java (96%) rename server/src/main/java/org/spine3/server/{ => failure}/FailureThrowable.java (96%) rename server/src/main/java/org/spine3/server/{EventHandler.java => failure/package-info.java} (70%) rename server/src/main/java/org/spine3/server/{internal => reflect}/CommandHandlerMethod.java (74%) rename server/src/main/java/org/spine3/server/{internal => reflect}/EventHandlerMethod.java (73%) rename server/src/main/java/org/spine3/server/{internal/MessageHandlerMethod.java => reflect/HandlerMethod.java} (64%) create mode 100644 server/src/main/java/org/spine3/server/reflect/MethodRegistry.java delete mode 100644 server/src/main/java/org/spine3/server/reflect/Methods.java rename {client/src/main/java/org/spine3 => server/src/main/java/org/spine3/server}/type/CommandClass.java (97%) rename {client/src/main/java/org/spine3 => server/src/main/java/org/spine3/server}/type/EventClass.java (96%) rename client/src/main/java/org/spine3/type/ClassTypeValue.java => server/src/main/java/org/spine3/server/type/MessageClass.java (84%) create mode 100644 server/src/main/java/org/spine3/server/type/package-info.java rename server/src/test/java/org/spine3/server/{internal => reflect}/MessageHandlerMethodShould.java (76%) rename {client/src/test/java/org/spine3 => server/src/test/java/org/spine3/server}/type/EventClassShould.java (97%) rename client/src/test/java/org/spine3/type/ClassTypeValueShould.java => server/src/test/java/org/spine3/server/type/MessageClassShould.java (77%) rename server/src/test/proto/spine/test/{validation => validate}/msg/commands.proto (95%) rename server/src/test/proto/spine/test/{validation => validate}/msg/messages.proto (97%) rename server/src/test/proto/spine/test/{validation => validate}/msg/stub_aggregate.proto (93%) diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml index a422528b53a..258e4a12c25 100644 --- a/.idea/codeStyleSettings.xml +++ b/.idea/codeStyleSettings.xml @@ -9,11 +9,12 @@

Logs warning for the methods with a non-private modifier. * * @param methods the map of methods to check - * @see MessageHandlerMethod#log() + * @see HandlerMethod#log() */ /* package */ static void checkModifiers(Iterable methods) { for (Method method : methods) { @@ -76,6 +75,41 @@ protected R invoke(Message message) throws InvocationTargetException { } } + public static HandlerMethod.Factory factory() { + return Factory.instance(); + } + + /** + * The factory for filtering methods that match {@code EventApplier} specification. + */ + private static class Factory implements HandlerMethod.Factory { + + @Override + public Class getMethodClass() { + return EventApplier.class; + } + + @Override + public EventApplier create(Method method) { + return new EventApplier(method); + } + + @Override + public Predicate getPredicate() { + return PREDICATE; + } + + private enum Singleton { + INSTANCE; + @SuppressWarnings("NonSerializableFieldInSerializableClass") + private final Factory value = new Factory(); + } + + private static Factory instance() { + return Singleton.INSTANCE.value; + } + } + /** * The predicate for filtering event applier methods. */ diff --git a/server/src/main/java/org/spine3/server/aggregate/Registry.java b/server/src/main/java/org/spine3/server/aggregate/Registry.java deleted file mode 100644 index 17520909e97..00000000000 --- a/server/src/main/java/org/spine3/server/aggregate/Registry.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016, TeamDev Ltd. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.spine3.server.aggregate; - -import org.spine3.server.internal.CommandHandlerMethod; -import org.spine3.server.reflect.MethodMap; - -import javax.annotation.CheckReturnValue; - -/** - * The registry of method maps for all aggregate classes. - * - *

This registry is used for caching command handlers and event appliers. - * Aggregates register their classes in {@link Aggregate#init()} method. - * - * @author Alexander Yevsyukov - */ -/* package */ class Registry { - - private final MethodMap.Registry commandHandlers = new MethodMap.Registry<>(); - - private final MethodMap.Registry eventAppliers = new MethodMap.Registry<>(); - - /* package */ void register(Class clazz) { - commandHandlers.register(clazz, CommandHandlerMethod.PREDICATE); - CommandHandlerMethod.checkModifiers(commandHandlers.get(clazz).values()); - - eventAppliers.register(clazz, EventApplier.PREDICATE); - EventApplier.checkModifiers(eventAppliers.get(clazz).values()); - } - - @CheckReturnValue - /* package */ boolean contains(Class clazz) { - final boolean result = commandHandlers.contains(clazz); - return result; - } - - @CheckReturnValue - /* package */ MethodMap getCommandHandlers(Class clazz) { - final MethodMap result = commandHandlers.get(clazz); - return result; - } - - @CheckReturnValue - /* package */ MethodMap getEventAppliers(Class clazz) { - final MethodMap result = eventAppliers.get(clazz); - return result; - } - - @CheckReturnValue - /* package */ static Registry getInstance() { - return Singleton.INSTANCE.value; - } - - private enum Singleton { - INSTANCE; - @SuppressWarnings("NonSerializableFieldInSerializableClass") - private final Registry value = new Registry(); - } -} - diff --git a/server/src/main/java/org/spine3/server/aggregate/error/DuplicateApplierException.java b/server/src/main/java/org/spine3/server/aggregate/error/DuplicateApplierException.java index f48f7f70488..98470039e9d 100644 --- a/server/src/main/java/org/spine3/server/aggregate/error/DuplicateApplierException.java +++ b/server/src/main/java/org/spine3/server/aggregate/error/DuplicateApplierException.java @@ -19,7 +19,7 @@ */ package org.spine3.server.aggregate.error; -import org.spine3.type.EventClass; +import org.spine3.server.type.EventClass; /** * Exception that is thrown when more than one applier diff --git a/server/src/main/java/org/spine3/server/Assign.java b/server/src/main/java/org/spine3/server/command/Assign.java similarity index 96% rename from server/src/main/java/org/spine3/server/Assign.java rename to server/src/main/java/org/spine3/server/command/Assign.java index d1f1a194c01..574cb631ac2 100644 --- a/server/src/main/java/org/spine3/server/Assign.java +++ b/server/src/main/java/org/spine3/server/command/Assign.java @@ -18,9 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; - -import org.spine3.server.command.CommandBus; +package org.spine3.server.command; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index 002d65f2fdf..646a2c1e96c 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -28,24 +28,19 @@ import org.spine3.base.CommandContext; import org.spine3.base.CommandId; import org.spine3.base.Errors; -import org.spine3.base.Event; import org.spine3.base.Response; import org.spine3.base.Responses; -import org.spine3.server.CommandDispatcher; -import org.spine3.server.CommandHandler; -import org.spine3.server.FailureThrowable; import org.spine3.server.error.UnsupportedCommandException; -import org.spine3.server.internal.CommandHandlerMethod; +import org.spine3.server.failure.FailureThrowable; +import org.spine3.server.type.CommandClass; import org.spine3.server.validate.MessageValidator; -import org.spine3.type.CommandClass; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.validate.options.ConstraintViolation; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Collections.emptyList; import static org.spine3.base.Commands.*; import static org.spine3.server.command.CommandValidation.invalidCommand; import static org.spine3.server.command.CommandValidation.unsupportedCommand; @@ -67,9 +62,9 @@ public class CommandBus implements AutoCloseable { @Nullable private final CommandScheduler scheduler; - - private final CommandStatusHelper commandStatus; + private ProblemLog problemLog = new ProblemLog(); + private final CommandStatusService commandStatusService; private CommandBus(Builder builder) { commandStore = builder.getCommandStore(); @@ -77,7 +72,7 @@ private CommandBus(Builder builder) { if (scheduler != null) { scheduler.setPostFunction(newPostFunction()); } - commandStatus = new CommandStatusHelper(commandStore); + commandStatusService = new CommandStatusService(commandStore); } /** @@ -166,69 +161,78 @@ private boolean isUnsupportedCommand(CommandClass commandClass) { /** * Directs a command request to the corresponding handler. * - * @param command the command request to be processed - * @return a list of the events as the result of handling the command + * @param command the command to be processed * @throws UnsupportedCommandException if there is neither handler nor dispatcher registered for * the class of the passed command */ - public List post(Command command) { - //TODO:2016-01-24:alexander.yevsyukov: Do not return value. + public void post(Command command) { checkNotDefault(command); if (isScheduled(command)) { schedule(command); - return emptyList(); + return; } store(command); final CommandClass commandClass = CommandClass.of(command); if (isDispatcherRegistered(commandClass)) { - return dispatch(command); + dispatch(command); + return; } if (isHandlerRegistered(commandClass)) { final Message message = getMessage(command); final CommandContext context = command.getContext(); - return invokeHandler(message, context); + invokeHandler(message, context); + return; } throw new UnsupportedCommandException(getMessage(command)); } - private List dispatch(Command command) { + /** + * Obtains the instance of the {@link CommandStatusService} associated with this command bus. + */ + public CommandStatusService getCommandStatusService() { + return commandStatusService; + } + + private void dispatch(Command command) { final CommandClass commandClass = CommandClass.of(command); final CommandDispatcher dispatcher = getDispatcher(commandClass); - List result = emptyList(); + final CommandId commandId = command.getContext().getCommandId(); try { - result = dispatcher.dispatch(command); + dispatcher.dispatch(command); } catch (Exception e) { problemLog.errorDispatching(e, command); - commandStatus.setToError(command.getContext().getCommandId(), e); + commandStatusService.setToError(commandId, e); } - return result; } - private List invokeHandler(Message msg, CommandContext context) { + private void invokeHandler(Message msg, CommandContext context) { final CommandClass commandClass = CommandClass.of(msg); - final CommandHandlerMethod method = getHandler(commandClass); - List result = emptyList(); + final CommandHandler handler = getHandler(commandClass); + final CommandId commandId = context.getCommandId(); try { - result = method.invoke(msg, context); - commandStatus.setOk(context.getCommandId()); + handler.handle(msg, context); + commandStatusService.setOk(commandId); } catch (InvocationTargetException e) { - final CommandId commandId = context.getCommandId(); final Throwable cause = e.getCause(); //noinspection ChainOfInstanceofChecks if (cause instanceof Exception) { final Exception exception = (Exception) cause; problemLog.errorHandling(exception, msg, commandId); - commandStatus.setToError(commandId, exception); + commandStatusService.setToError(commandId, exception); } else if (cause instanceof FailureThrowable){ final FailureThrowable failure = (FailureThrowable) cause; problemLog.failureHandling(failure, msg, commandId); - commandStatus.setToFailure(commandId, failure); + commandStatusService.setToFailure(commandId, failure); } else { problemLog.errorHandlingUnknown(cause, msg, commandId); - commandStatus.setToError(commandId, Errors.fromThrowable(cause)); + commandStatusService.setToError(commandId, Errors.fromThrowable(cause)); } } - return result; + } + + private CommandHandler getHandler(CommandClass commandClass) { + final CommandHandler handler = handlerRegistry.getHandler(commandClass); + return handler; } private void schedule(Command command) { @@ -273,34 +277,6 @@ private void schedule(Command command) { this.problemLog = problemLog; } - /** - * The helper class for updating command status. - */ - private static class CommandStatusHelper { - - private final CommandStore commandStore; - - private CommandStatusHelper(CommandStore commandStore) { - this.commandStore = commandStore; - } - - private void setOk(CommandId commandId) { - commandStore.setCommandStatusOk(commandId); - } - - private void setToError(CommandId commandId, Exception exception) { - commandStore.updateStatus(commandId, exception); - } - - private void setToFailure(CommandId commandId, FailureThrowable failure) { - commandStore.updateStatus(commandId, failure.toMessage()); - } - - private void setToError(CommandId commandId, org.spine3.base.Error error) { - commandStore.updateStatus(commandId, error); - } - } - private void store(Command request) { commandStore.store(request); } @@ -319,10 +295,6 @@ private CommandDispatcher getDispatcher(CommandClass commandClass) { return dispatcherRegistry.getDispatcher(commandClass); } - private CommandHandlerMethod getHandler(CommandClass cls) { - return handlerRegistry.getHandler(cls); - } - private Function newPostFunction() { final Function function = new Function() { @Nullable diff --git a/server/src/main/java/org/spine3/server/CommandDispatcher.java b/server/src/main/java/org/spine3/server/command/CommandDispatcher.java similarity index 82% rename from server/src/main/java/org/spine3/server/CommandDispatcher.java rename to server/src/main/java/org/spine3/server/command/CommandDispatcher.java index 48f1d630cdf..90863cfbaf8 100644 --- a/server/src/main/java/org/spine3/server/CommandDispatcher.java +++ b/server/src/main/java/org/spine3/server/command/CommandDispatcher.java @@ -18,13 +18,11 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; +package org.spine3.server.command; import org.spine3.base.Command; -import org.spine3.base.Event; -import org.spine3.type.CommandClass; +import org.spine3.server.type.CommandClass; -import java.util.List; import java.util.Set; /** @@ -50,9 +48,6 @@ public interface CommandDispatcher { /** * Dispatches the command to its handler. */ - List dispatch(Command request) throws Exception; - //TODO:2016-01-24:alexander.yevsyukov: Do not return results to the CommandBus. - - //TODO:2016-01-24:alexander.yevsyukov: Do handle exceptions that can be thrown at CommandBus side. + void dispatch(Command request) throws Exception; } diff --git a/server/src/main/java/org/spine3/server/command/CommandHandler.java b/server/src/main/java/org/spine3/server/command/CommandHandler.java new file mode 100644 index 00000000000..a9b2406e52d --- /dev/null +++ b/server/src/main/java/org/spine3/server/command/CommandHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.command; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Empty; +import com.google.protobuf.Message; +import com.google.protobuf.util.TimeUtil; +import org.spine3.base.CommandContext; +import org.spine3.base.Event; +import org.spine3.base.EventContext; +import org.spine3.base.EventId; +import org.spine3.base.Events; +import org.spine3.base.Identifiers; +import org.spine3.server.entity.Entity; +import org.spine3.server.event.EventBus; +import org.spine3.server.failure.FailureThrowable; +import org.spine3.server.reflect.CommandHandlerMethod; +import org.spine3.server.reflect.MethodRegistry; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +/** + * The abstract base for classes that expose command handling methods + * and post their results to {@link EventBus}. + * + *

A command handler is responsible for: + *

    + *
  1. Changing the state of the business model
  2. + *
  3. Producing corresponding events.
  4. + *
  5. Posting events to {@code EventBus}.
  6. + *
+ * + *

Event messages are returned as values of command handling methods. + * + *

Command handling methods

+ *

A command handling method is a {@code public} method that accepts two parameters. + * The first parameter is a command message. The second parameter is {@link CommandContext}. + * + *

The method returns an event message of the specific type, or {@code List} of messages + * if it produces more than one event. + * + *

The method may throw one or more throwables derived from {@link FailureThrowable}. + * Throwing a {@code FailureThrowable} indicates that the passed command cannot be handled + * because of a business failure, which can be obtained via {@link FailureThrowable#getFailure()}. + * + * @author Alexander Yevsyukov + * @see CommandDispatcher + */ +public abstract class CommandHandler extends Entity { + + private final EventBus eventBus; + + /** + * Creates a new instance of the command handler. + * + * @param id the identifier, which will be used for recognizing events produced by this handler + * @param eventBus the Event Bus to post events generated by this handler + */ + protected CommandHandler(String id, EventBus eventBus) { + super(id); + this.eventBus = eventBus; + } + + public void handle(Message commandMessage, CommandContext context) throws InvocationTargetException { + final CommandHandlerMethod method = getHandlerMethod(commandMessage.getClass()); + + final List eventMessages = method.invoke(this, commandMessage, context); + + final List events = toEvents(eventMessages, context); + postEvents(events); + } + + /* package */ CommandHandlerMethod getHandlerMethod(Class commandClass) { + return MethodRegistry.getInstance().get(getClass(), commandClass, CommandHandlerMethod.factory()); + } + + private List toEvents(Iterable eventMessages, CommandContext context) { + final ImmutableList.Builder builder = ImmutableList.builder(); + for (Message eventMessage : eventMessages) { + final EventContext eventContext = createEventContext(context); + final Event event = Events.createEvent(eventMessage, eventContext); + builder.add(event); + } + return builder.build(); + } + + private EventContext createEventContext(CommandContext commandContext) { + final EventId eventId = Events.generateId(); + final EventContext.Builder builder = EventContext.newBuilder() + .setEventId(eventId) + .setCommandContext(commandContext) + .setTimestamp(TimeUtil.getCurrentTime()) + .setProducerId(Identifiers.idToAny(getId())); + return builder.build(); + } + + /** + * Posts passed events to {@link EventBus}. + */ + private void postEvents(Iterable events) { + for (Event event : events) { + eventBus.post(event); + } + } +} diff --git a/server/src/main/java/org/spine3/server/CommandHandler.java b/server/src/main/java/org/spine3/server/command/CommandStatusService.java similarity index 51% rename from server/src/main/java/org/spine3/server/CommandHandler.java rename to server/src/main/java/org/spine3/server/command/CommandStatusService.java index e6e43f42750..1e8e150f1cb 100644 --- a/server/src/main/java/org/spine3/server/CommandHandler.java +++ b/server/src/main/java/org/spine3/server/command/CommandStatusService.java @@ -18,34 +18,37 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; +package org.spine3.server.command; -import org.spine3.base.CommandContext; +import org.spine3.base.CommandId; +import org.spine3.server.failure.FailureThrowable; /** - * The marker interface for classes that expose command handling methods. - * - *

A command handler is responsible for: - *

    - *
  1. Changing the state of the business model
  2. - *
  3. Producing corresponding events.
  4. - *
- * - *

Events are returned as values of command handling methods. - * - *

Command handling methods

- *

A command handling method is a {@code public} method that accepts two parameters. - * The first parameter is a command message. The second parameter is {@link CommandContext}. - * - *

The method returns an event message of the specific type, or {@code List} of messages - * if it produces more than one event. - * - *

The method may throw one or more throwables derived from {@link FailureThrowable}. - * Throwing a {@code FailureThrowable} indicates that the passed command cannot be handled - * because of a business failure. + * The service for updating a status of a command. * * @author Alexander Yevsyukov - * @see CommandDispatcher */ -public interface CommandHandler { +public class CommandStatusService { + + private final CommandStore commandStore; + + /* package */ CommandStatusService(CommandStore commandStore) { + this.commandStore = commandStore; + } + + public void setOk(CommandId commandId) { + commandStore.setCommandStatusOk(commandId); + } + + public void setToError(CommandId commandId, Exception exception) { + commandStore.updateStatus(commandId, exception); + } + + public void setToFailure(CommandId commandId, FailureThrowable failure) { + commandStore.updateStatus(commandId, failure.toMessage()); + } + + public void setToError(CommandId commandId, org.spine3.base.Error error) { + commandStore.updateStatus(commandId, error); + } } diff --git a/server/src/main/java/org/spine3/server/command/CommandValidation.java b/server/src/main/java/org/spine3/server/command/CommandValidation.java index 08851323940..a20af63db79 100644 --- a/server/src/main/java/org/spine3/server/command/CommandValidation.java +++ b/server/src/main/java/org/spine3/server/command/CommandValidation.java @@ -29,7 +29,7 @@ import org.spine3.base.Failure; import org.spine3.base.Response; import org.spine3.base.ValidationFailure; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.validate.options.ConstraintViolation; import java.util.List; import java.util.Map; @@ -70,14 +70,6 @@ public static Response unsupportedCommand(Message command) { return response; } - public static boolean isUnsupportedCommand(Response response) { - if (response.getStatusCase() == Response.StatusCase.ERROR) { - final Error error = response.getError(); - return error.getCode() == CommandValidationError.UNSUPPORTED_COMMAND.getNumber(); - } - return false; - } - /** * Creates a {@code Response} for getting a command with invalid fields (e.g., marked as "required" but not set). * diff --git a/server/src/main/java/org/spine3/server/command/DispatcherRegistry.java b/server/src/main/java/org/spine3/server/command/DispatcherRegistry.java index 95ab446a29c..525a49d9db4 100644 --- a/server/src/main/java/org/spine3/server/command/DispatcherRegistry.java +++ b/server/src/main/java/org/spine3/server/command/DispatcherRegistry.java @@ -24,11 +24,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.protobuf.Message; -import org.spine3.server.CommandDispatcher; -import org.spine3.server.CommandHandler; -import org.spine3.server.internal.CommandHandlerMethod; import org.spine3.server.reflect.Classes; -import org.spine3.type.CommandClass; +import org.spine3.server.reflect.CommandHandlerMethod; +import org.spine3.server.type.CommandClass; import java.util.Map; import java.util.Set; diff --git a/server/src/main/java/org/spine3/server/command/HandlerRegistry.java b/server/src/main/java/org/spine3/server/command/HandlerRegistry.java index b92d45c2b08..d730e4fc766 100644 --- a/server/src/main/java/org/spine3/server/command/HandlerRegistry.java +++ b/server/src/main/java/org/spine3/server/command/HandlerRegistry.java @@ -23,12 +23,11 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.protobuf.Message; -import org.spine3.server.CommandDispatcher; -import org.spine3.server.CommandHandler; import org.spine3.server.error.CommandHandlerAlreadyRegisteredException; -import org.spine3.server.internal.CommandHandlerMethod; -import org.spine3.server.internal.MessageHandlerMethod; -import org.spine3.type.CommandClass; +import org.spine3.server.reflect.CommandHandlerMethod; +import org.spine3.server.reflect.HandlerMethod; +import org.spine3.server.reflect.MethodMap; +import org.spine3.server.type.CommandClass; import javax.annotation.CheckReturnValue; import java.util.Map; @@ -45,42 +44,43 @@ */ /* package */ class HandlerRegistry { - private final Map handlersByClass = Maps.newConcurrentMap(); + private final Map handlersByClass = Maps.newConcurrentMap(); /* package */ void register(CommandHandler object) { checkNotNull(object); - final Map handlers = CommandHandlerMethod.scan(object); + final MethodMap handlers = CommandHandlerMethod.scan(object); if (handlers.isEmpty()) { throw new IllegalArgumentException("No command handler methods found in :" + object); } - registerMap(handlers); + registerMap(object, handlers); } /* package */ void unregister(CommandHandler object) { checkNotNull(object); - final Map subscribers = CommandHandlerMethod.scan(object); - unregisterMap(subscribers); + final MethodMap handlers = CommandHandlerMethod.scan(object); + unregisterMap(handlers); } /** * Registers the passed handlers with the dispatcher. * - * @param handlers map from command classes to corresponding handlers + * @param object the command handler + * @param methods map from command classes to corresponding handlers */ - private void registerMap(Map handlers) { - checkDuplicates(handlers); - putAll(handlers); + private void registerMap(CommandHandler object, MethodMap methods) { + checkDuplicates(methods); + putAll(object, methods); } - private void unregisterMap(Map handlers) { - for (Map.Entry entry : handlers.entrySet()) { - final CommandClass commandClass = entry.getKey(); + private void unregisterMap(MethodMap methods) { + for (Map.Entry, CommandHandlerMethod> entry : methods.entrySet()) { + final CommandClass commandClass = CommandClass.of(entry.getKey()); if (handlerRegistered(commandClass)) { - final CommandHandlerMethod registered = getHandler(commandClass); + final CommandHandlerMethod registered = getHandler(commandClass).getHandlerMethod(commandClass.value()); final CommandHandlerMethod passed = entry.getValue(); if (registered.equals(passed)) { removeFor(commandClass); @@ -93,12 +93,12 @@ private void removeFor(CommandClass commandClass) { handlersByClass.remove(commandClass); } - private void checkDuplicates(Map handlers) { - for (Map.Entry entry : handlers.entrySet()) { - final CommandClass commandClass = entry.getKey(); + private void checkDuplicates(MethodMap handlers) { + for (Map.Entry, CommandHandlerMethod> entry : handlers.entrySet()) { + final CommandClass commandClass = CommandClass.of(entry.getKey()); if (handlerRegistered(commandClass)) { - final MessageHandlerMethod alreadyRegistered = getHandler(commandClass); + final HandlerMethod alreadyRegistered = getHandler(commandClass).getHandlerMethod(commandClass.value()); throw new CommandHandlerAlreadyRegisteredException(commandClass, alreadyRegistered.getFullName(), entry.getValue().getFullName()); @@ -111,12 +111,14 @@ private void checkDuplicates(Map handlers) { return handlersByClass.containsKey(cls); } - /* package */ CommandHandlerMethod getHandler(CommandClass cls) { + /* package */ CommandHandler getHandler(CommandClass cls) { return handlersByClass.get(cls); } - private void putAll(Map subscribers) { - handlersByClass.putAll(subscribers); + private void putAll(CommandHandler object, MethodMap methods) { + for (Class commandClass : methods.keySet()) { + handlersByClass.put(CommandClass.of(commandClass), object); + } } /* package */ void unregisterAll() { @@ -125,11 +127,6 @@ private void putAll(Map subscribers) { } } - /* package */ boolean hasHandlerFor(Message command) { - final CommandHandlerMethod method = getHandler(CommandClass.of(command)); - return method != null; - } - /** * Ensures that no handlers already registered for the command classes of the passed dispatcher. * diff --git a/server/src/main/java/org/spine3/server/command/RegistryUtil.java b/server/src/main/java/org/spine3/server/command/RegistryUtil.java index b6930d9aa4e..4fb9b849a3b 100644 --- a/server/src/main/java/org/spine3/server/command/RegistryUtil.java +++ b/server/src/main/java/org/spine3/server/command/RegistryUtil.java @@ -21,7 +21,7 @@ package org.spine3.server.command; import com.google.common.base.Joiner; -import org.spine3.type.CommandClass; +import org.spine3.server.type.CommandClass; import java.util.Set; diff --git a/server/src/main/java/org/spine3/server/entity/EntityEventDispatcher.java b/server/src/main/java/org/spine3/server/entity/EntityEventDispatcher.java index d087a43d8f6..994d7441cf0 100644 --- a/server/src/main/java/org/spine3/server/entity/EntityEventDispatcher.java +++ b/server/src/main/java/org/spine3/server/entity/EntityEventDispatcher.java @@ -22,8 +22,8 @@ import com.google.protobuf.Message; import org.spine3.base.EventContext; -import org.spine3.server.EventDispatcher; -import org.spine3.type.EventClass; +import org.spine3.server.event.EventDispatcher; +import org.spine3.server.type.EventClass; /** * Delivers events to handlers (which are supposed to be entities). diff --git a/server/src/main/java/org/spine3/server/command/GetTargetIdFromCommand.java b/server/src/main/java/org/spine3/server/entity/GetTargetIdFromCommand.java similarity index 93% rename from server/src/main/java/org/spine3/server/command/GetTargetIdFromCommand.java rename to server/src/main/java/org/spine3/server/entity/GetTargetIdFromCommand.java index cadceab3fe7..c40a3c67d72 100644 --- a/server/src/main/java/org/spine3/server/command/GetTargetIdFromCommand.java +++ b/server/src/main/java/org/spine3/server/entity/GetTargetIdFromCommand.java @@ -18,13 +18,11 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server.command; +package org.spine3.server.entity; import com.google.protobuf.Message; import org.spine3.base.CommandContext; import org.spine3.base.Identifiers; -import org.spine3.server.entity.Entity; -import org.spine3.server.entity.GetIdByFieldIndex; /** * Obtains a command target {@link Entity} ID based on a command {@link Message} and context. diff --git a/server/src/main/java/org/spine3/server/error/CommandHandlerAlreadyRegisteredException.java b/server/src/main/java/org/spine3/server/error/CommandHandlerAlreadyRegisteredException.java index 01f5a0c8c16..74e8c04fac5 100644 --- a/server/src/main/java/org/spine3/server/error/CommandHandlerAlreadyRegisteredException.java +++ b/server/src/main/java/org/spine3/server/error/CommandHandlerAlreadyRegisteredException.java @@ -19,7 +19,7 @@ */ package org.spine3.server.error; -import org.spine3.type.CommandClass; +import org.spine3.server.type.CommandClass; /** * Exception that is thrown when more than one handler diff --git a/server/src/main/java/org/spine3/server/event/DispatcherRegistry.java b/server/src/main/java/org/spine3/server/event/DispatcherRegistry.java index 8a50a9ec8a5..2ae5e814e26 100644 --- a/server/src/main/java/org/spine3/server/event/DispatcherRegistry.java +++ b/server/src/main/java/org/spine3/server/event/DispatcherRegistry.java @@ -21,8 +21,7 @@ package org.spine3.server.event; import com.google.common.collect.HashMultimap; -import org.spine3.server.EventDispatcher; -import org.spine3.type.EventClass; +import org.spine3.server.type.EventClass; import java.util.Set; diff --git a/server/src/main/java/org/spine3/server/event/EventBus.java b/server/src/main/java/org/spine3/server/event/EventBus.java index 1b3c6b2560a..9240794612e 100644 --- a/server/src/main/java/org/spine3/server/event/EventBus.java +++ b/server/src/main/java/org/spine3/server/event/EventBus.java @@ -20,7 +20,6 @@ package org.spine3.server.event; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Message; import org.slf4j.Logger; @@ -28,21 +27,15 @@ import org.spine3.base.Event; import org.spine3.base.EventContext; import org.spine3.base.Events; -import org.spine3.server.EventDispatcher; -import org.spine3.server.EventHandler; -import org.spine3.server.Subscribe; import org.spine3.server.aggregate.AggregateRepository; -import org.spine3.server.internal.EventHandlerMethod; import org.spine3.server.procman.ProcessManager; -import org.spine3.type.EventClass; +import org.spine3.server.type.EventClass; import java.lang.reflect.InvocationTargetException; import java.util.Collection; -import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** @@ -133,6 +126,15 @@ public static EventBus newInstance(EventStore eventStore, Executor executor) { return result; } + /** + * Determines the class of the event from the passed event record. + */ + public static EventClass getEventClass(Event event) { + final Message message = Events.getMessage(event); + final EventClass result = EventClass.of(message); + return result; + } + /** * Subscribes the event handler to receive events from the bus. * @@ -144,17 +146,7 @@ public static EventBus newInstance(EventStore eventStore, Executor executor) { */ public void subscribe(EventHandler object) { checkNotNull(object); - - final Map handlers = EventHandlerMethod.scan(object); - final boolean handlersEmpty = handlers.isEmpty(); - checkHandlersNotEmpty(object, handlersEmpty); - if (!handlersEmpty) { - handlerRegistry.subscribe(handlers); - } - } - - private static void checkHandlersNotEmpty(Object object, boolean handlersEmpty) { - checkArgument(!handlersEmpty, "No event subscriber methods found in %s", object); + handlerRegistry.subscribe(object); } /** @@ -171,13 +163,9 @@ public void register(EventDispatcher dispatcher) { } @VisibleForTesting - /* package */ Set getHandlers(EventClass eventClass) { - final Collection handlers = handlerRegistry.getSubscribers(eventClass); - final ImmutableSet.Builder result = ImmutableSet.builder(); - for (EventHandlerMethod handler : handlers) { - result.add(handler.getTarget()); - } - return result.build(); + /* package */ Collection getHandlers(EventClass eventClass) { + final Collection result = handlerRegistry.getSubscribers(eventClass); + return result; } @VisibleForTesting @@ -191,15 +179,9 @@ public void register(EventDispatcher dispatcher) { * @param object the object whose methods should be unregistered * @throws IllegalArgumentException if the object was not previously registered */ - public void unsubscribe(Object object) { + public void unsubscribe(EventHandler object) { checkNotNull(object); - - final Map handlers = EventHandlerMethod.scan(object); - final boolean handlersEmpty = handlers.isEmpty(); - checkHandlersNotEmpty(object, handlersEmpty); - if (!handlersEmpty) { - unsubscribeMap(handlers); - } + handlerRegistry.usubscribe(object); } /** @@ -209,16 +191,7 @@ public void unregister(EventDispatcher dispatcher) { dispatcherRegistry.unregister(dispatcher); } - /** - * Removes passed event handlers from the bus. - * - * @param handlers a map of the event handlers to remove - */ - private void unsubscribeMap(Map handlers) { - handlerRegistry.unsubscribe(handlers); - } - - private Collection getSubscribers(EventClass c) { + private Collection getSubscribers(EventClass c) { return handlerRegistry.getSubscribers(c); } @@ -248,7 +221,7 @@ public void post(Event event) { } private void callDispatchers(Event event) { - final EventClass eventClass = Events.getEventClass(event); + final EventClass eventClass = getEventClass(event); final Collection dispatchers = dispatcherRegistry.getDispatchers(eventClass); for (EventDispatcher dispatcher : dispatchers) { dispatcher.dispatch(event); @@ -256,14 +229,14 @@ private void callDispatchers(Event event) { } private void invokeHandlers(Message event, EventContext context) { - final Collection handlers = getSubscribers(EventClass.of(event)); + final Collection handlers = getSubscribers(EventClass.of(event)); if (handlers.isEmpty()) { handleDeadEvent(event); return; } - for (EventHandlerMethod handler : handlers) { + for (EventHandler handler : handlers) { invokeHandler(handler, event, context); } } @@ -272,14 +245,14 @@ private void store(Event event) { eventStore.append(event); } - private void invokeHandler(final EventHandlerMethod handler, final Message event, final EventContext context) { + private void invokeHandler(final EventHandler target, final Message event, final EventContext context) { executor.execute(new Runnable() { @Override public void run() { try { - handler.invoke(event, context); + target.handle(event, context); } catch (InvocationTargetException e) { - processHandlerException(handler, e); + processHandlerException(e, event, context); } } }); @@ -289,8 +262,11 @@ private static void handleDeadEvent(Message event) { log().warn("No handler defined for event class: " + event.getClass().getName()); } - private static void processHandlerException(EventHandlerMethod handler, InvocationTargetException e) { - log().error("Exception invoking method: " + handler.getFullName(), e); + private static void processHandlerException(InvocationTargetException e, + Message eventMessage, + EventContext eventContext) { + log().error("Exception handling event. Event message: {}, context: {}, cause: {}", + eventMessage, eventContext, e.getCause()); } @Override diff --git a/server/src/main/java/org/spine3/server/EventDispatcher.java b/server/src/main/java/org/spine3/server/event/EventDispatcher.java similarity index 95% rename from server/src/main/java/org/spine3/server/EventDispatcher.java rename to server/src/main/java/org/spine3/server/event/EventDispatcher.java index f7e6d3b29b0..ec0cff17ac8 100644 --- a/server/src/main/java/org/spine3/server/EventDispatcher.java +++ b/server/src/main/java/org/spine3/server/event/EventDispatcher.java @@ -18,10 +18,10 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; +package org.spine3.server.event; import org.spine3.base.Event; -import org.spine3.type.EventClass; +import org.spine3.server.type.EventClass; import java.util.Set; diff --git a/server/src/main/java/org/spine3/server/event/EventHandler.java b/server/src/main/java/org/spine3/server/event/EventHandler.java new file mode 100644 index 00000000000..eae65f9ec65 --- /dev/null +++ b/server/src/main/java/org/spine3/server/event/EventHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.event; + +import com.google.protobuf.Message; +import org.spine3.base.EventContext; +import org.spine3.server.reflect.EventHandlerMethod; +import org.spine3.server.reflect.MethodRegistry; + +import java.lang.reflect.InvocationTargetException; + +/** + * The abstract base for objects that can be subscribed to receive events from {@link EventBus}. + * + *

Objects may also receive events via {@link EventDispatcher}s that can be registered with {@code EventBus}. + * + * @author Alexander Yevsyukov + * @see EventBus#subscribe(EventHandler) + * @see EventBus#register(EventDispatcher) + */ +public abstract class EventHandler { + + public void handle(Message eventMessage, EventContext commandContext) throws InvocationTargetException { + final EventHandlerMethod method = getHandlerMethod(eventMessage.getClass()); + + method.invoke(this, eventMessage, commandContext); + } + + private EventHandlerMethod getHandlerMethod(Class eventClass) { + return MethodRegistry.getInstance().get(getClass(), eventClass, EventHandlerMethod.factory()); + } +} diff --git a/server/src/main/java/org/spine3/server/event/HandlerRegistry.java b/server/src/main/java/org/spine3/server/event/HandlerRegistry.java index 5e9ea8dfc54..d35b9f1e1cb 100644 --- a/server/src/main/java/org/spine3/server/event/HandlerRegistry.java +++ b/server/src/main/java/org/spine3/server/event/HandlerRegistry.java @@ -23,12 +23,17 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; -import org.spine3.server.internal.EventHandlerMethod; -import org.spine3.type.EventClass; +import com.google.protobuf.Message; +import org.spine3.server.reflect.EventHandlerMethod; +import org.spine3.server.reflect.MethodMap; +import org.spine3.server.type.EventClass; import java.util.Collection; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + /** * The registry of event handling methods by event class. * @@ -38,48 +43,44 @@ */ /* package */ class HandlerRegistry { - private final Multimap handlersByClass = HashMultimap.create(); - - /* package */ void subscribe(Map handlers) { - for (Map.Entry entry : handlers.entrySet()) { - handlersByClass.put(entry.getKey(), entry.getValue()); - } - } - - /* package */ void unsubscribe(Map handlers) { - for (Map.Entry entry : handlers.entrySet()) { + private final Multimap handlersByEventClass = HashMultimap.create(); - final EventClass eventClass = entry.getKey(); - final EventHandlerMethod handler = entry.getValue(); - - unsubscribe(eventClass, handler); + /* package */ void subscribe(EventHandler object) { + checkNotNull(object); + final MethodMap handlers = EventHandlerMethod.scan(object); + final boolean handlersEmpty = handlers.isEmpty(); + checkHandlersNotEmpty(object, handlersEmpty); + for (Map.Entry, EventHandlerMethod> entry : handlers.entrySet()) { + handlersByEventClass.put(EventClass.of(entry.getKey()), object); } } - private void unsubscribe(EventClass c, EventHandlerMethod handler) { - final Collection currentSubscribers = handlersByClass.get(c); - if (!currentSubscribers.contains(handler)) { - throw handlerMethodWasNotRegistered(handler); + /* package */ void usubscribe(EventHandler object) { + final MethodMap handlers = EventHandlerMethod.scan(object); + final boolean handlersEmpty = handlers.isEmpty(); + checkHandlersNotEmpty(object, handlersEmpty); + if (!handlersEmpty) { + for (Class eventClass : handlers.keySet()) { + handlersByEventClass.remove(EventClass.of(eventClass), object); + } } - currentSubscribers.remove(handler); } /* package */ void unsubscribeAll() { - handlersByClass.clear(); + handlersByEventClass.clear(); EventBus.log().info("All subscribers cleared."); } - private static IllegalArgumentException handlerMethodWasNotRegistered(EventHandlerMethod handler) { - return new IllegalArgumentException( - "Cannot un-subscribe the event handler, which was not subscribed before:" + handler.getFullName()); - } - - /* package */ Collection getSubscribers(EventClass c) { - return ImmutableList.copyOf(handlersByClass.get(c)); + /* package */ Collection getSubscribers(EventClass c) { + return ImmutableList.copyOf(handlersByEventClass.get(c)); } /* package */ boolean hasSubscribers(EventClass eventClass) { - final Collection handlers = getSubscribers(eventClass); + final Collection handlers = getSubscribers(eventClass); return !handlers.isEmpty(); } + + private static void checkHandlersNotEmpty(Object object, boolean handlersEmpty) { + checkArgument(!handlersEmpty, "No event subscriber methods found in %s", object); + } } diff --git a/server/src/main/java/org/spine3/server/Subscribe.java b/server/src/main/java/org/spine3/server/event/Subscribe.java similarity index 96% rename from server/src/main/java/org/spine3/server/Subscribe.java rename to server/src/main/java/org/spine3/server/event/Subscribe.java index 179f6364732..7b8e2cb1b2b 100644 --- a/server/src/main/java/org/spine3/server/Subscribe.java +++ b/server/src/main/java/org/spine3/server/event/Subscribe.java @@ -18,9 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; - -import org.spine3.server.event.EventBus; +package org.spine3.server.event; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/server/src/main/java/org/spine3/server/FailureThrowable.java b/server/src/main/java/org/spine3/server/failure/FailureThrowable.java similarity index 96% rename from server/src/main/java/org/spine3/server/FailureThrowable.java rename to server/src/main/java/org/spine3/server/failure/FailureThrowable.java index db10756efe6..f0ef6f92d92 100644 --- a/server/src/main/java/org/spine3/server/FailureThrowable.java +++ b/server/src/main/java/org/spine3/server/failure/FailureThrowable.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; +package org.spine3.server.failure; import com.google.common.base.Throwables; import com.google.protobuf.Any; @@ -32,7 +32,6 @@ * * @author Alexander Yevsyukov */ -@SuppressWarnings("AbstractClassWithoutAbstractMethods") public abstract class FailureThrowable extends Throwable { private static final long serialVersionUID = 0L; diff --git a/server/src/main/java/org/spine3/server/EventHandler.java b/server/src/main/java/org/spine3/server/failure/package-info.java similarity index 70% rename from server/src/main/java/org/spine3/server/EventHandler.java rename to server/src/main/java/org/spine3/server/failure/package-info.java index 3fd044c56d8..5784673ca97 100644 --- a/server/src/main/java/org/spine3/server/EventHandler.java +++ b/server/src/main/java/org/spine3/server/failure/package-info.java @@ -18,18 +18,11 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server; - -import org.spine3.server.event.EventBus; - /** - * The marker interface for objects that can be subscribed to receive events from {@link EventBus}. - * - *

Objects may also receive events via {@link EventDispatcher}s that can be registered with {@code EventBus}. - * - * @author Alexander Yevsyukov - * @see EventBus#subscribe(EventHandler) - * @see EventBus#register(EventDispatcher) + * This package provides classes for working with business failures. */ -public interface EventHandler { -} + +@ParametersAreNonnullByDefault +package org.spine3.server.failure; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/main/java/org/spine3/server/procman/ProcessManager.java b/server/src/main/java/org/spine3/server/procman/ProcessManager.java index 1ef1b2c47af..76e5c8c6ae9 100644 --- a/server/src/main/java/org/spine3/server/procman/ProcessManager.java +++ b/server/src/main/java/org/spine3/server/procman/ProcessManager.java @@ -35,26 +35,23 @@ import org.spine3.base.Events; import org.spine3.base.Identifiers; import org.spine3.base.UserId; -import org.spine3.server.CommandHandler; import org.spine3.server.command.CommandBus; import org.spine3.server.entity.Entity; -import org.spine3.server.internal.CommandHandlerMethod; -import org.spine3.server.internal.EventHandlerMethod; import org.spine3.server.reflect.Classes; -import org.spine3.server.reflect.MethodMap; +import org.spine3.server.reflect.CommandHandlerMethod; +import org.spine3.server.reflect.EventHandlerMethod; +import org.spine3.server.reflect.MethodRegistry; import org.spine3.time.ZoneOffset; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static org.spine3.server.internal.EventHandlerMethod.PREDICATE; -import static org.spine3.server.internal.EventHandlerMethod.checkModifiers; +import static org.spine3.server.reflect.EventHandlerMethod.PREDICATE; /** * An independent component that reacts to domain events in a cross-aggregate, eventually consistent manner. @@ -80,32 +77,13 @@ * @param the type of the process manager state * @author Alexander Litus */ -public abstract class ProcessManager extends Entity implements CommandHandler { - - /** - * Keeps initialization state of the process manager. - */ - private volatile boolean initialized = false; +public abstract class ProcessManager extends Entity { /** * The Command Bus to post routed commands. */ private volatile CommandBus commandBus; - /** - * The map of command handler methods of this process manager. - * - * @see Registry - */ - private MethodMap commandHandlers; - - /** - * The map of event handler methods of this process manager. - * - * @see Registry - */ - private MethodMap eventHandlers; - /** * Creates a new instance. * @@ -117,23 +95,6 @@ public ProcessManager(I id) { super(id); } - /** - * Performs initialization of the instance and registers this class of process managers - * in the {@link Registry} if it is not registered yet. - */ - private void init() { - if (!this.initialized) { - final Registry registry = Registry.getInstance(); - final Class pmClass = getClass(); - if (!registry.contains(pmClass)) { - registry.register(pmClass); - } - commandHandlers = registry.getCommandHandlers(pmClass); - eventHandlers = registry.getEventHandlers(pmClass); - this.initialized = true; - } - } - /** * The method to inject {@code CommandBus} instance from the repository. */ @@ -161,14 +122,16 @@ protected List dispatchCommand(Message command, CommandContext context) checkNotNull(command); checkNotNull(context); - init(); final Class commandClass = command.getClass(); - final Method method = commandHandlers.get(commandClass); + final CommandHandlerMethod method = MethodRegistry.getInstance() + .get(getClass(), + commandClass, + CommandHandlerMethod.factory()); if (method == null) { throw missingCommandHandler(commandClass); } - final CommandHandlerMethod commandHandler = new CommandHandlerMethod(this, method); - final List events = commandHandler.invoke(command, context); + + final List events = method.invoke(this, command, context); final List eventRecords = toEvents(events, context.getCommandId()); return eventRecords; } @@ -199,14 +162,14 @@ protected void dispatchEvent(Message event, EventContext context) throws Invocat checkNotNull(context); checkNotNull(event); - init(); final Class eventClass = event.getClass(); - final Method method = eventHandlers.get(eventClass); + final EventHandlerMethod method = MethodRegistry.getInstance() + .get(getClass(), eventClass, EventHandlerMethod.factory()); if (method == null) { throw missingEventHandler(eventClass); } - final EventHandlerMethod handler = new EventHandlerMethod(this, method); - handler.invoke(event, context); + + method.invoke(this, event, context); } /** @@ -386,52 +349,4 @@ private IllegalStateException missingEventHandler(Class event eventClass, this.getClass())); } - /** - * The registry of method maps for all process manager classes. - * - *

This registry is used for caching command/event handlers. - * Process managers register their classes in {@link ProcessManager#init()} method. - */ - private static class Registry { - - private final MethodMap.Registry commandHandlers = new MethodMap.Registry<>(); - private final MethodMap.Registry eventHandlers = new MethodMap.Registry<>(); - - /* package */ void register(Class clazz) { - commandHandlers.register(clazz, CommandHandlerMethod.PREDICATE); - CommandHandlerMethod.checkModifiers(commandHandlers.get(clazz).values()); - - eventHandlers.register(clazz, PREDICATE); - checkModifiers(eventHandlers.get(clazz).values()); - } - - @CheckReturnValue - /* package */ boolean contains(Class clazz) { - final boolean result = commandHandlers.contains(clazz) && eventHandlers.contains(clazz); - return result; - } - - @CheckReturnValue - /* package */ MethodMap getCommandHandlers(Class clazz) { - final MethodMap result = commandHandlers.get(clazz); - return result; - } - - @CheckReturnValue - /* package */ MethodMap getEventHandlers(Class clazz) { - final MethodMap result = eventHandlers.get(clazz); - return result; - } - - @CheckReturnValue - /* package */ static Registry getInstance() { - return Singleton.INSTANCE.value; - } - - private enum Singleton { - INSTANCE; - @SuppressWarnings("NonSerializableFieldInSerializableClass") - private final Registry value = new Registry(); - } - } } diff --git a/server/src/main/java/org/spine3/server/procman/ProcessManagerRepository.java b/server/src/main/java/org/spine3/server/procman/ProcessManagerRepository.java index 3a22cf1e479..0e9d1de544c 100644 --- a/server/src/main/java/org/spine3/server/procman/ProcessManagerRepository.java +++ b/server/src/main/java/org/spine3/server/procman/ProcessManagerRepository.java @@ -30,14 +30,15 @@ import org.spine3.base.EventContext; import org.spine3.base.Events; import org.spine3.server.BoundedContext; -import org.spine3.server.CommandDispatcher; import org.spine3.server.command.CommandBus; +import org.spine3.server.command.CommandDispatcher; import org.spine3.server.entity.EntityEventDispatcher; import org.spine3.server.entity.EntityRepository; -import org.spine3.server.command.GetTargetIdFromCommand; +import org.spine3.server.entity.GetTargetIdFromCommand; import org.spine3.server.entity.IdFunction; -import org.spine3.type.CommandClass; -import org.spine3.type.EventClass; +import org.spine3.server.event.EventBus; +import org.spine3.server.type.CommandClass; +import org.spine3.server.type.EventClass; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -112,7 +113,7 @@ public Set getEventClasses() { * @throws IllegalArgumentException if commands of this type are not handled by the process manager */ @Override - public List dispatch(Command command) + public void dispatch(Command command) throws InvocationTargetException, IllegalStateException, IllegalArgumentException { final Message commandMessage = getMessage(checkNotNull(command)); final CommandContext context = command.getContext(); @@ -122,7 +123,17 @@ public List dispatch(Command command) final PM manager = load(id); final List events = manager.dispatchCommand(commandMessage, context); store(manager); - return events; + postEvents(events); + } + + /** + * Posts passed events to {@link EventBus}. + */ + private void postEvents(Iterable events) { + final EventBus eventBus = getBoundedContext().getEventBus(); + for (Event event : events) { + eventBus.post(event); + } } /** diff --git a/server/src/main/java/org/spine3/server/projection/Projection.java b/server/src/main/java/org/spine3/server/projection/Projection.java index 15bf01c5ab5..e134d022946 100644 --- a/server/src/main/java/org/spine3/server/projection/Projection.java +++ b/server/src/main/java/org/spine3/server/projection/Projection.java @@ -24,15 +24,14 @@ import com.google.protobuf.Message; import org.spine3.base.EventContext; import org.spine3.server.entity.Entity; -import org.spine3.server.internal.EventHandlerMethod; import org.spine3.server.reflect.Classes; -import org.spine3.server.reflect.MethodMap; +import org.spine3.server.reflect.EventHandlerMethod; +import org.spine3.server.reflect.MethodRegistry; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import static com.google.common.base.Throwables.propagate; -import static org.spine3.server.internal.EventHandlerMethod.PREDICATE; +import static org.spine3.server.reflect.EventHandlerMethod.PREDICATE; /** * {@link Projection} holds a structural representation of data extracted from a stream of events. @@ -49,8 +48,6 @@ */ public abstract class Projection extends Entity { - private MethodMap handlers; - /** * Creates a new instance. * @@ -63,44 +60,23 @@ public Projection(I id) { } protected void handle(Message event, EventContext ctx) { - init(); dispatch(event, ctx); } private void dispatch(Message event, EventContext ctx) { final Class eventClass = event.getClass(); - final Method method = handlers.get(eventClass); + final EventHandlerMethod method = MethodRegistry.getInstance() + .get(getClass(), eventClass, EventHandlerMethod.factory()); if (method == null) { throw missingEventHandler(eventClass); } - final EventHandlerMethod handler = new EventHandlerMethod(this, method); try { - handler.invoke(event, ctx); + method.invoke(this, event, ctx); } catch (InvocationTargetException e) { propagate(e); } } - protected boolean isInitialized() { - return handlers != null; - } - - /** - * Performs initialization of the instance. - */ - protected void init() { - if (!isInitialized()) { - final Registry registry = Registry.getInstance(); - final Class thisClass = getClass(); - - if (!registry.contains(thisClass)) { - registry.register(thisClass); - } - - handlers = registry.getEventHandlers(thisClass); - } - } - /** * Returns the set of event classes handled by the passed {@link Projection} class. * @@ -116,31 +92,4 @@ private IllegalStateException missingEventHandler(Class event eventClass, this.getClass())); } - private static class Registry { - private final MethodMap.Registry eventHandlers = new MethodMap.Registry<>(); - - boolean contains(Class clazz) { - return eventHandlers.contains(clazz); - } - - void register(Class clazz) { - eventHandlers.register(clazz, PREDICATE); - } - - MethodMap getEventHandlers(Class clazz) { - final MethodMap result = eventHandlers.get(clazz); - return result; - } - - static Registry getInstance() { - return RegistrySingleton.INSTANCE.value; - } - - private enum RegistrySingleton { - INSTANCE; - - @SuppressWarnings("NonSerializableFieldInSerializableClass") - private final Registry value = new Registry(); - } - } } \ No newline at end of file diff --git a/server/src/main/java/org/spine3/server/projection/ProjectionRepository.java b/server/src/main/java/org/spine3/server/projection/ProjectionRepository.java index 378dc8c657c..f0083ca0c3e 100644 --- a/server/src/main/java/org/spine3/server/projection/ProjectionRepository.java +++ b/server/src/main/java/org/spine3/server/projection/ProjectionRepository.java @@ -26,12 +26,12 @@ import org.spine3.base.EventContext; import org.spine3.base.Events; import org.spine3.server.BoundedContext; -import org.spine3.server.EventDispatcher; import org.spine3.server.entity.EntityRepository; +import org.spine3.server.event.EventDispatcher; import org.spine3.server.storage.EntityStorage; import org.spine3.server.storage.ProjectionStorage; import org.spine3.server.storage.StorageFactory; -import org.spine3.type.EventClass; +import org.spine3.server.type.EventClass; import javax.annotation.Nonnull; import java.util.Set; diff --git a/server/src/main/java/org/spine3/server/reflect/Classes.java b/server/src/main/java/org/spine3/server/reflect/Classes.java index 18fad13a09f..d6b59f9dbfd 100644 --- a/server/src/main/java/org/spine3/server/reflect/Classes.java +++ b/server/src/main/java/org/spine3/server/reflect/Classes.java @@ -75,7 +75,7 @@ public static ImmutableSet> getHandledMessageClasses(Cl for (Method method : clazz.getDeclaredMethods()) { final boolean methodMatches = predicate.apply(method); if (methodMatches) { - final Class firstParamType = Methods.getFirstParamType(method); + final Class firstParamType = HandlerMethod.getFirstParamType(method); builder.add(firstParamType); } } diff --git a/server/src/main/java/org/spine3/server/internal/CommandHandlerMethod.java b/server/src/main/java/org/spine3/server/reflect/CommandHandlerMethod.java similarity index 74% rename from server/src/main/java/org/spine3/server/internal/CommandHandlerMethod.java rename to server/src/main/java/org/spine3/server/reflect/CommandHandlerMethod.java index a4eb6f18911..71e2ac48d31 100644 --- a/server/src/main/java/org/spine3/server/internal/CommandHandlerMethod.java +++ b/server/src/main/java/org/spine3/server/reflect/CommandHandlerMethod.java @@ -18,16 +18,13 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server.internal; +package org.spine3.server.reflect; import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableMap; import com.google.protobuf.Message; import org.spine3.base.CommandContext; -import org.spine3.server.Assign; -import org.spine3.server.CommandHandler; -import org.spine3.server.reflect.MethodMap; -import org.spine3.type.CommandClass; +import org.spine3.server.command.Assign; +import org.spine3.server.command.CommandHandler; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -35,7 +32,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; -import java.util.Map; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -45,7 +41,7 @@ * * @author Alexander Yevsyukov */ -public class CommandHandlerMethod extends MessageHandlerMethod { +public class CommandHandlerMethod extends HandlerMethod { /** * The instance of the predicate to filter command handler methods of a class. @@ -55,23 +51,22 @@ public class CommandHandlerMethod extends MessageHandlerMethod R invoke(Message message, CommandContext context) throws InvocationTargetException { - final R handlingResult = super.invoke(message, context); + public R invoke(Object target, Message message, CommandContext context) throws InvocationTargetException { + final R handlingResult = super.invoke(target, message, context); - final List events = commandHandlingResultToEvents(handlingResult); + final List events = toList(handlingResult); // The list of event messages/records is the return type expected. @SuppressWarnings("unchecked") final R result = (R) events; @@ -85,7 +80,7 @@ public R invoke(Message message, CommandContext context) throws InvocationTa * or {@code null}. * @return the list of event messages or an empty list if {@code null} is passed */ - protected List commandHandlingResultToEvents(@Nullable R handlingResult) { + private static List toList(@Nullable R handlingResult) { if (handlingResult == null) { return emptyList(); } @@ -110,29 +105,9 @@ protected List commandHandlingResultToEvents(@Nullable R * @return immutable map */ @CheckReturnValue - public static Map scan(CommandHandler object) { - final ImmutableMap.Builder result = ImmutableMap.builder(); - - final Map handlers = getHandlers(object); - result.putAll(handlers); - - return result.build(); - } - - private static Map getHandlers(CommandHandler object) { - final ImmutableMap.Builder result = ImmutableMap.builder(); - - final Predicate methodPredicate = PREDICATE; - final MethodMap handlers = new MethodMap(object.getClass(), methodPredicate); - - checkModifiers(handlers.values()); - - for (Map.Entry, Method> entry : handlers.entrySet()) { - final CommandClass commandClass = CommandClass.of(entry.getKey()); - final CommandHandlerMethod handler = new CommandHandlerMethod(object, entry.getValue()); - result.put(commandClass, handler); - } - return result.build(); + public static MethodMap scan(CommandHandler object) { + final MethodMap handlers = MethodMap.create(object.getClass(), factory()); + return handlers; } /** @@ -141,7 +116,7 @@ private static Map getHandlers(CommandHandle *

Logs warning for the methods with a non-public modifier. * * @param methods the methods to check - * @see MessageHandlerMethod#log() + * @see HandlerMethod#log() */ public static void checkModifiers(Iterable methods) { for (Method method : methods) { @@ -152,6 +127,41 @@ public static void checkModifiers(Iterable methods) { } } + public static HandlerMethod.Factory factory() { + return Factory.instance(); + } + + /** + * The factory for filtering methods that match {@code CommandHandlerMethod} specification. + */ + private static class Factory implements HandlerMethod.Factory { + + @Override + public Class getMethodClass() { + return CommandHandlerMethod.class; + } + + @Override + public CommandHandlerMethod create(Method method) { + return new CommandHandlerMethod(method); + } + + @Override + public Predicate getPredicate() { + return PREDICATE; + } + + private enum Singleton { + INSTANCE; + @SuppressWarnings("NonSerializableFieldInSerializableClass") + private final Factory value = new Factory(); + } + + private static Factory instance() { + return Singleton.INSTANCE.value; + } + } + /** * The predicate class that allows to filter command handling methods. */ diff --git a/server/src/main/java/org/spine3/server/internal/EventHandlerMethod.java b/server/src/main/java/org/spine3/server/reflect/EventHandlerMethod.java similarity index 73% rename from server/src/main/java/org/spine3/server/internal/EventHandlerMethod.java rename to server/src/main/java/org/spine3/server/reflect/EventHandlerMethod.java index 5737bd48e10..80935ca14be 100644 --- a/server/src/main/java/org/spine3/server/internal/EventHandlerMethod.java +++ b/server/src/main/java/org/spine3/server/reflect/EventHandlerMethod.java @@ -18,22 +18,18 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server.internal; +package org.spine3.server.reflect; import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableMap; import com.google.protobuf.Message; import org.spine3.base.EventContext; -import org.spine3.server.Subscribe; -import org.spine3.server.reflect.MethodMap; -import org.spine3.type.EventClass; +import org.spine3.server.event.Subscribe; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Map; import static com.google.common.base.Preconditions.checkNotNull; @@ -42,7 +38,7 @@ * * @author Alexander Yevsyukov */ -public class EventHandlerMethod extends MessageHandlerMethod { +public class EventHandlerMethod extends HandlerMethod { /** * The instance of the predicate to filter event handler methods of a class. @@ -52,11 +48,10 @@ public class EventHandlerMethod extends MessageHandlerMethod scan(Object target) { - final ImmutableMap.Builder result = ImmutableMap.builder(); - - // Scan for declared event handler methods. - final MethodMap handlers = new MethodMap(target.getClass(), PREDICATE); - checkModifiers(handlers.values()); - - for (ImmutableMap.Entry, Method> entry : handlers.entrySet()) { - final EventClass eventClass = EventClass.of(entry.getKey()); - final EventHandlerMethod handler = new EventHandlerMethod(target, entry.getValue()); - result.put(eventClass, handler); - } - return result.build(); + public static MethodMap scan(Object target) { + final MethodMap result = MethodMap.create(target.getClass(), factory()); + return result; } @Override - public R invoke(Message message, EventContext context) throws InvocationTargetException { - return super.invoke(message, context); + public R invoke(Object target, Message message, EventContext context) throws InvocationTargetException { + return super.invoke(target, message, context); } /** @@ -92,7 +77,7 @@ public R invoke(Message message, EventContext context) throws InvocationTarg *

Logs warning for the methods with a non-public modifier. * * @param methods the map of methods to check - * @see MessageHandlerMethod#log() + * @see HandlerMethod#log() */ public static void checkModifiers(Iterable methods) { for (Method method : methods) { @@ -104,13 +89,42 @@ public static void checkModifiers(Iterable methods) { } /** - * {@inheritDoc} + * @return the factory for filtering and creating event handler methods */ - @Override // Promote to public to make it visible to routines inspecting EventBus. - public Object getTarget() { - return super.getTarget(); + public static HandlerMethod.Factory factory() { + return Factory.instance(); } + /** + * The factory for filtering methods that match {@code EventHandlerMethod} specification. + */ + private static class Factory implements HandlerMethod.Factory { + + @Override + public Class getMethodClass() { + return EventHandlerMethod.class; + } + + @Override + public EventHandlerMethod create(Method method) { + return new EventHandlerMethod(method); + } + + @Override + public Predicate getPredicate() { + return PREDICATE; + } + + private enum Singleton { + INSTANCE; + @SuppressWarnings("NonSerializableFieldInSerializableClass") + private final Factory value = new Factory(); + } + + private static Factory instance() { + return Singleton.INSTANCE.value; + } + } /** * The predicate class allowing to filter event handling methods. * diff --git a/server/src/main/java/org/spine3/server/internal/MessageHandlerMethod.java b/server/src/main/java/org/spine3/server/reflect/HandlerMethod.java similarity index 64% rename from server/src/main/java/org/spine3/server/internal/MessageHandlerMethod.java rename to server/src/main/java/org/spine3/server/reflect/HandlerMethod.java index 9edd4caecc3..ebc50cc9cce 100644 --- a/server/src/main/java/org/spine3/server/internal/MessageHandlerMethod.java +++ b/server/src/main/java/org/spine3/server/reflect/HandlerMethod.java @@ -17,12 +17,12 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server.internal; +package org.spine3.server.reflect; +import com.google.common.base.Predicate; import com.google.protobuf.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.spine3.server.reflect.Methods; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; @@ -41,44 +41,43 @@ * * @author Mikhail Melnik * @author Alexander Yevsyukov - - * @param the type of the target object * @param the type of the message context or {@code Void} if context is not used */ -@SuppressWarnings("AbstractClassWithoutAbstractMethods") -public abstract class MessageHandlerMethod { +public abstract class HandlerMethod { /** - * Object sporting the handler method. + * The method to be called. */ - private final T target; + private final Method method; /** - * Handler method. + * The number of parameters the method has. */ - private final Method method; + private final int paramCount; /** * Creates a new instance to wrap {@code method} on {@code target}. * - * @param target object to which the method applies * @param method subscriber method */ - protected MessageHandlerMethod(T target, Method method) { - this.target = checkNotNull(target); + protected HandlerMethod(Method method) { this.method = checkNotNull(method); + this.paramCount = method.getParameterTypes().length; method.setAccessible(true); } protected static void warnOnWrongModifier(String messageFormat, Method method) { - log().warn(messageFormat, Methods.getFullMethodName(method)); + log().warn(messageFormat, getFullMethodName(method)); } /** - * @return the target object on which the method call is made + * Returns a full method name without parameters. + * + * @param method a method to get name for + * @return full method name */ - protected T getTarget() { - return target; + private static String getFullMethodName(Method method) { + return method.getDeclaringClass().getName() + '.' + method.getName() + "()"; } /** @@ -104,17 +103,25 @@ protected boolean isPrivate() { return result; } + //TODO:2016-04-19:alexander.yevsyukov: Check the value returned by this method in the invoke() methods below. + // See if we can have a common internal method that accepts null as the context parameter and ignores it + // if the underlying method accepts only one parameter. + + protected int getParamCount() { + return paramCount; + } + /** * Invokes the wrapped subscriber method to handle {@code message} with the {@code context}. * * @param the type of the expected handler invocation result - * @param message the message to handle + * @param target the target object on which call the method + * @param message the message to handle @return the result of message handling * @param context the context of the message - * @return the result of message handling * @throws InvocationTargetException if the wrapped method throws any {@link Throwable} that is not an {@link Error}. * {@code Error} instances are propagated as-is. */ - protected R invoke(Message message, C context) throws InvocationTargetException { + protected R invoke(Object target, Message message, C context) throws InvocationTargetException { checkNotNull(message); checkNotNull(context); try { @@ -136,12 +143,13 @@ protected R invoke(Message message, C context) throws InvocationTargetExcept * Invokes the wrapped subscriber method to handle {@code message}. * * @param the type of the expected handler invocation result - * @param message a message to handle + * @param target the target object on which call the method + * @param message a message to handle * @return the result of message handling - * @throws InvocationTargetException if the wrapped method throws any {@link Throwable} that is not an {@link Error}. - * {@code Error} instances are propagated as-is. + * @throws InvocationTargetException if the wrapped method throws any {@link Throwable} that is not + * an {@link Error}. {@code Error} instances are propagated as-is. */ - protected R invoke(Message message) throws InvocationTargetException { + protected R invoke(Object target, Message message) throws InvocationTargetException { checkNotNull(message); try { @SuppressWarnings("unchecked") @@ -167,7 +175,7 @@ protected R invoke(Message message) throws InvocationTargetException { * @return full name of the subscriber */ public String getFullName() { - return Methods.getFullMethodName(method); + return getFullMethodName(method); } /** @@ -182,8 +190,7 @@ public String toString() { @Override public int hashCode() { final int prime = 31; - return (prime + method.hashCode()) * prime - + System.identityHashCode(target); + return (prime + method.hashCode()); } @Override @@ -194,19 +201,48 @@ public boolean equals(@Nullable Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final MessageHandlerMethod other = (MessageHandlerMethod) obj; + final HandlerMethod other = (HandlerMethod) obj; + + return Objects.equals(this.method, other.method); + } + + /** + * Returns the class of the first parameter of the passed handler method object. + * + *

It is expected that the first parameter of the passed method is always of + * a class implementing {@link Message}. + * + * @param handler the method object to take first parameter type from + * @return the class of the first method parameter + * @throws ClassCastException if the first parameter isn't a class implementing {@link Message} + */ + /* package */ static Class getFirstParamType(Method handler) { + @SuppressWarnings("unchecked") /* we always expect first param as {@link Message} */ + final Class result = (Class) handler.getParameterTypes()[0]; + return result; + } + + /** + * The interface for factory objects that can filter {@link Method} objects + * that represent handler methods and create corresponding {@code HandlerMethod} instances + * that wrap those methods. + * + * @param the type of the handler method objects to create + */ + public interface Factory { + + Class getMethodClass(); + + H create(Method method); - // Use == to verify that the instances of the target objects are the same. - // This way we'd allow having handlers for target objects that are otherwise equal. - return (this.target == other.target) - && Objects.equals(this.method, other.method); + Predicate getPredicate(); } private enum LogSingleton { INSTANCE; @SuppressWarnings("NonSerializableFieldInSerializableClass") - private final Logger value = LoggerFactory.getLogger(MessageHandlerMethod.class); + private final Logger value = LoggerFactory.getLogger(HandlerMethod.class); } /** diff --git a/server/src/main/java/org/spine3/server/reflect/MethodMap.java b/server/src/main/java/org/spine3/server/reflect/MethodMap.java index 2032b8c3fca..4d8b7932690 100644 --- a/server/src/main/java/org/spine3/server/reflect/MethodMap.java +++ b/server/src/main/java/org/spine3/server/reflect/MethodMap.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.protobuf.Message; +import org.spine3.error.DuplicateHandlerMethodException; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -37,20 +38,64 @@ /** * A map for storing methods handling messages. * + * @param the type of the handler method instances stored in the map * @author Alexander Yevsyukov */ -public class MethodMap { +public class MethodMap { - private final ImmutableMap, Method> map; + private final ImmutableMap, H> map; - public MethodMap(Class clazz, Predicate filter) { - this(Methods.scan(clazz, filter)); + private MethodMap(Class clazz, HandlerMethod.Factory factory) { + final Map, Method> rawMethods = scan(clazz, factory.getPredicate()); + + final ImmutableMap.Builder, H> builder = ImmutableMap.builder(); + for (Map.Entry, Method> entry : rawMethods.entrySet()) { + final H value = factory.create(entry.getValue()); + builder.put(entry.getKey(), value); + } + + this.map = builder.build(); } - private MethodMap(Map, Method> map) { - this.map = ImmutableMap., Method>builder() - .putAll(map) - .build(); + /** + * Creates a new method map for the passed class using the passed factory. + * + * @param clazz the class to inspect + * @param factory the factory for handler methods + * @param the type of the handler methods + * @return new method map + */ + public static MethodMap create(Class clazz, HandlerMethod.Factory factory) { + return new MethodMap<>(clazz, factory); + } + + /** + * Returns a map of the {@link HandlerMethod} objects to the corresponding message class. + * + * @param declaringClass the class that declares methods to scan + * @param filter the predicate that defines rules for subscriber scanning + * @return the map of message subscribers + * @throws DuplicateHandlerMethodException if there are more than one handler for the same message class are encountered + */ + private static Map, Method> scan(Class declaringClass, Predicate filter) { + final Map, Method> tempMap = Maps.newHashMap(); + for (Method method : declaringClass.getDeclaredMethods()) { + if (filter.apply(method)) { + final Class messageClass = HandlerMethod.getFirstParamType(method); + if (tempMap.containsKey(messageClass)) { + final Method alreadyPresent = tempMap.get(messageClass); + throw new DuplicateHandlerMethodException( + declaringClass, + messageClass, + alreadyPresent.getName(), + method.getName()); + } + tempMap.put(messageClass, method); + } + } + final ImmutableMap.Builder, Method> builder = ImmutableMap.builder(); + builder.putAll(tempMap); + return builder.build(); } /** @@ -67,66 +112,18 @@ public ImmutableSet> keySet() { } @CheckReturnValue - public ImmutableSet, Method>> entrySet() { + public ImmutableSet, H>> entrySet() { return map.entrySet(); } @CheckReturnValue - public ImmutableCollection values() { + public ImmutableCollection values() { return map.values(); } @CheckReturnValue @Nullable - public Method get(Class messageClass) { + public H get(Class messageClass) { return map.get(checkNotNull(messageClass)); } - - /** - * The registry of message maps by class. - * - * @param the type of objects tracked by the registry - */ - public static class Registry { - - private final Map, MethodMap> entries = Maps.newConcurrentMap(); - - /** - * Verifies if the class is already registered. - * - * @param clazz the class to check - * @return {@code true} if there is a message map for the passed class, {@code false} otherwise - */ - @CheckReturnValue - public boolean contains(Class clazz) { - final boolean result = entries.containsKey(clazz); - return result; - } - - /** - * Registers methods of the class in the registry. - * - * @param clazz the class to register - * @param filter a filter for selecting methods to register - * @throws IllegalArgumentException if the class was already registered - * @see #contains(Class) - */ - public void register(Class clazz, Predicate filter) { - if (contains(clazz)) { - throw new IllegalArgumentException("The class is already registered: " + clazz.getName()); - } - - final MethodMap entry = new MethodMap(clazz, filter); - entries.put(clazz, entry); - } - - /** - * Obtains method map for the passed class. - */ - @CheckReturnValue - public MethodMap get(Class clazz) { - final MethodMap result = entries.get(clazz); - return result; - } - } } diff --git a/server/src/main/java/org/spine3/server/reflect/MethodRegistry.java b/server/src/main/java/org/spine3/server/reflect/MethodRegistry.java new file mode 100644 index 00000000000..4bd525e73da --- /dev/null +++ b/server/src/main/java/org/spine3/server/reflect/MethodRegistry.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.spine3.server.reflect; + +import com.google.common.collect.Maps; +import com.google.protobuf.Message; + +import javax.annotation.CheckReturnValue; +import java.util.Map; +import java.util.Objects; + +/** + * The {@code MethodRegistry} stores handler methods per class exposing those methods. + * + * @author Alexander Yevsyukov + */ +public class MethodRegistry { + + private final Map, MethodMap> items = Maps.newConcurrentMap(); + + /** + * Obtains a handler method, which handles the passed message class. + * + *

If the registry does not have the data structures ready for the target class, + * they are created using the passed {@code factory} of the handler methods. + * Then the method is obtained. + * + * @param targetClass the class of the target object + * @param messageClass the class of the message to handle + * @param factory the handler method factory for getting methods from the target class + * @param the type of the target object class + * @param the type of the message handler method + * @return a handler method + */ + public H get(Class targetClass, + Class messageClass, + HandlerMethod.Factory factory) { + + final Key key = new Key<>(targetClass, factory.getMethodClass()); + @SuppressWarnings("unchecked") /* We can cast as the map type is the same as one of the passed factory, + which is used in creating the entry in the `if` block below. */ + MethodMap methods = items.get(key); + if (methods == null) { + methods = MethodMap.create(targetClass, factory); + items.put(key, methods); + } + + final H handlerMethod = methods.get(messageClass); + return handlerMethod; + } + + /** + * The map entry key which consists of target object class and method class. + * + * @param the type of the target class + * @param the type of the method handler class + */ + private static class Key { + private final Class targetClass; + private final Class methodClass; + + private Key(Class targetClass, Class methodClass) { + this.targetClass = targetClass; + this.methodClass = methodClass; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Key)) { + return false; + } + final Key key = (Key) o; + return Objects.equals(targetClass, key.targetClass) && + Objects.equals(methodClass, key.methodClass); + } + + @Override + public int hashCode() { + return Objects.hash(targetClass, methodClass); + } + } + + @CheckReturnValue + public static MethodRegistry getInstance() { + return Singleton.INSTANCE.value; + } + + private enum Singleton { + INSTANCE; + @SuppressWarnings("NonSerializableFieldInSerializableClass") + private final MethodRegistry value = new MethodRegistry(); + } +} diff --git a/server/src/main/java/org/spine3/server/reflect/Methods.java b/server/src/main/java/org/spine3/server/reflect/Methods.java deleted file mode 100644 index 58e62ad6f4e..00000000000 --- a/server/src/main/java/org/spine3/server/reflect/Methods.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2016, TeamDev Ltd. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.spine3.server.reflect; - -import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.protobuf.Message; -import org.spine3.error.DuplicateHandlerMethodException; -import org.spine3.server.internal.MessageHandlerMethod; - -import java.lang.reflect.Method; -import java.util.Map; - -/** - * Utilities for client-side methods. - * - * @author Alexander Yevsyukov - */ -public class Methods { - - private Methods() {} - - /** - * Returns a full method name without parameters. - * - * @param method a method to get name for - * @return full method name - */ - public static String getFullMethodName(Method method) { - return method.getDeclaringClass().getName() + '.' + method.getName() + "()"; - } - - /** - * Returns the class of the first parameter of the passed handler method object. - * - *

It is expected that the first parameter of the passed method is always of - * a class implementing {@link Message}. - * - * @param handler the method object to take first parameter type from - * @return the class of the first method parameter - * @throws ClassCastException if the first parameter isn't a class implementing {@link Message} - */ - public static Class getFirstParamType(Method handler) { - @SuppressWarnings("unchecked") /* we always expect first param as {@link Message} */ - final Class result = (Class) handler.getParameterTypes()[0]; - return result; - } - - /** - * Returns a map of the {@link MessageHandlerMethod} objects to the corresponding message class. - * - * @param declaringClass the class that declares methods to scan - * @param filter the predicate that defines rules for subscriber scanning - * @return the map of message subscribers - * @throws DuplicateHandlerMethodException if there are more than one handler for the same message class are encountered - */ - public static Map, Method> scan(Class declaringClass, Predicate filter) { - final Map, Method> tempMap = Maps.newHashMap(); - for (Method method : declaringClass.getDeclaredMethods()) { - if (filter.apply(method)) { - final Class messageClass = getFirstParamType(method); - if (tempMap.containsKey(messageClass)) { - final Method alreadyPresent = tempMap.get(messageClass); - throw new DuplicateHandlerMethodException( - declaringClass, - messageClass, - alreadyPresent.getName(), - method.getName()); - } - tempMap.put(messageClass, method); - } - } - final ImmutableMap.Builder, Method> builder = ImmutableMap.builder(); - builder.putAll(tempMap); - return builder.build(); - } -} diff --git a/server/src/main/java/org/spine3/server/storage/CommandStorage.java b/server/src/main/java/org/spine3/server/storage/CommandStorage.java index 48d9727ba8a..5b5897b4a65 100644 --- a/server/src/main/java/org/spine3/server/storage/CommandStorage.java +++ b/server/src/main/java/org/spine3/server/storage/CommandStorage.java @@ -32,7 +32,7 @@ import org.spine3.base.Error; import org.spine3.base.Failure; import org.spine3.server.command.CommandStore; -import org.spine3.server.command.GetTargetIdFromCommand; +import org.spine3.server.entity.GetTargetIdFromCommand; import org.spine3.server.error.MissingEntityIdException; import org.spine3.type.TypeName; @@ -52,7 +52,6 @@ public abstract class CommandStorage extends AbstractStorage ID_FUNCTION = GetTargetIdFromCommand.newInstance(); - //TODO:2016-02-18:alexander.yevsyukov: Define constraints the command declaration and use our validation to check the passed parameter. /** * Stores a command by a command ID from a command context. * diff --git a/client/src/main/java/org/spine3/type/CommandClass.java b/server/src/main/java/org/spine3/server/type/CommandClass.java similarity index 97% rename from client/src/main/java/org/spine3/type/CommandClass.java rename to server/src/main/java/org/spine3/server/type/CommandClass.java index 4fdde819a32..bf296d13e3d 100644 --- a/client/src/main/java/org/spine3/type/CommandClass.java +++ b/server/src/main/java/org/spine3/server/type/CommandClass.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.type; +package org.spine3.server.type; import com.google.common.collect.ImmutableSet; import com.google.protobuf.Message; @@ -34,7 +34,7 @@ * * @author Alexander Yevsyukov */ -public final class CommandClass extends ClassTypeValue { +public final class CommandClass extends MessageClass { private CommandClass(Class value) { super(value); diff --git a/client/src/main/java/org/spine3/type/EventClass.java b/server/src/main/java/org/spine3/server/type/EventClass.java similarity index 96% rename from client/src/main/java/org/spine3/type/EventClass.java rename to server/src/main/java/org/spine3/server/type/EventClass.java index 7b3379bf74b..190290f7b49 100644 --- a/client/src/main/java/org/spine3/type/EventClass.java +++ b/server/src/main/java/org/spine3/server/type/EventClass.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.type; +package org.spine3.server.type; import com.google.common.collect.ImmutableSet; import com.google.protobuf.Message; @@ -32,7 +32,7 @@ * * @author Alexander Yevsyukov */ -public final class EventClass extends ClassTypeValue { +public final class EventClass extends MessageClass { private EventClass(Class value) { super(value); diff --git a/client/src/main/java/org/spine3/type/ClassTypeValue.java b/server/src/main/java/org/spine3/server/type/MessageClass.java similarity index 84% rename from client/src/main/java/org/spine3/type/ClassTypeValue.java rename to server/src/main/java/org/spine3/server/type/MessageClass.java index ae794d5dc7a..8c7de4109f2 100644 --- a/client/src/main/java/org/spine3/type/ClassTypeValue.java +++ b/server/src/main/java/org/spine3/server/type/MessageClass.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.type; +package org.spine3.server.type; import com.google.protobuf.Message; @@ -30,13 +30,11 @@ * * @author Alexander Yevsyukov */ -@SuppressWarnings("AbstractClassWithoutAbstractMethods") // is OK for value object base. -// NOTE: this class is named using 'Type' infix to prevent the name clash with java.lang.ClassValue. -abstract class ClassTypeValue { +public abstract class MessageClass { private final Class value; - protected ClassTypeValue(Class value) { + protected MessageClass(Class value) { this.value = value; } @@ -72,7 +70,7 @@ public boolean equals(@Nullable Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final ClassTypeValue other = (ClassTypeValue) obj; + final MessageClass other = (MessageClass) obj; return Objects.equals(this.value, other.value); } diff --git a/server/src/main/java/org/spine3/server/type/package-info.java b/server/src/main/java/org/spine3/server/type/package-info.java new file mode 100644 index 00000000000..29b6db4ea3b --- /dev/null +++ b/server/src/main/java/org/spine3/server/type/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016, TeamDev Ltd. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This package contains commonly used server-side data classes. + */ + +@ParametersAreNonnullByDefault +package org.spine3.server.type; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/main/java/org/spine3/server/validate/ByteStringFieldValidator.java b/server/src/main/java/org/spine3/server/validate/ByteStringFieldValidator.java index 97a40953899..a174442fd54 100644 --- a/server/src/main/java/org/spine3/server/validate/ByteStringFieldValidator.java +++ b/server/src/main/java/org/spine3/server/validate/ByteStringFieldValidator.java @@ -24,7 +24,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.FieldDescriptor; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.validate.options.ConstraintViolation; import java.util.List; diff --git a/server/src/main/java/org/spine3/server/validate/FieldValidator.java b/server/src/main/java/org/spine3/server/validate/FieldValidator.java index 15aba8e65a7..389268604b5 100644 --- a/server/src/main/java/org/spine3/server/validate/FieldValidator.java +++ b/server/src/main/java/org/spine3/server/validate/FieldValidator.java @@ -28,9 +28,9 @@ import com.google.protobuf.GeneratedMessage.GeneratedExtension; import com.google.protobuf.Message; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; -import org.spine3.validation.options.RequiredOption; -import org.spine3.validation.options.ValidationProto; +import org.spine3.validate.options.ConstraintViolation; +import org.spine3.validate.options.RequiredOption; +import org.spine3.validate.options.ValidationProto; import java.util.List; @@ -167,7 +167,7 @@ protected void addViolation(ConstraintViolation violation) { } private ConstraintViolation newViolation(RequiredOption option) { - final String msg = getErrorMsgFormat(option, option.getMsg()); + final String msg = getErrorMsgFormat(option, option.getMsgFormat()); final ConstraintViolation violation = ConstraintViolation.newBuilder() .setMsgFormat(msg) .setFieldPath(getFieldPath()) diff --git a/server/src/main/java/org/spine3/server/validate/FloatFieldValidatorBase.java b/server/src/main/java/org/spine3/server/validate/FloatFieldValidatorBase.java index e7bbb6ac2bf..b4e5a7a9d93 100644 --- a/server/src/main/java/org/spine3/server/validate/FloatFieldValidatorBase.java +++ b/server/src/main/java/org/spine3/server/validate/FloatFieldValidatorBase.java @@ -23,7 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.Descriptors.FieldDescriptor; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.validate.options.ConstraintViolation; /** * A base for floating point number field validators. diff --git a/server/src/main/java/org/spine3/server/validate/MessageFieldValidator.java b/server/src/main/java/org/spine3/server/validate/MessageFieldValidator.java index 50c72d488a0..204590a700e 100644 --- a/server/src/main/java/org/spine3/server/validate/MessageFieldValidator.java +++ b/server/src/main/java/org/spine3/server/validate/MessageFieldValidator.java @@ -25,11 +25,11 @@ import com.google.protobuf.Message; import com.google.protobuf.Timestamp; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; -import org.spine3.validation.options.Time; -import org.spine3.validation.options.TimeOption; -import org.spine3.validation.options.ValidOption; -import org.spine3.validation.options.ValidationProto; +import org.spine3.validate.options.ConstraintViolation; +import org.spine3.validate.options.Time; +import org.spine3.validate.options.TimeOption; +import org.spine3.validate.options.ValidOption; +import org.spine3.validate.options.ValidationProto; import java.util.List; @@ -37,8 +37,8 @@ import static org.spine3.protobuf.Messages.toAny; import static org.spine3.protobuf.Timestamps.isAfter; import static org.spine3.validate.Validate.isDefault; -import static org.spine3.validation.options.Time.FUTURE; -import static org.spine3.validation.options.Time.UNDEFINED; +import static org.spine3.validate.options.Time.FUTURE; +import static org.spine3.validate.options.Time.UNDEFINED; /** * Validates fields of type {@link Message}. @@ -128,7 +128,7 @@ private static boolean isTimeInvalid(Timestamp time, Time when, Timestamp now) { } private ConstraintViolation newTimeViolation(Timestamp fieldValue) { - final String msg = getErrorMsgFormat(timeOption, timeOption.getMsg()); + final String msg = getErrorMsgFormat(timeOption, timeOption.getMsgFormat()); final String when = timeOption.getIn().toString().toLowerCase(); final ConstraintViolation violation = ConstraintViolation.newBuilder() .setMsgFormat(msg) @@ -140,7 +140,7 @@ private ConstraintViolation newTimeViolation(Timestamp fieldValue) { } private ConstraintViolation newValidViolation(Message fieldValue, Iterable violations) { - final String msg = getErrorMsgFormat(validOption, validOption.getMsg()); + final String msg = getErrorMsgFormat(validOption, validOption.getMsgFormat()); final ConstraintViolation violation = ConstraintViolation.newBuilder() .setMsgFormat(msg) .setFieldPath(getFieldPath()) diff --git a/server/src/main/java/org/spine3/server/validate/MessageValidator.java b/server/src/main/java/org/spine3/server/validate/MessageValidator.java index 7f801bbc618..64dbb23ca8a 100644 --- a/server/src/main/java/org/spine3/server/validate/MessageValidator.java +++ b/server/src/main/java/org/spine3/server/validate/MessageValidator.java @@ -25,7 +25,7 @@ import com.google.protobuf.Message; import org.spine3.Internal; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.validate.options.ConstraintViolation; import java.util.List; diff --git a/server/src/main/java/org/spine3/server/validate/NumberFieldValidator.java b/server/src/main/java/org/spine3/server/validate/NumberFieldValidator.java index 6af1582d221..2ac60a59baf 100644 --- a/server/src/main/java/org/spine3/server/validate/NumberFieldValidator.java +++ b/server/src/main/java/org/spine3/server/validate/NumberFieldValidator.java @@ -27,13 +27,13 @@ import com.google.protobuf.Int32Value; import com.google.protobuf.Message; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; -import org.spine3.validation.options.DecimalMaxOption; -import org.spine3.validation.options.DecimalMinOption; -import org.spine3.validation.options.DigitsOption; -import org.spine3.validation.options.MaxOption; -import org.spine3.validation.options.MinOption; -import org.spine3.validation.options.ValidationProto; +import org.spine3.validate.options.ConstraintViolation; +import org.spine3.validate.options.DecimalMaxOption; +import org.spine3.validate.options.DecimalMinOption; +import org.spine3.validate.options.DigitsOption; +import org.spine3.validate.options.MaxOption; +import org.spine3.validate.options.MinOption; +import org.spine3.validate.options.ValidationProto; import java.util.List; import java.util.regex.Pattern; @@ -112,19 +112,19 @@ protected boolean isValueNotSet(V value) { private void validateRangeOptions(V value) { if (notFitToDecimalMin(value)) { addViolation( - newDecimalViolation(value, minDecimalOpt, minDecimalOpt.getMsg(), + newDecimalViolation(value, minDecimalOpt, minDecimalOpt.getMsgFormat(), minDecimalOpt.getInclusive(), minDecimalOpt.getValue())); } if (notFitToDecimalMax(value)) { addViolation( - newDecimalViolation(value, maxDecimalOpt, maxDecimalOpt.getMsg(), + newDecimalViolation(value, maxDecimalOpt, maxDecimalOpt.getMsgFormat(), maxDecimalOpt.getInclusive(), maxDecimalOpt.getValue())); } if (notFitToMin(value)) { - addViolation(newMinOrMaxViolation(value, minOption, minOption.getMsg(), minOption.getValue())); + addViolation(newMinOrMaxViolation(value, minOption, minOption.getMsgFormat(), minOption.getValue())); } if (notFitToMax(value)) { - addViolation(newMinOrMaxViolation(value, maxOption, maxOption.getMsg(), maxOption.getValue())); + addViolation(newMinOrMaxViolation(value, maxOption, maxOption.getMsgFormat(), maxOption.getValue())); } } @@ -219,7 +219,7 @@ private ConstraintViolation newMinOrMaxViolation(V value, Message option, String } private ConstraintViolation newDigitsViolation(V value) { - final String msg = getErrorMsgFormat(digitsOption, digitsOption.getMsg()); + final String msg = getErrorMsgFormat(digitsOption, digitsOption.getMsgFormat()); final String intMax = String.valueOf(digitsOption.getIntegerMax()); final String fractionMax = String.valueOf(digitsOption.getFractionMax()); final ConstraintViolation.Builder violation = ConstraintViolation.newBuilder() diff --git a/server/src/main/java/org/spine3/server/validate/StringFieldValidator.java b/server/src/main/java/org/spine3/server/validate/StringFieldValidator.java index 80742348dec..3d72cbe69e9 100644 --- a/server/src/main/java/org/spine3/server/validate/StringFieldValidator.java +++ b/server/src/main/java/org/spine3/server/validate/StringFieldValidator.java @@ -23,9 +23,9 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.Descriptors.FieldDescriptor; import org.spine3.base.FieldPath; -import org.spine3.validation.options.ConstraintViolation; -import org.spine3.validation.options.PatternOption; -import org.spine3.validation.options.ValidationProto; +import org.spine3.validate.options.ConstraintViolation; +import org.spine3.validate.options.PatternOption; +import org.spine3.validate.options.ValidationProto; import java.util.List; @@ -75,7 +75,7 @@ private void checkIfMatchesToRegexp() { } private ConstraintViolation newViolation(String fieldValue) { - final String msg = getErrorMsgFormat(patternOption, patternOption.getMsg()); + final String msg = getErrorMsgFormat(patternOption, patternOption.getMsgFormat()); final ConstraintViolation violation = ConstraintViolation.newBuilder() .setMsgFormat(msg) .addParam(regex) diff --git a/server/src/test/java/org/spine3/server/BoundedContextShould.java b/server/src/test/java/org/spine3/server/BoundedContextShould.java index 390458b1fea..1a07e74c237 100644 --- a/server/src/test/java/org/spine3/server/BoundedContextShould.java +++ b/server/src/test/java/org/spine3/server/BoundedContextShould.java @@ -22,11 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Any; -import com.google.protobuf.Duration; import com.google.protobuf.Empty; import com.google.protobuf.Message; import com.google.protobuf.StringValue; -import com.google.protobuf.Timestamp; import io.grpc.stub.StreamObserver; import org.junit.After; import org.junit.Before; @@ -34,18 +32,20 @@ import org.spine3.base.Command; import org.spine3.base.CommandContext; import org.spine3.base.CommandValidationError; -import org.spine3.base.Event; import org.spine3.base.EventContext; import org.spine3.base.Response; +import org.spine3.base.Responses; import org.spine3.base.UserId; import org.spine3.server.aggregate.Aggregate; import org.spine3.server.aggregate.AggregateRepository; import org.spine3.server.aggregate.Apply; +import org.spine3.server.command.Assign; import org.spine3.server.entity.IdFunction; -import org.spine3.server.error.UnsupportedCommandException; import org.spine3.server.event.EventBus; +import org.spine3.server.event.EventHandler; import org.spine3.server.event.EventStore; import org.spine3.server.event.GetProducerIdFromEvent; +import org.spine3.server.event.Subscribe; import org.spine3.server.procman.CommandRouted; import org.spine3.server.procman.ProcessManager; import org.spine3.server.procman.ProcessManagerRepository; @@ -53,6 +53,7 @@ import org.spine3.server.projection.ProjectionRepository; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import org.spine3.server.type.EventClass; import org.spine3.test.project.Project; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; @@ -62,21 +63,16 @@ import org.spine3.test.project.event.ProjectStarted; import org.spine3.test.project.event.TaskAdded; import org.spine3.testdata.TestAggregateIdFactory; -import org.spine3.type.EventClass; import java.util.List; import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.collect.Lists.newLinkedList; -import static com.google.protobuf.util.TimeUtil.add; import static com.google.protobuf.util.TimeUtil.getCurrentTime; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import static org.spine3.base.Identifiers.newUuid; import static org.spine3.client.UserUtil.newUserId; -import static org.spine3.protobuf.Durations.seconds; -import static org.spine3.protobuf.Messages.fromAny; -import static org.spine3.testdata.TestCommands.*; +import static org.spine3.testdata.TestCommands.createProject; +import static org.spine3.testdata.TestCommands.newCommandBus; import static org.spine3.testdata.TestEventMessageFactory.*; /** @@ -93,6 +89,20 @@ public class BoundedContextShould { private BoundedContext boundedContext; private boolean handlersRegistered = false; + private final StreamObserver responseObserver = new StreamObserver() { + @Override + public void onNext(Response response) { + } + + @Override + public void onError(Throwable throwable) { + } + + @Override + public void onCompleted() { + } + }; + @Before public void setUp() { storageFactory = InMemoryStorageFactory.getInstance(); @@ -125,32 +135,6 @@ private void registerAll() { handlersRegistered = true; } - //TODO:2016-01-25:alexander.yevsyukov: Move the command result verification tests into AggregateRepositoryShould. - - private List processRequests(Iterable requests) { - - final List results = newLinkedList(); - for (Command request : requests) { - final CommandResult result = boundedContext.process(request); - results.add(result); - } - return results; - } - - private List generateRequests() { - - final Duration delta = seconds(10); - final Timestamp time1 = getCurrentTime(); - final Timestamp time2 = add(time1, delta); - final Timestamp time3 = add(time2, delta); - - final Command createProject = createProject(userId, projectId, time1); - final Command addTask = addTask(userId, projectId, time2); - final Command startProject = startProject(userId, projectId, time3); - - return newArrayList(createProject, addTask, startProject); - } - @Test public void return_EventBus() { assertNotNull(boundedContext.getEventBus()); @@ -161,15 +145,20 @@ public void return_CommandDispatcher() { assertNotNull(boundedContext.getCommandBus()); } - @SuppressWarnings("ConstantConditions") // Passing null is the purpose of this method. - @Test(expected = NullPointerException.class) - public void throw_NPE_on_null_Command() { - boundedContext.process(null); - } - - @Test(expected = UnsupportedCommandException.class) - public void throw_exception_if_not_register_any_repositories_and_try_to_process_command() { - boundedContext.post(createProject()); + @Test + public void return_unsupported_command_response_if_no_handlers_or_dispatchers() { + boundedContext.post(createProject(), new StreamObserver() { + @Override + public void onNext(Response response) { + assertTrue(Responses.isUnsupportedCommand(response)); + } + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onCompleted() {} + }); } @Test @@ -198,31 +187,18 @@ public void post_Command() { registerAll(); final Command request = createProject(userId, projectId, getCurrentTime()); - boundedContext.post(request); - } + boundedContext.post(request, new StreamObserver() { + @Override + public void onNext(Response response) {} - private void assertCommandResultsAreValid(List requests, List results) { - assertEquals(requests.size(), results.size()); + @Override + public void onError(Throwable throwable) {} - for (int i = 0; i < requests.size(); i++) { - assertRequestAndResultMatch(requests.get(i), results.get(i)); - } + @Override + public void onCompleted() {} + }); } - private void assertRequestAndResultMatch(Command request, CommandResult result) { - final Timestamp expectedTime = request.getContext().getTimestamp(); - - final List events = result.getEventList(); - assertEquals(1, events.size()); - final Event actualRecord = events.get(0); - final ProjectId actualProjectId = fromAny(actualRecord.getContext().getProducerId()); - - assertEquals(projectId, actualProjectId); - assertEquals(userId, actualRecord.getContext().getCommandContext().getActor()); - assertEquals(expectedTime, actualRecord.getContext().getCommandContext().getTimestamp()); - } - - private static class ResponseObserver implements StreamObserver { private Response response; @@ -344,7 +320,7 @@ private ProjectAggregateRepository(BoundedContext boundedContext) { } @SuppressWarnings("UnusedParameters") // It is intended in this empty handler class. - private static class EmptyHandler implements EventHandler { + private static class EmptyHandler extends EventHandler { @Subscribe public void on(ProjectCreated event, EventContext context) { diff --git a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java index 37a4ed47157..30ac415fc1f 100644 --- a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java +++ b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java @@ -27,6 +27,8 @@ import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import static org.mockito.Mockito.spy; + import static org.spine3.testdata.TestCommands.newCommandBus; /** @@ -44,13 +46,13 @@ public static BoundedContext create() { public static BoundedContext create(StorageFactory storageFactory) { final CommandBus commandBus = newCommandBus(storageFactory); final EventBus eventBus = EventBus.newInstance(EventStore.newBuilder() - .setStreamExecutor(MoreExecutors.directExecutor()) - .setStorage(storageFactory.createEventStorage()) - .build()); + .setStreamExecutor(MoreExecutors.directExecutor()) + .setStorage(storageFactory.createEventStorage()) + .build()); final BoundedContext.Builder builder = BoundedContext.newBuilder() .setStorageFactory(storageFactory) .setCommandBus(commandBus) - .setEventBus(eventBus); + .setEventBus(spy(eventBus)); return builder.build(); } diff --git a/server/src/test/java/org/spine3/server/aggregate/AggregateRepositoryShould.java b/server/src/test/java/org/spine3/server/aggregate/AggregateRepositoryShould.java index 473c380f92a..29f707dbaee 100644 --- a/server/src/test/java/org/spine3/server/aggregate/AggregateRepositoryShould.java +++ b/server/src/test/java/org/spine3/server/aggregate/AggregateRepositoryShould.java @@ -22,20 +22,13 @@ import org.junit.Before; import org.junit.Test; -import org.spine3.base.CommandContext; -import org.spine3.server.Assign; import org.spine3.server.BoundedContext; import org.spine3.server.BoundedContextTestStubs; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; -import org.spine3.test.project.Project; +import org.spine3.server.type.CommandClass; import org.spine3.test.project.ProjectId; -import org.spine3.test.project.command.AddTask; -import org.spine3.test.project.command.CreateProject; -import org.spine3.test.project.event.ProjectCreated; -import org.spine3.test.project.event.TaskAdded; import org.spine3.testdata.ProjectAggregate; -import org.spine3.type.CommandClass; import org.spine3.validate.Validate; import java.util.Set; @@ -43,8 +36,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.spine3.testdata.TestEventMessageFactory.projectCreatedEvent; -import static org.spine3.testdata.TestEventMessageFactory.taskAddedEvent; @SuppressWarnings("InstanceMethodNamingConvention") public class AggregateRepositoryShould { diff --git a/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java b/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java index a1e5b485cb9..54218c5f365 100644 --- a/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java +++ b/server/src/test/java/org/spine3/server/aggregate/AggregateShould.java @@ -35,7 +35,7 @@ import org.spine3.base.UserId; import org.spine3.client.UserUtil; import org.spine3.protobuf.Timestamps; -import org.spine3.server.Assign; +import org.spine3.server.command.Assign; import org.spine3.test.project.Project; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java index b2d3368039c..2298911bd04 100644 --- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java +++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java @@ -29,32 +29,28 @@ import org.spine3.base.CommandId; import org.spine3.base.Commands; import org.spine3.base.Errors; -import org.spine3.base.Event; import org.spine3.base.Responses; import org.spine3.client.CommandFactory; import org.spine3.client.test.TestCommandFactory; -import org.spine3.server.Assign; -import org.spine3.server.CommandDispatcher; -import org.spine3.server.CommandHandler; -import org.spine3.server.FailureThrowable; import org.spine3.server.error.UnsupportedCommandException; +import org.spine3.server.event.EventBus; +import org.spine3.server.failure.FailureThrowable; +import org.spine3.server.type.CommandClass; import org.spine3.test.failures.Failures; import org.spine3.test.project.command.AddTask; import org.spine3.test.project.command.CreateProject; import org.spine3.test.project.command.StartProject; import org.spine3.test.project.event.ProjectCreated; -import org.spine3.type.CommandClass; import java.io.IOException; import java.util.Collections; -import java.util.List; import java.util.Set; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.spine3.base.Identifiers.newUuid; +import static org.spine3.base.Responses.isUnsupportedCommand; import static org.spine3.protobuf.Durations.minutes; -import static org.spine3.server.command.CommandValidation.isUnsupportedCommand; import static org.spine3.testdata.TestCommands.*; import static org.spine3.testdata.TestContextFactory.createCommandContext; @@ -64,8 +60,9 @@ public class CommandBusShould { private CommandBus commandBus; private CommandStore commandStore; private CommandFactory commandFactory; - private ExecutorCommandScheduler scheduler; private CommandBus.ProblemLog log; + private EventBus eventBus; + private ExecutorCommandScheduler scheduler; @Before public void setUp() { @@ -77,6 +74,7 @@ public void setUp() { .build(); log = spy(CommandBus.ProblemLog.class); commandBus.setProblemLog(log); + eventBus = mock(EventBus.class); commandFactory = TestCommandFactory.newInstance(CommandBusShould.class); } @@ -99,9 +97,7 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { - //noinspection ReturnOfNull - return null; + public void dispatch(Command request) throws Exception { } } @@ -112,11 +108,14 @@ public void do_not_accept_empty_dispatchers() { @Test(expected = IllegalArgumentException.class) public void do_not_accept_command_handlers_without_methods() { - commandBus.register(new EmptyCommandHandler()); + commandBus.register(new EmptyCommandHandler(eventBus)); } @SuppressWarnings("EmptyClass") - private static class EmptyCommandHandler implements CommandHandler { + private static class EmptyCommandHandler extends CommandHandler { + private EmptyCommandHandler(EventBus eventBus) { + super(newUuid(), eventBus); + } } // @@ -131,9 +130,7 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { - //noinspection ReturnOfNull - return null; + public void dispatch(Command request) throws Exception { } } @@ -155,9 +152,7 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { - //noinspection ReturnOfNull - return null; + public void dispatch(Command request) throws Exception { } }); @@ -179,9 +174,7 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { - //noinspection ReturnOfNull - return null; + public void dispatch(Command request) throws Exception { } }; @@ -201,27 +194,33 @@ public List dispatch(Command request) throws Exception { @Test(expected = IllegalArgumentException.class) public void do_not_allow_to_register_dispatcher_for_the_command_with_registered_handler() { - final CommandHandler createProjectHandler = new CreateProjectHandler(); + final CommandHandler createProjectHandler = new CreateProjectHandler(eventBus); final CommandDispatcher createProjectDispatcher = new CreateProjectDispatcher(); commandBus.register(createProjectHandler); + commandBus.register(createProjectDispatcher); } @Test(expected = IllegalArgumentException.class) public void do_not_allow_to_register_handler_for_the_command_with_registered_dispatcher() { - final CommandHandler createProjectHandler = new CreateProjectHandler(); + final CommandHandler createProjectHandler = new CreateProjectHandler(eventBus); final CommandDispatcher createProjectDispatcher = new CreateProjectDispatcher(); commandBus.register(createProjectDispatcher); + commandBus.register(createProjectHandler); } - private static class CreateProjectHandler implements CommandHandler { + private static class CreateProjectHandler extends CommandHandler { private boolean handlerInvoked = false; + private CreateProjectHandler(EventBus eventBus) { + super(newUuid(), eventBus); + } + @Assign public ProjectCreated handle(CreateProject command, CommandContext ctx) throws TestFailure, TestThrowable { handlerInvoked = true; @@ -240,15 +239,13 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { - //noinspection ReturnOfNull - return null; + public void dispatch(Command request) throws Exception { } } @Test public void unregister_handler() { - final CommandHandler handler = new CreateProjectHandler(); + final CommandHandler handler = new CreateProjectHandler(eventBus); commandBus.register(handler); commandBus.unregister(handler); final String projectId = newUuid(); @@ -257,7 +254,7 @@ public void unregister_handler() { @Test public void validate_commands_both_dispatched_and_handled() { - final CommandHandler handler = new CreateProjectHandler(); + final CommandHandler handler = new CreateProjectHandler(eventBus); final CommandDispatcher dispatcher = new AddTaskDispatcher(); commandBus.register(handler); commandBus.register(dispatcher); @@ -277,10 +274,8 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { + public void dispatch(Command request) throws Exception { dispatcherInvoked = true; - //noinspection ReturnOfNull - return null; } public boolean wasDispatcherInvoked() { @@ -309,7 +304,7 @@ public void shutdown_CommandScheduler_when_closed() throws Exception { @Test public void remove_all_handlers_on_close() throws Exception { - final CommandHandler handler = new CreateProjectHandler(); + final CommandHandler handler = new CreateProjectHandler(eventBus); commandBus.register(handler); commandBus.close(); @@ -318,7 +313,7 @@ public void remove_all_handlers_on_close() throws Exception { @Test public void invoke_handler_when_command_posted() { - final CreateProjectHandler handler = new CreateProjectHandler(); + final CreateProjectHandler handler = new CreateProjectHandler(eventBus); commandBus.register(handler); final Command command = commandFactory.create(createProject(newUuid())); @@ -348,15 +343,14 @@ public void throw_exception_when_there_is_no_neither_handler_nor_dispatcher() { @Test public void set_command_status_to_OK_when_handler_returns() { - final CreateProjectHandler handler = new CreateProjectHandler(); + final CreateProjectHandler handler = new CreateProjectHandler(eventBus); commandBus.register(handler); final Command command = commandFactory.create(createProject(newUuid())); commandBus.post(command); // See that we called CommandStore only once with the right command ID. - verify(commandStore, times(1)).setCommandStatusOk(command.getContext() - .getCommandId()); + verify(commandStore, times(1)).setCommandStatusOk(command.getContext().getCommandId()); } @Test @@ -384,16 +378,19 @@ public void set_command_status_to_failure_when_handler_throws_failure() throws T commandBus.post(command); // Verify we updated the status. - - verify(commandStore, times(1)).updateStatus(eq(commandId), eq(failure.toMessage())); + verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(failure.toMessage())); // Verify we logged the failure. - verify(log, times(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId)); + verify(log, atMost(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId)); } @Test public void set_command_status_to_failure_when_handler_throws_exception() throws TestFailure, TestThrowable { + final CreateProjectHandler handler = mock(CreateProjectHandler.class); final RuntimeException exception = new IllegalStateException("handler throws"); - givenThrowingHandler(exception); + doThrow(exception).when(handler) + .handle(any(CreateProject.class), any(CommandContext.class)); + + commandBus.register(handler); final Command command = commandFactory.create(createProject(newUuid())); final CommandId commandId = command.getContext().getCommandId(); final Message commandMessage = Commands.getMessage(command); @@ -401,15 +398,19 @@ public void set_command_status_to_failure_when_handler_throws_exception() throws commandBus.post(command); // Verify we updated the status. - verify(commandStore, times(1)).updateStatus(eq(commandId), eq(exception)); + verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(exception)); // Verify we logged the failure. - verify(log, times(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId)); + verify(log, atMost(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId)); } @Test public void set_command_status_to_failure_when_handler_throws_unknown_Throwable() throws TestFailure, TestThrowable { + final CreateProjectHandler handler = mock(CreateProjectHandler.class); final Throwable throwable = new TestThrowable(); - givenThrowingHandler(throwable); + doThrow(throwable).when(handler) + .handle(any(CreateProject.class), any(CommandContext.class)); + + commandBus.register(handler); final Command command = commandFactory.create(createProject(newUuid())); final CommandId commandId = command.getContext().getCommandId(); final Message commandMessage = Commands.getMessage(command); @@ -417,11 +418,12 @@ public void set_command_status_to_failure_when_handler_throws_unknown_Throwable( commandBus.post(command); // Verify we updated the status. - verify(commandStore, times(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable))); + verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable))); // Verify we logged the failure. - verify(log, times(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId)); + verify(log, atMost(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId)); } + private void givenThrowingHandler(E throwable) throws TestThrowable, TestFailure { final CreateProjectHandler handler = mock(CreateProjectHandler.class); doThrow(throwable) @@ -454,7 +456,7 @@ public void schedule_command_if_delay_is_set() { @Test public void do_not_schedule_command_if_no_scheduling_options_are_set() { - commandBus.register(new CreateProjectHandler()); + commandBus.register(new CreateProjectHandler(eventBus)); final Command cmd = commandFactory.create(createProject(newUuid())); commandBus.post(cmd); diff --git a/server/src/test/java/org/spine3/server/event/EventBusShould.java b/server/src/test/java/org/spine3/server/event/EventBusShould.java index dcab9aba90c..00111cd1336 100644 --- a/server/src/test/java/org/spine3/server/event/EventBusShould.java +++ b/server/src/test/java/org/spine3/server/event/EventBusShould.java @@ -26,14 +26,12 @@ import org.junit.Test; import org.spine3.base.Event; import org.spine3.base.EventContext; -import org.spine3.server.EventDispatcher; -import org.spine3.server.EventHandler; -import org.spine3.server.Subscribe; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import org.spine3.server.type.EventClass; import org.spine3.test.project.event.ProjectCreated; -import org.spine3.type.EventClass; +import java.util.Collection; import java.util.Set; import java.util.concurrent.Executors; @@ -83,7 +81,7 @@ public void reject_object_with_no_subscriber_methods() { /** * A simple one subscriber method handler class used in tests below. */ - private static class ProjectCreatedHandler implements EventHandler { + private static class ProjectCreatedHandler extends EventHandler { private boolean methodCalled = false; @@ -109,7 +107,7 @@ public void register_event_handler() { final EventClass eventClass = EventClass.of(ProjectCreated.class); assertTrue(eventBus.hasSubscribers(eventClass)); - final Set subscribers = eventBus.getHandlers(eventClass); + final Collection subscribers = eventBus.getHandlers(eventClass); assertTrue(subscribers.contains(handlerOne)); assertTrue(subscribers.contains(handlerTwo)); } @@ -126,7 +124,7 @@ public void unregister_handlers() { // Check that the 2nd subscriber with the same event handling method remains // after the 1st subscriber unregisters. - final Set subscribers = eventBus.getHandlers(eventClass); + final Collection subscribers = eventBus.getHandlers(eventClass); assertFalse(subscribers.contains(handlerOne)); assertTrue(subscribers.contains(handlerTwo)); @@ -221,7 +219,7 @@ public void catches_exceptions_caused_by_handlers() { /** * The handler which throws exception from the subscriber method. */ - private static class FaultyHandler implements EventHandler { + private static class FaultyHandler extends EventHandler { private boolean methodCalled = false; diff --git a/server/src/test/java/org/spine3/server/procman/ProcessManagerRepositoryShould.java b/server/src/test/java/org/spine3/server/procman/ProcessManagerRepositoryShould.java index e19e64ee0b6..e0b93dfb8a3 100644 --- a/server/src/test/java/org/spine3/server/procman/ProcessManagerRepositoryShould.java +++ b/server/src/test/java/org/spine3/server/procman/ProcessManagerRepositoryShould.java @@ -25,21 +25,24 @@ import com.google.protobuf.StringValue; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.spine3.base.Command; import org.spine3.base.CommandContext; import org.spine3.base.Commands; import org.spine3.base.Event; import org.spine3.base.EventContext; import org.spine3.base.Events; -import org.spine3.server.Assign; import org.spine3.server.BoundedContext; import org.spine3.server.BoundedContextTestStubs; -import org.spine3.server.CommandDispatcher; -import org.spine3.server.FailureThrowable; -import org.spine3.server.Subscribe; +import org.spine3.server.command.Assign; +import org.spine3.server.command.CommandDispatcher; import org.spine3.server.entity.IdFunction; import org.spine3.server.event.GetProducerIdFromEvent; +import org.spine3.server.event.Subscribe; +import org.spine3.server.failure.FailureThrowable; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import org.spine3.server.type.CommandClass; +import org.spine3.server.type.EventClass; import org.spine3.test.project.Project; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; @@ -50,15 +53,13 @@ import org.spine3.test.project.event.TaskAdded; import org.spine3.testdata.TestAggregateIdFactory; import org.spine3.testdata.TestCommands; -import org.spine3.type.CommandClass; -import org.spine3.type.EventClass; import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.List; import java.util.Set; import static org.junit.Assert.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.spine3.protobuf.Messages.fromAny; import static org.spine3.testdata.TestCommands.*; import static org.spine3.testdata.TestEventMessageFactory.*; @@ -66,16 +67,17 @@ /** * @author Alexander Litus */ -@SuppressWarnings({"InstanceMethodNamingConvention"}) +@SuppressWarnings({"InstanceMethodNamingConvention", "OverlyCoupledClass"}) public class ProcessManagerRepositoryShould { private static final ProjectId ID = TestAggregateIdFactory.newProjectId(); + private BoundedContext boundedContext; private TestProcessManagerRepository repository; @Before public void setUp() { - final BoundedContext boundedContext = BoundedContextTestStubs.create(); + boundedContext = BoundedContextTestStubs.create(); boundedContext.getCommandBus().register(new CommandDispatcher() { @Override @@ -84,10 +86,9 @@ public Set getCommandClasses() { } @Override - public List dispatch(Command request) throws Exception { + public void dispatch(Command request) throws Exception { // Simply swallow the command. We need this dispatcher for allowing Process Manager // under test to route the AddTask command. - return Collections.emptyList(); } }); @@ -132,20 +133,23 @@ public void dispatch_several_commands() throws InvocationTargetException, Failur testDispatchCommand(startProject(ID)); } - private List testDispatchCommand(Message command) throws InvocationTargetException, FailureThrowable { + private void testDispatchCommand(Message command) throws InvocationTargetException, FailureThrowable { final Command request = Commands.create(command, CommandContext.getDefaultInstance()); - final List events = repository.dispatch(request); + repository.dispatch(request); final TestProcessManager manager = repository.load(ID); assertEquals(toState(command), manager.getState()); - return events; } @Test public void dispatch_command_and_return_events() throws InvocationTargetException, FailureThrowable { - final List events = testDispatchCommand(addTask(ID)); + testDispatchCommand(addTask(ID)); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Event.class); + + verify(boundedContext.getEventBus(), times(1)).post(argumentCaptor.capture()); + + final Event event = argumentCaptor.getValue(); - assertEquals(1, events.size()); - final Event event = events.get(0); assertNotNull(event); final TaskAdded message = fromAny(event.getMessage()); assertEquals(ID, message.getProjectId()); diff --git a/server/src/test/java/org/spine3/server/procman/ProcessManagerShould.java b/server/src/test/java/org/spine3/server/procman/ProcessManagerShould.java index 3aa069178c6..d7f88454790 100644 --- a/server/src/test/java/org/spine3/server/procman/ProcessManagerShould.java +++ b/server/src/test/java/org/spine3/server/procman/ProcessManagerShould.java @@ -33,9 +33,9 @@ import org.spine3.base.EventContext; import org.spine3.base.Events; import org.spine3.protobuf.Messages; -import org.spine3.server.Assign; -import org.spine3.server.Subscribe; +import org.spine3.server.command.Assign; import org.spine3.server.command.CommandBus; +import org.spine3.server.event.Subscribe; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; import org.spine3.test.project.command.CreateProject; diff --git a/server/src/test/java/org/spine3/server/projection/ProjectionRepositoryShould.java b/server/src/test/java/org/spine3/server/projection/ProjectionRepositoryShould.java index ac606c3bb1c..d74005a2457 100644 --- a/server/src/test/java/org/spine3/server/projection/ProjectionRepositoryShould.java +++ b/server/src/test/java/org/spine3/server/projection/ProjectionRepositoryShould.java @@ -29,16 +29,16 @@ import org.spine3.base.Events; import org.spine3.server.BoundedContext; import org.spine3.server.BoundedContextTestStubs; -import org.spine3.server.Subscribe; +import org.spine3.server.event.Subscribe; import org.spine3.server.storage.EntityStorage; import org.spine3.server.storage.StorageFactory; import org.spine3.server.storage.memory.InMemoryStorageFactory; +import org.spine3.server.type.EventClass; import org.spine3.test.project.Project; import org.spine3.test.project.ProjectId; import org.spine3.test.project.event.ProjectCreated; import org.spine3.test.project.event.ProjectStarted; import org.spine3.test.project.event.TaskAdded; -import org.spine3.type.EventClass; import java.lang.reflect.InvocationTargetException; import java.util.Set; diff --git a/server/src/test/java/org/spine3/server/projection/ProjectionShould.java b/server/src/test/java/org/spine3/server/projection/ProjectionShould.java index 0e05e2c3fb6..2e5e9e8e075 100644 --- a/server/src/test/java/org/spine3/server/projection/ProjectionShould.java +++ b/server/src/test/java/org/spine3/server/projection/ProjectionShould.java @@ -26,7 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.spine3.base.EventContext; -import org.spine3.server.Subscribe; +import org.spine3.server.event.Subscribe; import static org.junit.Assert.assertTrue; import static org.spine3.protobuf.Values.newStringValue; diff --git a/server/src/test/java/org/spine3/server/internal/MessageHandlerMethodShould.java b/server/src/test/java/org/spine3/server/reflect/MessageHandlerMethodShould.java similarity index 76% rename from server/src/test/java/org/spine3/server/internal/MessageHandlerMethodShould.java rename to server/src/test/java/org/spine3/server/reflect/MessageHandlerMethodShould.java index ab1666172a7..7e902d57d85 100644 --- a/server/src/test/java/org/spine3/server/internal/MessageHandlerMethodShould.java +++ b/server/src/test/java/org/spine3/server/reflect/MessageHandlerMethodShould.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.server.internal; +package org.spine3.server.reflect; import com.google.protobuf.BoolValue; import com.google.protobuf.StringValue; @@ -35,30 +35,30 @@ @SuppressWarnings("InstanceMethodNamingConvention") public class MessageHandlerMethodShould { - private static class TwoParamMethod extends MessageHandlerMethod { + private static class TwoParamMethod extends HandlerMethod { - protected TwoParamMethod(Object target, Method method) { - super(target, method); + protected TwoParamMethod(Method method) { + super(method); } } - private static class OneParamMethod extends MessageHandlerMethod { + private static class OneParamMethod extends HandlerMethod { - protected OneParamMethod(Object target, Method method) { - super(target, method); + protected OneParamMethod(Method method) { + super(method); } } - private MessageHandlerMethod twoParamMethod; - private MessageHandlerMethod oneParamMethod; + private HandlerMethod twoParamMethod; + private HandlerMethod oneParamMethod; private Object target; @Before public void setUp() { target = new StubHandler(); - twoParamMethod = new TwoParamMethod(target, StubHandler.getTwoParameterMethod()); - oneParamMethod = new OneParamMethod(target, StubHandler.getOneParameterMethod()); + twoParamMethod = new TwoParamMethod(StubHandler.getTwoParameterMethod()); + oneParamMethod = new OneParamMethod(StubHandler.getOneParameterMethod()); } @SuppressWarnings("UnusedParameters") // OK for test methods. @@ -107,21 +107,10 @@ private void handle(BoolValue message) { } } - @Test(expected = NullPointerException.class) - public void do_not_accept_null_target() { - //noinspection ConstantConditions,ResultOfObjectAllocationIgnored - new TwoParamMethod(null, StubHandler.getTwoParameterMethod()); - } - @Test(expected = NullPointerException.class) public void do_not_accept_null_method() { //noinspection ConstantConditions,ResultOfObjectAllocationIgnored - new TwoParamMethod(new StubHandler(), null); - } - - @Test - public void return_target() { - assertEquals(target, twoParamMethod.getTarget()); + new TwoParamMethod(null); } @Test @@ -141,14 +130,14 @@ public void check_if_private() { @Test public void invoke_the_method_with_two_parameters() throws InvocationTargetException { - twoParamMethod.invoke(StringValue.getDefaultInstance(), EventContext.getDefaultInstance()); + twoParamMethod.invoke(target, StringValue.getDefaultInstance(), EventContext.getDefaultInstance()); assertTrue(((StubHandler)target).wasOnInvoked()); } @Test public void invoke_the_method_with_one_parameter() throws InvocationTargetException { - oneParamMethod.invoke(BoolValue.getDefaultInstance()); + oneParamMethod.invoke(target, BoolValue.getDefaultInstance()); assertTrue(((StubHandler)target).wasHandleInvoked()); } @@ -178,8 +167,8 @@ public void be_not_equal_to_another_class_instance() { @Test public void compare_fields_in_equals() { - final MessageHandlerMethod anotherMethod = - new TwoParamMethod(target, StubHandler.getTwoParameterMethod()); + final HandlerMethod anotherMethod = + new TwoParamMethod(StubHandler.getTwoParameterMethod()); assertTrue(twoParamMethod.equals(anotherMethod)); } diff --git a/server/src/test/java/org/spine3/server/reflect/MethodMapShould.java b/server/src/test/java/org/spine3/server/reflect/MethodMapShould.java index 139f9667315..ce0d017b6d5 100644 --- a/server/src/test/java/org/spine3/server/reflect/MethodMapShould.java +++ b/server/src/test/java/org/spine3/server/reflect/MethodMapShould.java @@ -22,9 +22,8 @@ import org.junit.Test; import org.spine3.base.CommandContext; -import org.spine3.server.Assign; import org.spine3.server.aggregate.Aggregate; -import org.spine3.server.internal.CommandHandlerMethod; +import org.spine3.server.command.Assign; import org.spine3.test.project.Project; import org.spine3.test.project.command.CreateProject; import org.spine3.test.project.event.ProjectCreated; @@ -51,7 +50,7 @@ public ProjectCreated handle(CreateProject command, CommandContext context) { @Test public void expose_key_set() { - final MethodMap methodMap = new MethodMap(TestAggregate.class, CommandHandlerMethod.PREDICATE); + final MethodMap methodMap = MethodMap.create(TestAggregate.class, CommandHandlerMethod.factory()); assertFalse(methodMap.keySet().isEmpty()); } } diff --git a/server/src/test/java/org/spine3/server/reflect/UtilitiesShould.java b/server/src/test/java/org/spine3/server/reflect/UtilitiesShould.java index 0ea0e480062..d174d8befa4 100644 --- a/server/src/test/java/org/spine3/server/reflect/UtilitiesShould.java +++ b/server/src/test/java/org/spine3/server/reflect/UtilitiesShould.java @@ -61,7 +61,6 @@ public void have_private_constructors() { checkPrivateConstructor(Identifiers.class); checkPrivateConstructor(IoUtil.class); checkPrivateConstructor(Math.class); - checkPrivateConstructor(Methods.class); checkPrivateConstructor(Tests.class); // testutil package diff --git a/client/src/test/java/org/spine3/type/EventClassShould.java b/server/src/test/java/org/spine3/server/type/EventClassShould.java similarity index 97% rename from client/src/test/java/org/spine3/type/EventClassShould.java rename to server/src/test/java/org/spine3/server/type/EventClassShould.java index 779a7862254..402756df65a 100644 --- a/client/src/test/java/org/spine3/type/EventClassShould.java +++ b/server/src/test/java/org/spine3/server/type/EventClassShould.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.type; +package org.spine3.server.type; import com.google.protobuf.BoolValue; import com.google.protobuf.Int32Value; diff --git a/client/src/test/java/org/spine3/type/ClassTypeValueShould.java b/server/src/test/java/org/spine3/server/type/MessageClassShould.java similarity index 77% rename from client/src/test/java/org/spine3/type/ClassTypeValueShould.java rename to server/src/test/java/org/spine3/server/type/MessageClassShould.java index b1567f3c7f4..080f4aa2d50 100644 --- a/client/src/test/java/org/spine3/type/ClassTypeValueShould.java +++ b/server/src/test/java/org/spine3/server/type/MessageClassShould.java @@ -18,39 +18,41 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.spine3.type; +package org.spine3.server.type; import com.google.protobuf.Message; import com.google.protobuf.StringValue; import com.google.protobuf.UInt32Value; +import org.junit.Assert; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; -public class ClassTypeValueShould { +public class MessageClassShould { @Test public void return_enclosed_value() { final Class expected = StringValue.class; - assertEquals(expected, new ClassTypeValue(expected) {}.value()); + Assert.assertEquals(expected, new MessageClass(expected) {}.value()); } @Test public void be_not_equal_to_null() { //noinspection ObjectEqualsNull - assertFalse(new ClassTypeValue(UInt32Value.class){}.equals(null)); + assertFalse(new MessageClass(UInt32Value.class){}.equals(null)); } @Test public void be_not_equal_to_object_of_another_class() { // Notice that we're creating new inner classes here with the same value passed. //noinspection EqualsBetweenInconvertibleTypes - assertFalse(new ClassTypeValue(StringValue.class){}.equals(new ClassTypeValue(StringValue.class){})); + assertFalse(new MessageClass(StringValue.class){}.equals(new MessageClass(StringValue.class){})); } @Test public void be_equal_to_self() { - final ClassTypeValue test = new ClassTypeValue(UInt32Value.class){}; + final MessageClass test = new MessageClass(UInt32Value.class){}; //noinspection EqualsWithItself assertTrue(test.equals(test)); } diff --git a/server/src/test/java/org/spine3/server/validate/FieldValidatorFactoryShould.java b/server/src/test/java/org/spine3/server/validate/FieldValidatorFactoryShould.java index 86f37ca7334..0d0fba1444d 100644 --- a/server/src/test/java/org/spine3/server/validate/FieldValidatorFactoryShould.java +++ b/server/src/test/java/org/spine3/server/validate/FieldValidatorFactoryShould.java @@ -28,9 +28,9 @@ import com.google.protobuf.StringValue; import org.junit.Test; import org.spine3.base.FieldPath; -import org.spine3.test.validation.msg.AnnotatedEnumFieldValue; -import org.spine3.test.validation.msg.RequiredByteStringFieldValue; -import org.spine3.test.validation.msg.RequiredMsgFieldValue; +import org.spine3.test.validate.msg.AnnotatedEnumFieldValue; +import org.spine3.test.validate.msg.RequiredByteStringFieldValue; +import org.spine3.test.validate.msg.RequiredMsgFieldValue; import static com.google.protobuf.Descriptors.FieldDescriptor; import static org.junit.Assert.assertTrue; diff --git a/server/src/test/java/org/spine3/server/validate/MessageValidatorShould.java b/server/src/test/java/org/spine3/server/validate/MessageValidatorShould.java index 85670df1fd1..2db28876a83 100644 --- a/server/src/test/java/org/spine3/server/validate/MessageValidatorShould.java +++ b/server/src/test/java/org/spine3/server/validate/MessageValidatorShould.java @@ -30,33 +30,33 @@ import org.spine3.base.FieldPath; import org.spine3.protobuf.Durations; import org.spine3.protobuf.Values; -import org.spine3.test.validation.msg.AnnotatedBooleanFieldValue; -import org.spine3.test.validation.msg.AnnotatedEnumFieldValue; -import org.spine3.test.validation.msg.DecimalMaxIncNumberFieldValue; -import org.spine3.test.validation.msg.DecimalMaxNotIncNumberFieldValue; -import org.spine3.test.validation.msg.DecimalMinIncNumberFieldValue; -import org.spine3.test.validation.msg.DecimalMinNotIncNumberFieldValue; -import org.spine3.test.validation.msg.DigitsCountNumberFieldValue; -import org.spine3.test.validation.msg.EnclosedMessageFieldValue; -import org.spine3.test.validation.msg.EnclosedMessageWithoutAnnotationFieldValue; -import org.spine3.test.validation.msg.EntityIdByteStringFieldValue; -import org.spine3.test.validation.msg.EntityIdDoubleFieldValue; -import org.spine3.test.validation.msg.EntityIdIntFieldValue; -import org.spine3.test.validation.msg.EntityIdLongFieldValue; -import org.spine3.test.validation.msg.EntityIdMsgFieldValue; -import org.spine3.test.validation.msg.EntityIdRepeatedFieldValue; -import org.spine3.test.validation.msg.EntityIdStringFieldValue; -import org.spine3.test.validation.msg.MaxNumberFieldValue; -import org.spine3.test.validation.msg.MinNumberFieldValue; -import org.spine3.test.validation.msg.PatternStringFieldValue; -import org.spine3.test.validation.msg.RepeatedRequiredMsgFieldValue; -import org.spine3.test.validation.msg.RequiredByteStringFieldValue; -import org.spine3.test.validation.msg.RequiredMsgFieldValue; -import org.spine3.test.validation.msg.RequiredStringFieldValue; -import org.spine3.test.validation.msg.TimeInFutureFieldValue; -import org.spine3.test.validation.msg.TimeInPastFieldValue; -import org.spine3.test.validation.msg.TimeWithoutOptsFieldValue; -import org.spine3.validation.options.ConstraintViolation; +import org.spine3.test.validate.msg.AnnotatedBooleanFieldValue; +import org.spine3.test.validate.msg.AnnotatedEnumFieldValue; +import org.spine3.test.validate.msg.DecimalMaxIncNumberFieldValue; +import org.spine3.test.validate.msg.DecimalMaxNotIncNumberFieldValue; +import org.spine3.test.validate.msg.DecimalMinIncNumberFieldValue; +import org.spine3.test.validate.msg.DecimalMinNotIncNumberFieldValue; +import org.spine3.test.validate.msg.DigitsCountNumberFieldValue; +import org.spine3.test.validate.msg.EnclosedMessageFieldValue; +import org.spine3.test.validate.msg.EnclosedMessageWithoutAnnotationFieldValue; +import org.spine3.test.validate.msg.EntityIdByteStringFieldValue; +import org.spine3.test.validate.msg.EntityIdDoubleFieldValue; +import org.spine3.test.validate.msg.EntityIdIntFieldValue; +import org.spine3.test.validate.msg.EntityIdLongFieldValue; +import org.spine3.test.validate.msg.EntityIdMsgFieldValue; +import org.spine3.test.validate.msg.EntityIdRepeatedFieldValue; +import org.spine3.test.validate.msg.EntityIdStringFieldValue; +import org.spine3.test.validate.msg.MaxNumberFieldValue; +import org.spine3.test.validate.msg.MinNumberFieldValue; +import org.spine3.test.validate.msg.PatternStringFieldValue; +import org.spine3.test.validate.msg.RepeatedRequiredMsgFieldValue; +import org.spine3.test.validate.msg.RequiredByteStringFieldValue; +import org.spine3.test.validate.msg.RequiredMsgFieldValue; +import org.spine3.test.validate.msg.RequiredStringFieldValue; +import org.spine3.test.validate.msg.TimeInFutureFieldValue; +import org.spine3.test.validate.msg.TimeInPastFieldValue; +import org.spine3.test.validate.msg.TimeWithoutOptsFieldValue; +import org.spine3.validate.options.ConstraintViolation; import java.util.List; diff --git a/server/src/test/java/org/spine3/testdata/ProjectAggregate.java b/server/src/test/java/org/spine3/testdata/ProjectAggregate.java index 0219abb0083..ced19aba786 100644 --- a/server/src/test/java/org/spine3/testdata/ProjectAggregate.java +++ b/server/src/test/java/org/spine3/testdata/ProjectAggregate.java @@ -21,8 +21,8 @@ package org.spine3.testdata; import org.spine3.base.CommandContext; -import org.spine3.server.Assign; import org.spine3.server.aggregate.Aggregate; +import org.spine3.server.command.Assign; import org.spine3.test.project.Project; import org.spine3.test.project.ProjectId; import org.spine3.test.project.command.AddTask; diff --git a/server/src/test/proto/spine/test/validation/msg/commands.proto b/server/src/test/proto/spine/test/validate/msg/commands.proto similarity index 95% rename from server/src/test/proto/spine/test/validation/msg/commands.proto rename to server/src/test/proto/spine/test/validate/msg/commands.proto index 5962a8e0d1b..fe2e791d434 100644 --- a/server/src/test/proto/spine/test/validation/msg/commands.proto +++ b/server/src/test/proto/spine/test/validate/msg/commands.proto @@ -19,12 +19,12 @@ // syntax = "proto3"; -package spine.test.validation.msg; +package spine.test.validate.msg; option java_generate_equals_and_hash = true; option java_multiple_files = true; option java_outer_classname = "ValidationTestCommandsProto"; -option java_package = "org.spine3.test.validation.msg"; +option java_package = "org.spine3.test.validate.msg"; import "google/protobuf/wrappers.proto"; import "spine/validation.proto"; diff --git a/server/src/test/proto/spine/test/validation/msg/messages.proto b/server/src/test/proto/spine/test/validate/msg/messages.proto similarity index 97% rename from server/src/test/proto/spine/test/validation/msg/messages.proto rename to server/src/test/proto/spine/test/validate/msg/messages.proto index d8bb6e9c16b..c42c476e065 100644 --- a/server/src/test/proto/spine/test/validation/msg/messages.proto +++ b/server/src/test/proto/spine/test/validate/msg/messages.proto @@ -19,12 +19,12 @@ // syntax = "proto3"; -package spine.test.validation.msg; +package spine.test.validate.msg; option java_generate_equals_and_hash = true; option java_multiple_files = true; option java_outer_classname = "ValidationTestMessagesProto"; -option java_package = "org.spine3.test.validation.msg"; +option java_package = "org.spine3.test.validate.msg"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; diff --git a/server/src/test/proto/spine/test/validation/msg/stub_aggregate.proto b/server/src/test/proto/spine/test/validate/msg/stub_aggregate.proto similarity index 93% rename from server/src/test/proto/spine/test/validation/msg/stub_aggregate.proto rename to server/src/test/proto/spine/test/validate/msg/stub_aggregate.proto index bb8db3bea14..9d9c4c8f865 100644 --- a/server/src/test/proto/spine/test/validation/msg/stub_aggregate.proto +++ b/server/src/test/proto/spine/test/validate/msg/stub_aggregate.proto @@ -19,11 +19,11 @@ // syntax = "proto3"; -package spine.test.validation.msg; +package spine.test.validate.msg; option java_generate_equals_and_hash = true; option java_multiple_files = true; -option java_package = "org.spine3.test.validation.msg"; +option java_package = "org.spine3.test.validate.msg"; import "spine/entity.proto"; diff --git a/values/build.gradle b/values/build.gradle index 205c7bd4370..504f638579f 100644 --- a/values/build.gradle +++ b/values/build.gradle @@ -4,7 +4,7 @@ buildscript { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } dependencies { - classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.2', changing: true + classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.3.1', changing: true } } @@ -36,9 +36,10 @@ protobuf { generateProtoTasks { all().each { final task -> task.plugins { - grpc { -// option 'nano=true' - } + grpc {} + task.generateDescriptorSet = true + task.descriptorSetOptions.path = "${projectDir}/build/descriptors/${task.sourceSet.name}.desc" + task.descriptorSetOptions.includeImports = true } } } From a2567acce16990bdee34ef1f2f18887f07945a63 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 17:32:29 +0300 Subject: [PATCH 19/26] Merge branch 'master' into this one. --- server/src/main/java/org/spine3/server/command/CommandBus.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java index 7cb22537b6e..646a2c1e96c 100644 --- a/server/src/main/java/org/spine3/server/command/CommandBus.java +++ b/server/src/main/java/org/spine3/server/command/CommandBus.java @@ -35,7 +35,6 @@ import org.spine3.server.type.CommandClass; import org.spine3.server.validate.MessageValidator; import org.spine3.validate.options.ConstraintViolation; -import org.spine3.type.CommandClass; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; From 5be63438f17d662556292d161ffb9deac30697ee Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 18:05:53 +0300 Subject: [PATCH 20/26] 0.3 version. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4855b483023..ee199c9c158 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ allprojects { apply plugin: 'jacoco' group = 'org.spine3' - version = '0.2' + version = '0.3' } project.ext { From 203390e2145c9869c73a2a1c35116a8799083121 Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 18:24:40 +0300 Subject: [PATCH 21/26] Update protobuf-plugin dep-cy. --- client/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/build.gradle b/client/build.gradle index 7491242a5e7..c775db48eb0 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -4,7 +4,7 @@ buildscript { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } dependencies { - classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.2', changing: true + classpath group: 'org.spine3.tools', name: 'protobuf-plugin', version: '1.3.1', changing: true } } From 5384729d0b0aa8d7b8836b17cd4cea42ae51a42e Mon Sep 17 00:00:00 2001 From: Alexander Litus Date: Tue, 19 Apr 2016 18:27:35 +0300 Subject: [PATCH 22/26] Remove redundant tag in docs. --- client/src/main/java/org/spine3/protobuf/Durations.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/org/spine3/protobuf/Durations.java b/client/src/main/java/org/spine3/protobuf/Durations.java index 6556e08e8d1..c14ea52ef16 100644 --- a/client/src/main/java/org/spine3/protobuf/Durations.java +++ b/client/src/main/java/org/spine3/protobuf/Durations.java @@ -19,7 +19,7 @@ /** * Utility class for working with durations in addition to those available from {@link TimeUtil}. - *

+ * *

Use {@code import static org.spine3.protobuf.Durations.*} for compact initialization like this: *

  *      Duration d = add(hours(2), minutes(30));
@@ -87,6 +87,7 @@ public static Duration ofHours(long hours) {
 
     // Methods for brief computations with Durations like
     //       add(hours(2), minutes(30));
+    /////////////////////////////////////////////////////
 
     /**
      * Obtains an instance of {@code Duration} representing the passed number of nanoseconds.

From 9193013a9e0e91d6b549eff59537788466a12815 Mon Sep 17 00:00:00 2001
From: Alexander Litus 
Date: Tue, 19 Apr 2016 18:33:17 +0300
Subject: [PATCH 23/26] Minor changes.

---
 .../src/main/java/org/spine3/server/reflect/MethodMap.java   | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/server/src/main/java/org/spine3/server/reflect/MethodMap.java b/server/src/main/java/org/spine3/server/reflect/MethodMap.java
index 4d8b7932690..e097bb4065d 100644
--- a/server/src/main/java/org/spine3/server/reflect/MethodMap.java
+++ b/server/src/main/java/org/spine3/server/reflect/MethodMap.java
@@ -93,9 +93,8 @@ private static Map, Method> scan(Class declaringClas
                 tempMap.put(messageClass, method);
             }
         }
-        final ImmutableMap.Builder, Method> builder = ImmutableMap.builder();
-        builder.putAll(tempMap);
-        return builder.build();
+        final ImmutableMap, Method> result = ImmutableMap.copyOf(tempMap);
+        return result;
     }
 
     /**

From e225e12d6bc92130d076505d73812acdccb6999f Mon Sep 17 00:00:00 2001
From: Alexander Litus 
Date: Tue, 19 Apr 2016 18:36:07 +0300
Subject: [PATCH 24/26] Remove redundant code.

---
 values/build.gradle | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/values/build.gradle b/values/build.gradle
index 504f638579f..310100b1162 100644
--- a/values/build.gradle
+++ b/values/build.gradle
@@ -28,15 +28,9 @@ sourceSets {
 }
 
 protobuf {
-    plugins {
-        grpc {
-            artifact = 'io.grpc:protoc-gen-grpc-java:0.13.1'
-        }
-    }
     generateProtoTasks {
         all().each { final task ->
             task.plugins {
-                grpc {}
                 task.generateDescriptorSet = true
                 task.descriptorSetOptions.path = "${projectDir}/build/descriptors/${task.sourceSet.name}.desc"
                 task.descriptorSetOptions.includeImports = true

From cdb9a1eebb639cf20f3886ba3c377d593c2eb03a Mon Sep 17 00:00:00 2001
From: Alexander Litus 
Date: Wed, 20 Apr 2016 11:49:24 +0300
Subject: [PATCH 25/26] Add a method to Responses.

---
 .../main/java/org/spine3/base/Responses.java  | 21 +++++++++++++
 .../java/org/spine3/base/ResponsesShould.java | 30 +++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/client/src/main/java/org/spine3/base/Responses.java b/client/src/main/java/org/spine3/base/Responses.java
index 0a8a5e602ec..d1eaf4ebac6 100644
--- a/client/src/main/java/org/spine3/base/Responses.java
+++ b/client/src/main/java/org/spine3/base/Responses.java
@@ -22,6 +22,8 @@
 
 import com.google.protobuf.Empty;
 
+import static org.spine3.protobuf.Messages.fromAny;
+
 /**
  * Utilities for working with {@link org.spine3.base.Response} objects.
  *
@@ -47,6 +49,8 @@ public static Response ok() {
     }
 
     /**
+     * Checks if the response is OK.
+     *
      * @return {@code true} if the passed response represents `ok` status,
      *         {@code false} otherwise
      */
@@ -56,6 +60,8 @@ public static boolean isOk(Response response) {
     }
 
     /**
+     * Checks if the response is `unsupported command`.
+     *
      * @return {@code true} if the passed response represents `unsupported command` error,
      *         {@code false} otherwise
      */
@@ -67,4 +73,19 @@ public static boolean isUnsupportedCommand(Response response) {
         }
         return false;
     }
+
+    /**
+     * Checks if the response is `invalid command`.
+     *
+     * @return {@code true} if the passed response represents `invalid command` error,
+     *         {@code false} otherwise
+     */
+    public static boolean isInvalidCommand(Response response) {
+        if (response.getStatusCase() == Response.StatusCase.FAILURE) {
+            final ValidationFailure failure = fromAny(response.getFailure().getInstance());
+            final boolean isInvalid = !failure.getConstraintViolationList().isEmpty();
+            return isInvalid;
+        }
+        return false;
+    }
 }
diff --git a/client/src/test/java/org/spine3/base/ResponsesShould.java b/client/src/test/java/org/spine3/base/ResponsesShould.java
index 427fe545c4e..b99833fbed7 100644
--- a/client/src/test/java/org/spine3/base/ResponsesShould.java
+++ b/client/src/test/java/org/spine3/base/ResponsesShould.java
@@ -21,10 +21,15 @@
 package org.spine3.base;
 
 import org.junit.Test;
+import org.spine3.validate.options.ConstraintViolation;
+
+import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.newArrayList;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.spine3.protobuf.Messages.toAny;
 import static org.spine3.test.Tests.hasPrivateUtilityConstructor;
 
 @SuppressWarnings("InstanceMethodNamingConvention")
@@ -35,6 +40,8 @@ public class ResponsesShould {
                            .setCode(CommandValidationError.UNSUPPORTED_COMMAND.getNumber()))
             .build();
 
+    private static final Response RESPONSE_INVALID_COMMAND = newInvalidCommandResponse();
+
     @Test
     public void have_private_constructor() {
         assertTrue(hasPrivateUtilityConstructor(Responses.class));
@@ -64,4 +71,27 @@ public void recognize_UNSUPPORTED_COMMAND_response() {
     public void return_false_if_not_UNSUPPORTED_COMMAND_response() {
         assertFalse(Responses.isUnsupportedCommand(Responses.ok()));
     }
+
+    @Test
+    public void recognize_INVALID_COMMAND_response() {
+        assertTrue(Responses.isInvalidCommand(RESPONSE_INVALID_COMMAND));
+    }
+
+    @Test
+    public void return_false_if_not_INVALID_COMMAND_response() {
+        assertFalse(Responses.isInvalidCommand(Responses.ok()));
+    }
+
+    private static Response newInvalidCommandResponse() {
+        final List violations = newArrayList(ConstraintViolation.getDefaultInstance());
+        final ValidationFailure failureInstance = ValidationFailure.newBuilder()
+                                                                   .addAllConstraintViolation(violations)
+                                                                   .build();
+        final Failure.Builder failure = Failure.newBuilder()
+                                               .setInstance(toAny(failureInstance));
+        final Response response = Response.newBuilder()
+                                          .setFailure(failure)
+                                          .build();
+        return response;
+    }
 }

From e0e8215c1a713a722e17dbdfafeca2eb7bbfd1fc Mon Sep 17 00:00:00 2001
From: Alexander Litus 
Date: Wed, 20 Apr 2016 11:54:29 +0300
Subject: [PATCH 26/26] Fix and add command bus tests.

---
 .../org/spine3/server/command/CommandBus.java |  14 +-
 .../server/BoundedContextBuilderShould.java   |  10 +-
 .../spine3/server/BoundedContextShould.java   |  15 +-
 .../server/BoundedContextTestStubs.java       |  11 +-
 .../server/command/CommandBusShould.java      | 146 +++++++++++-------
 .../org/spine3/testdata/TestCommands.java     |  14 +-
 .../org/spine3/testdata/TestEventFactory.java |  16 ++
 7 files changed, 130 insertions(+), 96 deletions(-)

diff --git a/server/src/main/java/org/spine3/server/command/CommandBus.java b/server/src/main/java/org/spine3/server/command/CommandBus.java
index 646a2c1e96c..b54da078e83 100644
--- a/server/src/main/java/org/spine3/server/command/CommandBus.java
+++ b/server/src/main/java/org/spine3/server/command/CommandBus.java
@@ -65,6 +65,7 @@ public class CommandBus implements AutoCloseable {
     
     private ProblemLog problemLog = new ProblemLog();
     private final CommandStatusService commandStatusService;
+    private MessageValidator messageValidator;
 
     private CommandBus(Builder builder) {
         commandStore = builder.getCommandStore();
@@ -73,6 +74,7 @@ private CommandBus(Builder builder) {
             scheduler.setPostFunction(newPostFunction());
         }
         commandStatusService = new CommandStatusService(commandStore);
+        messageValidator = new MessageValidator();
     }
 
     /**
@@ -145,8 +147,7 @@ public Response validate(Message command) {
         if (isUnsupportedCommand(commandClass)) {
             return unsupportedCommand(command);
         }
-        final MessageValidator validator = new MessageValidator();
-        final List violations = validator.validate(command);
+        final List violations = messageValidator.validate(command);
         if (!violations.isEmpty()) {
             return invalidCommand(command, violations);
         }
@@ -277,6 +278,11 @@ private void schedule(Command command) {
         this.problemLog = problemLog;
     }
 
+    @VisibleForTesting
+    /* package */ void setMessageValidator(MessageValidator messageValidator) {
+        this.messageValidator = messageValidator;
+    }
+
     private void store(Command request) {
         commandStore.store(request);
     }
@@ -300,9 +306,7 @@ private Function newPostFunction() {
             @Nullable
             @Override
             public Command apply(@Nullable Command command) {
-                if (command == null) {
-                    return null;
-                }
+                //noinspection ConstantConditions
                 post(command);
                 return command;
             }
diff --git a/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java b/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java
index 173412cbd5e..92a6ebbed5b 100644
--- a/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java
+++ b/server/src/test/java/org/spine3/server/BoundedContextBuilderShould.java
@@ -20,18 +20,17 @@
 
 package org.spine3.server;
 
-import com.google.common.util.concurrent.MoreExecutors;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.spine3.server.command.CommandBus;
 import org.spine3.server.event.EventBus;
-import org.spine3.server.event.EventStore;
 import org.spine3.server.storage.StorageFactory;
 import org.spine3.server.storage.memory.InMemoryStorageFactory;
 
 import static org.junit.Assert.*;
 import static org.spine3.testdata.TestCommands.newCommandBus;
+import static org.spine3.testdata.TestEventFactory.newEventBus;
 
 @SuppressWarnings("InstanceMethodNamingConvention")
 public class BoundedContextBuilderShould {
@@ -99,11 +98,4 @@ public void do_not_accept_null_EventBus() {
         //noinspection ConstantConditions
         BoundedContext.newBuilder().setEventBus(null);
     }
-
-    private static EventBus newEventBus(StorageFactory storageFactory) {
-        return EventBus.newInstance(EventStore.newBuilder()
-                                              .setStreamExecutor(MoreExecutors.directExecutor())
-                                              .setStorage(storageFactory.createEventStorage())
-                                              .build());
-    }
 }
diff --git a/server/src/test/java/org/spine3/server/BoundedContextShould.java b/server/src/test/java/org/spine3/server/BoundedContextShould.java
index 4463b4a2122..56b9d0dfb24 100644
--- a/server/src/test/java/org/spine3/server/BoundedContextShould.java
+++ b/server/src/test/java/org/spine3/server/BoundedContextShould.java
@@ -20,7 +20,6 @@
 
 package org.spine3.server;
 
-import com.google.common.util.concurrent.MoreExecutors;
 import com.google.protobuf.Any;
 import com.google.protobuf.Empty;
 import com.google.protobuf.Message;
@@ -40,13 +39,8 @@
 import org.spine3.server.aggregate.AggregateRepository;
 import org.spine3.server.aggregate.Apply;
 import org.spine3.server.command.Assign;
-import org.spine3.server.command.Assign;
-import org.spine3.server.command.CommandBus;
-import org.spine3.server.command.CommandStore;
 import org.spine3.server.entity.IdFunction;
-import org.spine3.server.event.EventBus;
 import org.spine3.server.event.EventHandler;
-import org.spine3.server.event.EventStore;
 import org.spine3.server.event.GetProducerIdFromEvent;
 import org.spine3.server.event.Subscribe;
 import org.spine3.server.procman.CommandRouted;
@@ -75,8 +69,8 @@
 import static org.spine3.base.Identifiers.newUuid;
 import static org.spine3.client.UserUtil.newUserId;
 import static org.spine3.testdata.TestCommands.createProject;
-import static org.spine3.testdata.TestCommands.createProject;
 import static org.spine3.testdata.TestCommands.newCommandBus;
+import static org.spine3.testdata.TestEventFactory.newEventBus;
 import static org.spine3.testdata.TestEventMessageFactory.*;
 
 /**
@@ -113,13 +107,6 @@ public void setUp() {
         boundedContext = BoundedContextTestStubs.create(storageFactory);
     }
 
-    private static EventBus newEventBus(StorageFactory storageFactory) {
-        return EventBus.newInstance(EventStore.newBuilder()
-                                              .setStreamExecutor(MoreExecutors.directExecutor())
-                                              .setStorage(storageFactory.createEventStorage())
-                                              .build());
-    }
-
     @After
     public void tearDown() throws Exception {
         if (handlersRegistered) {
diff --git a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java
index dc155c69a1f..98da6e59f80 100644
--- a/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java
+++ b/server/src/test/java/org/spine3/server/BoundedContextTestStubs.java
@@ -20,18 +20,14 @@
 
 package org.spine3.server;
 
-import com.google.common.util.concurrent.MoreExecutors;
 import org.spine3.server.command.CommandBus;
 import org.spine3.server.event.EventBus;
-import org.spine3.server.event.EventStore;
 import org.spine3.server.storage.StorageFactory;
 import org.spine3.server.storage.memory.InMemoryStorageFactory;
 
 import static org.mockito.Mockito.spy;
-
 import static org.spine3.testdata.TestCommands.newCommandBus;
-
-import static org.mockito.Mockito.spy;
+import static org.spine3.testdata.TestEventFactory.newEventBus;
 
 /**
  * Creates stubs with instances of {@link BoundedContext} for testing purposes.
@@ -47,10 +43,7 @@ public static BoundedContext create() {
 
     public static BoundedContext create(StorageFactory storageFactory) {
         final CommandBus commandBus = newCommandBus(storageFactory);
-        final EventBus eventBus = EventBus.newInstance(EventStore.newBuilder()
-                                                                 .setStreamExecutor(MoreExecutors.directExecutor())
-                                                                 .setStorage(storageFactory.createEventStorage())
-                                                                 .build());
+        final EventBus eventBus = newEventBus(storageFactory);
         final BoundedContext.Builder builder = BoundedContext.newBuilder()
                 .setStorageFactory(storageFactory)
                 .setCommandBus(commandBus)
diff --git a/server/src/test/java/org/spine3/server/command/CommandBusShould.java b/server/src/test/java/org/spine3/server/command/CommandBusShould.java
index 2298911bd04..4f70704a8da 100644
--- a/server/src/test/java/org/spine3/server/command/CommandBusShould.java
+++ b/server/src/test/java/org/spine3/server/command/CommandBusShould.java
@@ -29,32 +29,41 @@
 import org.spine3.base.CommandId;
 import org.spine3.base.Commands;
 import org.spine3.base.Errors;
+import org.spine3.base.Response;
 import org.spine3.base.Responses;
 import org.spine3.client.CommandFactory;
 import org.spine3.client.test.TestCommandFactory;
 import org.spine3.server.error.UnsupportedCommandException;
 import org.spine3.server.event.EventBus;
 import org.spine3.server.failure.FailureThrowable;
+import org.spine3.server.storage.memory.InMemoryStorageFactory;
 import org.spine3.server.type.CommandClass;
+import org.spine3.server.validate.MessageValidator;
 import org.spine3.test.failures.Failures;
 import org.spine3.test.project.command.AddTask;
 import org.spine3.test.project.command.CreateProject;
 import org.spine3.test.project.command.StartProject;
 import org.spine3.test.project.event.ProjectCreated;
+import org.spine3.testdata.TestEventFactory;
+import org.spine3.validate.options.ConstraintViolation;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Set;
 
+import static com.google.common.collect.Lists.newArrayList;
 import static org.junit.Assert.*;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.*;
 import static org.spine3.base.Identifiers.newUuid;
+import static org.spine3.base.Responses.isInvalidCommand;
 import static org.spine3.base.Responses.isUnsupportedCommand;
+import static org.spine3.protobuf.Durations.milliseconds;
 import static org.spine3.protobuf.Durations.minutes;
 import static org.spine3.testdata.TestCommands.*;
 import static org.spine3.testdata.TestContextFactory.createCommandContext;
 
-@SuppressWarnings({"InstanceMethodNamingConvention", "ClassWithTooManyMethods"})
+@SuppressWarnings({"InstanceMethodNamingConvention", "ClassWithTooManyMethods", "OverlyCoupledClass"})
 public class CommandBusShould {
 
     private CommandBus commandBus;
@@ -63,19 +72,19 @@ public class CommandBusShould {
     private CommandBus.ProblemLog log;
     private EventBus eventBus;
     private ExecutorCommandScheduler scheduler;
+    private CreateProjectHandler handler;
 
     @Before
     public void setUp() {
-        commandStore = mock(CommandStore.class);
-        scheduler = mock(ExecutorCommandScheduler.class);
-        commandBus = CommandBus.newBuilder()
-                               .setCommandStore(commandStore)
-                               .setScheduler(scheduler)
-                               .build();
-        log = spy(CommandBus.ProblemLog.class);
+        final InMemoryStorageFactory storageFactory = InMemoryStorageFactory.getInstance();
+        commandStore = spy(new CommandStore(storageFactory.createCommandStorage()));
+        scheduler = spy(new ExecutorCommandScheduler());
+        commandBus = newCommandBus(commandStore, scheduler);
+        log = spy(new CommandBus.ProblemLog());
         commandBus.setProblemLog(log);
-        eventBus = mock(EventBus.class);
+        eventBus = spy(TestEventFactory.newEventBus(storageFactory));
         commandFactory = TestCommandFactory.newInstance(CommandBusShould.class);
+        handler = new CreateProjectHandler(newUuid(), eventBus);
     }
 
     @Test(expected = NullPointerException.class)
@@ -108,13 +117,13 @@ public void do_not_accept_empty_dispatchers() {
 
     @Test(expected = IllegalArgumentException.class)
     public void do_not_accept_command_handlers_without_methods() {
-        commandBus.register(new EmptyCommandHandler(eventBus));
+        commandBus.register(new EmptyCommandHandler(newUuid(), eventBus));
     }
 
     @SuppressWarnings("EmptyClass")
     private static class EmptyCommandHandler extends CommandHandler {
-        private EmptyCommandHandler(EventBus eventBus) {
-            super(newUuid(), eventBus);
+        protected EmptyCommandHandler(String id, EventBus eventBus) {
+            super(id, eventBus);
         }
     }
 
@@ -187,38 +196,43 @@ public void dispatch(Command request) throws Exception {
         assertTrue(isUnsupportedCommand(commandBus.validate(addTask(projectId))));
     }
 
+    @Test
+    public void return_invalid_command_response_if_command_is_invalid() {
+        commandBus.register(handler);
+        final MessageValidator validator = mock(MessageValidator.class);
+        doReturn(newArrayList(ConstraintViolation.getDefaultInstance()))
+                .when(validator)
+                .validate(any(Message.class));
+        commandBus.setMessageValidator(validator);
+
+        final Response response = commandBus.validate(createProject(newUuid()));
+        assertTrue(isInvalidCommand(response));
+    }
+
     //
     // Tests for not overriding handlers by dispatchers and vice versa
     //-------------------------------------------------------------------
 
     @Test(expected = IllegalArgumentException.class)
     public void do_not_allow_to_register_dispatcher_for_the_command_with_registered_handler() {
-
-        final CommandHandler createProjectHandler = new CreateProjectHandler(eventBus);
         final CommandDispatcher createProjectDispatcher = new CreateProjectDispatcher();
-
-        commandBus.register(createProjectHandler);
-
+        commandBus.register(handler);
         commandBus.register(createProjectDispatcher);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void do_not_allow_to_register_handler_for_the_command_with_registered_dispatcher() {
-
-        final CommandHandler createProjectHandler = new CreateProjectHandler(eventBus);
         final CommandDispatcher createProjectDispatcher = new CreateProjectDispatcher();
-
         commandBus.register(createProjectDispatcher);
-
-        commandBus.register(createProjectHandler);
+        commandBus.register(handler);
     }
 
     private static class CreateProjectHandler extends CommandHandler {
 
         private boolean handlerInvoked = false;
 
-        private CreateProjectHandler(EventBus eventBus) {
-            super(newUuid(), eventBus);
+        protected CreateProjectHandler(String id, EventBus eventBus) {
+            super(id, eventBus);
         }
 
         @Assign
@@ -245,7 +259,6 @@ public void dispatch(Command request) throws Exception {
 
     @Test
     public void unregister_handler() {
-        final CommandHandler handler = new CreateProjectHandler(eventBus);
         commandBus.register(handler);
         commandBus.unregister(handler);
         final String projectId = newUuid();
@@ -254,7 +267,6 @@ public void unregister_handler() {
 
     @Test
     public void validate_commands_both_dispatched_and_handled() {
-        final CommandHandler handler = new CreateProjectHandler(eventBus);
         final CommandDispatcher dispatcher = new AddTaskDispatcher();
         commandBus.register(handler);
         commandBus.register(dispatcher);
@@ -288,6 +300,11 @@ public void have_log() {
         assertNotNull(CommandBus.log());
     }
 
+    @Test // To improve coverage stats.
+    public void have_command_status_service() {
+        assertNotNull(commandBus.getCommandStatusService());
+    }
+
     @Test
     public void close_CommandStore_when_closed() throws Exception {
         commandBus.close();
@@ -304,7 +321,6 @@ public void shutdown_CommandScheduler_when_closed() throws Exception {
 
     @Test
     public void remove_all_handlers_on_close() throws Exception {
-        final CommandHandler handler = new CreateProjectHandler(eventBus);
         commandBus.register(handler);
 
         commandBus.close();
@@ -313,7 +329,6 @@ public void remove_all_handlers_on_close() throws Exception {
 
     @Test
     public void invoke_handler_when_command_posted() {
-        final CreateProjectHandler handler = new CreateProjectHandler(eventBus);
         commandBus.register(handler);
 
         final Command command = commandFactory.create(createProject(newUuid()));
@@ -343,14 +358,14 @@ public void throw_exception_when_there_is_no_neither_handler_nor_dispatcher() {
 
     @Test
     public void set_command_status_to_OK_when_handler_returns() {
-        final CreateProjectHandler handler = new CreateProjectHandler(eventBus);
         commandBus.register(handler);
         final Command command = commandFactory.create(createProject(newUuid()));
 
         commandBus.post(command);
 
         // See that we called CommandStore only once with the right command ID.
-        verify(commandStore, times(1)).setCommandStatusOk(command.getContext().getCommandId());
+        verify(commandStore, times(1)).setCommandStatusOk(command.getContext()
+                                                                 .getCommandId());
     }
 
     @Test
@@ -370,66 +385,55 @@ public void set_command_status_to_error_when_dispatcher_throws() throws Exceptio
     @Test
     public void set_command_status_to_failure_when_handler_throws_failure() throws TestFailure, TestThrowable {
         final TestFailure failure = new TestFailure();
-        givenThrowingHandler(failure);
-        final Command command = commandFactory.create(createProject(newUuid()));
+        final Command command = givenThrowingHandler(failure);
         final CommandId commandId = command.getContext().getCommandId();
         final Message commandMessage = Commands.getMessage(command);
 
         commandBus.post(command);
 
         // Verify we updated the status.
-        verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(failure.toMessage()));
+
+        verify(commandStore, times(1)).updateStatus(eq(commandId), eq(failure.toMessage()));
         // Verify we logged the failure.
-        verify(log, atMost(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId));
+        verify(log, times(1)).failureHandling(eq(failure), eq(commandMessage), eq(commandId));
     }
 
     @Test
     public void set_command_status_to_failure_when_handler_throws_exception() throws TestFailure, TestThrowable {
-        final CreateProjectHandler handler = mock(CreateProjectHandler.class);
         final RuntimeException exception = new IllegalStateException("handler throws");
-        doThrow(exception).when(handler)
-                          .handle(any(CreateProject.class), any(CommandContext.class));
-
-        commandBus.register(handler);
-        final Command command = commandFactory.create(createProject(newUuid()));
+        final Command command = givenThrowingHandler(exception);
         final CommandId commandId = command.getContext().getCommandId();
         final Message commandMessage = Commands.getMessage(command);
 
         commandBus.post(command);
 
         // Verify we updated the status.
-        verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(exception));
+        verify(commandStore, times(1)).updateStatus(eq(commandId), eq(exception));
         // Verify we logged the failure.
-        verify(log, atMost(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId));
+        verify(log, times(1)).errorHandling(eq(exception), eq(commandMessage), eq(commandId));
     }
 
     @Test
     public void set_command_status_to_failure_when_handler_throws_unknown_Throwable() throws TestFailure, TestThrowable {
-        final CreateProjectHandler handler = mock(CreateProjectHandler.class);
         final Throwable throwable = new TestThrowable();
-        doThrow(throwable).when(handler)
-                          .handle(any(CreateProject.class), any(CommandContext.class));
-
-        commandBus.register(handler);
-        final Command command = commandFactory.create(createProject(newUuid()));
+        final Command command = givenThrowingHandler(throwable);
         final CommandId commandId = command.getContext().getCommandId();
         final Message commandMessage = Commands.getMessage(command);
 
         commandBus.post(command);
 
         // Verify we updated the status.
-        verify(commandStore, atMost(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable)));
+        verify(commandStore, times(1)).updateStatus(eq(commandId), eq(Errors.fromThrowable(throwable)));
         // Verify we logged the failure.
-        verify(log, atMost(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId));
+        verify(log, times(1)).errorHandlingUnknown(eq(throwable), eq(commandMessage), eq(commandId));
     }
 
-
-    private  void givenThrowingHandler(E throwable) throws TestThrowable, TestFailure {
-        final CreateProjectHandler handler = mock(CreateProjectHandler.class);
-        doThrow(throwable)
-                .when(handler)
-                .handle(any(CreateProject.class), any(CommandContext.class));
+    private  Command givenThrowingHandler(E throwable) throws TestThrowable, TestFailure {
+        final CommandHandler handler = new ThrowingCreateProjectHandler(eventBus, throwable);
         commandBus.register(handler);
+        final CreateProject msg = createProject(newUuid());
+        final Command command = commandFactory.create(msg);
+        return command;
     }
 
     private  E givenThrowingDispatcher() throws Exception {
@@ -447,16 +451,19 @@ private  E givenThrowingDispatcher() throws Exception {
 
     @Test
     public void schedule_command_if_delay_is_set() {
-        final Command cmd = newCommand(/*delay=*/minutes(1));
+        final int delayMsec = 1100;
+        final Command cmd = newCommand(/*delay=*/milliseconds(delayMsec));
 
         commandBus.post(cmd);
 
         verify(scheduler, times(1)).schedule(cmd);
+        verify(scheduler, never()).post(cmd);
+        verify(scheduler, after(delayMsec).times(1)).post(cmd);
     }
 
     @Test
     public void do_not_schedule_command_if_no_scheduling_options_are_set() {
-        commandBus.register(new CreateProjectHandler(eventBus));
+        commandBus.register(new CreateProjectHandler(newUuid(), eventBus));
         final Command cmd = commandFactory.create(createProject(newUuid()));
 
         commandBus.post(cmd);
@@ -501,4 +508,27 @@ private TestFailure() {
     @SuppressWarnings("serial")
     private static class TestThrowable extends Throwable {
     }
+
+    /**
+     * A stub handler that throws passed `Throwable` in the command handler method.
+     *
+     * @see #set_command_status_to_failure_when_handler_throws_failure
+     * @see #set_command_status_to_failure_when_handler_throws_exception
+     * @see #set_command_status_to_failure_when_handler_throws_unknown_Throwable
+     */
+    private static class ThrowingCreateProjectHandler extends CommandHandler {
+
+        private final Throwable throwable;
+
+        protected ThrowingCreateProjectHandler(EventBus eventBus, Throwable throwable) {
+            super(newUuid(), eventBus);
+            this.throwable = throwable;
+        }
+
+        @Assign
+        public ProjectCreated handle(CreateProject msg, CommandContext context) throws Throwable {
+            //noinspection ProhibitedExceptionThrown
+            throw throwable;
+        }
+    }
 }
diff --git a/server/src/test/java/org/spine3/testdata/TestCommands.java b/server/src/test/java/org/spine3/testdata/TestCommands.java
index dcfaba22832..9172fbec74e 100644
--- a/server/src/test/java/org/spine3/testdata/TestCommands.java
+++ b/server/src/test/java/org/spine3/testdata/TestCommands.java
@@ -29,6 +29,7 @@
 import org.spine3.base.Commands;
 import org.spine3.base.UserId;
 import org.spine3.server.command.CommandBus;
+import org.spine3.server.command.CommandScheduler;
 import org.spine3.server.command.CommandStore;
 import org.spine3.server.storage.StorageFactory;
 import org.spine3.test.project.ProjectId;
@@ -157,7 +158,7 @@ public static StartProject startProject(String projectId) {
     }
 
     /**
-     * Creates a new command bus.
+     * Creates a new command bus with the given storage factory.
      */
     public static CommandBus newCommandBus(StorageFactory storageFactory) {
         final CommandStore store = new CommandStore(storageFactory.createCommandStorage());
@@ -166,4 +167,15 @@ public static CommandBus newCommandBus(StorageFactory storageFactory) {
                 .build();
         return commandBus;
     }
+
+    /**
+     * Creates a new command bus with the given command store and scheduler.
+     */
+    public static CommandBus newCommandBus(CommandStore store, CommandScheduler scheduler) {
+        final CommandBus commandBus = CommandBus.newBuilder()
+                                                .setCommandStore(store)
+                                                .setScheduler(scheduler)
+                                                .build();
+        return commandBus;
+    }
 }
diff --git a/server/src/test/java/org/spine3/testdata/TestEventFactory.java b/server/src/test/java/org/spine3/testdata/TestEventFactory.java
index b613514b543..36ac3dcf3a7 100644
--- a/server/src/test/java/org/spine3/testdata/TestEventFactory.java
+++ b/server/src/test/java/org/spine3/testdata/TestEventFactory.java
@@ -20,8 +20,12 @@
 
 package org.spine3.testdata;
 
+import com.google.common.util.concurrent.MoreExecutors;
 import org.spine3.base.Event;
 import org.spine3.base.EventContext;
+import org.spine3.server.event.EventBus;
+import org.spine3.server.event.EventStore;
+import org.spine3.server.storage.StorageFactory;
 import org.spine3.test.project.ProjectId;
 import org.spine3.test.project.event.ProjectCreated;
 import org.spine3.test.project.event.ProjectStarted;
@@ -117,4 +121,16 @@ public static Event projectStarted(ProjectId projectId, EventContext eventContex
         final Event.Builder builder = Event.newBuilder().setContext(eventContext).setMessage(toAny(event));
         return builder.build();
     }
+
+    /**
+     * Creates a new event bus with the given storage factory.
+     */
+    public static EventBus newEventBus(StorageFactory storageFactory) {
+        final EventStore store = EventStore.newBuilder()
+                                           .setStreamExecutor(MoreExecutors.directExecutor())
+                                           .setStorage(storageFactory.createEventStorage())
+                                           .build();
+        final EventBus eventBus = EventBus.newInstance(store);
+        return eventBus;
+    }
 }