Make the AbstractBehavior builder mutable (#26309)

* Make the AbstractBehavior builder mutable #26260

* Use mutable builder style in first sample, mention that fluent is an option

* A bit of rework of the Java builders:

 * onAnyMessage added
 * use the japi SAMs throughout in the APIs
 * avoid wrapping the japi functions in Scala functions for the most common cases
 * more Java test coverage

* Not just any exception

* Works on 2.11 as well as 2.12
This commit is contained in:
Johan Andrén 2019-02-12 15:38:35 +01:00 committed by Patrik Nordwall
parent ddada9a8e1
commit 8fabb73f2b
22 changed files with 391 additions and 268 deletions

View file

@ -4,6 +4,9 @@
package akka.actor.typed.javadsl;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import org.junit.ClassRule;
import org.junit.Test;
import org.scalatest.junit.JUnitSuite;
@ -15,9 +18,13 @@ import java.util.ArrayList;
import static akka.actor.typed.javadsl.Behaviors.same;
import static akka.actor.typed.javadsl.Behaviors.stopped;
import static org.junit.Assert.assertEquals;
/** Test creating [[Behavior]]s using [[BehaviorBuilder]] */
public class BehaviorBuilderTest extends JUnitSuite {
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource();
interface Message {}
static final class One implements Message {
@ -28,7 +35,6 @@ public class BehaviorBuilderTest extends JUnitSuite {
static final class MyList<T> extends ArrayList<T> implements Message {};
@Test
public void shouldCompile() {
Behavior<Message> b =
Behaviors.receive(Message.class)
@ -54,6 +60,86 @@ public class BehaviorBuilderTest extends JUnitSuite {
.build();
}
@Test
public void caseSelectedInOrderAdded() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
BehaviorBuilder.create()
.onMessage(
String.class,
(context, msg) -> {
probe.ref().tell("handler 1: " + msg);
return Behaviors.same();
})
.onMessage(
String.class,
(context, msg) -> {
probe.ref().tell("handler 2: " + msg);
return Behaviors.same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("handler 1: message");
}
@Test
public void handleMessageBasedOnEquality() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
BehaviorBuilder.create()
.onMessageEquals(
"message",
(context) -> {
probe.ref().tell("got it");
return Behaviors.same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("got it");
}
@Test
public void applyPredicate() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
BehaviorBuilder.create()
.onMessage(
String.class,
(msg) -> "other".equals(msg),
(context, msg) -> {
probe.ref().tell("handler 1: " + msg);
return Behaviors.same();
})
.onMessage(
String.class,
(context, msg) -> {
probe.ref().tell("handler 2: " + msg);
return Behaviors.same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("handler 2: message");
}
@Test
public void catchAny() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
BehaviorBuilder.create()
.onAnyMessage(
(context, msg) -> {
probe.ref().tell(msg);
return same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("message");
}
interface CounterMessage {};
static final class Increase implements CounterMessage {};
@ -92,6 +178,12 @@ public class BehaviorBuilderTest extends JUnitSuite {
@Test
public void testImmutableCounter() {
Behavior<CounterMessage> immutable = immutableCounter(0);
ActorRef<CounterMessage> ref = testKit.spawn(immutableCounter(0));
TestProbe<Got> probe = testKit.createTestProbe();
ref.tell(new Get(probe.getRef()));
assertEquals(0, probe.expectMessageClass(Got.class).n);
ref.tell(new Increase());
ref.tell(new Get(probe.getRef()));
assertEquals(1, probe.expectMessageClass(Got.class).n);
}
}

View file

@ -4,16 +4,23 @@
package akka.actor.typed.javadsl;
import akka.actor.testkit.typed.javadsl.TestKitJunitResource;
import akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorRef;
import org.junit.ClassRule;
import org.junit.Test;
import org.scalatest.junit.JUnitSuite;
import akka.actor.typed.Behavior;
import static akka.actor.typed.javadsl.Behaviors.same;
import static org.junit.Assert.assertEquals;
/** Test creating [[MutableActor]]s using [[ReceiveBuilder]] */
public class ReceiveBuilderTest extends JUnitSuite {
@ClassRule public static final TestKitJunitResource testKit = new TestKitJunitResource();
@Test
public void testMutableCounter() {
Behavior<BehaviorBuilderTest.CounterMessage> mutable =
@ -36,7 +43,7 @@ public class ReceiveBuilderTest extends JUnitSuite {
@Override
public Receive<BehaviorBuilderTest.CounterMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(BehaviorBuilderTest.Increase.class, this::receiveIncrease)
.onMessage(BehaviorBuilderTest.Get.class, this::receiveGet)
.build();
@ -56,7 +63,7 @@ public class ReceiveBuilderTest extends JUnitSuite {
@Override
public Receive<BehaviorBuilderTest.CounterMessage> createReceive() {
assertEquals(42, value);
return receiveBuilder().build();
return newReceiveBuilder().build();
}
}
@ -65,4 +72,67 @@ public class ReceiveBuilderTest extends JUnitSuite {
MyAbstractBehavior mutable = new MyAbstractBehavior(42);
assertEquals(Behaviors.unhandled(), mutable.receive(null, new BehaviorBuilderTest.Increase()));
}
@Test
public void caseSelectedInOrderAdded() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
ReceiveBuilder.<Object>create()
.onMessage(
String.class,
msg -> {
probe.ref().tell("handler 1: " + msg);
return Behaviors.same();
})
.onMessage(
String.class,
msg -> {
probe.ref().tell("handler 2: " + msg);
return Behaviors.same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("handler 1: message");
}
@Test
public void applyPredicate() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
ReceiveBuilder.create()
.onMessage(
String.class,
msg -> "other".equals(msg),
msg -> {
probe.ref().tell("handler 1: " + msg);
return Behaviors.same();
})
.onMessage(
String.class,
msg -> {
probe.ref().tell("handler 2: " + msg);
return Behaviors.same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("handler 2: message");
}
@Test
public void catchAny() {
final TestProbe<Object> probe = testKit.createTestProbe();
Behavior<Object> behavior =
ReceiveBuilder.create()
.onAnyMessage(
msg -> {
probe.ref().tell(msg);
return same();
})
.build();
ActorRef<Object> ref = testKit.spawn(behavior);
ref.tell("message");
probe.expectMessage("message");
}
}

View file

@ -207,7 +207,7 @@ public class InteractionPatternsTest extends JUnitSuite {
@Override
public Receive<Command> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(
Translate.class,
cmd -> {
@ -603,7 +603,7 @@ public class InteractionPatternsTest extends JUnitSuite {
@Override
public Receive<Object> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(
Wallet.class,
(wallet) -> {

View file

@ -5,15 +5,11 @@
package jdocs.akka.typed;
// #imports
import akka.NotUsed;
import akka.actor.typed.ActorRef;
import akka.actor.typed.ActorSystem;
import akka.actor.typed.Behavior;
import akka.actor.typed.Terminated;
import akka.actor.typed.javadsl.AbstractBehavior;
import akka.actor.typed.javadsl.ActorContext;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.javadsl.Receive;
import akka.actor.typed.javadsl.*;
// #imports
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@ -112,29 +108,32 @@ public class OOIntroTest {
@Override
public Receive<RoomCommand> createReceive() {
return receiveBuilder()
.onMessage(
GetSession.class,
getSession -> {
ActorRef<SessionEvent> client = getSession.replyTo;
ActorRef<SessionCommand> ses =
context.spawn(
session(context.getSelf(), getSession.screenName, client),
URLEncoder.encode(getSession.screenName, StandardCharsets.UTF_8.name()));
// narrow to only expose PostMessage
client.tell(new SessionGranted(ses.narrow()));
sessions.add(ses);
return this;
})
.onMessage(
PublishSessionMessage.class,
pub -> {
NotifyClient notification =
new NotifyClient((new MessagePosted(pub.screenName, pub.message)));
sessions.forEach(s -> s.tell(notification));
return this;
})
.build();
ReceiveBuilder<RoomCommand> builder = newReceiveBuilder();
builder.onMessage(
GetSession.class,
getSession -> {
ActorRef<SessionEvent> client = getSession.replyTo;
ActorRef<SessionCommand> ses =
context.spawn(
session(context.getSelf(), getSession.screenName, client),
URLEncoder.encode(getSession.screenName, StandardCharsets.UTF_8.name()));
// narrow to only expose PostMessage
client.tell(new SessionGranted(ses.narrow()));
sessions.add(ses);
return this;
});
builder.onMessage(
PublishSessionMessage.class,
pub -> {
NotifyClient notification =
new NotifyClient((new MessagePosted(pub.screenName, pub.message)));
sessions.forEach(s -> s.tell(notification));
return this;
});
return builder.build();
}
}

View file

@ -42,13 +42,13 @@ abstract class AbstractBehavior[T] extends ExtensibleBehavior[T] {
/**
* Implement this to define how messages and signals are processed. Use the
* [[AbstractBehavior.receiveBuilder]] to define the message dispatch.
* [[AbstractBehavior.newReceiveBuilder]] to define the message dispatch.
*/
def createReceive: Receive[T]
/**
* Create a [[ReceiveBuilder]] to define the message dispatch of the `Behavior`.
* Create a new [[ReceiveBuilder]] to define the message dispatch of the `Behavior`.
* Typically used from [[AbstractBehavior.createReceive]].
*/
def receiveBuilder: ReceiveBuilder[T] = ReceiveBuilder.create
def newReceiveBuilder: ReceiveBuilder[T] = ReceiveBuilder.create
}

View file

@ -5,29 +5,34 @@
package akka.actor.typed.javadsl
import scala.annotation.tailrec
import akka.japi.function.{ Function, Function2, Predicate }
import akka.japi.function.{ Function JFunction }
import akka.japi.function.{ Function2 JFunction2 }
import akka.japi.function.{ Predicate JPredicate }
import akka.annotation.InternalApi
import akka.actor.typed
import akka.actor.typed.{ Behavior, ExtensibleBehavior, Signal }
import akka.actor.typed.Behavior
import akka.actor.typed.ExtensibleBehavior
import akka.actor.typed.Signal
import akka.actor.typed.TypedActorContext
import akka.actor.typed.Behavior.unhandled
import BehaviorBuilder._
import akka.util.OptionVal
/**
* Used for creating a [[Behavior]] by 'chaining' message and signal handlers.
* Immutable builder used for creating a [[Behavior]] by 'chaining' message and signal handlers.
*
* When handling a message or signal, this [[Behavior]] will consider all handlers in the order they were added,
* looking for the first handler for which both the type and the (optional) predicate match.
*
* @tparam T the common superclass of all supported messages.
*/
class BehaviorBuilder[T] private (
private val messageHandlers: List[Case[T, T]],
private val signalHandlers: List[Case[T, Signal]]
final class BehaviorBuilder[T] private (
messageHandlers: List[Case[T, T]],
signalHandlers: List[Case[T, Signal]]
) {
/**
* Build a Behavior from the current state of the builder
*/
def build(): Behavior[T] = new BuiltBehavior(messageHandlers.reverse, signalHandlers.reverse)
/**
@ -36,10 +41,10 @@ class BehaviorBuilder[T] private (
* @param type type of message to match
* @param handler action to apply if the type matches
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onMessage[M <: T](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withMessage(`type`, None, (i1: ActorContext[T], msg: T) handler.apply(i1, msg.asInstanceOf[M]))
def onMessage[M <: T](`type`: Class[M], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withMessage(OptionVal.Some(`type`), OptionVal.None, handler)
/**
* Add a new predicated case to the message handling.
@ -48,14 +53,13 @@ class BehaviorBuilder[T] private (
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onMessage[M <: T](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
def onMessage[M <: T](`type`: Class[M], test: JPredicate[M], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withMessage(
`type`,
Some((t: T) test.test(t.asInstanceOf[M])),
(i1: ActorContext[T], msg: T) handler.apply(i1, msg.asInstanceOf[M])
)
OptionVal.Some(`type`),
OptionVal.Some((t: T) test.test(t.asInstanceOf[M])),
handler)
/**
* Add a new case to the message handling without compile time type check.
@ -65,20 +69,36 @@ class BehaviorBuilder[T] private (
*
* @param type type of message to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withMessage(`type`, None, (i1: ActorContext[T], msg: T) handler.apply(i1, msg.asInstanceOf[M]))
def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withMessage[M](
OptionVal.Some(`type`.asInstanceOf[Class[M]]), OptionVal.None, handler)
/**
* Add a new case to the message handling matching equal messages.
*
* @param msg the message to compare to
* @param handler action to apply when the message matches
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onMessageEquals(msg: T, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] =
withMessage(msg.getClass, Some(_.equals(msg)), (ctx: ActorContext[T], _: T) handler.apply(ctx))
def onMessageEquals(msg: T, handler: JFunction[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] =
withMessage[T](
OptionVal.Some(msg.getClass.asInstanceOf[Class[T]]),
OptionVal.Some(_.equals(msg)),
new JFunction2[ActorContext[T], T, Behavior[T]] {
override def apply(ctx: ActorContext[T], msg: T): Behavior[T] = handler.apply(ctx)
})
/**
* Add a new case to the message handling matching any message. Subsequent `onMessage` clauses will
* never see any messages.
*
* @param handler action to apply for any message
* @return a new behavior builder with the specified handling appended
*/
def onAnyMessage(handler: JFunction2[ActorContext[T], T, Behavior[T]]): BehaviorBuilder[T] =
withMessage(OptionVal.None, OptionVal.None, handler)
/**
* Add a new case to the signal handling.
@ -86,10 +106,13 @@ class BehaviorBuilder[T] private (
* @param type type of signal to match
* @param handler action to apply if the type matches
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onSignal[M <: Signal](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withSignal(`type`, None, (ctx: ActorContext[T], signal: Signal) handler.apply(ctx, signal.asInstanceOf[M]))
def onSignal[M <: Signal](`type`: Class[M], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withSignal(
`type`,
OptionVal.None,
handler.asInstanceOf[JFunction2[ActorContext[T], Signal, Behavior[T]]])
/**
* Add a new predicated case to the signal handling.
@ -98,168 +121,87 @@ class BehaviorBuilder[T] private (
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onSignal[M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
def onSignal[M <: Signal](`type`: Class[M], test: JPredicate[M], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withSignal(
`type`,
Some((t: Signal) test.test(t.asInstanceOf[M])),
(ctx: ActorContext[T], signal: Signal) handler.apply(ctx, signal.asInstanceOf[M])
OptionVal.Some((t: Signal) test.test(t.asInstanceOf[M])),
handler.asInstanceOf[JFunction2[ActorContext[T], Signal, Behavior[T]]]
)
/**
* Add a new case to the signal handling without compile time type check.
*
* Should normally not be used, but when matching on class with generic type
* argument it can be useful, e.g. <code>GenMsg.class</code> and <code>(ActorContext<Message> ctx, GenMsg&lt;String&gt; list) -> {...}</code>
*
* @param type type of signal to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
*/
def onSignalUnchecked[M <: Signal](`type`: Class[_ <: Signal], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
withSignal(`type`, None, (ctx: ActorContext[T], signal: Signal) handler.apply(ctx, signal.asInstanceOf[M]))
/**
* Add a new case to the signal handling matching equal signals.
*
* @param signal the signal to compare to
* @param handler action to apply when the message matches
* @return a new behavior with the specified handling appended
* @return a new behavior builder with the specified handling appended
*/
def onSignalEquals(signal: Signal, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] =
withSignal(signal.getClass, Some(_.equals(signal)), (ctx: ActorContext[T], _: Signal) handler.apply(ctx))
withSignal(
signal.getClass,
OptionVal.Some(_.equals(signal)),
new JFunction2[ActorContext[T], Signal, Behavior[T]] {
override def apply(ctx: ActorContext[T], signal: Signal): Behavior[T] = {
handler.apply(ctx)
}
})
private def withMessage(`type`: Class[_ <: T], test: Option[T Boolean], handler: (ActorContext[T], T) Behavior[T]): BehaviorBuilder[T] =
new BehaviorBuilder[T](Case[T, T](`type`, test, handler) +: messageHandlers, signalHandlers)
private def withMessage[M <: T](clazz: OptionVal[Class[M]], test: OptionVal[M Boolean], handler: JFunction2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] = {
val newCase = Case(
clazz,
test,
handler
)
new BehaviorBuilder[T](newCase.asInstanceOf[Case[T, T]] +: messageHandlers, signalHandlers)
}
private def withSignal[M <: Signal](`type`: Class[M], test: Option[Signal Boolean], handler: (ActorContext[T], Signal) Behavior[T]): BehaviorBuilder[T] =
new BehaviorBuilder[T](messageHandlers, Case[T, Signal](`type`, test, handler) +: signalHandlers)
private def withSignal[M <: Signal](`type`: Class[M], test: OptionVal[Signal Boolean], handler: JFunction2[ActorContext[T], Signal, Behavior[T]]): BehaviorBuilder[T] = {
new BehaviorBuilder[T](
messageHandlers,
Case(OptionVal.Some(`type`), test, handler).asInstanceOf[Case[T, Signal]] +: signalHandlers
)
}
}
object BehaviorBuilder {
def create[T]: BehaviorBuilder[T] = new BehaviorBuilder[T](Nil, Nil)
private val _empty = new BehaviorBuilder[Nothing](Nil, Nil)
// used for both matching signals and messages so we throw away types after they are enforced by the builder API above
/** INTERNAL API */
@InternalApi
private[javadsl] final case class Case[BT, MT](`type`: Class[_ <: MT], test: Option[MT Boolean], handler: (ActorContext[BT], MT) Behavior[BT])
private[javadsl] final case class Case[BT, MT](`type`: OptionVal[Class[_ <: MT]], test: OptionVal[MT Boolean], handler: JFunction2[ActorContext[BT], MT, Behavior[BT]])
/**
* Start a new behavior chain starting with this case.
*
* @param type type of message to match
* @param handler action to apply if the type matches
* @tparam T type of behavior to create
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
* @return new empty immutable behavior builder.
*/
def message[T, M <: T](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onMessage(`type`, handler)
/**
* Start a new behavior chain starting with this predicated case.
*
* @param type type of message to match
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam T type of behavior to create
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
*/
def message[T, M <: T](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onMessage(`type`, test, handler)
/**
* Start a new behavior chain starting with a handler without compile time type check.
*
* Should normally not be used, but when matching on class with generic type
* argument it can be useful, e.g. <code>List.class</code> and <code>(List&lt;String&gt; list) -> {...}</code>
*
* @param type type of message to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
*/
def messageUnchecked[T, M <: T](`type`: Class[_ <: T], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onMessageUnchecked(`type`, handler)
/**
* Start a new behavior chain starting with a handler for equal messages.
*
* @param msg the message to compare to
* @param handler action to apply when the message matches
* @tparam T type of behavior to create
* @return a new behavior with the specified handling appended
*/
def messageEquals[T](msg: T, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onMessageEquals(msg, handler)
/**
* Start a new behavior chain starting with this signal case.
*
* @param type type of signal to match
* @param handler action to apply if the type matches
* @tparam T type of behavior to create
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
*/
def signal[T, M <: Signal](`type`: Class[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onSignal(`type`, handler)
/**
* Start a new behavior chain starting with this predicated signal case.
*
* @param type type of signals to match
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam T type of behavior to create
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
*/
def signal[T, M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onSignal(`type`, test, handler)
/**
* Start a new behavior chain starting with this unchecked signal case.
*
* Should normally not be used, but when matching on class with generic type
* argument it can be useful, e.g. <code>GenMsg.class</code> and <code>(ActorContext<Message> ctx, GenMsg&lt;String&gt; list) -> {...}</code>
*
* @param type type of signal to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
*/
def signalUnchecked[T, M <: Signal](`type`: Class[_ <: Signal], handler: Function2[ActorContext[T], M, Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onSignalUnchecked(`type`, handler)
/**
* Start a new behavior chain starting with a handler for this specific signal.
*
* @param signal the signal to compare to
* @param handler action to apply when the message matches
* @tparam T type of behavior to create
* @return a new behavior with the specified handling appended
*/
def signalEquals[T](signal: Signal, handler: Function[ActorContext[T], Behavior[T]]): BehaviorBuilder[T] =
BehaviorBuilder.create[T].onSignalEquals(signal, handler)
def create[T]: BehaviorBuilder[T] = _empty.asInstanceOf[BehaviorBuilder[T]]
}
private class BuiltBehavior[T](
private val messageHandlers: List[Case[T, T]],
private val signalHandlers: List[Case[T, Signal]]
/**
* The concrete behavior
*
* INTERNAL API
*/
@InternalApi
private final class BuiltBehavior[T](
messageHandlers: List[Case[T, T]],
signalHandlers: List[Case[T, Signal]]
) extends ExtensibleBehavior[T] {
override def receive(ctx: typed.TypedActorContext[T], msg: T): Behavior[T] = receive[T](ctx.asJava, msg, messageHandlers)
override def receive(ctx: TypedActorContext[T], msg: T): Behavior[T] = receive(ctx.asJava, msg, messageHandlers)
override def receiveSignal(ctx: typed.TypedActorContext[T], msg: Signal): Behavior[T] = receive[Signal](ctx.asJava, msg, signalHandlers)
override def receiveSignal(ctx: TypedActorContext[T], msg: Signal): Behavior[T] = receive(ctx.asJava, msg, signalHandlers)
@tailrec
private def receive[M](ctx: ActorContext[T], msg: M, handlers: List[Case[T, M]]): Behavior[T] =
handlers match {
case Case(cls, predicate, handler) :: tail
if (cls.isAssignableFrom(msg.getClass) && (predicate.isEmpty || predicate.get.apply(msg))) handler(ctx, msg)
else receive[M](ctx, msg, tail)
case _
if ((cls.isEmpty || cls.get.isAssignableFrom(msg.getClass)) && (predicate.isEmpty || predicate.get.apply(msg)))
handler(ctx, msg)
else receive(ctx, msg, tail)
case Nil
unhandled[T]
}

View file

@ -5,26 +5,29 @@
package akka.actor.typed.javadsl
import scala.annotation.tailrec
import akka.japi.function.{ Creator, Function, Predicate }
import akka.japi.function.Creator
import akka.japi.function.{ Function JFunction }
import akka.japi.function.{ Predicate JPredicate }
import akka.actor.typed.{ Behavior, Signal }
import akka.annotation.InternalApi
import akka.util.OptionVal
/**
* Used when implementing [[AbstractBehavior]].
* Mutable builder used when implementing [[AbstractBehavior]].
*
* When handling a message or signal, this [[Behavior]] will consider all handlers in the order they were added,
* looking for the first handler for which both the type and the (optional) predicate match.
*
* @tparam T the common superclass of all supported messages.
*/
class ReceiveBuilder[T] private (
private val messageHandlers: List[ReceiveBuilder.Case[T, T]],
private val signalHandlers: List[ReceiveBuilder.Case[T, Signal]]
final class ReceiveBuilder[T] private (
private var messageHandlers: List[ReceiveBuilder.Case[T, T]],
private var signalHandlers: List[ReceiveBuilder.Case[T, Signal]]
) {
import ReceiveBuilder.Case
def build(): Receive[T] = new BuiltReceive(messageHandlers.reverse, signalHandlers.reverse)
def build(): Receive[T] = new BuiltReceive[T](messageHandlers.reverse, signalHandlers.reverse)
/**
* Add a new case to the message handling.
@ -32,10 +35,10 @@ class ReceiveBuilder[T] private (
* @param type type of message to match
* @param handler action to apply if the type matches
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onMessage[M <: T](`type`: Class[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage(`type`, None, msg handler.apply(msg.asInstanceOf[M]))
def onMessage[M <: T](`type`: Class[M], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage(OptionVal.Some(`type`), OptionVal.None, handler)
/**
* Add a new predicated case to the message handling.
@ -44,14 +47,10 @@ class ReceiveBuilder[T] private (
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam M type of message to match
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onMessage[M <: T](`type`: Class[M], test: Predicate[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage(
`type`,
Some((t: T) test.test(t.asInstanceOf[M])),
msg handler.apply(msg.asInstanceOf[M])
)
def onMessage[M <: T](`type`: Class[M], test: JPredicate[M], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage(OptionVal.Some(`type`), OptionVal.Some(test), handler)
/**
* Add a new case to the message handling without compile time type check.
@ -61,20 +60,35 @@ class ReceiveBuilder[T] private (
*
* @param type type of message to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage(`type`, None, msg handler.apply(msg.asInstanceOf[M]))
def onMessageUnchecked[M <: T](`type`: Class[_ <: T], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] =
withMessage[M](OptionVal.Some(`type`.asInstanceOf[Class[M]]), OptionVal.None, handler)
/**
* Add a new case to the message handling matching equal messages.
*
* @param msg the message to compare to
* @param handler action to apply when the message matches
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onMessageEquals(msg: T, handler: Creator[Behavior[T]]): ReceiveBuilder[T] =
withMessage(msg.getClass, Some(_.equals(msg)), _ handler.create())
withMessage(OptionVal.Some(msg.getClass), OptionVal.Some(new JPredicate[T] {
override def test(param: T): Boolean = param == (msg)
}), new JFunction[T, Behavior[T]] {
// invoke creator without the message
override def apply(param: T): Behavior[T] = handler.create()
})
/**
* Add a new case to the message handling matching any message. Subsequent `onMessage` clauses will
* never see any messages.
*
* @param handler action to apply for any message
* @return this behavior builder
*/
def onAnyMessage(handler: JFunction[T, Behavior[T]]): ReceiveBuilder[T] =
withMessage(OptionVal.None, OptionVal.None, handler)
/**
* Add a new case to the signal handling.
@ -82,10 +96,10 @@ class ReceiveBuilder[T] private (
* @param type type of signal to match
* @param handler action to apply if the type matches
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onSignal[M <: Signal](`type`: Class[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withSignal(`type`, None, signal handler.apply(signal.asInstanceOf[M]))
def onSignal[M <: Signal](`type`: Class[M], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] =
withSignal(`type`, OptionVal.None, handler)
/**
* Add a new predicated case to the signal handling.
@ -94,60 +108,55 @@ class ReceiveBuilder[T] private (
* @param test a predicate that will be evaluated on the argument if the type matches
* @param handler action to apply if the type matches and the predicate returns true
* @tparam M type of signal to match
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onSignal[M <: Signal](`type`: Class[M], test: Predicate[M], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withSignal(
`type`,
Some((t: Signal) test.test(t.asInstanceOf[M])),
signal handler.apply(signal.asInstanceOf[M])
)
/**
* Add a new case to the signal handling without compile time type check.
*
* Should normally not be used, but when matching on class with generic type
* argument it can be useful, e.g. <code>GenMsg.class</code> and <code>(ActorContext<Message> ctx, GenMsg&lt;String&gt; list) -> {...}</code>
*
* @param type type of signal to match
* @param handler action to apply when the type matches
* @return a new behavior with the specified handling appended
*/
def onSignalUnchecked[M <: Signal](`type`: Class[_ <: Signal], handler: Function[M, Behavior[T]]): ReceiveBuilder[T] =
withSignal(`type`, None, signal handler.apply(signal.asInstanceOf[M]))
def onSignal[M <: Signal](`type`: Class[M], test: JPredicate[M], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] =
withSignal(`type`, OptionVal.Some(test), handler)
/**
* Add a new case to the signal handling matching equal signals.
*
* @param signal the signal to compare to
* @param handler action to apply when the message matches
* @return a new behavior with the specified handling appended
* @return this behavior builder
*/
def onSignalEquals(signal: Signal, handler: Creator[Behavior[T]]): ReceiveBuilder[T] =
withSignal(signal.getClass, Some(_.equals(signal)), _ handler.create())
withSignal(signal.getClass, OptionVal.Some(new JPredicate[Signal] {
override def test(param: Signal): Boolean = param == signal
}), new JFunction[Signal, Behavior[T]] {
override def apply(param: Signal): Behavior[T] = handler.create()
})
private def withMessage(`type`: Class[_ <: T], test: Option[T Boolean], handler: T Behavior[T]): ReceiveBuilder[T] =
new ReceiveBuilder[T](Case[T, T](`type`, test, handler) +: messageHandlers, signalHandlers)
private def withMessage[M <: T](`type`: OptionVal[Class[M]], test: OptionVal[JPredicate[M]], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] = {
messageHandlers = Case[T, M](`type`, test, handler).asInstanceOf[Case[T, T]] +: messageHandlers
this
}
private def withSignal[M <: Signal](`type`: Class[M], test: Option[Signal Boolean], handler: Signal Behavior[T]): ReceiveBuilder[T] =
new ReceiveBuilder[T](messageHandlers, Case[T, Signal](`type`, test, handler) +: signalHandlers)
private def withSignal[M <: Signal](`type`: Class[M], test: OptionVal[JPredicate[M]], handler: JFunction[M, Behavior[T]]): ReceiveBuilder[T] = {
signalHandlers = Case[T, M](OptionVal.Some(`type`), test, handler).asInstanceOf[Case[T, Signal]] +: signalHandlers
this
}
}
object ReceiveBuilder {
/** Create a new mutable receive builder */
def create[T]: ReceiveBuilder[T] = new ReceiveBuilder[T](Nil, Nil)
/** INTERNAL API */
@InternalApi
private[javadsl] final case class Case[BT, MT](`type`: Class[_ <: MT], test: Option[MT Boolean], handler: MT Behavior[BT])
private[javadsl] final case class Case[BT, MT](`type`: OptionVal[Class[_ <: MT]], test: OptionVal[JPredicate[MT]], handler: JFunction[MT, Behavior[BT]])
}
/**
* Receive type for [[AbstractBehavior]]
*
* INTERNAL API
*/
@InternalApi
private final class BuiltReceive[T](
private val messageHandlers: List[ReceiveBuilder.Case[T, T]],
private val signalHandlers: List[ReceiveBuilder.Case[T, Signal]]
messageHandlers: List[ReceiveBuilder.Case[T, T]],
signalHandlers: List[ReceiveBuilder.Case[T, Signal]]
) extends Receive[T] {
import ReceiveBuilder.Case
@ -159,7 +168,7 @@ private final class BuiltReceive[T](
private def receive[M](msg: M, handlers: List[Case[T, M]]): Behavior[T] =
handlers match {
case Case(cls, predicate, handler) :: tail
if (cls.isAssignableFrom(msg.getClass) && (predicate.isEmpty || predicate.get.apply(msg))) handler(msg)
if ((cls.isEmpty || cls.get.isAssignableFrom(msg.getClass)) && (predicate.isEmpty || predicate.get.test(msg))) handler(msg)
else receive[M](msg, tail)
case _
Behaviors.unhandled

View file

@ -7,6 +7,7 @@ package akka.japi.function
/**
* A Function interface. Used to create first-class-functions is Java.
* `Serializable` is needed to be able to grab line number for Java 8 lambdas.
* Supports throwing `Exception` in the apply, which the `java.util.function.Function` counterpart does not.
*/
@SerialVersionUID(1L)
trait Function[-T, +R] extends java.io.Serializable {
@ -17,6 +18,7 @@ trait Function[-T, +R] extends java.io.Serializable {
/**
* A Function interface. Used to create 2-arg first-class-functions is Java.
* `Serializable` is needed to be able to grab line number for Java 8 lambdas.
* Supports throwing `Exception` in the apply, which the `java.util.function.BiFunction` counterpart does not.
*/
@SerialVersionUID(1L)
trait Function2[-T1, -T2, +R] extends java.io.Serializable {
@ -27,6 +29,7 @@ trait Function2[-T1, -T2, +R] extends java.io.Serializable {
/**
* A Procedure is like a Function, but it doesn't produce a return value.
* `Serializable` is needed to be able to grab line number for Java 8 lambdas.
* Supports throwing `Exception` in the apply, which the `java.util.function.Consumer` counterpart does not.
*/
@SerialVersionUID(1L)
trait Procedure[-T] extends java.io.Serializable {
@ -37,9 +40,11 @@ trait Procedure[-T] extends java.io.Serializable {
/**
* An executable piece of code that takes no parameters and doesn't return any value.
* `Serializable` is needed to be able to grab line number for Java 8 lambdas.
* Supports throwing `Exception` in the apply, which the `java.util.function.Effect` counterpart does not.
*/
@SerialVersionUID(1L)
trait Effect extends java.io.Serializable {
@throws(classOf[Exception])
def apply(): Unit
}
@ -47,6 +52,7 @@ trait Effect extends java.io.Serializable {
/**
* Java API: Defines a criteria and determines whether the parameter meets this criteria.
* `Serializable` is needed to be able to grab line number for Java 8 lambdas.
* Supports throwing `Exception` in the apply, which the `java.util.function.Predicate` counterpart does not.
*/
@SerialVersionUID(1L)
trait Predicate[-T] extends java.io.Serializable {
@ -55,6 +61,7 @@ trait Predicate[-T] extends java.io.Serializable {
/**
* A constructor/factory, takes no parameters but creates a new value of type T every call.
* Supports throwing `Exception` in the apply, which the `java.util.function.Creator` counterpart does not.
*/
@SerialVersionUID(1L)
trait Creator[+T] extends Serializable {

View file

@ -134,7 +134,7 @@ public class ReplicatorTest extends JUnitSuite {
@Override
public Receive<ClientCommand> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(Increment.class, this::onIncrement)
.onMessage(InternalUpdateResponse.class, msg -> Behaviors.same())
.onMessage(GetValue.class, this::onGetValue)

View file

@ -50,7 +50,7 @@ public class ReceptionistExampleTest extends JUnitSuite {
@Override
public Receive<Object> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(
Receptionist.Listing.class,
listing -> listing.isForKey(serviceKey),
@ -131,7 +131,7 @@ public class ReceptionistExampleTest extends JUnitSuite {
@Override
public Receive<Object> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(
Receptionist.Listing.class,
listing -> listing.isForKey(serviceKey),

View file

@ -141,7 +141,7 @@ The console output may look like this:
The next example is more realistic and demonstrates some important patterns:
* Using a sealed trait and case class/objects to represent multiple messages an actor can receive
* Using @scala[a sealed trait and case class/objects]@java[an interface and classes implementing that interface] to represent multiple messages an actor can receive
* Handle sessions by using child actors
* Handling state by changing behavior
* Using multiple typed actors to represent different parts of a protocol in a type safe way
@ -357,6 +357,10 @@ As the state is mutable, we never return a different behavior from the message l
the `AbstractBehavior` instance itself (`this`) as a behavior to use for processing the next message coming in.
We could also return `Behavior.same` to achieve the same.
@java[In this sample we make separate statements for creating the behavior builder, but it also returns the builder
itself from each step so a more fluent behavior definition style is also possible. What you should prefer depends on
how big the set of messages the actor accepts is.]
It is also possible to return a new different `AbstractBehavior`, for example to represent a different state in a
finite state machine (FSM), or use one of the functional behavior factories to combine the object oriented
with the functional style for different parts of the lifecycle of the same Actor behavior.

View file

@ -36,7 +36,7 @@ class PrintMyActorRefActor extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder().onMessageEquals("printit", this::printIt).build();
return newReceiveBuilder().onMessageEquals("printit", this::printIt).build();
}
private Behavior<String> printIt() {
@ -60,7 +60,7 @@ class StartStopActor1 extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessageEquals("stop", Behaviors::stopped)
.onSignal(PostStop.class, signal -> postStop())
.build();
@ -84,7 +84,7 @@ class StartStopActor2 extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder().onSignal(PostStop.class, signal -> postStop()).build();
return newReceiveBuilder().onSignal(PostStop.class, signal -> postStop()).build();
}
private Behavior<String> postStop() {
@ -113,7 +113,7 @@ class SupervisingActor extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder().onMessageEquals("failChild", this::failChild).build();
return newReceiveBuilder().onMessageEquals("failChild", this::failChild).build();
}
private Behavior<String> failChild() {
@ -134,7 +134,7 @@ class SupervisedActor extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessageEquals("fail", this::fail)
.onSignal(PreRestart.class, signal -> preRestart())
.onSignal(PostStop.class, signal -> postStop())
@ -174,7 +174,7 @@ class Main extends AbstractBehavior<String> {
@Override
public Receive<String> createReceive() {
return receiveBuilder().onMessageEquals("start", this::start).build();
return newReceiveBuilder().onMessageEquals("start", this::start).build();
}
private Behavior<String> start() {

View file

@ -28,7 +28,7 @@ public class IotSupervisor extends AbstractBehavior<Void> {
// No need to handle any messages
@Override
public Receive<Void> createReceive() {
return receiveBuilder().onSignal(PostStop.class, signal -> postStop()).build();
return newReceiveBuilder().onSignal(PostStop.class, signal -> postStop()).build();
}
private IotSupervisor postStop() {

View file

@ -46,7 +46,7 @@ public class Device extends AbstractBehavior<DeviceMessage> {
@Override
public Receive<DeviceMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RecordTemperature.class, this::recordTemperature)
.onMessage(ReadTemperature.class, this::readTemperature)
.onSignal(PostStop.class, signal -> postStop())

View file

@ -46,7 +46,7 @@ public class Device extends AbstractBehavior<DeviceMessage> {
@Override
public Receive<DeviceMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(ReadTemperature.class, this::readTemperature)
.onSignal(PostStop.class, signal -> postStop())
.build();

View file

@ -46,7 +46,7 @@ public class Device extends AbstractBehavior<DeviceMessage> {
@Override
public Receive<DeviceMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RecordTemperature.class, this::recordTemperature)
.onMessage(ReadTemperature.class, this::readTemperature)
.onMessage(Passivate.class, m -> Behaviors.stopped())

View file

@ -98,7 +98,7 @@ public class DeviceGroup extends AbstractBehavior<DeviceGroupMessage> {
@Override
public Receive<DeviceGroupMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
// #device-group-register
// #device-group-remove

View file

@ -74,7 +74,7 @@ public class DeviceManager extends AbstractBehavior<DeviceManagerMessage> {
}
public Receive<DeviceManagerMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
.onMessage(RequestDeviceList.class, this::onRequestDeviceList)
.onMessage(DeviceGroupTerminated.class, this::onTerminated)

View file

@ -37,7 +37,7 @@ public class Device extends AbstractBehavior<DeviceMessage> {
@Override
public Receive<DeviceMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RecordTemperature.class, this::recordTemperature)
.onMessage(ReadTemperature.class, this::readTemperature)
.onMessage(Passivate.class, m -> Behaviors.stopped())

View file

@ -113,7 +113,7 @@ public class DeviceGroup extends AbstractBehavior<DeviceGroupMessage> {
@Override
public Receive<DeviceGroupMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
// #query-added
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
.onMessage(RequestDeviceList.class, r -> r.groupId.equals(groupId), this::onDeviceList)

View file

@ -95,7 +95,7 @@ public class DeviceGroupQuery extends AbstractBehavior<DeviceGroupQueryMessage>
// #query-state
@Override
public Receive<DeviceGroupQueryMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(WrappedRespondTemperature.class, this::onRespondTemperature)
.onMessage(DeviceTerminated.class, this::onDeviceTerminated)
.onMessage(CollectionTimeout.class, this::onCollectionTimeout)

View file

@ -83,7 +83,7 @@ public class DeviceManager extends AbstractBehavior<DeviceManagerMessage> {
}
public Receive<DeviceManagerMessage> createReceive() {
return receiveBuilder()
return newReceiveBuilder()
.onMessage(RequestTrackDevice.class, this::onTrackDevice)
.onMessage(RequestDeviceList.class, this::onRequestDeviceList)
.onMessage(RequestAllTemperatures.class, this::onRequestAllTemperatures)