From a86fc10968deca5bab778f5e3d0bcbf0c12ddade Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Thu, 25 Feb 2010 17:19:50 +0100 Subject: [PATCH 01/17] initial camel integration (early-access, see also http://doc.akkasource.org/Camel) --- akka-camel/pom.xml | 51 ++++++ .../services/org/apache/camel/component/actor | 1 + akka-camel/src/main/scala/CamelConsumer.scala | 23 +++ .../main/scala/component/ActorComponent.scala | 166 ++++++++++++++++++ .../scala/service/CamelContextManager.scala | 23 +++ .../src/main/scala/service/CamelService.scala | 99 +++++++++++ .../scala/component/ActorComponentTest.scala | 57 ++++++ .../test/scala/service/CamelServiceTest.scala | 107 +++++++++++ akka-core/src/test/scala/SerializerTest.scala | 6 +- akka-kernel/pom.xml | 8 + akka-kernel/src/main/scala/Kernel.scala | 3 +- akka-samples/akka-sample-camel/pom.xml | 39 ++++ .../src/main/scala/Boot.scala | 36 ++++ .../src/main/scala/Consumer1.scala | 20 +++ .../src/main/scala/Consumer2.scala | 18 ++ akka-samples/pom.xml | 6 + .../akka/annotation/consume.java | 18 ++ config/akka-reference.conf | 9 +- pom.xml | 1 + 19 files changed, 684 insertions(+), 7 deletions(-) create mode 100644 akka-camel/pom.xml create mode 100644 akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor create mode 100644 akka-camel/src/main/scala/CamelConsumer.scala create mode 100644 akka-camel/src/main/scala/component/ActorComponent.scala create mode 100644 akka-camel/src/main/scala/service/CamelContextManager.scala create mode 100644 akka-camel/src/main/scala/service/CamelService.scala create mode 100644 akka-camel/src/test/scala/component/ActorComponentTest.scala create mode 100644 akka-camel/src/test/scala/service/CamelServiceTest.scala create mode 100644 akka-samples/akka-sample-camel/pom.xml create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Boot.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala create mode 100644 akka-util-java/src/main/java/se/scalablesolutions/akka/annotation/consume.java diff --git a/akka-camel/pom.xml b/akka-camel/pom.xml new file mode 100644 index 0000000000..bc42439e84 --- /dev/null +++ b/akka-camel/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + akka-camel + Akka Camel Module + + jar + + + akka + se.scalablesolutions.akka + 0.7-SNAPSHOT + + + + + + akka-core + ${project.groupId} + ${project.version} + + + org.apache.camel + camel-core + 2.2.0 + + + org.apache.camel + camel-jetty + 2.2.0 + + + + + org.scalatest + scalatest + 1.0 + test + + + junit + junit + 4.5 + test + + + + \ No newline at end of file diff --git a/akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor b/akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor new file mode 100644 index 0000000000..a2141db8a9 --- /dev/null +++ b/akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor @@ -0,0 +1 @@ +class=se.scalablesolutions.akka.camel.component.ActorComponent \ No newline at end of file diff --git a/akka-camel/src/main/scala/CamelConsumer.scala b/akka-camel/src/main/scala/CamelConsumer.scala new file mode 100644 index 0000000000..f3518e03f6 --- /dev/null +++ b/akka-camel/src/main/scala/CamelConsumer.scala @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel + +import se.scalablesolutions.akka.actor.Actor + +/** + * Mixed in by Actor subclasses to be Camel endpoint consumers. + * + * @author Martin Krasser + */ +trait CamelConsumer { + + self: Actor => + + /** + * Returns the Camel endpoint URI to consume messages from. + */ + def endpointUri: String + +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala new file mode 100644 index 0000000000..4c99bfd809 --- /dev/null +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -0,0 +1,166 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel.component + +import java.lang.{RuntimeException, String} +import java.util.{Map => JavaMap} + +import org.apache.camel.{Exchange, Consumer, Processor} +import org.apache.camel.impl.{DefaultProducer, DefaultEndpoint, DefaultComponent} + +import se.scalablesolutions.akka.actor.{ActorRegistry, Actor} + +/** + * Camel component for interacting with actors. + * + * @see se.scalablesolutions.akka.camel.component.ActorEndpoint + * @see se.scalablesolutions.akka.camel.component.ActorProducer + * + * @author Martin Krasser + */ +class ActorComponent extends DefaultComponent { + + def createEndpoint(uri: String, remaining: String, parameters: JavaMap[String, Object]): ActorEndpoint = { + val idAndUuid = idAndUuidPair(remaining) + new ActorEndpoint(uri, this, idAndUuid._1, idAndUuid._2) + } + + private def idAndUuidPair(remaining: String): Tuple2[String, Option[String]] = { + remaining split "/" toList match { + case id :: Nil => (id, None) + case id :: uuid :: Nil => (id, Some(uuid)) + case _ => throw new IllegalArgumentException( + "invalid path format: %s - should be [/]" format remaining) + } + } + +} + +/** + * Camel endpoint for interacting with actors. An actor can be addressed by its + * Actor.id or by an Actor.id - Actor.uuid + * combination. The URI format is actor://[/]. + * + * @see se.scalablesolutions.akka.camel.component.ActorComponent + * @see se.scalablesolutions.akka.camel.component.ActorProducer + + * @author Martin Krasser + */ +class ActorEndpoint(uri: String, comp: ActorComponent, val id: String, val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { + + // TODO: clarify uuid details + // - do they change after persist/restore + // - what about remote actors and uuids + + /** + * @throws UnsupportedOperationException + */ + def createConsumer(processor: Processor): Consumer = + throw new UnsupportedOperationException("actor consumer not supported yet") + + def createProducer: ActorProducer = new ActorProducer(this) + + def isSingleton: Boolean = true + +} + +/** + * Sends the in-message of an exchange to an actor. If the exchange pattern is out-capable, + * the producer waits for a reply (using the !! operator), otherwise the ! operator is used + * for sending the message. Asynchronous communication is not implemented yet but will be + * added for Camel components that support the Camel Async API (like the jetty component that + * makes use of Jetty continuations). + * + * @see se.scalablesolutions.akka.camel.component.ActorComponent + * @see se.scalablesolutions.akka.camel.component.ActorEndpoint + * + * @author Martin Krasser + */ +class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { + + implicit val sender = Some(Sender) + + def process(exchange: Exchange) { + val actor = target getOrElse (throw new ActorNotRegisteredException(ep.id, ep.uuid)) + if (exchange.getPattern.isOutCapable) + processInOut(exchange, actor) + else + processInOnly(exchange, actor) + } + + override def start { + super.start + } + + protected def receive = { + throw new UnsupportedOperationException + } + + protected def processInOnly(exchange: Exchange, actor: Actor) { + actor ! exchange.getIn + } + + protected def processInOut(exchange: Exchange, actor: Actor) { + val outmsg = exchange.getOut + // TODO: make timeout configurable + // TODO: send immutable message + // TODO: support asynchronous communication + // - jetty component: jetty continuations + // - file component: completion callbacks + val result: Any = actor !! exchange.getIn + + result match { + case Some((body, headers:Map[String, Any])) => { + outmsg.setBody(body) + for (header <- headers) + outmsg.getHeaders.put(header._1, header._2.asInstanceOf[AnyRef]) + } + case Some(body) => outmsg.setBody(body) + } + } + + private def target: Option[Actor] = { + ActorRegistry.actorsFor(ep.id) match { + case actor :: Nil if targetMatchesUuid(actor) => Some(actor) + case Nil => None + case actors => actors find (targetMatchesUuid _) + } + } + + private def targetMatchesUuid(target: Actor): Boolean = + // if ep.uuid is not defined always return true + target.uuid == (ep.uuid getOrElse target.uuid) + +} + +/** + * Generic message sender used by ActorProducer. + * + * @author Martin Krasser + */ +private[component] object Sender extends Actor { + + start + + /** + * Ignores any message. + */ + protected def receive = { + case _ => { /* ignore any reply */ } + } + +} + +/** + * Thrown to indicate that an actor referenced by an endpoint URI cannot be + * found in the ActorRegistry. + * + * @author Martin Krasser + */ +class ActorNotRegisteredException(name: String, uuid: Option[String]) extends RuntimeException { + + override def getMessage = "actor(id=%s,uuid=%s) not registered" format (name, uuid getOrElse "") + +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/service/CamelContextManager.scala b/akka-camel/src/main/scala/service/CamelContextManager.scala new file mode 100644 index 0000000000..a6f84c158c --- /dev/null +++ b/akka-camel/src/main/scala/service/CamelContextManager.scala @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel.service + +import org.apache.camel.CamelContext +import org.apache.camel.impl.DefaultCamelContext + +/** + * Manages the CamelContext used by CamelService. + * + * @author Martin Krasser + */ +object CamelContextManager { + + /** + * The CamelContext used by CamelService. Can be modified by applications prior to + * loading the CamelService. + */ + var context: CamelContext = new DefaultCamelContext + +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala new file mode 100644 index 0000000000..2811ab88fe --- /dev/null +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel.service + +import java.io.InputStream + +import org.apache.camel.builder.RouteBuilder + +import se.scalablesolutions.akka.actor.{Actor, ActorRegistry} +import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.CamelConsumer +import se.scalablesolutions.akka.util.{Bootable, Logging} + +/** + * Started by the Kernel to expose actors as Camel endpoints. + * + * @see CamelRouteBuilder + * + * @author Martin Krasser + */ +trait CamelService extends Bootable with Logging { + + import CamelContextManager.context + + abstract override def onLoad = { + super.onLoad + context.addRoutes(new CamelRouteBuilder) + context.setStreamCaching(true) + context.start + log.info("Camel context started") + } + + abstract override def onUnload = { + super.onUnload + context.stop + log.info("Camel context stopped") + } + +} + +/** + * Generic route builder that searches the registry for actors that are + * either annotated with @se.scalablesolutions.akka.annotation.consume or + * mixed in se.scalablesolutions.akka.camel.CamelConsumer and exposes them + * as Camel endpoints. + * + * @author Martin Krasser + */ +class CamelRouteBuilder extends RouteBuilder with Logging { + + def configure = { + val actors = ActorRegistry.actors + + // + // TODO: resolve/clarify issues with ActorRegistry + // - custom Actor.id ignored + // - actor de-registration issues + // - multiple registration with same id/uuid possible + // + + // TODO: avoid redundant registrations + actors.filter(isConsumeAnnotated _).foreach { actor: Actor => + val fromUri = actor.getClass.getAnnotation(classOf[consume]).value() + configure(fromUri, "actor://%s" format actor.id) + log.debug("registered actor (id=%s) for consuming messages from %s " + format (actor.id, fromUri)) + } + + // TODO: avoid redundant registrations + actors.filter(isConsumerInstance _).foreach { actor: Actor => + val fromUri = actor.asInstanceOf[CamelConsumer].endpointUri + configure(fromUri, "actor://%s/%s" format (actor.id, actor.uuid)) + log.debug("registered actor (id=%s, uuid=%s) for consuming messages from %s " + format (actor.id, actor.uuid, fromUri)) + } + } + + private def configure(fromUri: String, toUri: String) { + val schema = fromUri take fromUri.indexOf(":") // e.g. "http" from "http://whatever/..." + bodyConversions.get(schema) match { + case Some(clazz) => from(fromUri).convertBodyTo(clazz).to(toUri) + case None => from(fromUri).to(toUri) + } + } + + // TODO: make conversions configurable + private def bodyConversions = Map( + "file" -> classOf[InputStream] + ) + + private def isConsumeAnnotated(actor: Actor) = + actor.getClass.getAnnotation(classOf[consume]) ne null + + private def isConsumerInstance(actor: Actor) = + actor.isInstanceOf[CamelConsumer] + +} diff --git a/akka-camel/src/test/scala/component/ActorComponentTest.scala b/akka-camel/src/test/scala/component/ActorComponentTest.scala new file mode 100644 index 0000000000..3a06d483be --- /dev/null +++ b/akka-camel/src/test/scala/component/ActorComponentTest.scala @@ -0,0 +1,57 @@ +package se.scalablesolutions.akka.camel.component + +import org.apache.camel.{Message, RuntimeCamelException} +import org.apache.camel.impl.{SimpleRegistry, DefaultCamelContext} +import org.junit._ +import org.junit.Assert._ +import org.scalatest.junit.JUnitSuite + +import se.scalablesolutions.akka.actor.Actor + +/** + * @author Martin Krasser + */ +class ActorComponentTest extends JUnitSuite { + + import ActorComponentTestSetup._ + + val actor = ActorComponentTestActor.start + + @Test + def testMatchIdOnly() { + val result = template.requestBody("actor:%s" format actor.id, "Martin") + assertEquals("Hello Martin", result) + } + + @Test + def testMatchIdAndUuid() { + val result = template.requestBody("actor:%s/%s" format (actor.id, actor.uuid), "Martin") + assertEquals("Hello Martin", result) + } + + @Test + def testMatchIdButNotUuid() { + intercept[RuntimeCamelException] { + template.requestBody("actor:%s/%s" format (actor.id, "wrong"), "Martin") + } + } + +} + +object ActorComponentTestActor extends Actor { + + protected def receive = { + case msg: Message => reply("Hello %s" format msg.getBody) + } + +} + +object ActorComponentTestSetup { + + val context = new DefaultCamelContext(new SimpleRegistry) + val template = context.createProducerTemplate + + context.start + template.start + +} \ No newline at end of file diff --git a/akka-camel/src/test/scala/service/CamelServiceTest.scala b/akka-camel/src/test/scala/service/CamelServiceTest.scala new file mode 100644 index 0000000000..8cce9ec2bd --- /dev/null +++ b/akka-camel/src/test/scala/service/CamelServiceTest.scala @@ -0,0 +1,107 @@ +package se.scalablesolutions.akka.camel.service + +import org.apache.camel.Message +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.DefaultCamelContext +import org.junit.Test +import org.junit.Assert._ +import org.scalatest.junit.JUnitSuite + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.CamelConsumer + + +/** + * @author Martin Krasser + */ +class CamelServiceTest extends JUnitSuite { + + import CamelServiceTestSetup._ + + @Test + def testActor1() { + val result = template.requestBody("direct:actor1", "Martin") + assertEquals("Hello Martin (actor1)", result) + } + + @Test + def testActor2() { + val result = template.requestBody("direct:actor2", "Martin") + assertEquals("Hello Martin (actor2)", result) + } + + @Test + def testActor3() { + val result = template.requestBody("direct:actor3", "Martin") + assertEquals("Hello Tester (actor3)", result) + } + +} + +class TestActor1 extends Actor with CamelConsumer { + + def endpointUri = "direct:actor1" + + protected def receive = { + case msg: Message => reply("Hello %s (actor1)" format msg.getBody) + } + +} + +@consume("direct:actor2") +class TestActor2 extends Actor { + + protected def receive = { + case msg: Message => reply("Hello %s (actor2)" format msg.getBody) + } + +} + +class TestActor3 extends Actor { + + protected def receive = { + case msg: Message => reply("Hello %s (actor3)" format msg.getBody) + } + +} + +class TestBuilder extends RouteBuilder { + + def configure { + val actorUri = "actor://%s" format classOf[TestActor3].getName + from("direct:actor3").transform(constant("Tester")).to(actorUri) + } + +} + +object CamelServiceTestSetup extends CamelService { + + import CamelContextManager.context + + // use a custom camel context + context = new DefaultCamelContext + + val template = context.createProducerTemplate + var loaded = false + + onLoad + + override def onLoad = { + if (!loaded) { + // use a custom camel context + context.addRoutes(new TestBuilder) + // register test actors + new TestActor1().start + new TestActor2().start + new TestActor3().start + // start Camel service + super.onLoad + + template.start + loaded = true + } + } + +} + diff --git a/akka-core/src/test/scala/SerializerTest.scala b/akka-core/src/test/scala/SerializerTest.scala index e11e83a2f5..889dea4ba8 100644 --- a/akka-core/src/test/scala/SerializerTest.scala +++ b/akka-core/src/test/scala/SerializerTest.scala @@ -3,7 +3,7 @@ package se.scalablesolutions.akka.serialization import junit.framework.TestCase import org.scalatest.junit.JUnitSuite -import org.junit.{Test, Before, After} +import org.junit.{Test, Before, After, Ignore} import scala.reflect.BeanInfo @BeanInfo @@ -18,7 +18,7 @@ case class MyMessage(val id: String, val value: Tuple2[String, Int]) { class SerializerTest extends JUnitSuite { - @Test + @Test @Ignore // TODO: resolve test failure def shouldSerializeString = { val f = Foo("debasish") val json = Serializer.ScalaJSON.out(f) @@ -27,7 +27,7 @@ class SerializerTest extends JUnitSuite { assert(fo == f) } - @Test + @Test @Ignore // TODO: resolve test failure def shouldSerializeTuple2 = { val message = MyMessage("id", ("hello", 34)) val json = Serializer.ScalaJSON.out(message) diff --git a/akka-kernel/pom.xml b/akka-kernel/pom.xml index 4b1d114d45..1d578023fd 100644 --- a/akka-kernel/pom.xml +++ b/akka-kernel/pom.xml @@ -51,6 +51,11 @@ ${project.groupId} ${project.version} + + akka-camel + ${project.groupId} + ${project.version} + akka-cluster-jgroups ${project.groupId} @@ -104,6 +109,9 @@ + + META-INF/services/org/apache/camel/TypeConverter + diff --git a/akka-kernel/src/main/scala/Kernel.scala b/akka-kernel/src/main/scala/Kernel.scala index f63a50a0a7..406c914577 100644 --- a/akka-kernel/src/main/scala/Kernel.scala +++ b/akka-kernel/src/main/scala/Kernel.scala @@ -4,6 +4,7 @@ package se.scalablesolutions.akka +import se.scalablesolutions.akka.camel.service.CamelService import se.scalablesolutions.akka.remote.BootableRemoteActorService import se.scalablesolutions.akka.actor.BootableActorLoaderService import se.scalablesolutions.akka.util.{Logging,Bootable} @@ -32,7 +33,7 @@ object Kernel extends Logging { /** * Boots up the Kernel with default bootables */ - def boot : Unit = boot(true, new BootableActorLoaderService with BootableRemoteActorService with BootableCometActorService) + def boot : Unit = boot(true, new BootableActorLoaderService with BootableRemoteActorService with BootableCometActorService with CamelService) /** * Boots up the Kernel. diff --git a/akka-samples/akka-sample-camel/pom.xml b/akka-samples/akka-sample-camel/pom.xml new file mode 100644 index 0000000000..95adba3149 --- /dev/null +++ b/akka-samples/akka-sample-camel/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + akka-sample-camel + Akka Camel Sample Module + + jar + + + akka-samples-parent + se.scalablesolutions.akka + 0.7-SNAPSHOT + + + + src/main/scala + + + maven-antrun-plugin + + + install + + + + + + + run + + + + + + + + diff --git a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala new file mode 100644 index 0000000000..0b3726c08b --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala @@ -0,0 +1,36 @@ +package sample.camel + +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.DefaultCamelContext + +import se.scalablesolutions.akka.actor.SupervisorFactory +import se.scalablesolutions.akka.camel.service.CamelContextManager +import se.scalablesolutions.akka.config.ScalaConfig._ + +/** + * @author Martin Krasser + */ +class Boot { + + import CamelContextManager.context + + context = new DefaultCamelContext + context.addRoutes(new CustomRouteBuilder) + + val factory = SupervisorFactory( + SupervisorConfig( + RestartStrategy(OneForOne, 3, 100, List(classOf[Exception])), + Supervise(new Consumer1, LifeCycle(Permanent)) :: + Supervise(new Consumer2, LifeCycle(Permanent)) :: Nil)) + factory.newInstance.start + +} + +class CustomRouteBuilder extends RouteBuilder { + + def configure { + val actorUri = "actor:%s" format classOf[Consumer2].getName + from ("jetty:http://0.0.0.0:8877/camel/test2").to(actorUri) + } + +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala new file mode 100644 index 0000000000..fd9b38a3a9 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala @@ -0,0 +1,20 @@ +package sample.camel + +import org.apache.camel.Message + +import se.scalablesolutions.akka.util.Logging +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.CamelConsumer + +/** + * @author Martin Krasser + */ +class Consumer1 extends Actor with CamelConsumer with Logging { + + def endpointUri = "file:data/input" + + def receive = { + case msg: Message => log.info("received %s" format msg.getBody(classOf[String])) + } + +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala new file mode 100644 index 0000000000..aa9cd5e612 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala @@ -0,0 +1,18 @@ +package sample.camel + +import org.apache.camel.Message + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.annotation.consume + +/** + * @author Martin Krasser + */ +@consume("jetty:http://0.0.0.0:8877/camel/test1") +class Consumer2 extends Actor { + + def receive = { + case msg: Message => reply("Hello %s" format msg.getBody(classOf[String])) + } + +} \ No newline at end of file diff --git a/akka-samples/pom.xml b/akka-samples/pom.xml index ad94fc8aab..427ad3d665 100644 --- a/akka-samples/pom.xml +++ b/akka-samples/pom.xml @@ -19,6 +19,7 @@ akka-sample-security akka-sample-rest-scala akka-sample-rest-java + akka-sample-camel @@ -47,6 +48,11 @@ ${project.groupId} ${project.version} + + akka-camel + ${project.groupId} + ${project.version} + akka-security ${project.groupId} diff --git a/akka-util-java/src/main/java/se/scalablesolutions/akka/annotation/consume.java b/akka-util-java/src/main/java/se/scalablesolutions/akka/annotation/consume.java new file mode 100644 index 0000000000..3f8ab9455a --- /dev/null +++ b/akka-util-java/src/main/java/se/scalablesolutions/akka/annotation/consume.java @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface consume { + + public abstract String value(); + +} \ No newline at end of file diff --git a/config/akka-reference.conf b/config/akka-reference.conf index 749b599e0b..48207e9966 100644 --- a/config/akka-reference.conf +++ b/config/akka-reference.conf @@ -19,9 +19,12 @@ # FQN to the class doing initial active object/actor # supervisor bootstrap, should be defined in default constructor - boot = ["sample.java.Boot", - "sample.scala.Boot", - "se.scalablesolutions.akka.security.samples.Boot"] + boot = ["sample.camel.Boot"] + + # Disable other boot configurations at the moment + #boot = ["sample.java.Boot", + # "sample.scala.Boot", + # "se.scalablesolutions.akka.security.samples.Boot"] timeout = 5000 # default timeout for future based invocations diff --git a/pom.xml b/pom.xml index 3cfc2839a8..dec242af2d 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ akka-amqp akka-security akka-patterns + akka-camel akka-kernel akka-fun-test-java akka-samples From 17ffeb3cf9e391989d578031c83238a47ca2f7f5 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sun, 28 Feb 2010 09:13:23 +0100 Subject: [PATCH 02/17] Fixed actor deregistration-by-id issue and added ActorRegistry unit test. --- .../src/main/scala/actor/ActorRegistry.scala | 17 +++++- .../src/test/scala/ActorRegistryTest.scala | 55 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 akka-core/src/test/scala/ActorRegistryTest.scala diff --git a/akka-core/src/main/scala/actor/ActorRegistry.scala b/akka-core/src/main/scala/actor/ActorRegistry.scala index 509750340e..7ca40d1a82 100644 --- a/akka-core/src/main/scala/actor/ActorRegistry.scala +++ b/akka-core/src/main/scala/actor/ActorRegistry.scala @@ -10,13 +10,15 @@ import scala.collection.mutable.{ListBuffer, HashMap} import scala.reflect.Manifest /** - * Registry holding all actor instances, mapped by class and the actor's id field (which can be set by user-code). + * Registry holding all actor instances, mapped by class, the actor's uuid and the actor's id field (which can be set + * by user-code). * * @author Jonas Bonér */ object ActorRegistry extends Logging { private val actorsByClassName = new HashMap[String, List[Actor]] private val actorsById = new HashMap[String, List[Actor]] + private val actorsByUuid = new HashMap[String, Actor] /** * Returns all actors in the system. @@ -50,7 +52,7 @@ object ActorRegistry extends Logging { } /** - * Finds all actors that has a specific id. + * Finds all actors that have a specific id. */ def actorsFor(id : String): List[Actor] = synchronized { actorsById.get(id) match { @@ -59,6 +61,13 @@ object ActorRegistry extends Logging { } } + /** + * Finds the actor that has a specific uuid. + */ + def actorFor(uuid : String): Option[Actor] = synchronized { + actorsByUuid.get(uuid) + } + def register(actor: Actor) = synchronized { val className = actor.getClass.getName actorsByClassName.get(className) match { @@ -71,11 +80,13 @@ object ActorRegistry extends Logging { case Some(instances) => actorsById + (id -> (actor :: instances)) case None => actorsById + (id -> (actor :: Nil)) } + actorsByUuid + (actor.uuid -> actor) } def unregister(actor: Actor) = synchronized { actorsByClassName - actor.getClass.getName - actorsById - actor.getClass.getName + actorsById - actor.getId + actorsByUuid - actor.uuid } def shutdownAll = { diff --git a/akka-core/src/test/scala/ActorRegistryTest.scala b/akka-core/src/test/scala/ActorRegistryTest.scala new file mode 100644 index 0000000000..4c0292abcf --- /dev/null +++ b/akka-core/src/test/scala/ActorRegistryTest.scala @@ -0,0 +1,55 @@ +package se.scalablesolutions.akka.actor + +import org.junit.Assert._ +import org.scalatest.junit.JUnitSuite +import org.junit.Test + +class ActorRegistryTest extends JUnitSuite { + + val registry = ActorRegistry + + @Test + def testRegistrationWithDefaultId { + val actor = new TestActor1 + assertEquals(actor.getClass.getName, actor.getId) + testRegistration(actor, classOf[TestActor1]) + } + + @Test + def testRegistrationWithCustomId { + val actor = new TestActor2 + assertEquals("customid", actor.getId) + testRegistration(actor, classOf[TestActor2]) + } + + private def testRegistration[T <: Actor](actor: T, actorClass: Class[T]) { + assertEquals("non-started actor registered", Nil, registry.actorsFor(actorClass)) + assertEquals("non-started actor registered", Nil, registry.actorsFor(actor.getId)) + assertEquals("non-started actor registered", None, registry.actorFor(actor.uuid)) + actor.start + assertEquals("actor not registered", List(actor), registry.actorsFor(actorClass)) + assertEquals("actor not registered", List(actor), registry.actorsFor(actor.getId)) + assertEquals("actor not registered", Some(actor), registry.actorFor(actor.uuid)) + actor.stop + assertEquals("stopped actor registered", Nil, registry.actorsFor(actorClass)) + assertEquals("stopped actor registered", Nil, registry.actorsFor(actor.getId)) + assertEquals("stopped actor registered", None, registry.actorFor(actor.uuid)) + } + +} + +class TestActor1 extends Actor { + + // use default id + + protected def receive = null + +} + +class TestActor2 extends Actor { + + id = "customid" + + protected def receive = null + +} \ No newline at end of file From 94483e102d6f5c74a61f8b3130d08346540c0b3c Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Mon, 1 Mar 2010 06:07:32 +0100 Subject: [PATCH 03/17] changed actor URI format, cleanup unit tests. --- .../{CamelConsumer.scala => Consumer.scala} | 2 +- .../main/scala/component/ActorComponent.scala | 58 +++++------ .../src/main/scala/service/CamelService.scala | 20 ++-- .../scala/component/ActorComponentTest.scala | 70 ++++++-------- .../test/scala/service/CamelServiceTest.scala | 95 +++++++++---------- akka-core/src/test/scala/SerializerTest.scala | 6 +- .../src/main/scala/Consumer1.scala | 4 +- 7 files changed, 121 insertions(+), 134 deletions(-) rename akka-camel/src/main/scala/{CamelConsumer.scala => Consumer.scala} (94%) diff --git a/akka-camel/src/main/scala/CamelConsumer.scala b/akka-camel/src/main/scala/Consumer.scala similarity index 94% rename from akka-camel/src/main/scala/CamelConsumer.scala rename to akka-camel/src/main/scala/Consumer.scala index f3518e03f6..3dbb101292 100644 --- a/akka-camel/src/main/scala/CamelConsumer.scala +++ b/akka-camel/src/main/scala/Consumer.scala @@ -11,7 +11,7 @@ import se.scalablesolutions.akka.actor.Actor * * @author Martin Krasser */ -trait CamelConsumer { +trait Consumer { self: Actor => diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index 4c99bfd809..b85cf4829f 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -27,12 +27,13 @@ class ActorComponent extends DefaultComponent { new ActorEndpoint(uri, this, idAndUuid._1, idAndUuid._2) } - private def idAndUuidPair(remaining: String): Tuple2[String, Option[String]] = { - remaining split "/" toList match { - case id :: Nil => (id, None) - case id :: uuid :: Nil => (id, Some(uuid)) + private def idAndUuidPair(remaining: String): Tuple2[Option[String], Option[String]] = { + remaining split ":" toList match { + case id :: Nil => (Some(id), None) + case "id" :: id :: Nil => (Some(id), None) + case "uuid" :: uuid :: Nil => (None, Some(uuid)) case _ => throw new IllegalArgumentException( - "invalid path format: %s - should be [/]" format remaining) + "invalid path format: %s - should be or id: or uuid:" format remaining) } } @@ -40,19 +41,17 @@ class ActorComponent extends DefaultComponent { /** * Camel endpoint for interacting with actors. An actor can be addressed by its - * Actor.id or by an Actor.id - Actor.uuid - * combination. The URI format is actor://[/]. + * Actor.getId or its Actor.uuid combination. Supported URI formats are + * actor:<actorid>, + * actor:id:<actorid> and + * actor:uuid:<actoruuid>. * * @see se.scalablesolutions.akka.camel.component.ActorComponent * @see se.scalablesolutions.akka.camel.component.ActorProducer * @author Martin Krasser */ -class ActorEndpoint(uri: String, comp: ActorComponent, val id: String, val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { - - // TODO: clarify uuid details - // - do they change after persist/restore - // - what about remote actors and uuids +class ActorEndpoint(uri: String, comp: ActorComponent, val id: Option[String], val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { /** * @throws UnsupportedOperationException @@ -80,10 +79,10 @@ class ActorEndpoint(uri: String, comp: ActorComponent, val id: String, val uuid: */ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { - implicit val sender = Some(Sender) + implicit val sender = Some(new Sender) def process(exchange: Exchange) { - val actor = target getOrElse (throw new ActorNotRegisteredException(ep.id, ep.uuid)) + val actor = target getOrElse (throw new ActorNotRegisteredException(ep.getEndpointUri)) if (exchange.getPattern.isOutCapable) processInOut(exchange, actor) else @@ -92,6 +91,12 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { override def start { super.start + sender.get.start + } + + override def stop { + sender.get.stop + super.stop } protected def receive = { @@ -122,16 +127,17 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { } private def target: Option[Actor] = { - ActorRegistry.actorsFor(ep.id) match { - case actor :: Nil if targetMatchesUuid(actor) => Some(actor) - case Nil => None - case actors => actors find (targetMatchesUuid _) - } + if (ep.id.isDefined) targetById(ep.id.get) + else targetByUuid(ep.uuid.get) } - private def targetMatchesUuid(target: Actor): Boolean = - // if ep.uuid is not defined always return true - target.uuid == (ep.uuid getOrElse target.uuid) + private def targetById(id: String) = ActorRegistry.actorsFor(id) match { + case Nil => None + case actor :: Nil => Some(actor) + case actors => Some(actors.first) + } + + private def targetByUuid(uuid: String) = ActorRegistry.actorFor(uuid) } @@ -140,9 +146,7 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { * * @author Martin Krasser */ -private[component] object Sender extends Actor { - - start +private[component] class Sender extends Actor { /** * Ignores any message. @@ -159,8 +163,8 @@ private[component] object Sender extends Actor { * * @author Martin Krasser */ -class ActorNotRegisteredException(name: String, uuid: Option[String]) extends RuntimeException { +class ActorNotRegisteredException(uri: String) extends RuntimeException { - override def getMessage = "actor(id=%s,uuid=%s) not registered" format (name, uuid getOrElse "") + override def getMessage = "%s not registered" format uri } \ No newline at end of file diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index 2811ab88fe..98a767797b 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -10,7 +10,7 @@ import org.apache.camel.builder.RouteBuilder import se.scalablesolutions.akka.actor.{Actor, ActorRegistry} import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.CamelConsumer +import se.scalablesolutions.akka.camel.Consumer import se.scalablesolutions.akka.util.{Bootable, Logging} /** @@ -43,7 +43,7 @@ trait CamelService extends Bootable with Logging { /** * Generic route builder that searches the registry for actors that are * either annotated with @se.scalablesolutions.akka.annotation.consume or - * mixed in se.scalablesolutions.akka.camel.CamelConsumer and exposes them + * mixed in se.scalablesolutions.akka.camel.Consumer and exposes them * as Camel endpoints. * * @author Martin Krasser @@ -55,25 +55,23 @@ class CamelRouteBuilder extends RouteBuilder with Logging { // // TODO: resolve/clarify issues with ActorRegistry - // - custom Actor.id ignored - // - actor de-registration issues // - multiple registration with same id/uuid possible // // TODO: avoid redundant registrations actors.filter(isConsumeAnnotated _).foreach { actor: Actor => val fromUri = actor.getClass.getAnnotation(classOf[consume]).value() - configure(fromUri, "actor://%s" format actor.id) + configure(fromUri, "actor:id:%s" format actor.getId) log.debug("registered actor (id=%s) for consuming messages from %s " - format (actor.id, fromUri)) + format (actor.getId, fromUri)) } // TODO: avoid redundant registrations actors.filter(isConsumerInstance _).foreach { actor: Actor => - val fromUri = actor.asInstanceOf[CamelConsumer].endpointUri - configure(fromUri, "actor://%s/%s" format (actor.id, actor.uuid)) - log.debug("registered actor (id=%s, uuid=%s) for consuming messages from %s " - format (actor.id, actor.uuid, fromUri)) + val fromUri = actor.asInstanceOf[Consumer].endpointUri + configure(fromUri, "actor:uuid:%s" format actor.uuid) + log.debug("registered actor (uuid=%s) for consuming messages from %s " + format (actor.uuid, fromUri)) } } @@ -94,6 +92,6 @@ class CamelRouteBuilder extends RouteBuilder with Logging { actor.getClass.getAnnotation(classOf[consume]) ne null private def isConsumerInstance(actor: Actor) = - actor.isInstanceOf[CamelConsumer] + actor.isInstanceOf[Consumer] } diff --git a/akka-camel/src/test/scala/component/ActorComponentTest.scala b/akka-camel/src/test/scala/component/ActorComponentTest.scala index 3a06d483be..15a8aa523a 100644 --- a/akka-camel/src/test/scala/component/ActorComponentTest.scala +++ b/akka-camel/src/test/scala/component/ActorComponentTest.scala @@ -1,11 +1,10 @@ package se.scalablesolutions.akka.camel.component -import org.apache.camel.{Message, RuntimeCamelException} +import org.apache.camel.Message import org.apache.camel.impl.{SimpleRegistry, DefaultCamelContext} import org.junit._ import org.junit.Assert._ import org.scalatest.junit.JUnitSuite - import se.scalablesolutions.akka.actor.Actor /** @@ -13,45 +12,38 @@ import se.scalablesolutions.akka.actor.Actor */ class ActorComponentTest extends JUnitSuite { - import ActorComponentTestSetup._ - - val actor = ActorComponentTestActor.start - - @Test - def testMatchIdOnly() { - val result = template.requestBody("actor:%s" format actor.id, "Martin") - assertEquals("Hello Martin", result) - } - - @Test - def testMatchIdAndUuid() { - val result = template.requestBody("actor:%s/%s" format (actor.id, actor.uuid), "Martin") - assertEquals("Hello Martin", result) - } - - @Test - def testMatchIdButNotUuid() { - intercept[RuntimeCamelException] { - template.requestBody("actor:%s/%s" format (actor.id, "wrong"), "Martin") - } - } - -} - -object ActorComponentTestActor extends Actor { - - protected def receive = { - case msg: Message => reply("Hello %s" format msg.getBody) - } - -} - -object ActorComponentTestSetup { - val context = new DefaultCamelContext(new SimpleRegistry) val template = context.createProducerTemplate - context.start - template.start + @Before def setUp = { + context.start + template.start + } + @After def tearDown = { + template.stop + context.stop + } + + @Test def shouldCommunicateWithActorReferencedById = { + val actor = new ActorComponentTestActor + actor.start + assertEquals("Hello Martin", template.requestBody("actor:%s" format actor.getId, "Martin")) + assertEquals("Hello Martin", template.requestBody("actor:id:%s" format actor.getId, "Martin")) + actor.stop + } + + @Test def shouldCommunicateWithActorReferencedByUuid = { + val actor = new ActorComponentTestActor + actor.start + assertEquals("Hello Martin", template.requestBody("actor:uuid:%s" format actor.uuid, "Martin")) + actor.stop + } + +} + +class ActorComponentTestActor extends Actor { + protected def receive = { + case msg: Message => reply("Hello %s" format msg.getBody) + } } \ No newline at end of file diff --git a/akka-camel/src/test/scala/service/CamelServiceTest.scala b/akka-camel/src/test/scala/service/CamelServiceTest.scala index 8cce9ec2bd..1e00413b14 100644 --- a/akka-camel/src/test/scala/service/CamelServiceTest.scala +++ b/akka-camel/src/test/scala/service/CamelServiceTest.scala @@ -3,105 +3,98 @@ package se.scalablesolutions.akka.camel.service import org.apache.camel.Message import org.apache.camel.builder.RouteBuilder import org.apache.camel.impl.DefaultCamelContext -import org.junit.Test import org.junit.Assert._ +import org.junit.{Before, After, Test} import org.scalatest.junit.JUnitSuite -import se.scalablesolutions.akka.actor.Actor import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.CamelConsumer - +import se.scalablesolutions.akka.camel.Consumer +import se.scalablesolutions.akka.actor.Actor /** * @author Martin Krasser */ class CamelServiceTest extends JUnitSuite { - import CamelServiceTestSetup._ + import CamelContextManager.context - @Test - def testActor1() { + context = new DefaultCamelContext + context.addRoutes(new TestBuilder) + + val template = context.createProducerTemplate + var service: CamelService = _ + var actor1: Actor = _ + var actor2: Actor = _ + var actor3: Actor = _ + + @Before def setUp = { + service = new CamelService { + override def onUnload = super.onUnload + override def onLoad = super.onLoad + } + + actor1 = new TestActor1().start + actor2 = new TestActor2().start + actor3 = new TestActor3().start + + service.onLoad + template.start + + } + + @After def tearDown = { + actor1.stop + actor2.stop + actor3.stop + + template.stop + service.onUnload + } + + @Test def shouldCommunicateWithAutoDetectedActor1ViaGeneratedRoute = { val result = template.requestBody("direct:actor1", "Martin") assertEquals("Hello Martin (actor1)", result) } - @Test - def testActor2() { + @Test def shouldCommunicateWithAutoDetectedActor2ViaGeneratedRoute = { val result = template.requestBody("direct:actor2", "Martin") assertEquals("Hello Martin (actor2)", result) } - @Test - def testActor3() { + @Test def shouldCommunicateWithAutoDetectedActor3ViaCustomRoute = { val result = template.requestBody("direct:actor3", "Martin") assertEquals("Hello Tester (actor3)", result) } } -class TestActor1 extends Actor with CamelConsumer { - +class TestActor1 extends Actor with Consumer { def endpointUri = "direct:actor1" protected def receive = { case msg: Message => reply("Hello %s (actor1)" format msg.getBody) } - } @consume("direct:actor2") class TestActor2 extends Actor { - protected def receive = { case msg: Message => reply("Hello %s (actor2)" format msg.getBody) } - } class TestActor3 extends Actor { + id = "actor3" protected def receive = { case msg: Message => reply("Hello %s (actor3)" format msg.getBody) } - } class TestBuilder extends RouteBuilder { - def configure { - val actorUri = "actor://%s" format classOf[TestActor3].getName - from("direct:actor3").transform(constant("Tester")).to(actorUri) + val actorUri = "actor:%s" format classOf[TestActor3].getName + from("direct:actor3").transform(constant("Tester")).to("actor:actor3") } - -} - -object CamelServiceTestSetup extends CamelService { - - import CamelContextManager.context - - // use a custom camel context - context = new DefaultCamelContext - - val template = context.createProducerTemplate - var loaded = false - - onLoad - - override def onLoad = { - if (!loaded) { - // use a custom camel context - context.addRoutes(new TestBuilder) - // register test actors - new TestActor1().start - new TestActor2().start - new TestActor3().start - // start Camel service - super.onLoad - - template.start - loaded = true - } - } - } diff --git a/akka-core/src/test/scala/SerializerTest.scala b/akka-core/src/test/scala/SerializerTest.scala index 889dea4ba8..e11e83a2f5 100644 --- a/akka-core/src/test/scala/SerializerTest.scala +++ b/akka-core/src/test/scala/SerializerTest.scala @@ -3,7 +3,7 @@ package se.scalablesolutions.akka.serialization import junit.framework.TestCase import org.scalatest.junit.JUnitSuite -import org.junit.{Test, Before, After, Ignore} +import org.junit.{Test, Before, After} import scala.reflect.BeanInfo @BeanInfo @@ -18,7 +18,7 @@ case class MyMessage(val id: String, val value: Tuple2[String, Int]) { class SerializerTest extends JUnitSuite { - @Test @Ignore // TODO: resolve test failure + @Test def shouldSerializeString = { val f = Foo("debasish") val json = Serializer.ScalaJSON.out(f) @@ -27,7 +27,7 @@ class SerializerTest extends JUnitSuite { assert(fo == f) } - @Test @Ignore // TODO: resolve test failure + @Test def shouldSerializeTuple2 = { val message = MyMessage("id", ("hello", 34)) val json = Serializer.ScalaJSON.out(message) diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala index fd9b38a3a9..4e07866393 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala @@ -4,12 +4,12 @@ import org.apache.camel.Message import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.CamelConsumer +import se.scalablesolutions.akka.camel.Consumer /** * @author Martin Krasser */ -class Consumer1 extends Actor with CamelConsumer with Logging { +class Consumer1 extends Actor with Consumer with Logging { def endpointUri = "file:data/input" From 3b62a7a658c1e24d662914eeb03564698cc731f4 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Mon, 1 Mar 2010 13:35:11 +0100 Subject: [PATCH 04/17] use immutable messages for communication with actors --- akka-camel/src/main/scala/Message.scala | 61 +++++++++++++++++++ .../main/scala/component/ActorComponent.scala | 20 +++--- akka-camel/src/test/scala/MessageTest.scala | 24 ++++++++ .../scala/component/ActorComponentTest.scala | 25 ++++---- .../scala/component/ActorProducerTest.scala | 40 ++++++++++++ .../test/scala/service/CamelServiceTest.scala | 18 +++--- .../src/main/scala/Consumer1.scala | 6 +- .../src/main/scala/Consumer2.scala | 5 +- 8 files changed, 163 insertions(+), 36 deletions(-) create mode 100644 akka-camel/src/main/scala/Message.scala create mode 100644 akka-camel/src/test/scala/MessageTest.scala create mode 100644 akka-camel/src/test/scala/component/ActorProducerTest.scala diff --git a/akka-camel/src/main/scala/Message.scala b/akka-camel/src/main/scala/Message.scala new file mode 100644 index 0000000000..88f810e045 --- /dev/null +++ b/akka-camel/src/main/scala/Message.scala @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel + +import org.apache.camel.{Message => CamelMessage} +import org.apache.camel.impl.DefaultCamelContext + +import scala.collection.jcl.{Map => MapWrapper} + +/** + * @author Martin Krasser + */ +class Message(val body: Any, val headers: Map[String, Any]) { + + def this(body: Any) = this(body, Map.empty) + + def bodyAs[T](clazz: Class[T]): T = Message.converter.mandatoryConvertTo[T](clazz, body) + +} + +/** + * @author Martin Krasser + */ +object Message { + + val converter = new DefaultCamelContext().getTypeConverter + + def apply(body: Any) = new Message(body) + + def apply(body: Any, headers: Map[String, Any]) = new Message(body, headers) + + def apply(cm: CamelMessage) = + new Message(cm.getBody, Map.empty ++ MapWrapper[String, AnyRef](cm.getHeaders).elements) + +} + +/** + * @author Martin Krasser + */ +class CamelMessageWrapper(val cm: CamelMessage) { + + def from(m: Message): CamelMessage = { + cm.setBody(m.body) + for (h <- m.headers) { + cm.getHeaders.put(h._1, h._2.asInstanceOf[AnyRef]) + } + cm + } + +} + +/** + * @author Martin Krasser + */ +object CamelMessageWrapper { + + implicit def wrapCamelMessage(cm: CamelMessage): CamelMessageWrapper = new CamelMessageWrapper(cm) + +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index b85cf4829f..d59a4261c9 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -11,6 +11,7 @@ import org.apache.camel.{Exchange, Consumer, Processor} import org.apache.camel.impl.{DefaultProducer, DefaultEndpoint, DefaultComponent} import se.scalablesolutions.akka.actor.{ActorRegistry, Actor} +import se.scalablesolutions.akka.camel.{CamelMessageWrapper, Message} /** * Camel component for interacting with actors. @@ -104,25 +105,26 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { } protected def processInOnly(exchange: Exchange, actor: Actor) { - actor ! exchange.getIn + actor ! Message(exchange.getIn) } protected def processInOut(exchange: Exchange, actor: Actor) { - val outmsg = exchange.getOut + + import CamelMessageWrapper._ + // TODO: make timeout configurable - // TODO: send immutable message // TODO: support asynchronous communication // - jetty component: jetty continuations // - file component: completion callbacks - val result: Any = actor !! exchange.getIn + val result: Any = actor !! Message(exchange.getIn) result match { - case Some((body, headers:Map[String, Any])) => { - outmsg.setBody(body) - for (header <- headers) - outmsg.getHeaders.put(header._1, header._2.asInstanceOf[AnyRef]) + case Some(m:Message) => { + exchange.getOut.from(m) + } + case Some(body) => { + exchange.getOut.setBody(body) } - case Some(body) => outmsg.setBody(body) } } diff --git a/akka-camel/src/test/scala/MessageTest.scala b/akka-camel/src/test/scala/MessageTest.scala new file mode 100644 index 0000000000..791f243ee4 --- /dev/null +++ b/akka-camel/src/test/scala/MessageTest.scala @@ -0,0 +1,24 @@ +package se.scalablesolutions.akka.camel.service + +import java.io.InputStream + +import org.apache.camel.NoTypeConversionAvailableException +import org.junit.Assert._ +import org.junit.Test +import org.scalatest.junit.JUnitSuite + +import se.scalablesolutions.akka.camel.Message + +class MessageTest extends JUnitSuite { + + @Test def shouldConvertDoubleBodyToString = { + assertEquals("1.4", new Message(1.4, null).bodyAs(classOf[String])) + } + + @Test def shouldThrowExceptionWhenConvertingDoubleBodyToInputStream { + intercept[NoTypeConversionAvailableException] { + new Message(1.4, null).bodyAs(classOf[InputStream]) + } + } + +} \ No newline at end of file diff --git a/akka-camel/src/test/scala/component/ActorComponentTest.scala b/akka-camel/src/test/scala/component/ActorComponentTest.scala index 15a8aa523a..30ea7d1a5b 100644 --- a/akka-camel/src/test/scala/component/ActorComponentTest.scala +++ b/akka-camel/src/test/scala/component/ActorComponentTest.scala @@ -1,11 +1,12 @@ package se.scalablesolutions.akka.camel.component -import org.apache.camel.Message -import org.apache.camel.impl.{SimpleRegistry, DefaultCamelContext} import org.junit._ import org.junit.Assert._ import org.scalatest.junit.JUnitSuite import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.Message +import org.apache.camel.{CamelContext, ExchangePattern} +import org.apache.camel.impl.{DefaultExchange, SimpleRegistry, DefaultCamelContext} /** * @author Martin Krasser @@ -25,25 +26,27 @@ class ActorComponentTest extends JUnitSuite { context.stop } - @Test def shouldCommunicateWithActorReferencedById = { - val actor = new ActorComponentTestActor + @Test def shouldReceiveResponseFromActorReferencedById = { + val actor = new TestActor actor.start assertEquals("Hello Martin", template.requestBody("actor:%s" format actor.getId, "Martin")) assertEquals("Hello Martin", template.requestBody("actor:id:%s" format actor.getId, "Martin")) actor.stop } - @Test def shouldCommunicateWithActorReferencedByUuid = { - val actor = new ActorComponentTestActor + @Test def shouldReceiveResponseFromActorReferencedByUuid = { + val actor = new TestActor actor.start assertEquals("Hello Martin", template.requestBody("actor:uuid:%s" format actor.uuid, "Martin")) actor.stop } + class TestActor extends Actor { + protected def receive = { + case msg: Message => reply("Hello %s" format msg.body) + } + } + } -class ActorComponentTestActor extends Actor { - protected def receive = { - case msg: Message => reply("Hello %s" format msg.getBody) - } -} \ No newline at end of file + diff --git a/akka-camel/src/test/scala/component/ActorProducerTest.scala b/akka-camel/src/test/scala/component/ActorProducerTest.scala new file mode 100644 index 0000000000..73e51ebb04 --- /dev/null +++ b/akka-camel/src/test/scala/component/ActorProducerTest.scala @@ -0,0 +1,40 @@ +package se.scalablesolutions.akka.camel.component + +import org.apache.camel.{CamelContext, ExchangePattern} +import org.junit.Assert._ +import org.junit.Test +import org.scalatest.junit.JUnitSuite + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.Message +import org.apache.camel.impl.{DefaultCamelContext, DefaultExchange} + +/** + * @author Martin Krasser + */ +class ActorProducerTest extends JUnitSuite { + + val context = new DefaultCamelContext + val endpoint = context.getEndpoint("actor:%s" format classOf[TestActor].getName) + val producer = endpoint.createProducer + + @Test def shouldSendAndReceiveMessageBodyAndHeaders = { + val exchange = new DefaultExchange(null.asInstanceOf[CamelContext], ExchangePattern.InOut) + val actor = new TestActor + actor.start + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + producer.process(exchange) + assertEquals("Hello Martin", exchange.getOut.getBody) + assertEquals("v1", exchange.getOut.getHeader("k1")) + assertEquals("v2", exchange.getOut.getHeader("k2")) + actor.stop + } + + class TestActor extends Actor { + protected def receive = { + case msg: Message => reply(Message("Hello %s" format msg.body, Map("k2" -> "v2") ++ msg.headers)) + } + } + +} diff --git a/akka-camel/src/test/scala/service/CamelServiceTest.scala b/akka-camel/src/test/scala/service/CamelServiceTest.scala index 1e00413b14..52f6d1fd04 100644 --- a/akka-camel/src/test/scala/service/CamelServiceTest.scala +++ b/akka-camel/src/test/scala/service/CamelServiceTest.scala @@ -1,15 +1,14 @@ package se.scalablesolutions.akka.camel.service -import org.apache.camel.Message import org.apache.camel.builder.RouteBuilder import org.apache.camel.impl.DefaultCamelContext import org.junit.Assert._ import org.junit.{Before, After, Test} import org.scalatest.junit.JUnitSuite -import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.Consumer import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.{Message, Consumer} /** * @author Martin Krasser @@ -51,17 +50,17 @@ class CamelServiceTest extends JUnitSuite { service.onUnload } - @Test def shouldCommunicateWithAutoDetectedActor1ViaGeneratedRoute = { + @Test def shouldReceiveResponseFromActor1ViaGeneratedRoute = { val result = template.requestBody("direct:actor1", "Martin") assertEquals("Hello Martin (actor1)", result) } - @Test def shouldCommunicateWithAutoDetectedActor2ViaGeneratedRoute = { + @Test def shouldReceiveResponseFromActor2ViaGeneratedRoute = { val result = template.requestBody("direct:actor2", "Martin") assertEquals("Hello Martin (actor2)", result) } - @Test def shouldCommunicateWithAutoDetectedActor3ViaCustomRoute = { + @Test def shouldReceiveResponseFromActor3ViaCustomRoute = { val result = template.requestBody("direct:actor3", "Martin") assertEquals("Hello Tester (actor3)", result) } @@ -72,14 +71,15 @@ class TestActor1 extends Actor with Consumer { def endpointUri = "direct:actor1" protected def receive = { - case msg: Message => reply("Hello %s (actor1)" format msg.getBody) + case msg: Message => reply("Hello %s (actor1)" format msg.body) } + } @consume("direct:actor2") class TestActor2 extends Actor { protected def receive = { - case msg: Message => reply("Hello %s (actor2)" format msg.getBody) + case msg: Message => reply("Hello %s (actor2)" format msg.body) } } @@ -87,7 +87,7 @@ class TestActor3 extends Actor { id = "actor3" protected def receive = { - case msg: Message => reply("Hello %s (actor3)" format msg.getBody) + case msg: Message => reply("Hello %s (actor3)" format msg.body) } } diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala index 4e07866393..b292d6e186 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala @@ -1,10 +1,8 @@ package sample.camel -import org.apache.camel.Message - import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.Consumer +import se.scalablesolutions.akka.camel.{Message, Consumer} /** * @author Martin Krasser @@ -14,7 +12,7 @@ class Consumer1 extends Actor with Consumer with Logging { def endpointUri = "file:data/input" def receive = { - case msg: Message => log.info("received %s" format msg.getBody(classOf[String])) + case msg: Message => log.info("received %s" format msg.bodyAs(classOf[String])) } } \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala index aa9cd5e612..4940c46f0d 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala @@ -1,9 +1,8 @@ package sample.camel -import org.apache.camel.Message - import se.scalablesolutions.akka.actor.Actor import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.Message /** * @author Martin Krasser @@ -12,7 +11,7 @@ import se.scalablesolutions.akka.annotation.consume class Consumer2 extends Actor { def receive = { - case msg: Message => reply("Hello %s" format msg.getBody(classOf[String])) + case msg: Message => reply("Hello %s" format msg.bodyAs(classOf[String])) } } \ No newline at end of file From c0b41379e6ce3cfb07a0cc4db4ba4c4102b7d7eb Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Fri, 5 Mar 2010 19:38:23 +0100 Subject: [PATCH 05/17] Producer trait for producing messages to Camel endpoints (sync/async, oneway/twoway), Immutable representation of Camel message, consumer/producer examples, refactorings/improvements/cleanups. --- .../main/scala/CamelContextLifecycle.scala | 97 ++++++++ akka-camel/src/main/scala/Consumer.scala | 2 +- akka-camel/src/main/scala/Message.scala | 225 ++++++++++++++++-- akka-camel/src/main/scala/Producer.scala | 199 ++++++++++++++++ .../main/scala/component/ActorComponent.scala | 95 +++----- .../scala/service/CamelContextManager.scala | 23 -- .../src/main/scala/service/CamelService.scala | 33 ++- akka-camel/src/test/scala/MessageTest.scala | 65 ++++- akka-camel/src/test/scala/ProducerTest.scala | 108 +++++++++ .../scala/component/ActorComponentTest.scala | 23 +- .../scala/component/ActorProducerTest.scala | 10 +- .../test/scala/service/CamelServiceTest.scala | 52 ++-- .../src/main/scala/Boot.scala | 26 +- .../src/main/scala/Consumer3.scala | 17 ++ .../src/main/scala/Producer1.scala | 17 ++ .../src/main/scala/Transformer.scala | 15 ++ 16 files changed, 822 insertions(+), 185 deletions(-) create mode 100644 akka-camel/src/main/scala/CamelContextLifecycle.scala create mode 100644 akka-camel/src/main/scala/Producer.scala delete mode 100644 akka-camel/src/main/scala/service/CamelContextManager.scala create mode 100644 akka-camel/src/test/scala/ProducerTest.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Producer1.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Transformer.scala diff --git a/akka-camel/src/main/scala/CamelContextLifecycle.scala b/akka-camel/src/main/scala/CamelContextLifecycle.scala new file mode 100644 index 0000000000..68e67a0612 --- /dev/null +++ b/akka-camel/src/main/scala/CamelContextLifecycle.scala @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel + +import org.apache.camel.{ProducerTemplate, CamelContext} +import org.apache.camel.impl.DefaultCamelContext + +import se.scalablesolutions.akka.util.Logging + +/** + * Defines the lifecycle of a CamelContext. Allowed state transitions are + * init -> start -> stop -> init -> ... etc. + * + * @author Martin Krasser + */ +trait CamelContextLifecycle extends Logging { + // TODO: enforce correct state transitions + // valid: init -> start -> stop -> init ... + + private var _context: CamelContext = _ + private var _template: ProducerTemplate = _ + + private var _initialized = false + private var _started = false + + /** + * Returns the managed CamelContext. + */ + protected def context: CamelContext = _context + + /** + * Returns the managed ProducerTemplate. + */ + protected def template: ProducerTemplate = _template + + /** + * Sets the managed CamelContext. + */ + protected def context_= (context: CamelContext) { _context = context } + + /** + * Sets the managed ProducerTemplate. + */ + protected def template_= (template: ProducerTemplate) { _template = template } + + def initialized = _initialized + def started = _started + + /** + * Starts the CamelContext and ProducerTemplate. + */ + def start() { + context.start + template.start + _started = true + log.info("Camel context started") + } + + /** + * Stops the CamelContext and ProducerTemplate. + */ + def stop() { + template.stop + context.stop + _initialized = false + _started = false + log.info("Camel context stopped") + } + + /** + * Initializes this lifecycle object with the a DefaultCamelContext. + */ + def init() { + init(new DefaultCamelContext) + } + + /** + * Initializes this lifecycle object with the given CamelContext. + */ + def init(context: CamelContext) { + this.context = context + this.template = context.createProducerTemplate + _initialized = true + log.info("Camel context initialized") + } +} + +/** + * Makes a global CamelContext and ProducerTemplate accessible to applications. The lifecycle + * of these objects is managed by se.scalablesolutions.akka.camel.service.CamelService. + */ +object CamelContextManager extends CamelContextLifecycle { + override def context: CamelContext = super.context + override def template: ProducerTemplate = super.template +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/Consumer.scala b/akka-camel/src/main/scala/Consumer.scala index 3dbb101292..1a3003d863 100644 --- a/akka-camel/src/main/scala/Consumer.scala +++ b/akka-camel/src/main/scala/Consumer.scala @@ -7,7 +7,7 @@ package se.scalablesolutions.akka.camel import se.scalablesolutions.akka.actor.Actor /** - * Mixed in by Actor subclasses to be Camel endpoint consumers. + * Mixed in by Actor implementations that consume message from Camel endpoints. * * @author Martin Krasser */ diff --git a/akka-camel/src/main/scala/Message.scala b/akka-camel/src/main/scala/Message.scala index 88f810e045..db23868fac 100644 --- a/akka-camel/src/main/scala/Message.scala +++ b/akka-camel/src/main/scala/Message.scala @@ -4,58 +4,241 @@ package se.scalablesolutions.akka.camel -import org.apache.camel.{Message => CamelMessage} -import org.apache.camel.impl.DefaultCamelContext +import org.apache.camel.{Exchange, Message => CamelMessage} +import org.apache.camel.util.ExchangeHelper import scala.collection.jcl.{Map => MapWrapper} /** + * An immutable representation of a Camel message. Actor classes that mix in + * se.scalablesolutions.akka.camel.Producer or + * se.scalablesolutions.akka.camel.Consumer use this message type for communication. + * * @author Martin Krasser */ -class Message(val body: Any, val headers: Map[String, Any]) { - +case class Message(val body: Any, val headers: Map[String, Any]) { + /** + * Creates a message with a body and an empty header map. + */ def this(body: Any) = this(body, Map.empty) - def bodyAs[T](clazz: Class[T]): T = Message.converter.mandatoryConvertTo[T](clazz, body) + /** + * Returns the body of the message converted to the type given by the clazz + * argument. Conversion is done using Camel's type converter. The type converter is obtained + * from the CamelContext managed by CamelContextManager. Applications have to ensure proper + * initialization of CamelContextManager. + * + * @see CamelContextManager. + */ + def bodyAs[T](clazz: Class[T]): T = { + CamelContextManager.context.getTypeConverter.mandatoryConvertTo[T](clazz, body) + } + /** + * Returns those headers from this message whose name is contained in names. + */ + def headers(names: Set[String]): Map[String, Any] = { + headers.filter(names contains _._1) + } + + /** + * Creates a Message with a new body using a transformer function. + */ + def transformBody[A](transformer: A => Any): Message = setBody(transformer(body.asInstanceOf[A])) + + /** + * Creates a Message with a new body converted to type clazz. + * + * @see Message#bodyAs(Class) + */ + def setBodyAs[T](clazz: Class[T]): Message = setBody(bodyAs(clazz)) + + /** + * Creates a Message with a new body. + */ + def setBody(body: Any) = new Message(body, this.headers) + + /** + * Creates a new Message with new headers. + */ + def setHeaders(headers: Map[String, Any]) = new Message(this.body, headers) + + /** + * Creates a new Message with the headers argument added to the existing headers. + */ + def addHeaders(headers: Map[String, Any]) = new Message(this.body, this.headers ++ headers) + + /** + * Creates a new Message with the header argument added to the existing headers. + */ + def addHeader(header: (String, Any)) = new Message(this.body, this.headers + header) + + /** + * Creates a new Message where the header with name headerName is removed from + * the existing headers. + */ + def removeHeader(headerName: String) = new Message(this.body, this.headers - headerName) } /** + * Companion object of Message class. + * * @author Martin Krasser */ object Message { + /** + * Message header to correlate request with response messages. Applications that send + * messages to a Producer actor may want to set this header on the request message + * so that it can be correlated with an asynchronous response. Messages send to Consumer + * actors have this header already set. + */ + val MessageExchangeId = "MessageExchangeId" - val converter = new DefaultCamelContext().getTypeConverter - + /** + * Creates a new Message with body as message body and an empty header map. + */ def apply(body: Any) = new Message(body) - def apply(body: Any, headers: Map[String, Any]) = new Message(body, headers) + /** + * Creates a canonical form of the given message msg. If msg of type + * Message then msg is returned, otherwise msg is set as body of a + * newly created Message object. + */ + def canonicalize(msg: Any) = msg match { + case mobj: Message => mobj + case body => new Message(body) + } +} - def apply(cm: CamelMessage) = - new Message(cm.getBody, Map.empty ++ MapWrapper[String, AnyRef](cm.getHeaders).elements) +/** + * An immutable representation of a failed Camel exchange. It contains the failure cause + * obtained from Exchange.getException and the headers from either the Exchange.getIn + * message or Exchange.getOut message, depending on the exchange pattern. + * + * @author Martin Krasser + */ +case class Failure(val cause: Throwable, val headers: Map[String, Any]) + +/** + * Adapter for converting an org.apache.camel.Exchange to and from Message and Failure objects. + * + * @author Martin Krasser + */ +class CamelExchangeAdapter(exchange: Exchange) { + + import CamelMessageConversion.toMessageAdapter + + /** + * Sets Exchange.getIn from the given Message object. + */ + def fromRequestMessage(msg: Message): Exchange = { requestMessage.fromMessage(msg); exchange } + + /** + * Depending on the exchange pattern, sets Exchange.getIn or Exchange.getOut from the given + * Message object. If the exchange is out-capable then the Exchange.getOut is set, otherwise + * Exchange.getIn. + */ + def fromResponseMessage(msg: Message): Exchange = { responseMessage.fromMessage(msg); exchange } + + /** + * Creates a Message object from Exchange.getIn. + */ + def toRequestMessage: Message = toRequestMessage(Map.empty) + + /** + * Depending on the exchange pattern, creates a Message object from Exchange.getIn or Exchange.getOut. + * If the exchange is out-capable then the Exchange.getOut is set, otherwise Exchange.getIn. + */ + def toResponseMessage: Message = toResponseMessage(Map.empty) + + /** + * Creates a Failure object from the adapted Exchange. + * + * @see Failure + */ + def toFailureMessage: Failure = toFailureMessage(Map.empty) + + /** + * Creates a Message object from Exchange.getIn. + * + * @param headers additional headers to set on the created Message in addition to those + * in the Camel message. + */ + def toRequestMessage(headers: Map[String, Any]): Message = requestMessage.toMessage(headers) + + /** + * Depending on the exchange pattern, creates a Message object from Exchange.getIn or Exchange.getOut. + * If the exchange is out-capable then the Exchange.getOut is set, otherwise Exchange.getIn. + * + * @param headers additional headers to set on the created Message in addition to those + * in the Camel message. + */ + def toResponseMessage(headers: Map[String, Any]): Message = responseMessage.toMessage(headers) + + /** + * Creates a Failure object from the adapted Exchange. + * + * @param headers additional headers to set on the created Message in addition to those + * in the Camel message. + * + * @see Failure + */ + def toFailureMessage(headers: Map[String, Any]): Failure = Failure(exchange.getException, headers ++ responseMessage.toMessage.headers) + + private def requestMessage = exchange.getIn + + private def responseMessage = ExchangeHelper.getResultMessage(exchange) } /** - * @author Martin Krasser + * Adapter for converting an org.apache.camel.Message to and from Message objects. + * + * @author Martin Krasser */ -class CamelMessageWrapper(val cm: CamelMessage) { - - def from(m: Message): CamelMessage = { +class CamelMessageAdapter(val cm: CamelMessage) { + /** + * Set the adapted Camel message from the given Message object. + */ + def fromMessage(m: Message): CamelMessage = { cm.setBody(m.body) - for (h <- m.headers) { - cm.getHeaders.put(h._1, h._2.asInstanceOf[AnyRef]) - } + for (h <- m.headers) cm.getHeaders.put(h._1, h._2.asInstanceOf[AnyRef]) cm } + /** + * Creates a new Message object from the adapted Camel message. + */ + def toMessage: Message = toMessage(Map.empty) + + /** + * Creates a new Message object from the adapted Camel message. + * + * @param headers additional headers to set on the created Message in addition to those + * in the Camel message. + */ + def toMessage(headers: Map[String, Any]): Message = { + Message(cm.getBody, cmHeaders(headers, cm)) + } + + private def cmHeaders(headers: Map[String, Any], cm: CamelMessage) = { + headers ++ MapWrapper[String, AnyRef](cm.getHeaders).elements + } + } /** - * @author Martin Krasser + * Defines conversion methods to CamelExchangeAdapter and CamelMessageAdapter. Imported by applications + * that implicitly want to use conversion methods of CamelExchangeAdapter and CamelMessageAdapter. */ -object CamelMessageWrapper { - - implicit def wrapCamelMessage(cm: CamelMessage): CamelMessageWrapper = new CamelMessageWrapper(cm) +object CamelMessageConversion { + /** + * Creates an CamelExchangeAdapter for the given Camel exchange. + */ + implicit def toExchangeAdapter(ce: Exchange): CamelExchangeAdapter = new CamelExchangeAdapter(ce) + /** + * Creates an CamelMessageAdapter for the given Camel message. + */ + implicit def toMessageAdapter(cm: CamelMessage): CamelMessageAdapter = new CamelMessageAdapter(cm) } \ No newline at end of file diff --git a/akka-camel/src/main/scala/Producer.scala b/akka-camel/src/main/scala/Producer.scala new file mode 100644 index 0000000000..326f208cfc --- /dev/null +++ b/akka-camel/src/main/scala/Producer.scala @@ -0,0 +1,199 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.camel + +import CamelMessageConversion.toExchangeAdapter + +import org.apache.camel.{Processor, ExchangePattern, Exchange, ProducerTemplate} +import org.apache.camel.impl.DefaultExchange +import org.apache.camel.spi.Synchronization + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.dispatch.CompletableFutureResult +import se.scalablesolutions.akka.util.Logging + +/** + * Mixed in by Actor implementations that produce messages to Camel endpoints. + * + * @author Martin Krasser + */ +trait Producer { + + self: Actor => + + /** + * If set to true (default), communication with the Camel endpoint is done via the Camel + * Async API. Camel then processes the + * message in a separate thread. If set to false, the actor thread is blocked until Camel + * has finished processing the produced message. + */ + def async: Boolean = true + + /** + * If set to false (default), this producer expects a response message from the Camel endpoint. + * If set to true, this producer communicates with the Camel endpoint with an in-only message + * exchange pattern (fire and forget). + */ + def oneway: Boolean = false + + /** + * Returns the Camel endpoint URI to produce messages to. + */ + def endpointUri: String + + /** + * Returns the names of message headers to copy from a request message to a response message. + * By default only the Message.MessageExchangeId is copied. Applications may override this to + * define an application-specific set of message headers to copy. + */ + def headersToCopy: Set[String] = Set(Message.MessageExchangeId) + + /** + * Returns the producer template from the CamelContextManager. Applications either have to ensure + * proper initialization of CamelContextManager or override this method. + * + * @see CamelContextManager. + */ + protected def template: ProducerTemplate = CamelContextManager.template + + /** + * Initiates a one-way (in-only) message exchange to the Camel endpoint given by + * endpointUri. This method blocks until Camel finishes processing + * the message exchange. + * + * @param msg: the message to produce. The message is converted to its canonical + * representation via Message.canonicalize. + */ + protected def produceOneway(msg: Any): Unit = { + template.send(endpointUri, createInOnlyExchange.fromRequestMessage(Message.canonicalize(msg))) + } + + /** + * Initiates a one-way (in-only) message exchange to the Camel endpoint given by + * endpointUri. This method triggers asynchronous processing of the + * message exchange by Camel. + * + * @param msg: the message to produce. The message is converted to its canonical + * representation via Message.canonicalize. + */ + protected def produceOnewayAsync(msg: Any): Unit = { + template.asyncSend(endpointUri, createInOnlyExchange.fromRequestMessage(Message.canonicalize(msg))) + } + + /** + * Initiates a two-way (in-out) message exchange to the Camel endpoint given by + * endpointUri. This method blocks until Camel finishes processing + * the message exchange. + * + * @param msg: the message to produce. The message is converted to its canonical + * representation via Message.canonicalize. + * @return either a response Message or a Failure object. + */ + protected def produce(msg: Any): Any = { + val cmsg = Message.canonicalize(msg) + val requestProcessor = new Processor() { + def process(exchange: Exchange) = exchange.fromRequestMessage(cmsg) + } + val result = template.request(endpointUri, requestProcessor) + if (result.isFailed) + result.toFailureMessage(cmsg.headers(headersToCopy)) + else + result.toResponseMessage(cmsg.headers(headersToCopy)) + } + + /** + * Initiates a two-way (in-out) message exchange to the Camel endpoint given by + * endpointUri. This method triggers asynchronous processing of the + * message exchange by Camel. The response message is returned asynchronously to + * the original sender (or sender future). + * + * @param msg: the message to produce. The message is converted to its canonical + * representation via Message.canonicalize. + * @return either a response Message or a Failure object. + * @see ProducerResponseSender + */ + protected def produceAsync(msg: Any): Unit = { + val cmsg = Message.canonicalize(msg) + val sync = new ProducerResponseSender(cmsg.headers(headersToCopy), this.sender, this.senderFuture, this) + template.asyncCallback(endpointUri, createInOutExchange.fromRequestMessage(cmsg), sync) + } + + /** + * Default implementation for Actor.receive. Implementors may choose to + * def receive = produce. This partial function calls one of + * the protected produce methods depending on the return values of + * oneway and async. + */ + protected def produce: PartialFunction[Any, Unit] = { + case msg => { + if ( oneway && !async) produceOneway(msg) + else if ( oneway && async) produceOnewayAsync(msg) + else if (!oneway && !async) reply(produce(msg)) + else /*(!oneway && async)*/ produceAsync(msg) + } + } + + /** + * Creates a new in-only Exchange. + */ + protected def createInOnlyExchange: Exchange = createExchange(ExchangePattern.InOnly) + + /** + * Creates a new in-out Exchange. + */ + protected def createInOutExchange: Exchange = createExchange(ExchangePattern.InOut) + + /** + * Creates a new Exchange with given pattern from the CamelContext managed by + * CamelContextManager. Applications either have to ensure proper initialization + * of CamelContextManager or override this method. + * + * @see CamelContextManager. + */ + protected def createExchange(pattern: ExchangePattern): Exchange = { + new DefaultExchange(CamelContextManager.context, pattern) + } +} + +/** + * Synchronization object that sends responses asynchronously to initial senders. This + * class is used by Producer for asynchronous two-way messaging with a Camel endpoint. + * + * @author Martin Krasser + */ +class ProducerResponseSender( + headers: Map[String, Any], + sender: Option[Actor], + senderFuture: Option[CompletableFutureResult], + producer: Actor) extends Synchronization with Logging { + + implicit val producerActor = Some(producer) // the response sender + + /** + * Replies a Failure message, created from the given exchange, to sender (or + * senderFuture if applicable). + */ + def onFailure(exchange: Exchange) = { + reply(exchange.toFailureMessage(headers)) + } + + /** + * Replies a response Message, created from the given exchange, to sender (or + * senderFuture if applicable). + */ + def onComplete(exchange: Exchange) = { + reply(exchange.toResponseMessage(headers)) + } + + private def reply(message: Any) = { + sender match { + case Some(actor) => actor ! message + case None => senderFuture match { + case Some(future) => future.completeWithResult(message) + case None => log.warning("no destination for sending response") + } + } + } +} diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index d59a4261c9..71db14021a 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -11,10 +11,10 @@ import org.apache.camel.{Exchange, Consumer, Processor} import org.apache.camel.impl.{DefaultProducer, DefaultEndpoint, DefaultComponent} import se.scalablesolutions.akka.actor.{ActorRegistry, Actor} -import se.scalablesolutions.akka.camel.{CamelMessageWrapper, Message} +import se.scalablesolutions.akka.camel.{CamelMessageConversion, Message} /** - * Camel component for interacting with actors. + * Camel component for sending messages to and receiving replies from actors. * * @see se.scalablesolutions.akka.camel.component.ActorEndpoint * @see se.scalablesolutions.akka.camel.component.ActorProducer @@ -22,7 +22,6 @@ import se.scalablesolutions.akka.camel.{CamelMessageWrapper, Message} * @author Martin Krasser */ class ActorComponent extends DefaultComponent { - def createEndpoint(uri: String, remaining: String, parameters: JavaMap[String, Object]): ActorEndpoint = { val idAndUuid = idAndUuidPair(remaining) new ActorEndpoint(uri, this, idAndUuid._1, idAndUuid._2) @@ -37,12 +36,12 @@ class ActorComponent extends DefaultComponent { "invalid path format: %s - should be or id: or uuid:" format remaining) } } - } /** - * Camel endpoint for interacting with actors. An actor can be addressed by its - * Actor.getId or its Actor.uuid combination. Supported URI formats are + * Camel endpoint for referencing an actor. The actor reference is given by the endpoint URI. + * An actor can be referenced by its Actor.getId or its Actor.uuid. + * Supported endpoint URI formats are * actor:<actorid>, * actor:id:<actorid> and * actor:uuid:<actoruuid>. @@ -53,25 +52,27 @@ class ActorComponent extends DefaultComponent { * @author Martin Krasser */ class ActorEndpoint(uri: String, comp: ActorComponent, val id: Option[String], val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { - /** * @throws UnsupportedOperationException */ def createConsumer(processor: Processor): Consumer = throw new UnsupportedOperationException("actor consumer not supported yet") + /** + * Creates a new ActorProducer instance initialized with this endpoint. + */ def createProducer: ActorProducer = new ActorProducer(this) + /** + * Returns true. + */ def isSingleton: Boolean = true - } /** * Sends the in-message of an exchange to an actor. If the exchange pattern is out-capable, * the producer waits for a reply (using the !! operator), otherwise the ! operator is used - * for sending the message. Asynchronous communication is not implemented yet but will be - * added for Camel components that support the Camel Async API (like the jetty component that - * makes use of Jetty continuations). + * for sending the message. * * @see se.scalablesolutions.akka.camel.component.ActorComponent * @see se.scalablesolutions.akka.camel.component.ActorEndpoint @@ -79,9 +80,17 @@ class ActorEndpoint(uri: String, comp: ActorComponent, val id: Option[String], v * @author Martin Krasser */ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { + import CamelMessageConversion.toExchangeAdapter - implicit val sender = Some(new Sender) + implicit val sender = None + /** + * Depending on the exchange pattern, this method either calls processInOut or + * processInOnly for interacting with an actor. This methods looks up the actor + * from the ActorRegistry according to this producer's endpoint URI. + * + * @param exchange represents the message exchange with the actor. + */ def process(exchange: Exchange) { val actor = target getOrElse (throw new ActorNotRegisteredException(ep.getEndpointUri)) if (exchange.getPattern.isOutCapable) @@ -90,40 +99,29 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { processInOnly(exchange, actor) } - override def start { - super.start - sender.get.start - } - - override def stop { - sender.get.stop - super.stop - } - - protected def receive = { - throw new UnsupportedOperationException - } - + /** + * Send the exchange in-message to the given actor using the ! operator. The message + * send to the actor is of type se.scalablesolutions.akka.camel.Message. + */ protected def processInOnly(exchange: Exchange, actor: Actor) { - actor ! Message(exchange.getIn) + actor ! exchange.toRequestMessage(Map(Message.MessageExchangeId -> exchange.getExchangeId)) } + /** + * Send the exchange in-message to the given actor using the !! operator. The exchange + * out-message is populated from the actor's reply message. The message sent to the + * actor is of type se.scalablesolutions.akka.camel.Message. + */ protected def processInOut(exchange: Exchange, actor: Actor) { - import CamelMessageWrapper._ - - // TODO: make timeout configurable // TODO: support asynchronous communication - // - jetty component: jetty continuations - // - file component: completion callbacks - val result: Any = actor !! Message(exchange.getIn) + val result: Any = actor !! exchange.toRequestMessage(Map(Message.MessageExchangeId -> exchange.getExchangeId)) result match { - case Some(m:Message) => { - exchange.getOut.from(m) - } - case Some(body) => { - exchange.getOut.setBody(body) + case Some(msg) => exchange.fromResponseMessage(Message.canonicalize(msg)) + case None => { + // TODO: handle timeout properly + // TODO: make timeout configurable } } } @@ -140,33 +138,14 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { } private def targetByUuid(uuid: String) = ActorRegistry.actorFor(uuid) - } /** - * Generic message sender used by ActorProducer. - * - * @author Martin Krasser - */ -private[component] class Sender extends Actor { - - /** - * Ignores any message. - */ - protected def receive = { - case _ => { /* ignore any reply */ } - } - -} - -/** - * Thrown to indicate that an actor referenced by an endpoint URI cannot be + * Thrown to indicate that an actor referenced by an endpoint URI cannot be * found in the ActorRegistry. * * @author Martin Krasser */ class ActorNotRegisteredException(uri: String) extends RuntimeException { - override def getMessage = "%s not registered" format uri - } \ No newline at end of file diff --git a/akka-camel/src/main/scala/service/CamelContextManager.scala b/akka-camel/src/main/scala/service/CamelContextManager.scala deleted file mode 100644 index a6f84c158c..0000000000 --- a/akka-camel/src/main/scala/service/CamelContextManager.scala +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ - -package se.scalablesolutions.akka.camel.service - -import org.apache.camel.CamelContext -import org.apache.camel.impl.DefaultCamelContext - -/** - * Manages the CamelContext used by CamelService. - * - * @author Martin Krasser - */ -object CamelContextManager { - - /** - * The CamelContext used by CamelService. Can be modified by applications prior to - * loading the CamelService. - */ - var context: CamelContext = new DefaultCamelContext - -} \ No newline at end of file diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index 98a767797b..aa4a54ef12 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -10,11 +10,15 @@ import org.apache.camel.builder.RouteBuilder import se.scalablesolutions.akka.actor.{Actor, ActorRegistry} import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.Consumer import se.scalablesolutions.akka.util.{Bootable, Logging} +import se.scalablesolutions.akka.camel.{CamelContextManager, Consumer} /** - * Started by the Kernel to expose actors as Camel endpoints. + * Started by the Kernel to expose certain actors as Camel endpoints. It uses + * se.scalablesolutions.akka.camel.CamelContextManage to create and manage the + * lifecycle of a global CamelContext. This class further uses the + * se.scalablesolutions.akka.camel.service.CamelServiceRouteBuilder to implement + * routes from Camel endpoints to actors. * * @see CamelRouteBuilder * @@ -22,42 +26,35 @@ import se.scalablesolutions.akka.util.{Bootable, Logging} */ trait CamelService extends Bootable with Logging { - import CamelContextManager.context + import CamelContextManager._ abstract override def onLoad = { super.onLoad - context.addRoutes(new CamelRouteBuilder) + if (!initialized) init() + context.addRoutes(new CamelServiceRouteBuilder) context.setStreamCaching(true) - context.start - log.info("Camel context started") + start() } abstract override def onUnload = { + stop() super.onUnload - context.stop - log.info("Camel context stopped") } } /** - * Generic route builder that searches the registry for actors that are - * either annotated with @se.scalablesolutions.akka.annotation.consume or - * mixed in se.scalablesolutions.akka.camel.Consumer and exposes them - * as Camel endpoints. + * Implements routes from Camel endpoints to actors. It searches the registry for actors + * that are either annotated with @se.scalablesolutions.akka.annotation.consume or mix in + * se.scalablesolutions.akka.camel.Consumer and exposes them as Camel endpoints. * * @author Martin Krasser */ -class CamelRouteBuilder extends RouteBuilder with Logging { +class CamelServiceRouteBuilder extends RouteBuilder with Logging { def configure = { val actors = ActorRegistry.actors - // - // TODO: resolve/clarify issues with ActorRegistry - // - multiple registration with same id/uuid possible - // - // TODO: avoid redundant registrations actors.filter(isConsumeAnnotated _).foreach { actor: Actor => val fromUri = actor.getClass.getAnnotation(classOf[consume]).value() diff --git a/akka-camel/src/test/scala/MessageTest.scala b/akka-camel/src/test/scala/MessageTest.scala index 791f243ee4..850119b5aa 100644 --- a/akka-camel/src/test/scala/MessageTest.scala +++ b/akka-camel/src/test/scala/MessageTest.scala @@ -1,24 +1,79 @@ -package se.scalablesolutions.akka.camel.service +package se.scalablesolutions.akka.camel import java.io.InputStream import org.apache.camel.NoTypeConversionAvailableException import org.junit.Assert._ -import org.junit.Test import org.scalatest.junit.JUnitSuite -import se.scalablesolutions.akka.camel.Message +import org.junit.Test class MessageTest extends JUnitSuite { + // + // TODO: extend/rewrite unit tests + // These tests currently only ensure proper functioning of basic features. + // + @Test def shouldConvertDoubleBodyToString = { - assertEquals("1.4", new Message(1.4, null).bodyAs(classOf[String])) + CamelContextManager.init() + assertEquals("1.4", Message(1.4, null).bodyAs(classOf[String])) } @Test def shouldThrowExceptionWhenConvertingDoubleBodyToInputStream { + CamelContextManager.init() intercept[NoTypeConversionAvailableException] { - new Message(1.4, null).bodyAs(classOf[InputStream]) + Message(1.4, null).bodyAs(classOf[InputStream]) } } + @Test def shouldReturnSubsetOfHeaders = { + val message = Message("test" , Map("A" -> "1", "B" -> "2")) + assertEquals(Map("B" -> "2"), message.headers(Set("B"))) + } + + @Test def shouldTransformBodyAndPreserveHeaders = { + assertEquals( + Message("ab", Map("A" -> "1")), + Message("a" , Map("A" -> "1")).transformBody[String](body => body + "b")) + } + + @Test def shouldConvertBodyAndPreserveHeaders = { + CamelContextManager.init() + assertEquals( + Message("1.4", Map("A" -> "1")), + Message(1.4 , Map("A" -> "1")).setBodyAs(classOf[String])) + } + + @Test def shouldSetBodyAndPreserveHeaders = { + assertEquals( + Message("test2" , Map("A" -> "1")), + Message("test1" , Map("A" -> "1")).setBody("test2")) + } + + @Test def shouldSetHeadersAndPreserveBody = { + assertEquals( + Message("test1" , Map("C" -> "3")), + Message("test1" , Map("A" -> "1")).setHeaders(Map("C" -> "3"))) + + } + + @Test def shouldAddHeaderAndPreserveBodyAndHeaders = { + assertEquals( + Message("test1" , Map("A" -> "1", "B" -> "2")), + Message("test1" , Map("A" -> "1")).addHeader("B" -> "2")) + } + + @Test def shouldAddHeadersAndPreserveBodyAndHeaders = { + assertEquals( + Message("test1" , Map("A" -> "1", "B" -> "2")), + Message("test1" , Map("A" -> "1")).addHeaders(Map("B" -> "2"))) + } + + @Test def shouldRemoveHeadersAndPreserveBodyAndRemainingHeaders = { + assertEquals( + Message("test1" , Map("A" -> "1")), + Message("test1" , Map("A" -> "1", "B" -> "2")).removeHeader("B")) + } + } \ No newline at end of file diff --git a/akka-camel/src/test/scala/ProducerTest.scala b/akka-camel/src/test/scala/ProducerTest.scala new file mode 100644 index 0000000000..1a69316836 --- /dev/null +++ b/akka-camel/src/test/scala/ProducerTest.scala @@ -0,0 +1,108 @@ +package se.scalablesolutions.akka.camel + +import org.apache.camel.{Exchange, Processor} +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.component.mock.MockEndpoint +import org.junit.Assert._ +import org.junit.{Test, After, Before} +import org.scalatest.junit.JUnitSuite + +import se.scalablesolutions.akka.actor.Actor + +class ProducerTest extends JUnitSuite { + + // + // TODO: extend/rewrite unit tests + // These tests currently only ensure proper functioning of basic features. + // + + import CamelContextManager._ + + var mock: MockEndpoint = _ + + @Before def setUp = { + init() + context.addRoutes(new TestRouteBuilder) + start() + mock = context.getEndpoint("mock:mock", classOf[MockEndpoint]) + } + + @After def tearDown = { + stop() + } + + // + // TODO: test replies to messages sent with ! (bang) + // + + @Test def shouldProduceMessageSyncAndReceiveResponse = { + val producer = new TestProducer("direct:input2", false, false).start + val message = Message("test1", Map(Message.MessageExchangeId -> "123")) + val expected = Message("Hello test1", Map(Message.MessageExchangeId -> "123")) + assertEquals(expected, producer !! message get) + producer.stop + } + + @Test def shouldProduceMessageSyncAndReceiveFailure = { + val producer = new TestProducer("direct:input2", false, false).start + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = producer.!![Failure](message).get + assertEquals("failure", result.cause.getMessage) + assertEquals(Map(Message.MessageExchangeId -> "123"), result.headers) + producer.stop + } + + @Test def shouldProduceMessageAsyncAndReceiveResponse = { + val producer = new TestProducer("direct:input2", true, false).start + val message = Message("test2", Map(Message.MessageExchangeId -> "124")) + val expected = Message("Hello test2", Map(Message.MessageExchangeId -> "124")) + assertEquals(expected, producer !! message get) + producer.stop + } + + @Test def shouldProduceMessageAsyncAndReceiveFailure = { + val producer = new TestProducer("direct:input2", true, false).start + val message = Message("fail", Map(Message.MessageExchangeId -> "124")) + val result = producer.!![Failure](message).get + assertEquals("failure", result.cause.getMessage) + assertEquals(Map(Message.MessageExchangeId -> "124"), result.headers) + producer.stop + } + + @Test def shouldProduceMessageSyncWithoutReceivingResponse = { + val producer = new TestProducer("direct:input1", false, true).start + mock.expectedBodiesReceived("test3") + producer.!("test3")(None) + producer.stop + } + + @Test def shouldProduceMessageAsyncAndReceiveResponseSync = { + val producer = new TestProducer("direct:input1", true, true).start + mock.expectedBodiesReceived("test4") + producer.!("test4")(None) + producer.stop + } + + class TestProducer(uri:String, prodAsync: Boolean, prodOneway: Boolean) extends Actor with Producer { + override def async = prodAsync + override def oneway = prodOneway + def endpointUri = uri + def receive = produce + } + + class TestRouteBuilder extends RouteBuilder { + def configure { + from("direct:input1").to("mock:mock") + from("direct:input2").process(new Processor() { + def process(exchange: Exchange) = { + val body = exchange.getIn.getBody + body match { + case "fail" => throw new Exception("failure") + case body => exchange.getOut.setBody("Hello %s" format body) + } + } + }) + } + } + +} \ No newline at end of file diff --git a/akka-camel/src/test/scala/component/ActorComponentTest.scala b/akka-camel/src/test/scala/component/ActorComponentTest.scala index 30ea7d1a5b..379349da7a 100644 --- a/akka-camel/src/test/scala/component/ActorComponentTest.scala +++ b/akka-camel/src/test/scala/component/ActorComponentTest.scala @@ -3,27 +3,24 @@ package se.scalablesolutions.akka.camel.component import org.junit._ import org.junit.Assert._ import org.scalatest.junit.JUnitSuite + import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.Message -import org.apache.camel.{CamelContext, ExchangePattern} -import org.apache.camel.impl.{DefaultExchange, SimpleRegistry, DefaultCamelContext} +import se.scalablesolutions.akka.camel.{CamelContextLifecycle, Message} -/** - * @author Martin Krasser - */ -class ActorComponentTest extends JUnitSuite { +class ActorComponentTest extends JUnitSuite with CamelContextLifecycle { - val context = new DefaultCamelContext(new SimpleRegistry) - val template = context.createProducerTemplate + // + // TODO: extend/rewrite unit tests + // These tests currently only ensure proper functioning of basic features. + // @Before def setUp = { - context.start - template.start + init() + start() } @After def tearDown = { - template.stop - context.stop + stop() } @Test def shouldReceiveResponseFromActorReferencedById = { diff --git a/akka-camel/src/test/scala/component/ActorProducerTest.scala b/akka-camel/src/test/scala/component/ActorProducerTest.scala index 73e51ebb04..954a4d21cd 100644 --- a/akka-camel/src/test/scala/component/ActorProducerTest.scala +++ b/akka-camel/src/test/scala/component/ActorProducerTest.scala @@ -1,19 +1,21 @@ package se.scalablesolutions.akka.camel.component import org.apache.camel.{CamelContext, ExchangePattern} +import org.apache.camel.impl.{DefaultCamelContext, DefaultExchange} import org.junit.Assert._ import org.junit.Test import org.scalatest.junit.JUnitSuite import se.scalablesolutions.akka.actor.Actor import se.scalablesolutions.akka.camel.Message -import org.apache.camel.impl.{DefaultCamelContext, DefaultExchange} -/** - * @author Martin Krasser - */ class ActorProducerTest extends JUnitSuite { + // + // TODO: extend/rewrite unit tests + // These tests currently only ensure proper functioning of basic features. + // + val context = new DefaultCamelContext val endpoint = context.getEndpoint("actor:%s" format classOf[TestActor].getName) val producer = endpoint.createProducer diff --git a/akka-camel/src/test/scala/service/CamelServiceTest.scala b/akka-camel/src/test/scala/service/CamelServiceTest.scala index 52f6d1fd04..58ccc76273 100644 --- a/akka-camel/src/test/scala/service/CamelServiceTest.scala +++ b/akka-camel/src/test/scala/service/CamelServiceTest.scala @@ -1,68 +1,50 @@ package se.scalablesolutions.akka.camel.service import org.apache.camel.builder.RouteBuilder -import org.apache.camel.impl.DefaultCamelContext import org.junit.Assert._ import org.junit.{Before, After, Test} import org.scalatest.junit.JUnitSuite import se.scalablesolutions.akka.actor.Actor import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.{Message, Consumer} +import se.scalablesolutions.akka.camel.{CamelContextManager, Consumer, Message} -/** - * @author Martin Krasser - */ -class CamelServiceTest extends JUnitSuite { +class CamelServiceTest extends JUnitSuite with CamelService { - import CamelContextManager.context + // + // TODO: extend/rewrite unit tests + // These tests currently only ensure proper functioning of basic features. + // - context = new DefaultCamelContext - context.addRoutes(new TestBuilder) + import CamelContextManager._ - val template = context.createProducerTemplate - var service: CamelService = _ var actor1: Actor = _ var actor2: Actor = _ var actor3: Actor = _ @Before def setUp = { - service = new CamelService { - override def onUnload = super.onUnload - override def onLoad = super.onLoad - } - actor1 = new TestActor1().start actor2 = new TestActor2().start actor3 = new TestActor3().start - - service.onLoad - template.start - + init() + context.addRoutes(new TestRouteBuilder) + onLoad } @After def tearDown = { + onUnload actor1.stop actor2.stop actor3.stop - - template.stop - service.onUnload } - @Test def shouldReceiveResponseFromActor1ViaGeneratedRoute = { - val result = template.requestBody("direct:actor1", "Martin") - assertEquals("Hello Martin (actor1)", result) + @Test def shouldReceiveResponseViaGeneratedRoute = { + assertEquals("Hello Martin (actor1)", template.requestBody("direct:actor1", "Martin")) + assertEquals("Hello Martin (actor2)", template.requestBody("direct:actor2", "Martin")) } - @Test def shouldReceiveResponseFromActor2ViaGeneratedRoute = { - val result = template.requestBody("direct:actor2", "Martin") - assertEquals("Hello Martin (actor2)", result) - } - - @Test def shouldReceiveResponseFromActor3ViaCustomRoute = { - val result = template.requestBody("direct:actor3", "Martin") - assertEquals("Hello Tester (actor3)", result) + @Test def shouldReceiveResponseViaCustomRoute = { + assertEquals("Hello Tester (actor3)", template.requestBody("direct:actor3", "Martin")) } } @@ -91,7 +73,7 @@ class TestActor3 extends Actor { } } -class TestBuilder extends RouteBuilder { +class TestRouteBuilder extends RouteBuilder { def configure { val actorUri = "actor:%s" format classOf[TestActor3].getName from("direct:actor3").transform(constant("Tester")).to("actor:actor3") diff --git a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala index 0b3726c08b..b23c99dafa 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala @@ -1,10 +1,10 @@ package sample.camel import org.apache.camel.builder.RouteBuilder -import org.apache.camel.impl.DefaultCamelContext +import org.apache.camel.{Exchange, Processor} import se.scalablesolutions.akka.actor.SupervisorFactory -import se.scalablesolutions.akka.camel.service.CamelContextManager +import se.scalablesolutions.akka.camel.CamelContextManager import se.scalablesolutions.akka.config.ScalaConfig._ /** @@ -12,10 +12,8 @@ import se.scalablesolutions.akka.config.ScalaConfig._ */ class Boot { - import CamelContextManager.context - - context = new DefaultCamelContext - context.addRoutes(new CustomRouteBuilder) + CamelContextManager.init() + CamelContextManager.context.addRoutes(new CustomRouteBuilder) val factory = SupervisorFactory( SupervisorConfig( @@ -24,13 +22,27 @@ class Boot { Supervise(new Consumer2, LifeCycle(Permanent)) :: Nil)) factory.newInstance.start + val producer = new Producer1 + val mediator = new Transformer(producer) + val consumer = new Consumer3(mediator) + + producer.start + mediator.start + consumer.start + } class CustomRouteBuilder extends RouteBuilder { def configure { val actorUri = "actor:%s" format classOf[Consumer2].getName - from ("jetty:http://0.0.0.0:8877/camel/test2").to(actorUri) + from("jetty:http://0.0.0.0:8877/camel/test2").to(actorUri) + from("direct:welcome").process(new Processor() { + def process(exchange: Exchange) { + exchange.getOut.setBody("Welcome %s" format exchange.getIn.getBody) + } + }) + } } \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala new file mode 100644 index 0000000000..39cf1f0652 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala @@ -0,0 +1,17 @@ +package sample.camel + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.{Message, Consumer} + +/** + * @author Martin Krasser + */ +class Consumer3(transformer: Actor) extends Actor with Consumer { + + def endpointUri = "jetty:http://0.0.0.0:8877/camel/welcome" + + def receive = { + case msg: Message => transformer.forward(msg.setBodyAs(classOf[String])) + } + +} diff --git a/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala new file mode 100644 index 0000000000..11151a58df --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala @@ -0,0 +1,17 @@ +package sample.camel + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.Producer + +/** + * @author Martin Krasser + */ +class Producer1 extends Actor with Producer { + + def endpointUri = "direct:welcome" + + override def oneway = false // default + override def async = true // default + + protected def receive = produce +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala b/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala new file mode 100644 index 0000000000..0df05c594c --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala @@ -0,0 +1,15 @@ +package sample.camel + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.Message + +/** + * @author Martin Krasser + */ +class Transformer(producer: Actor) extends Actor { + + protected def receive = { + case msg: Message => producer.forward(msg.transformBody[String]("- %s -" format _)) + } + +} \ No newline at end of file From 68fcbe8562667db5f7d9692ca99d5c064ff49dc0 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Fri, 5 Mar 2010 22:18:26 +0100 Subject: [PATCH 06/17] fixed compile errors after merging with master --- akka-camel/src/main/scala/Producer.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-camel/src/main/scala/Producer.scala b/akka-camel/src/main/scala/Producer.scala index 326f208cfc..cd827ce415 100644 --- a/akka-camel/src/main/scala/Producer.scala +++ b/akka-camel/src/main/scala/Producer.scala @@ -11,7 +11,7 @@ import org.apache.camel.impl.DefaultExchange import org.apache.camel.spi.Synchronization import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.dispatch.CompletableFutureResult +import se.scalablesolutions.akka.dispatch.CompletableFuture import se.scalablesolutions.akka.util.Logging /** @@ -166,7 +166,7 @@ trait Producer { class ProducerResponseSender( headers: Map[String, Any], sender: Option[Actor], - senderFuture: Option[CompletableFutureResult], + senderFuture: Option[CompletableFuture], producer: Actor) extends Synchronization with Logging { implicit val producerActor = Some(producer) // the response sender From c04ebf4dee68eda928e980b23f9d2940435cb5b5 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sat, 6 Mar 2010 08:47:37 +0100 Subject: [PATCH 07/17] CamelService companion object for standalone applications to create their own CamelService instances --- akka.iml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/akka.iml b/akka.iml index 74542e8e48..c418d66936 100644 --- a/akka.iml +++ b/akka.iml @@ -1,27 +1,5 @@ - - - - - - - - - - - - - - From 9f31bf5c3135782b51856ef75cd8c7bbf5afdf99 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sat, 6 Mar 2010 08:54:10 +0100 Subject: [PATCH 08/17] Fixed mess-up of previous commit (rollback changes to akka.iml), CamelService companion object for standalone applications to create their own CamelService instances --- .../src/main/scala/service/CamelService.scala | 17 +++++++++++++- akka.iml | 22 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index aa4a54ef12..7c04edc939 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -44,7 +44,22 @@ trait CamelService extends Bootable with Logging { } /** - * Implements routes from Camel endpoints to actors. It searches the registry for actors + * CamelService companion object used by standalone applications to create their own + * CamelService instances. + * + * @author Martin Krasser + */ +object CamelService { + + /** + * Creates a new CamelService instance. + */ + def newInstance: CamelService = new CamelService {} + +} + +/** + * Implements routes from Camel endpoints to actors. It searches the registry for actors * that are either annotated with @se.scalablesolutions.akka.annotation.consume or mix in * se.scalablesolutions.akka.camel.Consumer and exposes them as Camel endpoints. * diff --git a/akka.iml b/akka.iml index c418d66936..74542e8e48 100644 --- a/akka.iml +++ b/akka.iml @@ -1,5 +1,27 @@ + + + + + + + + + + + + + + From a6ffa67846389e43b2ff8dce5077f5a296576213 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sat, 6 Mar 2010 10:32:01 +0100 Subject: [PATCH 09/17] Added lifecycle methods to CamelService --- akka-camel/src/main/scala/service/CamelService.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index 7c04edc939..0b77e56796 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -41,6 +41,10 @@ trait CamelService extends Bootable with Logging { super.onUnload } + def load = onLoad + + def unload = onUnload + } /** From 35a557de6db6f79aeee8006eba2a278ed61e5003 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sat, 6 Mar 2010 19:23:10 +0100 Subject: [PATCH 10/17] performance improvement --- akka-camel/src/main/scala/Producer.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/akka-camel/src/main/scala/Producer.scala b/akka-camel/src/main/scala/Producer.scala index cd827ce415..4c0e42a69f 100644 --- a/akka-camel/src/main/scala/Producer.scala +++ b/akka-camel/src/main/scala/Producer.scala @@ -23,6 +23,8 @@ trait Producer { self: Actor => + private val headersToCopyDefault = Set(Message.MessageExchangeId) + /** * If set to true (default), communication with the Camel endpoint is done via the Camel * Async API. Camel then processes the @@ -48,7 +50,7 @@ trait Producer { * By default only the Message.MessageExchangeId is copied. Applications may override this to * define an application-specific set of message headers to copy. */ - def headersToCopy: Set[String] = Set(Message.MessageExchangeId) + def headersToCopy: Set[String] = headersToCopyDefault /** * Returns the producer template from the CamelContextManager. Applications either have to ensure From f8fab07bc88689147e4e60766b7a957a4ae5ed04 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Mon, 8 Mar 2010 16:38:23 +0100 Subject: [PATCH 11/17] error handling enhancements --- akka-camel/src/main/scala/Message.scala | 8 +++++++- .../main/scala/component/ActorComponent.scala | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/akka-camel/src/main/scala/Message.scala b/akka-camel/src/main/scala/Message.scala index db23868fac..b145ff10cd 100644 --- a/akka-camel/src/main/scala/Message.scala +++ b/akka-camel/src/main/scala/Message.scala @@ -117,7 +117,7 @@ object Message { * * @author Martin Krasser */ -case class Failure(val cause: Throwable, val headers: Map[String, Any]) +case class Failure(val cause: Exception, val headers: Map[String, Any]) /** * Adapter for converting an org.apache.camel.Exchange to and from Message and Failure objects. @@ -140,6 +140,12 @@ class CamelExchangeAdapter(exchange: Exchange) { */ def fromResponseMessage(msg: Message): Exchange = { responseMessage.fromMessage(msg); exchange } + /** + * Sets Exchange.getException from the given Failure message. Headers of the Failure message + * are ignored. + */ + def fromFailureMessage(msg: Failure): Exchange = { exchange.setException(msg.cause); exchange } + /** * Creates a Message object from Exchange.getIn. */ diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index 71db14021a..2fa116926c 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -6,12 +6,13 @@ package se.scalablesolutions.akka.camel.component import java.lang.{RuntimeException, String} import java.util.{Map => JavaMap} +import java.util.concurrent.TimeoutException import org.apache.camel.{Exchange, Consumer, Processor} import org.apache.camel.impl.{DefaultProducer, DefaultEndpoint, DefaultComponent} import se.scalablesolutions.akka.actor.{ActorRegistry, Actor} -import se.scalablesolutions.akka.camel.{CamelMessageConversion, Message} +import se.scalablesolutions.akka.camel.{Failure, CamelMessageConversion, Message} /** * Camel component for sending messages to and receiving replies from actors. @@ -52,6 +53,7 @@ class ActorComponent extends DefaultComponent { * @author Martin Krasser */ class ActorEndpoint(uri: String, comp: ActorComponent, val id: Option[String], val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { + /** * @throws UnsupportedOperationException */ @@ -113,15 +115,15 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { * actor is of type se.scalablesolutions.akka.camel.Message. */ protected def processInOut(exchange: Exchange, actor: Actor) { - - // TODO: support asynchronous communication - val result: Any = actor !! exchange.toRequestMessage(Map(Message.MessageExchangeId -> exchange.getExchangeId)) + val header = Map(Message.MessageExchangeId -> exchange.getExchangeId) + val result: Any = actor !! exchange.toRequestMessage(header) result match { - case Some(msg) => exchange.fromResponseMessage(Message.canonicalize(msg)) - case None => { - // TODO: handle timeout properly - // TODO: make timeout configurable + case Some(msg: Failure) => exchange.fromFailureMessage(msg) + case Some(msg) => exchange.fromResponseMessage(Message.canonicalize(msg)) + case None => { + throw new TimeoutException("communication with %s timed out after %d ms" + format (ep.getEndpointUri, actor.timeout)) } } } From e056af15fa5d989f366c8f5966dd5a979ee20ead Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Thu, 11 Mar 2010 15:53:17 +0100 Subject: [PATCH 12/17] support for remote actors, consumer actor publishing at any time --- .../main/scala/component/ActorComponent.scala | 4 +- .../src/main/scala/service/CamelService.scala | 113 ++++++-------- .../scala/service/ConsumerPublisher.scala | 145 ++++++++++++++++++ akka-camel/src/test/scala/ProducerTest.scala | 1 + .../test/scala/service/CamelServiceTest.scala | 33 +++- .../src/main/scala/actor/ActorRegistry.scala | 37 ++++- .../src/main/scala/Actors.scala | 57 +++++++ .../src/main/scala/Application1.scala | 26 ++++ .../src/main/scala/Application2.scala | 22 +++ .../src/main/scala/Boot.scala | 3 - .../src/main/scala/Consumer1.scala | 18 --- .../src/main/scala/Consumer2.scala | 17 -- .../src/main/scala/Consumer3.scala | 17 -- .../src/main/scala/Transformer.scala | 15 -- 14 files changed, 356 insertions(+), 152 deletions(-) create mode 100644 akka-camel/src/main/scala/service/ConsumerPublisher.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Actors.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Application1.scala create mode 100644 akka-samples/akka-sample-camel/src/main/scala/Application2.scala delete mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala delete mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala delete mode 100644 akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala delete mode 100644 akka-samples/akka-sample-camel/src/main/scala/Transformer.scala diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index 2fa116926c..5788fd9028 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -122,8 +122,8 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { case Some(msg: Failure) => exchange.fromFailureMessage(msg) case Some(msg) => exchange.fromResponseMessage(Message.canonicalize(msg)) case None => { - throw new TimeoutException("communication with %s timed out after %d ms" - format (ep.getEndpointUri, actor.timeout)) + throw new TimeoutException("timeout (%d ms) while waiting response from %s" + format (actor.timeout, ep.getEndpointUri)) } } } diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index 0b77e56796..0f61f2c0f3 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -4,23 +4,14 @@ package se.scalablesolutions.akka.camel.service -import java.io.InputStream - -import org.apache.camel.builder.RouteBuilder - -import se.scalablesolutions.akka.actor.{Actor, ActorRegistry} -import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.actor.ActorRegistry +import se.scalablesolutions.akka.camel.CamelContextManager import se.scalablesolutions.akka.util.{Bootable, Logging} -import se.scalablesolutions.akka.camel.{CamelContextManager, Consumer} /** - * Started by the Kernel to expose certain actors as Camel endpoints. It uses - * se.scalablesolutions.akka.camel.CamelContextManage to create and manage the - * lifecycle of a global CamelContext. This class further uses the - * se.scalablesolutions.akka.camel.service.CamelServiceRouteBuilder to implement - * routes from Camel endpoints to actors. - * - * @see CamelRouteBuilder + * Used by applications (and the Kernel) to publish consumer actors via Camel + * endpoints and to manage the life cycle of a a global CamelContext which can + * be accessed via se.scalablesolutions.akka.camel.CamelContextManager. * * @author Martin Krasser */ @@ -28,28 +19,63 @@ trait CamelService extends Bootable with Logging { import CamelContextManager._ + private[camel] val consumerPublisher = new ConsumerPublisher + private[camel] val publishRequestor = new PublishRequestor(consumerPublisher) + + /** + * Starts the CamelService. Any started actor that is a consumer actor will be (asynchronously) + * published as Camel endpoint. Consumer actors that are started after this method returned will + * be published as well. Actor publishing is done asynchronously. + */ abstract override def onLoad = { super.onLoad + + // Only init and start if not already done by application if (!initialized) init() - context.addRoutes(new CamelServiceRouteBuilder) + if (!started) start() + + // Camel should cache input streams context.setStreamCaching(true) - start() + + // start actor that exposes consumer actors via Camel endpoints + consumerPublisher.start + + // add listener for actor registration events + ActorRegistry.addRegistrationListener(publishRequestor.start) + + // publish already registered consumer actors + for (publish <- Publish.forConsumers(ActorRegistry.actors)) consumerPublisher.!(publish)(None) } + /** + * Stops the CamelService. + */ abstract override def onUnload = { + ActorRegistry.removeRegistrationListener(publishRequestor) + publishRequestor.stop + consumerPublisher.stop stop() super.onUnload } + /** + * Starts the CamelService. + * + * @see onLoad + */ def load = onLoad + /** + * Stops the CamelService. + * + * @see onUnload + */ def unload = onUnload - } /** * CamelService companion object used by standalone applications to create their own - * CamelService instances. + * CamelService instance. * * @author Martin Krasser */ @@ -59,55 +85,4 @@ object CamelService { * Creates a new CamelService instance. */ def newInstance: CamelService = new CamelService {} - -} - -/** - * Implements routes from Camel endpoints to actors. It searches the registry for actors - * that are either annotated with @se.scalablesolutions.akka.annotation.consume or mix in - * se.scalablesolutions.akka.camel.Consumer and exposes them as Camel endpoints. - * - * @author Martin Krasser - */ -class CamelServiceRouteBuilder extends RouteBuilder with Logging { - - def configure = { - val actors = ActorRegistry.actors - - // TODO: avoid redundant registrations - actors.filter(isConsumeAnnotated _).foreach { actor: Actor => - val fromUri = actor.getClass.getAnnotation(classOf[consume]).value() - configure(fromUri, "actor:id:%s" format actor.getId) - log.debug("registered actor (id=%s) for consuming messages from %s " - format (actor.getId, fromUri)) - } - - // TODO: avoid redundant registrations - actors.filter(isConsumerInstance _).foreach { actor: Actor => - val fromUri = actor.asInstanceOf[Consumer].endpointUri - configure(fromUri, "actor:uuid:%s" format actor.uuid) - log.debug("registered actor (uuid=%s) for consuming messages from %s " - format (actor.uuid, fromUri)) - } - } - - private def configure(fromUri: String, toUri: String) { - val schema = fromUri take fromUri.indexOf(":") // e.g. "http" from "http://whatever/..." - bodyConversions.get(schema) match { - case Some(clazz) => from(fromUri).convertBodyTo(clazz).to(toUri) - case None => from(fromUri).to(toUri) - } - } - - // TODO: make conversions configurable - private def bodyConversions = Map( - "file" -> classOf[InputStream] - ) - - private def isConsumeAnnotated(actor: Actor) = - actor.getClass.getAnnotation(classOf[consume]) ne null - - private def isConsumerInstance(actor: Actor) = - actor.isInstanceOf[Consumer] - } diff --git a/akka-camel/src/main/scala/service/ConsumerPublisher.scala b/akka-camel/src/main/scala/service/ConsumerPublisher.scala new file mode 100644 index 0000000000..4e5adea0b8 --- /dev/null +++ b/akka-camel/src/main/scala/service/ConsumerPublisher.scala @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package se.scalablesolutions.akka.camel.service + +import java.io.InputStream +import java.util.concurrent.CountDownLatch + +import org.apache.camel.builder.RouteBuilder + +import se.scalablesolutions.akka.actor.{ActorUnregistered, ActorRegistered, Actor} +import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.{Consumer, CamelContextManager} +import se.scalablesolutions.akka.util.Logging + +/** + * Actor that publishes consumer actors as Camel endpoints at the CamelContext managed + * by se.scalablesolutions.akka.camel.CamelContextManager. It accepts messages of type + * se.scalablesolutions.akka.camel.service.Publish. + * + * @author Martin Krasser + */ +class ConsumerPublisher extends Actor with Logging { + @volatile private var latch = new CountDownLatch(0) + + /** + * Adds a route to the actor identified by a Publish message to the global CamelContext. + */ + protected def receive = { + case p: Publish => publish(new ConsumerRoute(p.endpointUri, p.id, p.uuid)) + case _ => { /* ignore */} + } + + /** + * Sets the number of expected Publish messages received by this actor. Used for testing + * only. + */ + private[camel] def expectPublishCount(count: Int) { + latch = new CountDownLatch(count) + } + + /** + * Waits for the number of expected Publish messages to arrive. Used for testing only. + */ + private[camel] def awaitPublish = latch.await + + private def publish(route: ConsumerRoute) { + CamelContextManager.context.addRoutes(route) + log.info("published actor via endpoint %s" format route.endpointUri) + latch.countDown // needed for testing only. + } +} + +/** + * Defines the route to a consumer actor. + * + * @param endpointUri endpoint URI of the consumer actor + * @param id actor identifier + * @param uuid true if id refers to Actor.uuid, false if + * id refers to Acotr.getId. + * + * @author Martin Krasser + */ +class ConsumerRoute(val endpointUri: String, id: String, uuid: Boolean) extends RouteBuilder { + // TODO: make conversions configurable + private val bodyConversions = Map( + "file" -> classOf[InputStream] + ) + + def configure = { + val schema = endpointUri take endpointUri.indexOf(":") // e.g. "http" from "http://whatever/..." + bodyConversions.get(schema) match { + case Some(clazz) => from(endpointUri).convertBodyTo(clazz).to(actorUri) + case None => from(endpointUri).to(actorUri) + } + } + + private def actorUri = (if (uuid) "actor:uuid:%s" else "actor:id:%s") format id +} + +/** + * A registration listener that publishes consumer actors (and ignores other actors). + * + * @author Martin Krasser + */ +class PublishRequestor(consumerPublisher: Actor) extends Actor { + protected def receive = { + case ActorUnregistered(actor) => { /* ignore */ } + case ActorRegistered(actor) => Publish.forConsumer(actor) match { + case Some(publish) => consumerPublisher ! publish + case None => { /* ignore */ } + } + } +} + +/** + * Request message for publishing a consumer actor. + * + * @param endpointUri endpoint URI of the consumer actor + * @param id actor identifier + * @param uuid true if id refers to Actor.uuid, false if + * id refers to Acotr.getId. + * + * @author Martin Krasser + */ +case class Publish(endpointUri: String, id: String, uuid: Boolean) + +/** + * @author Martin Krasser + */ +object Publish { + /** + * Creates a list of Publish request messages for all consumer actors in the actors + * list. + */ + def forConsumers(actors: List[Actor]): List[Publish] = { + for (actor <- actors; pub = forConsumer(actor); if pub.isDefined) yield pub.get + } + + /** + * Creates a Publish request message if actor is a consumer actor. + */ + def forConsumer(actor: Actor): Option[Publish] = { + forConsumeAnnotated(actor) orElse forConsumerType(actor) + } + + private def forConsumeAnnotated(actor: Actor): Option[Publish] = { + val annotation = actor.getClass.getAnnotation(classOf[consume]) + if (annotation eq null) + None + else if (actor._remoteAddress.isDefined) + None // do not publish proxies + else + Some(Publish(annotation.value, actor.getId, false)) + } + + private def forConsumerType(actor: Actor): Option[Publish] = { + if (!actor.isInstanceOf[Consumer]) + None + else if (actor._remoteAddress.isDefined) + None + else + Some(Publish(actor.asInstanceOf[Consumer].endpointUri, actor.uuid, true)) + } +} diff --git a/akka-camel/src/test/scala/ProducerTest.scala b/akka-camel/src/test/scala/ProducerTest.scala index 1a69316836..268c8c6a6a 100644 --- a/akka-camel/src/test/scala/ProducerTest.scala +++ b/akka-camel/src/test/scala/ProducerTest.scala @@ -33,6 +33,7 @@ class ProducerTest extends JUnitSuite { // // TODO: test replies to messages sent with ! (bang) + // TODO: test copying of custom message headers // @Test def shouldProduceMessageSyncAndReceiveResponse = { diff --git a/akka-camel/src/test/scala/service/CamelServiceTest.scala b/akka-camel/src/test/scala/service/CamelServiceTest.scala index 58ccc76273..8fafea4687 100644 --- a/akka-camel/src/test/scala/service/CamelServiceTest.scala +++ b/akka-camel/src/test/scala/service/CamelServiceTest.scala @@ -2,12 +2,12 @@ package se.scalablesolutions.akka.camel.service import org.apache.camel.builder.RouteBuilder import org.junit.Assert._ -import org.junit.{Before, After, Test} import org.scalatest.junit.JUnitSuite import se.scalablesolutions.akka.actor.Actor import se.scalablesolutions.akka.annotation.consume import se.scalablesolutions.akka.camel.{CamelContextManager, Consumer, Message} +import org.junit.{Ignore, Before, After, Test} class CamelServiceTest extends JUnitSuite with CamelService { @@ -23,26 +23,40 @@ class CamelServiceTest extends JUnitSuite with CamelService { var actor3: Actor = _ @Before def setUp = { + // register actors before starting the CamelService actor1 = new TestActor1().start actor2 = new TestActor2().start actor3 = new TestActor3().start - init() + // initialize global CamelContext + init + // customize global CamelContext context.addRoutes(new TestRouteBuilder) - onLoad + consumerPublisher.expectPublishCount(2) + load + consumerPublisher.awaitPublish } @After def tearDown = { - onUnload + unload actor1.stop actor2.stop actor3.stop } - @Test def shouldReceiveResponseViaGeneratedRoute = { + @Test def shouldReceiveResponseViaPreStartGeneratedRoutes = { assertEquals("Hello Martin (actor1)", template.requestBody("direct:actor1", "Martin")) assertEquals("Hello Martin (actor2)", template.requestBody("direct:actor2", "Martin")) } + @Test def shouldReceiveResponseViaPostStartGeneratedRoute = { + consumerPublisher.expectPublishCount(1) + // register actor after starting CamelService + val actor4 = new TestActor4().start + consumerPublisher.awaitPublish + assertEquals("Hello Martin (actor4)", template.requestBody("direct:actor4", "Martin")) + actor4.stop + } + @Test def shouldReceiveResponseViaCustomRoute = { assertEquals("Hello Tester (actor3)", template.requestBody("direct:actor3", "Martin")) } @@ -55,7 +69,6 @@ class TestActor1 extends Actor with Consumer { protected def receive = { case msg: Message => reply("Hello %s (actor1)" format msg.body) } - } @consume("direct:actor2") @@ -73,6 +86,14 @@ class TestActor3 extends Actor { } } +class TestActor4 extends Actor with Consumer { + def endpointUri = "direct:actor4" + + protected def receive = { + case msg: Message => reply("Hello %s (actor4)" format msg.body) + } +} + class TestRouteBuilder extends RouteBuilder { def configure { val actorUri = "actor:%s" format classOf[TestActor3].getName diff --git a/akka-core/src/main/scala/actor/ActorRegistry.scala b/akka-core/src/main/scala/actor/ActorRegistry.scala index 9e0b1cba08..6db4d0375a 100644 --- a/akka-core/src/main/scala/actor/ActorRegistry.scala +++ b/akka-core/src/main/scala/actor/ActorRegistry.scala @@ -8,8 +8,7 @@ import se.scalablesolutions.akka.util.Logging import scala.collection.mutable.ListBuffer import scala.reflect.Manifest - -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.{CopyOnWriteArrayList, ConcurrentHashMap} /** * Registry holding all Actor instances in the whole system. @@ -23,9 +22,10 @@ import java.util.concurrent.ConcurrentHashMap * @author Jonas Bonér */ object ActorRegistry extends Logging { - private val actorsByUUID = new ConcurrentHashMap[String, Actor] - private val actorsById = new ConcurrentHashMap[String, List[Actor]] - private val actorsByClassName = new ConcurrentHashMap[String, List[Actor]] + private val actorsByUUID = new ConcurrentHashMap[String, Actor] + private val actorsById = new ConcurrentHashMap[String, List[Actor]] + private val actorsByClassName = new ConcurrentHashMap[String, List[Actor]] + private val registrationListeners = new CopyOnWriteArrayList[Actor] /** * Returns all actors in the system. @@ -103,6 +103,9 @@ object ActorRegistry extends Logging { if (actorsByClassName.containsKey(className)) { actorsByClassName.put(className, actor :: actorsByClassName.get(className)) } else actorsByClassName.put(className, actor :: Nil) + + // notify listeners + foreachListener(_.!(ActorRegistered(actor))(None)) } /** @@ -112,6 +115,8 @@ object ActorRegistry extends Logging { actorsByUUID remove actor.uuid actorsById remove actor.getId actorsByClassName remove actor.getClass.getName + // notify listeners + foreachListener(_.!(ActorUnregistered(actor))(None)) } /** @@ -125,4 +130,26 @@ object ActorRegistry extends Logging { actorsByClassName.clear log.info("All actors have been shut down and unregistered from ActorRegistry") } + + /** + * Adds the registration listener this this registry's listener list. + */ + def addRegistrationListener(listener: Actor) = { + registrationListeners.add(listener) + } + + /** + * Removes the registration listener this this registry's listener list. + */ + def removeRegistrationListener(listener: Actor) = { + registrationListeners.remove(listener) + } + + private def foreachListener(f: (Actor) => Unit) { + val iterator = registrationListeners.iterator + while (iterator.hasNext) f(iterator.next) + } } + +case class ActorRegistered(actor: Actor) +case class ActorUnregistered(actor: Actor) \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Actors.scala b/akka-samples/akka-sample-camel/src/main/scala/Actors.scala new file mode 100644 index 0000000000..51c3940991 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Actors.scala @@ -0,0 +1,57 @@ +package sample.camel + +import se.scalablesolutions.akka.actor.{Actor, RemoteActor} +import se.scalablesolutions.akka.annotation.consume +import se.scalablesolutions.akka.camel.{Message, Consumer} +import se.scalablesolutions.akka.util.Logging + +/** + * Client-initiated remote actor. + */ +class RemoteActor1 extends RemoteActor("localhost", 7777) with Consumer { + def endpointUri = "jetty:http://localhost:6644/remote1" + + protected def receive = { + case msg => reply("response from remote actor 1") + } +} + +/** + * Server-initiated remote actor. + */ +class RemoteActor2 extends Actor with Consumer { + def endpointUri = "jetty:http://localhost:6644/remote2" + + protected def receive = { + case msg => reply("response from remote actor 2") + } +} + +class Consumer1 extends Actor with Consumer with Logging { + def endpointUri = "file:data/input" + + def receive = { + case msg: Message => log.info("received %s" format msg.bodyAs(classOf[String])) + } +} + +@consume("jetty:http://0.0.0.0:8877/camel/test1") +class Consumer2 extends Actor { + def receive = { + case msg: Message => reply("Hello %s" format msg.bodyAs(classOf[String])) + } +} + +class Consumer3(transformer: Actor) extends Actor with Consumer { + def endpointUri = "jetty:http://0.0.0.0:8877/camel/welcome" + + def receive = { + case msg: Message => transformer.forward(msg.setBodyAs(classOf[String])) + } +} + +class Transformer(producer: Actor) extends Actor { + protected def receive = { + case msg: Message => producer.forward(msg.transformBody[String]("- %s -" format _)) + } +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Application1.scala b/akka-samples/akka-sample-camel/src/main/scala/Application1.scala new file mode 100644 index 0000000000..5b708bfac5 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Application1.scala @@ -0,0 +1,26 @@ +package sample.camel + +import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.remote.RemoteClient +/** + * @author Martin Krasser + */ +object Application1 { + + // + // TODO: completion of example + // + + def main(args: Array[String]) { + implicit val sender: Option[Actor] = None + + val actor1 = new RemoteActor1 + val actor2 = RemoteClient.actorFor("remote2", "localhost", 7777) + + actor1.start + + actor1 ! "hello" + actor2 ! "hello" + } + +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Application2.scala b/akka-samples/akka-sample-camel/src/main/scala/Application2.scala new file mode 100644 index 0000000000..83c6e8c439 --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/scala/Application2.scala @@ -0,0 +1,22 @@ +package sample.camel + +import se.scalablesolutions.akka.camel.service.CamelService +import se.scalablesolutions.akka.remote.RemoteNode + +/** + * @author Martin Krasser + */ +object Application2 { + + // + // TODO: completion of example + // + + def main(args: Array[String]) { + val camelService = CamelService.newInstance + camelService.load + RemoteNode.start("localhost", 7777) + RemoteNode.register("remote2", new RemoteActor2().start) + } + +} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala index b23c99dafa..81af9775cb 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala @@ -33,7 +33,6 @@ class Boot { } class CustomRouteBuilder extends RouteBuilder { - def configure { val actorUri = "actor:%s" format classOf[Consumer2].getName from("jetty:http://0.0.0.0:8877/camel/test2").to(actorUri) @@ -42,7 +41,5 @@ class CustomRouteBuilder extends RouteBuilder { exchange.getOut.setBody("Welcome %s" format exchange.getIn.getBody) } }) - } - } \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala deleted file mode 100644 index b292d6e186..0000000000 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer1.scala +++ /dev/null @@ -1,18 +0,0 @@ -package sample.camel - -import se.scalablesolutions.akka.util.Logging -import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.{Message, Consumer} - -/** - * @author Martin Krasser - */ -class Consumer1 extends Actor with Consumer with Logging { - - def endpointUri = "file:data/input" - - def receive = { - case msg: Message => log.info("received %s" format msg.bodyAs(classOf[String])) - } - -} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala deleted file mode 100644 index 4940c46f0d..0000000000 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer2.scala +++ /dev/null @@ -1,17 +0,0 @@ -package sample.camel - -import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.Message - -/** - * @author Martin Krasser - */ -@consume("jetty:http://0.0.0.0:8877/camel/test1") -class Consumer2 extends Actor { - - def receive = { - case msg: Message => reply("Hello %s" format msg.bodyAs(classOf[String])) - } - -} \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala b/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala deleted file mode 100644 index 39cf1f0652..0000000000 --- a/akka-samples/akka-sample-camel/src/main/scala/Consumer3.scala +++ /dev/null @@ -1,17 +0,0 @@ -package sample.camel - -import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.{Message, Consumer} - -/** - * @author Martin Krasser - */ -class Consumer3(transformer: Actor) extends Actor with Consumer { - - def endpointUri = "jetty:http://0.0.0.0:8877/camel/welcome" - - def receive = { - case msg: Message => transformer.forward(msg.setBodyAs(classOf[String])) - } - -} diff --git a/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala b/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala deleted file mode 100644 index 0df05c594c..0000000000 --- a/akka-samples/akka-sample-camel/src/main/scala/Transformer.scala +++ /dev/null @@ -1,15 +0,0 @@ -package sample.camel - -import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.Message - -/** - * @author Martin Krasser - */ -class Transformer(producer: Actor) extends Actor { - - protected def receive = { - case msg: Message => producer.forward(msg.transformBody[String]("- %s -" format _)) - } - -} \ No newline at end of file From e97e944613306212a473075ddff61f2ac4167465 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Sun, 14 Mar 2010 11:17:34 +0100 Subject: [PATCH 13/17] publish/subscribe examples using jms and cometd --- akka-camel/pom.xml | 47 +++++++++++++++++++ .../main/resources/sample-camel-context.xml | 23 +++++++++ .../src/main/scala/Actors.scala | 41 ++++++++++++++-- .../src/main/scala/Application1.scala | 6 ++- .../src/main/scala/Boot.scala | 29 +++++++++++- .../src/main/scala/Producer1.scala | 17 ------- 6 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 akka-samples/akka-sample-camel/src/main/resources/sample-camel-context.xml delete mode 100644 akka-samples/akka-sample-camel/src/main/scala/Producer1.scala diff --git a/akka-camel/pom.xml b/akka-camel/pom.xml index bc42439e84..f11d2ad46e 100644 --- a/akka-camel/pom.xml +++ b/akka-camel/pom.xml @@ -27,10 +27,57 @@ camel-core 2.2.0 + + + org.apache.camel camel-jetty 2.2.0 + + + + org.mortbay.jetty + jetty + + + org.mortbay.jetty + jetty-client + + + + + + org.mortbay.jetty + jetty + 6.1.11 + + + org.mortbay.jetty + jetty-client + 6.1.11 + + + org.apache.camel + camel-cometd + 2.2.0 + + + org.apache.camel + camel-jms + 2.2.0 + + + org.apache.activemq + activemq-core + 5.3.0 diff --git a/akka-samples/akka-sample-camel/src/main/resources/sample-camel-context.xml b/akka-samples/akka-sample-camel/src/main/resources/sample-camel-context.xml new file mode 100644 index 0000000000..b3d811d8de --- /dev/null +++ b/akka-samples/akka-sample-camel/src/main/resources/sample-camel-context.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/akka-samples/akka-sample-camel/src/main/scala/Actors.scala b/akka-samples/akka-sample-camel/src/main/scala/Actors.scala index 51c3940991..11367dedb4 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Actors.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Actors.scala @@ -2,7 +2,7 @@ package sample.camel import se.scalablesolutions.akka.actor.{Actor, RemoteActor} import se.scalablesolutions.akka.annotation.consume -import se.scalablesolutions.akka.camel.{Message, Consumer} +import se.scalablesolutions.akka.camel.{Producer, Message, Consumer} import se.scalablesolutions.akka.util.Logging /** @@ -12,7 +12,7 @@ class RemoteActor1 extends RemoteActor("localhost", 7777) with Consumer { def endpointUri = "jetty:http://localhost:6644/remote1" protected def receive = { - case msg => reply("response from remote actor 1") + case msg: Message => reply(Message("hello %s" format msg.body, Map("sender" -> "remote1"))) } } @@ -23,10 +23,19 @@ class RemoteActor2 extends Actor with Consumer { def endpointUri = "jetty:http://localhost:6644/remote2" protected def receive = { - case msg => reply("response from remote actor 2") + case msg: Message => reply(Message("hello %s" format msg.body, Map("sender" -> "remote2"))) } } +class Producer1 extends Actor with Producer { + def endpointUri = "direct:welcome" + + override def oneway = false // default + override def async = true // default + + protected def receive = produce +} + class Consumer1 extends Actor with Consumer with Logging { def endpointUri = "file:data/input" @@ -54,4 +63,30 @@ class Transformer(producer: Actor) extends Actor { protected def receive = { case msg: Message => producer.forward(msg.transformBody[String]("- %s -" format _)) } +} + +class Subscriber(name:String, uri: String) extends Actor with Consumer { + def endpointUri = uri + + protected def receive = { + case msg: Message => log.info("%s received: %s" format (name, msg.body)) + } +} + +class Publisher(name: String, uri: String) extends Actor with Producer { + id = name + def endpointUri = uri + override def oneway = true + protected def receive = produce +} + +class PublisherBridge(uri: String, publisher: Actor) extends Actor with Consumer { + def endpointUri = uri + + protected def receive = { + case msg: Message => { + publisher ! msg.bodyAs(classOf[String]) + reply("message published") + } + } } \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Application1.scala b/akka-samples/akka-sample-camel/src/main/scala/Application1.scala index 5b708bfac5..4a55f2014f 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Application1.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Application1.scala @@ -1,7 +1,9 @@ package sample.camel import se.scalablesolutions.akka.actor.Actor +import se.scalablesolutions.akka.camel.Message import se.scalablesolutions.akka.remote.RemoteClient + /** * @author Martin Krasser */ @@ -19,8 +21,8 @@ object Application1 { actor1.start - actor1 ! "hello" - actor2 ! "hello" + println(actor1 !! Message("actor1")) + println(actor2 !! Message("actor2")) } } \ No newline at end of file diff --git a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala index 81af9775cb..42cb367076 100644 --- a/akka-samples/akka-sample-camel/src/main/scala/Boot.scala +++ b/akka-samples/akka-sample-camel/src/main/scala/Boot.scala @@ -1,7 +1,10 @@ package sample.camel -import org.apache.camel.builder.RouteBuilder import org.apache.camel.{Exchange, Processor} +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.DefaultCamelContext +import org.apache.camel.spring.spi.ApplicationContextRegistry +import org.springframework.context.support.ClassPathXmlApplicationContext import se.scalablesolutions.akka.actor.SupervisorFactory import se.scalablesolutions.akka.camel.CamelContextManager @@ -12,9 +15,15 @@ import se.scalablesolutions.akka.config.ScalaConfig._ */ class Boot { - CamelContextManager.init() + // Create CamelContext with Spring-based registry and custom route builder + + val context = new ClassPathXmlApplicationContext("/sample-camel-context.xml", getClass) + val registry = new ApplicationContextRegistry(context) + CamelContextManager.init(new DefaultCamelContext(registry)) CamelContextManager.context.addRoutes(new CustomRouteBuilder) + // Basic example + val factory = SupervisorFactory( SupervisorConfig( RestartStrategy(OneForOne, 3, 100, List(classOf[Exception])), @@ -22,6 +31,8 @@ class Boot { Supervise(new Consumer2, LifeCycle(Permanent)) :: Nil)) factory.newInstance.start + // Routing example + val producer = new Producer1 val mediator = new Transformer(producer) val consumer = new Consumer3(mediator) @@ -30,6 +41,20 @@ class Boot { mediator.start consumer.start + // Publish subscribe example + + val cometdUri = "cometd://localhost:8111/test/abc?resourceBase=target" + val cometdSubscriber = new Subscriber("cometd-subscriber", cometdUri).start + val cometdPublisher = new Publisher("cometd-publisher", cometdUri).start + + val jmsUri = "jms:topic:test" + val jmsSubscriber1 = new Subscriber("jms-subscriber-1", jmsUri).start + val jmsSubscriber2 = new Subscriber("jms-subscriber-2", jmsUri).start + val jmsPublisher = new Publisher("jms-publisher", jmsUri).start + + val cometdPublisherBridge = new PublisherBridge("jetty:http://0.0.0.0:8877/camel/pub/cometd", cometdPublisher).start + val jmsPublisherBridge = new PublisherBridge("jetty:http://0.0.0.0:8877/camel/pub/jms", jmsPublisher).start + } class CustomRouteBuilder extends RouteBuilder { diff --git a/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala b/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala deleted file mode 100644 index 11151a58df..0000000000 --- a/akka-samples/akka-sample-camel/src/main/scala/Producer1.scala +++ /dev/null @@ -1,17 +0,0 @@ -package sample.camel - -import se.scalablesolutions.akka.actor.Actor -import se.scalablesolutions.akka.camel.Producer - -/** - * @author Martin Krasser - */ -class Producer1 extends Actor with Producer { - - def endpointUri = "direct:welcome" - - override def oneway = false // default - override def async = true // default - - protected def receive = produce -} \ No newline at end of file From 7eb4245390be61a8156fb1c92379ea860bdc27a7 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Mon, 15 Mar 2010 14:38:54 +0100 Subject: [PATCH 14/17] prepare merge with master --- .../akka-sample-lift/config/akka.conf | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/akka-samples/akka-sample-lift/config/akka.conf b/akka-samples/akka-sample-lift/config/akka.conf index 4a02b208bb..55334c6dbd 100644 --- a/akka-samples/akka-sample-lift/config/akka.conf +++ b/akka-samples/akka-sample-lift/config/akka.conf @@ -1,64 +1,64 @@ -##################### -# Akka Config File # -################### - -# This file has all the default settings, so all these could be removed with no visible effect. -# Modify as needed. - - - filename = "./logs/akka.log" - roll = "daily" # Options: never, hourly, daily, sunday/monday/... - level = "debug" # Options: fatal, critical, error, warning, info, debug, trace - console = on - # syslog_host = "" - # syslog_server_name = "" - - - - version = "0.7-SNAPSHOT" - - - timeout = 5000 # default timeout for future based invocations - concurrent-mode = off # if turned on, then the same actor instance is allowed to execute concurrently - - # e.g. departing from the actor model for better performance - serialize-messages = on # does a deep clone of (non-primitive) messages to ensure immutability - - - - service = on - restart-on-collision = off # (not implemented yet) if 'on' then it reschedules the transaction, - # if 'off' then throws an exception or rollback for user to handle - wait-for-completion = 100 # how long time in millis a transaction should be given time to complete when a collision is detected - wait-nr-of-times = 3 # the number of times it should check for completion of a pending transaction upon collision - distributed = off # not implemented yet - - - - service = on - hostname = "localhost" - port = 9999 - connection-timeout = 1000 # in millis - - - - service = on - hostname = "localhost" - port = 9998 - - - - system = "cassandra" # Options: cassandra (coming: terracotta, redis, tokyo-cabinet, tokyo-tyrant, voldemort, memcached, hazelcast) - - - service = on - storage-format = "java" # Options: java, scala-json, java-json - blocking = false # inserts and queries should be blocking or not - - - service = on - pidfile = "akka.pid" - - - - - +##################### +# Akka Config File # +################### + +# This file has all the default settings, so all these could be removed with no visible effect. +# Modify as needed. + + + filename = "./logs/akka.log" + roll = "daily" # Options: never, hourly, daily, sunday/monday/... + level = "debug" # Options: fatal, critical, error, warning, info, debug, trace + console = on + # syslog_host = "" + # syslog_server_name = "" + + + + version = "0.7-SNAPSHOT" + + + timeout = 5000 # default timeout for future based invocations + concurrent-mode = off # if turned on, then the same actor instance is allowed to execute concurrently - + # e.g. departing from the actor model for better performance + serialize-messages = on # does a deep clone of (non-primitive) messages to ensure immutability + + + + service = on + restart-on-collision = off # (not implemented yet) if 'on' then it reschedules the transaction, + # if 'off' then throws an exception or rollback for user to handle + wait-for-completion = 100 # how long time in millis a transaction should be given time to complete when a collision is detected + wait-nr-of-times = 3 # the number of times it should check for completion of a pending transaction upon collision + distributed = off # not implemented yet + + + + service = on + hostname = "localhost" + port = 9999 + connection-timeout = 1000 # in millis + + + + service = on + hostname = "localhost" + port = 9998 + + + + system = "cassandra" # Options: cassandra (coming: terracotta, redis, tokyo-cabinet, tokyo-tyrant, voldemort, memcached, hazelcast) + + + service = on + storage-format = "java" # Options: java, scala-json, java-json + blocking = false # inserts and queries should be blocking or not + + + service = on + pidfile = "akka.pid" + + + + + From 82f411a2d663fea54e6e780bd8c0fce55102975d Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Tue, 16 Mar 2010 06:45:04 +0100 Subject: [PATCH 15/17] Move to sbt --- akka-camel/pom.xml | 98 ------------------------- akka-kernel/src/main/scala/Kernel.scala | 4 +- akka-samples/akka-sample-camel/pom.xml | 39 ---------- project/build/AkkaProject.scala | 22 +++++- 4 files changed, 23 insertions(+), 140 deletions(-) delete mode 100644 akka-camel/pom.xml delete mode 100644 akka-samples/akka-sample-camel/pom.xml diff --git a/akka-camel/pom.xml b/akka-camel/pom.xml deleted file mode 100644 index f11d2ad46e..0000000000 --- a/akka-camel/pom.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - 4.0.0 - - akka-camel - Akka Camel Module - - jar - - - akka - se.scalablesolutions.akka - 0.7-SNAPSHOT - - - - - - akka-core - ${project.groupId} - ${project.version} - - - org.apache.camel - camel-core - 2.2.0 - - - - - - org.apache.camel - camel-jetty - 2.2.0 - - - - org.mortbay.jetty - jetty - - - org.mortbay.jetty - jetty-client - - - - - - org.mortbay.jetty - jetty - 6.1.11 - - - org.mortbay.jetty - jetty-client - 6.1.11 - - - org.apache.camel - camel-cometd - 2.2.0 - - - org.apache.camel - camel-jms - 2.2.0 - - - org.apache.activemq - activemq-core - 5.3.0 - - - - - org.scalatest - scalatest - 1.0 - test - - - junit - junit - 4.5 - test - - - - \ No newline at end of file diff --git a/akka-kernel/src/main/scala/Kernel.scala b/akka-kernel/src/main/scala/Kernel.scala index d7c7a4b2a5..6c0cd87058 100644 --- a/akka-kernel/src/main/scala/Kernel.scala +++ b/akka-kernel/src/main/scala/Kernel.scala @@ -7,6 +7,7 @@ package se.scalablesolutions.akka.kernel import se.scalablesolutions.akka.remote.BootableRemoteActorService import se.scalablesolutions.akka.comet.BootableCometActorService import se.scalablesolutions.akka.actor.BootableActorLoaderService +import se.scalablesolutions.akka.camel.service.CamelService import se.scalablesolutions.akka.config.Config import se.scalablesolutions.akka.util.{Logging, Bootable} @@ -37,7 +38,8 @@ object Kernel extends Logging { def boot: Unit = boot(true, new BootableActorLoaderService with BootableRemoteActorService - with BootableCometActorService) + with BootableCometActorService + with CamelService) /** * Boots up the Kernel. diff --git a/akka-samples/akka-sample-camel/pom.xml b/akka-samples/akka-sample-camel/pom.xml deleted file mode 100644 index 95adba3149..0000000000 --- a/akka-samples/akka-sample-camel/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - 4.0.0 - - akka-sample-camel - Akka Camel Sample Module - - jar - - - akka-samples-parent - se.scalablesolutions.akka - 0.7-SNAPSHOT - - - - src/main/scala - - - maven-antrun-plugin - - - install - - - - - - - run - - - - - - - - diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index 891126f22e..dc10973b8a 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -79,12 +79,13 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val akka_amqp = project("akka-amqp", "akka-amqp", new AkkaAMQPProject(_), akka_core) lazy val akka_rest = project("akka-rest", "akka-rest", new AkkaRestProject(_), akka_core) lazy val akka_comet = project("akka-comet", "akka-comet", new AkkaCometProject(_), akka_rest) + lazy val akka_camel = project("akka-camel", "akka-camel", new AkkaCamelProject(_), akka_core) lazy val akka_patterns = project("akka-patterns", "akka-patterns", new AkkaPatternsProject(_), akka_core) lazy val akka_security = project("akka-security", "akka-security", new AkkaSecurityProject(_), akka_core) lazy val akka_persistence = project("akka-persistence", "akka-persistence", new AkkaPersistenceParentProject(_)) lazy val akka_cluster = project("akka-cluster", "akka-cluster", new AkkaClusterParentProject(_)) lazy val akka_kernel = project("akka-kernel", "akka-kernel", new AkkaKernelProject(_), - akka_core, akka_rest, akka_persistence, akka_cluster, akka_amqp, akka_security, akka_comet, akka_patterns) + akka_core, akka_rest, akka_persistence, akka_cluster, akka_amqp, akka_security, akka_comet, akka_camel, akka_patterns) // functional tests in java lazy val akka_fun_test = project("akka-fun-test-java", "akka-fun-test-java", new AkkaFunTestProject(_), akka_kernel) @@ -207,6 +208,11 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val dist = deployTask(info, distPath) dependsOn(`package`) describedAs("Deploying") } + class AkkaCamelProject(info: ProjectInfo) extends DefaultProject(info) { + val camel_core = "org.apache.camel" % "camel-core" % "2.2.0" % "compile" + lazy val dist = deployTask(info, distPath) dependsOn(`package`) describedAs("Deploying") + } + class AkkaPatternsProject(info: ProjectInfo) extends DefaultProject(info) { // testing val scalatest = "org.scalatest" % "scalatest" % "1.0" % "test" @@ -305,7 +311,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { val lift_util = "net.liftweb" % "lift-util" % "1.1-M6" % "compile" val servlet = "javax.servlet" % "servlet-api" % "2.5" % "compile" // testing - val jetty = "org.mortbay.jetty" % "jetty" % "6.1.6" % "test" + val jetty = "org.mortbay.jetty" % "jetty" % "6.1.11" % "test" val junit = "junit" % "junit" % "4.5" % "test" lazy val dist = deployTask(info, deployPath) dependsOn(`package`) describedAs("Deploying") } @@ -319,6 +325,17 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val dist = deployTask(info, deployPath) dependsOn(`package`) describedAs("Deploying") } + class AkkaSampleCamelProject(info: ProjectInfo) extends DefaultProject(info) { + val jetty = "org.mortbay.jetty" % "jetty" % "6.1.11" % "compile" + val jetty_client = "org.mortbay.jetty" % "jetty-client" % "6.1.11" % "compile" + val camel_http = "org.apache.camel" % "camel-http" % "2.2.0" % "compile" + val camel_jetty = "org.apache.camel" % "camel-jetty" % "2.2.0" % "compile" intransitive() + val camel_jms = "org.apache.camel" % "camel-jms" % "2.2.0" % "compile" + val camel_cometd = "org.apache.camel" % "camel-cometd" % "2.2.0" % "compile" + val activemq_core = "org.apache.activemq" % "activemq-core" % "5.3.0" % "compile" + lazy val dist = deployTask(info, deployPath) dependsOn(`package`) describedAs("Deploying") + } + class AkkaSampleSecurityProject(info: ProjectInfo) extends DefaultProject(info) { val jsr311 = "javax.ws.rs" % "jsr311-api" % "1.1" % "compile" val jsr250 = "javax.annotation" % "jsr250-api" % "1.0" @@ -330,6 +347,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val akka_sample_lift = project("akka-sample-lift", "akka-sample-lift", new AkkaSampleLiftProject(_), akka_kernel) lazy val akka_sample_rest_java = project("akka-sample-rest-java", "akka-sample-rest-java", new AkkaSampleRestJavaProject(_), akka_kernel) lazy val akka_sample_rest_scala = project("akka-sample-rest-scala", "akka-sample-rest-scala", new AkkaSampleRestScalaProject(_), akka_kernel) + lazy val akka_sample_camel = project("akka-sample-camel", "akka-sample-camel", new AkkaSampleCamelProject(_), akka_kernel) lazy val akka_sample_security = project("akka-sample-security", "akka-sample-security", new AkkaSampleSecurityProject(_), akka_kernel) } From 1481d1d40cd639bd23028ecdf370ed6772b51243 Mon Sep 17 00:00:00 2001 From: Martin Krasser Date: Tue, 16 Mar 2010 07:08:10 +0100 Subject: [PATCH 16/17] akka-camel added to manifest classpath. All examples enabled. --- config/akka-reference.conf | 10 ++++------ project/build/AkkaProject.scala | 1 + 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config/akka-reference.conf b/config/akka-reference.conf index 6152bc1abb..6c442e3ef1 100644 --- a/config/akka-reference.conf +++ b/config/akka-reference.conf @@ -19,12 +19,10 @@ # FQN to the class doing initial active object/actor # supervisor bootstrap, should be defined in default constructor - boot = ["sample.camel.Boot"] - - # Disable other boot configurations at the moment - #boot = ["sample.java.Boot", - # "sample.scala.Boot", - # "se.scalablesolutions.akka.security.samples.Boot"] + boot = ["sample.camel.Boot", + "sample.java.Boot", + "sample.scala.Boot", + "se.scalablesolutions.akka.security.samples.Boot"] timeout = 5000 # default timeout for future based invocations diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index dc10973b8a..6bb6757518 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -113,6 +113,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { " dist/akka-cluster-jgroups_%s-%s.jar".format(defScalaVersion.value, version) + " dist/akka-rest_%s-%s.jar".format(defScalaVersion.value, version) + " dist/akka-comet_%s-%s.jar".format(defScalaVersion.value, version) + + " dist/akka-camel_%s-%s.jar".format(defScalaVersion.value, version) + " dist/akka-security_%s-%s.jar".format(defScalaVersion.value, version) + " dist/akka-amqp_%s-%s.jar".format(defScalaVersion.value, version) + " dist/akka-patterns_%s-%s.jar".format(defScalaVersion.value, version) + From 08e3a6156dccabb8ac7a175ae5ab5878f44d95d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Tue, 16 Mar 2010 22:54:48 +0100 Subject: [PATCH 17/17] Minor syntax edits --- .../main/scala/CamelContextLifecycle.scala | 8 ++--- akka-camel/src/main/scala/Consumer.scala | 5 +-- akka-camel/src/main/scala/Message.scala | 31 ++++++++-------- akka-camel/src/main/scala/Producer.scala | 35 +++++++------------ .../main/scala/component/ActorComponent.scala | 19 +++++----- .../src/main/scala/service/CamelService.scala | 9 ++--- .../scala/service/ConsumerPublisher.scala | 32 ++++++----------- akka-core/src/main/scala/actor/Actor.scala | 1 - 8 files changed, 57 insertions(+), 83 deletions(-) diff --git a/akka-camel/src/main/scala/CamelContextLifecycle.scala b/akka-camel/src/main/scala/CamelContextLifecycle.scala index 68e67a0612..b9a696207c 100644 --- a/akka-camel/src/main/scala/CamelContextLifecycle.scala +++ b/akka-camel/src/main/scala/CamelContextLifecycle.scala @@ -51,7 +51,7 @@ trait CamelContextLifecycle extends Logging { /** * Starts the CamelContext and ProducerTemplate. */ - def start() { + def start = { context.start template.start _started = true @@ -61,7 +61,7 @@ trait CamelContextLifecycle extends Logging { /** * Stops the CamelContext and ProducerTemplate. */ - def stop() { + def stop = { template.stop context.stop _initialized = false @@ -72,9 +72,7 @@ trait CamelContextLifecycle extends Logging { /** * Initializes this lifecycle object with the a DefaultCamelContext. */ - def init() { - init(new DefaultCamelContext) - } + def init: Unit = init(new DefaultCamelContext) /** * Initializes this lifecycle object with the given CamelContext. diff --git a/akka-camel/src/main/scala/Consumer.scala b/akka-camel/src/main/scala/Consumer.scala index 1a3003d863..27ec98b25d 100644 --- a/akka-camel/src/main/scala/Consumer.scala +++ b/akka-camel/src/main/scala/Consumer.scala @@ -11,13 +11,10 @@ import se.scalablesolutions.akka.actor.Actor * * @author Martin Krasser */ -trait Consumer { - - self: Actor => +trait Consumer { self: Actor => /** * Returns the Camel endpoint URI to consume messages from. */ def endpointUri: String - } \ No newline at end of file diff --git a/akka-camel/src/main/scala/Message.scala b/akka-camel/src/main/scala/Message.scala index b145ff10cd..8e0156c669 100644 --- a/akka-camel/src/main/scala/Message.scala +++ b/akka-camel/src/main/scala/Message.scala @@ -30,16 +30,13 @@ case class Message(val body: Any, val headers: Map[String, Any]) { * * @see CamelContextManager. */ - def bodyAs[T](clazz: Class[T]): T = { + def bodyAs[T](clazz: Class[T]): T = CamelContextManager.context.getTypeConverter.mandatoryConvertTo[T](clazz, body) - } /** * Returns those headers from this message whose name is contained in names. */ - def headers(names: Set[String]): Map[String, Any] = { - headers.filter(names contains _._1) - } + def headers(names: Set[String]): Map[String, Any] = headers.filter(names contains _._1) /** * Creates a Message with a new body using a transformer function. @@ -86,13 +83,14 @@ case class Message(val body: Any, val headers: Map[String, Any]) { * @author Martin Krasser */ object Message { + /** * Message header to correlate request with response messages. Applications that send * messages to a Producer actor may want to set this header on the request message * so that it can be correlated with an asynchronous response. Messages send to Consumer * actors have this header already set. */ - val MessageExchangeId = "MessageExchangeId" + val MessageExchangeId = "MessageExchangeId".intern /** * Creates a new Message with body as message body and an empty header map. @@ -189,7 +187,8 @@ class CamelExchangeAdapter(exchange: Exchange) { * * @see Failure */ - def toFailureMessage(headers: Map[String, Any]): Failure = Failure(exchange.getException, headers ++ responseMessage.toMessage.headers) + def toFailureMessage(headers: Map[String, Any]): Failure = + Failure(exchange.getException, headers ++ responseMessage.toMessage.headers) private def requestMessage = exchange.getIn @@ -223,28 +222,28 @@ class CamelMessageAdapter(val cm: CamelMessage) { * @param headers additional headers to set on the created Message in addition to those * in the Camel message. */ - def toMessage(headers: Map[String, Any]): Message = { - Message(cm.getBody, cmHeaders(headers, cm)) - } + def toMessage(headers: Map[String, Any]): Message = Message(cm.getBody, cmHeaders(headers, cm)) - private def cmHeaders(headers: Map[String, Any], cm: CamelMessage) = { + private def cmHeaders(headers: Map[String, Any], cm: CamelMessage) = headers ++ MapWrapper[String, AnyRef](cm.getHeaders).elements - } - } /** - * Defines conversion methods to CamelExchangeAdapter and CamelMessageAdapter. Imported by applications + * Defines conversion methods to CamelExchangeAdapter and CamelMessageAdapter. + * Imported by applications * that implicitly want to use conversion methods of CamelExchangeAdapter and CamelMessageAdapter. */ object CamelMessageConversion { + /** * Creates an CamelExchangeAdapter for the given Camel exchange. */ - implicit def toExchangeAdapter(ce: Exchange): CamelExchangeAdapter = new CamelExchangeAdapter(ce) + implicit def toExchangeAdapter(ce: Exchange): CamelExchangeAdapter = + new CamelExchangeAdapter(ce) /** * Creates an CamelMessageAdapter for the given Camel message. */ - implicit def toMessageAdapter(cm: CamelMessage): CamelMessageAdapter = new CamelMessageAdapter(cm) + implicit def toMessageAdapter(cm: CamelMessage): CamelMessageAdapter = + new CamelMessageAdapter(cm) } \ No newline at end of file diff --git a/akka-camel/src/main/scala/Producer.scala b/akka-camel/src/main/scala/Producer.scala index 4c0e42a69f..43e9b8b10e 100644 --- a/akka-camel/src/main/scala/Producer.scala +++ b/akka-camel/src/main/scala/Producer.scala @@ -19,9 +19,7 @@ import se.scalablesolutions.akka.util.Logging * * @author Martin Krasser */ -trait Producer { - - self: Actor => +trait Producer { self: Actor => private val headersToCopyDefault = Set(Message.MessageExchangeId) @@ -68,9 +66,8 @@ trait Producer { * @param msg: the message to produce. The message is converted to its canonical * representation via Message.canonicalize. */ - protected def produceOneway(msg: Any): Unit = { + protected def produceOneway(msg: Any): Unit = template.send(endpointUri, createInOnlyExchange.fromRequestMessage(Message.canonicalize(msg))) - } /** * Initiates a one-way (in-only) message exchange to the Camel endpoint given by @@ -80,9 +77,9 @@ trait Producer { * @param msg: the message to produce. The message is converted to its canonical * representation via Message.canonicalize. */ - protected def produceOnewayAsync(msg: Any): Unit = { - template.asyncSend(endpointUri, createInOnlyExchange.fromRequestMessage(Message.canonicalize(msg))) - } + protected def produceOnewayAsync(msg: Any): Unit = + template.asyncSend( + endpointUri, createInOnlyExchange.fromRequestMessage(Message.canonicalize(msg))) /** * Initiates a two-way (in-out) message exchange to the Camel endpoint given by @@ -99,10 +96,8 @@ trait Producer { def process(exchange: Exchange) = exchange.fromRequestMessage(cmsg) } val result = template.request(endpointUri, requestProcessor) - if (result.isFailed) - result.toFailureMessage(cmsg.headers(headersToCopy)) - else - result.toResponseMessage(cmsg.headers(headersToCopy)) + if (result.isFailed) result.toFailureMessage(cmsg.headers(headersToCopy)) + else result.toResponseMessage(cmsg.headers(headersToCopy)) } /** @@ -118,7 +113,8 @@ trait Producer { */ protected def produceAsync(msg: Any): Unit = { val cmsg = Message.canonicalize(msg) - val sync = new ProducerResponseSender(cmsg.headers(headersToCopy), this.sender, this.senderFuture, this) + val sync = new ProducerResponseSender( + cmsg.headers(headersToCopy), this.sender, this.senderFuture, this) template.asyncCallback(endpointUri, createInOutExchange.fromRequestMessage(cmsg), sync) } @@ -154,9 +150,8 @@ trait Producer { * * @see CamelContextManager. */ - protected def createExchange(pattern: ExchangePattern): Exchange = { + protected def createExchange(pattern: ExchangePattern): Exchange = new DefaultExchange(CamelContextManager.context, pattern) - } } /** @@ -177,24 +172,20 @@ class ProducerResponseSender( * Replies a Failure message, created from the given exchange, to sender (or * senderFuture if applicable). */ - def onFailure(exchange: Exchange) = { - reply(exchange.toFailureMessage(headers)) - } + def onFailure(exchange: Exchange) = reply(exchange.toFailureMessage(headers)) /** * Replies a response Message, created from the given exchange, to sender (or * senderFuture if applicable). */ - def onComplete(exchange: Exchange) = { - reply(exchange.toResponseMessage(headers)) - } + def onComplete(exchange: Exchange) = reply(exchange.toResponseMessage(headers)) private def reply(message: Any) = { sender match { case Some(actor) => actor ! message case None => senderFuture match { case Some(future) => future.completeWithResult(message) - case None => log.warning("no destination for sending response") + case None => log.warning("No destination for sending response") } } } diff --git a/akka-camel/src/main/scala/component/ActorComponent.scala b/akka-camel/src/main/scala/component/ActorComponent.scala index 5788fd9028..763f9dd017 100644 --- a/akka-camel/src/main/scala/component/ActorComponent.scala +++ b/akka-camel/src/main/scala/component/ActorComponent.scala @@ -52,7 +52,10 @@ class ActorComponent extends DefaultComponent { * @author Martin Krasser */ -class ActorEndpoint(uri: String, comp: ActorComponent, val id: Option[String], val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { +class ActorEndpoint(uri: String, + comp: ActorComponent, + val id: Option[String], + val uuid: Option[String]) extends DefaultEndpoint(uri, comp) { /** * @throws UnsupportedOperationException @@ -95,19 +98,16 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { */ def process(exchange: Exchange) { val actor = target getOrElse (throw new ActorNotRegisteredException(ep.getEndpointUri)) - if (exchange.getPattern.isOutCapable) - processInOut(exchange, actor) - else - processInOnly(exchange, actor) + if (exchange.getPattern.isOutCapable) processInOut(exchange, actor) + else processInOnly(exchange, actor) } /** * Send the exchange in-message to the given actor using the ! operator. The message * send to the actor is of type se.scalablesolutions.akka.camel.Message. */ - protected def processInOnly(exchange: Exchange, actor: Actor) { + protected def processInOnly(exchange: Exchange, actor: Actor): Unit = actor ! exchange.toRequestMessage(Map(Message.MessageExchangeId -> exchange.getExchangeId)) - } /** * Send the exchange in-message to the given actor using the !! operator. The exchange @@ -128,10 +128,9 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { } } - private def target: Option[Actor] = { + private def target: Option[Actor] = if (ep.id.isDefined) targetById(ep.id.get) else targetByUuid(ep.uuid.get) - } private def targetById(id: String) = ActorRegistry.actorsFor(id) match { case Nil => None @@ -143,7 +142,7 @@ class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) { } /** - * Thrown to indicate that an actor referenced by an endpoint URI cannot be + * Thrown to indicate that an actor referenced by an endpoint URI cannot be * found in the ActorRegistry. * * @author Martin Krasser diff --git a/akka-camel/src/main/scala/service/CamelService.scala b/akka-camel/src/main/scala/service/CamelService.scala index 0f61f2c0f3..86b4f2dc23 100644 --- a/akka-camel/src/main/scala/service/CamelService.scala +++ b/akka-camel/src/main/scala/service/CamelService.scala @@ -17,6 +17,7 @@ import se.scalablesolutions.akka.util.{Bootable, Logging} */ trait CamelService extends Bootable with Logging { + import se.scalablesolutions.akka.actor.Actor.Sender.Self import CamelContextManager._ private[camel] val consumerPublisher = new ConsumerPublisher @@ -31,8 +32,8 @@ trait CamelService extends Bootable with Logging { super.onLoad // Only init and start if not already done by application - if (!initialized) init() - if (!started) start() + if (!initialized) init + if (!started) start // Camel should cache input streams context.setStreamCaching(true) @@ -44,7 +45,7 @@ trait CamelService extends Bootable with Logging { ActorRegistry.addRegistrationListener(publishRequestor.start) // publish already registered consumer actors - for (publish <- Publish.forConsumers(ActorRegistry.actors)) consumerPublisher.!(publish)(None) + for (publish <- Publish.forConsumers(ActorRegistry.actors)) consumerPublisher ! publish } /** @@ -54,7 +55,7 @@ trait CamelService extends Bootable with Logging { ActorRegistry.removeRegistrationListener(publishRequestor) publishRequestor.stop consumerPublisher.stop - stop() + stop super.onUnload } diff --git a/akka-camel/src/main/scala/service/ConsumerPublisher.scala b/akka-camel/src/main/scala/service/ConsumerPublisher.scala index 4e5adea0b8..dee30882f1 100644 --- a/akka-camel/src/main/scala/service/ConsumerPublisher.scala +++ b/akka-camel/src/main/scala/service/ConsumerPublisher.scala @@ -35,9 +35,7 @@ class ConsumerPublisher extends Actor with Logging { * Sets the number of expected Publish messages received by this actor. Used for testing * only. */ - private[camel] def expectPublishCount(count: Int) { - latch = new CountDownLatch(count) - } + private[camel] def expectPublishCount(count: Int): Unit = latch = new CountDownLatch(count) /** * Waits for the number of expected Publish messages to arrive. Used for testing only. @@ -109,37 +107,29 @@ case class Publish(endpointUri: String, id: String, uuid: Boolean) * @author Martin Krasser */ object Publish { + /** * Creates a list of Publish request messages for all consumer actors in the actors * list. */ - def forConsumers(actors: List[Actor]): List[Publish] = { + def forConsumers(actors: List[Actor]): List[Publish] = for (actor <- actors; pub = forConsumer(actor); if pub.isDefined) yield pub.get - } /** * Creates a Publish request message if actor is a consumer actor. */ - def forConsumer(actor: Actor): Option[Publish] = { + def forConsumer(actor: Actor): Option[Publish] = forConsumeAnnotated(actor) orElse forConsumerType(actor) - } private def forConsumeAnnotated(actor: Actor): Option[Publish] = { val annotation = actor.getClass.getAnnotation(classOf[consume]) - if (annotation eq null) - None - else if (actor._remoteAddress.isDefined) - None // do not publish proxies - else - Some(Publish(annotation.value, actor.getId, false)) + if (annotation eq null) None + else if (actor._remoteAddress.isDefined) None // do not publish proxies + else Some(Publish(annotation.value, actor.getId, false)) } - private def forConsumerType(actor: Actor): Option[Publish] = { - if (!actor.isInstanceOf[Consumer]) - None - else if (actor._remoteAddress.isDefined) - None - else - Some(Publish(actor.asInstanceOf[Consumer].endpointUri, actor.uuid, true)) - } + private def forConsumerType(actor: Actor): Option[Publish] = + if (!actor.isInstanceOf[Consumer]) None + else if (actor._remoteAddress.isDefined) None + else Some(Publish(actor.asInstanceOf[Consumer].endpointUri, actor.uuid, true)) } diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 5f48f1cc1e..6ce822b7ed 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -218,7 +218,6 @@ trait Actor extends TransactionManagement { private[akka] var _replyToAddress: Option[InetSocketAddress] = None private[akka] val _mailbox: Queue[MessageInvocation] = new ConcurrentLinkedQueue[MessageInvocation] - // ==================================== // protected fields // ====================================