* 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`],
|
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`].
|
@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
|
Scala
|
||||||
: @@snip [AccountExampleWithEventHandlersInState.scala](/akka-persistence-typed/src/test/scala/docs/akka/persistence/typed/AccountExampleWithEventHandlersInState.scala) { #reply }
|
: @@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.annotation.InternalApi
|
||||||
import akka.persistence.typed.internal._
|
import akka.persistence.typed.internal._
|
||||||
import akka.persistence.typed.javadsl.CommandHandlerBuilderByState.CommandHandlerCase
|
|
||||||
import akka.util.OptionVal
|
import akka.util.OptionVal
|
||||||
|
|
||||||
import scala.compat.java8.FunctionConverters._
|
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
|
* 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.
|
* 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).
|
* 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
|
* While the actor is persisting events, the incoming messages are stashed and only
|
||||||
* delivered to the handler once persisting them has completed.
|
* 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.
|
* 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,
|
* The event handlers are invoked during recovery as well as running operation of this behavior,
|
||||||
* in order to keep updating the state state.
|
* 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 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]()
|
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.
|
* 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
|
* 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]].
|
* 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))
|
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
|
* https://github.com/lagom/online-auction-java/blob/master/bidding-impl/src/main/java/com/example/auction/bidding/impl/AuctionEntity.java
|
||||||
*/
|
*/
|
||||||
public class AuctionEntity
|
public class AuctionEntity
|
||||||
extends EventSourcedBehavior<AuctionCommand, AuctionEvent, AuctionState> {
|
extends EventSourcedBehaviorWithEnforcedReplies<AuctionCommand, AuctionEvent, AuctionState> {
|
||||||
|
|
||||||
private final UUID entityUUID;
|
private final UUID entityUUID;
|
||||||
|
|
||||||
|
|
@ -33,9 +33,10 @@ public class AuctionEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command handler for the not started state.
|
// Command handler for the not started state.
|
||||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
private CommandHandlerWithReplyBuilderByState<
|
||||||
|
AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||||
notStartedHandler =
|
notStartedHandler =
|
||||||
newCommandHandlerBuilder()
|
newCommandHandlerWithReplyBuilder()
|
||||||
.forState(state -> state.getStatus() == AuctionStatus.NOT_STARTED)
|
.forState(state -> state.getStatus() == AuctionStatus.NOT_STARTED)
|
||||||
.onCommand(StartAuction.class, this::startAuction)
|
.onCommand(StartAuction.class, this::startAuction)
|
||||||
.onCommand(
|
.onCommand(
|
||||||
|
|
@ -44,18 +45,20 @@ public class AuctionEntity
|
||||||
Effect().reply(cmd, createResult(state, PlaceBidStatus.NOT_STARTED)));
|
Effect().reply(cmd, createResult(state, PlaceBidStatus.NOT_STARTED)));
|
||||||
|
|
||||||
// Command handler for the under auction state.
|
// Command handler for the under auction state.
|
||||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
private CommandHandlerWithReplyBuilderByState<
|
||||||
|
AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||||
underAuctionHandler =
|
underAuctionHandler =
|
||||||
newCommandHandlerBuilder()
|
newCommandHandlerWithReplyBuilder()
|
||||||
.forState(state -> state.getStatus() == AuctionStatus.UNDER_AUCTION)
|
.forState(state -> state.getStatus() == AuctionStatus.UNDER_AUCTION)
|
||||||
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
||||||
.onCommand(PlaceBid.class, this::placeBid)
|
.onCommand(PlaceBid.class, this::placeBid)
|
||||||
.onCommand(FinishBidding.class, this::finishBidding);
|
.onCommand(FinishBidding.class, this::finishBidding);
|
||||||
|
|
||||||
// Command handler for the completed state.
|
// Command handler for the completed state.
|
||||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
private CommandHandlerWithReplyBuilderByState<
|
||||||
|
AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||||
completedHandler =
|
completedHandler =
|
||||||
newCommandHandlerBuilder()
|
newCommandHandlerWithReplyBuilder()
|
||||||
.forState(state -> state.getStatus() == AuctionStatus.COMPLETE)
|
.forState(state -> state.getStatus() == AuctionStatus.COMPLETE)
|
||||||
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
||||||
.onCommand(FinishBidding.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)));
|
Effect().reply(cmd, createResult(state, PlaceBidStatus.FINISHED)));
|
||||||
|
|
||||||
// Command handler for the cancelled state.
|
// Command handler for the cancelled state.
|
||||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
private CommandHandlerWithReplyBuilderByState<
|
||||||
|
AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||||
cancelledHandler =
|
cancelledHandler =
|
||||||
newCommandHandlerBuilder()
|
newCommandHandlerWithReplyBuilder()
|
||||||
.forState(state -> state.getStatus() == AuctionStatus.CANCELLED)
|
.forState(state -> state.getStatus() == AuctionStatus.CANCELLED)
|
||||||
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
.onCommand(StartAuction.class, (state, cmd) -> alreadyDone(cmd))
|
||||||
.onCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd))
|
.onCommand(FinishBidding.class, (state, cmd) -> alreadyDone(cmd))
|
||||||
|
|
@ -77,9 +81,10 @@ public class AuctionEntity
|
||||||
(state, cmd) ->
|
(state, cmd) ->
|
||||||
Effect().reply(cmd, createResult(state, PlaceBidStatus.CANCELLED)));
|
Effect().reply(cmd, createResult(state, PlaceBidStatus.CANCELLED)));
|
||||||
|
|
||||||
private CommandHandlerBuilderByState<AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
private CommandHandlerWithReplyBuilderByState<
|
||||||
|
AuctionCommand, AuctionEvent, AuctionState, AuctionState>
|
||||||
getAuctionHandler =
|
getAuctionHandler =
|
||||||
newCommandHandlerBuilder()
|
newCommandHandlerWithReplyBuilder()
|
||||||
.forStateType(AuctionState.class)
|
.forStateType(AuctionState.class)
|
||||||
.onCommand(GetAuction.class, (state, cmd) -> Effect().reply(cmd, state));
|
.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
|
// event from us, it will ignore the bidding finished event, so we need to update our state
|
||||||
// to reflect that.
|
// to reflect that.
|
||||||
|
|
||||||
private Effect<AuctionEvent, AuctionState> startAuction(AuctionState state, StartAuction cmd) {
|
private ReplyEffect<AuctionEvent, AuctionState> startAuction(
|
||||||
|
AuctionState state, StartAuction cmd) {
|
||||||
return Effect()
|
return Effect()
|
||||||
.persist(new AuctionStarted(entityUUID, cmd.getAuction()))
|
.persist(new AuctionStarted(entityUUID, cmd.getAuction()))
|
||||||
.thenReply(cmd, __ -> Done.getInstance());
|
.thenReply(cmd, __ -> Done.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Effect<AuctionEvent, AuctionState> finishBidding(AuctionState state, FinishBidding cmd) {
|
private ReplyEffect<AuctionEvent, AuctionState> finishBidding(
|
||||||
|
AuctionState state, FinishBidding cmd) {
|
||||||
return Effect()
|
return Effect()
|
||||||
.persist(new BiddingFinished(entityUUID))
|
.persist(new BiddingFinished(entityUUID))
|
||||||
.thenReply(cmd, __ -> Done.getInstance());
|
.thenReply(cmd, __ -> Done.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Effect<AuctionEvent, AuctionState> cancelAuction(AuctionState state, CancelAuction cmd) {
|
private ReplyEffect<AuctionEvent, AuctionState> cancelAuction(
|
||||||
|
AuctionState state, CancelAuction cmd) {
|
||||||
return Effect()
|
return Effect()
|
||||||
.persist(new AuctionCancelled(entityUUID))
|
.persist(new AuctionCancelled(entityUUID))
|
||||||
.thenReply(cmd, __ -> Done.getInstance());
|
.thenReply(cmd, __ -> Done.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The main logic for handling of bids. */
|
/** 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();
|
Auction auction = state.getAuction().get();
|
||||||
|
|
||||||
Instant now = Instant.now();
|
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
|
* <p>This emits two events, one for the bid currently being replace, and another automatic bid
|
||||||
* for the current bidder.
|
* for the current bidder.
|
||||||
*/
|
*/
|
||||||
private Effect<AuctionEvent, AuctionState> handleAutomaticOutbid(
|
private ReplyEffect<AuctionEvent, AuctionState> handleAutomaticOutbid(
|
||||||
PlaceBid bid,
|
PlaceBid bid,
|
||||||
Auction auction,
|
Auction auction,
|
||||||
Instant now,
|
Instant now,
|
||||||
|
|
@ -215,7 +223,7 @@ public class AuctionEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handle the situation where a bid will be accepted as the new winning bidder. */
|
/** 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) {
|
PlaceBid bid, Auction auction, Instant now, int currentBidMaximum) {
|
||||||
int nextIncrement = Math.min(currentBidMaximum + auction.getIncrement(), bid.getBidPrice());
|
int nextIncrement = Math.min(currentBidMaximum + auction.getIncrement(), bid.getBidPrice());
|
||||||
int newBidPrice;
|
int newBidPrice;
|
||||||
|
|
@ -247,7 +255,7 @@ public class AuctionEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandHandler<AuctionCommand, AuctionEvent, AuctionState> commandHandler() {
|
public CommandHandlerWithReply<AuctionCommand, AuctionEvent, AuctionState> commandHandler() {
|
||||||
return notStartedHandler
|
return notStartedHandler
|
||||||
.orElse(underAuctionHandler)
|
.orElse(underAuctionHandler)
|
||||||
.orElse(completedHandler)
|
.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());
|
return Effect().reply(cmd, Done.getInstance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue