* 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.
This commit is contained in:
parent
1442a2428c
commit
d3836aecfb
5 changed files with 456 additions and 25 deletions
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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._
|
||||
|
|
|
|||
|
|
@ -0,0 +1,390 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
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]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuctionCommand, AuctionEvent, AuctionState> {
|
||||
extends EventSourcedBehaviorWithEnforcedReplies<AuctionCommand, AuctionEvent, AuctionState> {
|
||||
|
||||
private final UUID entityUUID;
|
||||
|
||||
|
|
@ -33,9 +33,10 @@ public class AuctionEntity
|
|||
}
|
||||
|
||||
// Command handler for the not started state.
|
||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||
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<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||
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<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||
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<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||
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<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||
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<AuctionEvent, AuctionState> startAuction(AuctionState state, StartAuction cmd) {
|
||||
private ReplyEffect<AuctionEvent, AuctionState> startAuction(
|
||||
AuctionState state, StartAuction cmd) {
|
||||
return Effect()
|
||||
.persist(new AuctionStarted(entityUUID, cmd.getAuction()))
|
||||
.thenReply(cmd, __ -> Done.getInstance());
|
||||
}
|
||||
|
||||
private Effect<AuctionEvent, AuctionState> finishBidding(AuctionState state, FinishBidding cmd) {
|
||||
private ReplyEffect<AuctionEvent, AuctionState> finishBidding(
|
||||
AuctionState state, FinishBidding cmd) {
|
||||
return Effect()
|
||||
.persist(new BiddingFinished(entityUUID))
|
||||
.thenReply(cmd, __ -> Done.getInstance());
|
||||
}
|
||||
|
||||
private Effect<AuctionEvent, AuctionState> cancelAuction(AuctionState state, CancelAuction cmd) {
|
||||
private ReplyEffect<AuctionEvent, AuctionState> cancelAuction(
|
||||
AuctionState state, CancelAuction cmd) {
|
||||
return Effect()
|
||||
.persist(new AuctionCancelled(entityUUID))
|
||||
.thenReply(cmd, __ -> Done.getInstance());
|
||||
}
|
||||
|
||||
/** The main logic for handling of bids. */
|
||||
private Effect<AuctionEvent, AuctionState> placeBid(AuctionState state, PlaceBid bid) {
|
||||
private ReplyEffect<AuctionEvent, AuctionState> placeBid(AuctionState state, PlaceBid bid) {
|
||||
Auction auction = state.getAuction().get();
|
||||
|
||||
Instant now = Instant.now();
|
||||
|
|
@ -187,7 +195,7 @@ public class AuctionEntity
|
|||
* <p>This emits two events, one for the bid currently being replace, and another automatic bid
|
||||
* for the current bidder.
|
||||
*/
|
||||
private Effect<AuctionEvent, AuctionState> handleAutomaticOutbid(
|
||||
private ReplyEffect<AuctionEvent, AuctionState> 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<AuctionEvent, AuctionState> handleNewWinningBidder(
|
||||
private ReplyEffect<AuctionEvent, AuctionState> 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<AuctionCommand, AuctionEvent, AuctionState> commandHandler() {
|
||||
public CommandHandlerWithReply<AuctionCommand, AuctionEvent, AuctionState> commandHandler() {
|
||||
return notStartedHandler
|
||||
.orElse(underAuctionHandler)
|
||||
.orElse(completedHandler)
|
||||
|
|
@ -284,7 +292,7 @@ public class AuctionEntity
|
|||
}
|
||||
}
|
||||
|
||||
private Effect<AuctionEvent, AuctionState> alreadyDone(ExpectingReply<Done> cmd) {
|
||||
private ReplyEffect<AuctionEvent, AuctionState> alreadyDone(ExpectingReply<Done> cmd) {
|
||||
return Effect().reply(cmd, Done.getInstance());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue