From d3836aecfb7557bfab71b6ce58a0bbb3fb6d7b1f Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Sun, 17 Feb 2019 20:54:32 +0100 Subject: [PATCH] Command handler builder for EventSourcedBehaviorWithEnforcedReplies, #25482 (#26272) * It uses ReplyEffect instead of Effect * Unfortunatley the solution is a copy of the CommandHandler builder with a few mechanical changes to the types and names. * I tried to separate interface and implementation and use a shared implementation, but it didn't work out because methods with such function parameters which only differ in their type parameters can't be overloaded. --- .../src/main/paradox/typed/persistence.md | 3 + .../typed/javadsl/CommandHandler.scala | 1 - .../javadsl/CommandHandlerWithReply.scala | 390 ++++++++++++++++++ .../typed/javadsl/EventSourcedBehavior.scala | 41 +- .../typed/auction/AuctionEntity.java | 46 ++- 5 files changed, 456 insertions(+), 25 deletions(-) create mode 100644 akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandlerWithReply.scala diff --git a/akka-docs/src/main/paradox/typed/persistence.md b/akka-docs/src/main/paradox/typed/persistence.md index b5ea77e5a5..d2ff892933 100644 --- a/akka-docs/src/main/paradox/typed/persistence.md +++ b/akka-docs/src/main/paradox/typed/persistence.md @@ -307,6 +307,9 @@ Java The `ReplyEffect` is created with @scala[`Effect.reply`]@java[`Effects().reply`], @scala[`Effect.noReply`]@java[`Effects().noReply`], @scala[`Effect.thenReply`]@java[`Effects().thenReply`], or @scala[`Effect.thenNoReply`]@java[`Effects().thenNoReply`]. +@java[Note that command handlers are defined with `newCommandHandlerWithReplyBuilder` when using +`EventSourcedBehaviorWithEnforcedReplies`], as opposed to newCommandHandlerBuilder when using `EventSourcedBehavior`.] + Scala : @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply } diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandler.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandler.scala index 0709b2c8ff..fe2f5f1aa3 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandler.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandler.scala @@ -9,7 +9,6 @@ import java.util.function.{ BiFunction, Predicate, Supplier, Function ⇒ JFunct import akka.annotation.InternalApi import akka.persistence.typed.internal._ -import akka.persistence.typed.javadsl.CommandHandlerBuilderByState.CommandHandlerCase import akka.util.OptionVal import scala.compat.java8.FunctionConverters._ diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandlerWithReply.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandlerWithReply.scala new file mode 100644 index 0000000000..f539e3ed48 --- /dev/null +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/CommandHandlerWithReply.scala @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package akka.persistence.typed.javadsl + +import java.util.Objects +import java.util.function.{ BiFunction, Predicate, Supplier, Function ⇒ JFunction } + +import akka.annotation.InternalApi +import akka.persistence.typed.internal._ +import akka.util.OptionVal + +import scala.compat.java8.FunctionConverters._ + +/* Note that this is a copy of CommandHandler.scala to support ReplyEffect + * s/Effect/ReplyEffect/ + * s/CommandHandler/CommandHandlerWithReply/ + * s/CommandHandlerBuilder/CommandHandlerWithReplyBuilder/ + * s/CommandHandlerBuilderByState/CommandHandlerWithReplyBuilderByState/ + * s/EventSourcedBehavior/EventSourcedBehaviorWithEnforcedReplies/ + */ + +/** + * FunctionalInterface for reacting on commands + * + * Used with [[CommandHandlerWithReplyBuilder]] to setup the behavior of a [[EventSourcedBehaviorWithEnforcedReplies]] + */ +@FunctionalInterface +trait CommandHandlerWithReply[Command, Event, State] extends CommandHandler[Command, Event, State] { + def apply(state: State, command: Command): ReplyEffect[Event, State] +} + +object CommandHandlerWithReplyBuilder { + def builder[Command, Event, State](): CommandHandlerWithReplyBuilder[Command, Event, State] = + new CommandHandlerWithReplyBuilder[Command, Event, State] +} + +final class CommandHandlerWithReplyBuilder[Command, Event, State]() { + + private var builders: List[CommandHandlerWithReplyBuilderByState[Command, Event, State, State]] = Nil + + /** + * Use this method to define command handlers that are selected when the passed predicate holds true. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true` + * + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forState(statePredicate: Predicate[State]): CommandHandlerWithReplyBuilderByState[Command, Event, State, State] = { + val builder = CommandHandlerWithReplyBuilderByState.builder[Command, Event, State](statePredicate) + builders = builder :: builders + builder + } + + /** + * Use this method to define command handlers that are selected when the passed predicate holds true + * for a given subtype of your model. Useful when the model is defined as class hierarchy. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass` + * @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true` + * + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forState[S <: State](stateClass: Class[S], statePredicate: Predicate[S]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + val builder = new CommandHandlerWithReplyBuilderByState[Command, Event, S, State](stateClass, statePredicate) + builders = builder.asInstanceOf[CommandHandlerWithReplyBuilderByState[Command, Event, State, State]] :: builders + builder + } + + /** + * Use this method to define command handlers for a given subtype of your model. Useful when the model is defined as class hierarchy. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass`. + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forStateType[S <: State](stateClass: Class[S]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + val builder = CommandHandlerWithReplyBuilderByState.builder[Command, Event, S, State](stateClass) + builders = builder.asInstanceOf[CommandHandlerWithReplyBuilderByState[Command, Event, State, State]] :: builders + builder + } + + /** + * The handlers defined by this builder are used when the state is `null`. + * This variant is particular useful when the empty state of your model is defined as `null`. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forNullState(): CommandHandlerWithReplyBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.isNull(s)) + val builder = CommandHandlerWithReplyBuilderByState.builder[Command, Event, State](predicate) + builders = builder :: builders + builder + } + + /** + * The handlers defined by this builder are used for any not `null` state. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forNonNullState(): CommandHandlerWithReplyBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.nonNull(s)) + val builder = CommandHandlerWithReplyBuilderByState.builder[Command, Event, State](predicate) + builders = builder :: builders + builder + } + + /** + * The handlers defined by this builder are used for any state. + * This variant is particular useful for models that have a single type (ie: no class hierarchy). + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * Extra care should be taken when using [[forAnyState]] as it will match any state. Any command handler define after it will never be reached. + * + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def forAnyState(): CommandHandlerWithReplyBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(_ ⇒ true) + val builder = CommandHandlerWithReplyBuilderByState.builder[Command, Event, State](predicate) + builders = builder :: builders + builder + } + + def build(): CommandHandlerWithReply[Command, Event, State] = { + + val combined = + builders.reverse match { + case head :: Nil ⇒ head + case head :: tail ⇒ tail.foldLeft(head) { (acc, builder) ⇒ + acc.orElse(builder) + } + case Nil ⇒ throw new IllegalStateException("No matchers defined") + } + + combined.build() + } + +} + +object CommandHandlerWithReplyBuilderByState { + + private val _trueStatePredicate: Predicate[Any] = new Predicate[Any] { + override def test(t: Any): Boolean = true + } + + private def trueStatePredicate[S]: Predicate[S] = _trueStatePredicate.asInstanceOf[Predicate[S]] + + /** + * @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass` + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def builder[Command, Event, S <: State, State](stateClass: Class[S]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = + new CommandHandlerWithReplyBuilderByState(stateClass, statePredicate = trueStatePredicate) + + /** + * @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true`, + * useful for example when state type is an Optional + * @return A new, mutable, CommandHandlerWithReplyBuilderByState + */ + def builder[Command, Event, State](statePredicate: Predicate[State]): CommandHandlerWithReplyBuilderByState[Command, Event, State, State] = + new CommandHandlerWithReplyBuilderByState(classOf[Any].asInstanceOf[Class[State]], statePredicate) + + /** + * INTERNAL API + */ + @InternalApi private final case class CommandHandlerCase[Command, Event, State]( + commandPredicate: Command ⇒ Boolean, + statePredicate: State ⇒ Boolean, + handler: BiFunction[State, Command, ReplyEffect[Event, State]]) +} + +final class CommandHandlerWithReplyBuilderByState[Command, Event, S <: State, State] @InternalApi private[persistence] ( + private val stateClass: Class[S], private val statePredicate: Predicate[S]) { + + import CommandHandlerWithReplyBuilderByState.CommandHandlerCase + + private var cases: List[CommandHandlerCase[Command, Event, State]] = Nil + + private def addCase(predicate: Command ⇒ Boolean, handler: BiFunction[S, Command, ReplyEffect[Event, State]]): Unit = { + cases = CommandHandlerCase[Command, Event, State]( + commandPredicate = predicate, + statePredicate = state ⇒ + if (state == null) statePredicate.test(state.asInstanceOf[S]) + else statePredicate.test(state.asInstanceOf[S]) && stateClass.isAssignableFrom(state.getClass), + handler.asInstanceOf[BiFunction[State, Command, ReplyEffect[Event, State]]]) :: cases + } + + /** + * Matches any command which the given `predicate` returns true for. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + */ + def matchCommand(predicate: Predicate[Command], handler: BiFunction[S, Command, ReplyEffect[Event, State]]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + addCase(cmd ⇒ predicate.test(cmd), handler) + this + } + + /** + * Matches any command which the given `predicate` returns true for. + * + * Use this when the `State` is not needed in the `handler`, otherwise there is an overloaded method that pass + * the state in a `BiFunction`. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + */ + def matchCommand(predicate: Predicate[Command], handler: JFunction[Command, ReplyEffect[Event, State]]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + addCase(cmd ⇒ predicate.test(cmd), new BiFunction[S, Command, ReplyEffect[Event, State]] { + override def apply(state: S, cmd: Command): ReplyEffect[Event, State] = handler(cmd) + }) + this + } + + /** + * Matches commands that are of the given `commandClass` or subclass thereof + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + */ + def matchCommand[C <: Command](commandClass: Class[C], handler: BiFunction[S, C, ReplyEffect[Event, State]]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + addCase(cmd ⇒ commandClass.isAssignableFrom(cmd.getClass), handler.asInstanceOf[BiFunction[S, Command, ReplyEffect[Event, State]]]) + this + } + + /** + * Matches commands that are of the given `commandClass` or subclass thereof. + * + * Use this when the `State` is not needed in the `handler`, otherwise there is an overloaded method that pass + * the state in a `BiFunction`. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + */ + def matchCommand[C <: Command](commandClass: Class[C], handler: JFunction[C, ReplyEffect[Event, State]]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + matchCommand[C](commandClass, new BiFunction[S, C, ReplyEffect[Event, State]] { + override def apply(state: S, cmd: C): ReplyEffect[Event, State] = handler(cmd) + }) + } + + /** + * Matches commands that are of the given `commandClass` or subclass thereof. + * + * Use this when you just need to initialize the `State` without using any data from the command. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + */ + def matchCommand[C <: Command](commandClass: Class[C], handler: Supplier[ReplyEffect[Event, State]]): CommandHandlerWithReplyBuilderByState[Command, Event, S, State] = { + matchCommand[C](commandClass, new BiFunction[S, C, ReplyEffect[Event, State]] { + override def apply(state: S, cmd: C): ReplyEffect[Event, State] = handler.get() + }) + } + + /** + * Matches any command. + * + * Use this to declare a command handler that will match any command. This is particular useful when encoding + * a finite state machine in which the final state is not supposed to handle any new command. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * Extra care should be taken when using [[matchAny]] as it will match any command. + * This method builds and returns the command handler since this will not let through any states to subsequent match statements. + * + * @return A CommandHandlerWithReply from the appended states. + */ + def matchAny(handler: BiFunction[S, Command, ReplyEffect[Event, State]]): CommandHandlerWithReply[Command, Event, State] = { + addCase(_ ⇒ true, handler) + build() + } + + /** + * Matches any command. + * + * Use this to declare a command handler that will match any command. This is particular useful when encoding + * a finite state machine in which the final state is not supposed to handle any new command. + * + * Use this when you just need to return an [[ReplyEffect]] without using any data from the state. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * Extra care should be taken when using [[matchAny]] as it will match any command. + * This method builds and returns the command handler since this will not let through any states to subsequent match statements. + * + * @return A CommandHandlerWithReply from the appended states. + */ + def matchAny(handler: JFunction[Command, ReplyEffect[Event, State]]): CommandHandlerWithReply[Command, Event, State] = { + addCase(_ ⇒ true, new BiFunction[S, Command, ReplyEffect[Event, State]] { + override def apply(state: S, cmd: Command): ReplyEffect[Event, State] = handler(cmd) + }) + build() + } + /** + * Matches any command. + * + * Use this to declare a command handler that will match any command. This is particular useful when encoding + * a finite state machine in which the final state is not supposed to handle any new command. + * + * Use this when you just need to return an [[ReplyEffect]] without using any data from the command or from the state. + * + * Note: command handlers are matched in the order they are added. Once a matching is found, it's selected for handling the command + * and no further lookup is done. Therefore you must make sure that their matching conditions don't overlap, + * otherwise you risk to 'shadow' part of your command handlers. + * + * Extra care should be taken when using [[matchAny]] as it will match any command. + * This method builds and returns the command handler since this will not let through any states to subsequent match statements. + * + * @return A CommandHandlerWithReply from the appended states. + */ + def matchAny(handler: Supplier[ReplyEffect[Event, State]]): CommandHandlerWithReply[Command, Event, State] = { + addCase(_ ⇒ true, new BiFunction[S, Command, ReplyEffect[Event, State]] { + override def apply(state: S, cmd: Command): ReplyEffect[Event, State] = handler.get() + }) + build() + } + + /** + * Compose this builder with another builder. The handlers in this builder will be tried first followed + * by the handlers in `other`. + */ + def orElse[S2 <: State](other: CommandHandlerWithReplyBuilderByState[Command, Event, S2, State]): CommandHandlerWithReplyBuilderByState[Command, Event, S2, State] = { + val newBuilder = new CommandHandlerWithReplyBuilderByState[Command, Event, S2, State](other.stateClass, other.statePredicate) + // problem with overloaded constructor with `cases` as parameter + newBuilder.cases = other.cases ::: cases + newBuilder + } + + /** + * Builds and returns a handler from the appended states. The returned [[CommandHandlerWithReply]] will throw a [[scala.MatchError]] + * if applied to a command that has no defined case. + */ + def build(): CommandHandlerWithReply[Command, Event, State] = { + val builtCases = cases.reverse.toArray + + new CommandHandlerWithReply[Command, Event, State] { + override def apply(state: State, command: Command): ReplyEffect[Event, State] = { + var idx = 0 + var effect: OptionVal[ReplyEffect[Event, State]] = OptionVal.None + + while (idx < builtCases.length && effect.isEmpty) { + val curr = builtCases(idx) + if (curr.statePredicate(state) && curr.commandPredicate(command)) { + val x: ReplyEffect[Event, State] = curr.handler.apply(state, command) + effect = OptionVal.Some(x) + } + idx += 1 + } + + effect match { + case OptionVal.None ⇒ throw new MatchError(s"No match found for command of type [${command.getClass.getName}]") + case OptionVal.Some(e) ⇒ e.asInstanceOf[EffectImpl[Event, State]] + } + } + } + } + +} + diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventSourcedBehavior.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventSourcedBehavior.scala index a362ecfa77..a290eeb2de 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventSourcedBehavior.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventSourcedBehavior.scala @@ -52,6 +52,8 @@ abstract class EventSourcedBehavior[Command, Event, State >: Null] private[akka] * Implement by handling incoming commands and return an `Effect()` to persist or signal other effects * of the command handling such as stopping the behavior or others. * + * Use [[EventSourcedBehavior#newCommandHandlerBuilder]] to define the command handlers. + * * The command handlers are only invoked when the actor is running (i.e. not replaying). * While the actor is persisting events, the incoming messages are stashed and only * delivered to the handler once persisting them has completed. @@ -61,6 +63,8 @@ abstract class EventSourcedBehavior[Command, Event, State >: Null] private[akka] /** * Implement by applying the event to the current state in order to return a new state. * + * Use [[EventSourcedBehavior#newEventHandlerBuilder]] to define the event handlers. + * * The event handlers are invoked during recovery as well as running operation of this behavior, * in order to keep updating the state state. * @@ -69,7 +73,10 @@ abstract class EventSourcedBehavior[Command, Event, State >: Null] private[akka] */ protected def eventHandler(): EventHandler[State, Event] - protected final def newCommandHandlerBuilder(): CommandHandlerBuilder[Command, Event, State] = { + /** + * @return A new, mutable, command handler builder + */ + protected def newCommandHandlerBuilder(): CommandHandlerBuilder[Command, Event, State] = { CommandHandlerBuilder.builder[Command, Event, State]() } @@ -180,8 +187,6 @@ abstract class EventSourcedBehavior[Command, Event, State >: Null] private[akka] } /** - * FIXME This is not completed for javadsl yet. The compiler is not enforcing the replies yet. - * * A [[EventSourcedBehavior]] that is enforcing that replies to commands are not forgotten. * There will be compilation errors if the returned effect isn't a [[ReplyEffect]], which can be * created with `Effects().reply`, `Effects().noReply`, [[Effect.thenReply]], or [[Effect.thenNoReply]]. @@ -198,6 +203,32 @@ abstract class EventSourcedBehaviorWithEnforcedReplies[Command, Event, State >: this(persistenceId, Optional.ofNullable(backoffSupervisorStrategy)) } - // FIXME override commandHandler and commandHandlerBuilder to require the ReplyEffect return type, - // which is unfortunately intrusive to the CommandHandlerBuilder + /** + * Implement by handling incoming commands and return an `Effect()` to persist or signal other effects + * of the command handling such as stopping the behavior or others. + * + * Use [[EventSourcedBehaviorWithEnforcedReplies#newCommandHandlerWithReplyBuilder]] to define the command handlers. + * + * The command handlers are only invoked when the actor is running (i.e. not replaying). + * While the actor is persisting events, the incoming messages are stashed and only + * delivered to the handler once persisting them has completed. + */ + override protected def commandHandler(): CommandHandlerWithReply[Command, Event, State] + + /** + * @return A new, mutable, command handler builder + */ + protected def newCommandHandlerWithReplyBuilder(): CommandHandlerWithReplyBuilder[Command, Event, State] = { + CommandHandlerWithReplyBuilder.builder[Command, Event, State]() + } + + /** + * Use [[EventSourcedBehaviorWithEnforcedReplies#newCommandHandlerWithReplyBuilder]] instead, or + * extend [[EventSourcedBehavior]] instead of [[EventSourcedBehaviorWithEnforcedReplies]]. + * + * @throws UnsupportedOperationException use newCommandHandlerWithReplyBuilder instead + */ + override protected def newCommandHandlerBuilder(): CommandHandlerBuilder[Command, Event, State] = + throw new UnsupportedOperationException("Use newCommandHandlerWithReplyBuilder instead") + } diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/auction/AuctionEntity.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/auction/AuctionEntity.java index 863d1933f3..0b8ad1b6e8 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/auction/AuctionEntity.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/auction/AuctionEntity.java @@ -22,7 +22,7 @@ import java.util.UUID; * https://github.com/lagom/online-auction-java/blob/master/bidding-impl/src/main/java/com/example/auction/bidding/impl/AuctionEntity.java */ public class AuctionEntity - extends EventSourcedBehavior { + extends EventSourcedBehaviorWithEnforcedReplies { private final UUID entityUUID; @@ -33,9 +33,10 @@ public class AuctionEntity } // Command handler for the not started state. - private CommandHandlerBuilderByState + private CommandHandlerWithReplyBuilderByState< + AuctionCommand, AuctionEvent, AuctionState, AuctionState> notStartedHandler = - newCommandHandlerBuilder() + newCommandHandlerWithReplyBuilder() .forState(state -> state.getStatus() == AuctionStatus.NOT_STARTED) .onCommand(StartAuction.class, this::startAuction) .onCommand( @@ -44,18 +45,20 @@ public class AuctionEntity Effect().reply(cmd, createResult(state, PlaceBidStatus.NOT_STARTED))); // Command handler for the under auction state. - private CommandHandlerBuilderByState + private CommandHandlerWithReplyBuilderByState< + AuctionCommand, AuctionEvent, AuctionState, AuctionState> underAuctionHandler = - newCommandHandlerBuilder() + newCommandHandlerWithReplyBuilder() .forState(state -> state.getStatus() == AuctionStatus.UNDER_AUCTION) .onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .onCommand(PlaceBid.class, this::placeBid) .onCommand(FinishBidding.class, this::finishBidding); // Command handler for the completed state. - private CommandHandlerBuilderByState + private CommandHandlerWithReplyBuilderByState< + AuctionCommand, AuctionEvent, AuctionState, AuctionState> completedHandler = - newCommandHandlerBuilder() + newCommandHandlerWithReplyBuilder() .forState(state -> state.getStatus() == AuctionStatus.COMPLETE) .onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .onCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd)) @@ -65,9 +68,10 @@ public class AuctionEntity Effect().reply(cmd, createResult(state, PlaceBidStatus.FINISHED))); // Command handler for the cancelled state. - private CommandHandlerBuilderByState + private CommandHandlerWithReplyBuilderByState< + AuctionCommand, AuctionEvent, AuctionState, AuctionState> cancelledHandler = - newCommandHandlerBuilder() + newCommandHandlerWithReplyBuilder() .forState(state -> state.getStatus() == AuctionStatus.CANCELLED) .onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .onCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd)) @@ -77,9 +81,10 @@ public class AuctionEntity (state, cmd) -> Effect().reply(cmd, createResult(state, PlaceBidStatus.CANCELLED))); - private CommandHandlerBuilderByState + private CommandHandlerWithReplyBuilderByState< + AuctionCommand, AuctionEvent, AuctionState, AuctionState> getAuctionHandler = - newCommandHandlerBuilder() + newCommandHandlerWithReplyBuilder() .forStateType(AuctionState.class) .onCommand(GetAuction.class, (state, cmd) -> Effect().reply(cmd, state)); @@ -93,26 +98,29 @@ public class AuctionEntity // event from us, it will ignore the bidding finished event, so we need to update our state // to reflect that. - private Effect startAuction(AuctionState state, StartAuction cmd) { + private ReplyEffect startAuction( + AuctionState state, StartAuction cmd) { return Effect() .persist(new AuctionStarted(entityUUID, cmd.getAuction())) .thenReply(cmd, __ -> Done.getInstance()); } - private Effect finishBidding(AuctionState state, FinishBidding cmd) { + private ReplyEffect finishBidding( + AuctionState state, FinishBidding cmd) { return Effect() .persist(new BiddingFinished(entityUUID)) .thenReply(cmd, __ -> Done.getInstance()); } - private Effect cancelAuction(AuctionState state, CancelAuction cmd) { + private ReplyEffect cancelAuction( + AuctionState state, CancelAuction cmd) { return Effect() .persist(new AuctionCancelled(entityUUID)) .thenReply(cmd, __ -> Done.getInstance()); } /** The main logic for handling of bids. */ - private Effect placeBid(AuctionState state, PlaceBid bid) { + private ReplyEffect placeBid(AuctionState state, PlaceBid bid) { Auction auction = state.getAuction().get(); Instant now = Instant.now(); @@ -187,7 +195,7 @@ public class AuctionEntity *

This emits two events, one for the bid currently being replace, and another automatic bid * for the current bidder. */ - private Effect handleAutomaticOutbid( + private ReplyEffect handleAutomaticOutbid( PlaceBid bid, Auction auction, Instant now, @@ -215,7 +223,7 @@ public class AuctionEntity } /** Handle the situation where a bid will be accepted as the new winning bidder. */ - private Effect handleNewWinningBidder( + private ReplyEffect handleNewWinningBidder( PlaceBid bid, Auction auction, Instant now, int currentBidMaximum) { int nextIncrement = Math.min(currentBidMaximum + auction.getIncrement(), bid.getBidPrice()); int newBidPrice; @@ -247,7 +255,7 @@ public class AuctionEntity } @Override - public CommandHandler commandHandler() { + public CommandHandlerWithReply commandHandler() { return notStartedHandler .orElse(underAuctionHandler) .orElse(completedHandler) @@ -284,7 +292,7 @@ public class AuctionEntity } } - private Effect alreadyDone(ExpectingReply cmd) { + private ReplyEffect alreadyDone(ExpectingReply cmd) { return Effect().reply(cmd, Done.getInstance()); } }