From 5e8f844545b71a275afb0b7cf033d93605d05fc4 Mon Sep 17 00:00:00 2001 From: sclasen Date: Mon, 23 May 2011 11:37:56 -0400 Subject: [PATCH] first pass at moving modules over, sbt project compiles properly --- .../src/main/java/akka/camel/consume.java | 34 ++ .../org/apache/camel/component/typed-actor | 1 + .../main/scala/akka/camel/TypedCamel.scala | 55 +++ .../main/scala/akka/camel/TypedConsumer.scala | 38 ++ .../akka/camel/TypedConsumerPublisher.scala | 139 +++++++ .../camel/component/TypedActorComponent.scala | 118 ++++++ .../SampleErrorHandlingTypedConsumer.java | 11 + .../SampleErrorHandlingTypedConsumerImpl.java | 14 + .../akka/camel/SampleRemoteTypedConsumer.java | 12 + .../camel/SampleRemoteTypedConsumerImpl.java | 14 + .../camel/SampleRouteDefinitionHandler.java | 14 + .../java/akka/camel/SampleTypedActor.java | 9 + .../java/akka/camel/SampleTypedActorImpl.java | 14 + .../java/akka/camel/SampleTypedConsumer.java | 20 + .../akka/camel/SampleTypedConsumerImpl.java | 30 ++ .../akka/camel/SampleTypedSingleConsumer.java | 13 + .../camel/SampleTypedSingleConsumerImpl.java | 13 + .../akka/camel/TypedConsumerJavaTestBase.java | 46 +++ .../akka/camel/TypedCamelTestSupport.scala | 66 +++ .../akka/camel/TypedConsumerJavaTest.scala | 5 + .../TypedConsumerPublishRequestorTest.scala | 87 ++++ .../akka/camel/TypedConsumerScalaTest.scala | 99 +++++ .../TypedActorComponentFeatureTest.scala | 110 +++++ .../services/org/apache/camel/component/actor | 1 + .../akka/camel/CamelContextLifecycle.scala | 186 +++++++++ .../main/scala/akka/camel/CamelService.scala | 270 ++++++++++++ .../src/main/scala/akka/camel/Consumer.scala | 150 +++++++ .../scala/akka/camel/ConsumerPublisher.scala | 220 ++++++++++ .../src/main/scala/akka/camel/Message.scala | 385 ++++++++++++++++++ .../src/main/scala/akka/camel/Producer.scala | 270 ++++++++++++ .../scala/akka/camel/PublisherRequestor.scala | 64 +++ .../scala/akka/camel/TypedCamelAccess.scala | 36 ++ .../akka/camel/component/ActorComponent.scala | 332 +++++++++++++++ .../java/akka/camel/ConsumerJavaTestBase.java | 41 ++ .../java/akka/camel/MessageJavaTestBase.java | 129 ++++++ .../camel/SampleErrorHandlingConsumer.java | 34 ++ .../java/akka/camel/SampleUntypedActor.java | 12 + .../akka/camel/SampleUntypedConsumer.java | 21 + .../camel/SampleUntypedConsumerBlocking.java | 23 ++ .../SampleUntypedForwardingProducer.java | 18 + .../camel/SampleUntypedReplyingProducer.java | 12 + .../camel/CamelContextLifecycleTest.scala | 37 ++ .../akka/camel/CamelExchangeAdapterTest.scala | 123 ++++++ .../akka/camel/CamelMessageAdapterTest.scala | 40 ++ .../akka/camel/CamelServiceManagerTest.scala | 62 +++ .../scala/akka/camel/CamelTestSupport.scala | 79 ++++ .../scala/akka/camel/ConsumerJavaTest.scala | 5 + .../camel/ConsumerPublishRequestorTest.scala | 60 +++ .../akka/camel/ConsumerRegisteredTest.scala | 69 ++++ .../scala/akka/camel/ConsumerScalaTest.scala | 299 ++++++++++++++ .../scala/akka/camel/MessageJavaTest.scala | 5 + .../scala/akka/camel/MessageScalaTest.scala | 94 +++++ .../akka/camel/ProducerFeatureTest.scala | 301 ++++++++++++++ .../camel/UntypedProducerFeatureTest.scala | 97 +++++ .../component/ActorComponentFeatureTest.scala | 129 ++++++ .../camel/component/ActorComponentTest.scala | 103 +++++ .../camel/component/ActorProducerTest.scala | 253 ++++++++++++ .../scala/akka/kernel/DefaultAkkaLoader.scala | 23 ++ .../scala/akka/kernel/EmbeddedAppServer.scala | 73 ++++ .../src/main/scala/akka/kernel/Kernel.scala | 38 ++ .../main/scala/akka/servlet/Initializer.scala | 33 ++ .../src/main/scala/AkkaKernelProject.scala | 116 ++++++ .../main/resources/META-INF/spring.handlers | 1 + .../main/resources/META-INF/spring.schemas | 1 + .../akka/spring/akka-2.0-SNAPSHOT.xsd | 383 +++++++++++++++++ .../spring/ActorBeanDefinitionParser.scala | 93 +++++ .../scala/akka/spring/ActorFactoryBean.scala | 252 ++++++++++++ .../main/scala/akka/spring/ActorParser.scala | 232 +++++++++++ .../scala/akka/spring/ActorProperties.scala | 83 ++++ .../akka/spring/AkkaNamespaceHandler.scala | 23 ++ .../spring/AkkaSpringConfigurationTags.scala | 115 ++++++ .../CamelServiceBeanDefinitionParser.scala | 41 ++ .../akka/spring/CamelServiceFactoryBean.scala | 45 ++ ...onfiggyPropertyPlaceholderConfigurer.scala | 36 ++ .../DispatcherBeanDefinitionParser.scala | 28 ++ .../akka/spring/DispatcherFactoryBean.scala | 115 ++++++ .../akka/spring/DispatcherProperties.scala | 61 +++ .../scala/akka/spring/PropertyEntries.scala | 36 ++ .../scala/akka/spring/StringReflect.scala | 25 ++ .../SupervisionBeanDefinitionParser.scala | 85 ++++ .../akka/spring/SupervisionFactoryBean.scala | 91 +++++ .../src/test/java/akka/spring/Pojo.java | 51 +++ .../src/test/java/akka/spring/PojoInf.java | 13 + .../java/akka/spring/RemoteTypedActorOne.java | 6 + .../akka/spring/RemoteTypedActorOneImpl.java | 29 ++ .../java/akka/spring/RemoteTypedActorTwo.java | 6 + .../akka/spring/RemoteTypedActorTwoImpl.java | 29 ++ .../akka/spring/RemoteTypedSessionActor.java | 8 + .../spring/RemoteTypedSessionActorImpl.java | 49 +++ .../src/test/java/akka/spring/SampleBean.java | 25 ++ .../test/java/akka/spring/SampleBeanIntf.java | 6 + .../test/java/akka/spring/SampleRoute.java | 11 + .../src/test/java/akka/spring/foo/Bar.java | 17 + .../src/test/java/akka/spring/foo/Foo.java | 11 + .../src/test/java/akka/spring/foo/IBar.java | 7 + .../src/test/java/akka/spring/foo/IFoo.java | 12 + .../test/java/akka/spring/foo/IMyPojo.java | 19 + .../src/test/java/akka/spring/foo/MyPojo.java | 34 ++ .../test/java/akka/spring/foo/PingActor.java | 74 ++++ .../test/java/akka/spring/foo/PongActor.java | 18 + .../java/akka/spring/foo/StatefulPojo.java | 58 +++ akka-spring/src/test/resources/akka-test.conf | 158 +++++++ akka-spring/src/test/resources/appContext.xml | 43 ++ .../appContextCamelServiceCustom.xml | 27 ++ .../appContextCamelServiceDefault.xml | 15 + .../src/test/resources/dispatcher-config.xml | 114 ++++++ .../src/test/resources/failing-appContext.xml | 20 + .../src/test/resources/property-config.xml | 22 + .../test/resources/server-managed-config.xml | 57 +++ .../src/test/resources/supervisor-config.xml | 120 ++++++ .../src/test/resources/typed-actor-config.xml | 83 ++++ .../test/resources/untyped-actor-config.xml | 43 ++ .../src/test/scala/ActorFactoryBeanTest.scala | 113 +++++ .../scala/CamelServiceSpringFeatureTest.scala | 42 ++ ...ggyPropertyPlaceholderConfigurerSpec.scala | 42 ++ .../DispatcherBeanDefinitionParserTest.scala | 96 +++++ .../scala/DispatcherFactoryBeanTest.scala | 28 ++ .../scala/DispatcherSpringFeatureTest.scala | 138 +++++++ akka-spring/src/test/scala/ScalaDom.scala | 40 ++ .../SupervisionBeanDefinitionParserTest.scala | 127 ++++++ .../scala/SupervisionFactoryBeanTest.scala | 41 ++ .../scala/SupervisorSpringFeatureTest.scala | 57 +++ .../TypedActorBeanDefinitionParserTest.scala | 81 ++++ .../scala/TypedActorSpringFeatureTest.scala | 155 +++++++ .../scala/UntypedActorSpringFeatureTest.scala | 151 +++++++ project/build/AkkaProject.scala | 202 ++++++++- 126 files changed, 9447 insertions(+), 2 deletions(-) create mode 100644 akka-camel-typed/src/main/java/akka/camel/consume.java create mode 100644 akka-camel-typed/src/main/resources/META-INF/services/org/apache/camel/component/typed-actor create mode 100644 akka-camel-typed/src/main/scala/akka/camel/TypedCamel.scala create mode 100644 akka-camel-typed/src/main/scala/akka/camel/TypedConsumer.scala create mode 100644 akka-camel-typed/src/main/scala/akka/camel/TypedConsumerPublisher.scala create mode 100644 akka-camel-typed/src/main/scala/akka/camel/component/TypedActorComponent.scala create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumer.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumerImpl.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumer.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumerImpl.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleRouteDefinitionHandler.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedActor.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedActorImpl.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumer.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumerImpl.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumer.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumerImpl.java create mode 100644 akka-camel-typed/src/test/java/akka/camel/TypedConsumerJavaTestBase.java create mode 100644 akka-camel-typed/src/test/scala/akka/camel/TypedCamelTestSupport.scala create mode 100644 akka-camel-typed/src/test/scala/akka/camel/TypedConsumerJavaTest.scala create mode 100644 akka-camel-typed/src/test/scala/akka/camel/TypedConsumerPublishRequestorTest.scala create mode 100644 akka-camel-typed/src/test/scala/akka/camel/TypedConsumerScalaTest.scala create mode 100644 akka-camel-typed/src/test/scala/akka/camel/component/TypedActorComponentFeatureTest.scala create mode 100644 akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor create mode 100644 akka-camel/src/main/scala/akka/camel/CamelContextLifecycle.scala create mode 100644 akka-camel/src/main/scala/akka/camel/CamelService.scala create mode 100644 akka-camel/src/main/scala/akka/camel/Consumer.scala create mode 100644 akka-camel/src/main/scala/akka/camel/ConsumerPublisher.scala create mode 100644 akka-camel/src/main/scala/akka/camel/Message.scala create mode 100644 akka-camel/src/main/scala/akka/camel/Producer.scala create mode 100644 akka-camel/src/main/scala/akka/camel/PublisherRequestor.scala create mode 100644 akka-camel/src/main/scala/akka/camel/TypedCamelAccess.scala create mode 100644 akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala create mode 100644 akka-camel/src/test/java/akka/camel/ConsumerJavaTestBase.java create mode 100644 akka-camel/src/test/java/akka/camel/MessageJavaTestBase.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleErrorHandlingConsumer.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleUntypedActor.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleUntypedConsumer.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleUntypedConsumerBlocking.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleUntypedForwardingProducer.java create mode 100644 akka-camel/src/test/java/akka/camel/SampleUntypedReplyingProducer.java create mode 100644 akka-camel/src/test/scala/akka/camel/CamelContextLifecycleTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/CamelExchangeAdapterTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/CamelMessageAdapterTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/CamelServiceManagerTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/CamelTestSupport.scala create mode 100644 akka-camel/src/test/scala/akka/camel/ConsumerJavaTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/ConsumerPublishRequestorTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/ConsumerRegisteredTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/ConsumerScalaTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/MessageJavaTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/MessageScalaTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/ProducerFeatureTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/UntypedProducerFeatureTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/component/ActorComponentFeatureTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/component/ActorComponentTest.scala create mode 100644 akka-camel/src/test/scala/akka/camel/component/ActorProducerTest.scala create mode 100644 akka-kernel/src/main/scala/akka/kernel/DefaultAkkaLoader.scala create mode 100644 akka-kernel/src/main/scala/akka/kernel/EmbeddedAppServer.scala create mode 100644 akka-kernel/src/main/scala/akka/kernel/Kernel.scala create mode 100644 akka-kernel/src/main/scala/akka/servlet/Initializer.scala create mode 100644 akka-sbt-plugin/src/main/scala/AkkaKernelProject.scala create mode 100644 akka-spring/src/main/resources/META-INF/spring.handlers create mode 100644 akka-spring/src/main/resources/META-INF/spring.schemas create mode 100644 akka-spring/src/main/resources/akka/spring/akka-2.0-SNAPSHOT.xsd create mode 100644 akka-spring/src/main/scala/akka/spring/ActorBeanDefinitionParser.scala create mode 100644 akka-spring/src/main/scala/akka/spring/ActorFactoryBean.scala create mode 100644 akka-spring/src/main/scala/akka/spring/ActorParser.scala create mode 100644 akka-spring/src/main/scala/akka/spring/ActorProperties.scala create mode 100644 akka-spring/src/main/scala/akka/spring/AkkaNamespaceHandler.scala create mode 100644 akka-spring/src/main/scala/akka/spring/AkkaSpringConfigurationTags.scala create mode 100644 akka-spring/src/main/scala/akka/spring/CamelServiceBeanDefinitionParser.scala create mode 100644 akka-spring/src/main/scala/akka/spring/CamelServiceFactoryBean.scala create mode 100644 akka-spring/src/main/scala/akka/spring/ConfiggyPropertyPlaceholderConfigurer.scala create mode 100644 akka-spring/src/main/scala/akka/spring/DispatcherBeanDefinitionParser.scala create mode 100644 akka-spring/src/main/scala/akka/spring/DispatcherFactoryBean.scala create mode 100644 akka-spring/src/main/scala/akka/spring/DispatcherProperties.scala create mode 100644 akka-spring/src/main/scala/akka/spring/PropertyEntries.scala create mode 100644 akka-spring/src/main/scala/akka/spring/StringReflect.scala create mode 100644 akka-spring/src/main/scala/akka/spring/SupervisionBeanDefinitionParser.scala create mode 100644 akka-spring/src/main/scala/akka/spring/SupervisionFactoryBean.scala create mode 100644 akka-spring/src/test/java/akka/spring/Pojo.java create mode 100644 akka-spring/src/test/java/akka/spring/PojoInf.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedActorOne.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedActorOneImpl.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedActorTwo.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedActorTwoImpl.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedSessionActor.java create mode 100644 akka-spring/src/test/java/akka/spring/RemoteTypedSessionActorImpl.java create mode 100644 akka-spring/src/test/java/akka/spring/SampleBean.java create mode 100644 akka-spring/src/test/java/akka/spring/SampleBeanIntf.java create mode 100644 akka-spring/src/test/java/akka/spring/SampleRoute.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/Bar.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/Foo.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/IBar.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/IFoo.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/IMyPojo.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/MyPojo.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/PingActor.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/PongActor.java create mode 100644 akka-spring/src/test/java/akka/spring/foo/StatefulPojo.java create mode 100644 akka-spring/src/test/resources/akka-test.conf create mode 100644 akka-spring/src/test/resources/appContext.xml create mode 100644 akka-spring/src/test/resources/appContextCamelServiceCustom.xml create mode 100644 akka-spring/src/test/resources/appContextCamelServiceDefault.xml create mode 100644 akka-spring/src/test/resources/dispatcher-config.xml create mode 100644 akka-spring/src/test/resources/failing-appContext.xml create mode 100644 akka-spring/src/test/resources/property-config.xml create mode 100644 akka-spring/src/test/resources/server-managed-config.xml create mode 100644 akka-spring/src/test/resources/supervisor-config.xml create mode 100644 akka-spring/src/test/resources/typed-actor-config.xml create mode 100644 akka-spring/src/test/resources/untyped-actor-config.xml create mode 100644 akka-spring/src/test/scala/ActorFactoryBeanTest.scala create mode 100644 akka-spring/src/test/scala/CamelServiceSpringFeatureTest.scala create mode 100644 akka-spring/src/test/scala/ConfiggyPropertyPlaceholderConfigurerSpec.scala create mode 100644 akka-spring/src/test/scala/DispatcherBeanDefinitionParserTest.scala create mode 100644 akka-spring/src/test/scala/DispatcherFactoryBeanTest.scala create mode 100644 akka-spring/src/test/scala/DispatcherSpringFeatureTest.scala create mode 100644 akka-spring/src/test/scala/ScalaDom.scala create mode 100644 akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala create mode 100644 akka-spring/src/test/scala/SupervisionFactoryBeanTest.scala create mode 100644 akka-spring/src/test/scala/SupervisorSpringFeatureTest.scala create mode 100644 akka-spring/src/test/scala/TypedActorBeanDefinitionParserTest.scala create mode 100644 akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala create mode 100644 akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala diff --git a/akka-camel-typed/src/main/java/akka/camel/consume.java b/akka-camel-typed/src/main/java/akka/camel/consume.java new file mode 100644 index 0000000000..ebcc2efd29 --- /dev/null +++ b/akka-camel-typed/src/main/java/akka/camel/consume.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used by implementations of {@link akka.actor.TypedActor} + * (on method-level) to define consumer endpoints. + * + * @author Martin Krasser + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface consume { + + /** + * Consumer endpoint URI + */ + public abstract String value(); + + /** + * Route definition handler class for customizing route to annotated method. + * The handler class must have a default constructor. + */ + public abstract Class routeDefinitionHandler() + default RouteDefinitionIdentity.class; + +} diff --git a/akka-camel-typed/src/main/resources/META-INF/services/org/apache/camel/component/typed-actor b/akka-camel-typed/src/main/resources/META-INF/services/org/apache/camel/component/typed-actor new file mode 100644 index 0000000000..02efe457e6 --- /dev/null +++ b/akka-camel-typed/src/main/resources/META-INF/services/org/apache/camel/component/typed-actor @@ -0,0 +1 @@ +class=akka.camel.component.TypedActorComponent \ No newline at end of file diff --git a/akka-camel-typed/src/main/scala/akka/camel/TypedCamel.scala b/akka-camel-typed/src/main/scala/akka/camel/TypedCamel.scala new file mode 100644 index 0000000000..43acec977f --- /dev/null +++ b/akka-camel-typed/src/main/scala/akka/camel/TypedCamel.scala @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.camel + +import org.apache.camel.CamelContext + +import akka.actor.Actor._ +import akka.actor.ActorRef +import akka.camel.component.TypedActorComponent + +/** + * Module that adds typed consumer actor support to akka-camel. It is automatically + * detected by CamelService if added to the classpath. + * + * @author Martin Krasser + */ +private[camel] object TypedCamel { + private var consumerPublisher: ActorRef = _ + private var publishRequestor: ActorRef = _ + + /** + * Adds the TypedActorComponent to context. + */ + def onCamelContextInit(context: CamelContext) { + context.addComponent(TypedActorComponent.InternalSchema, new TypedActorComponent) + } + + /** + * Configures a TypedConsumerPublishRequestor and a TypedConsumerPublisher + * and re-uses the activationTracker of service. + */ + def onCamelServiceStart(service: CamelService) { + consumerPublisher = actorOf(new TypedConsumerPublisher(service.activationTracker)) + publishRequestor = actorOf(new TypedConsumerPublishRequestor) + + registerPublishRequestor + + for (event <- PublishRequestor.pastActorRegisteredEvents) publishRequestor ! event + publishRequestor ! InitPublishRequestor(consumerPublisher.start) + } + + /** + * Stops the configured Configures TypedConsumerPublishRequestor and + * TypedConsumerPublisher. + */ + def onCamelServiceStop(service: CamelService) { + unregisterPublishRequestor + consumerPublisher.stop + } + + private def registerPublishRequestor: Unit = registry.addListener(publishRequestor) + private def unregisterPublishRequestor: Unit = registry.removeListener(publishRequestor) +} \ No newline at end of file diff --git a/akka-camel-typed/src/main/scala/akka/camel/TypedConsumer.scala b/akka-camel-typed/src/main/scala/akka/camel/TypedConsumer.scala new file mode 100644 index 0000000000..e2a4cf2e58 --- /dev/null +++ b/akka-camel-typed/src/main/scala/akka/camel/TypedConsumer.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.camel + +import java.lang.reflect.Method + +import akka.actor.{TypedActor, ActorRef} + +/** + * @author Martin Krasser + */ +private[camel] object TypedConsumer { + /** + * Applies a function f to actorRef if actorRef + * references a typed consumer actor. A valid reference to a typed consumer actor is a + * local actor reference with a target actor that implements TypedActor and + * has at least one of its methods annotated with @consume (on interface or + * implementation class). For each @consume-annotated method, f + * is called with the corresponding method instance and the return value is + * added to a list which is then returned by this method. + */ + def withTypedConsumer[T](actorRef: ActorRef)(f: Method => T): List[T] = { + if (!actorRef.actor.isInstanceOf[TypedActor]) Nil + else if (actorRef.homeAddress.isDefined) Nil + else { + val typedActor = actorRef.actor.asInstanceOf[TypedActor] + // TODO: support consumer annotation inheritance + // - visit overridden methods in superclasses + // - visit implemented method declarations in interfaces + val intfClass = typedActor.proxy.getClass + val implClass = typedActor.getClass + (for (m <- intfClass.getMethods.toList; if (m.isAnnotationPresent(classOf[consume]))) yield f(m)) ++ + (for (m <- implClass.getMethods.toList; if (m.isAnnotationPresent(classOf[consume]))) yield f(m)) + } + } +} diff --git a/akka-camel-typed/src/main/scala/akka/camel/TypedConsumerPublisher.scala b/akka-camel-typed/src/main/scala/akka/camel/TypedConsumerPublisher.scala new file mode 100644 index 0000000000..8e7ed87100 --- /dev/null +++ b/akka-camel-typed/src/main/scala/akka/camel/TypedConsumerPublisher.scala @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.camel + +import java.lang.reflect.Method + +import akka.actor._ +import akka.event.EventHandler +import akka.camel.component.TypedActorComponent + +/** + * Concrete publish requestor that requests publication of typed consumer actor methods on + * ActorRegistered events and unpublication of typed consumer actor methods on + * ActorUnregistered events. + * + * @author Martin Krasser + */ +private[camel] class TypedConsumerPublishRequestor extends PublishRequestor { + def receiveActorRegistryEvent = { + case ActorRegistered(actor) => for (event <- ConsumerMethodRegistered.eventsFor(actor)) deliverCurrentEvent(event) + case ActorUnregistered(actor) => for (event <- ConsumerMethodUnregistered.eventsFor(actor)) deliverCurrentEvent(event) + } +} + +/** + * Publishes a typed consumer actor method on ConsumerMethodRegistered events and + * unpublishes a typed consumer actor method on ConsumerMethodUnregistered events. + * Publications are tracked by sending an activationTracker an EndpointActivated + * event, unpublications are tracked by sending an EndpointActivated event. + * + * @author Martin Krasser + */ +private[camel] class TypedConsumerPublisher(activationTracker: ActorRef) extends Actor { + import TypedConsumerPublisher._ + + def receive = { + case mr: ConsumerMethodRegistered => { + handleConsumerMethodRegistered(mr) + activationTracker ! EndpointActivated + } + case mu: ConsumerMethodUnregistered => { + handleConsumerMethodUnregistered(mu) + activationTracker ! EndpointDeactivated + } + case _ => { /* ignore */} + } +} + +/** + * @author Martin Krasser + */ +private[camel] object TypedConsumerPublisher { + /** + * Creates a route to a typed actor method. + */ + def handleConsumerMethodRegistered(event: ConsumerMethodRegistered) { + CamelContextManager.mandatoryContext.addRoutes(new ConsumerMethodRouteBuilder(event)) + EventHandler notifyListeners EventHandler.Info(this, "published method %s of %s at endpoint %s" format (event.methodName, event.typedActor, event.endpointUri)) + } + + /** + * Stops the route to the already un-registered typed consumer actor method. + */ + def handleConsumerMethodUnregistered(event: ConsumerMethodUnregistered) { + CamelContextManager.mandatoryContext.stopRoute(event.methodUuid) + EventHandler notifyListeners EventHandler.Info(this, "unpublished method %s of %s from endpoint %s" format (event.methodName, event.typedActor, event.endpointUri)) + } +} + +/** + * Builder of a route to a typed consumer actor method. + * + * @author Martin Krasser + */ +private[camel] class ConsumerMethodRouteBuilder(event: ConsumerMethodRegistered) extends ConsumerRouteBuilder(event.endpointUri, event.methodUuid) { + protected def routeDefinitionHandler: RouteDefinitionHandler = event.routeDefinitionHandler + protected def targetUri = "%s:%s?method=%s" format (TypedActorComponent.InternalSchema, event.methodUuid, event.methodName) +} + +/** + * A typed consumer method (un)registration event. + */ +private[camel] trait ConsumerMethodEvent extends ConsumerEvent { + val actorRef: ActorRef + val method: Method + + val uuid = actorRef.uuid.toString + val methodName = method.getName + val methodUuid = "%s_%s" format (uuid, methodName) + val typedActor = actorRef.actor.asInstanceOf[TypedActor].proxy + + lazy val routeDefinitionHandler = consumeAnnotation.routeDefinitionHandler.newInstance + lazy val consumeAnnotation = method.getAnnotation(classOf[consume]) + lazy val endpointUri = consumeAnnotation.value +} + +/** + * Event indicating that a typed consumer actor has been registered at the actor registry. For + * each @consume annotated typed actor method a separate event is created. + */ +private[camel] case class ConsumerMethodRegistered(actorRef: ActorRef, method: Method) extends ConsumerMethodEvent + +/** + * Event indicating that a typed consumer actor has been unregistered from the actor registry. For + * each @consume annotated typed actor method a separate event is created. + */ +private[camel] case class ConsumerMethodUnregistered(actorRef: ActorRef, method: Method) extends ConsumerMethodEvent + +/** + * @author Martin Krasser + */ +private[camel] object ConsumerMethodRegistered { + /** + * Creates a list of ConsumerMethodRegistered event messages for a typed consumer actor or an empty + * list if actorRef doesn't reference a typed consumer actor. + */ + def eventsFor(actorRef: ActorRef): List[ConsumerMethodRegistered] = { + TypedConsumer.withTypedConsumer(actorRef: ActorRef) { + m => ConsumerMethodRegistered(actorRef, m) + } + } +} + +/** + * @author Martin Krasser + */ +private[camel] object ConsumerMethodUnregistered { + /** + * Creates a list of ConsumerMethodUnregistered event messages for a typed consumer actor or an empty + * list if actorRef doesn't reference a typed consumer actor. + */ + def eventsFor(actorRef: ActorRef): List[ConsumerMethodUnregistered] = { + TypedConsumer.withTypedConsumer(actorRef) { + m => ConsumerMethodUnregistered(actorRef, m) + } + } +} diff --git a/akka-camel-typed/src/main/scala/akka/camel/component/TypedActorComponent.scala b/akka-camel-typed/src/main/scala/akka/camel/component/TypedActorComponent.scala new file mode 100644 index 0000000000..2731c4ab07 --- /dev/null +++ b/akka-camel-typed/src/main/scala/akka/camel/component/TypedActorComponent.scala @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel.component + +import java.util.Map +import java.util.concurrent.ConcurrentHashMap + +import org.apache.camel.CamelContext +import org.apache.camel.component.bean._ + +import akka.actor._ + +/** + * @author Martin Krasser + */ +object TypedActorComponent { + /** + * Default schema name for typed actor endpoint URIs. + */ + val InternalSchema = "typed-actor-internal" +} + +/** + * Camel component for exchanging messages with typed actors. This component + * tries to obtain the typed actor from Actor.registry if the + * schema is TypedActorComponent.InternalSchema. If the schema + * name is typed-actor this component tries to obtain the typed + * actor from the CamelContext's registry. + * + * @see org.apache.camel.component.bean.BeanComponent + * + * @author Martin Krasser + */ +class TypedActorComponent extends BeanComponent { + val typedActorRegistry = new ConcurrentHashMap[String, AnyRef] + + /** + * Creates an org.apache.camel.component.bean.BeanEndpoint with a custom + * bean holder that uses Actor.registry for getting access to typed actors + * (beans). + * + * @see akka.camel.component.TypedActorHolder + */ + override def createEndpoint(uri: String, remaining: String, parameters: Map[String, AnyRef]) = { + val endpoint = new BeanEndpoint(uri, this) + endpoint.setBeanName(remaining) + endpoint.setBeanHolder(createBeanHolder(uri, remaining)) + setProperties(endpoint.getProcessor, parameters) + endpoint + } + + private def createBeanHolder(uri: String, beanName: String) = + new TypedActorHolder(uri, getCamelContext, beanName).createCacheHolder +} + +/** + * org.apache.camel.component.bean.BeanHolder implementation that uses + * Actor.registry for getting access to typed actors. + * + * @author Martin Krasser + */ +class TypedActorHolder(uri: String, context: CamelContext, name: String) + extends RegistryBean(context, name) { + + /** + * Returns an akka.camel.component.TypedActorInfo instance. + */ + override def getBeanInfo: BeanInfo = + new TypedActorInfo(getContext, getBean.getClass, getParameterMappingStrategy) + + /** + * Obtains a typed actor from Actor.registry if the schema is + * TypedActorComponent.InternalSchema. If the schema name is + * typed-actor this method obtains the typed actor from the + * CamelContext's registry. + * + * @return a typed actor or null. + */ + override def getBean: AnyRef = { + val internal = uri.startsWith(TypedActorComponent.InternalSchema) + if (internal) Actor.registry.typedActorFor(uuidFrom(getName)) getOrElse null else super.getBean + } +} + +/** + * Typed actor meta information. + * + * @author Martin Krasser + */ +class TypedActorInfo(context: CamelContext, clazz: Class[_], strategy: ParameterMappingStrategy) + extends BeanInfo(context, clazz, strategy) { + + /** + * Introspects AspectWerkz proxy classes. + * + * @param clazz AspectWerkz proxy class. + */ + protected override def introspect(clazz: Class[_]): Unit = { + + // TODO: fix target class detection in BeanInfo.introspect(Class) + // Camel assumes that classes containing a '$$' in the class name + // are classes generated with CGLIB. This conflicts with proxies + // created from interfaces with AspectWerkz. Once the fix is in + // place this method can be removed. + + for (method <- clazz.getDeclaredMethods) { + if (isValidMethod(clazz, method)) { + introspect(clazz, method) + } + } + val superclass = clazz.getSuperclass + if ((superclass ne null) && !superclass.equals(classOf[AnyRef])) { + introspect(superclass) + } + } +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumer.java b/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumer.java new file mode 100644 index 0000000000..d8a8c79440 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumer.java @@ -0,0 +1,11 @@ +package akka.camel; + +/** + * @author Martin Krasser + */ +public interface SampleErrorHandlingTypedConsumer { + + @consume(value="direct:error-handler-test-java-typed", routeDefinitionHandler=SampleRouteDefinitionHandler.class) + String willFail(String s); + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumerImpl.java b/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumerImpl.java new file mode 100644 index 0000000000..cfa42a7521 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleErrorHandlingTypedConsumerImpl.java @@ -0,0 +1,14 @@ +package akka.camel; + +import akka.actor.TypedActor; + +/** + * @author Martin Krasser + */ +public class SampleErrorHandlingTypedConsumerImpl extends TypedActor implements SampleErrorHandlingTypedConsumer { + + public String willFail(String s) { + throw new RuntimeException(String.format("error: %s", s)); + } + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumer.java b/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumer.java new file mode 100644 index 0000000000..41a3c3f057 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumer.java @@ -0,0 +1,12 @@ +package akka.camel; + +import akka.camel.consume; + +/** + * @author Martin Krasser + */ +public interface SampleRemoteTypedConsumer { + + @consume("direct:remote-typed-consumer") + public String foo(String s); +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumerImpl.java b/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumerImpl.java new file mode 100644 index 0000000000..d7fb463b44 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleRemoteTypedConsumerImpl.java @@ -0,0 +1,14 @@ +package akka.camel; + +import akka.actor.TypedActor; + +/** + * @author Martin Krasser + */ +public class SampleRemoteTypedConsumerImpl extends TypedActor implements SampleRemoteTypedConsumer { + + public String foo(String s) { + return String.format("remote typed actor: %s", s); + } + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleRouteDefinitionHandler.java b/akka-camel-typed/src/test/java/akka/camel/SampleRouteDefinitionHandler.java new file mode 100644 index 0000000000..f1a99aa7d4 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleRouteDefinitionHandler.java @@ -0,0 +1,14 @@ +package akka.camel; + +import org.apache.camel.builder.Builder; +import org.apache.camel.model.ProcessorDefinition; +import org.apache.camel.model.RouteDefinition; + +/** + * @author Martin Krasser + */ +public class SampleRouteDefinitionHandler implements RouteDefinitionHandler { + public ProcessorDefinition onRouteDefinition(RouteDefinition rd) { + return rd.onException(Exception.class).handled(true).transform(Builder.exceptionMessage()).end(); + } +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedActor.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedActor.java new file mode 100644 index 0000000000..798d07a66c --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedActor.java @@ -0,0 +1,9 @@ +package akka.camel; + +/** + * @author Martin Krasser + */ +public interface SampleTypedActor { + + public String foo(String s); +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedActorImpl.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedActorImpl.java new file mode 100644 index 0000000000..773e3ec3ec --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedActorImpl.java @@ -0,0 +1,14 @@ +package akka.camel; + +import akka.actor.TypedActor; + +/** + * @author Martin Krasser + */ +public class SampleTypedActorImpl extends TypedActor implements SampleTypedActor { + + public String foo(String s) { + return String.format("foo: %s", s); + } + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumer.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumer.java new file mode 100644 index 0000000000..26283d8e61 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumer.java @@ -0,0 +1,20 @@ +package akka.camel; + +import org.apache.camel.Body; +import org.apache.camel.Header; + +import akka.camel.consume; + +/** + * @author Martin Krasser + */ +public interface SampleTypedConsumer { + + public String m1(String b, String h); + public String m2(@Body String b, @Header("test") String h); + public String m3(@Body String b, @Header("test") String h); + + @consume("direct:m4") + public String m4(@Body String b, @Header("test") String h); + public void m5(@Body String b, @Header("test") String h); +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumerImpl.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumerImpl.java new file mode 100644 index 0000000000..3bbe7a9442 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedConsumerImpl.java @@ -0,0 +1,30 @@ +package akka.camel; + +import akka.actor.TypedActor; + +/** + * @author Martin Krasser + */ +public class SampleTypedConsumerImpl extends TypedActor implements SampleTypedConsumer { + + public String m1(String b, String h) { + return "m1: " + b + " " + h; + } + + @consume("direct:m2") + public String m2(String b, String h) { + return "m2: " + b + " " + h; + } + + @consume("direct:m3") + public String m3(String b, String h) { + return "m3: " + b + " " + h; + } + + public String m4(String b, String h) { + return "m4: " + b + " " + h; + } + + public void m5(String b, String h) { + } +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumer.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumer.java new file mode 100644 index 0000000000..ff0b7bc715 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumer.java @@ -0,0 +1,13 @@ +package akka.camel; + +import akka.camel.consume; + +/** + * @author Martin Krasser + */ +public interface SampleTypedSingleConsumer { + + @consume("direct:foo") + public void foo(String b); + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumerImpl.java b/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumerImpl.java new file mode 100644 index 0000000000..27fbfdaa0d --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/SampleTypedSingleConsumerImpl.java @@ -0,0 +1,13 @@ +package akka.camel; + +import akka.actor.TypedActor; + +/** + * @author Martin Krasser + */ +public class SampleTypedSingleConsumerImpl extends TypedActor implements SampleTypedSingleConsumer { + + public void foo(String b) { + } + +} diff --git a/akka-camel-typed/src/test/java/akka/camel/TypedConsumerJavaTestBase.java b/akka-camel-typed/src/test/java/akka/camel/TypedConsumerJavaTestBase.java new file mode 100644 index 0000000000..64e8197de8 --- /dev/null +++ b/akka-camel-typed/src/test/java/akka/camel/TypedConsumerJavaTestBase.java @@ -0,0 +1,46 @@ +package akka.camel; + +import akka.actor.TypedActor; +import akka.japi.SideEffect; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static akka.actor.Actors.*; +import static akka.camel.CamelContextManager.*; +import static akka.camel.CamelServiceManager.*; + +import static org.junit.Assert.*; + +/** + * @author Martin Krasser + */ +public class TypedConsumerJavaTestBase { + + private SampleErrorHandlingTypedConsumer consumer; + + @BeforeClass + public static void setUpBeforeClass() { + startCamelService(); + } + + @AfterClass + public static void tearDownAfterClass() { + stopCamelService(); + registry().shutdownAll(); + } + + @Test + public void shouldHandleExceptionThrownByTypedActorAndGenerateCustomResponse() { + getMandatoryService().awaitEndpointActivation(1, new SideEffect() { + public void apply() { + consumer = TypedActor.newInstance( + SampleErrorHandlingTypedConsumer.class, + SampleErrorHandlingTypedConsumerImpl.class); + } + }); + String result = getMandatoryTemplate().requestBody("direct:error-handler-test-java-typed", "hello", String.class); + assertEquals("error: hello", result); + } +} diff --git a/akka-camel-typed/src/test/scala/akka/camel/TypedCamelTestSupport.scala b/akka-camel-typed/src/test/scala/akka/camel/TypedCamelTestSupport.scala new file mode 100644 index 0000000000..ea67772b88 --- /dev/null +++ b/akka-camel-typed/src/test/scala/akka/camel/TypedCamelTestSupport.scala @@ -0,0 +1,66 @@ +package akka.camel + +import java.util.concurrent.CountDownLatch + +import collection.mutable.Buffer + +import akka.actor.Actor + +object TypedCamelTestSupport { + type Handler = PartialFunction[Any, Any] + + trait TestActor extends Actor { + def receive = { + case msg => { + handler(msg) + } + } + + def handler: Handler + } + + trait Countdown { this: Actor => + var latch: CountDownLatch = new CountDownLatch(0) + def countdown: Handler = { + case SetExpectedMessageCount(num) => { + latch = new CountDownLatch(num) + self.reply(latch) + } + case msg => latch.countDown + } + } + + trait Respond { this: Actor => + def respond: Handler = { + case msg: Message => self.reply(response(msg)) + } + + def response(msg: Message): Any = "Hello %s" format msg.body + } + + trait Retain { this: Actor => + val messages = Buffer[Any]() + + def retain: Handler = { + case GetRetainedMessage => self.reply(messages.last) + case GetRetainedMessages(p) => self.reply(messages.toList.filter(p)) + case msg => { + messages += msg + msg + } + } + } + + trait Noop { this: Actor => + def noop: Handler = { + case msg => msg + } + } + + case class SetExpectedMessageCount(num: Int) + case class GetRetainedMessage() + case class GetRetainedMessages(p: Any => Boolean) { + def this() = this(_ => true) + } +} + diff --git a/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerJavaTest.scala b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerJavaTest.scala new file mode 100644 index 0000000000..d887a378ad --- /dev/null +++ b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerJavaTest.scala @@ -0,0 +1,5 @@ +package akka.camel + +import org.scalatest.junit.JUnitSuite + +class TypedConsumerJavaTest extends TypedConsumerJavaTestBase with JUnitSuite diff --git a/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerPublishRequestorTest.scala b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerPublishRequestorTest.scala new file mode 100644 index 0000000000..16fd30bfe9 --- /dev/null +++ b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerPublishRequestorTest.scala @@ -0,0 +1,87 @@ +package akka.camel + +import java.util.concurrent.{CountDownLatch, TimeUnit} + +import org.junit.{Before, After, Test} +import org.scalatest.junit.JUnitSuite + +import akka.actor._ +import akka.actor.Actor._ +import akka.camel.TypedCamelTestSupport.{SetExpectedMessageCount => SetExpectedTestMessageCount, _} + +class TypedConsumerPublishRequestorTest extends JUnitSuite { + import TypedConsumerPublishRequestorTest._ + + var publisher: ActorRef = _ + var requestor: ActorRef = _ + var consumer: ActorRef = _ + + val ascendingMethodName = (r1: ConsumerMethodRegistered, r2: ConsumerMethodRegistered) => + r1.method.getName < r2.method.getName + + @Before def setUp: Unit = { + publisher = actorOf(new TypedConsumerPublisherMock).start + requestor = actorOf(new TypedConsumerPublishRequestor).start + requestor ! InitPublishRequestor(publisher) + consumer = actorOf(new Actor with Consumer { + def endpointUri = "mock:test" + protected def receive = null + }).start + } + + @After def tearDown = { + Actor.registry.removeListener(requestor); + Actor.registry.shutdownAll + } + + @Test def shouldReceiveOneConsumerMethodRegisteredEvent = { + Actor.registry.addListener(requestor) + val latch = (publisher !! SetExpectedTestMessageCount(1)).as[CountDownLatch].get + val obj = TypedActor.newInstance(classOf[SampleTypedSingleConsumer], classOf[SampleTypedSingleConsumerImpl]) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val event = (publisher !! GetRetainedMessage).as[ConsumerMethodRegistered].get + assert(event.endpointUri === "direct:foo") + assert(event.typedActor === obj) + assert(event.methodName === "foo") + } + + @Test def shouldReceiveOneConsumerMethodUnregisteredEvent = { + val obj = TypedActor.newInstance(classOf[SampleTypedSingleConsumer], classOf[SampleTypedSingleConsumerImpl]) + val latch = (publisher !! SetExpectedTestMessageCount(1)).as[CountDownLatch].get + Actor.registry.addListener(requestor) + TypedActor.stop(obj) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val event = (publisher !! GetRetainedMessage).as[ConsumerMethodUnregistered].get + assert(event.endpointUri === "direct:foo") + assert(event.typedActor === obj) + assert(event.methodName === "foo") + } + + @Test def shouldReceiveThreeConsumerMethodRegisteredEvents = { + Actor.registry.addListener(requestor) + val latch = (publisher !! SetExpectedTestMessageCount(3)).as[CountDownLatch].get + val obj = TypedActor.newInstance(classOf[SampleTypedConsumer], classOf[SampleTypedConsumerImpl]) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val request = GetRetainedMessages(_.isInstanceOf[ConsumerMethodRegistered]) + val events = (publisher !! request).as[List[ConsumerMethodRegistered]].get + assert(events.map(_.method.getName).sortWith(_ < _) === List("m2", "m3", "m4")) + } + + @Test def shouldReceiveThreeConsumerMethodUnregisteredEvents = { + val obj = TypedActor.newInstance(classOf[SampleTypedConsumer], classOf[SampleTypedConsumerImpl]) + val latch = (publisher !! SetExpectedTestMessageCount(3)).as[CountDownLatch].get + Actor.registry.addListener(requestor) + TypedActor.stop(obj) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val request = GetRetainedMessages(_.isInstanceOf[ConsumerMethodUnregistered]) + val events = (publisher !! request).as[List[ConsumerMethodUnregistered]].get + assert(events.map(_.method.getName).sortWith(_ < _) === List("m2", "m3", "m4")) + } +} + +object TypedConsumerPublishRequestorTest { + class TypedConsumerPublisherMock extends TestActor with Retain with Countdown { + def handler = retain andThen countdown + } +} + diff --git a/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerScalaTest.scala b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerScalaTest.scala new file mode 100644 index 0000000000..5041d09f28 --- /dev/null +++ b/akka-camel-typed/src/test/scala/akka/camel/TypedConsumerScalaTest.scala @@ -0,0 +1,99 @@ +package akka.camel + +import org.apache.camel.CamelExecutionException + +import org.scalatest.{BeforeAndAfterAll, WordSpec} +import org.scalatest.matchers.MustMatchers + +import akka.actor.Actor._ +import akka.actor._ + +/** + * @author Martin Krasser + */ +class TypedConsumerScalaTest extends WordSpec with BeforeAndAfterAll with MustMatchers { + import CamelContextManager.mandatoryTemplate + import TypedConsumerScalaTest._ + + var service: CamelService = _ + + override protected def beforeAll = { + registry.shutdownAll + service = CamelServiceManager.startCamelService + } + + override protected def afterAll = { + service.stop + registry.shutdownAll + } + + "A responding, typed consumer" when { + var actor: SampleTypedConsumer = null + "started" must { + "support in-out message exchanges via its endpoints" in { + service.awaitEndpointActivation(3) { + actor = TypedActor.newInstance(classOf[SampleTypedConsumer], classOf[SampleTypedConsumerImpl]) + } must be (true) + mandatoryTemplate.requestBodyAndHeader("direct:m2", "x", "test", "y") must equal ("m2: x y") + mandatoryTemplate.requestBodyAndHeader("direct:m3", "x", "test", "y") must equal ("m3: x y") + mandatoryTemplate.requestBodyAndHeader("direct:m4", "x", "test", "y") must equal ("m4: x y") + } + } + "stopped" must { + "not support in-out message exchanges via its endpoints" in { + service.awaitEndpointDeactivation(3) { + TypedActor.stop(actor) + } must be (true) + intercept[CamelExecutionException] { + mandatoryTemplate.requestBodyAndHeader("direct:m2", "x", "test", "y") + } + intercept[CamelExecutionException] { + mandatoryTemplate.requestBodyAndHeader("direct:m3", "x", "test", "y") + } + intercept[CamelExecutionException] { + mandatoryTemplate.requestBodyAndHeader("direct:m4", "x", "test", "y") + } + } + } + } + + "A responding, typed consumer (Scala)" when { + var actor: TestTypedConsumer = null + "started" must { + "support in-out message exchanges via its endpoints" in { + service.awaitEndpointActivation(2) { + actor = TypedActor.newInstance(classOf[TestTypedConsumer], classOf[TestTypedConsumerImpl]) + } must be (true) + mandatoryTemplate.requestBody("direct:publish-test-3", "x") must equal ("foo: x") + mandatoryTemplate.requestBody("direct:publish-test-4", "x") must equal ("bar: x") + } + } + "stopped" must { + "not support in-out message exchanges via its endpoints" in { + service.awaitEndpointDeactivation(2) { + TypedActor.stop(actor) + } must be (true) + intercept[CamelExecutionException] { + mandatoryTemplate.requestBody("direct:publish-test-3", "x") + } + intercept[CamelExecutionException] { + mandatoryTemplate.requestBody("direct:publish-test-4", "x") + } + } + } + } +} + +object TypedConsumerScalaTest { + trait TestTypedConsumer { + @consume("direct:publish-test-3") + def foo(s: String): String + def bar(s: String): String + } + + class TestTypedConsumerImpl extends TypedActor with TestTypedConsumer { + def foo(s: String) = "foo: %s" format s + @consume("direct:publish-test-4") + def bar(s: String) = "bar: %s" format s + } +} diff --git a/akka-camel-typed/src/test/scala/akka/camel/component/TypedActorComponentFeatureTest.scala b/akka-camel-typed/src/test/scala/akka/camel/component/TypedActorComponentFeatureTest.scala new file mode 100644 index 0000000000..ebbd58e66d --- /dev/null +++ b/akka-camel-typed/src/test/scala/akka/camel/component/TypedActorComponentFeatureTest.scala @@ -0,0 +1,110 @@ +package akka.camel.component + +import org.apache.camel._ +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.impl.{DefaultCamelContext, SimpleRegistry} +import org.scalatest.{BeforeAndAfterEach, BeforeAndAfterAll, FeatureSpec} + +import akka.actor.{Actor, TypedActor} +import akka.camel._ +import akka.util.ReflectiveAccess.TypedActorModule + +/** + * @author Martin Krasser + */ +class TypedActorComponentFeatureTest extends FeatureSpec with BeforeAndAfterAll with BeforeAndAfterEach { + import TypedActorComponentFeatureTest._ + import CamelContextManager.mandatoryTemplate + + var typedConsumerUuid: String = _ + + override protected def beforeAll = { + val typedActor = TypedActor.newInstance(classOf[SampleTypedActor], classOf[SampleTypedActorImpl]) // not a consumer + val typedConsumer = TypedActor.newInstance(classOf[SampleTypedConsumer], classOf[SampleTypedConsumerImpl]) + + typedConsumerUuid = TypedActorModule.typedActorObjectInstance.get.actorFor(typedConsumer).get.uuid.toString + + val registry = new SimpleRegistry + // external registration + registry.put("ta", typedActor) + + CamelContextManager.init(new DefaultCamelContext(registry)) + CamelContextManager.mandatoryContext.addRoutes(new CustomRouteBuilder) + CamelContextManager.start + } + + override protected def afterAll = { + CamelContextManager.stop + Actor.registry.shutdownAll + } + + feature("Communicate with an internally-registered typed actor using typed-actor-internal endpoint URIs") { + import TypedActorComponent.InternalSchema + import ExchangePattern._ + + scenario("two-way communication with method returning String") { + val result1 = mandatoryTemplate.requestBodyAndHeader("%s:%s?method=m2" format (InternalSchema, typedConsumerUuid), "x", "test", "y") + val result2 = mandatoryTemplate.requestBodyAndHeader("%s:%s?method=m4" format (InternalSchema, typedConsumerUuid), "x", "test", "y") + assert(result1 === "m2: x y") + assert(result2 === "m4: x y") + } + + scenario("two-way communication with method returning void") { + val result = mandatoryTemplate.requestBodyAndHeader("%s:%s?method=m5" format (InternalSchema, typedConsumerUuid), "x", "test", "y") + assert(result === "x") // returns initial body + } + + scenario("one-way communication with method returning String") { + val result = mandatoryTemplate.send("%s:%s?method=m2" format (InternalSchema, typedConsumerUuid), InOnly, new Processor { + def process(exchange: Exchange) = { + exchange.getIn.setBody("x") + exchange.getIn.setHeader("test", "y") + } + }); + assert(result.getPattern === InOnly) + assert(result.getIn.getBody === "m2: x y") + assert(result.getOut.getBody === null) + } + + scenario("one-way communication with method returning void") { + val result = mandatoryTemplate.send("%s:%s?method=m5" format (InternalSchema, typedConsumerUuid), InOnly, new Processor { + def process(exchange: Exchange) = { + exchange.getIn.setBody("x") + exchange.getIn.setHeader("test", "y") + } + }); + assert(result.getPattern === InOnly) + assert(result.getIn.getBody === "x") + assert(result.getOut.getBody === null) + } + + } + + feature("Communicate with an internally-registered typed actor using typed-actor endpoint URIs") { + scenario("communication not possible") { + intercept[ResolveEndpointFailedException] { + mandatoryTemplate.requestBodyAndHeader("typed-actor:%s?method=m2" format typedConsumerUuid, "x", "test", "y") + } + } + } + + feature("Communicate with an externally-registered typed actor using typed-actor endpoint URIs") { + scenario("two-way communication with method returning String") { + val result = mandatoryTemplate.requestBody("typed-actor:ta?method=foo", "test") + assert(result === "foo: test") + } + + scenario("two-way communication with method returning String via custom route") { + val result = mandatoryTemplate.requestBody("direct:test", "test") + assert(result === "foo: test") + } + } +} + +object TypedActorComponentFeatureTest { + class CustomRouteBuilder extends RouteBuilder { + def configure = { + from("direct:test").to("typed-actor:ta?method=foo") + } + } +} 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..386928c5a8 --- /dev/null +++ b/akka-camel/src/main/resources/META-INF/services/org/apache/camel/component/actor @@ -0,0 +1 @@ +class=akka.camel.component.ActorComponent \ No newline at end of file diff --git a/akka-camel/src/main/scala/akka/camel/CamelContextLifecycle.scala b/akka-camel/src/main/scala/akka/camel/CamelContextLifecycle.scala new file mode 100644 index 0000000000..d273c9cb6c --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/CamelContextLifecycle.scala @@ -0,0 +1,186 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel + +import org.apache.camel.{ ProducerTemplate, CamelContext } +import org.apache.camel.impl.DefaultCamelContext + +import akka.event.EventHandler +import akka.japi.{ Option ⇒ JOption } + +import TypedCamelAccess._ + +/** + * Manages the lifecycle of a CamelContext. Allowed transitions are + * init -> start -> stop -> init -> ... etc. + * + * @author Martin Krasser + */ +trait CamelContextLifecycle { + // TODO: enforce correct state transitions + // valid: init -> start -> stop -> init ... + + private var _context: Option[CamelContext] = None + private var _template: Option[ProducerTemplate] = None + + private var _initialized = false + private var _started = false + + /** + * Returns Some(CamelContext) (containing the current CamelContext) + * if CamelContextLifecycle has been initialized, otherwise None. + */ + def context: Option[CamelContext] = _context + + /** + * Returns Some(ProducerTemplate) (containing the current ProducerTemplate) + * if CamelContextLifecycle has been initialized, otherwise None. + */ + def template: Option[ProducerTemplate] = _template + + /** + * Returns Some(CamelContext) (containing the current CamelContext) + * if CamelContextLifecycle has been initialized, otherwise None. + *

+ * Java API. + */ + def getContext: JOption[CamelContext] = context + + /** + * Returns Some(ProducerTemplate) (containing the current ProducerTemplate) + * if CamelContextLifecycle has been initialized, otherwise None. + *

+ * Java API. + */ + def getTemplate: JOption[ProducerTemplate] = template + + /** + * Returns the current CamelContext if this CamelContextLifecycle + * has been initialized, otherwise throws an IllegalStateException. + */ + def mandatoryContext = + if (context.isDefined) context.get + else throw new IllegalStateException("no current CamelContext") + + /** + * Returns the current ProducerTemplate if this CamelContextLifecycle + * has been initialized, otherwise throws an IllegalStateException. + */ + def mandatoryTemplate = + if (template.isDefined) template.get + else throw new IllegalStateException("no current ProducerTemplate") + + /** + * Returns the current CamelContext if this CamelContextLifecycle + * has been initialized, otherwise throws an IllegalStateException. + *

+ * Java API. + */ + def getMandatoryContext = mandatoryContext + + /** + * Returns the current ProducerTemplate if this CamelContextLifecycle + * has been initialized, otherwise throws an IllegalStateException. + *

+ * Java API. + */ + def getMandatoryTemplate = mandatoryTemplate + + def initialized = _initialized + def started = _started + + /** + * Starts the CamelContext and an associated ProducerTemplate. + */ + def start = { + for { + c ← context + t ← template + } { + c.start + t.start + _started = true + EventHandler notifyListeners EventHandler.Info(this, "Camel context started") + } + } + + /** + * Stops the CamelContext and the associated ProducerTemplate. + */ + def stop = { + for { + t ← template + c ← context + } { + t.stop + c.stop + _started = false + _initialized = false + EventHandler notifyListeners EventHandler.Info(this, "Camel context stopped") + } + } + + /** + * Initializes this instance a new DefaultCamelContext. + */ + def init(): Unit = init(new DefaultCamelContext) + + /** + * Initializes this instance with the given CamelContext. For the passed context + * stream-caching is enabled. If applications want to disable stream-caching they can do so + * after this method returned and prior to calling start. + */ + def init(context: CamelContext) { + context.setStreamCaching(true) + + for (tc ← TypedCamelModule.typedCamelObject) tc.onCamelContextInit(context) + + this._context = Some(context) + this._template = Some(context.createProducerTemplate) + + _initialized = true + EventHandler notifyListeners EventHandler.Info(this, "Camel context initialized") + } +} + +/** + * Manages a global CamelContext and an associated ProducerTemplate. + */ +object CamelContextManager extends CamelContextLifecycle { + + // ----------------------------------------------------- + // The inherited getters aren't statically accessible + // from Java. Therefore, they are redefined here. + // TODO: investigate if this is a Scala bug. + // ----------------------------------------------------- + + /** + * see CamelContextLifecycle.getContext + *

+ * Java API. + */ + override def getContext: JOption[CamelContext] = super.getContext + + /** + * see CamelContextLifecycle.getTemplate + *

+ * Java API. + */ + override def getTemplate: JOption[ProducerTemplate] = super.getTemplate + + /** + * see CamelContextLifecycle.getMandatoryContext + *

+ * Java API. + */ + override def getMandatoryContext = super.getMandatoryContext + + /** + * see CamelContextLifecycle.getMandatoryTemplate + *

+ * Java API. + */ + override def getMandatoryTemplate = super.getMandatoryTemplate +} diff --git a/akka-camel/src/main/scala/akka/camel/CamelService.scala b/akka-camel/src/main/scala/akka/camel/CamelService.scala new file mode 100644 index 0000000000..b9f4cc3432 --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/CamelService.scala @@ -0,0 +1,270 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.camel + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +import org.apache.camel.CamelContext + +import akka.actor.Actor._ +import akka.config.Config._ +import akka.japi.{ SideEffect, Option ⇒ JOption } +import akka.util.Bootable + +import TypedCamelAccess._ + +/** + * Publishes consumer actors at their Camel endpoints. Consumer actors are published asynchronously when + * they are started and un-published asynchronously when they are stopped. The CamelService is notified + * about actor life cycle by registering listeners at Actor.registry. + *

+ * If the akka-camel-typed jar is on the classpath, it is automatically detected by the CamelService. The + * optional akka-camel-typed jar provides support for typed consumer actors. + * + * @author Martin Krasser + */ +trait CamelService extends Bootable { + private[camel] val activationTracker = actorOf(new ActivationTracker) + private[camel] val consumerPublisher = actorOf(new ConsumerPublisher(activationTracker)) + private[camel] val publishRequestor = actorOf(new ConsumerPublishRequestor) + + private val serviceEnabled = config.getList("akka.enabled-modules").exists(_ == "camel") + + /** + * Starts this CamelService if the akka.enabled-modules list contains "camel". + */ + abstract override def onLoad = { + super.onLoad + if (serviceEnabled) start + } + + /** + * Stops this CamelService if the akka.enabled-modules list contains "camel". + */ + abstract override def onUnload = { + if (serviceEnabled) stop + super.onUnload + } + + @deprecated("use start() instead", "1.1") + def load = start + + @deprecated("use stop() instead", "1.1") + def unload = stop + + /** + * Starts this CamelService. + */ + def start: CamelService = { + // Only init and start if not already done by application + if (!CamelContextManager.initialized) CamelContextManager.init + if (!CamelContextManager.started) CamelContextManager.start + + registerPublishRequestor + + activationTracker.start + consumerPublisher.start + + // send registration events for all (un)typed actors that have been registered in the past. + for (event ← PublishRequestor.pastActorRegisteredEvents) publishRequestor ! event + + // init publishRequestor so that buffered and future events are delivered to consumerPublisher + publishRequestor ! InitPublishRequestor(consumerPublisher) + + for (tc ← TypedCamelModule.typedCamelObject) tc.onCamelServiceStart(this) + + // Register this instance as current CamelService and return it + CamelServiceManager.register(this) + CamelServiceManager.mandatoryService + } + + /** + * Stops this CamelService. + */ + def stop = { + // Unregister this instance as current CamelService + CamelServiceManager.unregister(this) + + for (tc ← TypedCamelModule.typedCamelObject) tc.onCamelServiceStop(this) + + // Remove related listeners from registry + unregisterPublishRequestor + + // Stop related services + consumerPublisher.stop + activationTracker.stop + CamelContextManager.stop + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be activated + * during execution of f. The wait-timeout is by default 10 seconds. Other timeout + * values can be set via the timeout and timeUnit parameters. + */ + def awaitEndpointActivation(count: Int, timeout: Long = 10, timeUnit: TimeUnit = TimeUnit.SECONDS)(f: ⇒ Unit): Boolean = { + val activation = expectEndpointActivationCount(count) + f; activation.await(timeout, timeUnit) + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be de-activated + * during execution of f. The wait-timeout is by default 10 seconds. Other timeout + * values can be set via the timeout and timeUnit + * parameters. + */ + def awaitEndpointDeactivation(count: Int, timeout: Long = 10, timeUnit: TimeUnit = TimeUnit.SECONDS)(f: ⇒ Unit): Boolean = { + val activation = expectEndpointDeactivationCount(count) + f; activation.await(timeout, timeUnit) + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be activated + * during execution of p. The wait timeout is 10 seconds. + *

+ * Java API + */ + def awaitEndpointActivation(count: Int, p: SideEffect): Boolean = { + awaitEndpointActivation(count, 10, TimeUnit.SECONDS, p) + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be activated + * during execution of p. Timeout values can be set via the + * timeout and timeUnit parameters. + *

+ * Java API + */ + def awaitEndpointActivation(count: Int, timeout: Long, timeUnit: TimeUnit, p: SideEffect): Boolean = { + awaitEndpointActivation(count, timeout, timeUnit) { p.apply } + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be de-activated + * during execution of p. The wait timeout is 10 seconds. + *

+ * Java API + */ + def awaitEndpointDeactivation(count: Int, p: SideEffect): Boolean = { + awaitEndpointDeactivation(count, 10, TimeUnit.SECONDS, p) + } + + /** + * Waits for an expected number (count) of consumer actor endpoints to be de-activated + * during execution of p. Timeout values can be set via the + * timeout and timeUnit parameters. + *

+ * Java API + */ + def awaitEndpointDeactivation(count: Int, timeout: Long, timeUnit: TimeUnit, p: SideEffect): Boolean = { + awaitEndpointDeactivation(count, timeout, timeUnit) { p.apply } + } + + /** + * Sets an expectation on the number of upcoming endpoint activations and returns + * a CountDownLatch that can be used to wait for the activations to occur. Endpoint + * activations that occurred in the past are not considered. + */ + private def expectEndpointActivationCount(count: Int): CountDownLatch = + (activationTracker !! SetExpectedActivationCount(count)).as[CountDownLatch].get + + /** + * Sets an expectation on the number of upcoming endpoint de-activations and returns + * a CountDownLatch that can be used to wait for the de-activations to occur. Endpoint + * de-activations that occurred in the past are not considered. + */ + private def expectEndpointDeactivationCount(count: Int): CountDownLatch = + (activationTracker !! SetExpectedDeactivationCount(count)).as[CountDownLatch].get + + private[camel] def registerPublishRequestor: Unit = + registry.addListener(publishRequestor) + + private[camel] def unregisterPublishRequestor: Unit = + registry.removeListener(publishRequestor) +} + +/** + * Manages a CamelService (the 'current' CamelService). + * + * @author Martin Krasser + */ +object CamelServiceManager { + + /** + * The current CamelService which is defined when a CamelService has been started. + */ + private var _current: Option[CamelService] = None + + /** + * Starts a new CamelService, makes it the current CamelService and returns it. + * + * @see CamelService#start + * @see CamelService#onLoad + */ + def startCamelService = CamelServiceFactory.createCamelService.start + + /** + * Stops the current CamelService (if defined). + * + * @see CamelService#stop + * @see CamelService#onUnload + */ + def stopCamelService = for (s ← service) s.stop + + /** + * Returns Some(CamelService) if this CamelService + * has been started, None otherwise. + */ + def service = _current + + /** + * Returns the current CamelService if CamelService + * has been started, otherwise throws an IllegalStateException. + *

+ * Java API + */ + def getService: JOption[CamelService] = CamelServiceManager.service + + /** + * Returns Some(CamelService) (containing the current CamelService) + * if this CamelServicehas been started, None otherwise. + */ + def mandatoryService = + if (_current.isDefined) _current.get + else throw new IllegalStateException("co current CamelService") + + /** + * Returns Some(CamelService) (containing the current CamelService) + * if this CamelServicehas been started, None otherwise. + *

+ * Java API + */ + def getMandatoryService = mandatoryService + + private[camel] def register(service: CamelService) = + if (_current.isDefined) throw new IllegalStateException("current CamelService already registered") + else _current = Some(service) + + private[camel] def unregister(service: CamelService) = + if (_current == Some(service)) _current = None + else throw new IllegalStateException("only current CamelService can be unregistered") +} + +/** + * @author Martin Krasser + */ +object CamelServiceFactory { + /** + * Creates a new CamelService instance. + */ + def createCamelService: CamelService = new CamelService {} + + /** + * Creates a new CamelService instance and initializes it with the given CamelContext. + */ + def createCamelService(camelContext: CamelContext): CamelService = { + CamelContextManager.init(camelContext) + createCamelService + } +} diff --git a/akka-camel/src/main/scala/akka/camel/Consumer.scala b/akka-camel/src/main/scala/akka/camel/Consumer.scala new file mode 100644 index 0000000000..da45532b82 --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/Consumer.scala @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel + +import org.apache.camel.model.{ RouteDefinition, ProcessorDefinition } + +import akka.actor._ + +/** + * Mixed in by Actor implementations that consume message from Camel endpoints. + * + * @author Martin Krasser + */ +trait Consumer { this: Actor ⇒ + import RouteDefinitionHandler._ + + /** + * The default route definition handler is the identity function + */ + private[camel] var routeDefinitionHandler: RouteDefinitionHandler = identity + + /** + * Returns the Camel endpoint URI to consume messages from. + */ + def endpointUri: String + + /** + * Determines whether two-way communications between an endpoint and this consumer actor + * should be done in blocking or non-blocking mode (default is non-blocking). This method + * doesn't have any effect on one-way communications (they'll never block). + */ + def blocking = false + + /** + * Determines whether one-way communications between an endpoint and this consumer actor + * should be auto-acknowledged or application-acknowledged. + */ + def autoack = true + + /** + * Sets the route definition handler for creating a custom route to this consumer instance. + */ + def onRouteDefinition(h: RouteDefinition ⇒ ProcessorDefinition[_]): Unit = onRouteDefinition(from(h)) + + /** + * Sets the route definition handler for creating a custom route to this consumer instance. + *

+ * Java API. + */ + def onRouteDefinition(h: RouteDefinitionHandler): Unit = routeDefinitionHandler = h +} + +/** + * Java-friendly Consumer. + * + * @see UntypedConsumerActor + * @see RemoteUntypedConsumerActor + * + * @author Martin Krasser + */ +trait UntypedConsumer extends Consumer { self: UntypedActor ⇒ + final override def endpointUri = getEndpointUri + final override def blocking = isBlocking + final override def autoack = isAutoack + + /** + * Returns the Camel endpoint URI to consume messages from. + */ + def getEndpointUri(): String + + /** + * Determines whether two-way communications between an endpoint and this consumer actor + * should be done in blocking or non-blocking mode (default is non-blocking). This method + * doesn't have any effect on one-way communications (they'll never block). + */ + def isBlocking() = super.blocking + + /** + * Determines whether one-way communications between an endpoint and this consumer actor + * should be auto-acknowledged or application-acknowledged. + */ + def isAutoack() = super.autoack +} + +/** + * Subclass this abstract class to create an MDB-style untyped consumer actor. This + * class is meant to be used from Java. + */ +abstract class UntypedConsumerActor extends UntypedActor with UntypedConsumer + +/** + * A callback handler for route definitions to consumer actors. + * + * @author Martin Krasser + */ +trait RouteDefinitionHandler { + def onRouteDefinition(rd: RouteDefinition): ProcessorDefinition[_] +} + +/** + * The identity route definition handler. + * + * @author Martin Krasser + * + */ +class RouteDefinitionIdentity extends RouteDefinitionHandler { + def onRouteDefinition(rd: RouteDefinition) = rd +} + +/** + * @author Martin Krasser + */ +object RouteDefinitionHandler { + /** + * Returns the identity route definition handler + */ + val identity = new RouteDefinitionIdentity + + /** + * Created a route definition handler from the given function. + */ + def from(f: RouteDefinition ⇒ ProcessorDefinition[_]) = new RouteDefinitionHandler { + def onRouteDefinition(rd: RouteDefinition) = f(rd) + } +} + +/** + * @author Martin Krasser + */ +private[camel] object Consumer { + /** + * Applies a function f to actorRef if actorRef + * references a consumer actor. A valid reference to a consumer actor is a local actor + * reference with a target actor that implements the Consumer trait. The + * target Consumer instance is passed as argument to f. This + * method returns None if actorRef is not a valid reference + * to a consumer actor, Some contained the return value of f + * otherwise. + */ + def withConsumer[T](actorRef: ActorRef)(f: Consumer ⇒ T): Option[T] = { + if (!actorRef.actor.isInstanceOf[Consumer]) None + + // TODO: check if this is needed at all + //else if (actorRef.homeAddress.isDefined) None + + else Some(f(actorRef.actor.asInstanceOf[Consumer])) + } +} diff --git a/akka-camel/src/main/scala/akka/camel/ConsumerPublisher.scala b/akka-camel/src/main/scala/akka/camel/ConsumerPublisher.scala new file mode 100644 index 0000000000..fb15c9d1fc --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/ConsumerPublisher.scala @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.camel + +import java.io.InputStream +import java.util.concurrent.CountDownLatch + +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.model.RouteDefinition + +import akka.actor._ +import akka.event.EventHandler + +/** + * Concrete publish requestor that requests publication of consumer actors on ActorRegistered + * events and unpublication of consumer actors on ActorUnregistered events. + * + * @author Martin Krasser + */ +private[camel] class ConsumerPublishRequestor extends PublishRequestor { + def receiveActorRegistryEvent = { + case ActorRegistered(_, actor) ⇒ for (event ← ConsumerActorRegistered.eventFor(actor)) deliverCurrentEvent(event) + case ActorUnregistered(_, actor) ⇒ for (event ← ConsumerActorUnregistered.eventFor(actor)) deliverCurrentEvent(event) + } +} + +/** + * Publishes consumer actors on ConsumerActorRegistered events and unpublishes + * consumer actors on ConsumerActorUnregistered events. Publications are tracked + * by sending an activationTracker an EndpointActivated event, + * unpublications are tracked by sending an EndpointActivated event. + * + * @author Martin Krasser + */ +private[camel] class ConsumerPublisher(activationTracker: ActorRef) extends Actor { + import ConsumerPublisher._ + + def receive = { + case r: ConsumerActorRegistered ⇒ { + handleConsumerActorRegistered(r) + activationTracker ! EndpointActivated + } + case u: ConsumerActorUnregistered ⇒ { + handleConsumerActorUnregistered(u) + activationTracker ! EndpointDeactivated + } + case _ ⇒ { /* ignore */ } + } +} + +/** + * @author Martin Krasser + */ +private[camel] object ConsumerPublisher { + /** + * Creates a route to the registered consumer actor. + */ + def handleConsumerActorRegistered(event: ConsumerActorRegistered) { + CamelContextManager.mandatoryContext.addRoutes(new ConsumerActorRouteBuilder(event)) + EventHandler notifyListeners EventHandler.Info(this, "published actor %s at endpoint %s" format (event.actorRef, event.endpointUri)) + } + + /** + * Stops the route to the already un-registered consumer actor. + */ + def handleConsumerActorUnregistered(event: ConsumerActorUnregistered) { + CamelContextManager.mandatoryContext.stopRoute(event.uuid) + EventHandler notifyListeners EventHandler.Info(this, "unpublished actor %s from endpoint %s" format (event.actorRef, event.endpointUri)) + } +} + +/** + * Abstract builder of a route to a target which can be either an actor or an typed actor method. + * + * @param endpointUri endpoint URI of the consumer actor or typed actor method. + * @param id unique route identifier. + * + * @author Martin Krasser + */ +private[camel] abstract class ConsumerRouteBuilder(endpointUri: String, id: String) 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/..." + val cnvopt = bodyConversions.get(schema) + + onRouteDefinition(startRouteDefinition(cnvopt)).to(targetUri) + } + + protected def routeDefinitionHandler: RouteDefinitionHandler + protected def targetUri: String + + private def onRouteDefinition(rd: RouteDefinition) = routeDefinitionHandler.onRouteDefinition(rd) + private def startRouteDefinition(bodyConversion: Option[Class[_]]): RouteDefinition = bodyConversion match { + case Some(clazz) ⇒ from(endpointUri).routeId(id).convertBodyTo(clazz) + case None ⇒ from(endpointUri).routeId(id) + } +} + +/** + * Builder of a route to a consumer actor. + * + * @author Martin Krasser + */ +private[camel] class ConsumerActorRouteBuilder(event: ConsumerActorRegistered) extends ConsumerRouteBuilder(event.endpointUri, event.uuid) { + protected def routeDefinitionHandler: RouteDefinitionHandler = event.routeDefinitionHandler + protected def targetUri = "actor:uuid:%s?blocking=%s&autoack=%s" format (event.uuid, event.blocking, event.autoack) +} + +/** + * Tracks EndpointActivated and EndpointDectivated events. Used to wait for a + * certain number of endpoints activations and de-activations to occur. + * + * @see SetExpectedActivationCount + * @see SetExpectedDeactivationCount + * + * @author Martin Krasser + */ +private[camel] class ActivationTracker extends Actor { + private var activationLatch = new CountDownLatch(0) + private var deactivationLatch = new CountDownLatch(0) + + def receive = { + case SetExpectedActivationCount(num) ⇒ { + activationLatch = new CountDownLatch(num) + self.reply(activationLatch) + } + case SetExpectedDeactivationCount(num) ⇒ { + deactivationLatch = new CountDownLatch(num) + self.reply(deactivationLatch) + } + case EndpointActivated ⇒ activationLatch.countDown + case EndpointDeactivated ⇒ deactivationLatch.countDown + } +} + +/** + * Command message that sets the number of expected endpoint activations on ActivationTracker. + */ +private[camel] case class SetExpectedActivationCount(num: Int) + +/** + * Command message that sets the number of expected endpoint de-activations on ActivationTracker. + */ +private[camel] case class SetExpectedDeactivationCount(num: Int) + +/** + * Event message indicating that a single endpoint has been activated. + */ +private[camel] case class EndpointActivated() + +/** + * Event message indicating that a single endpoint has been de-activated. + */ +private[camel] case class EndpointDeactivated() + +/** + * A consumer (un)registration event. + */ +private[camel] trait ConsumerEvent { + val uuid: String +} + +/** + * A consumer actor (un)registration event. + */ +private[camel] trait ConsumerActorEvent extends ConsumerEvent { + val actorRef: ActorRef + val actor: Consumer + + val uuid = actorRef.uuid.toString + val endpointUri = actor.endpointUri + val blocking = actor.blocking + val autoack = actor.autoack + val routeDefinitionHandler = actor.routeDefinitionHandler +} + +/** + * Event indicating that a consumer actor has been registered at the actor registry. + */ +private[camel] case class ConsumerActorRegistered(actorRef: ActorRef, actor: Consumer) extends ConsumerActorEvent + +/** + * Event indicating that a consumer actor has been unregistered from the actor registry. + */ +private[camel] case class ConsumerActorUnregistered(actorRef: ActorRef, actor: Consumer) extends ConsumerActorEvent + +/** + * @author Martin Krasser + */ +private[camel] object ConsumerActorRegistered { + /** + * Creates an ConsumerActorRegistered event message for a consumer actor or None if + * actorRef is not a consumer actor. + */ + def eventFor(actorRef: ActorRef): Option[ConsumerActorRegistered] = { + Consumer.withConsumer[ConsumerActorRegistered](actorRef) { actor ⇒ + ConsumerActorRegistered(actorRef, actor) + } + } +} + +/** + * @author Martin Krasser + */ +private[camel] object ConsumerActorUnregistered { + /** + * Creates an ConsumerActorUnregistered event message for a consumer actor or None if + * actorRef is not a consumer actor. + */ + def eventFor(actorRef: ActorRef): Option[ConsumerActorUnregistered] = { + Consumer.withConsumer[ConsumerActorUnregistered](actorRef) { actor ⇒ + ConsumerActorUnregistered(actorRef, actor) + } + } +} + diff --git a/akka-camel/src/main/scala/akka/camel/Message.scala b/akka-camel/src/main/scala/akka/camel/Message.scala new file mode 100644 index 0000000000..5e21df4bff --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/Message.scala @@ -0,0 +1,385 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.camel + +import java.util.{ Map ⇒ JMap, Set ⇒ JSet } + +import scala.collection.JavaConversions._ + +import org.apache.camel.{ Exchange, Message ⇒ CamelMessage } +import org.apache.camel.util.ExchangeHelper + +import akka.japi.{ Function ⇒ JFunction } + +/** + * An immutable representation of a Camel message. + * + * @author Martin Krasser + */ +case class Message(val body: Any, val headers: Map[String, Any] = Map.empty) { + + /** + * Creates a Message with given body and empty headers map. + */ + def this(body: Any) = this(body, Map.empty[String, Any]) + + /** + * Creates a Message with given body and headers map. A copy of the headers map is made. + *

+ * Java API + */ + def this(body: Any, headers: JMap[String, Any]) = this(body, headers.toMap) + + /** + * Returns the body of the message converted to the type T. 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](implicit m: Manifest[T]): T = getBodyAs(m.erasure.asInstanceOf[Class[T]]) + + /** + * Returns the body of the message converted to the type as given by the clazz + * parameter. 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. + *

+ * Java API + * + * @see CamelContextManager. + */ + def getBodyAs[T](clazz: Class[T]): T = + CamelContextManager.mandatoryContext.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) + + /** + * Returns those headers from this message whose name is contained in names. + * The returned headers map is backed up by an immutable headers map. Any attempt to modify + * the returned map will throw an exception. + *

+ * Java API + */ + def getHeaders(names: JSet[String]): JMap[String, Any] = headers.filter(names contains _._1) + + /** + * Returns all headers from this message. The returned headers map is backed up by this + * message's immutable headers map. Any attempt to modify the returned map will throw an + * exception. + *

+ * Java API + */ + def getHeaders: JMap[String, Any] = headers + + /** + * Returns the header with given name. Throws NoSuchElementException + * if the header doesn't exist. + */ + def header(name: String): Any = headers(name) + + /** + * Returns the header with given name. Throws NoSuchElementException + * if the header doesn't exist. + *

+ * Java API + */ + def getHeader(name: String): Any = header(name) + + /** + * Returns the header with given name converted to type T. Throws + * NoSuchElementException if the header doesn't exist. + */ + def headerAs[T](name: String)(implicit m: Manifest[T]): T = + getHeaderAs(name, m.erasure.asInstanceOf[Class[T]]) + + /** + * Returns the header with given name converted to type as given by the clazz + * parameter. Throws NoSuchElementException if the header doesn't exist. + *

+ * Java API + */ + def getHeaderAs[T](name: String, clazz: Class[T]): T = + CamelContextManager.mandatoryContext.getTypeConverter.mandatoryConvertTo[T](clazz, header(name)) + + /** + * Creates a Message with a transformed body using a transformer function. + */ + def transformBody[A](transformer: A ⇒ Any): Message = setBody(transformer(body.asInstanceOf[A])) + + /** + * Creates a Message with a transformed body using a transformer function. + *

+ * Java API + */ + def transformBody[A](transformer: JFunction[A, Any]): Message = setBody(transformer(body.asInstanceOf[A])) + + /** + * Creates a Message with current body converted to type T. + */ + def setBodyAs[T](implicit m: Manifest[T]): Message = setBodyAs(m.erasure.asInstanceOf[Class[T]]) + + /** + * Creates a Message with current body converted to type clazz. + *

+ * Java API + */ + def setBodyAs[T](clazz: Class[T]): Message = setBody(getBodyAs(clazz)) + + /** + * Creates a Message with a given body. + */ + def setBody(body: Any) = new Message(body, this.headers) + + /** + * Creates a new Message with given headers. + */ + def setHeaders(headers: Map[String, Any]): Message = copy(this.body, headers) + + /** + * Creates a new Message with given headers. A copy of the headers map is made. + *

+ * Java API + */ + def setHeaders(headers: JMap[String, Any]): Message = setHeaders(headers.toMap) + + /** + * Creates a new Message with given headers added to the current headers. + */ + def addHeaders(headers: Map[String, Any]): Message = copy(this.body, this.headers ++ headers) + + /** + * Creates a new Message with given headers added to the current headers. + * A copy of the headers map is made. + *

+ * Java API + */ + def addHeaders(headers: JMap[String, Any]): Message = addHeaders(headers.toMap) + + /** + * Creates a new Message with the given header added to the current headers. + */ + def addHeader(header: (String, Any)): Message = copy(this.body, this.headers + header) + + /** + * Creates a new Message with the given header, represented by name and + * value added to the existing headers. + *

+ * Java API + */ + def addHeader(name: String, value: Any): Message = addHeader((name, value)) + + /** + * Creates a new Message where the header with given headerName is removed from + * the existing headers. + */ + def removeHeader(headerName: String) = copy(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".intern + + /** + * 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) + } +} + +/** + * Positive acknowledgement message (used for application-acknowledged message receipts). + * + * @author Martin Krasser + */ +case object Ack { + /** Java API to get the Ack singleton */ + def ack = this +} + +/** + * 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] = Map.empty) { + + /** + * Creates a Failure with cause body and empty headers map. + */ + def this(cause: Throwable) = this(cause, Map.empty[String, Any]) + + /** + * Creates a Failure with given cause and headers map. A copy of the headers map is made. + *

+ * Java API + */ + def this(cause: Throwable, headers: JMap[String, Any]) = this(cause, headers.toMap) + + /** + * Returns the cause of this Failure. + *

+ * Java API. + */ + def getCause = cause + + /** + * Returns all headers from this failure message. The returned headers map is backed up by + * this message's immutable headers map. Any attempt to modify the returned map will throw + * an exception. + *

+ * Java API + */ + def getHeaders: JMap[String, Any] = headers +} + +/** + * 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 } + + /** + * 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. + */ + 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) + +} + +/** + * Adapter for converting an org.apache.camel.Message to and from Message objects. + * + * @author Martin Krasser + */ +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]) + 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 ++ cm.getHeaders +} + +/** + * 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) + + /** + * Creates an CamelMessageAdapter for the given Camel message. + */ + implicit def toMessageAdapter(cm: CamelMessage): CamelMessageAdapter = + new CamelMessageAdapter(cm) +} diff --git a/akka-camel/src/main/scala/akka/camel/Producer.scala b/akka-camel/src/main/scala/akka/camel/Producer.scala new file mode 100644 index 0000000000..f4f745a294 --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/Producer.scala @@ -0,0 +1,270 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel + +import CamelMessageConversion.toExchangeAdapter + +import org.apache.camel._ +import org.apache.camel.processor.SendProcessor + +import akka.actor.{ Actor, ActorRef, UntypedActor } + +/** + * Support trait for producing messages to Camel endpoints. + * + * @author Martin Krasser + */ +trait ProducerSupport { this: Actor ⇒ + + /** + * Message headers to copy by default from request message to response-message. + */ + private val headersToCopyDefault = Set(Message.MessageExchangeId) + + /** + * Endpoint object resolved from the current CamelContext with + * endpointUri. + */ + private lazy val endpoint = CamelContextManager.mandatoryContext.getEndpoint(endpointUri) + + /** + * SendProcessor for producing messages to endpoint. + */ + private lazy val processor = createSendProcessor + + /** + * If set to false (default), this producer expects a response message from the Camel endpoint. + * If set to true, this producer initiates an in-only message exchange with the Camel endpoint + * (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] = headersToCopyDefault + + /** + * Default implementation of Actor.preRestart for freeing resources needed + * to actually send messages to endpointUri. + */ + override def preRestart(reason: Throwable) { + try { preRestartProducer(reason) } finally { processor.stop } + } + + /** + * Does nothing by default. Can be overridden by concrete producers for implementing a + * pre-restart callback handler. + */ + def preRestartProducer(reason: Throwable) {} + + /** + * Default implementation of Actor.postStop for freeing resources needed + * to actually send messages to endpointUri. + */ + override def postStop { + processor.stop + } + + /** + * Initiates a message exchange of given pattern with the endpoint specified by + * endpointUri. The in-message of the initiated exchange is the canonical form + * of msg. After sending the in-message, the processing result (response) is passed + * as argument to receiveAfterProduce. If the response is received synchronously from + * the endpoint then receiveAfterProduce is called synchronously as well. If the + * response is received asynchronously, the receiveAfterProduce is called + * asynchronously. This is done by wrapping the response, adding it to this producers + * mailbox, unwrapping it and calling receiveAfterProduce. The original + * sender and senderFuture are thereby preserved. + * + * @see Message#canonicalize(Any) + * + * @param msg message to produce + * @param pattern exchange pattern + */ + protected def produce(msg: Any, pattern: ExchangePattern): Unit = { + val cmsg = Message.canonicalize(msg) + val exchange = createExchange(pattern).fromRequestMessage(cmsg) + processor.process(exchange, new AsyncCallback { + val producer = self + // Need copies of sender and senderFuture references here + // since the callback could be done later by another thread. + val sender = self.sender + val senderFuture = self.senderFuture + + def done(doneSync: Boolean): Unit = { + (doneSync, exchange.isFailed) match { + case (true, true) ⇒ dispatchSync(exchange.toFailureMessage(cmsg.headers(headersToCopy))) + case (true, false) ⇒ dispatchSync(exchange.toResponseMessage(cmsg.headers(headersToCopy))) + case (false, true) ⇒ dispatchAsync(FailureResult(exchange.toFailureMessage(cmsg.headers(headersToCopy)))) + case (false, false) ⇒ dispatchAsync(MessageResult(exchange.toResponseMessage(cmsg.headers(headersToCopy)))) + } + } + + private def dispatchSync(result: Any) = + receiveAfterProduce(result) + + private def dispatchAsync(result: Any) = { + if (senderFuture.isDefined) + producer.postMessageToMailboxAndCreateFutureResultWithTimeout(result, producer.timeout, sender, senderFuture) + else + producer.postMessageToMailbox(result, sender) + } + }) + } + + /** + * Produces msg to the endpoint specified by endpointUri. Before the message is + * actually sent it is pre-processed by calling receiveBeforeProduce. If oneway + * is true, an in-only message exchange is initiated, otherwise an in-out message exchange. + * + * @see Producer#produce(Any, ExchangePattern) + */ + protected def produce: Receive = { + case res: MessageResult ⇒ receiveAfterProduce(res.message) + case res: FailureResult ⇒ receiveAfterProduce(res.failure) + case msg ⇒ { + if (oneway) + produce(receiveBeforeProduce(msg), ExchangePattern.InOnly) + else + produce(receiveBeforeProduce(msg), ExchangePattern.InOut) + } + } + + /** + * Called before the message is sent to the endpoint specified by endpointUri. The original + * message is passed as argument. By default, this method simply returns the argument but may be overridden + * by subtraits or subclasses. + */ + protected def receiveBeforeProduce: PartialFunction[Any, Any] = { + case msg ⇒ msg + } + + /** + * Called after a response was received from the endpoint specified by endpointUri. The + * response is passed as argument. By default, this method sends the response back to the original sender + * if oneway is false. If oneway is true, nothing is + * done. This method may be overridden by subtraits or subclasses (e.g. to forward responses to another + * actor). + */ + protected def receiveAfterProduce: Receive = { + case msg ⇒ if (!oneway) self.reply(msg) + } + + /** + * Creates a new Exchange of given pattern from the endpoint specified by + * endpointUri. + */ + private def createExchange(pattern: ExchangePattern): Exchange = endpoint.createExchange(pattern) + + /** + * Creates a new SendProcessor for endpoint. + */ + private def createSendProcessor = { + val sendProcessor = new SendProcessor(endpoint) + sendProcessor.start + sendProcessor + } +} + +/** + * Mixed in by Actor implementations to produce messages to Camel endpoints. + */ +trait Producer extends ProducerSupport { this: Actor ⇒ + + /** + * Default implementation of Actor.receive. Any messages received by this actors + * will be produced to the endpoint specified by endpointUri. + */ + protected def receive = produce +} + +/** + * Java-friendly ProducerSupport. + * + * @see UntypedProducerActor + * + * @author Martin Krasser + */ +trait UntypedProducer extends ProducerSupport { this: UntypedActor ⇒ + final override def endpointUri = getEndpointUri + final override def oneway = isOneway + + final override def receiveBeforeProduce = { + case msg ⇒ onReceiveBeforeProduce(msg) + } + + final override def receiveAfterProduce = { + case msg ⇒ onReceiveAfterProduce(msg) + } + + /** + * Default implementation of UntypedActor.onReceive + */ + def onReceive(message: Any) = produce(message) + + /** + * Returns the Camel endpoint URI to produce messages to. + */ + def getEndpointUri(): String + + /** + * 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 isOneway() = super.oneway + + /** + * Called before the message is sent to the endpoint specified by getEndpointUri. The original + * message is passed as argument. By default, this method simply returns the argument but may be overridden + * by subclasses. + */ + @throws(classOf[Exception]) + def onReceiveBeforeProduce(message: Any): Any = super.receiveBeforeProduce(message) + + /** + * Called after a response was received from the endpoint specified by endpointUri. The + * response is passed as argument. By default, this method sends the response back to the original sender + * if oneway is false. If oneway is true, nothing is + * done. This method may be overridden by subclasses (e.g. to forward responses to another actor). + */ + @throws(classOf[Exception]) + def onReceiveAfterProduce(message: Any): Unit = super.receiveAfterProduce(message) +} + +/** + * Subclass this abstract class to create an untyped producer actor. This class is meant to be used from Java. + * + * @author Martin Krasser + */ +abstract class UntypedProducerActor extends UntypedActor with UntypedProducer + +/** + * @author Martin Krasser + */ +private[camel] case class MessageResult(message: Message) + +/** + * @author Martin Krasser + */ +private[camel] case class FailureResult(failure: Failure) + +/** + * A one-way producer. + * + * @author Martin Krasser + */ +trait Oneway extends Producer { this: Actor ⇒ + override def oneway = true +} + diff --git a/akka-camel/src/main/scala/akka/camel/PublisherRequestor.scala b/akka-camel/src/main/scala/akka/camel/PublisherRequestor.scala new file mode 100644 index 0000000000..7083cdbe6e --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/PublisherRequestor.scala @@ -0,0 +1,64 @@ +package akka.camel + +import akka.actor._ + +/** + * Base class for concrete (un)publish requestors. Subclasses are responsible for requesting + * (un)publication of consumer actors by calling deliverCurrentEvent. + * + * @author Martin Krasser + */ +private[camel] abstract class PublishRequestor extends Actor { + private val events = collection.mutable.Set[ConsumerEvent]() + private var publisher: Option[ActorRef] = None + + def receiveActorRegistryEvent: Receive + + /** + * Accepts + *

    + *
  • InitPublishRequestor messages to configure a publisher for this requestor.
  • + *
  • ActorRegistryEvent messages to be handled receiveActorRegistryEvent + * implementators
  • . + *
+ * Other messages are simply ignored. Calls to deliverCurrentEvent prior to setting a + * publisher are buffered. They will be sent after a publisher has been set. + */ + def receive = { + case InitPublishRequestor(pub) ⇒ { + publisher = Some(pub) + deliverBufferedEvents + } + case e: ActorRegistryEvent ⇒ receiveActorRegistryEvent(e) + case _ ⇒ { /* ignore */ } + } + + /** + * Deliver the given event to publisher or buffer the event if + * publisher is not defined yet. + */ + protected def deliverCurrentEvent(event: ConsumerEvent) { + publisher match { + case Some(pub) ⇒ pub ! event + case None ⇒ events += event + } + } + + private def deliverBufferedEvents { + for (event ← events) deliverCurrentEvent(event) + events.clear + } +} + +/** + * @author Martin Krasser + */ +private[camel] object PublishRequestor { + def pastActorRegisteredEvents = for (actor ← Actor.registry.local.actors) yield ActorRegistered(actor.address, actor) +} + +/** + * Command message to initialize a PublishRequestor to use publisher + * for publishing consumer actors. + */ +private[camel] case class InitPublishRequestor(publisher: ActorRef) diff --git a/akka-camel/src/main/scala/akka/camel/TypedCamelAccess.scala b/akka-camel/src/main/scala/akka/camel/TypedCamelAccess.scala new file mode 100644 index 0000000000..003fe595b2 --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/TypedCamelAccess.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.camel + +import org.apache.camel.CamelContext +import akka.event.EventHandler + +import akka.util.ReflectiveAccess.getObjectFor + +/** + * Module for reflective access to akka-camel-typed. + * + * @author Martin Krasser + */ +private[camel] object TypedCamelAccess { + val loader = getClass.getClassLoader + + object TypedCamelModule { + + type TypedCamelObject = { + def onCamelContextInit(context: CamelContext): Unit + def onCamelServiceStart(service: CamelService): Unit + def onCamelServiceStop(service: CamelService): Unit + } + + val typedCamelObject: Option[TypedCamelObject] = + getObjectFor("akka.camel.TypedCamel$", loader) match { + case Right(value) ⇒ Some(value) + case Left(exception) ⇒ + EventHandler.debug(this, exception.toString) + None + } + } +} \ No newline at end of file diff --git a/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala b/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala new file mode 100644 index 0000000000..c05ee8f1aa --- /dev/null +++ b/akka-camel/src/main/scala/akka/camel/component/ActorComponent.scala @@ -0,0 +1,332 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.camel.component + +import java.net.InetSocketAddress +import java.util.{ Map ⇒ JMap } +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicReference + +import org.apache.camel._ +import org.apache.camel.impl.{ DefaultProducer, DefaultEndpoint, DefaultComponent } + +import akka.actor._ +import akka.camel.{ Ack, Failure, Message } +import akka.camel.CamelMessageConversion.toExchangeAdapter +import akka.dispatch.{ CompletableFuture, MessageInvocation, MessageDispatcher } + +import scala.reflect.BeanProperty + +/** + * @author Martin Krasser + */ +object ActorComponent { + /** + * Name of the message header containing the actor id or uuid. + */ + val ActorIdentifier = "CamelActorIdentifier" +} + +/** + * Camel component for sending messages to and receiving replies from (untyped) actors. + * + * @see akka.camel.component.ActorEndpoint + * @see akka.camel.component.ActorProducer + * + * @author Martin Krasser + */ +class ActorComponent extends DefaultComponent { + def createEndpoint(uri: String, remaining: String, parameters: JMap[String, Object]): ActorEndpoint = { + val (idType, idValue) = parsePath(remaining) + new ActorEndpoint(uri, this, idType, idValue) + } + + private def parsePath(remaining: String): Tuple2[String, Option[String]] = remaining match { + case null | "" ⇒ throw new IllegalArgumentException("invalid path: [%s] - should be or id: or uuid:" format remaining) + case id if id startsWith "id:" ⇒ ("id", parseIdentifier(id substring 3)) + case uuid if uuid startsWith "uuid:" ⇒ ("uuid", parseIdentifier(uuid substring 5)) + case id ⇒ ("id", parseIdentifier(id)) + } + + private def parseIdentifier(identifier: String): Option[String] = + if (identifier.length > 0) Some(identifier) else None +} + +/** + * Camel endpoint for sending messages to and receiving replies from (untyped) actors. Actors + * are referenced using actor endpoint URIs of the following format: + * actor:, + * actor:id:[] and + * actor:uuid:[], + * where refers to ActorRef.id and + * refers to the String-representation od ActorRef.uuid. In URIs that contain + * id: or uuid:, an actor identifier (id or uuid) is optional. In this + * case, the in-message of an exchange produced to this endpoint must contain a message header + * with name CamelActorIdentifier and a value that is the target actor's identifier. + * If the URI contains an actor identifier, a message with a CamelActorIdentifier + * header overrides the identifier in the endpoint URI. + * + * @see akka.camel.component.ActorComponent + * @see akka.camel.component.ActorProducer + * + * @author Martin Krasser + */ +class ActorEndpoint(uri: String, + comp: ActorComponent, + val idType: String, + val idValue: Option[String]) extends DefaultEndpoint(uri, comp) { + + /** + * Whether to block caller thread during two-way message exchanges with (untyped) actors. This is + * set via the blocking=true|false endpoint URI parameter. Default value is + * false. + */ + @BeanProperty + var blocking: Boolean = false + + /** + * Whether to auto-acknowledge one-way message exchanges with (untyped) actors. This is + * set via the blocking=true|false endpoint URI parameter. Default value is + * true. When set to true consumer actors need to additionally + * call Consumer.ack within Actor.receive. + */ + @BeanProperty + var autoack: Boolean = true + + /** + * @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 (untyped) actor, identified by an + * actor endpoint URI or by a CamelActorIdentifier message header. + *
    + *
  • If the exchange pattern is out-capable and blocking is set to + * true then the producer waits for a reply, using the !! operator.
  • + *
  • If the exchange pattern is out-capable and blocking is set to + * false then the producer sends the message using the ! operator, together + * with a callback handler. The callback handler is an ActorRef that can be + * used by the receiving actor to asynchronously reply to the route that is sending the + * message.
  • + *
  • If the exchange pattern is in-only then the producer sends the message using the + * ! operator.
  • + *
+ * + * @see akka.camel.component.ActorComponent + * @see akka.camel.component.ActorEndpoint + * + * @author Martin Krasser + */ +class ActorProducer(val ep: ActorEndpoint) extends DefaultProducer(ep) with AsyncProcessor { + import ActorProducer._ + + private lazy val uuid = uuidFrom(ep.idValue.getOrElse(throw new ActorIdentifierNotSetException)) + + def process(exchange: Exchange) = + if (exchange.getPattern.isOutCapable) sendSync(exchange) else sendAsync(exchange) + + def process(exchange: Exchange, callback: AsyncCallback): Boolean = { + (exchange.getPattern.isOutCapable, ep.blocking, ep.autoack) match { + case (true, true, _) ⇒ { + sendSync(exchange) + callback.done(true) + true + } + case (true, false, _) ⇒ { + sendAsync(exchange, Some(AsyncCallbackAdapter(exchange, callback))) + false + } + case (false, false, true) ⇒ { + sendAsync(exchange) + callback.done(true) + true + } + case (false, false, false) ⇒ { + sendAsync(exchange, Some(AsyncCallbackAdapter(exchange, callback))) + false + } + case (false, true, false) ⇒ { + sendSync(exchange) + callback.done(true) + true + } + case (false, true, true) ⇒ { + throw new IllegalStateException("cannot have blocking=true and autoack=true for in-only message exchanges") + } + } + } + + private def sendSync(exchange: Exchange) = { + import akka.camel.Consumer._ + + val actor = target(exchange) + val result: Any = try { actor !! requestFor(exchange) } catch { case e ⇒ Some(Failure(e)) } + + result match { + case Some(Ack) ⇒ { /* no response message to set */ } + case Some(msg: Failure) ⇒ exchange.fromFailureMessage(msg) + case Some(msg) ⇒ exchange.fromResponseMessage(Message.canonicalize(msg)) + case None ⇒ throw new TimeoutException("timeout (%d ms) while waiting response from %s" + format (actor.timeout, ep.getEndpointUri)) + } + } + + private def sendAsync(exchange: Exchange, sender: Option[ActorRef] = None) = + target(exchange).!(requestFor(exchange))(sender) + + private def target(exchange: Exchange) = + targetOption(exchange) getOrElse (throw new ActorNotRegisteredException(ep.getEndpointUri)) + + private def targetOption(exchange: Exchange): Option[ActorRef] = ep.idType match { + case "id" ⇒ targetById(targetId(exchange)) + case "uuid" ⇒ targetByUuid(targetUuid(exchange)) + } + + private def targetId(exchange: Exchange) = exchange.getIn.getHeader(ActorComponent.ActorIdentifier) match { + case id: String ⇒ id + case null ⇒ ep.idValue.getOrElse(throw new ActorIdentifierNotSetException) + } + + private def targetUuid(exchange: Exchange) = exchange.getIn.getHeader(ActorComponent.ActorIdentifier) match { + case uuid: Uuid ⇒ uuid + case uuid: String ⇒ uuidFrom(uuid) + case null ⇒ uuid + } + + private def targetById(id: String) = Actor.registry.local.actorFor(id) + private def targetByUuid(uuid: Uuid) = Actor.registry.local.actorFor(uuid) +} + +/** + * @author Martin Krasser + */ +private[camel] object ActorProducer { + def requestFor(exchange: Exchange) = + exchange.toRequestMessage(Map(Message.MessageExchangeId -> exchange.getExchangeId)) +} + +/** + * Thrown to indicate that an actor referenced by an endpoint URI cannot be + * found in the Actor.registry. + * + * @author Martin Krasser + */ +class ActorNotRegisteredException(uri: String) extends RuntimeException { + override def getMessage = "%s not registered" format uri +} + +/** + * Thrown to indicate that no actor identifier has been set. + * + * @author Martin Krasser + */ +class ActorIdentifierNotSetException extends RuntimeException { + override def getMessage = "actor identifier not set" +} + +/** + * @author Martin Krasser + */ +private[akka] object AsyncCallbackAdapter { + /** + * Creates and starts an AsyncCallbackAdapter. + * + * @param exchange message exchange to write results to. + * @param callback callback object to generate completion notifications. + */ + def apply(exchange: Exchange, callback: AsyncCallback) = + new AsyncCallbackAdapter(exchange, callback).start +} + +/** + * Adapts an ActorRef to a Camel AsyncCallback. Used by receiving actors to reply + * asynchronously to Camel routes with ActorRef.reply. + *

+ * Please note that this adapter can only be used locally at the moment which should not + * be a problem is most situations since Camel endpoints are only activated for local actor references, + * never for remote references. + * + * @author Martin Krasser + */ +private[akka] class AsyncCallbackAdapter(exchange: Exchange, callback: AsyncCallback) extends ActorRef with ScalaActorRef { + import akka.camel.Consumer._ + + val address = exchange.getExchangeId + + def start = { + _status = ActorRefInternals.RUNNING + this + } + + def stop() = { + _status = ActorRefInternals.SHUTDOWN + } + + /** + * Populates the initial exchange with the reply message and uses the + * callback handler to notify Camel about the asynchronous completion of the message + * exchange. + * + * @param message reply message + * @param sender ignored + */ + protected[akka] def postMessageToMailbox(message: Any, senderOption: Option[ActorRef]) = { + message match { + case Ack ⇒ { /* no response message to set */ } + case msg: Failure ⇒ exchange.fromFailureMessage(msg) + case msg ⇒ exchange.fromResponseMessage(Message.canonicalize(msg)) + } + callback.done(false) + } + + def actorClass: Class[_ <: Actor] = unsupported + def actorClassName = unsupported + def dispatcher_=(md: MessageDispatcher): Unit = unsupported + def dispatcher: MessageDispatcher = unsupported + def makeRemote(hostname: String, port: Int): Unit = unsupported + def makeRemote(address: InetSocketAddress): Unit = unsupported + def homeAddress_=(address: InetSocketAddress): Unit = unsupported + def remoteAddress: Option[InetSocketAddress] = unsupported + def link(actorRef: ActorRef): Unit = unsupported + def unlink(actorRef: ActorRef): Unit = unsupported + def startLink(actorRef: ActorRef): ActorRef = unsupported + def startLinkRemote(actorRef: ActorRef, hostname: String, port: Int): Unit = unsupported + def spawn(clazz: Class[_ <: Actor]): ActorRef = unsupported + def spawnRemote(clazz: Class[_ <: Actor], hostname: String, port: Int, timeout: Long): ActorRef = unsupported + def spawnLink(clazz: Class[_ <: Actor]): ActorRef = unsupported + def spawnLinkRemote(clazz: Class[_ <: Actor], hostname: String, port: Int, timeout: Long): ActorRef = unsupported + def shutdownLinkedActors: Unit = unsupported + def supervisor: Option[ActorRef] = unsupported + def homeAddress: Option[InetSocketAddress] = None + protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout[T](message: Any, timeout: Long, senderOption: Option[ActorRef], senderFuture: Option[CompletableFuture[T]]) = unsupported + protected[akka] def mailbox: AnyRef = unsupported + protected[akka] def mailbox_=(msg: AnyRef): AnyRef = unsupported + protected[akka] def restart(reason: Throwable, maxNrOfRetries: Option[Int], withinTimeRange: Option[Int]): Unit = unsupported + protected[akka] def restartLinkedActors(reason: Throwable, maxNrOfRetries: Option[Int], withinTimeRange: Option[Int]): Unit = unsupported + protected[akka] def handleTrapExit(dead: ActorRef, reason: Throwable): Unit = unsupported + def linkedActors: JMap[Uuid, ActorRef] = unsupported + protected[akka] def linkedActorsAsList: List[ActorRef] = unsupported + protected[akka] def invoke(messageHandle: MessageInvocation): Unit = unsupported + protected[akka] def remoteAddress_=(addr: Option[InetSocketAddress]): Unit = unsupported + protected[akka] def registerSupervisorAsRemoteActor = unsupported + protected[akka] def supervisor_=(sup: Option[ActorRef]): Unit = unsupported + protected[akka] def actorInstance: AtomicReference[Actor] = unsupported + + private def unsupported = throw new UnsupportedOperationException("Not supported for %s" format classOf[AsyncCallbackAdapter].getName) +} + diff --git a/akka-camel/src/test/java/akka/camel/ConsumerJavaTestBase.java b/akka-camel/src/test/java/akka/camel/ConsumerJavaTestBase.java new file mode 100644 index 0000000000..929f72ceb6 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/ConsumerJavaTestBase.java @@ -0,0 +1,41 @@ +package akka.camel; + +import akka.japi.SideEffect; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static akka.actor.Actors.*; +import static akka.camel.CamelContextManager.*; +import static akka.camel.CamelServiceManager.*; + +import static org.junit.Assert.*; + +/** + * @author Martin Krasser + */ +public class ConsumerJavaTestBase { + + @BeforeClass + public static void setUpBeforeClass() { + startCamelService(); + } + + @AfterClass + public static void tearDownAfterClass() { + stopCamelService(); + registry().local().shutdownAll(); + } + + @Test + public void shouldHandleExceptionThrownByActorAndGenerateCustomResponse() { + getMandatoryService().awaitEndpointActivation(1, new SideEffect() { + public void apply() { + actorOf(SampleErrorHandlingConsumer.class).start(); + } + }); + String result = getMandatoryTemplate().requestBody("direct:error-handler-test-java", "hello", String.class); + assertEquals("error: hello", result); + } +} diff --git a/akka-camel/src/test/java/akka/camel/MessageJavaTestBase.java b/akka-camel/src/test/java/akka/camel/MessageJavaTestBase.java new file mode 100644 index 0000000000..38e0b95692 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/MessageJavaTestBase.java @@ -0,0 +1,129 @@ +package akka.camel; + +import org.apache.camel.NoTypeConversionAvailableException; +import org.junit.BeforeClass; +import org.junit.Test; + +import akka.camel.CamelContextManager; +import akka.camel.Message; +import akka.japi.Function; + +import java.io.InputStream; +import java.util.*; + +import static org.junit.Assert.*; + +/** + * @author Martin Krasser + */ +public class MessageJavaTestBase { + + @BeforeClass + public static void setUpBeforeClass() { + CamelContextManager.init(); + } + + @Test public void shouldConvertDoubleBodyToString() { + assertEquals("1.4", new Message("1.4").getBodyAs(String.class)); + } + + @Test(expected=NoTypeConversionAvailableException.class) + public void shouldThrowExceptionWhenConvertingDoubleBodyToInputStream() { + new Message(1.4).getBodyAs(InputStream.class); + } + + @Test public void shouldReturnDoubleHeader() { + Message message = new Message("test" , createMap("test", 1.4)); + assertEquals(1.4, message.getHeader("test")); + } + + @Test public void shouldConvertDoubleHeaderToString() { + Message message = new Message("test" , createMap("test", 1.4)); + assertEquals("1.4", message.getHeaderAs("test", String.class)); + } + + @Test public void shouldReturnSubsetOfHeaders() { + Message message = new Message("test" , createMap("A", "1", "B", "2")); + assertEquals(createMap("B", "2"), message.getHeaders(createSet("B"))); + } + + @Test(expected=UnsupportedOperationException.class) + public void shouldReturnSubsetOfHeadersUnmodifiable() { + Message message = new Message("test" , createMap("A", "1", "B", "2")); + message.getHeaders(createSet("B")).put("x", "y"); + } + + @Test public void shouldReturnAllHeaders() { + Message message = new Message("test" , createMap("A", "1", "B", "2")); + assertEquals(createMap("A", "1", "B", "2"), message.getHeaders()); + } + + @Test(expected=UnsupportedOperationException.class) + public void shouldReturnAllHeadersUnmodifiable() { + Message message = new Message("test" , createMap("A", "1", "B", "2")); + message.getHeaders().put("x", "y"); + } + + @Test public void shouldTransformBodyAndPreserveHeaders() { + assertEquals( + new Message("ab", createMap("A", "1")), + new Message("a" , createMap("A", "1")).transformBody((Function)new TestTransformer())); + } + + @Test public void shouldConvertBodyAndPreserveHeaders() { + assertEquals( + new Message("1.4", createMap("A", "1")), + new Message(1.4 , createMap("A", "1")).setBodyAs(String.class)); + } + + @Test public void shouldSetBodyAndPreserveHeaders() { + assertEquals( + new Message("test2" , createMap("A", "1")), + new Message("test1" , createMap("A", "1")).setBody("test2")); + } + + @Test public void shouldSetHeadersAndPreserveBody() { + assertEquals( + new Message("test1" , createMap("C", "3")), + new Message("test1" , createMap("A", "1")).setHeaders(createMap("C", "3"))); + } + + @Test public void shouldAddHeaderAndPreserveBodyAndHeaders() { + assertEquals( + new Message("test1" , createMap("A", "1", "B", "2")), + new Message("test1" , createMap("A", "1")).addHeader("B", "2")); + } + + @Test public void shouldAddHeadersAndPreserveBodyAndHeaders() { + assertEquals( + new Message("test1" , createMap("A", "1", "B", "2")), + new Message("test1" , createMap("A", "1")).addHeaders(createMap("B", "2"))); + } + + @Test public void shouldRemoveHeadersAndPreserveBodyAndRemainingHeaders() { + assertEquals( + new Message("test1" , createMap("A", "1")), + new Message("test1" , createMap("A", "1", "B", "2")).removeHeader("B")); + } + + private static Set createSet(String... entries) { + HashSet set = new HashSet(); + set.addAll(Arrays.asList(entries)); + return set; + } + + private static Map createMap(Object... pairs) { + HashMap map = new HashMap(); + for (int i = 0; i < pairs.length; i += 2) { + map.put((String)pairs[i], pairs[i+1]); + } + return map; + } + + private static class TestTransformer implements Function { + public String apply(String param) { + return param + "b"; + } + } + +} diff --git a/akka-camel/src/test/java/akka/camel/SampleErrorHandlingConsumer.java b/akka-camel/src/test/java/akka/camel/SampleErrorHandlingConsumer.java new file mode 100644 index 0000000000..4e35d4e6ab --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleErrorHandlingConsumer.java @@ -0,0 +1,34 @@ +package akka.camel; + +import org.apache.camel.builder.Builder; +import org.apache.camel.model.ProcessorDefinition; +import org.apache.camel.model.RouteDefinition; + +/** + * @author Martin Krasser + */ +public class SampleErrorHandlingConsumer extends UntypedConsumerActor { + + public String getEndpointUri() { + return "direct:error-handler-test-java"; + } + + public boolean isBlocking() { + return true; + } + + public void preStart() { + onRouteDefinition(new RouteDefinitionHandler() { + public ProcessorDefinition onRouteDefinition(RouteDefinition rd) { + return rd.onException(Exception.class).handled(true).transform(Builder.exceptionMessage()).end(); + } + }); + } + + public void onReceive(Object message) throws Exception { + Message msg = (Message)message; + String body = msg.getBodyAs(String.class); + throw new Exception(String.format("error: %s", body)); + } + +} diff --git a/akka-camel/src/test/java/akka/camel/SampleUntypedActor.java b/akka-camel/src/test/java/akka/camel/SampleUntypedActor.java new file mode 100644 index 0000000000..7559fbe545 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleUntypedActor.java @@ -0,0 +1,12 @@ +package akka.camel; + +import akka.actor.UntypedActor; + +/** + * @author Martin Krasser + */ +public class SampleUntypedActor extends UntypedActor { + public void onReceive(Object message) { + System.out.println("Yay! I haz a message!"); + } +} diff --git a/akka-camel/src/test/java/akka/camel/SampleUntypedConsumer.java b/akka-camel/src/test/java/akka/camel/SampleUntypedConsumer.java new file mode 100644 index 0000000000..99300836c1 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleUntypedConsumer.java @@ -0,0 +1,21 @@ +package akka.camel; + +import akka.camel.UntypedConsumerActor; + +/** + * @author Martin Krasser + */ +public class SampleUntypedConsumer extends UntypedConsumerActor { + + public String getEndpointUri() { + return "direct:test-untyped-consumer"; + } + + public void onReceive(Object message) { + Message msg = (Message)message; + String body = msg.getBodyAs(String.class); + String header = msg.getHeaderAs("test", String.class); + getContext().replySafe(String.format("%s %s", body, header)); + } + +} diff --git a/akka-camel/src/test/java/akka/camel/SampleUntypedConsumerBlocking.java b/akka-camel/src/test/java/akka/camel/SampleUntypedConsumerBlocking.java new file mode 100644 index 0000000000..b5b22a04ae --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleUntypedConsumerBlocking.java @@ -0,0 +1,23 @@ +package akka.camel; + +/** + * @author Martin Krasser + */ +public class SampleUntypedConsumerBlocking extends UntypedConsumerActor { + + public String getEndpointUri() { + return "direct:test-untyped-consumer-blocking"; + } + + public boolean isBlocking() { + return true; + } + + public void onReceive(Object message) { + Message msg = (Message)message; + String body = msg.getBodyAs(String.class); + String header = msg.getHeaderAs("test", String.class); + getContext().replySafe(String.format("%s %s", body, header)); + } + +} diff --git a/akka-camel/src/test/java/akka/camel/SampleUntypedForwardingProducer.java b/akka-camel/src/test/java/akka/camel/SampleUntypedForwardingProducer.java new file mode 100644 index 0000000000..3161c0f2d8 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleUntypedForwardingProducer.java @@ -0,0 +1,18 @@ +package akka.camel; + +/** + * @author Martin Krasser + */ +public class SampleUntypedForwardingProducer extends UntypedProducerActor { + + public String getEndpointUri() { + return "direct:producer-test-1"; + } + + @Override + public void onReceiveAfterProduce(Object message) { + Message msg = (Message)message; + String body = msg.getBodyAs(String.class); + CamelContextManager.getMandatoryTemplate().sendBody("direct:forward-test-1", body); + } +} diff --git a/akka-camel/src/test/java/akka/camel/SampleUntypedReplyingProducer.java b/akka-camel/src/test/java/akka/camel/SampleUntypedReplyingProducer.java new file mode 100644 index 0000000000..09b7b86502 --- /dev/null +++ b/akka-camel/src/test/java/akka/camel/SampleUntypedReplyingProducer.java @@ -0,0 +1,12 @@ +package akka.camel; + +/** + * @author Martin Krasser + */ +public class SampleUntypedReplyingProducer extends UntypedProducerActor { + + public String getEndpointUri() { + return "direct:producer-test-1"; + } + +} diff --git a/akka-camel/src/test/scala/akka/camel/CamelContextLifecycleTest.scala b/akka-camel/src/test/scala/akka/camel/CamelContextLifecycleTest.scala new file mode 100644 index 0000000000..58ba676685 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/CamelContextLifecycleTest.scala @@ -0,0 +1,37 @@ +package akka.camel + +import org.apache.camel.impl.{ DefaultProducerTemplate, DefaultCamelContext } +import org.junit.Test +import org.scalatest.junit.JUnitSuite + +class CamelContextLifecycleTest extends JUnitSuite with CamelContextLifecycle { + @Test + def shouldManageCustomCamelContext { + assert(context === None) + assert(template === None) + + intercept[IllegalStateException] { mandatoryContext } + intercept[IllegalStateException] { mandatoryTemplate } + + val ctx = new TestCamelContext + assert(ctx.isStreamCaching === false) + + init(ctx) + + assert(mandatoryContext.isStreamCaching === true) + assert(!mandatoryContext.asInstanceOf[TestCamelContext].isStarted) + assert(mandatoryTemplate.asInstanceOf[DefaultProducerTemplate].isStarted) + + start + + assert(mandatoryContext.asInstanceOf[TestCamelContext].isStarted) + assert(mandatoryTemplate.asInstanceOf[DefaultProducerTemplate].isStarted) + + stop + + assert(!mandatoryContext.asInstanceOf[TestCamelContext].isStarted) + assert(!mandatoryTemplate.asInstanceOf[DefaultProducerTemplate].isStarted) + } + + class TestCamelContext extends DefaultCamelContext +} diff --git a/akka-camel/src/test/scala/akka/camel/CamelExchangeAdapterTest.scala b/akka-camel/src/test/scala/akka/camel/CamelExchangeAdapterTest.scala new file mode 100644 index 0000000000..7668aa2c65 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/CamelExchangeAdapterTest.scala @@ -0,0 +1,123 @@ +package akka.camel + +import org.apache.camel.impl.{ DefaultCamelContext, DefaultExchange } +import org.apache.camel.ExchangePattern +import org.junit.Test +import org.scalatest.junit.JUnitSuite + +class CamelExchangeAdapterTest extends JUnitSuite { + import CamelMessageConversion.toExchangeAdapter + + @Test + def shouldSetInMessageFromRequestMessage = { + val e1 = sampleInOnly.fromRequestMessage(Message("x")) + assert(e1.getIn.getBody === "x") + val e2 = sampleInOut.fromRequestMessage(Message("y")) + assert(e2.getIn.getBody === "y") + } + + @Test + def shouldSetOutMessageFromResponseMessage = { + val e1 = sampleInOut.fromResponseMessage(Message("y")) + assert(e1.getOut.getBody === "y") + } + + @Test + def shouldSetInMessageFromResponseMessage = { + val e1 = sampleInOnly.fromResponseMessage(Message("x")) + assert(e1.getIn.getBody === "x") + } + + @Test + def shouldSetExceptionFromFailureMessage = { + val e1 = sampleInOnly.fromFailureMessage(Failure(new Exception("test1"))) + assert(e1.getException.getMessage === "test1") + val e2 = sampleInOut.fromFailureMessage(Failure(new Exception("test2"))) + assert(e2.getException.getMessage === "test2") + } + + @Test + def shouldCreateRequestMessageFromInMessage = { + val m = sampleInOnly.toRequestMessage + assert(m === Message("test-in", Map("key-in" -> "val-in"))) + } + + @Test + def shouldCreateResponseMessageFromInMessage = { + val m = sampleInOnly.toResponseMessage + assert(m === Message("test-in", Map("key-in" -> "val-in"))) + } + + @Test + def shouldCreateResponseMessageFromOutMessage = { + val m = sampleInOut.toResponseMessage + assert(m === Message("test-out", Map("key-out" -> "val-out"))) + } + + @Test + def shouldCreateFailureMessageFromExceptionAndInMessage = { + val e1 = sampleInOnly + e1.setException(new Exception("test1")) + assert(e1.toFailureMessage.cause.getMessage === "test1") + assert(e1.toFailureMessage.headers("key-in") === "val-in") + } + + @Test + def shouldCreateFailureMessageFromExceptionAndOutMessage = { + val e1 = sampleInOut + e1.setException(new Exception("test2")) + assert(e1.toFailureMessage.cause.getMessage === "test2") + assert(e1.toFailureMessage.headers("key-out") === "val-out") + } + + @Test + def shouldCreateRequestMessageFromInMessageWithAdditionalHeader = { + val m = sampleInOnly.toRequestMessage(Map("x" -> "y")) + assert(m === Message("test-in", Map("key-in" -> "val-in", "x" -> "y"))) + } + + @Test + def shouldCreateResponseMessageFromInMessageWithAdditionalHeader = { + val m = sampleInOnly.toResponseMessage(Map("x" -> "y")) + assert(m === Message("test-in", Map("key-in" -> "val-in", "x" -> "y"))) + } + + @Test + def shouldCreateResponseMessageFromOutMessageWithAdditionalHeader = { + val m = sampleInOut.toResponseMessage(Map("x" -> "y")) + assert(m === Message("test-out", Map("key-out" -> "val-out", "x" -> "y"))) + } + + @Test + def shouldCreateFailureMessageFromExceptionAndInMessageWithAdditionalHeader = { + val e1 = sampleInOnly + e1.setException(new Exception("test1")) + assert(e1.toFailureMessage.cause.getMessage === "test1") + val headers = e1.toFailureMessage(Map("x" -> "y")).headers + assert(headers("key-in") === "val-in") + assert(headers("x") === "y") + } + + @Test + def shouldCreateFailureMessageFromExceptionAndOutMessageWithAdditionalHeader = { + val e1 = sampleInOut + e1.setException(new Exception("test2")) + assert(e1.toFailureMessage.cause.getMessage === "test2") + val headers = e1.toFailureMessage(Map("x" -> "y")).headers + assert(headers("key-out") === "val-out") + assert(headers("x") === "y") + } + + private def sampleInOnly = sampleExchange(ExchangePattern.InOnly) + private def sampleInOut = sampleExchange(ExchangePattern.InOut) + + private def sampleExchange(pattern: ExchangePattern) = { + val exchange = new DefaultExchange(new DefaultCamelContext) + exchange.getIn.setBody("test-in") + exchange.getOut.setBody("test-out") + exchange.getIn.setHeader("key-in", "val-in") + exchange.getOut.setHeader("key-out", "val-out") + exchange.setPattern(pattern) + exchange + } +} diff --git a/akka-camel/src/test/scala/akka/camel/CamelMessageAdapterTest.scala b/akka-camel/src/test/scala/akka/camel/CamelMessageAdapterTest.scala new file mode 100644 index 0000000000..3a1199bfc1 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/CamelMessageAdapterTest.scala @@ -0,0 +1,40 @@ +package akka.camel + +import org.apache.camel.impl.DefaultMessage +import org.junit.Test +import org.scalatest.junit.JUnitSuite + +class CamelMessageAdapterTest extends JUnitSuite { + import CamelMessageConversion.toMessageAdapter + + @Test + def shouldOverwriteBodyAndAddHeader = { + val cm = sampleMessage.fromMessage(Message("blah", Map("key" -> "baz"))) + assert(cm.getBody === "blah") + assert(cm.getHeader("foo") === "bar") + assert(cm.getHeader("key") === "baz") + } + + @Test + def shouldCreateMessageWithBodyAndHeader = { + val m = sampleMessage.toMessage + assert(m.body === "test") + assert(m.headers("foo") === "bar") + } + + @Test + def shouldCreateMessageWithBodyAndHeaderAndCustomHeader = { + val m = sampleMessage.toMessage(Map("key" -> "baz")) + assert(m.body === "test") + assert(m.headers("foo") === "bar") + assert(m.headers("key") === "baz") + } + + private[camel] def sampleMessage = { + val message = new DefaultMessage + message.setBody("test") + message.setHeader("foo", "bar") + message + } + +} diff --git a/akka-camel/src/test/scala/akka/camel/CamelServiceManagerTest.scala b/akka-camel/src/test/scala/akka/camel/CamelServiceManagerTest.scala new file mode 100644 index 0000000000..3db2b1fe54 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/CamelServiceManagerTest.scala @@ -0,0 +1,62 @@ +package akka.camel + +import org.scalatest.{ BeforeAndAfterAll, WordSpec } +import org.scalatest.matchers.MustMatchers + +import akka.actor.Actor + +/** + * @author Martin Krasser + */ +class CamelServiceManagerTest extends WordSpec with BeforeAndAfterAll with MustMatchers { + + override def afterAll = { + CamelServiceManager.stopCamelService + Actor.registry.local.shutdownAll + } + + "A CamelServiceManager" when { + "the startCamelService method been has been called" must { + "have registered the started CamelService instance" in { + val service = CamelServiceManager.startCamelService + CamelServiceManager.mandatoryService must be theSameInstanceAs (service) + } + } + "the stopCamelService method been has been called" must { + "have unregistered the current CamelService instance" in { + val service = CamelServiceManager.stopCamelService + CamelServiceManager.service must be(None) + } + } + } + + "A CamelServiceManager" when { + val service = CamelServiceFactory.createCamelService + "a CamelService instance has been started externally" must { + "have registered the started CamelService instance" in { + service.start + CamelServiceManager.mandatoryService must be theSameInstanceAs (service) + } + } + "the current CamelService instance has been stopped externally" must { + "have unregistered the current CamelService instance" in { + service.stop + CamelServiceManager.service must be(None) + } + } + } + + "A CamelServiceManager" when { + "a CamelService has been started" must { + "not allow further CamelService instances to be started" in { + CamelServiceManager.startCamelService + intercept[IllegalStateException] { CamelServiceManager.startCamelService } + } + } + "a CamelService has been stopped" must { + "only allow the current CamelService instance to be stopped" in { + intercept[IllegalStateException] { CamelServiceFactory.createCamelService.stop } + } + } + } +} diff --git a/akka-camel/src/test/scala/akka/camel/CamelTestSupport.scala b/akka-camel/src/test/scala/akka/camel/CamelTestSupport.scala new file mode 100644 index 0000000000..519fbc07bd --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/CamelTestSupport.scala @@ -0,0 +1,79 @@ +package akka.camel + +import java.util.concurrent.CountDownLatch + +import collection.mutable.Buffer + +import akka.actor.Actor + +object CamelTestSupport { + type Handler = PartialFunction[Any, Any] + + trait TestActor extends Actor { + def receive = { + case msg ⇒ { + handler(msg) + } + } + + def handler: Handler + } + + class Tester1 extends TestActor with Retain with Countdown { + def handler = retain andThen countdown + } + + class Tester2 extends TestActor with Respond { + def handler = respond + } + + class Tester3 extends TestActor with Noop { + self.timeout = 1 + def handler = noop + } + + trait Countdown { this: Actor ⇒ + var latch: CountDownLatch = new CountDownLatch(0) + def countdown: Handler = { + case SetExpectedMessageCount(num) ⇒ { + latch = new CountDownLatch(num) + self.reply(latch) + } + case msg ⇒ latch.countDown + } + } + + trait Respond { this: Actor ⇒ + def respond: Handler = { + case msg: Message ⇒ self.reply(response(msg)) + } + + def response(msg: Message): Any = "Hello %s" format msg.body + } + + trait Retain { this: Actor ⇒ + val messages = Buffer[Any]() + + def retain: Handler = { + case GetRetainedMessage ⇒ self.reply(messages.last) + case GetRetainedMessages(p) ⇒ self.reply(messages.toList.filter(p)) + case msg ⇒ { + messages += msg + msg + } + } + } + + trait Noop { this: Actor ⇒ + def noop: Handler = { + case msg ⇒ msg + } + } + + case class SetExpectedMessageCount(num: Int) + case class GetRetainedMessage() + case class GetRetainedMessages(p: Any ⇒ Boolean) { + def this() = this(_ ⇒ true) + } +} + diff --git a/akka-camel/src/test/scala/akka/camel/ConsumerJavaTest.scala b/akka-camel/src/test/scala/akka/camel/ConsumerJavaTest.scala new file mode 100644 index 0000000000..48741dda96 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/ConsumerJavaTest.scala @@ -0,0 +1,5 @@ +package akka.camel + +import org.scalatest.junit.JUnitSuite + +class ConsumerJavaTest extends ConsumerJavaTestBase with JUnitSuite \ No newline at end of file diff --git a/akka-camel/src/test/scala/akka/camel/ConsumerPublishRequestorTest.scala b/akka-camel/src/test/scala/akka/camel/ConsumerPublishRequestorTest.scala new file mode 100644 index 0000000000..4de98f335f --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/ConsumerPublishRequestorTest.scala @@ -0,0 +1,60 @@ +package akka.camel + +import java.util.concurrent.{ CountDownLatch, TimeUnit } + +import org.junit.{ Before, After, Test } +import org.scalatest.junit.JUnitSuite + +import akka.actor._ +import akka.actor.Actor._ +import akka.camel.CamelTestSupport.{ SetExpectedMessageCount ⇒ SetExpectedTestMessageCount, _ } + +class ConsumerPublishRequestorTest extends JUnitSuite { + import ConsumerPublishRequestorTest._ + + var publisher: ActorRef = _ + var requestor: ActorRef = _ + var consumer: ActorRef = _ + + @Before + def setUp: Unit = { + publisher = actorOf(new ConsumerPublisherMock).start + requestor = actorOf(new ConsumerPublishRequestor).start + requestor ! InitPublishRequestor(publisher) + consumer = actorOf(new Actor with Consumer { + def endpointUri = "mock:test" + protected def receive = null + }).start + } + + @After + def tearDown = { + Actor.registry.removeListener(requestor); + Actor.registry.local.shutdownAll + } + + @Test + def shouldReceiveOneConsumerRegisteredEvent = { + val latch = (publisher !! SetExpectedTestMessageCount(1)).as[CountDownLatch].get + requestor ! ActorRegistered(consumer.address, consumer) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + assert((publisher !! GetRetainedMessage) === + Some(ConsumerActorRegistered(consumer, consumer.actor.asInstanceOf[Consumer]))) + } + + @Test + def shouldReceiveOneConsumerUnregisteredEvent = { + val latch = (publisher !! SetExpectedTestMessageCount(1)).as[CountDownLatch].get + requestor ! ActorUnregistered(consumer.address, consumer) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + assert((publisher !! GetRetainedMessage) === + Some(ConsumerActorUnregistered(consumer, consumer.actor.asInstanceOf[Consumer]))) + } +} + +object ConsumerPublishRequestorTest { + class ConsumerPublisherMock extends TestActor with Retain with Countdown { + def handler = retain andThen countdown + } +} + diff --git a/akka-camel/src/test/scala/akka/camel/ConsumerRegisteredTest.scala b/akka-camel/src/test/scala/akka/camel/ConsumerRegisteredTest.scala new file mode 100644 index 0000000000..aaab35a69c --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/ConsumerRegisteredTest.scala @@ -0,0 +1,69 @@ +package akka.camel + +import org.junit.Test +import org.scalatest.junit.JUnitSuite +import akka.actor.{ ActorRef, Actor } + +class ConsumerRegisteredTest extends JUnitSuite { + import ConsumerRegisteredTest._ + + @Test + def shouldCreateSomeNonBlockingPublishRequestFromConsumer = { + val c = Actor.actorOf[ConsumerActor1] + val event = ConsumerActorRegistered.eventFor(c) + assert(event === Some(ConsumerActorRegistered(c, consumerOf(c)))) + } + + @Test + def shouldCreateSomeBlockingPublishRequestFromConsumer = { + val c = Actor.actorOf[ConsumerActor2] + val event = ConsumerActorRegistered.eventFor(c) + assert(event === Some(ConsumerActorRegistered(c, consumerOf(c)))) + } + + @Test + def shouldCreateNoneFromConsumer = { + val event = ConsumerActorRegistered.eventFor(Actor.actorOf[PlainActor]) + assert(event === None) + } + + @Test + def shouldCreateSomeNonBlockingPublishRequestFromUntypedConsumer = { + val uc = Actor.actorOf(classOf[SampleUntypedConsumer]) + val event = ConsumerActorRegistered.eventFor(uc) + assert(event === Some(ConsumerActorRegistered(uc, consumerOf(uc)))) + } + + @Test + def shouldCreateSomeBlockingPublishRequestFromUntypedConsumer = { + val uc = Actor.actorOf(classOf[SampleUntypedConsumerBlocking]) + val event = ConsumerActorRegistered.eventFor(uc) + assert(event === Some(ConsumerActorRegistered(uc, consumerOf(uc)))) + } + + @Test + def shouldCreateNoneFromUntypedConsumer = { + val a = Actor.actorOf(classOf[SampleUntypedActor]) + val event = ConsumerActorRegistered.eventFor(a) + assert(event === None) + } + + private def consumerOf(ref: ActorRef) = ref.actor.asInstanceOf[Consumer] +} + +object ConsumerRegisteredTest { + class ConsumerActor1 extends Actor with Consumer { + def endpointUri = "mock:test1" + protected def receive = null + } + + class ConsumerActor2 extends Actor with Consumer { + def endpointUri = "mock:test2" + override def blocking = true + protected def receive = null + } + + class PlainActor extends Actor { + protected def receive = null + } +} diff --git a/akka-camel/src/test/scala/akka/camel/ConsumerScalaTest.scala b/akka-camel/src/test/scala/akka/camel/ConsumerScalaTest.scala new file mode 100644 index 0000000000..d80e847efa --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/ConsumerScalaTest.scala @@ -0,0 +1,299 @@ +package akka.camel + +import java.util.concurrent.{ TimeoutException, CountDownLatch, TimeUnit } + +import org.apache.camel.{ AsyncProcessor, AsyncCallback, CamelExecutionException } +import org.apache.camel.builder.Builder +import org.apache.camel.component.direct.DirectEndpoint +import org.apache.camel.model.RouteDefinition +import org.scalatest.{ BeforeAndAfterAll, WordSpec } +import org.scalatest.matchers.MustMatchers + +import akka.actor.Actor._ +import akka.actor._ +import akka.config.Supervision._ + +/** + * @author Martin Krasser + */ +class ConsumerScalaTest extends WordSpec with BeforeAndAfterAll with MustMatchers { + import CamelContextManager.mandatoryContext + import CamelContextManager.mandatoryTemplate + import ConsumerScalaTest._ + + var service: CamelService = _ + + override protected def beforeAll = { + registry.local.shutdownAll + service = CamelServiceFactory.createCamelService + // register test consumer before registering the publish requestor + // and before starting the CamelService (registry is scanned for consumers) + actorOf(new TestConsumer("direct:publish-test-1")).start + service.registerPublishRequestor + service.consumerPublisher.start + service.activationTracker.start + service.awaitEndpointActivation(1) { + service.start + } must be(true) + } + + override protected def afterAll = { + service.stop + registry.local.shutdownAll + } + + "A responding consumer" when { + val consumer = actorOf(new TestConsumer("direct:publish-test-2")) + "started before starting the CamelService" must { + "support an in-out message exchange via its endpoint" in { + mandatoryTemplate.requestBody("direct:publish-test-1", "msg1") must equal("received msg1") + } + } + "not started" must { + "not have an associated endpoint in the CamelContext" in { + CamelContextManager.mandatoryContext.hasEndpoint("direct:publish-test-2") must be(null) + } + } + "started" must { + "support an in-out message exchange via its endpoint" in { + service.awaitEndpointActivation(1) { + consumer.start + } must be(true) + mandatoryTemplate.requestBody("direct:publish-test-2", "msg2") must equal("received msg2") + } + "have an associated endpoint in the CamelContext" in { + CamelContextManager.mandatoryContext.hasEndpoint("direct:publish-test-2") must not be (null) + } + } + "stopped" must { + "not support an in-out message exchange via its endpoint" in { + service.awaitEndpointDeactivation(1) { + consumer.stop + } must be(true) + intercept[CamelExecutionException] { + mandatoryTemplate.requestBody("direct:publish-test-2", "msg2") + } + } + } + } + + "A responding, untyped consumer" when { + val consumer = Actor.actorOf(classOf[SampleUntypedConsumer]) + "started" must { + "support an in-out message exchange via its endpoint" in { + service.awaitEndpointActivation(1) { + consumer.start + } must be(true) + mandatoryTemplate.requestBodyAndHeader("direct:test-untyped-consumer", "x", "test", "y") must equal("x y") + } + } + "stopped" must { + "not support an in-out message exchange via its endpoint" in { + service.awaitEndpointDeactivation(1) { + consumer.stop + } must be(true) + intercept[CamelExecutionException] { + mandatoryTemplate.sendBodyAndHeader("direct:test-untyped-consumer", "blah", "test", "blub") + } + } + } + } + + "A non-responding, blocking consumer" when { + "receiving an in-out message exchange" must { + "lead to a TimeoutException" in { + service.awaitEndpointActivation(1) { + actorOf(new TestBlocker("direct:publish-test-5")).start + } must be(true) + + try { + mandatoryTemplate.requestBody("direct:publish-test-5", "msg3") + fail("expected TimoutException not thrown") + } catch { + case e ⇒ { + assert(e.getCause.isInstanceOf[TimeoutException]) + } + } + } + } + } + + "A responding, blocking consumer" when { + "activated with a custom error handler" must { + "handle thrown exceptions by generating a custom response" in { + service.awaitEndpointActivation(1) { + actorOf[ErrorHandlingConsumer].start + } must be(true) + mandatoryTemplate.requestBody("direct:error-handler-test", "hello") must equal("error: hello") + + } + } + "activated with a custom redelivery handler" must { + "handle thrown exceptions by redelivering the initial message" in { + service.awaitEndpointActivation(1) { + actorOf[RedeliveringConsumer].start + } must be(true) + mandatoryTemplate.requestBody("direct:redelivery-test", "hello") must equal("accepted: hello") + + } + } + } + + "An non auto-acknowledging consumer" when { + val consumer = actorOf(new TestAckConsumer("direct:application-ack-test")) + "started" must { + "must support acknowledgements on application level" in { + service.awaitEndpointActivation(1) { + consumer.start + } must be(true) + + val endpoint = mandatoryContext.getEndpoint("direct:application-ack-test", classOf[DirectEndpoint]) + val producer = endpoint.createProducer.asInstanceOf[AsyncProcessor] + val exchange = endpoint.createExchange + + val latch = new CountDownLatch(1) + val handler = new AsyncCallback { + def done(doneSync: Boolean) = { + doneSync must be(false) + latch.countDown + } + } + + exchange.getIn.setBody("test") + producer.process(exchange, handler) + + latch.await(5, TimeUnit.SECONDS) must be(true) + } + } + } + + "A supervised consumer" must { + "be able to reply during receive" in { + val consumer = Actor.actorOf(new SupervisedConsumer("reply-channel-test-1")).start + (consumer !! "succeed") match { + case Some(r) ⇒ r must equal("ok") + case None ⇒ fail("reply expected") + } + } + + "be able to reply on failure during preRestart" in { + val consumer = Actor.actorOf(new SupervisedConsumer("reply-channel-test-2")) + val supervisor = Supervisor( + SupervisorConfig( + OneForOneStrategy(List(classOf[Exception]), 2, 10000), + Supervise(consumer, Permanent) :: Nil)) + + val latch = new CountDownLatch(1) + val sender = Actor.actorOf(new Sender("pr", latch)).start + + consumer.!("fail")(Some(sender)) + latch.await(5, TimeUnit.SECONDS) must be(true) + } + + "be able to reply on failure during postStop" in { + val consumer = Actor.actorOf(new SupervisedConsumer("reply-channel-test-3")) + val supervisor = Supervisor( + SupervisorConfig( + OneForOneStrategy(List(classOf[Exception]), 2, 10000), + Supervise(consumer, Temporary) :: Nil)) + + val latch = new CountDownLatch(1) + val sender = Actor.actorOf(new Sender("ps", latch)).start + + consumer.!("fail")(Some(sender)) + latch.await(5, TimeUnit.SECONDS) must be(true) + } + } +} + +object ConsumerScalaTest { + trait BlockingConsumer extends Consumer { self: Actor ⇒ + override def blocking = true + } + + class TestConsumer(uri: String) extends Actor with Consumer { + def endpointUri = uri + protected def receive = { + case msg: Message ⇒ self.reply("received %s" format msg.body) + } + } + + class TestBlocker(uri: String) extends Actor with BlockingConsumer { + self.timeout = 1000 + def endpointUri = uri + protected def receive = { + case msg: Message ⇒ { /* do not reply */ } + } + } + + class TestAckConsumer(uri: String) extends Actor with Consumer { + def endpointUri = uri + override def autoack = false + protected def receive = { + case msg: Message ⇒ self.reply(Ack) + } + } + + class ErrorHandlingConsumer extends Actor with BlockingConsumer { + def endpointUri = "direct:error-handler-test" + + onRouteDefinition { rd: RouteDefinition ⇒ + rd.onException(classOf[Exception]).handled(true).transform(Builder.exceptionMessage).end + } + + protected def receive = { + case msg: Message ⇒ throw new Exception("error: %s" format msg.body) + } + } + + class SupervisedConsumer(name: String) extends Actor with Consumer { + def endpointUri = "direct:%s" format name + + protected def receive = { + case "fail" ⇒ { throw new Exception("test") } + case "succeed" ⇒ self.reply("ok") + } + + override def preRestart(reason: scala.Throwable) { + self.reply_?("pr") + } + + override def postStop { + self.reply_?("ps") + } + } + + class Sender(expected: String, latch: CountDownLatch) extends Actor { + def receive = { + case msg if (msg == expected) ⇒ latch.countDown + case _ ⇒ {} + } + } + + class RedeliveringConsumer extends Actor with BlockingConsumer { + def endpointUri = "direct:redelivery-test" + + onRouteDefinition { rd: RouteDefinition ⇒ + rd.onException(classOf[Exception]).maximumRedeliveries(1).end + } + + // + // first message to this actor is not valid and will be rejected + // + + var valid = false + + protected def receive = { + case msg: Message ⇒ try { + respondTo(msg) + } finally { + valid = true + } + } + + private def respondTo(msg: Message) = + if (valid) self.reply("accepted: %s" format msg.body) + else throw new Exception("rejected: %s" format msg.body) + + } +} diff --git a/akka-camel/src/test/scala/akka/camel/MessageJavaTest.scala b/akka-camel/src/test/scala/akka/camel/MessageJavaTest.scala new file mode 100644 index 0000000000..3c95887eb4 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/MessageJavaTest.scala @@ -0,0 +1,5 @@ +package akka.camel + +import org.scalatest.junit.JUnitSuite + +class MessageJavaTest extends MessageJavaTestBase with JUnitSuite diff --git a/akka-camel/src/test/scala/akka/camel/MessageScalaTest.scala b/akka-camel/src/test/scala/akka/camel/MessageScalaTest.scala new file mode 100644 index 0000000000..7e8145ed4a --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/MessageScalaTest.scala @@ -0,0 +1,94 @@ +package akka.camel + +import java.io.InputStream + +import org.apache.camel.NoTypeConversionAvailableException +import org.junit.Assert._ +import org.junit.Test + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.junit.JUnitSuite + +class MessageScalaTest extends JUnitSuite with BeforeAndAfterAll { + override protected def beforeAll = CamelContextManager.init + + @Test + def shouldConvertDoubleBodyToString = { + assertEquals("1.4", Message(1.4).bodyAs[String]) + } + + @Test + def shouldThrowExceptionWhenConvertingDoubleBodyToInputStream { + intercept[NoTypeConversionAvailableException] { + Message(1.4).bodyAs[InputStream] + } + } + + @Test + def shouldReturnDoubleHeader = { + val message = Message("test", Map("test" -> 1.4)) + assertEquals(1.4, message.header("test")) + } + + @Test + def shouldConvertDoubleHeaderToString = { + val message = Message("test", Map("test" -> 1.4)) + assertEquals("1.4", message.headerAs[String]("test")) + } + + @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((body: String) ⇒ body + "b")) + } + + @Test + def shouldConvertBodyAndPreserveHeaders = { + assertEquals( + Message("1.4", Map("A" -> "1")), + Message(1.4, Map("A" -> "1")).setBodyAs[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")) + } +} diff --git a/akka-camel/src/test/scala/akka/camel/ProducerFeatureTest.scala b/akka-camel/src/test/scala/akka/camel/ProducerFeatureTest.scala new file mode 100644 index 0000000000..8b22ef46d8 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/ProducerFeatureTest.scala @@ -0,0 +1,301 @@ +package akka.camel + +import org.apache.camel.{ Exchange, Processor } +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.component.mock.MockEndpoint +import org.scalatest.{ GivenWhenThen, BeforeAndAfterEach, BeforeAndAfterAll, FeatureSpec } + +import akka.actor.Actor._ +import akka.actor.{ ActorRef, Actor } + +class ProducerFeatureTest extends FeatureSpec with BeforeAndAfterAll with BeforeAndAfterEach with GivenWhenThen { + import ProducerFeatureTest._ + + override protected def beforeAll = { + Actor.registry.local.shutdownAll + CamelContextManager.init + CamelContextManager.mandatoryContext.addRoutes(new TestRoute) + CamelContextManager.start + } + + override protected def afterAll = { + CamelContextManager.stop + Actor.registry.local.shutdownAll + } + + override protected def afterEach = { + mockEndpoint.reset + } + + feature("Produce a message to a sync Camel route") { + + scenario("produce message and receive normal response") { + given("a registered two-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-2", true)) + producer.start + + when("a test message is sent to the producer with !!") + val message = Message("test", Map(Message.MessageExchangeId -> "123")) + val result = producer !! message + + then("a normal response should have been returned by the producer") + val expected = Message("received TEST", Map(Message.MessageExchangeId -> "123")) + assert(result === Some(expected)) + } + + scenario("produce message and receive failure response") { + given("a registered two-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-2")) + producer.start + + when("a test message causing an exception is sent to the producer with !!") + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = (producer !! message).as[Failure] + + then("a failure response should have been returned by the producer") + val expectedFailureText = result.get.cause.getMessage + val expectedHeaders = result.get.headers + assert(expectedFailureText === "failure") + assert(expectedHeaders === Map(Message.MessageExchangeId -> "123")) + } + + scenario("produce message oneway") { + given("a registered one-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-1", true) with Oneway) + producer.start + + when("a test message is sent to the producer with !") + mockEndpoint.expectedBodiesReceived("TEST") + producer ! Message("test") + + then("the test message should have been sent to mock:mock") + mockEndpoint.assertIsSatisfied + } + + scenario("produce message twoway without sender reference") { + given("a registered two-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-1")) + producer.start + + when("a test message is sent to the producer with !") + mockEndpoint.expectedBodiesReceived("test") + producer ! Message("test") + + then("there should be only a warning that there's no sender reference") + mockEndpoint.assertIsSatisfied + } + } + + feature("Produce a message to an async Camel route") { + + scenario("produce message and receive normal response") { + given("a registered two-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-3")) + producer.start + + when("a test message is sent to the producer with !!") + val message = Message("test", Map(Message.MessageExchangeId -> "123")) + val result = producer !! message + + then("a normal response should have been returned by the producer") + val expected = Message("received test", Map(Message.MessageExchangeId -> "123")) + assert(result === Some(expected)) + } + + scenario("produce message and receive failure response") { + given("a registered two-way producer") + val producer = actorOf(new TestProducer("direct:producer-test-3")) + producer.start + + when("a test message causing an exception is sent to the producer with !!") + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = (producer !! message).as[Failure] + + then("a failure response should have been returned by the producer") + val expectedFailureText = result.get.cause.getMessage + val expectedHeaders = result.get.headers + assert(expectedFailureText === "failure") + assert(expectedHeaders === Map(Message.MessageExchangeId -> "123")) + } + } + + feature("Produce a message to a sync Camel route and then forward the response") { + + scenario("produce message, forward normal response to a replying target actor and receive response") { + given("a registered two-way producer configured with a forward target") + val target = actorOf[ReplyingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-2", target)).start + + when("a test message is sent to the producer with !!") + val message = Message("test", Map(Message.MessageExchangeId -> "123")) + val result = producer !! message + + then("a normal response should have been returned by the forward target") + val expected = Message("received test", Map(Message.MessageExchangeId -> "123", "test" -> "result")) + assert(result === Some(expected)) + } + + scenario("produce message, forward failure response to a replying target actor and receive response") { + given("a registered two-way producer configured with a forward target") + val target = actorOf[ReplyingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-2", target)).start + + when("a test message causing an exception is sent to the producer with !!") + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = (producer !! message).as[Failure] + + then("a failure response should have been returned by the forward target") + val expectedFailureText = result.get.cause.getMessage + val expectedHeaders = result.get.headers + assert(expectedFailureText === "failure") + assert(expectedHeaders === Map(Message.MessageExchangeId -> "123", "test" -> "failure")) + } + + scenario("produce message, forward normal response to a producing target actor and produce response to direct:forward-test-1") { + given("a registered one-way producer configured with a forward target") + val target = actorOf[ProducingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-2", target)).start + + when("a test message is sent to the producer with !") + mockEndpoint.expectedBodiesReceived("received test") + val result = producer.!(Message("test"))(Some(producer)) + + then("a normal response should have been produced by the forward target") + mockEndpoint.assertIsSatisfied + } + + scenario("produce message, forward failure response to a producing target actor and produce response to direct:forward-test-1") { + given("a registered one-way producer configured with a forward target") + val target = actorOf[ProducingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-2", target)).start + + when("a test message causing an exception is sent to the producer with !") + mockEndpoint.expectedMessageCount(1) + mockEndpoint.message(0).body().isInstanceOf(classOf[Failure]) + val result = producer.!(Message("fail"))(Some(producer)) + + then("a failure response should have been produced by the forward target") + mockEndpoint.assertIsSatisfied + } + } + + feature("Produce a message to an async Camel route and then forward the response") { + + scenario("produce message, forward normal response to a replying target actor and receive response") { + given("a registered two-way producer configured with a forward target") + val target = actorOf[ReplyingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-3", target)).start + + when("a test message is sent to the producer with !!") + val message = Message("test", Map(Message.MessageExchangeId -> "123")) + val result = producer !! message + + then("a normal response should have been returned by the forward target") + val expected = Message("received test", Map(Message.MessageExchangeId -> "123", "test" -> "result")) + assert(result === Some(expected)) + } + + scenario("produce message, forward failure response to a replying target actor and receive response") { + given("a registered two-way producer configured with a forward target") + val target = actorOf[ReplyingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-3", target)).start + + when("a test message causing an exception is sent to the producer with !!") + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = (producer !! message).as[Failure] + + then("a failure response should have been returned by the forward target") + val expectedFailureText = result.get.cause.getMessage + val expectedHeaders = result.get.headers + assert(expectedFailureText === "failure") + assert(expectedHeaders === Map(Message.MessageExchangeId -> "123", "test" -> "failure")) + } + + scenario("produce message, forward normal response to a producing target actor and produce response to direct:forward-test-1") { + given("a registered one-way producer configured with a forward target") + val target = actorOf[ProducingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-3", target)).start + + when("a test message is sent to the producer with !") + mockEndpoint.expectedBodiesReceived("received test") + val result = producer.!(Message("test"))(Some(producer)) + + then("a normal response should have been produced by the forward target") + mockEndpoint.assertIsSatisfied + } + + scenario("produce message, forward failure response to a producing target actor and produce response to direct:forward-test-1") { + given("a registered one-way producer configured with a forward target") + val target = actorOf[ProducingForwardTarget].start + val producer = actorOf(new TestForwarder("direct:producer-test-3", target)).start + + when("a test message causing an exception is sent to the producer with !") + mockEndpoint.expectedMessageCount(1) + mockEndpoint.message(0).body().isInstanceOf(classOf[Failure]) + val result = producer.!(Message("fail"))(Some(producer)) + + then("a failure response should have been produced by the forward target") + mockEndpoint.assertIsSatisfied + } + } + + private def mockEndpoint = CamelContextManager.mandatoryContext.getEndpoint("mock:mock", classOf[MockEndpoint]) +} + +object ProducerFeatureTest { + class TestProducer(uri: String, upper: Boolean = false) extends Actor with Producer { + def endpointUri = uri + override protected def receiveBeforeProduce = { + case msg: Message ⇒ if (upper) msg.transformBody { body: String ⇒ body.toUpperCase } else msg + } + } + + class TestForwarder(uri: String, target: ActorRef) extends Actor with Producer { + def endpointUri = uri + override protected def receiveAfterProduce = { + case msg ⇒ target forward msg + } + } + + class TestResponder extends Actor { + protected def receive = { + case msg: Message ⇒ msg.body match { + case "fail" ⇒ self.reply(Failure(new Exception("failure"), msg.headers)) + case _ ⇒ self.reply(msg.transformBody { body: String ⇒ "received %s" format body }) + } + } + } + + class ReplyingForwardTarget extends Actor { + protected def receive = { + case msg: Message ⇒ + self.reply(msg.addHeader("test" -> "result")) + case msg: Failure ⇒ + self.reply(Failure(msg.cause, msg.headers + ("test" -> "failure"))) + } + } + + class ProducingForwardTarget extends Actor with Producer with Oneway { + def endpointUri = "direct:forward-test-1" + } + + class TestRoute extends RouteBuilder { + val responder = actorOf[TestResponder].start + def configure { + from("direct:forward-test-1").to("mock:mock") + // for one-way messaging tests + from("direct:producer-test-1").to("mock:mock") + // for two-way messaging tests (async) + from("direct:producer-test-3").to("actor:uuid:%s" format responder.uuid) + // for two-way messaging tests (sync) + from("direct:producer-test-2").process(new Processor() { + def process(exchange: Exchange) = { + exchange.getIn.getBody match { + case "fail" ⇒ throw new Exception("failure") + case body ⇒ exchange.getOut.setBody("received %s" format body) + } + } + }) + } + } +} diff --git a/akka-camel/src/test/scala/akka/camel/UntypedProducerFeatureTest.scala b/akka-camel/src/test/scala/akka/camel/UntypedProducerFeatureTest.scala new file mode 100644 index 0000000000..3c66f3de6a --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/UntypedProducerFeatureTest.scala @@ -0,0 +1,97 @@ +package akka.camel + +import org.apache.camel.{ Exchange, Processor } +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.component.mock.MockEndpoint +import org.scalatest.{ GivenWhenThen, BeforeAndAfterEach, BeforeAndAfterAll, FeatureSpec } + +import akka.actor.Actor._ + +class UntypedProducerFeatureTest extends FeatureSpec with BeforeAndAfterAll with BeforeAndAfterEach with GivenWhenThen { + import UntypedProducerFeatureTest._ + + override protected def beforeAll = { + registry.local.shutdownAll + CamelContextManager.init + CamelContextManager.mandatoryContext.addRoutes(new TestRoute) + CamelContextManager.start + } + + override protected def afterAll = { + CamelContextManager.stop + registry.local.shutdownAll + } + + override protected def afterEach = { + mockEndpoint.reset + } + + feature("Produce a message to a sync Camel route") { + + scenario("produce message and receive normal response") { + given("a registered two-way producer") + val producer = actorOf(classOf[SampleUntypedReplyingProducer]) + producer.start + + when("a test message is sent to the producer with !!") + val message = Message("test", Map(Message.MessageExchangeId -> "123")) + val result = producer.sendRequestReply(message) + + then("a normal response should have been returned by the producer") + val expected = Message("received test", Map(Message.MessageExchangeId -> "123")) + assert(result === expected) + } + + scenario("produce message and receive failure response") { + given("a registered two-way producer") + val producer = actorOf(classOf[SampleUntypedReplyingProducer]) + producer.start + + when("a test message causing an exception is sent to the producer with !!") + val message = Message("fail", Map(Message.MessageExchangeId -> "123")) + val result = producer.sendRequestReply(message).asInstanceOf[Failure] + + then("a failure response should have been returned by the producer") + val expectedFailureText = result.cause.getMessage + val expectedHeaders = result.headers + assert(expectedFailureText === "failure") + assert(expectedHeaders === Map(Message.MessageExchangeId -> "123")) + } + + } + + feature("Produce a message to a sync Camel route and then forward the response") { + + scenario("produce message and send normal response to direct:forward-test-1") { + given("a registered one-way producer configured with a forward target") + val producer = actorOf(classOf[SampleUntypedForwardingProducer]) + producer.start + + when("a test message is sent to the producer with !") + mockEndpoint.expectedBodiesReceived("received test") + val result = producer.sendOneWay(Message("test"), producer) + + then("a normal response should have been sent") + mockEndpoint.assertIsSatisfied + } + + } + + private def mockEndpoint = CamelContextManager.mandatoryContext.getEndpoint("mock:mock", classOf[MockEndpoint]) +} + +object UntypedProducerFeatureTest { + class TestRoute extends RouteBuilder { + def configure { + from("direct:forward-test-1").to("mock:mock") + from("direct:producer-test-1").process(new Processor() { + def process(exchange: Exchange) = { + exchange.getIn.getBody match { + case "fail" ⇒ throw new Exception("failure") + case body ⇒ exchange.getOut.setBody("received %s" format body) + } + } + }) + } + } +} diff --git a/akka-camel/src/test/scala/akka/camel/component/ActorComponentFeatureTest.scala b/akka-camel/src/test/scala/akka/camel/component/ActorComponentFeatureTest.scala new file mode 100644 index 0000000000..c35ddb114a --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/component/ActorComponentFeatureTest.scala @@ -0,0 +1,129 @@ +package akka.camel.component + +import java.util.concurrent.{ TimeUnit, CountDownLatch } + +import org.apache.camel.RuntimeCamelException +import org.apache.camel.builder.RouteBuilder +import org.apache.camel.component.mock.MockEndpoint +import org.scalatest.{ BeforeAndAfterEach, BeforeAndAfterAll, FeatureSpec } + +import akka.actor.Actor +import akka.actor.Actor._ +import akka.camel.{ Failure, Message, CamelContextManager } +import akka.camel.CamelTestSupport._ + +class ActorComponentFeatureTest extends FeatureSpec with BeforeAndAfterAll with BeforeAndAfterEach { + import ActorComponentFeatureTest._ + + override protected def beforeAll = { + Actor.registry.local.shutdownAll + CamelContextManager.init + CamelContextManager.mandatoryContext.addRoutes(new TestRoute) + CamelContextManager.start + } + + override protected def afterAll = CamelContextManager.stop + + override protected def afterEach = { + Actor.registry.local.shutdownAll + mockEndpoint.reset + } + + feature("Communicate with an actor via an actor:uuid endpoint") { + import CamelContextManager.mandatoryTemplate + + scenario("one-way communication") { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + mandatoryTemplate.sendBody("actor:uuid:%s" format actor.uuid, "Martin") + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val reply = (actor !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply.body === "Martin") + } + + scenario("two-way communication") { + val actor = actorOf[Tester2].start + assert(mandatoryTemplate.requestBody("actor:uuid:%s" format actor.uuid, "Martin") === "Hello Martin") + } + + scenario("two-way communication with timeout") { + val actor = actorOf[Tester3].start + intercept[RuntimeCamelException] { + mandatoryTemplate.requestBody("actor:uuid:%s?blocking=true" format actor.uuid, "Martin") + } + } + + scenario("two-way communication via a custom route with failure response") { + mockEndpoint.expectedBodiesReceived("whatever") + mandatoryTemplate.requestBody("direct:failure-test-1", "whatever") + mockEndpoint.assertIsSatisfied + } + + scenario("two-way communication via a custom route with exception") { + mockEndpoint.expectedBodiesReceived("whatever") + mandatoryTemplate.requestBody("direct:failure-test-2", "whatever") + mockEndpoint.assertIsSatisfied + } + } + + feature("Communicate with an actor via an actor:id endpoint") { + import CamelContextManager.mandatoryTemplate + + scenario("one-way communication") { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + mandatoryTemplate.sendBody("actor:%s" format actor.address, "Martin") + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val reply = (actor !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply.body === "Martin") + } + + scenario("two-way communication") { + val actor = actorOf[Tester2].start + assert(mandatoryTemplate.requestBody("actor:%s" format actor.address, "Martin") === "Hello Martin") + } + + scenario("two-way communication via a custom route") { + val actor = actorOf[CustomIdActor]("custom-id").start + assert(mandatoryTemplate.requestBody("direct:custom-id-test-1", "Martin") === "Received Martin") + assert(mandatoryTemplate.requestBody("direct:custom-id-test-2", "Martin") === "Received Martin") + } + } + + private def mockEndpoint = CamelContextManager.mandatoryContext.getEndpoint("mock:mock", classOf[MockEndpoint]) +} + +object ActorComponentFeatureTest { + class CustomIdActor extends Actor { + protected def receive = { + case msg: Message ⇒ self.reply("Received %s" format msg.body) + } + } + + class FailWithMessage extends Actor { + protected def receive = { + case msg: Message ⇒ self.reply(Failure(new Exception("test"))) + } + } + + class FailWithException extends Actor { + protected def receive = { + case msg: Message ⇒ throw new Exception("test") + } + } + + class TestRoute extends RouteBuilder { + val failWithMessage = actorOf[FailWithMessage].start + val failWithException = actorOf[FailWithException].start + def configure { + from("direct:custom-id-test-1").to("actor:custom-id") + from("direct:custom-id-test-2").to("actor:id:custom-id") + from("direct:failure-test-1") + .onException(classOf[Exception]).to("mock:mock").handled(true).end + .to("actor:uuid:%s" format failWithMessage.uuid) + from("direct:failure-test-2") + .onException(classOf[Exception]).to("mock:mock").handled(true).end + .to("actor:uuid:%s?blocking=true" format failWithException.uuid) + } + } +} diff --git a/akka-camel/src/test/scala/akka/camel/component/ActorComponentTest.scala b/akka-camel/src/test/scala/akka/camel/component/ActorComponentTest.scala new file mode 100644 index 0000000000..b84ba70105 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/component/ActorComponentTest.scala @@ -0,0 +1,103 @@ +package akka.camel.component + +import org.apache.camel.{ Endpoint, AsyncProcessor } +import org.apache.camel.impl.DefaultCamelContext +import org.junit._ +import org.scalatest.junit.JUnitSuite + +import akka.actor.uuidFrom + +class ActorComponentTest extends JUnitSuite { + val component: ActorComponent = ActorComponentTest.actorComponent + + def testUUID = "93da8c80-c3fd-11df-abed-60334b120057" + + @Test + def shouldCreateEndpointWithIdDefined = { + val ep1: ActorEndpoint = component.createEndpoint("actor:abc").asInstanceOf[ActorEndpoint] + val ep2: ActorEndpoint = component.createEndpoint("actor:id:abc").asInstanceOf[ActorEndpoint] + assert(ep1.idValue === Some("abc")) + assert(ep2.idValue === Some("abc")) + assert(ep1.idType === "id") + assert(ep2.idType === "id") + assert(!ep1.blocking) + assert(!ep2.blocking) + assert(ep1.autoack) + assert(ep2.autoack) + } + + @Test + def shouldCreateEndpointWithIdTemplate = { + val ep: ActorEndpoint = component.createEndpoint("actor:id:").asInstanceOf[ActorEndpoint] + assert(ep.idValue === None) + assert(ep.idType === "id") + assert(!ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithIdTemplateAndBlockingSet = { + val ep: ActorEndpoint = component.createEndpoint("actor:id:?blocking=true").asInstanceOf[ActorEndpoint] + assert(ep.idValue === None) + assert(ep.idType === "id") + assert(ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithUuidDefined = { + val ep: ActorEndpoint = component.createEndpoint("actor:uuid:%s" format testUUID).asInstanceOf[ActorEndpoint] + assert(ep.idValue === Some(testUUID)) + assert(ep.idType === "uuid") + assert(!ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithUuidTemplate = { + val ep: ActorEndpoint = component.createEndpoint("actor:uuid:").asInstanceOf[ActorEndpoint] + assert(ep.idValue === None) + assert(ep.idType === "uuid") + assert(!ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithUuidTemplateAndBlockingSet = { + val ep: ActorEndpoint = component.createEndpoint("actor:uuid:?blocking=true").asInstanceOf[ActorEndpoint] + assert(ep.idValue === None) + assert(ep.idType === "uuid") + assert(ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithBlockingSet = { + val ep: ActorEndpoint = component.createEndpoint("actor:uuid:%s?blocking=true" format testUUID).asInstanceOf[ActorEndpoint] + assert(ep.idValue === Some(testUUID)) + assert(ep.idType === "uuid") + assert(ep.blocking) + assert(ep.autoack) + } + + @Test + def shouldCreateEndpointWithAutoackUnset = { + val ep: ActorEndpoint = component.createEndpoint("actor:uuid:%s?autoack=false" format testUUID).asInstanceOf[ActorEndpoint] + assert(ep.idValue === Some(testUUID)) + assert(ep.idType === "uuid") + assert(!ep.blocking) + assert(!ep.autoack) + } +} + +object ActorComponentTest { + def actorComponent = { + val component = new ActorComponent + component.setCamelContext(new DefaultCamelContext) + component + } + + def actorEndpoint(uri: String) = actorComponent.createEndpoint(uri) + def actorProducer(endpoint: Endpoint) = endpoint.createProducer + def actorAsyncProducer(endpoint: Endpoint) = endpoint.createProducer.asInstanceOf[AsyncProcessor] +} diff --git a/akka-camel/src/test/scala/akka/camel/component/ActorProducerTest.scala b/akka-camel/src/test/scala/akka/camel/component/ActorProducerTest.scala new file mode 100644 index 0000000000..b9096e0523 --- /dev/null +++ b/akka-camel/src/test/scala/akka/camel/component/ActorProducerTest.scala @@ -0,0 +1,253 @@ +package akka.camel.component + +import ActorComponentTest._ + +import java.util.concurrent.{ CountDownLatch, TimeoutException, TimeUnit } + +import org.apache.camel.{ AsyncCallback, ExchangePattern } + +import org.junit.{ After, Test } +import org.scalatest.junit.JUnitSuite +import org.scalatest.BeforeAndAfterAll + +import akka.actor.Actor._ +import akka.camel.{ Failure, Message } +import akka.camel.CamelTestSupport._ + +class ActorProducerTest extends JUnitSuite with BeforeAndAfterAll { + import ActorProducerTest._ + + @After + def tearDown = registry.local.shutdownAll + + @Test + def shouldSendMessageToActorWithSyncProcessor = { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOnly) + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + actorProducer(endpoint).process(exchange) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val reply = (actor !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply.body === "Martin") + assert(reply.headers === Map(Message.MessageExchangeId -> exchange.getExchangeId, "k1" -> "v1")) + } + + @Test + def shouldSendMessageToActorWithAsyncProcessor = { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOnly) + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + actorAsyncProducer(endpoint).process(exchange, expectSyncCompletion) + assert(latch.await(5000, TimeUnit.MILLISECONDS)) + val reply = (actor !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply.body === "Martin") + assert(reply.headers === Map(Message.MessageExchangeId -> exchange.getExchangeId, "k1" -> "v1")) + } + + @Test + def shouldSendMessageToActorAndReceiveResponseWithSyncProcessor = { + val actor = actorOf(new Tester2 { + override def response(msg: Message) = Message(super.response(msg), Map("k2" -> "v2")) + }).start + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOut) + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + actorProducer(endpoint).process(exchange) + assert(exchange.getOut.getBody === "Hello Martin") + assert(exchange.getOut.getHeader("k2") === "v2") + } + + @Test + def shouldSendMessageToActorAndReceiveResponseWithAsyncProcessor = { + val actor = actorOf(new Tester2 { + override def response(msg: Message) = Message(super.response(msg), Map("k2" -> "v2")) + }).start + val completion = expectAsyncCompletion + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOut) + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + actorAsyncProducer(endpoint).process(exchange, completion) + assert(completion.latch.await(5000, TimeUnit.MILLISECONDS)) + assert(exchange.getOut.getBody === "Hello Martin") + assert(exchange.getOut.getHeader("k2") === "v2") + } + + @Test + def shouldSendMessageToActorAndReceiveFailureWithAsyncProcessor = { + val actor = actorOf(new Tester2 { + override def response(msg: Message) = Failure(new Exception("testmsg"), Map("k3" -> "v3")) + }).start + val completion = expectAsyncCompletion + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOut) + exchange.getIn.setBody("Martin") + exchange.getIn.setHeader("k1", "v1") + actorAsyncProducer(endpoint).process(exchange, completion) + assert(completion.latch.await(5000, TimeUnit.MILLISECONDS)) + assert(exchange.getException.getMessage === "testmsg") + assert(exchange.getOut.getBody === null) + assert(exchange.getOut.getHeader("k3") === null) // headers from failure message are currently ignored + } + + @Test + def shouldSendMessageToActorAndReceiveAckWithAsyncProcessor = { + val actor = actorOf(new Tester2 { + override def response(msg: Message) = akka.camel.Ack + }).start + val completion = expectAsyncCompletion + val endpoint = actorEndpoint("actor:uuid:%s?autoack=false" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOnly) + exchange.getIn.setBody("Martin") + actorAsyncProducer(endpoint).process(exchange, completion) + assert(completion.latch.await(5000, TimeUnit.MILLISECONDS)) + assert(exchange.getIn.getBody === "Martin") + assert(exchange.getOut.getBody === null) + } + + @Test + def shouldDynamicallyRouteMessageToActorWithDefaultId = { + val actor1 = actorOf[Tester1]("x") + val actor2 = actorOf[Tester1]("y") + actor1.start + actor2.start + val latch1 = (actor1 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val latch2 = (actor2 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:id:%s" format actor1.address) + val exchange1 = endpoint.createExchange(ExchangePattern.InOnly) + val exchange2 = endpoint.createExchange(ExchangePattern.InOnly) + exchange1.getIn.setBody("Test1") + exchange2.getIn.setBody("Test2") + exchange2.getIn.setHeader(ActorComponent.ActorIdentifier, actor2.address) + actorProducer(endpoint).process(exchange1) + actorProducer(endpoint).process(exchange2) + assert(latch1.await(5, TimeUnit.SECONDS)) + assert(latch2.await(5, TimeUnit.SECONDS)) + val reply1 = (actor1 !! GetRetainedMessage).get.asInstanceOf[Message] + val reply2 = (actor2 !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply1.body === "Test1") + assert(reply2.body === "Test2") + } + + @Test + def shouldDynamicallyRouteMessageToActorWithoutDefaultId = { + val actor1 = actorOf[Tester1]("x") + val actor2 = actorOf[Tester1]("y") + actor1.start + actor2.start + val latch1 = (actor1 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val latch2 = (actor2 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:id:") + val exchange1 = endpoint.createExchange(ExchangePattern.InOnly) + val exchange2 = endpoint.createExchange(ExchangePattern.InOnly) + exchange1.getIn.setBody("Test1") + exchange2.getIn.setBody("Test2") + exchange1.getIn.setHeader(ActorComponent.ActorIdentifier, actor1.address) + exchange2.getIn.setHeader(ActorComponent.ActorIdentifier, actor2.address) + actorProducer(endpoint).process(exchange1) + actorProducer(endpoint).process(exchange2) + assert(latch1.await(5, TimeUnit.SECONDS)) + assert(latch2.await(5, TimeUnit.SECONDS)) + val reply1 = (actor1 !! GetRetainedMessage).get.asInstanceOf[Message] + val reply2 = (actor2 !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply1.body === "Test1") + assert(reply2.body === "Test2") + } + + @Test + def shouldDynamicallyRouteMessageToActorWithDefaultUuid = { + val actor1 = actorOf[Tester1].start + val actor2 = actorOf[Tester1].start + val latch1 = (actor1 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val latch2 = (actor2 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:uuid:%s" format actor1.uuid) + val exchange1 = endpoint.createExchange(ExchangePattern.InOnly) + val exchange2 = endpoint.createExchange(ExchangePattern.InOnly) + exchange1.getIn.setBody("Test1") + exchange2.getIn.setBody("Test2") + exchange2.getIn.setHeader(ActorComponent.ActorIdentifier, actor2.uuid.toString) + actorProducer(endpoint).process(exchange1) + actorProducer(endpoint).process(exchange2) + assert(latch1.await(5, TimeUnit.SECONDS)) + assert(latch2.await(5, TimeUnit.SECONDS)) + val reply1 = (actor1 !! GetRetainedMessage).get.asInstanceOf[Message] + val reply2 = (actor2 !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply1.body === "Test1") + assert(reply2.body === "Test2") + } + + @Test + def shouldDynamicallyRouteMessageToActorWithoutDefaultUuid = { + val actor1 = actorOf[Tester1].start + val actor2 = actorOf[Tester1].start + val latch1 = (actor1 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val latch2 = (actor2 !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:uuid:") + val exchange1 = endpoint.createExchange(ExchangePattern.InOnly) + val exchange2 = endpoint.createExchange(ExchangePattern.InOnly) + exchange1.getIn.setBody("Test1") + exchange2.getIn.setBody("Test2") + exchange1.getIn.setHeader(ActorComponent.ActorIdentifier, actor1.uuid) + exchange2.getIn.setHeader(ActorComponent.ActorIdentifier, actor2.uuid.toString) + actorProducer(endpoint).process(exchange1) + actorProducer(endpoint).process(exchange2) + assert(latch1.await(5, TimeUnit.SECONDS)) + assert(latch2.await(5, TimeUnit.SECONDS)) + val reply1 = (actor1 !! GetRetainedMessage).get.asInstanceOf[Message] + val reply2 = (actor2 !! GetRetainedMessage).get.asInstanceOf[Message] + assert(reply1.body === "Test1") + assert(reply2.body === "Test2") + } + + @Test + def shouldThrowExceptionWhenIdNotSet: Unit = { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:id:") + intercept[ActorIdentifierNotSetException] { + actorProducer(endpoint).process(endpoint.createExchange(ExchangePattern.InOnly)) + } + } + + @Test + def shouldThrowExceptionWhenUuidNotSet: Unit = { + val actor = actorOf[Tester1].start + val latch = (actor !! SetExpectedMessageCount(1)).as[CountDownLatch].get + val endpoint = actorEndpoint("actor:uuid:") + intercept[ActorIdentifierNotSetException] { + actorProducer(endpoint).process(endpoint.createExchange(ExchangePattern.InOnly)) + } + } + + @Test + def shouldSendMessageToActorAndTimeout(): Unit = { + val actor = actorOf[Tester3].start + val endpoint = actorEndpoint("actor:uuid:%s" format actor.uuid) + val exchange = endpoint.createExchange(ExchangePattern.InOut) + exchange.getIn.setBody("Martin") + intercept[TimeoutException] { + endpoint.createProducer.process(exchange) + } + } +} + +object ActorProducerTest { + def expectSyncCompletion = new AsyncCallback { + def done(doneSync: Boolean) = assert(doneSync) + } + + def expectAsyncCompletion = new AsyncCallback { + val latch = new CountDownLatch(1); + def done(doneSync: Boolean) = { + assert(!doneSync) + latch.countDown + } + } +} diff --git a/akka-kernel/src/main/scala/akka/kernel/DefaultAkkaLoader.scala b/akka-kernel/src/main/scala/akka/kernel/DefaultAkkaLoader.scala new file mode 100644 index 0000000000..33b8c855a8 --- /dev/null +++ b/akka-kernel/src/main/scala/akka/kernel/DefaultAkkaLoader.scala @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.http + +import akka.config.Config +import akka.util.{ Bootable, AkkaLoader } +import akka.remote.BootableRemoteActorService +import akka.actor.BootableActorLoaderService + +class DefaultAkkaLoader extends AkkaLoader { + def boot(): Unit = boot(true, new EmbeddedAppServer with BootableActorLoaderService with BootableRemoteActorService) +} + +/** + * Can be used to boot Akka + * + * java -cp ... akka.http.Main + */ +object Main extends DefaultAkkaLoader { + def main(args: Array[String]) = boot +} diff --git a/akka-kernel/src/main/scala/akka/kernel/EmbeddedAppServer.scala b/akka-kernel/src/main/scala/akka/kernel/EmbeddedAppServer.scala new file mode 100644 index 0000000000..286d2b3b04 --- /dev/null +++ b/akka-kernel/src/main/scala/akka/kernel/EmbeddedAppServer.scala @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.http + +import javax.ws.rs.core.UriBuilder +import javax.servlet.ServletConfig +import java.io.File + +import akka.actor.BootableActorLoaderService +import akka.util.Bootable + +import org.eclipse.jetty.xml.XmlConfiguration +import org.eclipse.jetty.server.{ Handler, Server } +import org.eclipse.jetty.server.handler.{ HandlerList, HandlerCollection, ContextHandler } +import java.net.URL +import akka.AkkaException + +/** + * Handles the Akka Comet Support (load/unload) + */ +trait EmbeddedAppServer extends Bootable { + self: BootableActorLoaderService ⇒ + + import akka.config.Config._ + + val REST_HOSTNAME = config.getString("akka.http.hostname", "localhost") + val REST_PORT = config.getInt("akka.http.port", 9998) + + val isRestEnabled = config.getList("akka.enabled-modules").exists(_ == "http") + + protected var server: Option[Server] = None + + protected def findJettyConfigXML: Option[URL] = + Option(applicationLoader.getOrElse(this.getClass.getClassLoader).getResource("microkernel-server.xml")) orElse + HOME.map(home ⇒ new File(home + "/config/microkernel-server.xml").toURI.toURL) + + abstract override def onLoad = { + super.onLoad + if (isRestEnabled) { + + val configuration = new XmlConfiguration(findJettyConfigXML.getOrElse(error("microkernel-server.xml not found!"))) + + System.setProperty("jetty.port", REST_PORT.toString) + System.setProperty("jetty.host", REST_HOSTNAME) + + HOME.foreach(home ⇒ System.setProperty("jetty.home", home + "/deploy/root")) + + server = Option(configuration.configure.asInstanceOf[Server]) map { s ⇒ //Set the correct classloader to our contexts + applicationLoader foreach { loader ⇒ + //We need to provide the correct classloader to the servlets + def setClassLoader(handlers: Seq[Handler]): Unit = { + handlers foreach { + case c: ContextHandler ⇒ c.setClassLoader(loader) + case c: HandlerCollection ⇒ setClassLoader(c.getHandlers) + case _ ⇒ + } + } + setClassLoader(s.getHandlers) + } + //Start the server + s.start() + s + } + } + } + + abstract override def onUnload = { + super.onUnload + server foreach { _.stop() } + } +} diff --git a/akka-kernel/src/main/scala/akka/kernel/Kernel.scala b/akka-kernel/src/main/scala/akka/kernel/Kernel.scala new file mode 100644 index 0000000000..1561a488aa --- /dev/null +++ b/akka-kernel/src/main/scala/akka/kernel/Kernel.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.kernel + +import akka.http.EmbeddedAppServer +import akka.util.AkkaLoader +import akka.remote.BootableRemoteActorService +import akka.actor.BootableActorLoaderService +import akka.camel.CamelService + +import java.util.concurrent.CountDownLatch + +object Main { + val keepAlive = new CountDownLatch(2) + + def main(args: Array[String]) = { + Kernel.boot + keepAlive.await + } +} + +/** + * The Akka Kernel, is used to start And postStop Akka in standalone/kernel mode. + * + * @author Jonas Bonér + */ +object Kernel extends AkkaLoader { + + def boot(): Unit = boot(true, new EmbeddedAppServer with BootableActorLoaderService with BootableRemoteActorService with CamelService) + + // For testing purposes only + def startRemoteService(): Unit = bundles.foreach(_ match { + case x: BootableRemoteActorService ⇒ x.startRemoteService + case _ ⇒ + }) +} diff --git a/akka-kernel/src/main/scala/akka/servlet/Initializer.scala b/akka-kernel/src/main/scala/akka/servlet/Initializer.scala new file mode 100644 index 0000000000..7b683c3d76 --- /dev/null +++ b/akka-kernel/src/main/scala/akka/servlet/Initializer.scala @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +package akka.servlet + +import akka.remote.BootableRemoteActorService +import akka.actor.BootableActorLoaderService +import akka.config.Config +import akka.util.{ Bootable, AkkaLoader } + +import javax.servlet.{ ServletContextListener, ServletContextEvent } + +/** + * This class can be added to web.xml mappings as a listener to start and postStop Akka. + * + * + * ... + * + * akka.servlet.Initializer + * + * ... + * + */ +class Initializer extends ServletContextListener { + lazy val loader = new AkkaLoader + + def contextDestroyed(e: ServletContextEvent): Unit = + loader.shutdown + + def contextInitialized(e: ServletContextEvent): Unit = + loader.boot(true, new BootableActorLoaderService with BootableRemoteActorService) +} diff --git a/akka-sbt-plugin/src/main/scala/AkkaKernelProject.scala b/akka-sbt-plugin/src/main/scala/AkkaKernelProject.scala new file mode 100644 index 0000000000..29f44f8404 --- /dev/null +++ b/akka-sbt-plugin/src/main/scala/AkkaKernelProject.scala @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2009-2011 Scalable Solutions AB + */ + +import sbt._ + +trait AkkaKernelProject extends AkkaProject with AkkaMicrokernelProject { + // automatic akka kernel dependency + val akkaKernel = akkaModule("kernel") +} + +trait AkkaMicrokernelProject extends AkkaConfigProject { + def distOutputPath = outputPath / "dist" + + def distBinName = "bin" + def distConfigName = "config" + def distDeployName = "deploy" + def distLibName = "lib" + + def distBinPath = distOutputPath / distBinName + def distConfigPath = distOutputPath / distConfigName + def distDeployPath = distOutputPath / distDeployName + def distLibPath = distOutputPath / distLibName + + def distJvmOptions = "-Xms1024M -Xmx1024M -Xss1M -XX:MaxPermSize=256M -XX:+UseParallelGC" + def distMainClass = "akka.kernel.Main" + + def distProjectDependencies = topologicalSort.dropRight(1) + + def distProjectDependenciesConfig = { + distProjectDependencies.flatMap( p => p match { + case acp: AkkaConfigProject => Some(acp.configSources) + case _ => None + }).foldLeft(Path.emptyPathFinder)(_ +++ _) + } + + def distConfigSources = configSources +++ distProjectDependenciesConfig + + def distDeployJars = jarPath + + def distRuntimeJars = { + runClasspath + .filter(ClasspathUtilities.isArchive) + .filter(jar => !jar.name.contains("-sources")) + .filter(jar => !jar.name.contains("-docs")) + } + + def distProjectDependencyJars = jarsOfProjectDependencies + + def distLibs = distRuntimeJars +++ distProjectDependencyJars +++ buildLibraryJar + + lazy val dist = (distAction dependsOn (`package`, distClean) + describedAs "Create an Akka microkernel distribution.") + + def distAction = task { + log.info("Creating distribution %s ..." format distOutputPath) + writeScripts(distScripts, distBinPath) orElse + copyFiles(distConfigSources, distConfigPath) orElse + copyFiles(distDeployJars, distDeployPath) orElse + copyFiles(distLibs, distLibPath) orElse { + log.info("Distribution created.") + None + } + } + + def copyFiles(from: PathFinder, to: Path) = { + FileUtilities.copyFlat(from.get, to, log).left.toOption + } + + lazy val distClean = distCleanAction describedAs "Clean the dist target dir." + + def distCleanAction = task { FileUtilities.clean(distOutputPath, log) } + + case class DistScript(name: String, contents: String, executable: Boolean) + + def distScripts = Set(DistScript("start", distShScript, true), + DistScript("start.bat", distBatScript, true)) + + def distShScript = """|#!/bin/sh + | + |AKKA_HOME="$(cd "$(cd "$(dirname "$0")"; pwd -P)"/..; pwd)" + |AKKA_CLASSPATH="$AKKA_HOME/lib/*:$AKKA_HOME/config" + |JAVA_OPTS="%s" + | + |java $JAVA_OPTS -cp "$AKKA_CLASSPATH" -Dakka.home="$AKKA_HOME" %s + |""".stripMargin.format(distJvmOptions, distMainClass) + + def distBatScript = """|@echo off + |set AKKA_HOME=%%~dp0.. + |set AKKA_CLASSPATH=%%AKKA_HOME%%\lib\*;%%AKKA_HOME%%\config + |set JAVA_OPTS=%s + | + |java %%JAVA_OPTS%% -cp "%%AKKA_CLASSPATH%%" -Dakka.home="%%AKKA_HOME%%" %s + |""".stripMargin.format(distJvmOptions, distMainClass) + + def writeScripts(scripts: Set[DistScript], to: Path) = { + scripts.map { script => + val target = to / script.name + FileUtilities.write(target.asFile, script.contents, log) orElse + setExecutable(target, script.executable) + }.foldLeft(None: Option[String])(_ orElse _) + } + + def setExecutable(target: Path, executable: Boolean): Option[String] = { + val success = target.asFile.setExecutable(executable, false) + if (success) None else Some("Couldn't set permissions of " + target) + } +} + +trait AkkaConfigProject extends BasicScalaProject with MavenStyleScalaPaths { + def mainConfigPath = mainSourcePath / "config" + + def configSources = mainConfigPath ** "*.*" + + override def mainUnmanagedClasspath = super.mainUnmanagedClasspath +++ mainConfigPath +} diff --git a/akka-spring/src/main/resources/META-INF/spring.handlers b/akka-spring/src/main/resources/META-INF/spring.handlers new file mode 100644 index 0000000000..645e812ed3 --- /dev/null +++ b/akka-spring/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://akka.io/schema/akka=akka.spring.AkkaNamespaceHandler diff --git a/akka-spring/src/main/resources/META-INF/spring.schemas b/akka-spring/src/main/resources/META-INF/spring.schemas new file mode 100644 index 0000000000..68e55a2fb4 --- /dev/null +++ b/akka-spring/src/main/resources/META-INF/spring.schemas @@ -0,0 +1 @@ +http\://akka.io/akka-2.0-SNAPSHOT.xsd=akka/spring/akka-2.0-SNAPSHOT.xsd diff --git a/akka-spring/src/main/resources/akka/spring/akka-2.0-SNAPSHOT.xsd b/akka-spring/src/main/resources/akka/spring/akka-2.0-SNAPSHOT.xsd new file mode 100644 index 0000000000..1ffc793102 --- /dev/null +++ b/akka-spring/src/main/resources/akka/spring/akka-2.0-SNAPSHOT.xsd @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the remote host. + + + + + + + Port of the remote host. + + + + + + + Management type for remote actors: client managed or server managed. + + + + + + + Custom service name for server managed actor. + + + + + + + + + + + + + + + + + + Name of the interface implemented by implementation class. + + + + + + + Name of the implementation class. + + + + + + + Bean instance behind the actor + + + + + + + The default timeout for '!!' invocations in milliseconds. + + + + + + + Defines the lifecycle, can be either 'permanent' or 'temporary'. + + + + + + + Supported scopes are 'singleton' and 'prototype'. + + + + + + + comma-separated list of beans that must be initialized before this bean + + + + + + + + + + + + + + + + + + Name of the implementation class. + + + + + + + Bean instance behind the actor + + + + + + + The default timeout for '!!' invocations in milliseconds. + + + + + + + Defines the lifecycle, can be either 'permanent' or 'temporary'. + + + + + + + Supported scopes are 'singleton' and 'prototype'. + + + + + + + true or false, defaults to false if omitted. + + + + + + + comma-separated list of beans that must be initialized before this bean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the remote host. + + + + + + + Port of the remote host. + + + + + + + Custom service name or class name for the server managed actor. + + + + + + + Name of the interface the typed actor implements. + + + + + + + + + + + + + + + Failover scheme, can be one of 'AllForOne' or 'OneForOne'. + + + + + + + Maximal number of restarts. + + + + + + + Time range for maximal number of restart. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akka-spring/src/main/scala/akka/spring/ActorBeanDefinitionParser.scala b/akka-spring/src/main/scala/akka/spring/ActorBeanDefinitionParser.scala new file mode 100644 index 0000000000..2abb1024d8 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/ActorBeanDefinitionParser.scala @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser +import org.springframework.beans.factory.xml.ParserContext +import AkkaSpringConfigurationTags._ +import org.w3c.dom.Element + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class TypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val typedActorConf = parseActor(element) + typedActorConf.typed = TYPED_ACTOR_TAG + typedActorConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] +} + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class UntypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val untypedActorConf = parseActor(element) + untypedActorConf.typed = UNTYPED_ACTOR_TAG + untypedActorConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] +} + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class ActorForBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorForParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val actorForConf = parseActorFor(element) + actorForConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorForFactoryBean] +} + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class ConfigBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val location = element.getAttribute(LOCATION) + builder.addPropertyValue(LOCATION, location) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ConfiggyPropertyPlaceholderConfigurer] + + override def shouldGenerateId() = true +} diff --git a/akka-spring/src/main/scala/akka/spring/ActorFactoryBean.scala b/akka-spring/src/main/scala/akka/spring/ActorFactoryBean.scala new file mode 100644 index 0000000000..26ae06fb8a --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/ActorFactoryBean.scala @@ -0,0 +1,252 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.spring + +import org.springframework.beans.{BeanUtils,BeansException,BeanWrapper,BeanWrapperImpl} +import org.springframework.beans.factory.config.AbstractFactoryBean +import org.springframework.context.{ApplicationContext,ApplicationContextAware} +import org.springframework.util.StringUtils + +import akka.actor.{ActorRef, ActorRegistry, AspectInitRegistry, TypedActorConfiguration, TypedActor,Actor} +import akka.event.EventHandler +import akka.dispatch.MessageDispatcher +import akka.util.Duration + +import scala.reflect.BeanProperty + +import java.net.InetSocketAddress + +/** + * Exception to use when something goes wrong during bean creation. + * + * @author Johan Rask + */ +class AkkaBeansException(message: String, cause:Throwable) extends BeansException(message, cause) { + def this(message: String) = this(message, null) +} + +/** + * Factory bean for typed and untyped actors. + * + * @author michaelkober + * @author Johan Rask + * @author Martin Krasser + * @author Jonas Bonér + */ +class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with ApplicationContextAware { + import StringReflect._ + import AkkaSpringConfigurationTags._ + @BeanProperty var id: String = "" + @BeanProperty var typed: String = "" + @BeanProperty var interface: String = "" + @BeanProperty var implementation: String = "" + @BeanProperty var beanRef: String = null + @BeanProperty var timeoutStr: String = "" + @BeanProperty var host: String = "" + @BeanProperty var port: String = "" + @BeanProperty var serverManaged: Boolean = false + @BeanProperty var autostart: Boolean = false + @BeanProperty var serviceName: String = "" + @BeanProperty var lifecycle: String = "" + @BeanProperty var dispatcher: DispatcherProperties = _ + @BeanProperty var scope: String = VAL_SCOPE_SINGLETON + @BeanProperty var property: PropertyEntries = _ + @BeanProperty var applicationContext: ApplicationContext = _ + + lazy val timeout = try { + if (!timeoutStr.isEmpty) timeoutStr.toLong else -1L + } catch { + case nfe: NumberFormatException => + EventHandler notifyListeners EventHandler.Error(nfe, this, "could not parse timeout %s" format timeoutStr) + throw nfe + } + + // Holds info about if deps have been set or not. Depends on + // if interface is specified or not. We must set deps on + // target instance if interface is specified + var hasSetDependecies = false + + override def isSingleton = scope.equals(VAL_SCOPE_SINGLETON) + + /* + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + def getObjectType: Class[AnyRef] = try { + implementation.toClass + } catch { + // required by contract to return null + case e: IllegalArgumentException => null + } + + /* + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + def createInstance: AnyRef = { + val ref = typed match { + case TYPED_ACTOR_TAG => val typedActor = createTypedInstance() + setProperties(AspectInitRegistry.initFor(typedActor).targetInstance) + typedActor + case UNTYPED_ACTOR_TAG => val untypedActor = createUntypedInstance() + setProperties(untypedActor.actor) + if (autostart) + untypedActor.start + untypedActor + case _ => throw new IllegalArgumentException("Unknown actor type") + } + ref + } + + private[akka] def createTypedInstance() : AnyRef = { + if (!StringUtils.hasText(interface)) throw new AkkaBeansException( + "The 'interface' part of the 'akka:actor' element in the Spring config file can't be null or empty string") + if ((!StringUtils.hasText(implementation)) && (beanRef eq null)) throw new AkkaBeansException( + "Either 'implementation' or 'ref' must be specified as attribute of the 'akka:typed-actor' element in the Spring config file ") + + val typedActor: AnyRef = if (beanRef eq null ) + TypedActor.newInstance(interface.toClass, implementation.toClass, createConfig) + else + TypedActor.newInstance(interface.toClass, getBeanFactory().getBean(beanRef), createConfig) + + if (isRemote && serverManaged) { + if (serviceName.isEmpty) { + Actor.remote.registerTypedActor(interface, typedActor) + } else { + Actor.remote.registerTypedActor(serviceName, typedActor) + } + } + typedActor + } + + /** + * Create an UntypedActor. + */ + private[akka] def createUntypedInstance() : ActorRef = { + if ((!StringUtils.hasText(implementation)) && (beanRef eq null)) throw new AkkaBeansException( + "Either 'implementation' or 'ref' must be specified as attribute of the 'akka:untyped-actor' element in the Spring config file ") + + val actorRef = if (isRemote && !serverManaged) { //If clientManaged + if (beanRef ne null) + Actor.remote.actorOf(getBeanFactory().getBean(beanRef).asInstanceOf[Actor], host, port.toInt) + else + Actor.remote.actorOf(implementation.toClass, host, port.toInt) + } else { + if (beanRef ne null) + Actor.actorOf(getBeanFactory().getBean(beanRef).asInstanceOf[Actor]) + else + Actor.actorOf(implementation.toClass) + } + + if (timeout > 0) + actorRef.setTimeout(timeout) + + if(StringUtils.hasText(id)) + actorRef.id = id + + if (hasDispatcher) + actorRef.setDispatcher( dispatcherInstance( if (dispatcher.dispatcherType == THREAD_BASED) Some(actorRef) else None ) ) + + if (isRemote && serverManaged) { + if (serviceName.isEmpty) + Actor.remote.register(actorRef) + else + Actor.remote.register(serviceName, actorRef) + } + + actorRef + } + + /** + * Stop the typed actor if it is a singleton. + */ + override def destroyInstance(instance: AnyRef) { + typed match { + case TYPED_ACTOR_TAG => TypedActor.stop(instance) + case UNTYPED_ACTOR_TAG => instance.asInstanceOf[ActorRef].stop + } + } + + private def setProperties(ref: AnyRef): AnyRef = { + if (hasSetDependecies) return ref + val beanWrapper = new BeanWrapperImpl(ref) + if (ref.isInstanceOf[ApplicationContextAware]) { + beanWrapper.setPropertyValue("applicationContext", applicationContext) + } + for (entry <- property.entryList) { + val propertyDescriptor = BeanUtils.getPropertyDescriptor(ref.getClass, entry.name) + val method = propertyDescriptor.getWriteMethod + if (StringUtils.hasText(entry.ref)) { + method.invoke(ref,getBeanFactory().getBean(entry.ref)) + } else if(StringUtils.hasText(entry.value)) { + beanWrapper.setPropertyValue(entry.name,entry.value) + } else throw new AkkaBeansException("Either property@ref or property@value must be set on property element") + } + ref + } + + + private[akka] def createConfig: TypedActorConfiguration = { + val config = new TypedActorConfiguration().timeout(Duration(timeout, "millis")) + if (isRemote && !serverManaged) config.makeRemote(host, port.toInt) + if (StringUtils.hasText(id)) config.id(id) + if (hasDispatcher) { + if (dispatcher.dispatcherType == THREAD_BASED) { + config.threadBasedDispatcher() + } else { + config.dispatcher(dispatcherInstance()) + } + } + config + } + + private[akka] def isRemote = StringUtils.hasText(host) + + private[akka] def hasDispatcher = (dispatcher ne null) && (StringUtils.hasText(dispatcher.dispatcherType)) + + /** + * Create dispatcher instance with dispatcher properties. + * @param actorRef actorRef for thread based dispatcher + * @return new dispatcher instance + */ + private[akka] def dispatcherInstance(actorRef: Option[ActorRef] = None) : MessageDispatcher = { + import DispatcherFactoryBean._ + if (dispatcher.dispatcherType == THREAD_BASED) { + createNewInstance(dispatcher, actorRef) + } else { + createNewInstance(dispatcher) + } + } +} + +/** + * Factory bean for remote client actor-for. + * + * @author michaelkober + */ +class ActorForFactoryBean extends AbstractFactoryBean[AnyRef] with ApplicationContextAware { + import StringReflect._ + import AkkaSpringConfigurationTags._ + + @BeanProperty var interface: String = "" + @BeanProperty var host: String = "" + @BeanProperty var port: String = "" + @BeanProperty var serviceName: String = "" + @BeanProperty var applicationContext: ApplicationContext = _ + + override def isSingleton = false + + /* + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + def getObjectType: Class[AnyRef] = classOf[AnyRef] + + /* + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + def createInstance: AnyRef = interface match { + case null|"" => Actor.remote.actorFor(serviceName, host, port.toInt) + case iface => Actor.remote.typedActorFor(iface.toClass, serviceName, host, port.toInt) + } +} + diff --git a/akka-spring/src/main/scala/akka/spring/ActorParser.scala b/akka-spring/src/main/scala/akka/spring/ActorParser.scala new file mode 100644 index 0000000000..fd815b36c5 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/ActorParser.scala @@ -0,0 +1,232 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.util.xml.DomUtils +import org.w3c.dom.Element +import scala.collection.JavaConversions._ + +/** + * Parser trait for custom namespace configuration for typed-actor. + * @author michaelkober + * @author Johan Rask + * @author Martin Krasser + */ +trait ActorParser extends BeanParser with DispatcherParser { + import AkkaSpringConfigurationTags._ + + /** + * Parses the given element and returns a TypedActorProperties. + * @param element dom element to parse + * @return configuration for the typed actor + */ + def parseActor(element: Element): ActorProperties = { + val objectProperties = new ActorProperties() + val remoteElement = DomUtils.getChildElementByTagName(element, REMOTE_TAG); + val dispatcherElement = DomUtils.getChildElementByTagName(element, DISPATCHER_TAG) + val propertyEntries = DomUtils.getChildElementsByTagName(element, PROPERTYENTRY_TAG) + + if (remoteElement ne null) { + objectProperties.host = mandatory(remoteElement, HOST) + objectProperties.port = mandatory(remoteElement, PORT) + objectProperties.serverManaged = SERVER_MANAGED == remoteElement.getAttribute(MANAGED_BY) + val serviceName = remoteElement.getAttribute(SERVICE_NAME) + if ((serviceName ne null) && (!serviceName.isEmpty)) { + objectProperties.serviceName = serviceName + objectProperties.serverManaged = true + } + } + + if (dispatcherElement ne null) { + val dispatcherProperties = parseDispatcher(dispatcherElement) + objectProperties.dispatcher = dispatcherProperties + } + + for (element <- propertyEntries) { + val entry = new PropertyEntry + entry.name = element.getAttribute("name"); + entry.value = element.getAttribute("value") + entry.ref = element.getAttribute("ref") + objectProperties.propertyEntries.add(entry) + } + + objectProperties.timeoutStr = element.getAttribute(TIMEOUT) + objectProperties.target = if (element.getAttribute(IMPLEMENTATION).isEmpty) null else element.getAttribute(IMPLEMENTATION) + objectProperties.beanRef = if (element.getAttribute(BEANREF).isEmpty) null else element.getAttribute(BEANREF) + objectProperties.id = element.getAttribute("id") + objectProperties.autostart = element.getAttribute(AUTOSTART) match { + case null|"" => false + case other => other.toBoolean + } + objectProperties.dependsOn = element.getAttribute(DEPENDS_ON) match { + case null|"" => Array[String]() + case other => for (dep <- other.split(",")) yield dep.trim + } + + if (objectProperties.target == null && objectProperties.beanRef == null) { + throw new IllegalArgumentException("Mandatory attribute missing, you need to provide either implementation or ref ") + } + + if (element.hasAttribute(INTERFACE)) { + objectProperties.interface = element.getAttribute(INTERFACE) + } + if (element.hasAttribute(LIFECYCLE)) { + objectProperties.lifecycle = element.getAttribute(LIFECYCLE) + } + if (element.hasAttribute(SCOPE)) { + objectProperties.scope = element.getAttribute(SCOPE) + } + + objectProperties + } + +} + +/** + * Parser trait for custom namespace configuration for RemoteClient actor-for. + * @author michaelkober + */ +trait ActorForParser extends BeanParser { + import AkkaSpringConfigurationTags._ + + /** + * Parses the given element and returns a ActorForProperties. + * @param element dom element to parse + * @return configuration for the typed actor + */ + def parseActorFor(element: Element): ActorForProperties = { + val objectProperties = new ActorForProperties() + + objectProperties.host = mandatory(element, HOST) + objectProperties.port = mandatory(element, PORT) + objectProperties.serviceName = mandatory(element, SERVICE_NAME) + if (element.hasAttribute(INTERFACE)) { + objectProperties.interface = element.getAttribute(INTERFACE) + } + objectProperties + } + +} + +/** + * Base trait with utility methods for bean parsing. + */ +trait BeanParser { + + /** + * Get a mandatory element attribute. + * @param element the element with the mandatory attribute + * @param attribute name of the mandatory attribute + */ + def mandatory(element: Element, attribute: String): String = { + if ((element.getAttribute(attribute) eq null) || (element.getAttribute(attribute).isEmpty)) { + throw new IllegalArgumentException("Mandatory attribute missing: " + attribute) + } else { + element.getAttribute(attribute) + } + } + + /** + * Get a mandatory child element. + * @param element the parent element + * @param childName name of the mandatory child element + */ + def mandatoryElement(element: Element, childName: String): Element = { + val childElement = DomUtils.getChildElementByTagName(element, childName); + if (childElement eq null) { + throw new IllegalArgumentException("Mandatory element missing: ''") + } else { + childElement + } + } + +} + + +/** + * Parser trait for custom namespace for Akka dispatcher configuration. + * @author michaelkober + */ +trait DispatcherParser extends BeanParser { + import AkkaSpringConfigurationTags._ + + /** + * Parses the given element and returns a DispatcherProperties. + * @param element dom element to parse + * @return configuration for the dispatcher + */ + def parseDispatcher(element: Element): DispatcherProperties = { + val properties = new DispatcherProperties() + var dispatcherElement = element + if (hasRef(element)) { + val ref = element.getAttribute(REF) + dispatcherElement = element.getOwnerDocument.getElementById(ref) + if (dispatcherElement eq null) { + throw new IllegalArgumentException("Referenced dispatcher not found: '" + ref + "'") + } + } + + properties.dispatcherType = mandatory(dispatcherElement, TYPE) + if (properties.dispatcherType == THREAD_BASED) { + val allowedParentNodes = "akka:typed-actor" :: "akka:untyped-actor" :: "typed-actor" :: "untyped-actor" :: Nil + if (!allowedParentNodes.contains(dispatcherElement.getParentNode.getNodeName)) { + throw new IllegalArgumentException("Thread based dispatcher must be nested in 'typed-actor' or 'untyped-actor' element!") + } + } + + properties.name = mandatory(dispatcherElement, NAME) + + val threadPoolElement = DomUtils.getChildElementByTagName(dispatcherElement, THREAD_POOL_TAG); + if (threadPoolElement ne null) { + if (properties.dispatcherType == THREAD_BASED) { + throw new IllegalArgumentException("Element 'thread-pool' not allowed for this dispatcher type.") + } + val threadPoolProperties = parseThreadPool(threadPoolElement) + properties.threadPool = threadPoolProperties + } + properties + } + + /** + * Parses the given element and returns a ThreadPoolProperties. + * @param element dom element to parse + * @return configuration for the thread pool + */ + def parseThreadPool(element: Element): ThreadPoolProperties = { + val properties = new ThreadPoolProperties() + properties.queue = element.getAttribute(QUEUE) + if (element.hasAttribute(CAPACITY)) { + properties.capacity = element.getAttribute(CAPACITY).toInt + } + if (element.hasAttribute(BOUND)) { + properties.bound = element.getAttribute(BOUND).toInt + } + if (element.hasAttribute(FAIRNESS)) { + properties.fairness = element.getAttribute(FAIRNESS).toBoolean + } + if (element.hasAttribute(CORE_POOL_SIZE)) { + properties.corePoolSize = element.getAttribute(CORE_POOL_SIZE).toInt + } + if (element.hasAttribute(MAX_POOL_SIZE)) { + properties.maxPoolSize = element.getAttribute(MAX_POOL_SIZE).toInt + } + if (element.hasAttribute(KEEP_ALIVE)) { + properties.keepAlive = element.getAttribute(KEEP_ALIVE).toLong + } + if (element.hasAttribute(REJECTION_POLICY)) { + properties.rejectionPolicy = element.getAttribute(REJECTION_POLICY) + } + if (element.hasAttribute(MAILBOX_CAPACITY)) { + properties.mailboxCapacity = element.getAttribute(MAILBOX_CAPACITY).toInt + } + properties + } + + def hasRef(element: Element): Boolean = { + val ref = element.getAttribute(REF) + (ref ne null) && !ref.isEmpty + } + +} + diff --git a/akka-spring/src/main/scala/akka/spring/ActorProperties.scala b/akka-spring/src/main/scala/akka/spring/ActorProperties.scala new file mode 100644 index 0000000000..6f1a2b639d --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/ActorProperties.scala @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import AkkaSpringConfigurationTags._ + +/** + * Data container for actor configuration data. + * @author michaelkober + * @author Martin Krasser + */ +class ActorProperties { + var id: String = "" + var typed: String = "" + var target: String = "" + var beanRef: String = "" + var timeoutStr: String = "" + var interface: String = "" + var host: String = "" + var port: String = "" + var serverManaged: Boolean = false + var autostart: Boolean = false + var serviceName: String = "" + var lifecycle: String = "" + var scope:String = VAL_SCOPE_SINGLETON + var dispatcher: DispatcherProperties = _ + var propertyEntries = new PropertyEntries() + var dependsOn: Array[String] = Array[String]() + + /** + * Sets the properties to the given builder. + * @param builder bean definition builder + */ + def setAsProperties(builder: BeanDefinitionBuilder) { + builder.addPropertyValue("typed", typed) + builder.addPropertyValue(HOST, host) + builder.addPropertyValue(PORT, port) + builder.addPropertyValue("serverManaged", serverManaged) + builder.addPropertyValue("serviceName", serviceName) + builder.addPropertyValue("timeoutStr", timeoutStr) + builder.addPropertyValue(IMPLEMENTATION, target) + builder.addPropertyValue("beanRef", beanRef) + builder.addPropertyValue(INTERFACE, interface) + builder.addPropertyValue(LIFECYCLE, lifecycle) + builder.addPropertyValue(SCOPE, scope) + builder.addPropertyValue(DISPATCHER_TAG, dispatcher) + builder.addPropertyValue(PROPERTYENTRY_TAG,propertyEntries) + builder.addPropertyValue("id", id) + builder.addPropertyValue(AUTOSTART, autostart) + dependsOn foreach { dep => builder.addDependsOn(dep) } + } + + def timeout() : Long = { + if (!timeoutStr.isEmpty) timeoutStr.toLong else -1L + } + +} + +/** + * Data container for actor configuration data. + * @author michaelkober + */ +class ActorForProperties { + var interface: String = "" + var host: String = "" + var port: String = "" + var serviceName: String = "" + + /** + * Sets the properties to the given builder. + * @param builder bean definition builder + */ + def setAsProperties(builder: BeanDefinitionBuilder) { + builder.addPropertyValue(HOST, host) + builder.addPropertyValue(PORT, port) + builder.addPropertyValue("serviceName", serviceName) + builder.addPropertyValue(INTERFACE, interface) + } + +} diff --git a/akka-spring/src/main/scala/akka/spring/AkkaNamespaceHandler.scala b/akka-spring/src/main/scala/akka/spring/AkkaNamespaceHandler.scala new file mode 100644 index 0000000000..38041a3ea4 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/AkkaNamespaceHandler.scala @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport +import AkkaSpringConfigurationTags._ + +/** + * Custom spring namespace handler for Akka. + * @author michaelkober + */ +class AkkaNamespaceHandler extends NamespaceHandlerSupport { + def init = { + registerBeanDefinitionParser(CONFIG_TAG, new ConfigBeanDefinitionParser()); + registerBeanDefinitionParser(TYPED_ACTOR_TAG, new TypedActorBeanDefinitionParser()) + registerBeanDefinitionParser(UNTYPED_ACTOR_TAG, new UntypedActorBeanDefinitionParser()) + registerBeanDefinitionParser(SUPERVISION_TAG, new SupervisionBeanDefinitionParser()) + registerBeanDefinitionParser(DISPATCHER_TAG, new DispatcherBeanDefinitionParser()) + registerBeanDefinitionParser(CAMEL_SERVICE_TAG, new CamelServiceBeanDefinitionParser) + registerBeanDefinitionParser(ACTOR_FOR_TAG, new ActorForBeanDefinitionParser()); + } +} diff --git a/akka-spring/src/main/scala/akka/spring/AkkaSpringConfigurationTags.scala b/akka-spring/src/main/scala/akka/spring/AkkaSpringConfigurationTags.scala new file mode 100644 index 0000000000..6ca7bb9933 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/AkkaSpringConfigurationTags.scala @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +/** + * XML configuration tags. + * @author michaelkober + * @author Martin Krasser + */ +object AkkaSpringConfigurationTags { + + // --- TAGS + // + // top level tags + val CONFIG_TAG = "property-placeholder" + val TYPED_ACTOR_TAG = "typed-actor" + val UNTYPED_ACTOR_TAG = "untyped-actor" + val SUPERVISION_TAG = "supervision" + val DISPATCHER_TAG = "dispatcher" + val PROPERTYENTRY_TAG = "property" + val CAMEL_SERVICE_TAG = "camel-service" + val ACTOR_FOR_TAG = "actor-for" + + // actor sub tags + val REMOTE_TAG = "remote" + + // superivision sub tags + val TYPED_ACTORS_TAG = "typed-actors" + val UNTYPED_ACTORS_TAG = "untyped-actors" + val STRATEGY_TAG = "restart-strategy" + val TRAP_EXISTS_TAG = "trap-exits" + val TRAP_EXIT_TAG = "trap-exit" + + // dispatcher sub tags + val THREAD_POOL_TAG = "thread-pool" + + // camel-service sub tags + val CAMEL_CONTEXT_TAG = "camel-context" + + // --- ATTRIBUTES + // + // actor attributes + val TIMEOUT = "timeout" + val IMPLEMENTATION = "implementation" + val BEANREF = "ref" + val INTERFACE = "interface" + val HOST = "host" + val PORT = "port" + val MANAGED_BY = "managed-by" + val SERVICE_NAME = "service-name" + val LIFECYCLE = "lifecycle" + val SCOPE = "scope" + val AUTOSTART = "autostart" + val DEPENDS_ON = "depends-on" + + // supervision attributes + val FAILOVER = "failover" + val RETRIES = "retries" + val TIME_RANGE = "timerange" + + // dispatcher attributes + val NAME = "name" + val REF = "ref" + val TYPE = "type" + + // thread pool attributes + val QUEUE = "queue" + val CAPACITY = "capacity" + val FAIRNESS = "fairness" + val CORE_POOL_SIZE = "core-pool-size" + val MAX_POOL_SIZE = "max-pool-size" + val KEEP_ALIVE = "keep-alive" + val BOUND ="bound" + val REJECTION_POLICY ="rejection-policy" + val MAILBOX_CAPACITY ="mailbox-capacity" + + // config attribute + val LOCATION = "location" + + // --- VALUES + // + // Lifecycle + val VAL_LIFECYCYLE_TEMPORARY = "temporary" + val VAL_LIFECYCYLE_PERMANENT = "permanent" + + val VAL_SCOPE_SINGLETON = "singleton" + val VAL_SCOPE_PROTOTYPE = "prototype" + + // Failover + val VAL_ALL_FOR_ONE = "AllForOne" + val VAL_ONE_FOR_ONE = "OneForOne" + + // rejection policies + val VAL_ABORT_POLICY = "abort-policy" + val VAL_CALLER_RUNS_POLICY = "caller-runs-policy" + val VAL_DISCARD_OLDEST_POLICY = "discard-oldest-policy" + val VAL_DISCARD_POLICY = "discard-policy" + + // dispatcher queue types + val VAL_BOUNDED_LINKED_BLOCKING_QUEUE = "bounded-linked-blocking-queue" + val VAL_UNBOUNDED_LINKED_BLOCKING_QUEUE = "unbounded-linked-blocking-queue" + val VAL_SYNCHRONOUS_QUEUE = "synchronous-queue" + val VAL_BOUNDED_ARRAY_BLOCKING_QUEUE = "bounded-array-blocking-queue" + + // dispatcher types + val EXECUTOR_BASED_EVENT_DRIVEN = "executor-based-event-driven" + val EXECUTOR_BASED_EVENT_DRIVEN_WORK_STEALING = "executor-based-event-driven-work-stealing" + val THREAD_BASED = "thread-based" + + // managed by types + val SERVER_MANAGED = "server" + val CLIENT_MANAGED = "client" + +} diff --git a/akka-spring/src/main/scala/akka/spring/CamelServiceBeanDefinitionParser.scala b/akka-spring/src/main/scala/akka/spring/CamelServiceBeanDefinitionParser.scala new file mode 100644 index 0000000000..4025a831a8 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/CamelServiceBeanDefinitionParser.scala @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.{ParserContext, AbstractSingleBeanDefinitionParser} +import org.springframework.util.xml.DomUtils +import org.w3c.dom.Element + +import akka.spring.AkkaSpringConfigurationTags._ + + +/** + * Parser for <camel-service> elements. + * + * @author Martin Krasser + */ +class CamelServiceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + /** + * Parses the <camel-service> element. If a nested <camel-context> element + * is defined then the referenced context is set on the {@link CamelServiceFactoryBean}. + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val camelContextElement = DomUtils.getChildElementByTagName(element, CAMEL_CONTEXT_TAG); + if (camelContextElement ne null) { + val camelContextReference = camelContextElement.getAttribute("ref") + builder.addPropertyReference("camelContext", camelContextReference) + } + } + + /** + * Returns the class of {@link CamelServiceFactoryBean} + */ + override def getBeanClass(element: Element): Class[_] = classOf[CamelServiceFactoryBean] + + /** + * Returns true. + */ + override def shouldGenerateIdAsFallback = true +} diff --git a/akka-spring/src/main/scala/akka/spring/CamelServiceFactoryBean.scala b/akka-spring/src/main/scala/akka/spring/CamelServiceFactoryBean.scala new file mode 100644 index 0000000000..337413f0eb --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/CamelServiceFactoryBean.scala @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.apache.camel.CamelContext +import org.springframework.beans.factory.{DisposableBean, InitializingBean, FactoryBean} + +import akka.camel.{CamelContextManager, CamelService, CamelServiceFactory} + +/** + * Factory bean for a {@link CamelService}. + * + * @author Martin Krasser + */ +class CamelServiceFactoryBean extends FactoryBean[CamelService] with InitializingBean with DisposableBean { + @scala.reflect.BeanProperty var camelContext: CamelContext = _ + + var instance: CamelService = _ + + def isSingleton = true + + def getObjectType = classOf[CamelService] + + def getObject = instance + + /** + * Initializes the {@link CamelContextManager} with camelService if defined, then + * creates and starts the {@link CamelService} singleton. + */ + def afterPropertiesSet = { + if (camelContext ne null) { + CamelContextManager.init(camelContext) + } + instance = CamelServiceFactory.createCamelService + instance.start + } + + /** + * Stops the {@link CamelService} singleton. + */ + def destroy = { + instance.stop + } +} diff --git a/akka-spring/src/main/scala/akka/spring/ConfiggyPropertyPlaceholderConfigurer.scala b/akka-spring/src/main/scala/akka/spring/ConfiggyPropertyPlaceholderConfigurer.scala new file mode 100644 index 0000000000..38bf1dc111 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/ConfiggyPropertyPlaceholderConfigurer.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import akka.config.Configuration +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +import org.springframework.core.io.Resource +import java.util.Properties + +/** + * ConfiggyPropertyPlaceholderConfigurer. Property resource configurer for configgy files. + */ +class ConfiggyPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { + + /** + * Sets the akka properties as local properties, leaves the location empty. + * @param configgyResource akka.conf + */ + override def setLocation(configgyResource: Resource) { + if (configgyResource eq null) throw new IllegalArgumentException("Property 'config' must be set") + val properties = loadAkkaConfig(configgyResource) + setProperties(properties) + } + + /** + * Load the akka.conf and transform to properties. + */ + private def loadAkkaConfig(configgyResource: Resource) : Properties = { + val config = Configuration.fromFile(configgyResource.getFile.getPath) + val properties = new Properties() + config.map.foreach {case (k, v) => properties.put(k, v.asInstanceOf[AnyRef]); println("(k,v)=" + k + ", " + v)} + properties + } + +} diff --git a/akka-spring/src/main/scala/akka/spring/DispatcherBeanDefinitionParser.scala b/akka-spring/src/main/scala/akka/spring/DispatcherBeanDefinitionParser.scala new file mode 100644 index 0000000000..4f2a40469f --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/DispatcherBeanDefinitionParser.scala @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.w3c.dom.Element +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.{ParserContext, AbstractSingleBeanDefinitionParser} + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class DispatcherBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser with DispatcherParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val dispatcherProperties = parseDispatcher(element) + dispatcherProperties.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[DispatcherFactoryBean] +} diff --git a/akka-spring/src/main/scala/akka/spring/DispatcherFactoryBean.scala b/akka-spring/src/main/scala/akka/spring/DispatcherFactoryBean.scala new file mode 100644 index 0000000000..14c2a802df --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/DispatcherFactoryBean.scala @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.config.AbstractFactoryBean +import akka.config.Supervision._ +import AkkaSpringConfigurationTags._ +import reflect.BeanProperty +import akka.actor.ActorRef +import java.util.concurrent.RejectedExecutionHandler +import java.util.concurrent.ThreadPoolExecutor.{DiscardPolicy, DiscardOldestPolicy, CallerRunsPolicy, AbortPolicy} +import akka.dispatch._ +import akka.util.Duration + +/** + * Reusable factory method for dispatchers. + */ +object DispatcherFactoryBean { + + /** + * factory method for dispatchers + * @param properties dispatcher properties + * @param actorRef actorRef needed for thread based dispatcher + */ + def createNewInstance(properties: DispatcherProperties, actorRef: Option[ActorRef] = None): MessageDispatcher = { + + //Creates a ThreadPoolConfigDispatcherBuilder and applies the configuration to it + def configureThreadPool(createDispatcher: => (ThreadPoolConfig) => MessageDispatcher): ThreadPoolConfigDispatcherBuilder = { + if ((properties.threadPool ne null) && (properties.threadPool.queue ne null)) { + import ThreadPoolConfigDispatcherBuilder.conf_? + import properties._ + val queueDef = Some(threadPool.queue) + val corePoolSize = if (threadPool.corePoolSize > -1) Some(threadPool.corePoolSize) else None + val maxPoolSize = if (threadPool.maxPoolSize > -1) Some(threadPool.maxPoolSize) else None + val keepAlive = if (threadPool.keepAlive > -1) Some(threadPool.keepAlive) else None + val executorBounds = if (threadPool.bound > -1) Some(threadPool.bound) else None + val flowHandler = threadPool.rejectionPolicy match { + case null | "" => None + case "abort-policy" => Some(new AbortPolicy()) + case "caller-runs-policy" => Some(new CallerRunsPolicy()) + case "discard-oldest-policy" => Some(new DiscardOldestPolicy()) + case "discard-policy" => Some(new DiscardPolicy()) + case x => throw new IllegalArgumentException("Unknown rejection-policy '" + x + "'") + } + + //Apply the following options to the config if they are present in the cfg + ThreadPoolConfigDispatcherBuilder(createDispatcher,ThreadPoolConfig()).configure( + conf_?(queueDef )(definition => definition match { + case VAL_BOUNDED_ARRAY_BLOCKING_QUEUE => + _.withNewThreadPoolWithArrayBlockingQueueWithCapacityAndFairness(threadPool.capacity,threadPool.fairness) + case VAL_UNBOUNDED_LINKED_BLOCKING_QUEUE if threadPool.capacity > -1 => + _.withNewThreadPoolWithLinkedBlockingQueueWithCapacity(threadPool.capacity) + case VAL_UNBOUNDED_LINKED_BLOCKING_QUEUE if threadPool.capacity <= 0 => + _.withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity + case VAL_BOUNDED_LINKED_BLOCKING_QUEUE => + _.withNewBoundedThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity(threadPool.bound) + case VAL_SYNCHRONOUS_QUEUE => + _.withNewThreadPoolWithSynchronousQueueWithFairness(threadPool.fairness) + case unknown => + throw new IllegalArgumentException("Unknown queue type " + unknown) + }), + conf_?(keepAlive )(time => _.setKeepAliveTimeInMillis(time)), + conf_?(corePoolSize )(count => _.setCorePoolSize(count)), + conf_?(maxPoolSize )(count => _.setMaxPoolSize(count)), + conf_?(executorBounds)(bounds => _.setExecutorBounds(bounds)), + conf_?(flowHandler )(policy => _.setRejectionPolicy(policy))) + } + else + ThreadPoolConfigDispatcherBuilder(createDispatcher,ThreadPoolConfig()) + } + + //Create the dispatcher + properties.dispatcherType match { + case EXECUTOR_BASED_EVENT_DRIVEN => + configureThreadPool(poolConfig => + new ExecutorBasedEventDrivenDispatcher(properties.name, poolConfig)).build + case EXECUTOR_BASED_EVENT_DRIVEN_WORK_STEALING => + configureThreadPool(poolConfig => + new ExecutorBasedEventDrivenWorkStealingDispatcher( + properties.name, + Dispatchers.THROUGHPUT, + Dispatchers.THROUGHPUT_DEADLINE_TIME_MILLIS, + Dispatchers.MAILBOX_TYPE, + poolConfig)).build + case THREAD_BASED if actorRef.isEmpty => + throw new IllegalArgumentException("Need an ActorRef to create a thread based dispatcher.") + case THREAD_BASED if actorRef.isDefined => + Dispatchers.newThreadBasedDispatcher(actorRef.get) + case unknown => + throw new IllegalArgumentException("Unknown dispatcher type " + unknown) + } + } +} + +/** + * Factory bean for supervisor configuration. + * @author michaelkober + */ +class DispatcherFactoryBean extends AbstractFactoryBean[MessageDispatcher] { + @BeanProperty var properties: DispatcherProperties = _ + + /* + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + def getObjectType: Class[MessageDispatcher] = classOf[MessageDispatcher] + + /* + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + def createInstance: MessageDispatcher = { + import DispatcherFactoryBean._ + createNewInstance(properties) + } +} diff --git a/akka-spring/src/main/scala/akka/spring/DispatcherProperties.scala b/akka-spring/src/main/scala/akka/spring/DispatcherProperties.scala new file mode 100644 index 0000000000..8dd33602df --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/DispatcherProperties.scala @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder + +/** + * Data container for dispatcher configuration data. + * @author michaelkober + */ +class DispatcherProperties { + var ref: String = "" + var dispatcherType: String = "" + var name: String = "" + var threadPool: ThreadPoolProperties = _ + var aggregate = true + + /** + * Sets the properties to the given builder. + * @param builder bean definition builder + */ + def setAsProperties(builder: BeanDefinitionBuilder) { + builder.addPropertyValue("properties", this) + } + + override def toString : String = { + "DispatcherProperties[ref=" + ref + + ", dispatcher-type=" + dispatcherType + + ", name=" + name + + ", threadPool=" + threadPool + "]" + } +} + +/** + * Data container for thread pool configuration data. + * @author michaelkober + */ +class ThreadPoolProperties { + var queue = "" + var bound = -1 + var capacity = -1 + var fairness = false + var corePoolSize = -1 + var maxPoolSize = -1 + var keepAlive = -1L + var rejectionPolicy = "" + var mailboxCapacity = -1 + + override def toString : String = { + "ThreadPoolProperties[queue=" + queue + + ", bound=" + bound + + ", capacity=" + capacity + + ", fairness=" + fairness + + ", corePoolSize=" + corePoolSize + + ", maxPoolSize=" + maxPoolSize + + ", keepAlive=" + keepAlive + + ", policy=" + rejectionPolicy + + ", mailboxCapacity=" + mailboxCapacity + "]" + } +} diff --git a/akka-spring/src/main/scala/akka/spring/PropertyEntries.scala b/akka-spring/src/main/scala/akka/spring/PropertyEntries.scala new file mode 100644 index 0000000000..9f6493bbb3 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/PropertyEntries.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder + +import scala.collection.mutable._ + +/** + * Simple container for Properties + * @author Johan Rask + */ +class PropertyEntries { + var entryList: ListBuffer[PropertyEntry] = ListBuffer[PropertyEntry]() + + def add(entry: PropertyEntry) = { + entryList.append(entry) + } +} + +/** + * Represents a property element + * @author Johan Rask + */ +class PropertyEntry { + var name: String = _ + var value: String = null + var ref: String = null + + + override def toString(): String = { + format("name = %s,value = %s, ref = %s", name, value, ref) + } +} + diff --git a/akka-spring/src/main/scala/akka/spring/StringReflect.scala b/akka-spring/src/main/scala/akka/spring/StringReflect.scala new file mode 100644 index 0000000000..2b77f8caa6 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/StringReflect.scala @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package akka.spring + +object StringReflect { + + /** + * Implicit conversion from String to StringReflect. + */ + implicit def string2StringReflect(x: String) = new StringReflect(x) +} + +/** + * Reflection helper class. + * @author michaelkober + */ +class StringReflect(val self: String) { + if ((self eq null) || self == "") throw new IllegalArgumentException("Class name can't be null or empty string [" + self + "]") + def toClass[T <: AnyRef]: Class[T] = { + val clazz = Class.forName(self) + clazz.asInstanceOf[Class[T]] + } +} diff --git a/akka-spring/src/main/scala/akka/spring/SupervisionBeanDefinitionParser.scala b/akka-spring/src/main/scala/akka/spring/SupervisionBeanDefinitionParser.scala new file mode 100644 index 0000000000..d9672cb574 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/SupervisionBeanDefinitionParser.scala @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.{ParserContext, AbstractSingleBeanDefinitionParser} +import akka.config.Supervision._ +import AkkaSpringConfigurationTags._ + + +import org.w3c.dom.Element +import org.springframework.util.xml.DomUtils + + +/** + * Parser for custom namespace for Akka declarative supervisor configuration. + * @author michaelkober + */ +class SupervisionBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + parseSupervisor(element, builder) + } + + /** + * made accessible for testing + */ + private[akka] def parseSupervisor(element: Element, builder: BeanDefinitionBuilder) { + val strategyElement = mandatoryElement(element, STRATEGY_TAG) + val typedActorsElement = DomUtils.getChildElementByTagName(element, TYPED_ACTORS_TAG) + val untypedActorsElement = DomUtils.getChildElementByTagName(element, UNTYPED_ACTORS_TAG) + if ((typedActorsElement eq null) && (untypedActorsElement eq null)) { + throw new IllegalArgumentException("One of 'akka:typed-actors' or 'akka:untyped-actors' needed.") + } + parseRestartStrategy(strategyElement, builder) + if (typedActorsElement ne null) { + builder.addPropertyValue("typed", AkkaSpringConfigurationTags.TYPED_ACTOR_TAG) + parseTypedActorList(typedActorsElement, builder) + } else { + builder.addPropertyValue("typed", AkkaSpringConfigurationTags.UNTYPED_ACTOR_TAG) + parseUntypedActorList(untypedActorsElement, builder) + } + } + + private[akka] def parseRestartStrategy(element: Element, builder: BeanDefinitionBuilder) { + val failover = mandatory(element, FAILOVER) + val timeRange = mandatory(element, TIME_RANGE).toInt + val retries = mandatory(element, RETRIES).toInt + val trapExitsElement = mandatoryElement(element, TRAP_EXISTS_TAG) + val trapExceptions = parseTrapExits(trapExitsElement) + + val restartStrategy = failover match { + case "AllForOne" => new AllForOneStrategy(trapExceptions, retries, timeRange) + case "OneForOne" => new OneForOneStrategy(trapExceptions, retries, timeRange) + case _ => new OneForOneStrategy(trapExceptions, retries, timeRange) //Default to OneForOne + } + builder.addPropertyValue("restartStrategy", restartStrategy) + } + + private[akka] def parseTypedActorList(element: Element, builder: BeanDefinitionBuilder) { + val typedActors = DomUtils.getChildElementsByTagName(element, TYPED_ACTOR_TAG).toArray.toList.asInstanceOf[List[Element]] + val actorProperties = typedActors.map(parseActor(_)) + builder.addPropertyValue("supervised", actorProperties) + } + + private[akka] def parseUntypedActorList(element: Element, builder: BeanDefinitionBuilder) { + val untypedActors = DomUtils.getChildElementsByTagName(element, UNTYPED_ACTOR_TAG).toArray.toList.asInstanceOf[List[Element]] + val actorProperties = untypedActors.map(parseActor(_)) + builder.addPropertyValue("supervised", actorProperties) + } + + private def parseTrapExits(element: Element): Array[Class[_ <: Throwable]] = { + import StringReflect._ + val trapExits = DomUtils.getChildElementsByTagName(element, TRAP_EXIT_TAG).toArray.toList.asInstanceOf[List[Element]] + trapExits.map(DomUtils.getTextValue(_).toClass.asInstanceOf[Class[_ <: Throwable]]).toArray + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[SupervisionFactoryBean] +} diff --git a/akka-spring/src/main/scala/akka/spring/SupervisionFactoryBean.scala b/akka-spring/src/main/scala/akka/spring/SupervisionFactoryBean.scala new file mode 100644 index 0000000000..dd4e500908 --- /dev/null +++ b/akka-spring/src/main/scala/akka/spring/SupervisionFactoryBean.scala @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.springframework.beans.factory.config.AbstractFactoryBean +import akka.config.Supervision._ +import akka.actor.{Supervisor, SupervisorFactory, Actor, ActorRegistry} +import AkkaSpringConfigurationTags._ +import reflect.BeanProperty +import akka.config.{TypedActorConfigurator, RemoteAddress} + +/** + * Factory bean for supervisor configuration. + * @author michaelkober + */ +class SupervisionFactoryBean extends AbstractFactoryBean[AnyRef] { + @BeanProperty var restartStrategy: FaultHandlingStrategy = _ + @BeanProperty var supervised: List[ActorProperties] = _ + @BeanProperty var typed: String = "" + + /* + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + def getObjectType: Class[AnyRef] = classOf[AnyRef] + + /* + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + def createInstance: AnyRef = typed match { + case AkkaSpringConfigurationTags.TYPED_ACTOR_TAG => createInstanceForTypedActors + case AkkaSpringConfigurationTags.UNTYPED_ACTOR_TAG => createInstanceForUntypedActors + } + + private def createInstanceForTypedActors() : TypedActorConfigurator = { + val configurator = new TypedActorConfigurator() + configurator.configure( + restartStrategy, + supervised.map(createComponent(_)).toArray + ).supervise + + } + + private def createInstanceForUntypedActors() : Supervisor = { + val factory = new SupervisorFactory( + new SupervisorConfig( + restartStrategy, + supervised.map(createSupervise(_)))) + factory.newInstance + } + + /** + * Create configuration for TypedActor + */ + private[akka] def createComponent(props: ActorProperties): SuperviseTypedActor = { + import StringReflect._ + val lifeCycle = if (!props.lifecycle.isEmpty && props.lifecycle.equalsIgnoreCase(VAL_LIFECYCYLE_TEMPORARY)) Temporary else Permanent + val isRemote = (props.host ne null) && (!props.host.isEmpty) + val withInterface = (props.interface ne null) && (!props.interface.isEmpty) + if (isRemote) { + //val remote = new RemoteAddress(props.host, props.port) + val remote = new RemoteAddress(props.host, props.port.toInt) + if (withInterface) { + new SuperviseTypedActor(props.interface.toClass, props.target.toClass, lifeCycle, props.timeout, remote) + } else { + new SuperviseTypedActor(props.target.toClass, lifeCycle, props.timeout, remote) + } + } else { + if (withInterface) { + new SuperviseTypedActor(props.interface.toClass, props.target.toClass, lifeCycle, props.timeout) + } else { + new SuperviseTypedActor(props.target.toClass, lifeCycle, props.timeout) + } + } + } + + /** + * Create configuration for UntypedActor + */ + private[akka] def createSupervise(props: ActorProperties): Server = { + import StringReflect._ + val lifeCycle = if (!props.lifecycle.isEmpty && props.lifecycle.equalsIgnoreCase(VAL_LIFECYCYLE_TEMPORARY)) Temporary else Permanent + val isRemote = (props.host ne null) && (!props.host.isEmpty) + val actorRef = Actor.actorOf(props.target.toClass) + + if (props.timeout > 0) + actorRef.setTimeout(props.timeout) + + Supervise(actorRef, lifeCycle, isRemote) + } +} diff --git a/akka-spring/src/test/java/akka/spring/Pojo.java b/akka-spring/src/test/java/akka/spring/Pojo.java new file mode 100644 index 0000000000..618adc8cc3 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/Pojo.java @@ -0,0 +1,51 @@ +package akka.spring; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import javax.annotation.PreDestroy; +import javax.annotation.PostConstruct; + +import akka.actor.*; + +public class Pojo extends TypedActor implements PojoInf, ApplicationContextAware { + + private String stringFromVal; + private String stringFromRef; + + private boolean gotApplicationContext = false; + private boolean preStartInvoked = false; + + public boolean gotApplicationContext() { + return gotApplicationContext; + } + + public void setApplicationContext(ApplicationContext context) { + gotApplicationContext = true; + } + + public String getStringFromVal() { + return stringFromVal; + } + + public void setStringFromVal(String s) { + stringFromVal = s; + } + + public String getStringFromRef() { + return stringFromRef; + } + + public void setStringFromRef(String s) { + stringFromRef = s; + } + + @Override + public void preStart() { + preStartInvoked = true; + } + + public boolean isPreStartInvoked() { + return preStartInvoked; + } +} diff --git a/akka-spring/src/test/java/akka/spring/PojoInf.java b/akka-spring/src/test/java/akka/spring/PojoInf.java new file mode 100644 index 0000000000..f73ce35814 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/PojoInf.java @@ -0,0 +1,13 @@ +package akka.spring; + +import javax.annotation.PreDestroy; +import javax.annotation.PostConstruct; + +public interface PojoInf { + + public String getStringFromVal(); + public String getStringFromRef(); + public boolean gotApplicationContext(); + public boolean isPreStartInvoked(); + +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedActorOne.java b/akka-spring/src/test/java/akka/spring/RemoteTypedActorOne.java new file mode 100644 index 0000000000..4bf1da9287 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedActorOne.java @@ -0,0 +1,6 @@ +package akka.spring; + +public interface RemoteTypedActorOne { + public String requestReply(String s) throws Exception; + public void oneWay() throws Exception; +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedActorOneImpl.java b/akka-spring/src/test/java/akka/spring/RemoteTypedActorOneImpl.java new file mode 100644 index 0000000000..c7ad0f3de6 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedActorOneImpl.java @@ -0,0 +1,29 @@ +package akka.spring; + +import akka.actor.*; + +import java.util.concurrent.CountDownLatch; + +public class RemoteTypedActorOneImpl extends TypedActor implements RemoteTypedActorOne { + + public static CountDownLatch latch = new CountDownLatch(1); + + public String requestReply(String s) throws Exception { + if (s.equals("ping")) { + RemoteTypedActorLog.messageLog().put("ping"); + return "pong"; + } else if (s.equals("die")) { + throw new RuntimeException("Expected exception; to test fault-tolerance"); + } else return null; + } + + public void oneWay() throws Exception { + RemoteTypedActorLog.oneWayLog().put("oneway"); + } + + @Override + public void preRestart(Throwable e) { + try { RemoteTypedActorLog.messageLog().put(e.getMessage()); } catch(Exception ex) {} + latch.countDown(); + } +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwo.java b/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwo.java new file mode 100644 index 0000000000..1ef0420c73 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwo.java @@ -0,0 +1,6 @@ +package akka.spring; + +public interface RemoteTypedActorTwo { + public String requestReply(String s) throws Exception; + public void oneWay() throws Exception; +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwoImpl.java b/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwoImpl.java new file mode 100644 index 0000000000..1479db0d8c --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedActorTwoImpl.java @@ -0,0 +1,29 @@ +package akka.spring; + +import akka.actor.*; + +import java.util.concurrent.CountDownLatch; + +public class RemoteTypedActorTwoImpl extends TypedActor implements RemoteTypedActorTwo { + + public static CountDownLatch latch = new CountDownLatch(1); + + public String requestReply(String s) throws Exception { + if (s.equals("ping")) { + RemoteTypedActorLog.messageLog().put("ping"); + return "pong"; + } else if (s.equals("die")) { + throw new RuntimeException("Expected exception; to test fault-tolerance"); + } else return null; + } + + public void oneWay() throws Exception { + RemoteTypedActorLog.oneWayLog().put("oneway"); + } + + @Override + public void preRestart(Throwable e) { + try { RemoteTypedActorLog.messageLog().put(e.getMessage()); } catch(Exception ex) {} + latch.countDown(); + } +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActor.java b/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActor.java new file mode 100644 index 0000000000..737a7420dd --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActor.java @@ -0,0 +1,8 @@ +package akka.spring; + +public interface RemoteTypedSessionActor { + + public void login(String user); + public String getUser(); + public void doSomethingFunny() throws Exception; +} diff --git a/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActorImpl.java b/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActorImpl.java new file mode 100644 index 0000000000..6633fc5fae --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/RemoteTypedSessionActorImpl.java @@ -0,0 +1,49 @@ +package akka.spring; + +import akka.actor.*; + +import java.util.Set; +import java.util.HashSet; + +import java.util.concurrent.CountDownLatch; + +public class RemoteTypedSessionActorImpl extends TypedActor implements RemoteTypedSessionActor { + + + private static Set instantiatedSessionActors = new HashSet(); + + public static Set getInstances() { + return instantiatedSessionActors; + } + + @Override + public void preStart() { + instantiatedSessionActors.add(this); + } + + @Override + public void postStop() { + instantiatedSessionActors.remove(this); + } + + + private String user="anonymous"; + + @Override + public void login(String user) { + this.user = user; + } + + @Override + public String getUser() + { + return this.user; + } + + @Override + public void doSomethingFunny() throws Exception + { + throw new Exception("Bad boy"); + } + +} diff --git a/akka-spring/src/test/java/akka/spring/SampleBean.java b/akka-spring/src/test/java/akka/spring/SampleBean.java new file mode 100644 index 0000000000..e23672d060 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/SampleBean.java @@ -0,0 +1,25 @@ +package akka.spring; + +import akka.actor.*; + +public class SampleBean extends TypedActor implements SampleBeanIntf { + + private boolean down; + + public SampleBean() { + down = false; + } + + public boolean down() { + return down; + } + + public String foo(String s) { + return "hello " + s; + } + + @Override + public void postStop() { + down = true; + } + } diff --git a/akka-spring/src/test/java/akka/spring/SampleBeanIntf.java b/akka-spring/src/test/java/akka/spring/SampleBeanIntf.java new file mode 100644 index 0000000000..365275f193 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/SampleBeanIntf.java @@ -0,0 +1,6 @@ +package akka.spring; + +public interface SampleBeanIntf { + public boolean down(); + public String foo(String s); + } diff --git a/akka-spring/src/test/java/akka/spring/SampleRoute.java b/akka-spring/src/test/java/akka/spring/SampleRoute.java new file mode 100644 index 0000000000..fb3565661d --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/SampleRoute.java @@ -0,0 +1,11 @@ +package akka.spring; + +import org.apache.camel.builder.RouteBuilder; + +public class SampleRoute extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("direct:test").to("typed-actor:sample?method=foo"); + } +} diff --git a/akka-spring/src/test/java/akka/spring/foo/Bar.java b/akka-spring/src/test/java/akka/spring/foo/Bar.java new file mode 100644 index 0000000000..36276ff108 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/Bar.java @@ -0,0 +1,17 @@ +package akka.spring.foo; + +import java.io.IOException; +import akka.actor.*; + +public class Bar extends TypedActor implements IBar { + + @Override + public String getBar() { + return "bar"; + } + + public void throwsIOException() throws IOException { + throw new IOException("some IO went wrong"); + } + +} diff --git a/akka-spring/src/test/java/akka/spring/foo/Foo.java b/akka-spring/src/test/java/akka/spring/foo/Foo.java new file mode 100644 index 0000000000..189f146e51 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/Foo.java @@ -0,0 +1,11 @@ +package akka.spring.foo; + +import akka.actor.*; + +public class Foo extends TypedActor implements IFoo{ + + public String foo() { + return "foo"; + } + +} diff --git a/akka-spring/src/test/java/akka/spring/foo/IBar.java b/akka-spring/src/test/java/akka/spring/foo/IBar.java new file mode 100644 index 0000000000..803b4ab50a --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/IBar.java @@ -0,0 +1,7 @@ +package akka.spring.foo; + +public interface IBar { + + String getBar(); + +} diff --git a/akka-spring/src/test/java/akka/spring/foo/IFoo.java b/akka-spring/src/test/java/akka/spring/foo/IFoo.java new file mode 100644 index 0000000000..e47809f3af --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/IFoo.java @@ -0,0 +1,12 @@ +package akka.spring.foo; + +/** + * Created by IntelliJ IDEA. + * User: michaelkober + * Date: Aug 11, 2010 + * Time: 12:49:58 PM + * To change this template use File | Settings | File Templates. + */ +public interface IFoo { + public String foo(); +} diff --git a/akka-spring/src/test/java/akka/spring/foo/IMyPojo.java b/akka-spring/src/test/java/akka/spring/foo/IMyPojo.java new file mode 100644 index 0000000000..825d797cf2 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/IMyPojo.java @@ -0,0 +1,19 @@ +package akka.spring.foo; + +/** + * Created by IntelliJ IDEA. + * User: michaelkober + * Date: Aug 11, 2010 + * Time: 12:01:00 PM + * To change this template use File | Settings | File Templates. + */ +public interface IMyPojo { + public void oneWay(String message); + + public String getFoo(); + + public String longRunning(); + + + +} diff --git a/akka-spring/src/test/java/akka/spring/foo/MyPojo.java b/akka-spring/src/test/java/akka/spring/foo/MyPojo.java new file mode 100644 index 0000000000..54019f53d2 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/MyPojo.java @@ -0,0 +1,34 @@ +package akka.spring.foo; + +import akka.actor.TypedActor; + +import java.util.concurrent.CountDownLatch; + +public class MyPojo extends TypedActor implements IMyPojo { + + public static CountDownLatch latch = new CountDownLatch(1); + public static String lastOneWayMessage = null; + private String foo = "foo"; + + + public MyPojo() { + } + + public String getFoo() { + return foo; + } + + public void oneWay(String message) { + lastOneWayMessage = message; + latch.countDown(); + } + + public String longRunning() { + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + } + return "this took long"; + } + +} diff --git a/akka-spring/src/test/java/akka/spring/foo/PingActor.java b/akka-spring/src/test/java/akka/spring/foo/PingActor.java new file mode 100644 index 0000000000..6c4d185860 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/PingActor.java @@ -0,0 +1,74 @@ +package akka.spring.foo; + +import static akka.actor.Actors.*; +import akka.actor.ActorRef; +import akka.actor.UntypedActor; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.util.concurrent.CountDownLatch; + + +/** + * test class + */ +public class PingActor extends UntypedActor implements ApplicationContextAware { + + private String stringFromVal; + private String stringFromRef; + public static String lastMessage = null; + public static CountDownLatch latch = new CountDownLatch(1); + + + private boolean gotApplicationContext = false; + + + public void setApplicationContext(ApplicationContext context) { + gotApplicationContext = true; + } + + public boolean gotApplicationContext() { + return gotApplicationContext; + } + + public String getStringFromVal() { + return stringFromVal; + } + + public void setStringFromVal(String s) { + stringFromVal = s; + } + + public String getStringFromRef() { + return stringFromRef; + } + + public void setStringFromRef(String s) { + stringFromRef = s; + } + + private String longRunning() { + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + } + return "this took long"; + } + + public void onReceive(Object message) throws Exception { + if (message instanceof String) { + lastMessage = (String) message; + if (message.equals("longRunning")) { + ActorRef pongActor = actorOf(PongActor.class).start(); + pongActor.sendRequestReply("longRunning", getContext()); + } + latch.countDown(); + } else { + throw new IllegalArgumentException("Unknown message: " + message); + } + } + + +} + diff --git a/akka-spring/src/test/java/akka/spring/foo/PongActor.java b/akka-spring/src/test/java/akka/spring/foo/PongActor.java new file mode 100644 index 0000000000..d4f19078a6 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/PongActor.java @@ -0,0 +1,18 @@ +package akka.spring.foo; + +import akka.actor.UntypedActor; + +/** + * test class + */ +public class PongActor extends UntypedActor { + + public void onReceive(Object message) throws Exception { + if (message instanceof String) { + System.out.println("Pongeceived String message: " + message); + getContext().replyUnsafe(message + " from " + getContext().getUuid()); + } else { + throw new IllegalArgumentException("Unknown message: " + message); + } + } +} diff --git a/akka-spring/src/test/java/akka/spring/foo/StatefulPojo.java b/akka-spring/src/test/java/akka/spring/foo/StatefulPojo.java new file mode 100644 index 0000000000..8f291d2a36 --- /dev/null +++ b/akka-spring/src/test/java/akka/spring/foo/StatefulPojo.java @@ -0,0 +1,58 @@ +package akka.spring.foo; + +/* +import akka.stm.TransactionalMap; +import akka.stm.TransactionalVector; +import akka.stm.Ref; +import akka.actor.*; +import akka.stm.Atomic; + +public class StatefulPojo extends TypedActor { + private TransactionalMap mapState; + private TransactionalVector vectorState; + private Ref refState; + private boolean isInitialized = false; + + @Override + public void preStart() { + if(!isInitialized) { + isInitialized = new Atomic() { + public Boolean atomically() { + mapState = new TransactionalMap(); + vectorState = new TransactionalVector(); + refState = new Ref(); + return true; + } + }.execute(); + } + } + + public String getMapState(String key) { + return (String)mapState.get(key).get(); + } + + public String getVectorState() { + return (String)vectorState.last(); + } + + public String getRefState() { + return (String)refState.get().get(); + } + + public void setMapState(String key, String msg) { + mapState.put(key, msg); + } + + public void setVectorState(String msg) { + vectorState.add(msg); + } + + public void setRefState(String msg) { + refState.swap(msg); + } + + public boolean isInitialized() { + return isInitialized; + } +} +*/ diff --git a/akka-spring/src/test/resources/akka-test.conf b/akka-spring/src/test/resources/akka-test.conf new file mode 100644 index 0000000000..bb9c0e347d --- /dev/null +++ b/akka-spring/src/test/resources/akka-test.conf @@ -0,0 +1,158 @@ +#################### +# Akka Config File # +#################### + +# This file has all the default settings, so all these could be removed with no visible effect. +# Modify as needed. + +akka { + version = "2.0-SNAPSHOT" # Akka version, checked against the runtime version of Akka. + + enabled-modules = ["remote"] # Comma separated list of the enabled modules. Options: ["remote", "camel", "http"] + + time-unit = "seconds" # Time unit for all timeout properties throughout the config + + event-handlers = ["akka.event.EventHandler$DefaultListener"] # event handlers to register at boot time (EventHandler$DefaultListener logs to STDOUT) + + # These boot classes are loaded (and created) automatically when the Akka Microkernel boots up + # Can be used to bootstrap your application(s) + # Should be the FQN (Fully Qualified Name) of the boot class which needs to have a default constructor + # boot = ["sample.camel.Boot", + # "sample.rest.java.Boot", + # "sample.rest.scala.Boot", + # "sample.security.Boot"] + boot = [] + + actor { + timeout = 2000 # Default timeout for Future based invocations + # - Actor: !! && !!! + # - UntypedActor: sendRequestReply && sendRequestReplyFuture + # - TypedActor: methods with non-void return type + serialize-messages = off # Does a deep clone of (non-primitive) messages to ensure immutability + throughput = 5 # Default throughput for all ExecutorBasedEventDrivenDispatcher, set to 1 for complete fairness + throughput-deadline-time = -1 # Default throughput deadline for all ExecutorBasedEventDrivenDispatcher, set to 0 or negative for no deadline + dispatcher-shutdown-timeout = 1 # Using the akka.time-unit, how long dispatchers by default will wait for new actors until they shut down + + default-dispatcher { + type = "GlobalDispatcher" # Must be one of the following, all "Global*" are non-configurable + # - ExecutorBasedEventDriven + # - ExecutorBasedEventDrivenWorkStealing + # - GlobalExecutorBasedEventDriven + keep-alive-time = 60 # Keep alive time for threads + core-pool-size-factor = 1.0 # No of core threads ... ceil(available processors * factor) + max-pool-size-factor = 4.0 # Max no of threads ... ceil(available processors * factor) + executor-bounds = -1 # Makes the Executor bounded, -1 is unbounded + allow-core-timeout = on # Allow core threads to time out + rejection-policy = "caller-runs" # abort, caller-runs, discard-oldest, discard + throughput = 5 # Throughput for ExecutorBasedEventDrivenDispatcher, set to 1 for complete fairness + throughput-deadline-time = -1 # Throughput deadline for ExecutorBasedEventDrivenDispatcher, set to 0 or negative for no deadline + mailbox-capacity = -1 # If negative (or zero) then an unbounded mailbox is used (default) + # If positive then a bounded mailbox is used and the capacity is set using the property + # NOTE: setting a mailbox to 'blocking' can be a bit dangerous, + # could lead to deadlock, use with care + # + # The following are only used for ExecutorBasedEventDriven + # and only if mailbox-capacity > 0 + mailbox-push-timeout-time = 10 # Specifies the timeout to add a new message to a mailbox that is full - negative number means infinite timeout + # (in unit defined by the time-unit property) + } + } + + stm { + fair = on # Should global transactions be fair or non-fair (non fair yield better performance) + max-retries = 1000 + timeout = 5 # Default timeout for blocking transactions and transaction set (in unit defined by + # the time-unit property) + write-skew = true + blocking-allowed = false + interruptible = false + speculative = true + quick-release = true + propagation = "requires" + trace-level = "none" + hooks = true + jta-aware = off # Option 'on' means that if there JTA Transaction Manager available then the STM will + # begin (or join), commit or rollback the JTA transaction. Default is 'off'. + } + + jta { + provider = "from-jndi" # Options: - "from-jndi" (means that Akka will try to detect a TransactionManager in the JNDI) + # - "atomikos" (means that Akka will use the Atomikos based JTA impl in 'akka-jta', + # e.g. you need the akka-jta JARs on classpath). + timeout = 60 + } + + http { + hostname = "localhost" + port = 9998 + + #If you are using akka.http.AkkaRestServlet + filters = ["se.scalablesolutions.akka.security.AkkaSecurityFilterFactory"] # List with all jersey filters to use + # resource-packages = ["sample.rest.scala", + # "sample.rest.java", + # "sample.security"] # List with all resource packages for your Jersey services + resource-packages = [] + + # The authentication service to use. Need to be overridden (sample now) + # authenticator = "sample.security.BasicAuthenticationService" + authenticator = "N/A" + + # Uncomment if you are using the KerberosAuthenticationActor + # kerberos { + # servicePrincipal = "HTTP/localhost@EXAMPLE.COM" + # keyTabLocation = "URL to keytab" + # kerberosDebug = "true" + # realm = "EXAMPLE.COM" + # } + kerberos { + servicePrincipal = "N/A" + keyTabLocation = "N/A" + kerberosDebug = "N/A" + realm = "" + } + + #If you are using akka.http.AkkaMistServlet + mist-dispatcher { + #type = "GlobalExecutorBasedEventDriven" # Uncomment if you want to use a different dispatcher than the default one for Comet + } + connection-close = true # toggles the addition of the "Connection" response header with a "close" value + root-actor-id = "_httproot" # the id of the actor to use as the root endpoint + root-actor-builtin = true # toggles the use of the built-in root endpoint base class + timeout = 1000 # the default timeout for all async requests (in ms) + expired-header-name = "Async-Timeout" # the name of the response header to use when an async request expires + expired-header-value = "expired" # the value of the response header to use when an async request expires + } + + remote { + + # secure-cookie = "050E0A0D0D06010A00000900040D060F0C09060B" # generate your own with '$AKKA_HOME/scripts/generate_secure_cookie.sh' or using 'Crypt.generateSecureCookie' + secure-cookie = "" + + compression-scheme = "zlib" # Options: "zlib" (lzf to come), leave out for no compression + zlib-compression-level = 6 # Options: 0-9 (1 being fastest and 9 being the most compressed), default is 6 + + layer = "akka.remote.netty.NettyRemoteSupport" + + server { + hostname = "localhost" # The hostname or IP that clients should connect to + port = 9995 # The port clients should connect to. Default is 2552 (AKKA) + message-frame-size = 1048576 # Increase this if you want to be able to send messages with large payloads + connection-timeout = 1 + require-cookie = off # Should the remote server require that it peers share the same secure-cookie (defined in the 'remote' section)? + untrusted-mode = off # Enable untrusted mode for full security of server managed actors, allows untrusted clients to connect. + backlog = 4096 # Sets the size of the connection backlog + } + + client { + reconnect-delay = 5 + read-timeout = 10 + message-frame-size = 1048576 + reap-futures-delay = 5 + reconnection-time-window = 600 # Maximum time window that a client should try to reconnect for + } + } + + storage { + max-retries = 10 + } +} diff --git a/akka-spring/src/test/resources/appContext.xml b/akka-spring/src/test/resources/appContext.xml new file mode 100644 index 0000000000..ee2f011b6b --- /dev/null +++ b/akka-spring/src/test/resources/appContext.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/appContextCamelServiceCustom.xml b/akka-spring/src/test/resources/appContextCamelServiceCustom.xml new file mode 100644 index 0000000000..a6e4d63444 --- /dev/null +++ b/akka-spring/src/test/resources/appContextCamelServiceCustom.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/appContextCamelServiceDefault.xml b/akka-spring/src/test/resources/appContextCamelServiceDefault.xml new file mode 100644 index 0000000000..12c12b39f5 --- /dev/null +++ b/akka-spring/src/test/resources/appContextCamelServiceDefault.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/akka-spring/src/test/resources/dispatcher-config.xml b/akka-spring/src/test/resources/dispatcher-config.xml new file mode 100644 index 0000000000..591070ac9f --- /dev/null +++ b/akka-spring/src/test/resources/dispatcher-config.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.io.IOException + java.lang.NullPointerException + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/failing-appContext.xml b/akka-spring/src/test/resources/failing-appContext.xml new file mode 100644 index 0000000000..a8f1feb24f --- /dev/null +++ b/akka-spring/src/test/resources/failing-appContext.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/property-config.xml b/akka-spring/src/test/resources/property-config.xml new file mode 100644 index 0000000000..f199df7074 --- /dev/null +++ b/akka-spring/src/test/resources/property-config.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/server-managed-config.xml b/akka-spring/src/test/resources/server-managed-config.xml new file mode 100644 index 0000000000..f6ccafba93 --- /dev/null +++ b/akka-spring/src/test/resources/server-managed-config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/supervisor-config.xml b/akka-spring/src/test/resources/supervisor-config.xml new file mode 100644 index 0000000000..9fbd85534d --- /dev/null +++ b/akka-spring/src/test/resources/supervisor-config.xml @@ -0,0 +1,120 @@ + + + + + + + java.io.IOException + java.lang.NullPointerException + + + + + + + + + + + + + + java.io.IOException + java.lang.NullPointerException + + + + + + + + + + + + + + + + + java.lang.Exception + + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/typed-actor-config.xml b/akka-spring/src/test/resources/typed-actor-config.xml new file mode 100644 index 0000000000..1fd1d5f561 --- /dev/null +++ b/akka-spring/src/test/resources/typed-actor-config.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + java.io.IOException + java.lang.NullPointerException + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/untyped-actor-config.xml b/akka-spring/src/test/resources/untyped-actor-config.xml new file mode 100644 index 0000000000..2d96f76bb7 --- /dev/null +++ b/akka-spring/src/test/resources/untyped-actor-config.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/scala/ActorFactoryBeanTest.scala b/akka-spring/src/test/scala/ActorFactoryBeanTest.scala new file mode 100644 index 0000000000..10edb0916a --- /dev/null +++ b/akka-spring/src/test/scala/ActorFactoryBeanTest.scala @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import akka.actor.{Actor, ActorRef, ActorInitializationException} +import akka.spring.foo.PingActor + +import org.junit.runner.RunWith +import org.springframework.context.support.ClassPathXmlApplicationContext +import org.scalatest.junit.JUnitRunner +import org.scalatest.{BeforeAndAfterAll, Spec} +import org.scalatest.matchers.ShouldMatchers + +/** + * Test for TypedActorFactoryBean + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class ActorFactoryBeanTest extends Spec with ShouldMatchers with BeforeAndAfterAll { + override protected def afterAll = Actor.registry.shutdownAll + + describe("A ActorFactoryBean") { + val bean = new ActorFactoryBean + it("should have java getters and setters for all properties") { + bean.setImplementation("java.lang.String") + assert(bean.getImplementation == "java.lang.String") + bean.setTimeoutStr("1000") + assert(bean.getTimeoutStr === "1000") + } + + it("should create a remote typed actor when a host is set") { + bean.setHost("some.host.com"); + assert(bean.isRemote) + } + + it("should create a typed actor with dispatcher if dispatcher is set") { + val props = new DispatcherProperties() + props.dispatcherType = "executor-based-event-driven" + bean.setDispatcher(props); + assert(bean.hasDispatcher) + } + + it("should return the object type") { + bean.setImplementation("java.lang.String") + assert(bean.getObjectType == classOf[String]) + } + + it("should create a proxy of type PojoInf") { + val bean = new ActorFactoryBean() + bean.setInterface("akka.spring.PojoInf") + bean.setImplementation("akka.spring.Pojo") + bean.timeoutStr = "1000" + bean.typed = AkkaSpringConfigurationTags.TYPED_ACTOR_TAG + val entries = new PropertyEntries() + val entry = new PropertyEntry() + entry.name = "stringFromVal" + entry.value = "tests rock" + entries.add(entry) + bean.setProperty(entries) + assert(classOf[PojoInf].isAssignableFrom(bean.getObjectType)) + + // Check that we have injected the dependency correctly + val target = bean.createInstance.asInstanceOf[PojoInf] + assert(target.getStringFromVal === entry.value) + } + + it("should create an application context and verify dependency injection for typed") { + var ctx = new ClassPathXmlApplicationContext("appContext.xml"); + val ta = ctx.getBean("typedActor").asInstanceOf[PojoInf]; + assert(ta.isPreStartInvoked) + assert(ta.getStringFromVal === "akka rocks") + assert(ta.getStringFromRef === "spring rocks") + assert(ta.gotApplicationContext) + ctx.close + } + + it("should create an application context and verify dependency injection for untyped actors") { + var ctx = new ClassPathXmlApplicationContext("appContext.xml") + val uta = ctx.getBean("untypedActor").asInstanceOf[ActorRef] + val ping = uta.actor.asInstanceOf[PingActor] + assert(ping.getStringFromVal === "akka rocks") + assert(ping.getStringFromRef === "spring rocks") + assert(ping.gotApplicationContext) + ctx.close + } + + it("should stop the created untyped actor when scope is singleton and the context is closed") { + var ctx = new ClassPathXmlApplicationContext("appContext.xml"); + val target = ctx.getBean("untypedActor").asInstanceOf[ActorRef] + target.start + assert(target.isRunning) + ctx.close + assert(!target.isRunning) + } + + it("should stop the created typed actor when scope is singleton and the context is closed") { + var ctx = new ClassPathXmlApplicationContext("appContext.xml"); + val target = ctx.getBean("bean-singleton").asInstanceOf[SampleBeanIntf] + assert(!target.down) + ctx.close + evaluating { target.down } should produce [ActorInitializationException] + } + + it("should not stop the created typed actor when scope is prototype and the context is closed") { + var ctx = new ClassPathXmlApplicationContext("appContext.xml"); + val target = ctx.getBean("bean-prototype").asInstanceOf[SampleBeanIntf] + assert(!target.down) + ctx.close + assert(!target.down) + } + } +} diff --git a/akka-spring/src/test/scala/CamelServiceSpringFeatureTest.scala b/akka-spring/src/test/scala/CamelServiceSpringFeatureTest.scala new file mode 100644 index 0000000000..d58f65fe4f --- /dev/null +++ b/akka-spring/src/test/scala/CamelServiceSpringFeatureTest.scala @@ -0,0 +1,42 @@ +package akka.spring + +import org.apache.camel.impl.{SimpleRegistry, DefaultCamelContext} +import org.apache.camel.spring.SpringCamelContext +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FeatureSpec} +import org.springframework.context.support.ClassPathXmlApplicationContext + +import akka.camel.CamelContextManager +import akka.actor.{TypedActor, Actor} + +class CamelServiceSpringFeatureTest extends FeatureSpec with BeforeAndAfterEach with BeforeAndAfterAll { + override protected def beforeAll = { + Actor.registry.shutdownAll + } + + override protected def afterEach = { + Actor.registry.shutdownAll + } + + feature("start CamelService from Spring application context") { + import CamelContextManager._ + scenario("with a custom CamelContext and access a registered typed actor") { + val appctx = new ClassPathXmlApplicationContext("/appContextCamelServiceCustom.xml") + assert(mandatoryContext.isInstanceOf[SpringCamelContext]) + assert("hello sample" === mandatoryTemplate.requestBody("direct:test", "sample")) + appctx.close + } + + scenario("with a default CamelContext and access a registered typed actor") { + val appctx = new ClassPathXmlApplicationContext("/appContextCamelServiceDefault.xml") + // create a custom registry + val registry = new SimpleRegistry + registry.put("custom", TypedActor.newInstance(classOf[SampleBeanIntf], classOf[SampleBean])) + // set custom registry in DefaultCamelContext + assert(mandatoryContext.isInstanceOf[DefaultCamelContext]) + mandatoryContext.asInstanceOf[DefaultCamelContext].setRegistry(registry) + // access registered typed actor + assert("hello sample" === mandatoryTemplate.requestBody("typed-actor:custom?method=foo", "sample")) + appctx.close + } + } +} diff --git a/akka-spring/src/test/scala/ConfiggyPropertyPlaceholderConfigurerSpec.scala b/akka-spring/src/test/scala/ConfiggyPropertyPlaceholderConfigurerSpec.scala new file mode 100644 index 0000000000..bc862dc7a2 --- /dev/null +++ b/akka-spring/src/test/scala/ConfiggyPropertyPlaceholderConfigurerSpec.scala @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + + +import foo.{IMyPojo, MyPojo, PingActor} +import akka.dispatch._ +import org.scalatest.FeatureSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader +import org.springframework.context.ApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext +import org.springframework.core.io.{ClassPathResource, Resource} +import java.util.concurrent._ +import akka.actor.{UntypedActor, Actor, ActorRef} + + + + +/** + * Tests for spring configuration of typed actors. + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class ConfiggyPropertyPlaceholderConfigurerSpec extends FeatureSpec with ShouldMatchers { + val EVENT_DRIVEN_PREFIX = "akka:event-driven:dispatcher:" + + feature("The ConfiggyPropertyPlaceholderConfigurator") { + + scenario("should provide the akkka config for spring") { + val context = new ClassPathXmlApplicationContext("/property-config.xml") + val actor1 = context.getBean("actor-1").asInstanceOf[ActorRef] + assert(actor1.homeAddress.get.getHostName === "localhost") + assert(actor1.homeAddress.get.getPort === 9995) + assert(actor1.timeout === 2000) + } + } +} diff --git a/akka-spring/src/test/scala/DispatcherBeanDefinitionParserTest.scala b/akka-spring/src/test/scala/DispatcherBeanDefinitionParserTest.scala new file mode 100644 index 0000000000..1c01245aac --- /dev/null +++ b/akka-spring/src/test/scala/DispatcherBeanDefinitionParserTest.scala @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import ScalaDom._ + +/** + * Test for DispatcherBeanDefinitionParser + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class DispatcherBeanDefinitionParserTest extends Spec with ShouldMatchers { + describe("A DispatcherBeanDefinitionParser") { + val parser = new DispatcherBeanDefinitionParser() + + it("should be able to parse the dispatcher configuration") { + // executor-based-event-driven + val xml = + var props = parser.parseDispatcher(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.dispatcherType === "executor-based-event-driven") + assert(props.name === "myDispatcher") + + // executor-based-event-driven-work-stealing + val xml2 = + props = parser.parseDispatcher(dom(xml2).getDocumentElement); + assert(props.dispatcherType === "executor-based-event-driven-work-stealing") + } + + it("should be able to parse the thread pool configuration") { + val xml = + val props = parser.parseThreadPool(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.queue == "bounded-array-blocking-queue") + assert(props.capacity == 100) + assert(props.fairness) + assert(props.corePoolSize == 6) + assert(props.maxPoolSize == 40) + assert(props.keepAlive == 2000L) + assert(props.rejectionPolicy == "caller-runs-policy") + } + + it("should be able to parse the dispatcher with a thread pool configuration") { + val xml = + + + val props = parser.parseDispatcher(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.dispatcherType == "executor-based-event-driven") + assert(props.name == "myDispatcher") + assert(props.threadPool.corePoolSize == 2) + assert(props.threadPool.maxPoolSize == 10) + assert(props.threadPool.keepAlive == 1000) + assert(props.threadPool.queue == "linked-blocking-queue") + } + + it("should throw IllegalArgumentException on not existing reference") { + val xml = + evaluating {parser.parseDispatcher(dom(xml).getDocumentElement)} should produce[IllegalArgumentException] + } + + it("should throw IllegalArgumentException on missing mandatory attributes") { + val xml = + evaluating {parser.parseDispatcher(dom(xml).getDocumentElement)} should produce[IllegalArgumentException] + } + + it("should throw IllegalArgumentException when configuring a thread based dispatcher without TypedActor or UntypedActor") { + val xml = + evaluating {parser.parseDispatcher(dom(xml).getDocumentElement)} should produce[IllegalArgumentException] + } + } +} + + diff --git a/akka-spring/src/test/scala/DispatcherFactoryBeanTest.scala b/akka-spring/src/test/scala/DispatcherFactoryBeanTest.scala new file mode 100644 index 0000000000..486ec8820c --- /dev/null +++ b/akka-spring/src/test/scala/DispatcherFactoryBeanTest.scala @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import akka.config.Supervision._ +import akka.dispatch.MessageDispatcher + +@RunWith(classOf[JUnitRunner]) +class DispatcherFactoryBeanTest extends Spec with ShouldMatchers { + + describe("A DispatcherFactoryBean") { + val bean = new DispatcherFactoryBean + it("should have java getters and setters for the dispatcher properties") { + val props = new DispatcherProperties() + bean.setProperties(props) + assert(bean.getProperties == props) + } + + it("should return the object type MessageDispatcher") { + assert(bean.getObjectType == classOf[MessageDispatcher]) + } + } +} diff --git a/akka-spring/src/test/scala/DispatcherSpringFeatureTest.scala b/akka-spring/src/test/scala/DispatcherSpringFeatureTest.scala new file mode 100644 index 0000000000..457ca7c9bd --- /dev/null +++ b/akka-spring/src/test/scala/DispatcherSpringFeatureTest.scala @@ -0,0 +1,138 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + + +import foo.{IMyPojo, MyPojo, PingActor} +import akka.dispatch._ +import org.scalatest.FeatureSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader +import org.springframework.context.ApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext +import org.springframework.core.io.{ClassPathResource, Resource} +import java.util.concurrent._ +import akka.actor.{UntypedActor, Actor, ActorRef} + +/** + * Tests for spring configuration of typed actors. + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class DispatcherSpringFeatureTest extends FeatureSpec with ShouldMatchers { + val EVENT_DRIVEN_PREFIX = "akka:event-driven:dispatcher:" + + feature("Spring configuration") { + + scenario("get a executor-event-driven-dispatcher with array-blocking-queue from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-1").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-1") + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getCorePoolSize() === 1) + assert(executor.getMaximumPoolSize() === 20) + assert(executor.getKeepAliveTime(TimeUnit.MILLISECONDS) === 3000) + assert(executor.getQueue().isInstanceOf[ArrayBlockingQueue[Runnable]]); + assert(executor.getQueue().remainingCapacity() === 100) + } + + + scenario("get a dispatcher via ref from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val pojo = context.getBean("typed-actor-with-dispatcher-ref").asInstanceOf[IMyPojo] + assert(pojo ne null) + } + + scenario("get a executor-event-driven-dispatcher with blocking-queue with unbounded capacity from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-2").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getQueue().isInstanceOf[BlockingQueue[Runnable]]) + assert(executor.getQueue().remainingCapacity() === Integer.MAX_VALUE) + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-2") + } +/* + scenario("get a executor-event-driven-dispatcher with bounded-blocking-queue and with bounded mailbox capacity") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-mc").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-mc") + val actorRef = UntypedActor.actorOf(classOf[PingActor]) + actorRef.dispatcher = dispatcher + actorRef.start + assert(actorRef.mailbox.isInstanceOf[BlockingQueue[MessageInvocation]]) + assert((actorRef.mailbox.asInstanceOf[BlockingQueue[MessageInvocation]]).remainingCapacity === 1000) + } +*/ + scenario("get a executor-event-driven-dispatcher with unbounded-linked-blocking-queue with bounded capacity from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-4").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-4") + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getQueue().isInstanceOf[BlockingQueue[Runnable]]) + assert(executor.getQueue().remainingCapacity() === 55) + } + + scenario("get a executor-event-driven-dispatcher with unbounded-linked-blocking-queue with unbounded capacity from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-5").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-5") + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getQueue().isInstanceOf[BlockingQueue[Runnable]]) + assert(executor.getQueue().remainingCapacity() === Integer.MAX_VALUE) + } + + scenario("get a executor-event-driven-dispatcher with synchronous-queue from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-event-driven-dispatcher-6").asInstanceOf[ExecutorBasedEventDrivenDispatcher] + assert(dispatcher.name === EVENT_DRIVEN_PREFIX + "dispatcher-6") + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getQueue().isInstanceOf[SynchronousQueue[Runnable]]) + } + + scenario("get a executor-based-event-driven-work-stealing-dispatcher from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val dispatcher = context.getBean("executor-based-event-driven-work-stealing-dispatcher").asInstanceOf[ExecutorBasedEventDrivenWorkStealingDispatcher] + assert(dispatcher ne null) + assert(dispatcher.name === "akka:event-driven:dispatcher:workStealingDispatcher") + val executor = getThreadPoolExecutorAndAssert(dispatcher) + assert(executor.getQueue().isInstanceOf[BlockingQueue[Runnable]]) + } + + scenario("get a thread-based-dispatcher for typed actor from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val pojo = context.getBean("typed-actor-with-thread-based-dispatcher").asInstanceOf[IMyPojo] + assert(pojo ne null) + } + + scenario("get a thread-based-dispatcher for untyped from context") { + val context = new ClassPathXmlApplicationContext("/dispatcher-config.xml") + val actorRef = context.getBean("untyped-actor-with-thread-based-dispatcher").asInstanceOf[ActorRef] + assert(actorRef.getActorClassName() === "akka.spring.foo.PingActor") + actorRef.start() + actorRef.sendOneWay("Hello") + assert(actorRef.getDispatcher.isInstanceOf[ThreadBasedDispatcher]) + } + } + + /** + * get ThreadPoolExecutor via reflection and assert that dispatcher is correct type + */ + private def getThreadPoolExecutorAndAssert(dispatcher: MessageDispatcher): ThreadPoolExecutor = { + + def unpackExecutorService(e: ExecutorService): ExecutorService = e match { + case b: ExecutorServiceDelegate => unpackExecutorService(b.executor) + case t: ThreadPoolExecutor => t + case e => throw new IllegalStateException("Illegal executor type: " + e) + } + + unpackExecutorService(dispatcher match { + case e: ExecutorBasedEventDrivenDispatcher => e.executorService.get() + case x => throw new IllegalStateException("Illegal dispatcher type: " + x) + }).asInstanceOf[ThreadPoolExecutor] + } + +} diff --git a/akka-spring/src/test/scala/ScalaDom.scala b/akka-spring/src/test/scala/ScalaDom.scala new file mode 100644 index 0000000000..9319b0c328 --- /dev/null +++ b/akka-spring/src/test/scala/ScalaDom.scala @@ -0,0 +1,40 @@ +package akka.spring +/** + * from http://stackoverflow.com/questions/2002685/any-conversion-from-scalas-xml-to-w3c-dom + */ + +object ScalaDom { + import scala.xml._ + import org.w3c.dom.{Document => JDocument, Node => JNode} + import javax.xml.parsers.DocumentBuilderFactory + + def dom(n: Node): JDocument = { + + val doc = DocumentBuilderFactory + .newInstance + .newDocumentBuilder + .getDOMImplementation + .createDocument(null, null, null) + + def build(node: Node, parent: JNode): Unit = { + val jnode: JNode = node match { + case e: Elem => { + val jn = doc.createElement(e.label) + e.attributes foreach { a => jn.setAttribute(a.key, a.value.mkString) } + jn + } + case a: Atom[_] => doc.createTextNode(a.text) + case c: Comment => doc.createComment(c.commentText) + case er: EntityRef => doc.createEntityReference(er.entityName) + case pi: ProcInstr => doc.createProcessingInstruction(pi.target, pi.proctext) + } + parent.appendChild(jnode) + node.child.map { build(_, jnode) } + } + + build(n, doc) + doc + + } +} + diff --git a/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala b/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala new file mode 100644 index 0000000000..e5b4fc2c70 --- /dev/null +++ b/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import ScalaDom._ + +import org.w3c.dom.Element +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import akka.config.Supervision. {FaultHandlingStrategy, AllForOneStrategy} + +/** + * Test for SupervisionBeanDefinitionParser + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class SupervisionBeanDefinitionParserTest extends Spec with ShouldMatchers { + private class Parser extends SupervisionBeanDefinitionParser + + describe("A SupervisionBeanDefinitionParser") { + val parser = new Parser() + val builder = BeanDefinitionBuilder.genericBeanDefinition("foo.bar.Foo") + + it("should be able to parse typed actor configuration") { + val props = parser.parseActor(createTypedActorElement); + assert(props ne null) + assert(props.timeout == 1000) + assert(props.target == "foo.bar.MyPojo") + } + + it("should parse the supervisor restart strategy") { + parser.parseSupervisor(createSupervisorElement, builder); + val strategy = builder.getBeanDefinition.getPropertyValues.getPropertyValue("restartStrategy").getValue.asInstanceOf[FaultHandlingStrategy] + assert(strategy ne null) + assert(strategy.isInstanceOf[AllForOneStrategy]) + expect(3) { strategy.asInstanceOf[AllForOneStrategy].maxNrOfRetries.get } + expect(1000) { strategy.asInstanceOf[AllForOneStrategy].withinTimeRange.get } + } + + it("should parse the supervised typed actors") { + parser.parseSupervisor(createSupervisorElement, builder); + val supervised = builder.getBeanDefinition.getPropertyValues.getPropertyValue("supervised").getValue.asInstanceOf[List[ActorProperties]] + assert(supervised ne null) + expect(4) { supervised.length } + val iterator = supervised.iterator + val prop1 = iterator.next + val prop2 = iterator.next + val prop3 = iterator.next + val prop4 = iterator.next + expect("foo.bar.Foo") { prop1.target } + expect("foo.bar.Bar") { prop2.target } + expect("foo.bar.MyPojo") { prop3.target } + expect("foo.bar.MyPojo") { prop4.target } + expect("permanent") { prop1.lifecycle } + expect("temporary") { prop4.lifecycle } + } + + it("should throw IllegalArgumentException on missing mandatory attributes") { + evaluating { parser.parseSupervisor(createSupervisorMissingAttribute, builder) } should produce [IllegalArgumentException] + } + + it("should throw IllegalArgumentException on missing mandatory elements") { + evaluating { parser.parseSupervisor(createSupervisorMissingElement, builder) } should produce [IllegalArgumentException] + } + } + + private def createTypedActorElement : Element = { + val xml = + dom(xml).getDocumentElement + } + + private def createSupervisorElement : Element = { + val xml = + + + java.io.IOException + java.lang.NullPointerException + + + + + + + + + + + + + + dom(xml).getDocumentElement + } + + + private def createSupervisorMissingAttribute : Element = { + val xml = + + + java.io.IOException + + + + + + + dom(xml).getDocumentElement + } + + private def createSupervisorMissingElement : Element = { + val xml = + + + + + + + + dom(xml).getDocumentElement + } +} + diff --git a/akka-spring/src/test/scala/SupervisionFactoryBeanTest.scala b/akka-spring/src/test/scala/SupervisionFactoryBeanTest.scala new file mode 100644 index 0000000000..542b8a1377 --- /dev/null +++ b/akka-spring/src/test/scala/SupervisionFactoryBeanTest.scala @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import akka.config.Supervision._ +import akka.config.TypedActorConfigurator + +private[akka] class Foo + +@RunWith(classOf[JUnitRunner]) +class SupervisionFactoryBeanTest extends Spec with ShouldMatchers { + + val faultHandlingStrategy = new AllForOneStrategy(List(classOf[Exception]), 3, 1000) + val typedActors = List(createTypedActorProperties("akka.spring.Foo", "1000")) + + private def createTypedActorProperties(target: String, timeout: String) : ActorProperties = { + val properties = new ActorProperties() + properties.target = target + properties.timeoutStr = timeout + properties + } + + describe("A SupervisionFactoryBean") { + val bean = new SupervisionFactoryBean + it("should have java getters and setters for all properties") { + bean.setRestartStrategy(faultHandlingStrategy) + assert(bean.getRestartStrategy == faultHandlingStrategy) + bean.setSupervised(typedActors) + assert(bean.getSupervised == typedActors) + } + + it("should return the object type AnyRef") { + assert(bean.getObjectType == classOf[AnyRef]) + } + } +} diff --git a/akka-spring/src/test/scala/SupervisorSpringFeatureTest.scala b/akka-spring/src/test/scala/SupervisorSpringFeatureTest.scala new file mode 100644 index 0000000000..2ce629ed38 --- /dev/null +++ b/akka-spring/src/test/scala/SupervisorSpringFeatureTest.scala @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + + +import akka.spring.foo.{IMyPojo, MyPojo, IFoo, IBar} +import akka.dispatch._ +import akka.config.TypedActorConfigurator +import akka.actor.Supervisor + +import org.scalatest.FeatureSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader +import org.springframework.context.ApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext +import org.springframework.core.io.{ClassPathResource, Resource} +import java.util.concurrent._ + +/** + * Tests for spring configuration of supervisor hierarchies. + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class SupervisorSpringFeatureTest extends FeatureSpec with ShouldMatchers { + + feature("Spring configuration") { + + scenario("get a supervisor for typed actors from context") { + val context = new ClassPathXmlApplicationContext("/supervisor-config.xml") + val myConfigurator = context.getBean("supervision1").asInstanceOf[TypedActorConfigurator] + // get TypedActors + val foo = myConfigurator.getInstance(classOf[IFoo]) + assert(foo ne null) + val bar = myConfigurator.getInstance(classOf[IBar]) + assert(bar ne null) + val pojo = myConfigurator.getInstance(classOf[IMyPojo]) + assert(pojo ne null) + } + + scenario("get a supervisor for untyped actors from context") { + val context = new ClassPathXmlApplicationContext("/supervisor-config.xml") + val supervisor = context.getBean("supervision-untyped-actors").asInstanceOf[Supervisor] + supervisor.children + } + + scenario("get a supervisor and dispatcher from context") { + val context = new ClassPathXmlApplicationContext("/supervisor-config.xml") + val myConfigurator = context.getBean("supervision-with-dispatcher").asInstanceOf[TypedActorConfigurator] + val foo = myConfigurator.getInstance(classOf[IFoo]) + assert(foo ne null) + } + } +} diff --git a/akka-spring/src/test/scala/TypedActorBeanDefinitionParserTest.scala b/akka-spring/src/test/scala/TypedActorBeanDefinitionParserTest.scala new file mode 100644 index 0000000000..f665784355 --- /dev/null +++ b/akka-spring/src/test/scala/TypedActorBeanDefinitionParserTest.scala @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import ScalaDom._ + +import org.w3c.dom.Element + +/** + * Test for TypedActorParser + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class TypedActorBeanDefinitionParserTest extends Spec with ShouldMatchers { + private class Parser extends ActorParser + + describe("A TypedActorParser") { + val parser = new Parser() + it("should parse the typed actor configuration") { + val xml = + + + + val props = parser.parseActor(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.timeout === 1000) + assert(props.target === "foo.bar.MyPojo") + assert(props.scope === "prototype") + assert(props.propertyEntries.entryList.size === 1) + } + + it("should throw IllegalArgumentException on missing mandatory attributes") { + val xml = + + evaluating { parser.parseActor(dom(xml).getDocumentElement) } should produce [IllegalArgumentException] + } + + it("should parse TypedActors configuration with dispatcher") { + val xml = + + + val props = parser.parseActor(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.dispatcher.dispatcherType === "thread-based") + } + + it("should parse remote TypedActors configuration") { + val xml = + + + val props = parser.parseActor(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.host === "com.some.host") + assert(props.port === "2552") + assert(!props.serverManaged) + } + + it("should parse remote server managed TypedActors configuration") { + val xml = + + + val props = parser.parseActor(dom(xml).getDocumentElement); + assert(props ne null) + assert(props.host === "com.some.host") + assert(props.port === "2552") + assert(props.serviceName === "my-service") + assert(props.serverManaged) + } + } +} diff --git a/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala b/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala new file mode 100644 index 0000000000..c6f0b2bff9 --- /dev/null +++ b/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + + +import foo.{PingActor, IMyPojo, MyPojo} +import akka.dispatch.FutureTimeoutException +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader +import org.springframework.context.ApplicationContext +import org.springframework.context.support.ClassPathXmlApplicationContext +import org.springframework.core.io.{ClassPathResource, Resource} +import org.scalatest.{BeforeAndAfterAll, FeatureSpec} +import java.util.concurrent.CountDownLatch +import akka.remote.netty.NettyRemoteSupport +import akka.actor._ +import akka.actor.Actor._ + + +object RemoteTypedActorLog { + import java.util.concurrent.{LinkedBlockingQueue, TimeUnit, BlockingQueue} + val messageLog: BlockingQueue[String] = new LinkedBlockingQueue[String] + val oneWayLog = new LinkedBlockingQueue[String] + + def clearMessageLogs { + messageLog.clear + oneWayLog.clear + } +} + +/** + * Tests for spring configuration of typed actors. + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class TypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers with BeforeAndAfterAll { + + var optimizeLocal_? = remote.asInstanceOf[NettyRemoteSupport].optimizeLocalScoped_? + + override def beforeAll { + remote.asInstanceOf[NettyRemoteSupport].optimizeLocal.set(false) //Can't run the test if we're eliminating all remote calls + remote.start("localhost",9990) + val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) + remote.registerTypedActor("typed-actor-service", typedActor) + } + + override def afterAll { + remote.asInstanceOf[NettyRemoteSupport].optimizeLocal.set(optimizeLocal_?) //Reset optimizelocal after all tests + + remote.shutdown + Thread.sleep(1000) + } + + def getTypedActorFromContext(config: String, id: String) : IMyPojo = { + MyPojo.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext(config) + val myPojo: IMyPojo = context.getBean(id).asInstanceOf[IMyPojo] + myPojo + } + + feature("parse Spring application context") { + + scenario("akka:typed-actor and akka:supervision and akka:dispatcher can be used as top level elements") { + val context = new ClassPathResource("/typed-actor-config.xml") + val beanFactory = new DefaultListableBeanFactory() + val reader = new XmlBeanDefinitionReader(beanFactory) + reader.loadBeanDefinitions(context) + assert(beanFactory.containsBeanDefinition("simple-typed-actor")) + assert(beanFactory.containsBeanDefinition("remote-typed-actor")) + assert(beanFactory.containsBeanDefinition("supervision1")) + assert(beanFactory.containsBeanDefinition("dispatcher1")) + } + + scenario("get a typed actor") { + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 1") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 1") + } + + scenario("get a typed actor of bean") { + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor-of-bean") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 1") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 1") + } + + scenario("FutureTimeoutException when timed out") { + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor") + evaluating {myPojo.longRunning()} should produce[FutureTimeoutException] + } + + scenario("typed-actor with timeout") { + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor-long-timeout") + assert(myPojo.longRunning() === "this took long"); + } + + scenario("get a remote typed-actor") { + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "remote-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 3") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 3") + } + + scenario("get a client-managed-remote-typed-actor") { + val myPojo = getTypedActorFromContext("/server-managed-config.xml", "client-managed-remote-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello client-managed-remote-typed-actor") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello client-managed-remote-typed-actor") + } + + scenario("get a server-managed-remote-typed-actor") { + val serverPojo = getTypedActorFromContext("/server-managed-config.xml", "server-managed-remote-typed-actor") + // + val myPojoProxy = remote.typedActorFor(classOf[IMyPojo], classOf[IMyPojo].getName, 5000L, "localhost", 9990) + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello server-managed-remote-typed-actor") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello server-managed-remote-typed-actor") + } + + scenario("get a server-managed-remote-typed-actor-custom-id") { + val serverPojo = getTypedActorFromContext("/server-managed-config.xml", "server-managed-remote-typed-actor-custom-id") + // + val myPojoProxy = remote.typedActorFor(classOf[IMyPojo], "mypojo-service", 5000L, "localhost", 9990) + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello server-managed-remote-typed-actor 2") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello server-managed-remote-typed-actor 2") + } + + scenario("get a client proxy for server-managed-remote-typed-actor") { + MyPojo.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext("/server-managed-config.xml") + val myPojo: IMyPojo = context.getBean("server-managed-remote-typed-actor-custom-id").asInstanceOf[IMyPojo] + // get client proxy from spring context + val myPojoProxy = context.getBean("typed-client-1").asInstanceOf[IMyPojo] + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello") + MyPojo.latch.await + } + + + } + +} + diff --git a/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala new file mode 100644 index 0000000000..e0c066e527 --- /dev/null +++ b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala @@ -0,0 +1,151 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.spring + + +import foo.PingActor +import akka.dispatch.ExecutorBasedEventDrivenWorkStealingDispatcher +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import org.springframework.context.support.ClassPathXmlApplicationContext +import akka.remote.netty.NettyRemoteSupport +import org.scalatest.{BeforeAndAfterAll, FeatureSpec} + +import java.util.concurrent.CountDownLatch +import akka.actor.{RemoteActorRef, Actor, ActorRef, TypedActor} +import akka.actor.Actor._ + +/** + * Tests for spring configuration of typed actors. + * @author michaelkober + */ +@RunWith(classOf[JUnitRunner]) +class UntypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers with BeforeAndAfterAll { + + var optimizeLocal_? = remote.asInstanceOf[NettyRemoteSupport].optimizeLocalScoped_? + + override def beforeAll { + remote.asInstanceOf[NettyRemoteSupport].optimizeLocal.set(false) //Can't run the test if we're eliminating all remote calls + remote.start("localhost",9990) + } + + override def afterAll { + remote.asInstanceOf[NettyRemoteSupport].optimizeLocal.set(optimizeLocal_?) //Reset optimizelocal after all tests + + remote.shutdown + Thread.sleep(1000) + } + + + def getPingActorFromContext(config: String, id: String) : ActorRef = { + PingActor.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext(config) + val pingActor = context.getBean(id).asInstanceOf[ActorRef] + assert(pingActor.getActorClassName() === "akka.spring.foo.PingActor") + pingActor.start() + } + + + feature("parse Spring application context") { + + scenario("get a untyped actor") { + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor") + myactor.sendOneWay("Hello") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello") + assert(myactor.isDefinedAt("some string message")) + } + + scenario("untyped-actor of provided bean") { + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor-of-bean") + myactor.sendOneWay("Hello") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello") + assert(myactor.isDefinedAt("some string message")) + } + + scenario("untyped-actor with timeout") { + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor-long-timeout") + assert(myactor.getTimeout() === 10000) + myactor.sendOneWay("Hello 2") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 2") + } + + scenario("get a remote typed-actor") { + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "remote-untyped-actor") + myactor.sendOneWay("Hello 4") + assert(myactor.homeAddress.isDefined) + assert(myactor.homeAddress.get.getHostName() === "localhost") + assert(myactor.homeAddress.get.getPort() === 9990) + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 4") + } + + scenario("untyped-actor with custom dispatcher") { + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "untyped-actor-with-dispatcher") + assert(myactor.id === "untyped-actor-with-dispatcher") + assert(myactor.getTimeout() === 1000) + assert(myactor.getDispatcher.isInstanceOf[ExecutorBasedEventDrivenWorkStealingDispatcher]) + myactor.sendOneWay("Hello 5") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 5") + } + + scenario("create client managed remote untyped-actor") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "client-managed-remote-untyped-actor") + myactor.sendOneWay("Hello client managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello client managed remote untyped-actor") + assert(myactor.homeAddress.isDefined) + assert(myactor.homeAddress.get.getHostName() === "localhost") + assert(myactor.homeAddress.get.getPort() === 9990) + } + + scenario("autostart untypedactor when requested in config") { + val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") + val pingActor = context.getBean("remote-untyped-actor-autostart").asInstanceOf[ActorRef] + assert(pingActor.isRunning === true) + assert(pingActor.id === "remote-untyped-actor-autostart") + } + + scenario("create server managed remote untyped-actor") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "server-managed-remote-untyped-actor") + val nrOfActors = Actor.registry.actors.length + val actorRef = remote.actorFor("server-managed-remote-untyped-actor", "localhost", 9990) + actorRef.sendOneWay("Hello server managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello server managed remote untyped-actor") + assert(Actor.registry.actors.length === nrOfActors) + } + + scenario("create server managed remote untyped-actor with custom service id") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "server-managed-remote-untyped-actor-custom-id") + val nrOfActors = Actor.registry.actors.length + val actorRef = remote.actorFor("ping-service", "localhost", 9990) + actorRef.sendOneWay("Hello server managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello server managed remote untyped-actor") + assert(Actor.registry.actors.length === nrOfActors) + } + + scenario("get client actor for server managed remote untyped-actor") { + PingActor.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext("/server-managed-config.xml") + val pingActor = context.getBean("server-managed-remote-untyped-actor-custom-id").asInstanceOf[ActorRef] + assert(pingActor.getActorClassName() === "akka.spring.foo.PingActor") + pingActor.start() + val nrOfActors = Actor.registry.actors.length + // get client actor ref from spring context + val actorRef = context.getBean("client-1").asInstanceOf[ActorRef] + assert(actorRef.isInstanceOf[RemoteActorRef]) + actorRef.sendOneWay("Hello") + PingActor.latch.await + assert(Actor.registry.actors.length === nrOfActors) + } + + } +} + diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index 255cb03091..789de9b46b 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -8,6 +8,7 @@ import java.util.jar.Attributes import java.util.jar.Attributes.Name._ import sbt._ import sbt.CompileOrder._ +import sbt.ScalaProject._ import spde._ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with ExecProject with DocParentProject { akkaParent => @@ -77,13 +78,15 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec // ------------------------------------------------------------------------------------------------------------------- // Versions // ------------------------------------------------------------------------------------------------------------------- - + lazy val CAMEL_VERSION = "2.7.0" + lazy val SPRING_VERSION = "3.0.5.RELEASE" lazy val JACKSON_VERSION = "1.8.0" lazy val JERSEY_VERSION = "1.3" lazy val MULTIVERSE_VERSION = "0.6.2" lazy val SCALATEST_VERSION = "1.4.1" lazy val JETTY_VERSION = "7.4.0.v20110414" lazy val JAVAX_SERVLET_VERSION = "3.0" + lazy val LOGBACK_VERSION = "0.9.28" lazy val SLF4J_VERSION = "1.6.0" lazy val ZOOKEEPER_VERSION = "3.4.0" @@ -93,20 +96,31 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec object Dependencies { - // Compile + + // Compile lazy val aopalliance = "aopalliance" % "aopalliance" % "1.0" % "compile" //Public domain lazy val aspectwerkz = "org.codehaus.aspectwerkz" % "aspectwerkz" % "2.2.3" % "compile" //ApacheV2 lazy val beanstalk = "beanstalk" % "beanstalk_client" % "1.4.5" //New BSD lazy val bookkeeper = "org.apache.hadoop.zookeeper" % "bookkeeper" % ZOOKEEPER_VERSION //ApacheV2 + lazy val camel_core = "org.apache.camel" % "camel-core" % CAMEL_VERSION % "compile" //ApacheV2 + lazy val commons_codec = "commons-codec" % "commons-codec" % "1.4" % "compile" //ApacheV2 lazy val commons_io = "commons-io" % "commons-io" % "2.0.1" % "compile" //ApacheV2 lazy val javax_servlet_30 = "org.glassfish" % "javax.servlet" % JAVAX_SERVLET_VERSION % "provided" //CDDL v1 lazy val jetty = "org.eclipse.jetty" % "jetty-server" % JETTY_VERSION % "provided" //Eclipse license + lazy val jetty_util = "org.eclipse.jetty" % "jetty-util" % JETTY_VERSION % "compile" //Eclipse license + lazy val jetty_xml = "org.eclipse.jetty" % "jetty-xml" % JETTY_VERSION % "compile" //Eclipse license + lazy val jetty_servlet = "org.eclipse.jetty" % "jetty-servlet" % JETTY_VERSION % "compile" //Eclipse license + lazy val guicey = "org.guiceyfruit" % "guice-all" % "2.0" % "compile" //ApacheV2 lazy val h2_lzf = "voldemort.store.compress" % "h2-lzf" % "1.0" % "compile" //ApacheV2 lazy val jackson = "org.codehaus.jackson" % "jackson-mapper-asl" % JACKSON_VERSION % "compile" //ApacheV2 lazy val jackson_core = "org.codehaus.jackson" % "jackson-core-asl" % JACKSON_VERSION % "compile" //ApacheV2 lazy val jersey_server = "com.sun.jersey" % "jersey-server" % JERSEY_VERSION % "provided" //CDDL v1 + lazy val jersey = "com.sun.jersey" % "jersey-core" % JERSEY_VERSION % "compile" //CDDL v1 + lazy val jersey_json = "com.sun.jersey" % "jersey-json" % JERSEY_VERSION % "compile" //CDDL v1 + lazy val jersey_contrib = "com.sun.jersey.contribs" % "jersey-scala" % JERSEY_VERSION % "compile" //CDDL v1 + lazy val jsr250 = "javax.annotation" % "jsr250-api" % "1.0" % "compile" //CDDL v1 lazy val jsr311 = "javax.ws.rs" % "jsr311-api" % "1.1" % "compile" //CDDL v1 lazy val multiverse = "org.multiverse" % "multiverse-alpha" % MULTIVERSE_VERSION % "compile" //ApacheV2 @@ -117,6 +131,10 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec lazy val sjson = "net.debasishg" %% "sjson" % "0.11" % "compile" //ApacheV2 lazy val sjson_test = "net.debasishg" %% "sjson" % "0.11" % "test" //ApacheV2 lazy val slf4j = "org.slf4j" % "slf4j-api" % SLF4J_VERSION // MIT + lazy val spring_beans = "org.springframework" % "spring-beans" % SPRING_VERSION % "compile" //ApacheV2 + lazy val spring_context = "org.springframework" % "spring-context" % SPRING_VERSION % "compile" //ApacheV2 + + lazy val stax_api = "javax.xml.stream" % "stax-api" % "1.0-2" % "compile" //ApacheV2 lazy val logback = "ch.qos.logback" % "logback-classic" % "0.9.28" % "runtime" //MIT lazy val log4j = "log4j" % "log4j" % "1.2.15" //ApacheV2 lazy val zookeeper = "org.apache.hadoop.zookeeper" % "zookeeper" % ZOOKEEPER_VERSION //ApacheV2 @@ -131,6 +149,9 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec lazy val junit = "junit" % "junit" % "4.5" % "test" //Common Public License 1.0 lazy val mockito = "org.mockito" % "mockito-all" % "1.8.1" % "test" //MIT lazy val scalatest = "org.scalatest" %% "scalatest" % SCALATEST_VERSION % "test" //ApacheV2 + lazy val testLogback = "ch.qos.logback" % "logback-classic" % LOGBACK_VERSION % "test" // EPL 1.0 / LGPL 2.1 + lazy val camel_spring = "org.apache.camel" % "camel-spring" % CAMEL_VERSION % "test" //ApacheV2 + } // ------------------------------------------------------------------------------------------------------------------- @@ -144,7 +165,13 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec lazy val akka_remote = project("akka-remote", "akka-remote", new AkkaRemoteProject(_), akka_stm, akka_actor_tests) lazy val akka_cluster = project("akka-cluster", "akka-cluster", new AkkaClusterProject(_), akka_remote) lazy val akka_durable_mailboxes = project("akka-durable-mailboxes", "akka-durable-mailboxes", new AkkaDurableMailboxesParentProject(_), akka_remote) + lazy val akka_camel = project("akka-camel", "akka-camel", new AkkaCamelProject(_), akka_actor, akka_slf4j) + lazy val akka_camel_typed = project("akka-camel-typed", "akka-camel-typed", new AkkaCamelTypedProject(_), akka_actor, akka_slf4j, akka_camel) + lazy val akka_spring = project("akka-spring", "akka-spring", new AkkaSpringProject(_), akka_remote, akka_actor) + lazy val akka_sbt_plugin = project("akka-sbt-plugin", "akka-sbt-plugin", new AkkaSbtPluginProject(_)) + + lazy val akka_kernel = project("akka-kernel", "akka-kernel", new AkkaKernelProject(_), akka_stm, akka_remote, akka_http, akka_slf4j, akka_camel) lazy val akka_http = project("akka-http", "akka-http", new AkkaHttpProject(_), akka_actor) lazy val akka_slf4j = project("akka-slf4j", "akka-slf4j", new AkkaSlf4jProject(_), akka_actor) lazy val akka_tutorials = project("akka-tutorials", "akka-tutorials", new AkkaTutorialsParentProject(_), akka_actor) @@ -159,6 +186,8 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec // add the sh action since it doesn't exist in ParentProject lazy val sh = task { args => execOut { Process("sh" :: "-c" :: args.mkString(" ") :: Nil) } } + + // ------------------------------------------------------------------------------------------------------------------- // Scaladocs // ------------------------------------------------------------------------------------------------------------------- @@ -407,6 +436,153 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec class AkkaZooKeeperMailboxProject(info: ProjectInfo) extends AkkaDefaultProject(info) + // ------------------------------------------------------------------------------------------------------------------- + // akka-camel subproject + // ------------------------------------------------------------------------------------------------------------------- + + class AkkaCamelProject(info: ProjectInfo) extends AkkaDefaultProject(info) { + + val camel_core = Dependencies.camel_core + + // testing + val junit = Dependencies.junit + val scalatest = Dependencies.scalatest + val logback = Dependencies.testLogback + + override def testOptions = createTestFilter( _.endsWith("Test")) + } + + // ------------------------------------------------------------------------------------------------------------------- + // akka-camel-typed subproject + // ------------------------------------------------------------------------------------------------------------------- + + class AkkaCamelTypedProject(info: ProjectInfo) extends AkkaDefaultProject(info) { + val camel_core = Dependencies.camel_core + + // testing + val junit = Dependencies.junit + val scalatest = Dependencies.scalatest + val logback = Dependencies.testLogback + + override def testOptions = createTestFilter( _.endsWith("Test")) + } + + // ------------------------------------------------------------------------------------------------------------------- + // akka-kernel subproject + // ------------------------------------------------------------------------------------------------------------------- + + class AkkaKernelProject(info: ProjectInfo) extends AkkaDefaultProject(info) { + val jetty = Dependencies.jetty + val jetty_util = Dependencies.jetty_util + val jetty_xml = Dependencies.jetty_xml + val jetty_servlet = Dependencies.jetty_servlet + val jackson_core = Dependencies.jackson_core + val jersey = Dependencies.jersey + val jersey_contrib = Dependencies.jersey_contrib + val jersey_json = Dependencies.jersey_json + val jersey_server = Dependencies.jersey_server + val stax_api = Dependencies.stax_api + } + + + // ------------------------------------------------------------------------------------------------------------------- + // akka-spring subproject + // ------------------------------------------------------------------------------------------------------------------- + + class AkkaSpringProject(info: ProjectInfo) extends AkkaDefaultProject(info) { + val spring_beans = Dependencies.spring_beans + val spring_context = Dependencies.spring_context + + // testing + val camel_spring = Dependencies.camel_spring + val junit = Dependencies.junit + val scalatest = Dependencies.scalatest + } + + // ------------------------------------------------------------------------------------------------------------------- + // akka-sbt-plugin subproject + // ------------------------------------------------------------------------------------------------------------------- + + class AkkaSbtPluginProject(info: ProjectInfo) extends PluginProject(info) { + val srcManagedScala = "src_managed" / "main" / "scala" + + lazy val addAkkaConfig = systemOptional[Boolean]("akka.release", false) + + lazy val generateAkkaSbtPlugin = { + val cleanSrcManaged = cleanTask(srcManagedScala) named ("clean src_managed") + task { + info.parent match { + case Some(project: ParentProject) => + xsbt.FileUtilities.write((srcManagedScala / "AkkaProject.scala").asFile, + GenerateAkkaSbtPlugin(project, addAkkaConfig.value)) + case _ => + } + None + } dependsOn cleanSrcManaged + } + + override def mainSourceRoots = super.mainSourceRoots +++ (srcManagedScala ##) + override def compileAction = super.compileAction dependsOn(generateAkkaSbtPlugin) + + lazy val publishRelease = { + val releaseConfiguration = new DefaultPublishConfiguration(localReleaseRepository, "release", false) + publishTask(publishIvyModule, releaseConfiguration) dependsOn (deliver, publishLocal, makePom) + } + } + + object GenerateAkkaSbtPlugin { + def apply(project: ParentProject, addAkkaConfig: Boolean): String = { + val extraConfigs = { + if (addAkkaConfig) Set(ModuleConfiguration("se.scalablesolutions.akka", Repositories.AkkaRepo)) + else Set.empty[ModuleConfiguration] + } + val akkaModules = project.subProjects.values.map(_.name).flatMap{ + case "akka-sbt-plugin" => Iterator.empty + case s if s.startsWith("akka-") => Iterator.single(s.drop(5)) + case _ => Iterator.empty + } + val (repos, configs) = (project.moduleConfigurations ++ extraConfigs).foldLeft((Set.empty[String], Set.empty[String])){ + case ((repos, configs), ModuleConfiguration(org, name, ver, MavenRepository(repoName, repoPath))) => + val repoId = repoName.replaceAll("""[^a-zA-Z]""", "_") + val configId = org.replaceAll("""[^a-zA-Z]""", "_") + + (if (name == "*") "" else ("_" + name.replaceAll("""[^a-zA-Z0-9]""", "_") + + (if (ver == "*") "" else ("_" + ver.replaceAll("""[^a-zA-Z0-9]""", "_"))))) + (repos + (" lazy val "+repoId+" = MavenRepository(\""+repoName+"\", \""+repoPath+"\")"), + configs + (" lazy val "+configId+" = ModuleConfiguration(\""+org+"\", \""+name+"\", \""+ver+"\", "+repoId+")")) + case (x, _) => x + } + """|import sbt._ + | + |object AkkaRepositories { + |%s + |} + | + |trait AkkaBaseProject extends BasicScalaProject { + | import AkkaRepositories._ + | + |%s + |} + | + |trait AkkaProject extends AkkaBaseProject { + | val akkaVersion = "%s" + | + | + | def akkaModule(module: String) = "se.scalablesolutions.akka" %% ("akka-" + module) %% { + | if (Set(%s).contains(module)) + | akkaModulesVersion + | else + | akkaVersion + | } + | + | val akkaActor = akkaModule("actor") + |} + |""".stripMargin.format(repos.mkString("\n"), + configs.mkString("\n"), + project.version.toString, + akkaModules.map("\"" + _ + "\"").mkString(", ")) + } + } + // ------------------------------------------------------------------------------------------------------------------- // Samples // ------------------------------------------------------------------------------------------------------------------- @@ -521,6 +697,13 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec val slf4j = Dependencies.slf4j } + // ------------------------------------------------------------------------------------------------------------------- + // Test options + // ------------------------------------------------------------------------------------------------------------------- + + lazy val integrationTestsEnabled = systemOptional[Boolean]("integration.tests",false) + lazy val stressTestsEnabled = systemOptional[Boolean]("stress.tests",false) + // ------------------------------------------------------------------------------------------------------------------- // Default project // ------------------------------------------------------------------------------------------------------------------- @@ -577,6 +760,21 @@ class AkkaParentProject(info: ProjectInfo) extends ParentProject(info) with Exec val releaseConfiguration = new DefaultPublishConfiguration(localReleaseRepository, "release", false) publishTask(publishIvyModule, releaseConfiguration) dependsOn (deliver, publishLocal, makePom) } + + /** + * Used for testOptions, possibility to enable the running of integration and or stresstests + * + * To enable set true and disable set false + * set integration.tests true + * set stress.tests true + */ + def createTestFilter(defaultTests: (String) => Boolean) = { TestFilter({ + case s: String if defaultTests(s) => true + case s: String if integrationTestsEnabled.value => s.endsWith("TestIntegration") + case s: String if stressTestsEnabled.value => s.endsWith("TestStress") + case _ => false + }) :: Nil + } } // -------------------------------------------------------------------------------------------------------------------