diff --git a/akka-actor-tests/src/test/java/akka/japi/MatchBuilderTest.java b/akka-actor-tests/src/test/java/akka/japi/MatchBuilderTest.java new file mode 100644 index 0000000000..adcacace73 --- /dev/null +++ b/akka-actor-tests/src/test/java/akka/japi/MatchBuilderTest.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi; + +import akka.japi.pf.FI; +import akka.japi.pf.Match; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.Test; +import scala.MatchError; + +import static org.junit.Assert.*; + +public class MatchBuilderTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void shouldPassBasicMatchTest() { + Match pf = Match.create(Match.match(Integer.class, new FI.Apply() { + @Override + public Double apply(Integer integer) { + return integer * 10.0; + } + }).match(Number.class, new FI.Apply() { + @Override + public Double apply(Number number) { + return number.doubleValue() * (-10.0); + } + })); + + assertTrue("An integer should be multiplied by 10", new Double(47110).equals(pf.match(new Integer(4711)))); + assertTrue("A double should be multiplied by -10", new Double(-47110).equals(pf.match(new Double(4711)))); + + exception.expect(MatchError.class); + assertFalse("A string should throw a MatchError", new Double(4711).equals(pf.match("4711"))); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java b/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java new file mode 100644 index 0000000000..cb2083a921 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/AbstractMatch.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.PartialFunction; + +/** + * Version of {@link scala.PartialFunction} that can be built during + * runtime from Java. + * + * @param the input type, that this PartialFunction will be applied to + * @param the return type, that the results of the application will have + */ +class AbstractMatch { + + protected final PartialFunction statements; + + AbstractMatch(PartialFunction statements) { + PartialFunction empty = CaseStatement.empty(); + if (statements == null) + this.statements = empty; + else + this.statements = statements.orElse(empty); + } + + /** + * Turn this {@link Match} into a {@link scala.PartialFunction}. + * + * @return a partial function representation ot his {@link Match} + */ + public PartialFunction asPF() { + return statements; + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java new file mode 100644 index 0000000000..e780178998 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/AbstractPFBuilder.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.PartialFunction; + +/** + * A builder for {@link scala.PartialFunction}. + * + * @param the input type, that this PartialFunction will be applied to + * @param the return type, that the results of the application will have + */ +abstract class AbstractPFBuilder { + + protected PartialFunction statements = null; + + protected void addStatement(PartialFunction statement) { + if (statements == null) + statements = statement; + else + statements = statements.orElse(statement); + } + + /** + * Build a {@link scala.PartialFunction} from this builder. + * After this call the builder will be reset. + * + * @return a PartialFunction for this builder. + */ + public PartialFunction build() { + PartialFunction empty = CaseStatement.empty(); + PartialFunction statements = this.statements; + + this.statements = null; + if (statements == null) + return empty; + else + return statements.orElse(empty); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/FI.java b/akka-actor/src/main/java/akka/japi/pf/FI.java new file mode 100644 index 0000000000..b52b51a3f3 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/FI.java @@ -0,0 +1,136 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +/** + * Class that encapsulates all the Functional Interfaces + * used for creating partial functions. + */ +public final class FI { + private FI() { + } + + /** + * Functional interface for an application. + * + * @param the input type, that this Apply will be applied to + * @param the return type, that the results of the application will have + */ + public static interface Apply { + /** + * The application to perform. + * + * @param i an instance that the application is performed on + * @return the result of the application + */ + public abstract R apply(I i); + } + + /** + * Functional interface for an application. + * + * @param the first input type, that this Apply will be applied to + * @param the second input type, that this Apply will be applied to + * @param the return type, that the results of the application will have + */ + public static interface Apply2 { + /** + * The application to perform. + * + * @param i1 an instance that the application is performed on + * @param i2 an instance that the application is performed on + * @return the result of the application + */ + public abstract R apply(I1 i1, I2 i2); + } + + /** + * Functional interface for a predicate. + * + * @param the type that the predicate will operate on. + */ + public static interface TypedPredicate { + /** + * The predicate to evaluate. + * + * @param t an instance that the predicate is evaluated on. + * @return the result of the predicate + */ + public abstract boolean defined(T t); + } + + /** + * Functional interface for an application. + * + * @param the input type, that this Apply will be applied to + */ + public static interface UnitApply { + /** + * The application to perform. + * + * @param i an instance that the application is performed on + */ + public abstract void apply(I i); + } + + /** + * Functional interface for an application. + * + * @param the first input type, that this Apply will be applied to + * @param the second input type, that this Apply will be applied to + */ + public static interface UnitApply2 { + /** + * The application to perform. + * + * @param i1 an instance that the application is performed on + * @param i2 an instance that the application is performed on + */ + public abstract void apply(I1 i1, I2 i2); + } + + /** + * Functional interface for an application. + * + * @param the first input type, that this Apply will be applied to + * @param the second input type, that this Apply will be applied to + * @param the third input type, that this Apply will be applied to + */ + public static interface UnitApply3 { + /** + * The application to perform. + * + * @param i1 an instance that the application is performed on + * @param i2 an instance that the application is performed on + * @param i3 an instance that the application is performed on + */ + public abstract void apply(I1 i1, I2 i2, I3 i3); + } + + /** + * Functional interface for an application. + */ + public static interface UnitApplyVoid { + /** + * The application to perform. + */ + public abstract void apply(); + } + + /** + * Package scoped functional interface for a predicate. Used internally to match against arbitrary types. + */ + static interface Predicate { + /** + * The predicate to evaluate. + * + * @param o an instance that the predicate is evaluated on. + * @return the result of the predicate + */ + public abstract boolean defined(Object o); + } + + +} diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java new file mode 100644 index 0000000000..0a571abe82 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/FSMStateFunctionBuilder.java @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import akka.actor.FSM; +import scala.PartialFunction; +import java.util.List; + +/** + * Builder used to create a partial function for {@link akka.actor.FSM#whenUnhandled}. + * + * @param the state type + * @param the data type + */ +public class FSMStateFunctionBuilder { + + private PFBuilder, FSM.State> builder = + new PFBuilder, FSM.State>(); + + /** + * Add a case statement that matches on an event and data type. + * + * @param eventType the event type to match on + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @param

the event type to match on + * @param the data type to match on + * @return the builder with the case statement added + */ + public FSMStateFunctionBuilder event(final Class

eventType, + final Class dataType, + final FI.Apply2> apply) { + builder.match(FSM.Event.class, + new FI.TypedPredicate() { + @Override + public boolean defined(FSM.Event e) { + return eventType.isInstance(e.event()) && dataType.isInstance(e.stateData()); + } + }, + new FI.Apply>() { + public FSM.State apply(FSM.Event e) { + @SuppressWarnings("unchecked") + P p = (P) e.event(); + @SuppressWarnings("unchecked") + Q q = (Q) e.stateData(); + return apply.apply(p, q); + } + } + ); + + return this; + } + + /** + * Add a case statement that matches on the data type and if any of the event types + * in the list match or any of the event instances in the list compares equal. + * + * @param eventMatches a list of types or instances to match against + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @param the data type to match on + * @return the builder with the case statement added + */ + public FSMStateFunctionBuilder event(final List eventMatches, + final Class dataType, + final FI.Apply> apply) { + builder.match(FSM.Event.class, + new FI.TypedPredicate() { + @Override + public boolean defined(FSM.Event e) { + if (!dataType.isInstance(e.stateData())) + return false; + + boolean emMatch = false; + Object event = e.event(); + for (Object em : eventMatches) { + if (em instanceof Class) { + Class emc = (Class) em; + emMatch = emc.isInstance(event); + } else { + emMatch = event.equals(em); + } + if (emMatch) + break; + } + return emMatch; + } + }, + new FI.Apply>() { + public FSM.State apply(FSM.Event e) { + @SuppressWarnings("unchecked") + Q q = (Q) e.stateData(); + return apply.apply(q); + } + } + ); + + return this; + } + + /** + * Add a case statement that matches on any type of event. + * + * @param apply an action to apply to the event and state data + * @return the builder with the case statement added + */ + public FSMStateFunctionBuilder anyEvent(final FI.Apply2> apply) { + builder.match(FSM.Event.class, + new FI.Apply>() { + public FSM.State apply(FSM.Event e) { + @SuppressWarnings("unchecked") + D d = (D) e.stateData(); + return apply.apply(e.event(), d); + } + }); + + return this; + } + + /** + * Build a {@link scala.PartialFunction} from this builder. + * After this call the builder will be reset. + * + * @return a PartialFunction for this builder. + */ + public PartialFunction, FSM.State> build() { + return builder.build(); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java new file mode 100644 index 0000000000..ffa77e26d2 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/FSMStopBuilder.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import akka.actor.FSM; +import scala.PartialFunction; +import scala.runtime.BoxedUnit; + +/** + * Builder used to create a partial function for {@link akka.actor.FSM#onTermination}. + * + * @param the state type + * @param the data type + */ +public class FSMStopBuilder { + + private UnitPFBuilder> builder = + new UnitPFBuilder>(); + + /** + * Add a case statement that matches on an {@link FSM.Reason}. + * + * @param reason the reason for the termination + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + public FSMStopBuilder stop(final FSM.Reason reason, + final FI.UnitApply2 apply) { + builder.match(FSM.StopEvent.class, + new FI.TypedPredicate() { + @Override + public boolean defined(FSM.StopEvent e) { + return reason.equals(e.reason()); + } + }, + new FI.UnitApply() { + public void apply(FSM.StopEvent e) { + @SuppressWarnings("unchecked") + S s = (S) e.currentState(); + @SuppressWarnings("unchecked") + D d = (D) e.stateData(); + apply.apply(s, d); + } + } + ); + + return this; + } + + /** + * Add a case statement that matches on a reason type. + * + * @param reasonType the reason type to match on + * @param apply an action to apply to the reason, event and state data if there is a match + * @param

the reason type to match on + * @return the builder with the case statement added + */ + public

FSMStopBuilder stop(final Class

reasonType, + final FI.UnitApply3 apply) { + return this.stop(reasonType, + new FI.TypedPredicate

() { + @Override + public boolean defined(P p) { + return true; + } + }, apply); + } + + /** + * Add a case statement that matches on a reason type and a predicate. + * + * @param reasonType the reason type to match on + * @param apply an action to apply to the reason, event and state data if there is a match + * @param predicate a predicate that will be evaluated on the reason if the type matches + * @param

the reason type to match on + * @return the builder with the case statement added + */ + public

FSMStopBuilder stop(final Class

reasonType, + final FI.TypedPredicate

predicate, + final FI.UnitApply3 apply) { + builder.match(FSM.StopEvent.class, + new FI.TypedPredicate() { + @Override + public boolean defined(FSM.StopEvent e) { + if (reasonType.isInstance(e.reason())) { + @SuppressWarnings("unchecked") + P p = (P) e.reason(); + return predicate.defined(p); + } else { + return false; + } + } + }, + new FI.UnitApply() { + public void apply(FSM.StopEvent e) { + @SuppressWarnings("unchecked") + P p = (P) e.reason(); + @SuppressWarnings("unchecked") + S s = (S) e.currentState(); + @SuppressWarnings("unchecked") + D d = (D) e.stateData(); + apply.apply(p, s, d); + } + } + ); + + return this; + } + + /** + * Build a {@link scala.PartialFunction} from this builder. + * After this call the builder will be reset. + * + * @return a PartialFunction for this builder. + */ + public PartialFunction, BoxedUnit> build() { + return builder.build(); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java b/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java new file mode 100644 index 0000000000..8bc377e8ad --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/FSMTransitionHandlerBuilder.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.PartialFunction; +import scala.runtime.BoxedUnit; +import scala.Tuple2; + +/** + * Builder used to create a partial function for {@link akka.actor.FSM#onTransition}. + * + * @param the state type + */ +public class FSMTransitionHandlerBuilder { + + private UnitPFBuilder> builder = + new UnitPFBuilder>(); + + /** + * Add a case statement that matches on a from state and a to state. + * + * @param fromState the from state to match on + * @param toState the to state to match on + * @param apply an action to apply when the states match + * @return the builder with the case statement added + */ + public FSMTransitionHandlerBuilder state(final S fromState, + final S toState, + final FI.UnitApplyVoid apply) { + builder.match(Tuple2.class, + new FI.TypedPredicate() { + @Override + public boolean defined(Tuple2 t) { + return fromState.equals(t._1()) && toState.equals(t._2()); + } + }, + new FI.UnitApply() { + @Override + public void apply(Tuple2 t) { + apply.apply(); + } + } + ); + return this; + } + + /** + * Build a {@link scala.PartialFunction} from this builder. + * After this call the builder will be reset. + * + * @return a PartialFunction for this builder. + */ + public PartialFunction, BoxedUnit> build() { + return builder.build(); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/Match.java b/akka-actor/src/main/java/akka/japi/pf/Match.java new file mode 100644 index 0000000000..3bd8838285 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/Match.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.MatchError; +import scala.PartialFunction; + +/** + * Version of {@link scala.PartialFunction} that can be built during + * runtime from Java. + * + * @param the input type, that this PartialFunction will be applied to + * @param the return type, that the results of the application will have + */ +public class Match extends AbstractMatch { + + /** + * Convenience function to create a {@link PFBuilder} with the first + * case statement added. + * + * @param type a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + * @see PFBuilder#match(Class, FI.Apply) + */ + public static final PFBuilder match(final Class

type, + final FI.Apply apply) { + return new PFBuilder().match(type, apply); + } + + /** + * Convenience function to create a {@link PFBuilder} with the first + * case statement added. + * + * @param type a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + * @see PFBuilder#match(Class, FI.TypedPredicate, FI.Apply) + */ + public static PFBuilder match(final Class

type, + final FI.TypedPredicate

predicate, + final FI.Apply apply) { + return new PFBuilder().match(type, predicate, apply); + } + + /** + * Create a {@link Match} from the builder. + * + * @param builder a builder representing the partial function + * @return a {@link Match} that can be reused + */ + public static final Match create(PFBuilder builder) { + return new Match(builder.build()); + } + + Match(PartialFunction statements) { + super(statements); + } + + /** + * Convenience function to make the Java code more readable. + * + *


+   *   Matcher<X, Y> matcher = Matcher.create(...);
+   *
+   *   Y someY = matcher.match(obj);
+   * 
+ * + * @param i the argument to apply the match to + * @return the result of the application + * @throws MatchError if there is no match + */ + public R match(I i) throws MatchError { + return statements.apply(i); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java new file mode 100644 index 0000000000..60b72a92d6 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/PFBuilder.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +/** + * A builder for {@link scala.PartialFunction}. + * + * @param the input type, that this PartialFunction will be applied to + * @param the return type, that the results of the application will have + */ +public final class PFBuilder extends AbstractPFBuilder { + + /** + * Create a PFBuilder. + */ + public PFBuilder() { + } + + /** + * Add a new case statement to this builder. + * + * @param type a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + */ + public

PFBuilder match(final Class

type, FI.Apply apply) { + addStatement(new CaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + return type.isInstance(o); + } + }, apply)); + return this; + } + + /** + * Add a new case statement to this builder. + * + * @param type a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type matches and the predicate returns true + * @return a builder with the case statement added + */ + public

PFBuilder match(final Class

type, + final FI.TypedPredicate

predicate, + final FI.Apply apply) { + addStatement(new CaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + if (!type.isInstance(o)) + return false; + else { + @SuppressWarnings("unchecked") + P p = (P) o; + return predicate.defined(p); + } + } + }, apply)); + return this; + } + + /** + * Add a new case statement to this builder, that matches any argument. + * @param apply an action to apply to the argument + * @return a builder with the case statement added + */ + public PFBuilder matchAny(final FI.Apply apply) { + addStatement(new CaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + return true; + } + }, apply)); + return this; + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java b/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java new file mode 100644 index 0000000000..4dbdad747b --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/ReceiveBuilder.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +/** + * Used for building a partial function for {@link akka.actor.Actor#receive() Actor.receive()}. + * + * There is both a match on type only, and a match on type and predicate. + * + * Inside an actor you can use it like this with Java 8 to define your receive method: + *

+ * @Override
+ * public PartialFunction receive() {
+ *   return ReceiveBuilder.
+ *     match(Double.class, d -> {
+ *       sender().tell(d.isNaN() ? 0 : d, self());
+ *     }).
+ *     match(Integer.class, i -> {
+ *       sender().tell(i * 10, self());
+ *     }).
+ *     match(String.class, s -> s.startsWith("foo"), s -> {
+ *       sender().tell(s.toUpperCase(), self());
+ *     }).build();
+ * }
+ * 
+ * + */ +public class ReceiveBuilder { + private ReceiveBuilder() { + } + + /** + * Return a new {@link UnitPFBuilder} with a case statement added. + * + * @param type a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + */ + public static

UnitPFBuilder match(final Class

type, FI.UnitApply

apply) { + return UnitMatch.match(type, apply); + } + + /** + * Return a new {@link UnitPFBuilder} with a case statement added. + * + * @param type a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type matches and the predicate returns true + * @return a builder with the case statement added + */ + public static

UnitPFBuilder match(final Class

type, + FI.TypedPredicate

predicate, + FI.UnitApply

apply) { + return UnitMatch.match(type, predicate, apply); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java b/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java new file mode 100644 index 0000000000..8afb742a83 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/UnitMatch.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.MatchError; +import scala.PartialFunction; +import scala.runtime.BoxedUnit; + +/** + * Version of {@link scala.PartialFunction} that can be built during + * runtime from Java. + * This is a specialized version of {@link UnitMatch} to map java + * void methods to {@link scala.runtime.BoxedUnit}. + * + * @param the input type, that this PartialFunction will be applied to + */ +public class UnitMatch extends AbstractMatch { + + /** + * Convenience function to create a {@link UnitPFBuilder} with the first + * case statement added. + * + * @param type a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + * @see UnitPFBuilder#match(Class, FI.UnitApply) + */ + public static final UnitPFBuilder match(final Class

type, FI.UnitApply

apply) { + return new UnitPFBuilder().match(type, apply); + } + + /** + * Convenience function to create a {@link UnitPFBuilder} with the first + * case statement added. + * + * @param type a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type and predicate matches + * @return a builder with the case statement added + * @see UnitPFBuilder#match(Class, FI.TypedPredicate, FI.UnitApply) + */ + public static UnitPFBuilder match(final Class

type, + final FI.TypedPredicate

predicate, + final FI.UnitApply

apply) { + return new UnitPFBuilder().match(type, predicate, apply); + } + + /** + * Create a {@link UnitMatch} from the builder. + * + * @param builder a builder representing the partial function + * @return a {@link UnitMatch} that can be reused + */ + public static UnitMatch create(UnitPFBuilder builder) { + return new UnitMatch(builder.build()); + } + + private UnitMatch(PartialFunction statements) { + super(statements); + } + + /** + * Convenience function to make the Java code more readable. + * + *


+   *   UnitMatcher<X> matcher = UnitMatcher.create(...);
+   *
+   *   matcher.match(obj);
+   * 
+ * + * @param i the argument to apply the match to + * @throws scala.MatchError if there is no match + */ + public void match(I i) throws MatchError { + statements.apply(i); + } +} diff --git a/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java b/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java new file mode 100644 index 0000000000..c5b20a4aa6 --- /dev/null +++ b/akka-actor/src/main/java/akka/japi/pf/UnitPFBuilder.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf; + +import scala.runtime.BoxedUnit; + +/** + * A builder for {@link scala.PartialFunction}. + * This is a specialized version of {@link PFBuilder} to map java + * void methods to {@link scala.runtime.BoxedUnit}. + * + * @param the input type, that this PartialFunction to be applied to + */ +public final class UnitPFBuilder extends AbstractPFBuilder { + + /** + * Create a UnitPFBuilder. + */ + public UnitPFBuilder() { + } + + /** + * Add a new case statement to this builder. + * + * @param type a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + */ + public

UnitPFBuilder match(final Class

type, + final FI.UnitApply

apply) { + addStatement(new UnitCaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + return type.isInstance(o); + } + }, apply)); + return this; + } + + /** + * Add a new case statement to this builder. + * + * @param type a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type matches and the predicate returns true + * @return a builder with the case statement added + */ + public

UnitPFBuilder match(final Class

type, + final FI.TypedPredicate

predicate, + final FI.UnitApply

apply) { + addStatement(new UnitCaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + if (!type.isInstance(o)) + return false; + else { + @SuppressWarnings("unchecked") + P p = (P) o; + return predicate.defined(p); + } + } + }, apply)); + return this; + } + + /** + * Add a new case statement to this builder, that matches any argument. + * @param apply an action to apply to the argument + * @return a builder with the case statement added + */ + public UnitPFBuilder matchAny(final FI.UnitApply apply) { + addStatement(new UnitCaseStatement( + new FI.Predicate() { + @Override + public boolean defined(Object o) { + return true; + } + }, apply)); + return this; + } +} diff --git a/akka-actor/src/main/scala/akka/actor/Actor.scala b/akka-actor/src/main/scala/akka/actor/Actor.scala index 5634c49e3c..bc25f92f18 100644 --- a/akka-actor/src/main/scala/akka/actor/Actor.scala +++ b/akka-actor/src/main/scala/akka/actor/Actor.scala @@ -5,11 +5,9 @@ package akka.actor import akka.AkkaException -import scala.collection.immutable import scala.annotation.tailrec import scala.reflect.BeanProperty import scala.util.control.NoStackTrace -import java.util.regex.Pattern import akka.event.LoggingAdapter /** @@ -566,3 +564,10 @@ trait Actor { } } +/** + * Java API + * + * Abstract base class for Java Actors. + * + */ +abstract class AbstractActor extends akka.actor.Actor diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 0586e571bd..f202b7ab67 100644 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -4,7 +4,6 @@ package akka.actor import language.implicitConversions -import akka.util._ import scala.concurrent.duration.Duration import scala.collection.mutable import akka.routing.{ Deafen, Listen, Listeners } @@ -162,6 +161,17 @@ object FSM { copy(stopReason = Some(reason)) } } + /** + * All messages sent to the [[akka.actor.FSM]] will be wrapped inside an + * `Event`, which allows pattern matching to extract both state and data. + */ + case class Event[D](event: Any, stateData: D) extends NoSerializationVerificationNeeded + + /** + * Case class representing the state of the [[akka.actor.FSM]] whithin the + * `onTermination` block. + */ + case class StopEvent[S, D](reason: Reason, currentState: S, stateData: D) extends NoSerializationVerificationNeeded } @@ -250,10 +260,15 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging { import FSM._ type State = FSM.State[S, D] + type Event = FSM.Event[D] + type StopEvent = FSM.StopEvent[S, D] type StateFunction = scala.PartialFunction[Event, State] type Timeout = Option[FiniteDuration] type TransitionHandler = PartialFunction[(S, S), Unit] + val Event: FSM.Event.type = FSM.Event + val StopEvent: FSM.StopEvent.type = FSM.StopEvent + /* * “import” so that these are visible without an import */ @@ -666,18 +681,6 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging { case Failure(msg: AnyRef) ⇒ log.error(msg.toString) case _ ⇒ } - - /** - * All messages sent to the [[akka.actor.FSM]] will be wrapped inside an - * `Event`, which allows pattern matching to extract both state and data. - */ - case class Event(event: Any, stateData: D) extends NoSerializationVerificationNeeded - - /** - * Case class representing the state of the [[akka.actor.FSM]] whithin the - * `onTermination` block. - */ - case class StopEvent(reason: Reason, currentState: S, stateData: D) extends NoSerializationVerificationNeeded } /** @@ -750,3 +753,251 @@ trait LoggingFSM[S, D] extends FSM[S, D] { this: Actor ⇒ } +/** + * Java API + * + * Finite State Machine actor abstract base class. + */ +abstract class AbstractFSM[S, D] extends FSM[S, D] { + import akka.japi.pf._ + import akka.japi.pf.FI._ + import java.util.{ List ⇒ JList } + import FSM._ + + /** + * Insert a new StateFunction at the end of the processing chain for the + * given state. + * + * @param stateName designator for the state + * @param stateFunction partial function describing response to input + */ + final def when(stateName: S)(stateFunction: StateFunction): Unit = + when(stateName, null: FiniteDuration)(stateFunction) + + /** + * Insert a new StateFunction at the end of the processing chain for the + * given state. + * + * @param stateName designator for the state + * @param stateFunctionBuilder partial function builder describing response to input + */ + final def when(stateName: S, stateFunctionBuilder: FSMStateFunctionBuilder[S, D]): Unit = + when(stateName, null, stateFunctionBuilder) + + /** + * Insert a new StateFunction at the end of the processing chain for the + * given state. If the stateTimeout parameter is set, entering this state + * without a differing explicit timeout setting will trigger a StateTimeout + * event; the same is true when using #stay. + * + * @param stateName designator for the state + * @param stateTimeout default state timeout for this state + * @param stateFunctionBuilder partial function builder describing response to input + */ + final def when(stateName: S, + stateTimeout: FiniteDuration, + stateFunctionBuilder: FSMStateFunctionBuilder[S, D]): Unit = + when(stateName, stateTimeout)(stateFunctionBuilder.build()) + + /** + * Set initial state. Call this method from the constructor before the [[#initialize]] method. + * If different state is needed after a restart this method, followed by [[#initialize]], can + * be used in the actor life cycle hooks [[akka.actor.Actor#preStart]] and [[akka.actor.Actor#postRestart]]. + * + * @param stateName initial state designator + * @param stateData initial state data + */ + final def startWith(stateName: S, stateData: D): Unit = + startWith(stateName, stateData, null: FiniteDuration) + + /** + * Set initial state. Call this method from the constructor before the [[#initialize]] method. + * If different state is needed after a restart this method, followed by [[#initialize]], can + * be used in the actor life cycle hooks [[akka.actor.Actor#preStart]] and [[akka.actor.Actor#postRestart]]. + * + * @param stateName initial state designator + * @param stateData initial state data + * @param timeout state timeout for the initial state, overriding the default timeout for that state + */ + final def startWith(stateName: S, stateData: D, timeout: FiniteDuration): Unit = + startWith(stateName, stateData, Option(timeout)) + + /** + * Add a handler which is called upon each state transition, i.e. not when + * staying in the same state. + * + * Multiple handlers may be installed, and every one of them will be + * called, not only the first one matching. + */ + final def onTransition(transitionHandlerBuilder: FSMTransitionHandlerBuilder[S]): Unit = + onTransition(transitionHandlerBuilder.build().asInstanceOf[TransitionHandler]) + + /** + * Set handler which is called upon reception of unhandled messages. Calling + * this method again will overwrite the previous contents. + * + * The current state may be queried using ``stateName``. + */ + final def whenUnhandled(stateFunctionBuilder: FSMStateFunctionBuilder[S, D]): Unit = + whenUnhandled(stateFunctionBuilder.build()) + + /** + * Set handler which is called upon termination of this FSM actor. Calling + * this method again will overwrite the previous contents. + */ + final def onTermination(stopBuilder: FSMStopBuilder[S, D]): Unit = + onTermination(stopBuilder.build().asInstanceOf[PartialFunction[StopEvent, Unit]]) + + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches on an event and data type. + * + * @param eventType the event type to match on + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent[ET, DT <: D](eventType: Class[ET], dataType: Class[DT], apply: Apply2[ET, DT, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(eventType, dataType, apply) + + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches on the data type and if any of the event types + * in the list match or any of the event instances in the list compares equal. + * + * @param eventMatches a list of types or instances to match against + * @param dataType the data type to match on + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchEvent[DT <: D](eventMatches: JList[AnyRef], dataType: Class[DT], apply: Apply[DT, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().event(eventMatches, dataType, apply); + + /** + * Create an [[akka.japi.pf.FSMStateFunctionBuilder]] with the first case statement set. + * + * A case statement that matches on any type of event. + * + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchAnyEvent(apply: Apply2[AnyRef, D, State]): FSMStateFunctionBuilder[S, D] = + new FSMStateFunctionBuilder[S, D]().anyEvent(apply) + + /** + * Create an [[akka.japi.pf.FSMTransitionHandlerBuilder]] with the first case statement set. + * + * A case statement that matches on a from state and a to state. + * + * @param fromState the from state to match on + * @param toState the to state to match on + * @param apply an action to apply when the states match + * @return the builder with the case statement added + */ + final def matchState(fromState: S, toState: S, apply: UnitApplyVoid): FSMTransitionHandlerBuilder[S] = + new FSMTransitionHandlerBuilder[S]().state(fromState, toState, apply) + + /** + * Create an [[akka.japi.pf.FSMStopBuilder]] with the first case statement set. + * + * A case statement that matches on an [[FSM.Reason]]. + * + * @param reason the reason for the termination + * @param apply an action to apply to the event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchStop(reason: Reason, apply: UnitApply2[S, D]): FSMStopBuilder[S, D] = + new FSMStopBuilder[S, D]().stop(reason, apply) + + /** + * Create an [[akka.japi.pf.FSMStopBuilder]] with the first case statement set. + * + * A case statement that matches on a reason type. + * + * @param reasonType the reason type to match on + * @param apply an action to apply to the reason, event and state data if there is a match + * @return the builder with the case statement added + */ + final def matchStop[RT <: Reason](reasonType: Class[RT], apply: UnitApply3[RT, S, D]): FSMStopBuilder[S, D] = + new FSMStopBuilder[S, D]().stop(reasonType, apply) + + /** + * Create an [[akka.japi.pf.FSMStopBuilder]] with the first case statement set. + * + * A case statement that matches on a reason type and a predicate. + * + * @param reasonType the reason type to match on + * @param apply an action to apply to the reason, event and state data if there is a match + * @param predicate a predicate that will be evaluated on the reason if the type matches + * @return the builder with the case statement added + */ + final def matchStop[RT <: Reason](reasonType: Class[RT], predicate: TypedPredicate[RT], apply: UnitApply3[RT, S, D]): FSMStopBuilder[S, D] = + new FSMStopBuilder[S, D]().stop(reasonType, predicate, apply) + + /** + * Create a [[akka.japi.pf.UnitPFBuilder]] with the first case statement set. + * + * @param dataType a type to match the argument against + * @param apply an action to apply to the argument if the type matches + * @return a builder with the case statement added + */ + final def matchData[DT <: D](dataType: Class[DT], apply: UnitApply[DT]): UnitPFBuilder[D] = + UnitMatch.`match`(dataType, apply) + + /** + * Create a [[akka.japi.pf.UnitPFBuilder]] with the first case statement set. + * + * @param dataType a type to match the argument against + * @param predicate a predicate that will be evaluated on the argument if the type matches + * @param apply an action to apply to the argument if the type and predicate matches + * @return a builder with the case statement added + */ + final def matchData[DT <: D](dataType: Class[DT], predicate: TypedPredicate[DT], apply: UnitApply[DT]): UnitPFBuilder[D] = + UnitMatch.`match`(dataType, predicate, apply) + + /** + * Produce transition to other state. Return this from a state function in + * order to effect the transition. + * + * @param nextStateName state designator for the next state + * @return state transition descriptor + */ + final def goTo(nextStateName: S): State = goto(nextStateName) + + /** + * Default reason if calling `stop()`. + */ + val Normal: FSM.Reason = FSM.Normal + + /** + * Reason given when someone was calling `system.stop(fsm)` from outside; + * also applies to `Stop` supervision directive. + */ + val Shutdown: FSM.Reason = FSM.Shutdown + + /** + * Signifies that the [[akka.actor.FSM]] is shutting itself down because of + * an error, e.g. if the state to transition into does not exist. You can use + * this to match on a Failure in the [[akka.japi.pf.FSMStopBuilder]]. + */ + def Failure: Class[_ <: FSM.Reason] = classOf[FSM.Failure] + + /** + * A partial function value which does not match anything and can be used to + * “reset” `whenUnhandled` and `onTermination` handlers. + * + * {{{ + * onTermination(FSM.NullFunction) + * }}} + */ + val NullFunction: PartialFunction[Any, Nothing] = FSM.NullFunction +} + +/** + * Java API + * + * Finite State Machine actor abstract base class. + */ +abstract class AbstractLoggingFSM[S, D] extends AbstractFSM[S, D] with LoggingFSM[S, D] diff --git a/akka-actor/src/main/scala/akka/japi/pf/CaseStatements.scala b/akka-actor/src/main/scala/akka/japi/pf/CaseStatements.scala new file mode 100644 index 0000000000..bfc0ddbe70 --- /dev/null +++ b/akka-actor/src/main/scala/akka/japi/pf/CaseStatements.scala @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.japi.pf + +import FI.{ UnitApply, Apply, Predicate } + +private[pf] object CaseStatement { + def empty[F, T](): PartialFunction[F, T] = PartialFunction.empty +} + +private[pf] class CaseStatement[F, P, T](predicate: Predicate, apply: Apply[P, T]) + extends PartialFunction[F, T] { + + override def isDefinedAt(o: F) = predicate.defined(o) + + override def apply(o: F) = apply.apply(o.asInstanceOf[P]) +} + +private[pf] class UnitCaseStatement[F, P](predicate: Predicate, apply: UnitApply[P]) + extends PartialFunction[F, Unit] { + + override def isDefinedAt(o: F) = predicate.defined(o) + + override def apply(o: F) = apply.apply(o.asInstanceOf[P]) +} diff --git a/akka-docs/rst/java/index-actors.rst b/akka-docs/rst/java/index-actors.rst index 20b567295b..d343351eec 100644 --- a/akka-docs/rst/java/index-actors.rst +++ b/akka-docs/rst/java/index-actors.rst @@ -13,3 +13,5 @@ Actors fsm persistence testing + lambda-actors + lambda-fsm diff --git a/akka-docs/rst/java/lambda-actors.rst b/akka-docs/rst/java/lambda-actors.rst new file mode 100644 index 0000000000..841762581b --- /dev/null +++ b/akka-docs/rst/java/lambda-actors.rst @@ -0,0 +1,46 @@ +.. _lambda-actors-java: + +################################### + Actors (Java8 with Lambda Support) +################################### + + +Defining an Actor class +----------------------- + +Actor classes are implemented by extending the :class:AbstractActor class and implementing +the :meth:`receive` method. The :meth:`receive` method should define a series of match +statements (which has the type ``PartialFunction``) that defines +which messages your Actor can handle, along with the implementation of how the +messages should be processed. + +Don't let the type signature scare you. To allow you to easily build up a partial +function there is a builder named ``ReceiveBuilder`` that you can use. + +Here is an example: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/MyActor.java + :include: imports,my-actor + +Please note that the Akka Actor ``receive`` message loop is exhaustive, which +is different compared to Erlang and the late Scala Actors. This means that you +need to provide a pattern match for all messages that it can accept and if you +want to be able to handle unknown messages then you need to have a default case +as in the example above. Otherwise an ``akka.actor.UnhandledMessage(message, +sender, recipient)`` will be published to the ``ActorSystem``'s ``EventStream``. + +Note further that the return type of the behavior defined above is ``Unit``; if +the actor shall reply to the received message then this must be done explicitly +as explained below. + +The result of the :meth:`receive` method is a partial function object, which is +stored within the actor as its “initial behavior”. + +Here is s slightly bigger example: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/SampleActor.java + +TODO: +----- + +Lots of doc missing here diff --git a/akka-docs/rst/java/lambda-fsm.rst b/akka-docs/rst/java/lambda-fsm.rst new file mode 100644 index 0000000000..b7b9189ec6 --- /dev/null +++ b/akka-docs/rst/java/lambda-fsm.rst @@ -0,0 +1,399 @@ +.. _lambda-fsm-java: + +################################ +FSM (Java 8 with Lambda Support) +################################ + + +Overview +======== + +The FSM (Finite State Machine) is available as an abstract base class that implements +an akka Actor and is best described in the `Erlang design principles +`_ + +A FSM can be described as a set of relations of the form: + + **State(S) x Event(E) -> Actions (A), State(S')** + +These relations are interpreted as meaning: + + *If we are in state S and the event E occurs, we should perform the actions A + and make a transition to the state S'.* + +A Simple Example +================ + +To demonstrate most of the features of the :class:`AbstractFSM` class, consider an +actor which shall receive and queue messages while they arrive in a burst and +send them on after the burst ended or a flush request is received. + +First, consider all of the below to use these import statements: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java#simple-imports + +The contract of our “Buncher” actor is that it accepts or produces the following messages: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Events.java + :include: simple-events + :exclude: boilerplate + +``SetTarget`` is needed for starting it up, setting the destination for the +``Batches`` to be passed on; ``Queue`` will add to the internal queue while +``Flush`` will mark the end of a burst. + + +The actor can be in two states: no message queued (aka ``Idle``) or some +message queued (aka ``Active``). The states and the state data is defined like this: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: simple-state + :exclude: boilerplate + +The actor starts out in the idle state. Once a message arrives it will go to the +active state and stay there as long as messages keep arriving and no flush is +requested. The internal state data of the actor is made up of the target actor +reference to send the batches to and the actual queue of messages. + +Now let’s take a look at the skeleton for our FSM actor: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: simple-fsm + :exclude: transition-elided,unhandled-elided,termination-elided + +The basic strategy is to declare the actor, by inheriting the :class:`AbstractFSM` class +and specifying the possible states and data values as type parameters. Within +the body of the actor a DSL is used for declaring the state machine: + + * :meth:`startWith` defines the initial state and initial data + * then there is one :meth:`when() { ... }` declaration per state to be + handled (could potentially be multiple ones, the passed + :class:`PartialFunction` will be concatenated using :meth:`orElse`) + * finally starting it up using :meth:`initialize`, which performs the + transition into the initial state and sets up timers (if required). + +In this case, we start out in the ``Idle`` and ``Uninitialized`` state, where +only the ``SetTarget()`` message is handled; ``stay`` prepares to end this +event’s processing for not leaving the current state, while the ``using`` +modifier makes the FSM replace the internal state (which is ``Uninitialized`` +at this point) with a fresh ``Todo()`` object containing the target actor +reference. The ``Active`` state has a state timeout declared, which means that +if no message is received for 1 second, a ``FSM.StateTimeout`` message will be +generated. This has the same effect as receiving the ``Flush`` command in this +case, namely to transition back into the ``Idle`` state and resetting the +internal queue to the empty vector. But how do messages get queued? Since this +shall work identically in both states, we make use of the fact that any event +which is not handled by the ``when()`` block is passed to the +``whenUnhandled()`` block: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java#unhandled-elided + +The first case handled here is adding ``Queue()`` requests to the internal +queue and going to the ``Active`` state (this does the obvious thing of staying +in the ``Active`` state if already there), but only if the FSM data are not +``Uninitialized`` when the ``Queue()`` event is received. Otherwise—and in all +other non-handled cases—the second case just logs a warning and does not change +the internal state. + +The only missing piece is where the ``Batches`` are actually sent to the +target, for which we use the ``onTransition`` mechanism: you can declare +multiple such blocks and all of them will be tried for matching behavior in +case a state transition occurs (i.e. only when the state actually changes). + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java#transition-elided + +The transition callback is a partial function which takes as input a pair of +states—the current and the next state. During the state change, the old state +data is available via ``stateData`` as shown, and the new state data would be +available as ``nextStateData``. + +TODO +---- + +Add the test here + +Reference +========= + +The AbstractFSM Class +--------------------- + +The :class:`AbstractFSM` abstract class is the base class used to implement an FSM. It implements +Actor since an Actor is created to drive the FSM. + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: simple-fsm + :exclude: fsm-body + +.. note:: + + The AbstractFSM class defines a ``receive`` method which handles internal messages + and passes everything else through to the FSM logic (according to the + current state). When overriding the ``receive`` method, keep in mind that + e.g. state timeout handling depends on actually passing the messages through + the FSM logic. + +The :class:`AbstractFSM` class takes two type parameters: + + #. the supertype of all state names, usually an enum, + #. the type of the state data which are tracked by the :class:`AbstractFSM` module + itself. + +.. _fsm-philosophy: + +.. note:: + + The state data together with the state name describe the internal state of + the state machine; if you stick to this scheme and do not add mutable fields + to the FSM class you have the advantage of making all changes of the + internal state explicit in a few well-known places. + +Defining States +--------------- + +A state is defined by one or more invocations of the method + + :func:`when([, stateTimeout = ])(stateFunction)`. + +The given name must be an object which is type-compatible with the first type +parameter given to the :class:`AbstractFSM` class. This object is used as a hash key, +so you must ensure that it properly implements :meth:`equals` and +:meth:`hashCode`; in particular it must not be mutable. The easiest fit for +these requirements are case objects. + +If the :meth:`stateTimeout` parameter is given, then all transitions into this +state, including staying, receive this timeout by default. Initiating the +transition with an explicit timeout may be used to override this default, see +`Initiating Transitions`_ for more information. The state timeout of any state +may be changed during action processing with +:func:`setStateTimeout(state, duration)`. This enables runtime configuration +e.g. via external message. + +The :meth:`stateFunction` argument is a :class:`PartialFunction[Event, State]`, +which is conveniently given using state function builder syntax as demonstrated below: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: when-syntax + +.. warning:: + + It is required that you define handlers for each of the possible FSM states, + otherwise there will be failures when trying to switch to undeclared states. + +It is recommended practice to declare the states as an enum and then verify that there is a +``when`` clause for each of the states. If you want to leave the handling of a state +“unhandled” (more below), it still needs to be declared like this: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java#NullFunction + +Defining the Initial State +-------------------------- + +Each FSM needs a starting point, which is declared using + + :func:`startWith(state, data[, timeout]);` + +The optionally given timeout argument overrides any specification given for the +desired initial state. If you want to cancel a default timeout, use +:obj:`Duration.Inf`. + +Unhandled Events +---------------- + +If a state doesn't handle a received event a warning is logged. If you want to +do something else in this case you can specify that with +:func:`whenUnhandled(stateFunction)`: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: unhandled-syntax + +Within this handler the state of the FSM may be queried using the +:meth:`stateName` method. + +**IMPORTANT**: This handler is not stacked, meaning that each invocation of +:func:`whenUnhandled` replaces the previously installed handler. + +Initiating Transitions +---------------------- + +The result of any :obj:`stateFunction` must be a definition of the next state +unless terminating the FSM, which is described in `Termination from Inside`_. +The state definition can either be the current state, as described by the +:func:`stay` directive, or it is a different state as given by +:func:`goto(state)`. The resulting object allows further qualification by way +of the modifiers described in the following: + +* :meth:`forMax(duration)` + + This modifier sets a state timeout on the next state. This means that a timer + is started which upon expiry sends a :obj:`StateTimeout` message to the FSM. + This timer is canceled upon reception of any other message in the meantime; + you can rely on the fact that the :obj:`StateTimeout` message will not be + processed after an intervening message. + + This modifier can also be used to override any default timeout which is + specified for the target state. If you want to cancel the default timeout, + use :obj:`Duration.Inf`. + +* :meth:`using(data)` + + This modifier replaces the old state data with the new data given. If you + follow the advice :ref:`above `, this is the only place where + internal state data are ever modified. + +* :meth:`replying(msg)` + + This modifier sends a reply to the currently processed message and otherwise + does not modify the state transition. + +All modifier can be chained to achieve a nice and concise description: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: modifier-syntax + +The parentheses are not actually needed in all cases, but they visually +distinguish between modifiers and their arguments and therefore make the code +even more pleasant to read for foreigners. + +.. note:: + + Please note that the ``return`` statement may not be used in :meth:`when` + blocks or similar; this is a Scala restriction. Either refactor your code + using ``if () ... else ...`` or move it into a method definition. + +Monitoring Transitions +---------------------- + +Transitions occur "between states" conceptually, which means after any actions +you have put into the event handling block; this is obvious since the next +state is only defined by the value returned by the event handling logic. You do +not need to worry about the exact order with respect to setting the internal +state variable, as everything within the FSM actor is running single-threaded +anyway. + +Internal Monitoring +^^^^^^^^^^^^^^^^^^^ + +Up to this point, the FSM DSL has been centered on states and events. The dual +view is to describe it as a series of transitions. This is enabled by the +method + + :func:`onTransition(handler)` + +which associates actions with a transition instead of with a state and event. +The handler is a partial function which takes a pair of states as input; no +resulting state is needed as it is not possible to modify the transition in +progress. + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: transition-syntax + +The convenience extractor :obj:`->` enables decomposition of the pair of states +with a clear visual reminder of the transition's direction. As usual in pattern +matches, an underscore may be used for irrelevant parts; alternatively you +could bind the unconstrained state to a variable, e.g. for logging as shown in +the last case. + +The handlers registered with this method are stacked, so you can intersperse +:func:`onTransition` blocks with :func:`when` blocks as suits your design. It +should be noted, however, that *all handlers will be invoked for each +transition*, not only the first matching one. This is designed specifically so +you can put all transition handling for a certain aspect into one place without +having to worry about earlier declarations shadowing later ones; the actions +are still executed in declaration order, though. + +.. note:: + + This kind of internal monitoring may be used to structure your FSM according + to transitions, so that for example the cancellation of a timer upon leaving + a certain state cannot be forgot when adding new target states. + +External Monitoring +^^^^^^^^^^^^^^^^^^^ + +External actors may be registered to be notified of state transitions by +sending a message :class:`SubscribeTransitionCallBack(actorRef)`. The named +actor will be sent a :class:`CurrentState(self, stateName)` message immediately +and will receive :class:`Transition(actorRef, oldState, newState)` messages +whenever a new state is reached. External monitors may be unregistered by +sending :class:`UnsubscribeTransitionCallBack(actorRef)` to the FSM actor. + +Stopping a listener without unregistering will not remove the listener from the +subscription list; use :class:`UnsubscribeTransitionCallback` before stopping +the listener. + +Timers +------ + +Besides state timeouts, FSM manages timers identified by :class:`String` names. +You may set a timer using + + :func:`setTimer(name, msg, interval, repeat)` + +where :obj:`msg` is the message object which will be sent after the duration +:obj:`interval` has elapsed. If :obj:`repeat` is :obj:`true`, then the timer is +scheduled at fixed rate given by the :obj:`interval` parameter. Timers may be +canceled using + + :func:`cancelTimer(name)` + +which is guaranteed to work immediately, meaning that the scheduled message +will not be processed after this call even if the timer already fired and +queued it. The status of any timer may be inquired with + + :func:`isTimerActive(name)` + +These named timers complement state timeouts because they are not affected by +intervening reception of other messages. + +Termination from Inside +----------------------- + +The FSM is stopped by specifying the result state as + + :func:`stop([reason[, data]])` + +The reason must be one of :obj:`Normal` (which is the default), :obj:`Shutdown` +or :obj:`Failure(reason)`, and the second argument may be given to change the +state data which is available during termination handling. + +.. note:: + + It should be noted that :func:`stop` does not abort the actions and stop the + FSM immediately. The stop action must be returned from the event handler in + the same way as a state transition (but note that the ``return`` statement + may not be used within a :meth:`when` block). + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: stop-syntax + +You can use :func:`onTermination(handler)` to specify custom code that is +executed when the FSM is stopped. The handler is a partial function which takes +a :class:`StopEvent(reason, stateName, stateData)` as argument: + +.. includecode:: ../../../akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java + :include: termination-syntax + +As for the :func:`whenUnhandled` case, this handler is not stacked, so each +invocation of :func:`onTermination` replaces the previously installed handler. + +Termination from Outside +------------------------ + +When an :class:`ActorRef` associated to a FSM is stopped using the +:meth:`stop()` method, its :meth:`postStop` hook will be executed. The default +implementation by the :class:`AbstractFSM` class is to execute the +:meth:`onTermination` handler if that is prepared to handle a +:obj:`StopEvent(Shutdown, ...)`. + +.. warning:: + + In case you override :meth:`postStop` and want to have your + :meth:`onTermination` handler called, do not forget to call + ``super.postStop``. + +Testing and Debugging Finite State Machines +=========================================== + +TODO +---- \ No newline at end of file diff --git a/akka-samples/akka-sample-java8/LICENSE b/akka-samples/akka-sample-java8/LICENSE new file mode 100644 index 0000000000..6c42406c7b --- /dev/null +++ b/akka-samples/akka-sample-java8/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Typesafe, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/akka-samples/akka-sample-java8/build.sbt b/akka-samples/akka-sample-java8/build.sbt new file mode 100644 index 0000000000..11a3f028e1 --- /dev/null +++ b/akka-samples/akka-sample-java8/build.sbt @@ -0,0 +1,17 @@ +name := "akka-sample-java8" + +version := "0.0.1-SNAPSHOT" + +scalaVersion := "2.10.3" + +compileOrder := CompileOrder.ScalaThenJava + +javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-Xlint") + +testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a") + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor" % "2.3-SNAPSHOT", + "com.typesafe.akka" %% "akka-testkit" % "2.3-SNAPSHOT" % "test", + "junit" % "junit" % "4.11" % "test", + "com.novocode" % "junit-interface" % "0.10" % "test") diff --git a/akka-samples/akka-sample-java8/pom.xml b/akka-samples/akka-sample-java8/pom.xml new file mode 100644 index 0000000000..85e566b9ea --- /dev/null +++ b/akka-samples/akka-sample-java8/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + UTF-8 + + + sample + akka-sample-java8 + jar + 0.0.1-SNAPSHOT + + + + com.typesafe.akka + akka-actor_2.10 + 2.3-SNAPSHOT + + + com.typesafe.akka + akka-testkit_2.10 + 2.3-SNAPSHOT + + + junit + junit + 4.11 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + true + + -Xlint + + + + + + + + diff --git a/akka-samples/akka-sample-java8/project/build.properties b/akka-samples/akka-sample-java8/project/build.properties new file mode 100644 index 0000000000..37b489cb6e --- /dev/null +++ b/akka-samples/akka-sample-java8/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.1 diff --git a/akka-samples/akka-sample-java8/src/main/java/sample/java8/MyActor.java b/akka-samples/akka-sample-java8/src/main/java/sample/java8/MyActor.java new file mode 100644 index 0000000000..3bd62a6212 --- /dev/null +++ b/akka-samples/akka-sample-java8/src/main/java/sample/java8/MyActor.java @@ -0,0 +1,24 @@ +package sample.java8; + +//#imports +import akka.actor.AbstractActor; +import akka.event.Logging; +import akka.event.LoggingAdapter; +import akka.japi.pf.ReceiveBuilder; +import scala.PartialFunction; +import scala.runtime.BoxedUnit; + +//#imports + +//#my-actor +public class MyActor extends AbstractActor { + private final LoggingAdapter log = Logging.getLogger(context().system(), this); + + @Override + public PartialFunction receive() { + return ReceiveBuilder. + match(String.class, s -> s.equals("test"), s -> log.info("received test")). + matchAny(o -> log.info("received unknown message")).build(); + } +} +//#my-actor diff --git a/akka-samples/akka-sample-java8/src/main/java/sample/java8/SampleActor.java b/akka-samples/akka-sample-java8/src/main/java/sample/java8/SampleActor.java new file mode 100644 index 0000000000..1d9e7f9ef6 --- /dev/null +++ b/akka-samples/akka-sample-java8/src/main/java/sample/java8/SampleActor.java @@ -0,0 +1,32 @@ +package sample.java8; + +//#sample-actor +import akka.actor.AbstractActor; +import akka.japi.pf.ReceiveBuilder; +import scala.PartialFunction; +import scala.runtime.BoxedUnit; + +public class SampleActor extends AbstractActor { + + private PartialFunction guarded = ReceiveBuilder. + match(String.class, s -> s.contains("guard"), s -> { + sender().tell("contains(guard): " + s, self()); + context().unbecome(); + }).build(); + + @Override + public PartialFunction receive() { + return ReceiveBuilder. + match(Double.class, d -> { + sender().tell(d.isNaN() ? 0 : d, self()); + }). + match(Integer.class, i -> { + sender().tell(i * 10, self()); + }). + match(String.class, s -> s.startsWith("guard"), s -> { + sender().tell("startsWith(guard): " + s.toUpperCase(), self()); + context().become(guarded, false); + }).build(); + } +} +//#sample-actor diff --git a/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java b/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java new file mode 100644 index 0000000000..4be0ad2701 --- /dev/null +++ b/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Buncher.java @@ -0,0 +1,155 @@ +package sample.java8.buncher; + +//#simple-imports +import akka.actor.AbstractFSM; +import akka.actor.ActorRef; +import akka.japi.pf.UnitMatch; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import scala.concurrent.duration.Duration; +//#simple-imports + +import static sample.java8.buncher.Buncher.State; +import static sample.java8.buncher.Buncher.Data; +import static sample.java8.buncher.Buncher.State.*; +import static sample.java8.buncher.Buncher.Uninitialized.*; +import static sample.java8.buncher.Events.*; + +//#simple-fsm +public class Buncher extends AbstractFSM { + + { + //#fsm-body + startWith(Idle, Uninitialized); + + //#when-syntax + when(Idle, + matchEvent(SetTarget.class, Uninitialized.class, + (setTarget, uninitialized) -> stay().using(new Todo(setTarget.getRef(), new LinkedList<>())))); + //#when-syntax + + //#transition-elided + onTransition( + matchState(Active, Idle, () -> { + // reuse this matcher + final UnitMatch m = UnitMatch.create( + matchData(Todo.class, + todo -> todo.getTarget().tell(new Batch(todo.getQueue()), self()))); + m.match(stateData()); + }). + state(Idle, Active, () -> {/* Do something here */})); + //#transition-elided + + when(Active, Duration.create(1, "second"), + matchEvent(Arrays.asList(Flush.class, StateTimeout()), Todo.class, + todo -> goTo(Idle).using(todo.copy(new LinkedList<>())))); + + //#unhandled-elided + whenUnhandled( + matchEvent(Queue.class, Todo.class, + (queue, todo) -> goTo(Active).using(todo.addElement(queue.getObj()))). + anyEvent((event, state) -> { + log().warning("received unhandled request {} in state {}/{}", event, stateName(), state); + return stay(); + })); + //#unhandled-elided + + //#termination-elided + //#termination-syntax + onTermination( + matchStop(Normal(), + (state, data) -> {/* Do something here */}). + stop(Shutdown(), + (state, data) -> {/* Do something here */}). + stop(Failure(), + (reason, state, data) -> {/* Do something here */})); + //#termination-syntax + //#termination-elided + //#fsm-body + + initialize(); + } + //#simple-fsm + + static + //#simple-state + // states + enum State { + Idle, Active + } + + //#simple-state + static + //#simple-state + // state data + interface Data { + } + + //#simple-state + static + //#simple-state + enum Uninitialized implements Data { + Uninitialized + } + + //#simple-state + static + //#simple-state + final class Todo implements Data { + private final ActorRef target; + private final List queue; + + public Todo(ActorRef target, List queue) { + this.target = target; + this.queue = queue; + } + + public ActorRef getTarget() { + return target; + } + + public List getQueue() { + return queue; + } + //#boilerplate + + @Override + public String toString() { + return "Todo{" + + "target=" + target + + ", queue=" + queue + + '}'; + } + + public Todo addElement(Object element) { + List nQueue = new LinkedList<>(queue); + nQueue.add(element); + return new Todo(this.target, nQueue); + } + + public Todo copy(List queue) { + return new Todo(this.target, queue); + } + + public Todo copy(ActorRef target) { + return new Todo(target, this.queue); + } + //#boilerplate + } + //#simple-state + //#simple-fsm +} +//#simple-fsm + +//#NullFunction +//#unhandled-syntax +//#modifier-syntax +//#transition-syntax +//#stop-syntax +// TODO add code example here +//#stop-syntax +//#transition-syntax +//#modifier-syntax +//#unhandled-syntax +//#NullFunction diff --git a/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Events.java b/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Events.java new file mode 100644 index 0000000000..77b13c0b68 --- /dev/null +++ b/akka-samples/akka-sample-java8/src/main/java/sample/java8/buncher/Events.java @@ -0,0 +1,104 @@ +package sample.java8.buncher; + +import akka.actor.ActorRef; +import java.util.List; + +public class Events { + + static + //#simple-events + public final class SetTarget { + private final ActorRef ref; + + public SetTarget(ActorRef ref) { + this.ref = ref; + } + + public ActorRef getRef() { + return ref; + } + //#boilerplate + + @Override + public String toString() { + return "SetTarget{" + + "ref=" + ref + + '}'; + } + //#boilerplate + } + + //#simple-events + static + //#simple-events + public final class Queue { + private final Object obj; + + public Queue(Object obj) { + this.obj = obj; + } + + public Object getObj() { + return obj; + } + //#boilerplate + + @Override + public String toString() { + return "Queue{" + + "obj=" + obj + + '}'; + } + //#boilerplate + } + + //#simple-events + static + //#simple-events + public final class Batch { + private final List list; + + public Batch(List list) { + this.list = list; + } + + public List getList() { + return list; + } + //#boilerplate + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Batch batch = (Batch) o; + + return list.equals(batch.list); + } + + @Override + public int hashCode() { + return list.hashCode(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append( "Batch{list="); + list.stream().forEachOrdered(e -> { builder.append(e); builder.append(","); }); + int len = builder.length(); + builder.replace(len, len, "}"); + return builder.toString(); + } + //#boilerplate + } + + //#simple-events + static + //#simple-events + public enum Flush { + Flush + } + //#simple-events +} diff --git a/akka-samples/akka-sample-java8/src/test/java/sample/BuncherTest.java b/akka-samples/akka-sample-java8/src/test/java/sample/BuncherTest.java new file mode 100644 index 0000000000..ad3dab275d --- /dev/null +++ b/akka-samples/akka-sample-java8/src/test/java/sample/BuncherTest.java @@ -0,0 +1,59 @@ +package sample; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.testkit.JavaTestKit; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import java.util.LinkedList; + +import sample.java8.buncher.*; +import static sample.java8.buncher.Events.Batch; +import static sample.java8.buncher.Events.Queue; +import static sample.java8.buncher.Events.SetTarget; +import static sample.java8.buncher.Events.Flush.Flush; + +public class BuncherTest { + + static ActorSystem system; + + @BeforeClass + public static void setup() { + system = ActorSystem.create("BuncherTest"); + } + + @AfterClass + public static void tearDown() { + JavaTestKit.shutdownActorSystem(system); + system = null; + } + + @Test + public void testBuncherActor() + { + new JavaTestKit(system) {{ + final ActorRef buncher = system.actorOf(Props.create(Buncher.class), "buncher-actor"); + final ActorRef probe = getRef(); + + buncher.tell(new SetTarget(probe), probe); + buncher.tell(new Queue(42), probe); + buncher.tell(new Queue(43), probe); + LinkedList list1 = new LinkedList<>(); + list1.add(42); + list1.add(43); + expectMsgEquals(new Batch(list1)); + buncher.tell(new Queue(44), probe); + buncher.tell(Flush, probe); + buncher.tell(new Queue(45), probe); + LinkedList list2 = new LinkedList<>(); + list2.add(44); + expectMsgEquals(new Batch(list2)); + LinkedList list3 = new LinkedList<>(); + list3.add(45); + expectMsgEquals(new Batch(list3)); + system.stop(buncher); + }}; + } +} diff --git a/akka-samples/akka-sample-java8/src/test/java/sample/SampleActorTest.java b/akka-samples/akka-sample-java8/src/test/java/sample/SampleActorTest.java new file mode 100644 index 0000000000..ce037f5057 --- /dev/null +++ b/akka-samples/akka-sample-java8/src/test/java/sample/SampleActorTest.java @@ -0,0 +1,52 @@ +package sample; + +import akka.actor.ActorRef; +import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.testkit.JavaTestKit; +import sample.java8.SampleActor; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SampleActorTest { + + static ActorSystem system; + + @BeforeClass + public static void setup() { + system = ActorSystem.create("SampleActorTest"); + } + + @AfterClass + public static void tearDown() { + JavaTestKit.shutdownActorSystem(system); + system = null; + } + + @Test + public void testSampleActor() + { + new JavaTestKit(system) {{ + final ActorRef subject = system.actorOf(Props.create(SampleActor.class), "sample-actor"); + final ActorRef probeRef = getRef(); + + subject.tell(47.11, probeRef); + subject.tell("and no guard in the beginning", probeRef); + subject.tell("guard is a good thing", probeRef); + subject.tell(47.11, probeRef); + subject.tell(4711, probeRef); + subject.tell("and no guard in the beginning", probeRef); + subject.tell(4711, probeRef); + subject.tell("and an unmatched message", probeRef); + + expectMsgEquals(47.11); + assertTrue(expectMsgClass(String.class).startsWith("startsWith(guard):")); + assertTrue(expectMsgClass(String.class).startsWith("contains(guard):")); + expectMsgEquals(47110); + expectNoMsg(); + }}; + } +}