Command handler builder for EventSourcedBehaviorWithEnforcedReplies, #25482 (#26272)

* It uses ReplyEffect instead of Effect
* Unfortunatley the solution is a copy of the CommandHandler builder with
  a few mechanical changes to the types and names.
* I tried to separate interface and implementation and use a shared implementation,
  but it didn't work out because methods with such function parameters which only differ in
  their type parameters can't be overloaded.
This commit is contained in:
Patrik Nordwall 2019-02-17 20:54:32 +01:00 committed by Christopher Batey
parent 1442a2428c
commit d3836aecfb
5 changed files with 456 additions and 25 deletions

View file

@ -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 }

View file

@ -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._

View file

@ -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]]
}
}
}
}
}

View file

@ -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")
}

View file

@ -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());
}
}