From db4f224f4acd027a82e26dcd707a7ea5d215b7c6 Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Fri, 18 Jan 2019 13:52:16 +0100 Subject: [PATCH] Improve builders in Java Persistentce Typed, #26109 * CommandHandlersBuilder DSL refactoring * some minor improvements * removes obsolete examples * event handlers by state (follows cmd handlers design) * fix state predicates * removes obsolete matchEvent methods * minor improvements and formatting * make it compile for 2.11.x * fixes sharding tests * promote forAnyState when applicable, improved javadoc and formatting * reformatted with new java formatter * matchAny in cmd handler builder builds the command handler * make stateClass and statePredicate private fields * build() does not reset the builder * improved scaladoc --- .../ClusterShardingPersistenceTest.java | 8 +- .../HelloWorldPersistentEntityExample.java | 3 +- .../typed/javadsl/CommandHandler.scala | 294 ++++++++++++++++-- .../typed/javadsl/EventHandler.scala | 243 +++++++++++++-- .../typed/javadsl/EventSourcedBehavior.scala | 20 +- .../typed/javadsl/NullEmptyStateTest.java | 22 +- .../PersistentActorCompileOnlyTest.java | 12 +- .../javadsl/PersistentActorJavaDslTest.java | 14 +- .../persistence/typed/AccountExample.java | 58 ++-- .../typed/AccountExampleOneLiners.java | 182 +++++++++++ .../typed/AccountExampleOneLinersInModel.java | 173 +++++++++++ ...ccountExampleOneLinersInModelWithNull.java | 169 ++++++++++ .../AccountExampleOneLinersWithNull.java | 181 +++++++++++ .../persistence/typed/BlogPostExample.java | 51 +-- .../persistence/typed/MovieWatchList.java | 6 +- .../akka/persistence/typed/NullBlogState.java | 33 +- .../persistence/typed/OptionalBlogState.java | 33 +- .../typed/auction/AuctionEntity.java | 52 ++-- 18 files changed, 1355 insertions(+), 199 deletions(-) create mode 100644 akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java create mode 100644 akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java create mode 100644 akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java create mode 100644 akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java diff --git a/akka-cluster-sharding-typed/src/test/java/akka/cluster/sharding/typed/javadsl/ClusterShardingPersistenceTest.java b/akka-cluster-sharding-typed/src/test/java/akka/cluster/sharding/typed/javadsl/ClusterShardingPersistenceTest.java index f02b4d6b8b..2e910db3fa 100644 --- a/akka-cluster-sharding-typed/src/test/java/akka/cluster/sharding/typed/javadsl/ClusterShardingPersistenceTest.java +++ b/akka-cluster-sharding-typed/src/test/java/akka/cluster/sharding/typed/javadsl/ClusterShardingPersistenceTest.java @@ -88,7 +88,8 @@ public class ClusterShardingPersistenceTest extends JUnitSuite { @Override public CommandHandler commandHandler() { - return commandHandlerBuilder(String.class) + return newCommandHandlerBuilder() + .forAnyState() .matchCommand(Add.class, this::add) .matchCommand(AddWithConfirmation.class, this::addWithConfirmation) .matchCommand(Get.class, this::getState) @@ -110,7 +111,10 @@ public class ClusterShardingPersistenceTest extends JUnitSuite { @Override public EventHandler eventHandler() { - return eventHandlerBuilder().matchEvent(String.class, this::applyEvent).build(); + return newEventHandlerBuilder() + .forAnyState() + .matchEvent(String.class, this::applyEvent) + .build(); } private String applyEvent(String state, String evt) { diff --git a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/HelloWorldPersistentEntityExample.java b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/HelloWorldPersistentEntityExample.java index 9be978974e..ec8d36195d 100644 --- a/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/HelloWorldPersistentEntityExample.java +++ b/akka-cluster-sharding-typed/src/test/java/jdocs/akka/cluster/sharding/typed/HelloWorldPersistentEntityExample.java @@ -132,7 +132,8 @@ public class HelloWorldPersistentEntityExample { @Override public CommandHandler commandHandler() { - return commandHandlerBuilder(KnownPeople.class) + return newCommandHandlerBuilder() + .forAnyState() .matchCommand(Greet.class, this::greet) .build(); } 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 1aa4ef52c0..47aa9b5d3b 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 @@ -4,14 +4,16 @@ package akka.persistence.typed.javadsl -import java.util.function.BiFunction -import java.util.function.Predicate -import java.util.function.{ Function ⇒ JFunction } +import java.util.Objects +import java.util.function.{ BiFunction, Predicate, Supplier, Function ⇒ JFunction } 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._ + /** * FunctionalInterface for reacting on commands * @@ -23,23 +25,155 @@ trait CommandHandler[Command, Event, State] { } object CommandHandlerBuilder { + def builder[Command, Event, State](): CommandHandlerBuilder[Command, Event, State] = + new CommandHandlerBuilder[Command, Event, State] +} + +final class CommandHandlerBuilder[Command, Event, State]() { + + private var builders: List[CommandHandlerBuilderByState[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, CommandHandlerBuilderByState + */ + def forState(statePredicate: Predicate[State]): CommandHandlerBuilderByState[Command, Event, State, State] = { + val builder = CommandHandlerBuilderByState.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, CommandHandlerBuilderByState + */ + def forState[S <: State](stateClass: Class[S], statePredicate: Predicate[S]): CommandHandlerBuilderByState[Command, Event, S, State] = { + val builder = new CommandHandlerBuilderByState[Command, Event, S, State](stateClass, statePredicate) + builders = builder.asInstanceOf[CommandHandlerBuilderByState[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, CommandHandlerBuilderByState + */ + def forStateType[S <: State](stateClass: Class[S]): CommandHandlerBuilderByState[Command, Event, S, State] = { + val builder = CommandHandlerBuilderByState.builder[Command, Event, S, State](stateClass) + builders = builder.asInstanceOf[CommandHandlerBuilderByState[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, CommandHandlerBuilderByState + */ + def forNullState(): CommandHandlerBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.isNull(s)) + val builder = CommandHandlerBuilderByState.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, CommandHandlerBuilderByState + */ + def forNonNullState(): CommandHandlerBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.nonNull(s)) + val builder = CommandHandlerBuilderByState.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, CommandHandlerBuilderByState + */ + def forAnyState(): CommandHandlerBuilderByState[Command, Event, State, State] = { + val predicate: Predicate[State] = asJavaPredicate(_ ⇒ true) + val builder = CommandHandlerBuilderByState.builder[Command, Event, State](predicate) + builders = builder :: builders + builder + } + + def build(): CommandHandler[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 CommandHandlerBuilderByState { + + 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, command handler builder + * @return A new, mutable, CommandHandlerBuilderByState */ - def builder[Command, Event, S <: State, State](stateClass: Class[S]): CommandHandlerBuilder[Command, Event, S, State] = - new CommandHandlerBuilder(statePredicate = new Predicate[S] { - override def test(state: S): Boolean = state != null && stateClass.isAssignableFrom(state.getClass) - }) + def builder[Command, Event, S <: State, State](stateClass: Class[S]): CommandHandlerBuilderByState[Command, Event, S, State] = + new CommandHandlerBuilderByState(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, command handler builder + * @return A new, mutable, CommandHandlerBuilderByState */ - def builder[Command, Event, State](statePredicate: Predicate[State]): CommandHandlerBuilder[Command, Event, State, State] = - new CommandHandlerBuilder(statePredicate) + def builder[Command, Event, State](statePredicate: Predicate[State]): CommandHandlerBuilderByState[Command, Event, State, State] = + new CommandHandlerBuilderByState(classOf[Any].asInstanceOf[Class[State]], statePredicate) /** * INTERNAL API @@ -50,34 +184,45 @@ object CommandHandlerBuilder { handler: BiFunction[State, Command, Effect[Event, State]]) } -final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalApi private[persistence] ( - val statePredicate: Predicate[S]) { - import CommandHandlerBuilder.CommandHandlerCase +final class CommandHandlerBuilderByState[Command, Event, S <: State, State] @InternalApi private[persistence] ( + private val stateClass: Class[S], private val statePredicate: Predicate[S]) { + + import CommandHandlerBuilderByState.CommandHandlerCase private var cases: List[CommandHandlerCase[Command, Event, State]] = Nil private def addCase(predicate: Command ⇒ Boolean, handler: BiFunction[S, Command, Effect[Event, State]]): Unit = { cases = CommandHandlerCase[Command, Event, State]( commandPredicate = predicate, - statePredicate = state ⇒ statePredicate.test(state.asInstanceOf[S]), + 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, Effect[Event, State]]]) :: cases } /** - * Match any command which the given `predicate` returns true for + * 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, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = { + def matchCommand(predicate: Predicate[Command], handler: BiFunction[S, Command, Effect[Event, State]]): CommandHandlerBuilderByState[Command, Event, S, State] = { addCase(cmd ⇒ predicate.test(cmd), handler) this } /** - * Match any command which the given `predicate` returns true for. + * Matches any command which the given `predicate` returns true for. * - * Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass + * 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, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = { + def matchCommand(predicate: Predicate[Command], handler: JFunction[Command, Effect[Event, State]]): CommandHandlerBuilderByState[Command, Event, S, State] = { addCase(cmd ⇒ predicate.test(cmd), new BiFunction[S, Command, Effect[Event, State]] { override def apply(state: S, cmd: Command): Effect[Event, State] = handler(cmd) }) @@ -85,42 +230,133 @@ final class CommandHandlerBuilder[Command, Event, S <: State, State] @InternalAp } /** - * Match commands that are of the given `commandClass` or subclass thereof + * 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, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = { + def matchCommand[C <: Command](commandClass: Class[C], handler: BiFunction[S, C, Effect[Event, State]]): CommandHandlerBuilderByState[Command, Event, S, State] = { addCase(cmd ⇒ commandClass.isAssignableFrom(cmd.getClass), handler.asInstanceOf[BiFunction[S, Command, Effect[Event, State]]]) this } /** - * Match commands that are of the given `commandClass` or subclass thereof. + * Matches commands that are of the given `commandClass` or subclass thereof. * - * Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass + * 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, Effect[Event, State]]): CommandHandlerBuilder[Command, Event, S, State] = { + def matchCommand[C <: Command](commandClass: Class[C], handler: JFunction[C, Effect[Event, State]]): CommandHandlerBuilderByState[Command, Event, S, State] = { matchCommand[C](commandClass, new BiFunction[S, C, Effect[Event, State]] { override def apply(state: S, cmd: C): Effect[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[Effect[Event, State]]): CommandHandlerBuilderByState[Command, Event, S, State] = { + matchCommand[C](commandClass, new BiFunction[S, C, Effect[Event, State]] { + override def apply(state: S, cmd: C): Effect[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 CommandHandler from the appended states. + */ + def matchAny(handler: BiFunction[S, Command, Effect[Event, State]]): CommandHandler[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 [[Effect]] 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 CommandHandler from the appended states. + */ + def matchAny(handler: JFunction[Command, Effect[Event, State]]): CommandHandler[Command, Event, State] = { + addCase(_ ⇒ true, new BiFunction[S, Command, Effect[Event, State]] { + override def apply(state: S, cmd: Command): Effect[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 [[Effect]] 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 CommandHandler from the appended states. + */ + def matchAny(handler: Supplier[Effect[Event, State]]): CommandHandler[Command, Event, State] = { + addCase(_ ⇒ true, new BiFunction[S, Command, Effect[Event, State]] { + override def apply(state: S, cmd: Command): Effect[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: CommandHandlerBuilder[Command, Event, S2, State]): CommandHandlerBuilder[Command, Event, S2, State] = { - val newBuilder = new CommandHandlerBuilder[Command, Event, S2, State](other.statePredicate) + def orElse[S2 <: State](other: CommandHandlerBuilderByState[Command, Event, S2, State]): CommandHandlerBuilderByState[Command, Event, S2, State] = { + val newBuilder = new CommandHandlerBuilderByState[Command, Event, S2, State](other.stateClass, other.statePredicate) // problem with overloaded constructor with `cases` as parameter newBuilder.cases = other.cases ::: cases newBuilder } /** - * Builds a Command Handler and resets this builder + * Builds and returns a handler from the appended states. The returned [[CommandHandler]] will throw a [[scala.MatchError]] + * if applied to a command that has no defined case. */ def build(): CommandHandler[Command, Event, State] = { val builtCases = cases.reverse.toArray - cases = Nil + new CommandHandler[Command, Event, State] { override def apply(state: State, command: Command): Effect[Event, State] = { var idx = 0 diff --git a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventHandler.scala b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventHandler.scala index ac8a35b539..979c22eefd 100644 --- a/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventHandler.scala +++ b/akka-persistence-typed/src/main/scala/akka/persistence/typed/javadsl/EventHandler.scala @@ -4,12 +4,14 @@ package akka.persistence.typed.javadsl -import java.util.function.BiFunction -import java.util.function.{ Function ⇒ JFunction } +import java.util.Objects +import java.util.function.{ BiFunction, Predicate, Supplier, Function ⇒ JFunction } import akka.annotation.InternalApi import akka.util.OptionVal +import scala.compat.java8.FunctionConverters._ + /** * FunctionalInterface for reacting on events having been persisted * @@ -24,6 +26,155 @@ object EventHandlerBuilder { def builder[State >: Null, Event](): EventHandlerBuilder[State, Event] = new EventHandlerBuilder[State, Event]() +} + +final class EventHandlerBuilder[State >: Null, Event]() { + + private var builders: List[EventHandlerBuilderByState[State, State, Event]] = Nil + + /** + * Use this method to define event handlers that are selected when the passed predicate holds true. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * @param statePredicate The handlers defined by this builder are used when the `statePredicate` is `true` + * + * @return A new, mutable, EventHandlerBuilderByState + */ + def forState(statePredicate: Predicate[State]): EventHandlerBuilderByState[State, State, Event] = { + val builder = EventHandlerBuilderByState.builder[State, Event](statePredicate) + builders = builder :: builders + builder + } + + /** + * Use this method to define event 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: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event 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, EventHandlerBuilderByState + */ + def forState[S <: State](stateClass: Class[S], statePredicate: Predicate[S]): EventHandlerBuilderByState[S, State, Event] = { + val builder = new EventHandlerBuilderByState[S, State, Event](stateClass, statePredicate) + builders = builder.asInstanceOf[EventHandlerBuilderByState[State, State, Event]] :: 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: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass` + * + * @return A new, mutable, EventHandlerBuilderByState + */ + def forStateType[S <: State](stateClass: Class[S]): EventHandlerBuilderByState[S, State, Event] = { + val builder = EventHandlerBuilderByState.builder[S, State, Event](stateClass) + builders = builder.asInstanceOf[EventHandlerBuilderByState[State, State, Event]] :: 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: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * @return A new, mutable, EventHandlerBuilderByState + */ + def forNullState(): EventHandlerBuilderByState[State, State, Event] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.isNull(s)) + val builder = EventHandlerBuilderByState.builder[State, Event](predicate) + builders = builder :: builders + builder + } + + /** + * The handlers defined by this builder are used for any not `null` state. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * @return A new, mutable, EventHandlerBuilderByState + */ + def forNonNullState(): EventHandlerBuilderByState[State, State, Event] = { + val predicate: Predicate[State] = asJavaPredicate(s ⇒ Objects.nonNull(s)) + val builder = EventHandlerBuilderByState.builder[State, Event](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: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * Extra care should be taken when using [[forAnyState]] as it will match any state. Any event handler define after it will never be reached. + * + * @return A new, mutable, EventHandlerBuilderByState + */ + def forAnyState(): EventHandlerBuilderByState[State, State, Event] = { + val predicate: Predicate[State] = asJavaPredicate(_ ⇒ true) + val builder = EventHandlerBuilderByState.builder[State, Event](predicate) + builders = builder :: builders + builder + } + + def build(): EventHandler[State, Event] = { + + 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 EventHandlerBuilderByState { + + 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, EventHandlerBuilderByState + */ + def builder[S <: State, State >: Null, Event](stateClass: Class[S]): EventHandlerBuilderByState[S, State, Event] = + new EventHandlerBuilderByState(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, EventHandlerBuilderByState + */ + def builder[State >: Null, Event](statePredicate: Predicate[State]): EventHandlerBuilderByState[State, State, Event] = + new EventHandlerBuilderByState(classOf[Any].asInstanceOf[Class[State]], statePredicate) + /** * INTERNAL API */ @@ -33,20 +184,30 @@ object EventHandlerBuilder { handler: BiFunction[State, Event, State]) } -final class EventHandlerBuilder[State >: Null, Event]() { - import EventHandlerBuilder.EventHandlerCase +final class EventHandlerBuilderByState[S <: State, State >: Null, Event](private val stateClass: Class[S], private val statePredicate: Predicate[S]) { + + import EventHandlerBuilderByState.EventHandlerCase private var cases: List[EventHandlerCase[State, Event]] = Nil private def addCase(eventPredicate: Event ⇒ Boolean, handler: BiFunction[State, Event, State]): Unit = { - cases = EventHandlerCase[State, Event](_ ⇒ true, eventPredicate, handler) :: cases + cases = EventHandlerCase[State, Event]( + statePredicate = state ⇒ + if (state == null) statePredicate.test(state.asInstanceOf[S]) + else statePredicate.test(state.asInstanceOf[S]) && stateClass.isAssignableFrom(state.getClass), + eventPredicate = eventPredicate, + handler) :: cases } /** - * Match any event which is an instance of `E` or a subtype of `E` + * Match any event which is an instance of `E` or a subtype of `E`. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. */ - def matchEvent[E <: Event](eventClass: Class[E], biFunction: BiFunction[State, E, State]): EventHandlerBuilder[State, Event] = { - addCase(e ⇒ eventClass.isAssignableFrom(e.getClass), biFunction.asInstanceOf[BiFunction[State, Event, State]]) + def matchEvent[E <: Event](eventClass: Class[E], handler: BiFunction[S, E, State]): EventHandlerBuilderByState[S, State, Event] = { + addCase(e ⇒ eventClass.isAssignableFrom(e.getClass), handler.asInstanceOf[BiFunction[State, Event, State]]) this } @@ -55,30 +216,49 @@ final class EventHandlerBuilder[State >: Null, Event]() { * * Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass * the state in a `BiFunction`. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. */ - def matchEvent[E <: Event](eventClass: Class[E], f: JFunction[E, State]): EventHandlerBuilder[State, Event] = { - matchEvent[E](eventClass, new BiFunction[State, E, State] { - override def apply(state: State, event: E): State = f(event) + def matchEvent[E <: Event](eventClass: Class[E], handler: JFunction[E, State]): EventHandlerBuilderByState[S, State, Event] = { + matchEvent[E](eventClass, new BiFunction[S, E, State] { + override def apply(state: S, event: E): State = handler(event) }) } - def matchEvent[E <: Event, S <: State](eventClass: Class[E], stateClass: Class[S], - biFunction: BiFunction[S, E, State]): EventHandlerBuilder[State, Event] = { + /** + * Match any event which is an instance of `E` or a subtype of `E`. + * + * Use this when then `State` and the `Event` are not needed in the `handler`. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + */ + def matchEvent[E <: Event](eventClass: Class[E], handler: Supplier[State]): EventHandlerBuilderByState[S, State, Event] = { - cases = EventHandlerCase[State, Event]( - statePredicate = s ⇒ s != null && stateClass.isAssignableFrom(s.getClass), - eventPredicate = e ⇒ eventClass.isAssignableFrom(e.getClass), - biFunction.asInstanceOf[BiFunction[State, Event, State]]) :: cases - this + val supplierBiFunction = new BiFunction[S, E, State] { + def apply(t: S, u: E): State = handler.get() + } + + matchEvent(eventClass, supplierBiFunction) } /** - * Match any event + * Match any event. * - * Builds and returns the handler since this will not let through any states to subsequent match statements + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * Extra care should be taken when using [[matchAny]] as it will match any event. + * This method builds and returns the event handler since this will not let through any states to subsequent match statements. + * + * @return An EventHandler from the appended states. */ - def matchAny(biFunction: BiFunction[State, Event, State]): EventHandler[State, Event] = { - addCase(_ ⇒ true, biFunction.asInstanceOf[BiFunction[State, Event, State]]) + def matchAny(handler: BiFunction[State, Event, State]): EventHandler[State, Event] = { + addCase(_ ⇒ true, handler.asInstanceOf[BiFunction[State, Event, State]]) build() } @@ -87,10 +267,19 @@ final class EventHandlerBuilder[State >: Null, Event]() { * * Use this when then `State` is not needed in the `handler`, otherwise there is an overloaded method that pass * the state in a `BiFunction`. + * + * Note: event handlers are matched in the order they are added. Once a matching is found, it's selected for handling the event + * 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 event handlers. + * + * Extra care should be taken when using [[matchAny]] as it will match any event. + * This method builds and returns the event handler since this will not let through any states to subsequent match statements. + * + * @return An EventHandler from the appended states. */ - def matchAny(f: JFunction[Event, State]): EventHandler[State, Event] = { + def matchAny(handler: JFunction[Event, State]): EventHandler[State, Event] = { matchAny(new BiFunction[State, Event, State] { - override def apply(state: State, event: Event): State = f(event) + override def apply(state: State, event: Event): State = handler(event) }) build() } @@ -99,8 +288,8 @@ final class EventHandlerBuilder[State >: Null, Event]() { * Compose this builder with another builder. The handlers in this builder will be tried first followed * by the handlers in `other`. */ - def orElse(other: EventHandlerBuilder[State, Event]): EventHandlerBuilder[State, Event] = { - val newBuilder = new EventHandlerBuilder[State, Event] + def orElse[S2 <: State](other: EventHandlerBuilderByState[S2, State, Event]): EventHandlerBuilderByState[S2, State, Event] = { + val newBuilder = new EventHandlerBuilderByState[S2, State, Event](other.stateClass, other.statePredicate) // problem with overloaded constructor with `cases` as parameter newBuilder.cases = other.cases ::: cases newBuilder @@ -109,8 +298,6 @@ final class EventHandlerBuilder[State >: Null, Event]() { /** * Builds and returns a handler from the appended states. The returned [[EventHandler]] will throw a [[scala.MatchError]] * if applied to an event that has no defined case. - * - * The builder is reset to empty after build has been called. */ def build(): EventHandler[State, Event] = { val builtCases = cases.reverse.toArray 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 12bd6c97bc..14a8416b7d 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 @@ -4,7 +4,6 @@ package akka.persistence.typed.javadsl -import java.util.function.Predicate import java.util.{ Collections, Optional } import akka.actor.typed @@ -65,25 +64,14 @@ abstract class EventSourcedBehavior[Command, Event, State >: Null] private[akka] */ protected def eventHandler(): EventHandler[State, Event] - /** - * @param stateClass The handlers defined by this builder are used when the state is an instance of the `stateClass` - * @return A new, mutable, command handler builder - */ - protected final def commandHandlerBuilder[S <: State](stateClass: Class[S]): CommandHandlerBuilder[Command, Event, S, State] = - CommandHandlerBuilder.builder[Command, Event, S, State](stateClass) - - /** - * @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, command handler builder - */ - protected final def commandHandlerBuilder(statePredicate: Predicate[State]): CommandHandlerBuilder[Command, Event, State, State] = - CommandHandlerBuilder.builder[Command, Event, State](statePredicate) + protected final def newCommandHandlerBuilder(): CommandHandlerBuilder[Command, Event, State] = { + CommandHandlerBuilder.builder[Command, Event, State]() + } /** * @return A new, mutable, event handler builder */ - protected final def eventHandlerBuilder(): EventHandlerBuilder[State, Event] = + protected final def newEventHandlerBuilder(): EventHandlerBuilder[State, Event] = EventHandlerBuilder.builder[State, Event]() /** diff --git a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/NullEmptyStateTest.java b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/NullEmptyStateTest.java index 80bb5d1cb2..88b4932b07 100644 --- a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/NullEmptyStateTest.java +++ b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/NullEmptyStateTest.java @@ -16,8 +16,6 @@ import org.junit.ClassRule; import org.junit.Test; import org.scalatest.junit.JUnitSuite; -import java.util.Objects; - public class NullEmptyStateTest extends JUnitSuite { private static final Config config = @@ -47,17 +45,12 @@ public class NullEmptyStateTest extends JUnitSuite { @Override public CommandHandler commandHandler() { - CommandHandlerBuilder b1 = - commandHandlerBuilder(Objects::isNull) - .matchCommand("stop"::equals, command -> Effect().stop()) - .matchCommand(String.class, this::persistCommand); - CommandHandlerBuilder b2 = - commandHandlerBuilder(String.class) - .matchCommand("stop"::equals, command -> Effect().stop()) - .matchCommand(String.class, this::persistCommand); - - return b1.orElse(b2).build(); + return newCommandHandlerBuilder() + .forAnyState() + .matchCommand("stop"::equals, command -> Effect().stop()) + .matchCommand(String.class, this::persistCommand) + .build(); } private Effect persistCommand(String command) { @@ -66,7 +59,10 @@ public class NullEmptyStateTest extends JUnitSuite { @Override public EventHandler eventHandler() { - return eventHandlerBuilder().matchEvent(String.class, this::applyEvent).build(); + return newEventHandlerBuilder() + .forAnyState() + .matchEvent(String.class, this::applyEvent) + .build(); } private String applyEvent(String state, String event) { diff --git a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java index 25fead9d9b..67ff556fc9 100644 --- a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java +++ b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorCompileOnlyTest.java @@ -173,7 +173,8 @@ public class PersistentActorCompileOnlyTest { public CommandHandler commandHandler() { // #commonChainedEffects - return commandHandlerBuilder(ExampleState.class) + return newCommandHandlerBuilder() + .forStateType(ExampleState.class) .matchCommand( Cmd.class, (state, cmd) -> @@ -187,7 +188,8 @@ public class PersistentActorCompileOnlyTest { @Override public EventHandler eventHandler() { - return eventHandlerBuilder() + return newEventHandlerBuilder() + .forStateType(ExampleState.class) .matchEvent( Evt.class, (state, event) -> { @@ -312,7 +314,8 @@ public class PersistentActorCompileOnlyTest { @Override public CommandHandler commandHandler() { - return commandHandlerBuilder(EventsInFlight.class) + return newCommandHandlerBuilder() + .forAnyState() .matchCommand( DoSideEffect.class, (state, cmd) -> @@ -334,7 +337,8 @@ public class PersistentActorCompileOnlyTest { @Override public EventHandler eventHandler() { - return eventHandlerBuilder() + return newEventHandlerBuilder() + .forAnyState() .matchEvent( IntentRecord.class, (state, event) -> { diff --git a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java index dee56281e9..cd8feb9791 100644 --- a/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java +++ b/akka-persistence-typed/src/test/java/akka/persistence/typed/javadsl/PersistentActorJavaDslTest.java @@ -198,7 +198,8 @@ public class PersistentActorJavaDslTest extends JUnitSuite { @Override public CommandHandler commandHandler() { - return commandHandlerBuilder(State.class) + return newCommandHandlerBuilder() + .forAnyState() .matchCommand(Increment.class, this::increment) .matchCommand(IncrementWithConfirmation.class, this::incrementWithConfirmation) .matchCommand(GetValue.class, this::getValue) @@ -269,7 +270,10 @@ public class PersistentActorJavaDslTest extends JUnitSuite { @Override public EventHandler eventHandler() { - return eventHandlerBuilder().matchEvent(Incremented.class, this::applyIncremented).build(); + return newEventHandlerBuilder() + .forAnyState() + .matchEvent(Incremented.class, this::applyIncremented) + .build(); } @Override @@ -454,12 +458,6 @@ public class PersistentActorJavaDslTest extends JUnitSuite { TestProbe signalProbe = testKit.createTestProbe(); BehaviorInterceptor tap = new BehaviorInterceptor() { - - @Override - public Class interceptMessageType() { - return Command.class; - } - @Override public Behavior aroundReceive( TypedActorContext ctx, Command msg, ReceiveTarget target) { diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java index dadd629dd3..27a89484ef 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExample.java @@ -8,10 +8,7 @@ import akka.actor.typed.Behavior; import akka.actor.typed.javadsl.ActorContext; import akka.actor.typed.javadsl.Behaviors; import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.CommandHandler; -import akka.persistence.typed.javadsl.CommandHandlerBuilder; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehavior; +import akka.persistence.typed.javadsl.*; public class AccountExample extends EventSourcedBehavior< @@ -88,15 +85,17 @@ public class AccountExample return new EmptyAccount(); } - private CommandHandlerBuilder - initialHandler() { - return commandHandlerBuilder(EmptyAccount.class) + private CommandHandlerBuilderByState + initialCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(EmptyAccount.class) .matchCommand(CreateAccount.class, (__, cmd) -> Effect().persist(new AccountCreated())); } - private CommandHandlerBuilder - openedAccountHandler() { - return commandHandlerBuilder(OpenedAccount.class) + private CommandHandlerBuilderByState + openedAccountCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(OpenedAccount.class) .matchCommand(Deposit.class, (__, cmd) -> Effect().persist(new Deposited(cmd.amount))) .matchCommand( Withdraw.class, @@ -123,30 +122,35 @@ public class AccountExample }); } - private CommandHandlerBuilder - closedHandler() { - return commandHandlerBuilder(ClosedAccount.class) - .matchCommand(AccountCommand.class, (__, ___) -> Effect().unhandled()); + private CommandHandlerBuilderByState + closedCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(ClosedAccount.class) + .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); } @Override public CommandHandler commandHandler() { - return initialHandler().orElse(openedAccountHandler()).orElse(closedHandler()).build(); + return initialCmdHandler().orElse(openedAccountCmdHandler()).orElse(closedCmdHandler()).build(); + } + + private EventHandlerBuilderByState initialEvtHandler() { + return newEventHandlerBuilder() + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, () -> new OpenedAccount(0.0)); + } + + private EventHandlerBuilderByState + openedAccountEvtHandler() { + return newEventHandlerBuilder() + .forStateType(OpenedAccount.class) + .matchEvent(Deposited.class, (acc, cmd) -> new OpenedAccount(acc.balance + cmd.amount)) + .matchEvent(Withdrawn.class, (acc, cmd) -> new OpenedAccount(acc.balance - cmd.amount)) + .matchEvent(AccountClosed.class, ClosedAccount::new); } @Override public EventHandler eventHandler() { - return eventHandlerBuilder() - .matchEvent(AccountCreated.class, EmptyAccount.class, (__, ___) -> new OpenedAccount(0.0)) - .matchEvent( - Deposited.class, - OpenedAccount.class, - (acc, cmd) -> new OpenedAccount(acc.balance + cmd.amount)) - .matchEvent( - Withdrawn.class, - OpenedAccount.class, - (acc, cmd) -> new OpenedAccount(acc.balance - cmd.amount)) - .matchEvent(AccountClosed.class, OpenedAccount.class, (acc, cmd) -> new ClosedAccount()) - .build(); + return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); } } diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java new file mode 100644 index 0000000000..62b6131947 --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLiners.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.*; + +public class AccountExampleOneLiners + extends EventSourcedBehavior< + AccountExampleOneLiners.AccountCommand, + AccountExampleOneLiners.AccountEvent, + AccountExampleOneLiners.Account> { + + interface AccountCommand {} + + public static class CreateAccount implements AccountCommand {} + + public static class Deposit implements AccountCommand { + public final double amount; + + public Deposit(double amount) { + this.amount = amount; + } + } + + public static class Withdraw implements AccountCommand { + public final double amount; + + public Withdraw(double amount) { + this.amount = amount; + } + } + + public static class CloseAccount implements AccountCommand {} + + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final double amount; + + Deposited(double amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final double amount; + + Withdrawn(double amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + interface Account {} + + public static class EmptyAccount implements Account {} + + public static class OpenedAccount implements Account { + public final double balance; + + OpenedAccount(double balance) { + this.balance = balance; + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountExampleOneLiners(context, accountNumber)); + } + + public AccountExampleOneLiners(ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return new EmptyAccount(); + } + + private Effect createAccount() { + return Effect().persist(new AccountCreated()); + } + + private Effect depositCommand(Deposit deposit) { + return Effect().persist(new Deposited(deposit.amount)); + } + + private Effect withdrawCommand(OpenedAccount account, Withdraw withdraw) { + if ((account.balance - withdraw.amount) < 0.0) { + return Effect().unhandled(); // TODO replies are missing in this example + } else { + return Effect() + .persist(new Withdrawn(withdraw.amount)) + .thenRun( + acc2 -> { + // we know this cast is safe, but somewhat ugly + OpenedAccount openAccount = (OpenedAccount) acc2; + // do some side-effect using balance + System.out.println(openAccount.balance); + }); + } + } + + private Effect closeCommand(OpenedAccount account, CloseAccount cmd) { + if (account.balance == 0.0) return Effect().persist(new AccountClosed()); + else return Effect().unhandled(); + } + + private CommandHandlerBuilderByState + initialCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(EmptyAccount.class) + .matchCommand(CreateAccount.class, this::createAccount); + } + + private CommandHandlerBuilderByState + openedAccountCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, this::depositCommand) + .matchCommand(Withdraw.class, this::withdrawCommand) + .matchCommand(CloseAccount.class, this::closeCommand); + } + + private CommandHandlerBuilderByState + closedCmdHandler() { + return newCommandHandlerBuilder() + .forStateType(ClosedAccount.class) + .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); + } + + @Override + public CommandHandler commandHandler() { + return initialCmdHandler().orElse(openedAccountCmdHandler()).orElse(closedCmdHandler()).build(); + } + + private OpenedAccount openAccount() { + return new OpenedAccount(0.0); + } + + private OpenedAccount makeDeposit(OpenedAccount acc, Deposited deposit) { + return new OpenedAccount(acc.balance + deposit.amount); + } + + private OpenedAccount makeWithdraw(OpenedAccount acc, Withdrawn withdrawn) { + return new OpenedAccount(acc.balance - withdrawn.amount); + } + + private ClosedAccount closeAccount() { + return new ClosedAccount(); + } + + private EventHandlerBuilderByState initialEvtHandler() { + return newEventHandlerBuilder() + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, this::openAccount); + } + + private EventHandlerBuilderByState + openedAccountEvtHandler() { + return newEventHandlerBuilder() + .forStateType(OpenedAccount.class) + .matchEvent(Deposited.class, this::makeDeposit) + .matchEvent(Withdrawn.class, this::makeWithdraw) + .matchEvent(AccountClosed.class, ClosedAccount::new); + } + + @Override + public EventHandler eventHandler() { + return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); + } +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java new file mode 100644 index 0000000000..d0b8f0da72 --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModel.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.*; + +public class AccountExampleOneLinersInModel + extends EventSourcedBehavior< + AccountExampleOneLinersInModel.AccountCommand, + AccountExampleOneLinersInModel.AccountEvent, + AccountExampleOneLinersInModel.Account> { + + interface AccountCommand {} + + public static class CreateAccount implements AccountCommand {} + + public static class Deposit implements AccountCommand { + public final double amount; + + public Deposit(double amount) { + this.amount = amount; + } + } + + public static class Withdraw implements AccountCommand { + public final double amount; + + public Withdraw(double amount) { + this.amount = amount; + } + } + + public static class CloseAccount implements AccountCommand {} + + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final double amount; + + Deposited(double amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final double amount; + + Withdrawn(double amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + interface Account {} + + public class EmptyAccount implements Account { + + Effect createAccount(CreateAccount cmd) { + return Effect().persist(new AccountCreated()); + } + + OpenedAccount openAccount(AccountCreated evt) { + return new OpenedAccount(0.0); + } + } + + public class OpenedAccount implements Account { + public final double balance; + + OpenedAccount(double balance) { + this.balance = balance; + } + + Effect depositCommand(Deposit deposit) { + return Effect().persist(new Deposited(deposit.amount)); + } + + Effect withdrawCommand(Withdraw withdraw) { + if ((balance - withdraw.amount) < 0.0) { + return Effect().unhandled(); // TODO replies are missing in this example + } else { + return Effect() + .persist(new Withdrawn(withdraw.amount)) + .thenRun( + acc2 -> { + // we know this cast is safe, but somewhat ugly + OpenedAccount openAccount = (OpenedAccount) acc2; + // do some side-effect using balance + System.out.println(openAccount.balance); + }); + } + } + + Effect closeCommand(CloseAccount cmd) { + if (balance == 0.0) return Effect().persist(new AccountClosed()); + else return Effect().unhandled(); + } + + OpenedAccount makeDeposit(Deposited deposit) { + return new OpenedAccount(balance + deposit.amount); + } + + OpenedAccount makeWithdraw(Withdrawn withdrawn) { + return new OpenedAccount(balance - withdrawn.amount); + } + + ClosedAccount closeAccount(AccountClosed evt) { + return new ClosedAccount(); + } + } + + public class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountExampleOneLinersInModel(context, accountNumber)); + } + + public AccountExampleOneLinersInModel( + ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return new EmptyAccount(); + } + + @Override + public CommandHandler commandHandler() { + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchCommand(CreateAccount.class, EmptyAccount::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, OpenedAccount::depositCommand) + .matchCommand(Withdraw.class, OpenedAccount::withdrawCommand) + .matchCommand(CloseAccount.class, OpenedAccount::closeCommand); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + @Override + public EventHandler eventHandler() { + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forStateType(EmptyAccount.class) + .matchEvent(AccountCreated.class, EmptyAccount::openAccount); + + builder + .forStateType(OpenedAccount.class) + .matchEvent(Deposited.class, OpenedAccount::makeDeposit) + .matchEvent(Withdrawn.class, OpenedAccount::makeWithdraw) + .matchEvent(AccountClosed.class, OpenedAccount::closeAccount); + + return builder.build(); + } +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java new file mode 100644 index 0000000000..a948d6fcb4 --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersInModelWithNull.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.*; + +public class AccountExampleOneLinersInModelWithNull + extends EventSourcedBehavior< + AccountExampleOneLinersInModelWithNull.AccountCommand, + AccountExampleOneLinersInModelWithNull.AccountEvent, + AccountExampleOneLinersInModelWithNull.Account> { + + interface AccountCommand {} + + public static class CreateAccount implements AccountCommand {} + + public static class Deposit implements AccountCommand { + public final double amount; + + public Deposit(double amount) { + this.amount = amount; + } + } + + public static class Withdraw implements AccountCommand { + public final double amount; + + public Withdraw(double amount) { + this.amount = amount; + } + } + + public static class CloseAccount implements AccountCommand {} + + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final double amount; + + Deposited(double amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final double amount; + + Withdrawn(double amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + interface Account {} + + public class OpenedAccount implements Account { + public final double balance; + + OpenedAccount(double balance) { + this.balance = balance; + } + + Effect depositCommand(Deposit deposit) { + return Effect().persist(new Deposited(deposit.amount)); + } + + Effect withdrawCommand(Withdraw withdraw) { + if ((balance - withdraw.amount) < 0.0) { + return Effect().unhandled(); // TODO replies are missing in this example + } else { + return Effect() + .persist(new Withdrawn(withdraw.amount)) + .thenRun( + acc2 -> { + // we know this cast is safe, but somewhat ugly + OpenedAccount openAccount = (OpenedAccount) acc2; + // do some side-effect using balance + System.out.println(openAccount.balance); + }); + } + } + + Effect closeCommand(CloseAccount cmd) { + if (balance == 0.0) return Effect().persist(new AccountClosed()); + else return Effect().unhandled(); + } + + OpenedAccount makeDeposit(Deposited deposit) { + return new OpenedAccount(balance + deposit.amount); + } + + OpenedAccount makeWithdraw(Withdrawn withdrawn) { + return new OpenedAccount(balance - withdrawn.amount); + } + + ClosedAccount closeAccount(AccountClosed cmd) { + return new ClosedAccount(); + } + } + + public class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup( + context -> new AccountExampleOneLinersInModelWithNull(context, accountNumber)); + } + + public AccountExampleOneLinersInModelWithNull( + ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return null; + } + + private Effect createAccount(CreateAccount cmd) { + return Effect().persist(new AccountCreated()); + } + + private OpenedAccount openAccount() { + return new OpenedAccount(0.0); + } + + @Override + public CommandHandler commandHandler() { + + CommandHandlerBuilder builder = + newCommandHandlerBuilder(); + + builder.forNullState().matchCommand(CreateAccount.class, this::createAccount); + + builder + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, OpenedAccount::depositCommand) + .matchCommand(Withdraw.class, OpenedAccount::withdrawCommand) + .matchCommand(CloseAccount.class, OpenedAccount::closeCommand); + + builder.forStateType(ClosedAccount.class).matchAny(() -> Effect().unhandled()); + + return builder.build(); + } + + @Override + public EventHandler eventHandler() { + + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder.forNullState().matchEvent(AccountCreated.class, this::openAccount); + + builder + .forStateType(OpenedAccount.class) + .matchEvent(Deposited.class, OpenedAccount::makeDeposit) + .matchEvent(Withdrawn.class, OpenedAccount::makeWithdraw) + .matchEvent(AccountClosed.class, OpenedAccount::closeAccount); + + return builder.build(); + } +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java new file mode 100644 index 0000000000..d9ccee5070 --- /dev/null +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/AccountExampleOneLinersWithNull.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018-2019 Lightbend Inc. + */ + +package jdocs.akka.persistence.typed; + +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.persistence.typed.PersistenceId; +import akka.persistence.typed.javadsl.*; + +public class AccountExampleOneLinersWithNull + extends EventSourcedBehavior< + AccountExampleOneLinersWithNull.AccountCommand, + AccountExampleOneLinersWithNull.AccountEvent, + AccountExampleOneLinersWithNull.Account> { + + interface AccountCommand {} + + public static class CreateAccount implements AccountCommand {} + + public static class Deposit implements AccountCommand { + public final double amount; + + public Deposit(double amount) { + this.amount = amount; + } + } + + public static class Withdraw implements AccountCommand { + public final double amount; + + public Withdraw(double amount) { + this.amount = amount; + } + } + + public static class CloseAccount implements AccountCommand {} + + interface AccountEvent {} + + public static class AccountCreated implements AccountEvent {} + + public static class Deposited implements AccountEvent { + public final double amount; + + Deposited(double amount) { + this.amount = amount; + } + } + + public static class Withdrawn implements AccountEvent { + public final double amount; + + Withdrawn(double amount) { + this.amount = amount; + } + } + + public static class AccountClosed implements AccountEvent {} + + interface Account {} + + public static class OpenedAccount implements Account { + public final double balance; + + OpenedAccount(double balance) { + this.balance = balance; + } + } + + public static class ClosedAccount implements Account {} + + public static Behavior behavior(String accountNumber) { + return Behaviors.setup(context -> new AccountExampleOneLinersWithNull(context, accountNumber)); + } + + public AccountExampleOneLinersWithNull( + ActorContext context, String accountNumber) { + super(new PersistenceId(accountNumber)); + } + + @Override + public Account emptyState() { + return null; + } + + private Effect createAccount() { + return Effect().persist(new AccountCreated()); + } + + private Effect depositCommand(Deposit deposit) { + return Effect().persist(new Deposited(deposit.amount)); + } + + private Effect withdrawCommand(OpenedAccount account, Withdraw withdraw) { + if ((account.balance - withdraw.amount) < 0.0) { + return Effect().unhandled(); // TODO replies are missing in this example + } else { + return Effect() + .persist(new Withdrawn(withdraw.amount)) + .thenRun( + acc2 -> { + // we know this cast is safe, but somewhat ugly + OpenedAccount openAccount = (OpenedAccount) acc2; + // do some side-effect using balance + System.out.println(openAccount.balance); + }); + } + } + + private Effect closeCommand(OpenedAccount account, CloseAccount cmd) { + if (account.balance == 0.0) return Effect().persist(new AccountClosed()); + else return Effect().unhandled(); + } + + private CommandHandlerBuilderByState + initialHandler() { + return newCommandHandlerBuilder() + .forNullState() + .matchCommand(CreateAccount.class, this::createAccount); + } + + private CommandHandlerBuilderByState + openedAccountHandler() { + return newCommandHandlerBuilder() + .forStateType(OpenedAccount.class) + .matchCommand(Deposit.class, this::depositCommand) + .matchCommand(Withdraw.class, this::withdrawCommand) + .matchCommand(CloseAccount.class, this::closeCommand); + } + + private CommandHandlerBuilderByState + closedHandler() { + return newCommandHandlerBuilder() + .forStateType(ClosedAccount.class) + .matchCommand(AccountCommand.class, __ -> Effect().unhandled()); + } + + @Override + public CommandHandler commandHandler() { + return initialHandler().orElse(openedAccountHandler()).orElse(closedHandler()).build(); + } + + private OpenedAccount openAccount() { + return new OpenedAccount(0.0); + } + + private OpenedAccount makeDeposit(OpenedAccount acc, Deposited deposit) { + return new OpenedAccount(acc.balance + deposit.amount); + } + + private OpenedAccount makeWithdraw(OpenedAccount acc, Withdrawn withdrawn) { + return new OpenedAccount(acc.balance - withdrawn.amount); + } + + private ClosedAccount closeAccount() { + return new ClosedAccount(); + } + + private EventHandlerBuilderByState initialEvtHandler() { + return newEventHandlerBuilder() + .forNullState() + .matchEvent(AccountCreated.class, this::openAccount); + } + + private EventHandlerBuilderByState + openedAccountEvtHandler() { + return newEventHandlerBuilder() + .forStateType(OpenedAccount.class) + .matchEvent(Deposited.class, this::makeDeposit) + .matchEvent(Withdrawn.class, this::makeWithdraw) + .matchEvent(AccountClosed.class, ClosedAccount::new); + } + + @Override + public EventHandler eventHandler() { + return initialEvtHandler().orElse(openedAccountEvtHandler()).build(); + } +} diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BlogPostExample.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BlogPostExample.java index d9f8f7322a..f97c2fdc36 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BlogPostExample.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/BlogPostExample.java @@ -10,10 +10,7 @@ import akka.actor.typed.Behavior; import akka.actor.typed.javadsl.ActorContext; import akka.actor.typed.javadsl.Behaviors; import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.CommandHandler; -import akka.persistence.typed.javadsl.CommandHandlerBuilder; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehavior; +import akka.persistence.typed.javadsl.*; public class BlogPostExample { @@ -161,9 +158,10 @@ public class BlogPostExample { } // #initial-command-handler - private CommandHandlerBuilder + private CommandHandlerBuilderByState initialCommandHandler() { - return commandHandlerBuilder(BlankState.class) + return newCommandHandlerBuilder() + .forStateType(BlankState.class) .matchCommand( AddPost.class, (state, cmd) -> { @@ -178,9 +176,10 @@ public class BlogPostExample { // #initial-command-handler // #post-added-command-handler - private CommandHandlerBuilder + private CommandHandlerBuilderByState draftCommandHandler() { - return commandHandlerBuilder(DraftState.class) + return newCommandHandlerBuilder() + .forStateType(DraftState.class) .matchCommand( ChangeBody.class, (state, cmd) -> { @@ -205,9 +204,10 @@ public class BlogPostExample { }); } - private CommandHandlerBuilder + private CommandHandlerBuilderByState publishedCommandHandler() { - return commandHandlerBuilder(PublishedState.class) + return newCommandHandlerBuilder() + .forStateType(PublishedState.class) .matchCommand( ChangeBody.class, (state, cmd) -> { @@ -222,9 +222,10 @@ public class BlogPostExample { }); } - private CommandHandlerBuilder + private CommandHandlerBuilderByState commonCommandHandler() { - return commandHandlerBuilder(BlogState.class) + return newCommandHandlerBuilder() + .forStateType(BlogState.class) .matchCommand(AddPost.class, (state, cmd) -> Effect().unhandled()); } // #post-added-command-handler @@ -243,25 +244,31 @@ public class BlogPostExample { // #event-handler @Override public EventHandler eventHandler() { - return eventHandlerBuilder() - .matchEvent(PostAdded.class, (state, event) -> new DraftState(event.content)) + + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forStateType(BlankState.class) + .matchEvent(PostAdded.class, event -> new DraftState(event.content)); + + builder + .forStateType(DraftState.class) .matchEvent( BodyChanged.class, - DraftState.class, (state, chg) -> state.withContent( new PostContent(state.postId(), state.postContent.title, chg.newBody))) + .matchEvent(Published.class, (state, event) -> new PublishedState(state.postContent)); + + builder + .forStateType(PublishedState.class) .matchEvent( BodyChanged.class, - PublishedState.class, (state, chg) -> state.withContent( - new PostContent(state.postId(), state.postContent.title, chg.newBody))) - .matchEvent( - Published.class, - DraftState.class, - (state, event) -> new PublishedState(state.postContent)) - .build(); + new PostContent(state.postId(), state.postContent.title, chg.newBody))); + + return builder.build(); } // #event-handler diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/MovieWatchList.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/MovieWatchList.java index ff9bf05f47..73160885b2 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/MovieWatchList.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/MovieWatchList.java @@ -98,7 +98,8 @@ public class MovieWatchList @Override public CommandHandler commandHandler() { - return commandHandlerBuilder(MovieList.class) + return newCommandHandlerBuilder() + .forAnyState() .matchCommand( AddMovie.class, (state, cmd) -> { @@ -120,7 +121,8 @@ public class MovieWatchList @Override public EventHandler eventHandler() { - return eventHandlerBuilder() + return newEventHandlerBuilder() + .forAnyState() .matchEvent(MovieAdded.class, (state, event) -> state.add(event.movieId)) .matchEvent(MovieRemoved.class, (state, event) -> state.remove(event.movieId)) .build(); diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/NullBlogState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/NullBlogState.java index 251c940fd8..68f7ac564c 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/NullBlogState.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/NullBlogState.java @@ -7,12 +7,7 @@ package jdocs.akka.persistence.typed; import akka.Done; import akka.actor.typed.ActorRef; import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.CommandHandler; -import akka.persistence.typed.javadsl.CommandHandlerBuilder; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehavior; - -import java.util.Objects; +import akka.persistence.typed.javadsl.*; public class NullBlogState { @@ -124,9 +119,10 @@ public class NullBlogState { public static class BlogBehavior extends EventSourcedBehavior { - private CommandHandlerBuilder + private CommandHandlerBuilderByState initialCommandHandler() { - return commandHandlerBuilder(Objects::isNull) + return newCommandHandlerBuilder() + .forNullState() .matchCommand( AddPost.class, cmd -> { @@ -137,9 +133,10 @@ public class NullBlogState { }); } - private CommandHandlerBuilder + private CommandHandlerBuilderByState postCommandHandler() { - return commandHandlerBuilder(Objects::nonNull) + return newCommandHandlerBuilder() + .forNonNullState() .matchCommand( ChangeBody.class, (state, cmd) -> { @@ -181,15 +178,23 @@ public class NullBlogState { @Override public EventHandler eventHandler() { - return eventHandlerBuilder() - .matchEvent(PostAdded.class, event -> new BlogState(event.content, false)) + + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forNullState() + .matchEvent(PostAdded.class, event -> new BlogState(event.content, false)); + + builder + .forNonNullState() .matchEvent( BodyChanged.class, (state, chg) -> state.withContent( new PostContent(state.postId(), state.postContent.title, chg.newBody))) - .matchEvent(Published.class, (state, event) -> new BlogState(state.postContent, true)) - .build(); + .matchEvent(Published.class, (state, event) -> new BlogState(state.postContent, true)); + + return builder.build(); } } } diff --git a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/OptionalBlogState.java b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/OptionalBlogState.java index 3c353807cb..a1e9436d37 100644 --- a/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/OptionalBlogState.java +++ b/akka-persistence-typed/src/test/java/jdocs/akka/persistence/typed/OptionalBlogState.java @@ -7,10 +7,7 @@ package jdocs.akka.persistence.typed; import akka.Done; import akka.actor.typed.ActorRef; import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.CommandHandler; -import akka.persistence.typed.javadsl.CommandHandlerBuilder; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehavior; +import akka.persistence.typed.javadsl.*; import java.util.Optional; @@ -125,9 +122,11 @@ public class OptionalBlogState { public static class BlogBehavior extends EventSourcedBehavior> { - private CommandHandlerBuilder, Optional> + private CommandHandlerBuilderByState< + BlogCommand, BlogEvent, Optional, Optional> initialCommandHandler() { - return commandHandlerBuilder(state -> !state.isPresent()) + return newCommandHandlerBuilder() + .forState(state -> !state.isPresent()) .matchCommand( AddPost.class, (state, cmd) -> { @@ -138,9 +137,11 @@ public class OptionalBlogState { }); } - private CommandHandlerBuilder, Optional> + private CommandHandlerBuilderByState< + BlogCommand, BlogEvent, Optional, Optional> postCommandHandler() { - return commandHandlerBuilder(state -> state.isPresent()) + return newCommandHandlerBuilder() + .forState(Optional::isPresent) .matchCommand( ChangeBody.class, (state, cmd) -> { @@ -182,9 +183,16 @@ public class OptionalBlogState { @Override public EventHandler, BlogEvent> eventHandler() { - return eventHandlerBuilder() + + EventHandlerBuilder, BlogEvent> builder = newEventHandlerBuilder(); + + builder + .forState(state -> !state.isPresent()) .matchEvent( - PostAdded.class, (state, event) -> Optional.of(new BlogState(event.content, false))) + PostAdded.class, (state, event) -> Optional.of(new BlogState(event.content, false))); + + builder + .forState(Optional::isPresent) .matchEvent( BodyChanged.class, (state, chg) -> @@ -195,8 +203,9 @@ public class OptionalBlogState { blogState.postId(), blogState.postContent.title, chg.newBody)))) .matchEvent( Published.class, - (state, event) -> state.map(blogState -> new BlogState(blogState.postContent, true))) - .build(); + (state, event) -> state.map(blogState -> new BlogState(blogState.postContent, true))); + + return builder.build(); } } } 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 fc29be7b09..bd7de303ec 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 @@ -7,11 +7,7 @@ package jdocs.akka.persistence.typed.auction; import akka.Done; import akka.persistence.typed.ExpectingReply; import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.javadsl.CommandHandler; -import akka.persistence.typed.javadsl.CommandHandlerBuilder; -import akka.persistence.typed.javadsl.Effect; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehavior; +import akka.persistence.typed.javadsl.*; import static jdocs.akka.persistence.typed.auction.AuctionCommand.*; import static jdocs.akka.persistence.typed.auction.AuctionEvent.*; @@ -37,9 +33,10 @@ public class AuctionEntity } // Command handler for the not started state. - private CommandHandlerBuilder + private CommandHandlerBuilderByState notStartedHandler = - commandHandlerBuilder(state -> state.getStatus() == AuctionStatus.NOT_STARTED) + newCommandHandlerBuilder() + .forState(state -> state.getStatus() == AuctionStatus.NOT_STARTED) .matchCommand(StartAuction.class, this::startAuction) .matchCommand( PlaceBid.class, @@ -47,17 +44,19 @@ public class AuctionEntity Effect().reply(cmd, createResult(state, PlaceBidStatus.NOT_STARTED))); // Command handler for the under auction state. - private CommandHandlerBuilder + private CommandHandlerBuilderByState underAuctionHandler = - commandHandlerBuilder(state -> state.getStatus() == AuctionStatus.UNDER_AUCTION) + newCommandHandlerBuilder() + .forState(state -> state.getStatus() == AuctionStatus.UNDER_AUCTION) .matchCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .matchCommand(PlaceBid.class, this::placeBid) .matchCommand(FinishBidding.class, this::finishBidding); // Command handler for the completed state. - private CommandHandlerBuilder + private CommandHandlerBuilderByState completedHandler = - commandHandlerBuilder(state -> state.getStatus() == AuctionStatus.COMPLETE) + newCommandHandlerBuilder() + .forState(state -> state.getStatus() == AuctionStatus.COMPLETE) .matchCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .matchCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd)) .matchCommand( @@ -66,9 +65,10 @@ public class AuctionEntity Effect().reply(cmd, createResult(state, PlaceBidStatus.FINISHED))); // Command handler for the cancelled state. - private CommandHandlerBuilder + private CommandHandlerBuilderByState cancelledHandler = - commandHandlerBuilder(state -> state.getStatus() == AuctionStatus.CANCELLED) + newCommandHandlerBuilder() + .forState(state -> state.getStatus() == AuctionStatus.CANCELLED) .matchCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd)) .matchCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd)) .matchCommand(CancelAuction.class, (state, cmd) -> alreadyDone(cmd)) @@ -77,14 +77,16 @@ public class AuctionEntity (state, cmd) -> Effect().reply(cmd, createResult(state, PlaceBidStatus.CANCELLED))); - private CommandHandlerBuilder + private CommandHandlerBuilderByState getAuctionHandler = - commandHandlerBuilder(AuctionState.class) + newCommandHandlerBuilder() + .forStateType(AuctionState.class) .matchCommand(GetAuction.class, (state, cmd) -> Effect().reply(cmd, state)); - private CommandHandlerBuilder + private CommandHandlerBuilderByState cancelHandler = - commandHandlerBuilder(AuctionState.class) + newCommandHandlerBuilder() + .forStateType(AuctionState.class) .matchCommand(CancelAuction.class, this::cancelAuction); // Note, an item can go from completed to cancelled, since it is the item service that controls // whether an auction is cancelled or not. If it cancels before it receives a bidding finished @@ -256,13 +258,21 @@ public class AuctionEntity @Override public EventHandler eventHandler() { - return eventHandlerBuilder() - .matchEvent(AuctionStarted.class, (state, evt) -> AuctionState.start(evt.getAuction())) + + EventHandlerBuilder builder = newEventHandlerBuilder(); + + builder + .forState(auction -> auction.getStatus() == AuctionStatus.NOT_STARTED) + .matchEvent(AuctionStarted.class, (state, evt) -> AuctionState.start(evt.getAuction())); + + builder + .forState(auction -> auction.getStatus() == AuctionStatus.UNDER_AUCTION) .matchEvent(BidPlaced.class, (state, evt) -> state.bid(evt.getBid())) .matchEvent(BiddingFinished.class, (state, evt) -> state.withStatus(AuctionStatus.COMPLETE)) .matchEvent( - AuctionCancelled.class, (state, evt) -> state.withStatus(AuctionStatus.CANCELLED)) - .build(); + AuctionCancelled.class, (state, evt) -> state.withStatus(AuctionStatus.CANCELLED)); + + return builder.build(); } private PlaceBidResult createResult(AuctionState state, PlaceBidStatus status) {