├── scalaz-camel-akka
└── src
│ ├── test
│ ├── resources
│ │ ├── akka.conf
│ │ └── logback-test.xml
│ └── scala
│ │ └── scalaz
│ │ └── camel
│ │ └── akka
│ │ ├── AkkaTestProcessors.scala
│ │ ├── AkkaTestContext.scala
│ │ └── AkkaTest.scala
│ └── main
│ └── scala
│ ├── scalaz
│ └── camel
│ │ └── akka
│ │ ├── Akka.scala
│ │ ├── ActorRefx.scala
│ │ ├── Conv.scala
│ │ ├── Dsl.scala
│ │ └── ActorMgnt.scala
│ └── akka
│ └── actor
│ └── Sender.scala
├── project
├── plugins.sbt
└── ScalazCamelBuild.scala
├── .gitignore
├── scalaz-camel-samples
└── src
│ ├── test
│ └── scala
│ │ └── scalaz
│ │ └── camel
│ │ └── samples
│ │ └── CoreExampleTest.scala
│ └── main
│ ├── resources
│ └── context.xml
│ └── scala
│ └── scalaz
│ └── camel
│ └── samples
│ └── CoreExample.scala
├── README.textile
├── scalaz-camel-core
└── src
│ ├── test
│ ├── scala
│ │ └── scalaz
│ │ │ └── camel
│ │ │ └── core
│ │ │ ├── ExecutorMgnt.scala
│ │ │ ├── CamelTestContext.scala
│ │ │ ├── CamelSetupTest.scala
│ │ │ ├── RouterTest.scala
│ │ │ ├── CamelJettyTest.scala
│ │ │ ├── CamelTestProcessors.scala
│ │ │ ├── CamelJmsTest.scala
│ │ │ ├── CamelLoadTest.scala
│ │ │ ├── MessageTest.scala
│ │ │ ├── CamelAttemptTest.scala
│ │ │ └── CamelTest.scala
│ └── resources
│ │ └── context.xml
│ └── main
│ └── scala
│ └── scalaz
│ └── camel
│ └── core
│ ├── Camel.scala
│ ├── Router.scala
│ ├── Message.scala
│ ├── Conv.scala
│ └── Dsl.scala
└── LICENSE
/scalaz-camel-akka/src/test/resources/akka.conf:
--------------------------------------------------------------------------------
1 | akka{
2 | }
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/"
2 |
3 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.0.0")
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | activemq-data
2 | scalaz-camel.*
3 | project/boot
4 | project/build/target
5 | project/plugins/project
6 | project/plugins/target
7 | lib_managed
8 | target
9 | .idea
10 | *.iml
11 |
--------------------------------------------------------------------------------
/scalaz-camel-samples/src/test/scala/scalaz/camel/samples/CoreExampleTest.scala:
--------------------------------------------------------------------------------
1 | package scalaz.camel.samples
2 |
3 | import org.scalatest.WordSpec
4 |
5 | /**
6 | * @author Martin Krasser
7 | */
8 | class CoreExampleTest extends WordSpec {
9 |
10 | "CoreExample" must {
11 | "run" in {
12 | CoreExample.run
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/scalaz-camel-akka/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ActorRef wrapper.
22 | *
23 | * @author Martin Krasser
24 | */
25 | class ActorRefx(actor: ActorRef) {
26 | /** Binds the life cycle of actor to that of the current CamelContext */
27 | def manage(implicit am: ActorMgnt): ActorRef = am.manage(actor)
28 |
29 | /** Returns the endpoint URI of actor */
30 | def uri: String = "actor:uuid:%s" format actor.uuid
31 |
32 | /** Returns the endpoint URI of actor containing the given options */
33 | def uri(options: String): String = "%s?%s" format (uri, options)
34 | }
35 |
--------------------------------------------------------------------------------
/scalaz-camel-akka/src/main/scala/scalaz/camel/akka/Conv.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.akka
17 |
18 | import akka.actor.ActorRef
19 | import akka.actor.Sender
20 |
21 | import scalaz._
22 |
23 | import scalaz.camel.core.Conv._
24 | import scalaz.camel.core.Message
25 |
26 | /**
27 | * @author Martin Krasser
28 | */
29 | trait Conv {
30 | import Scalaz.success
31 |
32 | /**
33 | * Converts actor into a MessageProcessor. The created processor
34 | * supports multiple replies from actor.
35 | */
36 | def messageProcessor(actor: ActorRef): MessageProcessor =
37 | (m: Message, k: MessageValidation => Unit) => {
38 | if (m.context.oneway) {
39 | actor.!(m); k(success(m))
40 | } else {
41 | actor.!(m)(Some(new Sender(k).start))
42 | }
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelTestContext.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import collection.mutable.Map
19 |
20 | import org.apache.camel.component.mock.MockEndpoint
21 | import org.apache.camel.impl.DefaultCamelContext
22 |
23 | /**
24 | * @author Martin Krasser
25 | */
26 | trait CamelTestContext extends Camel with CamelTestProcessors {
27 | import scalaz.concurrent.Strategy._
28 |
29 | dispatchConcurrencyStrategy = Sequential
30 | multicastConcurrencyStrategy = Sequential
31 | processorConcurrencyStrategy = Sequential
32 |
33 | val context = new DefaultCamelContext
34 | val template = context.createProducerTemplate
35 |
36 | implicit val router = new Router(context)
37 |
38 | val mocks = Map[String, MockEndpoint]()
39 | def mock(s: String) = {
40 | mocks.get(s) match {
41 | case Some(ep) => ep
42 | case None => {
43 | val ep = context.getEndpoint("mock:%s" format s, classOf[MockEndpoint])
44 | mocks.put(s, ep)
45 | ep
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/main/scala/scalaz/camel/core/Camel.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | /**
19 | * Provides the Camel DSL.
20 | *
21 | * @author Martin Krasser
22 | */
23 | trait Camel extends DslEip with DslAttempt with DslEndpoint with DslApply with Conv {
24 | import org.apache.camel.Processor
25 | import scalaz.concurrent.Strategy
26 |
27 | /**
28 | * Concurrency strategy to use for dispatching messages along the processor chain.
29 | * Defaults to Strategy.Sequential.
30 | */
31 | var dispatchConcurrencyStrategy: Strategy = Strategy.Sequential
32 |
33 | /**
34 | * Concurrency strategy to use for distributing messages to destinations with the
35 | * multicast and scatter-gather EIPs. Defaults to Strategy.Sequential.
36 | */
37 | var multicastConcurrencyStrategy: Strategy = Strategy.Sequential
38 |
39 | protected def dispatchStrategy = dispatchConcurrencyStrategy
40 | protected def multicastStrategy = multicastConcurrencyStrategy
41 |
42 | implicit def messageProcessorToMessageRoute(p: MessageProcessor): MessageRoute =
43 | messageRoute(p)
44 |
45 | implicit def messageProcessorToMessageRoute(p: Message => Message): MessageRoute =
46 | messageRoute(messageProcessor(p))
47 |
48 | implicit def camelProcessorToMessageRoute(p: Processor)(implicit cm: ContextMgnt): MessageRoute =
49 | messageRoute(messageProcessor(p, cm))
50 |
51 | implicit def responderToResponderApplication(r: Responder[MessageValidation]) =
52 | new ResponderApplication(r)
53 |
54 | implicit def routeToRouteApplication(p: MessageRoute) =
55 | new RouteApplication(p)
56 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelSetupTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.apache.camel.impl.DefaultCamelContext
19 |
20 | import org.scalatest.matchers.MustMatchers
21 | import org.scalatest.{BeforeAndAfterAll, WordSpec}
22 |
23 | import scalaz.concurrent.Strategy._
24 |
25 | /**
26 | * @author Martin Krasser
27 | */
28 | class CamelSetupTest extends Camel with CamelTestProcessors with WordSpec with MustMatchers with BeforeAndAfterAll {
29 |
30 | dispatchConcurrencyStrategy = Sequential
31 | multicastConcurrencyStrategy = Sequential
32 | processorConcurrencyStrategy = Naive
33 |
34 | val context = new DefaultCamelContext
35 | val template = context.createProducerTemplate
36 | implicit val router = new Router(context)
37 |
38 | override def afterAll = router.stop
39 | override def beforeAll = {
40 | // also setup these routes before router start
41 | from("direct:predef-1") { appendToBody("-p1") }
42 | from("direct:predef-2") { appendToBody("-p2") }
43 | }
44 |
45 | "scalaz.camel.core.Camel" when {
46 | "given an implicit router that has not been started" must {
47 | "allow setup of routes" in {
48 | from("direct:test-1") {
49 | to("direct:predef-1") >=> appendToBody("-1")
50 | }
51 | }
52 | }
53 | "given an implicit router that has been started" must {
54 | "allow setup of routes" in {
55 | router.start
56 | from("direct:test-2") {
57 | to("direct:predef-2") >=> appendToBody("-2")
58 | }
59 |
60 | template.requestBody("direct:test-1", "test") must equal("test-p1-1")
61 | template.requestBody("direct:test-2", "test") must equal("test-p2-2")
62 | }
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/RouterTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.apache.camel.{Consumer, Producer}
19 | import org.apache.camel.impl.{ServiceSupport, DefaultCamelContext}
20 |
21 | import org.scalatest.matchers.MustMatchers
22 | import org.scalatest.{BeforeAndAfterAll, WordSpec}
23 |
24 | /**
25 | * @author Martin Krasser
26 | */
27 | class RouterTest extends WordSpec with MustMatchers {
28 |
29 | val router = new Router(new DefaultCamelContext)
30 |
31 | implicit def toServiceSupport(consumer: Consumer): ServiceSupport = consumer.asInstanceOf[ServiceSupport]
32 | implicit def toServiceSupport(producer: Producer): ServiceSupport = producer.asInstanceOf[ServiceSupport]
33 |
34 | var c1, c2: Consumer = _
35 | var p1, p2: Producer = _
36 |
37 | "A router" when {
38 | "not started" must {
39 | "create not-started consumers and producers" in {
40 | c1 = router.createConsumer("direct:test-1", null)
41 | p1 = router.createProducer("direct:test-1")
42 | c1 must not be ('started)
43 | p1 must not be ('started)
44 | }
45 | }
46 | "started" must {
47 | "start previously created consumers and producers" in {
48 | router.start
49 | c1 must be ('started)
50 | p1 must be ('started)
51 | }
52 | "create started consumers and producers" in {
53 | c2 = router.createConsumer("direct:test-2", null)
54 | p2 = router.createProducer("direct:test-2")
55 | c2 must be ('started)
56 | p2 must be ('started)
57 | }
58 | }
59 | "stopped" must {
60 | "stop all previously created consumers and producers" in {
61 | router.stop
62 | c1 must not be ('started)
63 | p1 must not be ('started)
64 | c2 must not be ('started)
65 | p2 must not be ('started)
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelJettyTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.scalatest.{WordSpec, BeforeAndAfterAll}
19 | import org.scalatest.matchers.MustMatchers
20 |
21 | import scalaz._
22 | import scalaz.concurrent.Strategy._
23 |
24 | /**
25 | * @author Martin Krasser
26 | */
27 | class CamelJettyTest extends CamelTestContext with WordSpec with MustMatchers with BeforeAndAfterAll {
28 | import Scalaz._
29 |
30 | processorConcurrencyStrategy = Naive
31 |
32 | override def beforeAll = router.start
33 | override def afterAll = router.stop
34 |
35 | def support = afterWord("support")
36 |
37 | "scalaz.camel.core.Camel" should support {
38 | "non-blocking routing with asynchronous Jetty endpoints" in {
39 |
40 | // non-blocking server route with asynchronous CPS processors
41 | // and a Jetty endpoint using Jetty continuations.
42 | from("jetty:http://localhost:8766/test") {
43 | convertBodyToString >=> repeatBody
44 | }
45 |
46 | // non-blocking server route with asynchronous CPS processors
47 | // and a Jetty endpoint using an asynchronous HTTP client.
48 | from("direct:test-1") {
49 | to("jetty:http://localhost:8766/test") >=> appendToBody("-done")
50 | }
51 |
52 | // the only blocking operation here (waits for an answer)
53 | template.requestBody("direct:test-1", "test") must equal("testtest-done")
54 | }
55 |
56 | "passing the latest update of messages to error handlers" in {
57 | // latest update before failure is conversion of body to string
58 | from("jetty:http://localhost:8761/test") {
59 | attempt {
60 | convertBodyToString >=> failWithMessage("failure")
61 | } fallback {
62 | case e: Exception => appendToBody("-handled")
63 | }
64 | }
65 |
66 | template.requestBody("http://localhost:8761/test", "test", classOf[String]) must equal ("test-handled")
67 | }
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/resources/context.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 | actor to that of the current CamelContext.
79 | */
80 | def manage(actor: ActorRef)(implicit am: ActorMgnt): ActorRef =
81 | am.manage(actor)
82 |
83 | /**
84 | * Creates a MessageProcessor for communicating with actor.
85 | * The created processor supports multiple replies from actor.
86 | */
87 | def to(actor: ActorRef): MessageProcessor =
88 | messageProcessor(actor)
89 | }
90 |
--------------------------------------------------------------------------------
/scalaz-camel-akka/src/main/scala/scalaz/camel/akka/ActorMgnt.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.akka
17 |
18 | import java.util.Collection
19 | import java.util.concurrent.ThreadPoolExecutor
20 |
21 | import akka.actor.ActorRef
22 |
23 | import org.apache.camel._
24 | import org.apache.camel.support.ServiceSupport
25 | import org.apache.camel.spi.LifecycleStrategy
26 |
27 | import scalaz.camel.core.ContextMgnt
28 |
29 | /**
30 | * Manages the life cycle of actors. Should be used as follows:
31 | *
32 | *
33 | * val context: CamelContext = ... 34 | * implicit val router = new Router(context) with ActorMgnt 35 | *36 | * 37 | * @author Martin Krasser 38 | */ 39 | trait ActorMgnt { this: ContextMgnt => 40 | 41 | /** 42 | * Binds the life cycle of
actor to that of the current CamelContext.
43 | */
44 | def manage(actor: ActorRef): ActorRef = {
45 | context.addLifecycleStrategy(new LifecycleSync(actor))
46 | val service = context.asInstanceOf[ServiceSupport]
47 | if (service.isStarted) actor.start else actor
48 | }
49 | }
50 |
51 | /**
52 | * @author Martin Krasser
53 | */
54 | private[camel] class LifecycleSync(actor: ActorRef) extends LifecycleStrategy {
55 | import org.apache.camel.spi.RouteContext
56 | import org.apache.camel.builder.ErrorHandlerBuilder
57 |
58 | def onContextStart(context: CamelContext) = actor.start
59 | def onContextStop(context: CamelContext) = actor.stop
60 |
61 | // no action by default
62 | def onThreadPoolAdd(camelContext: CamelContext, threadPool: ThreadPoolExecutor) = {}
63 | def onErrorHandlerAdd(routeContext: RouteContext, errorHandler: Processor, errorHandlerBuilder: ErrorHandlerBuilder) = {}
64 | def onRouteContextCreate(routeContext: RouteContext) = {}
65 | def onRoutesRemove(routes: Collection[Route]) = {}
66 | def onRoutesAdd(routes: Collection[Route]) = {}
67 | def onServiceRemove(context: CamelContext, service: Service, route: Route) = {}
68 | def onServiceAdd(context: CamelContext, service: Service, route: Route) = {}
69 | def onEndpointRemove(endpoint: Endpoint) = {}
70 | def onEndpointAdd(endpoint: Endpoint) = {}
71 | def onComponentRemove(name: String, component: Component) = {}
72 | def onComponentAdd(name: String, component: Component) = {}
73 | def onThreadPoolAdd(context: CamelContext, executor: ThreadPoolExecutor, s: String, s1: String, s2: String, s3: String) {}
74 | def onErrorHandlerAdd(routeContext: RouteContext, errorHandler: Processor, errorHandlerBuilder: ErrorHandlerFactory) {}
75 | }
76 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelTestProcessors.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.apache.camel.{AsyncCallback, AsyncProcessor, Exchange, Processor}
19 |
20 | import scalaz._
21 |
22 | /**
23 | * @author Martin Krasser
24 | */
25 | trait CamelTestProcessors { this: Conv =>
26 | import scalaz.concurrent.Strategy
27 | import Scalaz._
28 |
29 | /** Concurrency strategy for each created processor (defaults to Strategy.Sequential) */
30 | var processorConcurrencyStrategy: Strategy = Strategy.Sequential
31 |
32 | //
33 | // Direct-style processors: Message => Message (may throw exception)
34 | //
35 |
36 | /** Fails with Exception and error message em (direct-style processor). */
37 | def ds_failWithMessage(em: String): Message => Message = (m: Message) => throw new Exception(em)
38 |
39 | /** Appends o to message body (direct-style processor) */
40 | def ds_appendToBody(o: Any)(implicit mgnt: ContextMgnt) = (m: Message) => m.appendToBody(o)
41 |
42 | /** Prints message to stdout (direct-style processor) */
43 | def ds_printMessage = (m: Message) => { println(m); m }
44 |
45 | //
46 | // CPS (continuation-passing style) processors: (Message, MessageValidation => Unit) => Unit
47 | //
48 |
49 | /** Fails with Exception and error message em. */
50 | def failWithMessage(em: String): MessageProcessor = cps(ds_failWithMessage(em))
51 |
52 | /** Converts message body to String */
53 | def convertBodyToString(implicit mgnt: ContextMgnt) = cps(m => m.bodyTo[String])
54 |
55 | /** Appends o to message body */
56 | def appendToBody(o: Any)(implicit mgnt: ContextMgnt) = cps(ds_appendToBody(o))
57 |
58 | /** Prints message to stdout */
59 | def printMessage = cps(ds_printMessage)
60 |
61 | /** Repeats message body (using String concatenation) */
62 | def repeatBody = new RepeatBodyProcessor(processorConcurrencyStrategy)
63 |
64 | /** Camel processor that repeats the body of the input message */
65 | class RepeatBodyProcessor(s: Strategy) extends AsyncProcessor {
66 | def process(exchange: Exchange) = {
67 | val body = exchange.getIn.getBody(classOf[String])
68 | exchange.getIn.setBody(body + body)
69 | }
70 |
71 | def process(exchange: Exchange, callback: AsyncCallback) = {
72 | s.apply {
73 | process(exchange)
74 | callback.done(false)
75 | }
76 | false
77 | }
78 |
79 | def sp = this.asInstanceOf[Processor]
80 | }
81 |
82 | /** Creates an CPS processor direct-style processor */
83 | def cps(p: Message => Message): MessageProcessor = messageProcessor(p, processorConcurrencyStrategy)
84 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelJmsTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.scalatest.matchers.MustMatchers
19 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, WordSpec}
20 |
21 | import scalaz.concurrent.Strategy._
22 |
23 | /**
24 | * @author Martin Krasser
25 | */
26 | class CamelJmsTest extends Camel with CamelTestProcessors with WordSpec with MustMatchers with BeforeAndAfterAll with BeforeAndAfterEach {
27 | import org.apache.camel.component.mock.MockEndpoint
28 | import org.apache.camel.spring.SpringCamelContext._
29 |
30 | dispatchConcurrencyStrategy = Sequential
31 | multicastConcurrencyStrategy = Sequential
32 | processorConcurrencyStrategy = Naive
33 |
34 | val context = springCamelContext("/context.xml")
35 | val template = context.createProducerTemplate
36 | implicit val router = new Router(context)
37 |
38 | override def beforeAll = router.start
39 | override def afterAll = router.stop
40 | override def afterEach = mock.reset
41 |
42 | def mock = context.getEndpoint("mock:mock", classOf[MockEndpoint])
43 |
44 | def support = afterWord("support")
45 |
46 | "scalaz.camel.core.Camel" should support {
47 |
48 | "communication with jms endpoints" in {
49 | from("jms:queue:test") {
50 | appendToBody("-1") >=> appendToBody("-2") >=> printMessage >=> to("mock:mock")
51 | }
52 |
53 | from("direct:test") {
54 | to("jms:queue:test") >=> printMessage
55 | }
56 |
57 | mock.expectedBodiesReceivedInAnyOrder("a-1-2", "b-1-2", "c-1-2")
58 | template.sendBody("direct:test", "a")
59 | template.sendBody("direct:test", "b")
60 | template.sendBody("direct:test", "c")
61 | mock.assertIsSatisfied
62 | }
63 |
64 | "receive-acknowledge and background processing scenarios" in {
65 | from("direct:ack") {
66 | oneway >=> to("jms:queue:background") >=> { m: Message => m.appendToBody("-ack") }
67 | }
68 |
69 | from("jms:queue:background") {
70 | appendToBody("-1") >=> to("mock:mock")
71 | }
72 |
73 | mock.expectedBodiesReceived("hello-1")
74 | template.requestBody("direct:ack", "hello") must equal ("hello-ack")
75 | mock.assertIsSatisfied
76 | }
77 |
78 | "fast failure of routes" in {
79 | from("jms:queue:test-failure") {
80 | appendToBody("-1") >=> choose {
81 | case Message("a-1", _) => failWithMessage("failure")
82 | case Message("b-1", _) => printMessage
83 | } >=> to("mock:mock")
84 | }
85 |
86 | from("direct:test-failure") {
87 | to("jms:queue:test-failure") >=> printMessage
88 | }
89 |
90 | mock.expectedBodiesReceived("b-1")
91 | template.sendBody("direct:test-failure", "a")
92 | template.sendBody("direct:test-failure", "b")
93 | mock.assertIsSatisfied
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelLoadTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import java.util.concurrent.{CountDownLatch, Executors}
19 |
20 | import org.scalatest.{WordSpec, BeforeAndAfterAll}
21 | import org.scalatest.matchers.MustMatchers
22 |
23 | import scalaz._
24 | import scalaz.concurrent.Strategy._
25 |
26 | /**
27 | * @author Martin Krasser
28 | */
29 | abstract class CamelLoadTest extends CamelTestContext with ExecutorMgnt with WordSpec with MustMatchers with BeforeAndAfterAll {
30 | import Scalaz._
31 |
32 | override def beforeAll = router.start
33 | override def afterAll = {
34 | shutdown
35 | router.stop
36 | }
37 |
38 | "scalaz.camel.core.Camel" should {
39 | "be able to pass a simple load test" in {
40 | val combine = (m1: Message, m2: Message) => m1.appendToBody(" + %s" format m2.body)
41 | val route = appendToBody("-1") >=> scatter(
42 | appendToBody("-2") >=> appendToBody("-3"),
43 | appendToBody("-4") >=> appendToBody("-5"),
44 | appendToBody("-6") >=> appendToBody("-7")
45 | ).gather(combine) >=> appendToBody(" done")
46 |
47 | val count = 1000
48 | val latch = new CountDownLatch(count)
49 |
50 | 1 to count foreach { i =>
51 | route apply Message("a-%s" format i).success respond { mv =>
52 | mv must equal (Success(Message("a-%s-1-2-3 + a-%s-1-4-5 + a-%s-1-6-7 done" format (i, i, i))))
53 | if (i % 50 == 0) print(".")
54 | latch.countDown
55 | }
56 | }
57 | latch.await
58 | println
59 | }
60 | }
61 | }
62 |
63 | class CamelLoadTestConcurrentN extends CamelLoadTest {
64 | import java.util.concurrent.ThreadPoolExecutor
65 | import java.util.concurrent.ArrayBlockingQueue
66 | import java.util.concurrent.TimeUnit
67 |
68 | // ----------------------------------------------------------------
69 | // Relevant when testing with 1 million messages or more:
70 | // Executors need to use a bounded queue and a CallerRunsPolicy
71 | // to avoid an overly high memory consumption. A comparable
72 | // setting is recommended for production.
73 | // ----------------------------------------------------------------
74 |
75 | val executor1 = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue[Runnable](100), new ThreadPoolExecutor.CallerRunsPolicy)
76 | val executor2 = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue[Runnable](100), new ThreadPoolExecutor.CallerRunsPolicy)
77 | val executor3 = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue[Runnable](100), new ThreadPoolExecutor.CallerRunsPolicy)
78 |
79 | dispatchConcurrencyStrategy = Executor(register(executor1))
80 | multicastConcurrencyStrategy = Executor(register(executor2))
81 | processorConcurrencyStrategy = Executor(register(executor3))
82 | }
83 |
84 | class CamelLoadTestSequential extends CamelLoadTest
--------------------------------------------------------------------------------
/project/ScalazCamelBuild.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import Keys._
3 |
4 | object ScalazCamelBuild extends Build {
5 |
6 | lazy val buildSettings = Seq(
7 | organization := "scalaz.camel",
8 | version := "0.4-SNAPSHOT",
9 | scalaVersion := "2.9.2"
10 | )
11 |
12 | override lazy val settings = super.settings ++ buildSettings
13 | lazy val baseSettings = Defaults.defaultSettings
14 | lazy val defaultSettings = baseSettings ++ Seq(
15 | resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/",
16 | resolvers += "Akka Repository" at "http://akka.io/repository",
17 | // compile options
18 | scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked") ++ (
19 | if (true || (System getProperty "java.runtime.version" startsWith "1.7")) Seq() else Seq("-optimize")), // -optimize fails with jdk7
20 | javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation")
21 | )
22 |
23 | object Dependencies {
24 |
25 | object V {
26 | val Scalaz = "6.0.4"
27 | val Camel = "2.9.2"
28 | val Akka = "1.3"
29 | val ActiveMQ = "5.6.0"
30 | val Slf4j = "1.6.4"
31 | val ScalaTest = "1.7.2"
32 | val Junit = "4.8.2"
33 | }
34 |
35 | lazy val scalazCore = "org.scalaz" %% "scalaz-core" % V.Scalaz
36 | lazy val camelCore = "org.apache.camel" % "camel-core" % V.Camel
37 | lazy val camelJms = "org.apache.camel" % "camel-jms" % V.Camel
38 | lazy val cameHttp = "org.apache.camel" % "camel-http" % V.Camel
39 | lazy val camelJetty = "org.apache.camel" % "camel-jetty" % V.Camel
40 | lazy val camelSpring = "org.apache.camel" % "camel-spring" % V.Camel
41 | lazy val akkaCamel = "se.scalablesolutions.akka" % "akka-camel" % V.Akka
42 |
43 | lazy val activemqCore = "org.apache.activemq" % "activemq-core" % V.ActiveMQ
44 | lazy val slf4jSimple = "org.slf4j" % "slf4j-simple" % V.Slf4j
45 | lazy val scalatest = "org.scalatest" %% "scalatest" % V.ScalaTest
46 | lazy val junit = "junit" % "junit" % V.Junit
47 | }
48 |
49 | lazy val root = Project("root", file(".")) aggregate(core, actor, samples)
50 |
51 | lazy val core = Project(
52 | id = "scalaz-camel-core",
53 | base = file("scalaz-camel-core"),
54 | settings = defaultSettings ++ Seq(libraryDependencies ++= Seq(
55 | Dependencies.scalazCore % "compile", Dependencies.camelCore % "compile",
56 | Dependencies.camelJms % "test", Dependencies.cameHttp % "test",
57 | Dependencies.camelJetty % "test", Dependencies.camelSpring % "test",
58 | Dependencies.activemqCore % "test", Dependencies.slf4jSimple % "test",
59 | Dependencies.scalatest % "test", Dependencies.junit % "test"))
60 | )
61 |
62 | lazy val actor = Project(
63 | id = "scalaz-camel-akka",
64 | base = file("scalaz-camel-akka"),
65 | dependencies = Seq(core),
66 | settings = defaultSettings ++ Seq(libraryDependencies ++= Seq(
67 | Dependencies.camelCore % "compile", Dependencies.akkaCamel,
68 | Dependencies.scalatest % "test", Dependencies.slf4jSimple % "test"))
69 | )
70 |
71 | lazy val samples = Project(
72 | id = "scalaz-camel-samples",
73 | base = file("scalaz-camel-samples"),
74 | dependencies = Seq(core),
75 | settings = defaultSettings ++ Seq(libraryDependencies ++= Seq(
76 | Dependencies.scalazCore % "compile", Dependencies.camelCore % "compile",
77 | Dependencies.camelJetty % "test", Dependencies.camelSpring % "compile",
78 | Dependencies.camelJms % "test", Dependencies.camelJms % "test",
79 | Dependencies.camelJetty % "test", Dependencies.camelSpring % "compile",
80 | Dependencies.activemqCore % "test", Dependencies.slf4jSimple % "test",
81 | Dependencies.scalatest % "test", Dependencies.junit % "test"))
82 | )
83 |
84 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/MessageTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import java.io.InputStream
19 |
20 | import org.apache.camel.NoTypeConversionAvailableException
21 | import org.apache.camel.impl.DefaultCamelContext
22 | import org.scalatest.{WordSpec, BeforeAndAfterAll}
23 | import org.scalatest.matchers.MustMatchers
24 |
25 | /**
26 | * @author Martin Krasser
27 | */
28 | class MessageTest extends WordSpec with BeforeAndAfterAll with MustMatchers {
29 | implicit val router = new ContextMgnt {
30 | val context = new DefaultCamelContext
31 | }
32 |
33 | override protected def beforeAll = router.start
34 |
35 | def support = afterWord("support")
36 |
37 | "A message" must support {
38 | "body conversion to a specified type" in {
39 | Message(1.4).bodyAs[String] must equal("1.4")
40 | Message(1.4).bodyTo[String].body must equal("1.4")
41 | evaluating { Message(1.4).bodyAs[InputStream] } must produce [NoTypeConversionAvailableException]
42 | evaluating { Message(1.4).bodyTo[InputStream] } must produce [NoTypeConversionAvailableException]
43 | }
44 |
45 | "header conversion to a specified type" in {
46 | val message = Message("test" , Map("test" -> 1.4))
47 | message.headerAs[String]("test") must equal(Some("1.4"))
48 | message.headerAs[String]("blah") must equal(None)
49 | }
50 |
51 | "getting a header by name" in {
52 | val message = Message("test" , Map("test" -> 1.4))
53 | message.header("test") must equal(Some(1.4))
54 | message.header("blah") must equal(None)
55 | }
56 |
57 | "getting headers by a set of name" in {
58 | val message = Message("test" , Map("A" -> "1", "B" -> "2"))
59 | message.headers(Set("B")) must equal(Map("B" -> "2"))
60 | }
61 |
62 | "transformation of the body using a transformer function" in {
63 | val message = Message("a" , Map("A" -> "1"))
64 | message.transform[String](body => body + "b") must equal(Message("ab", Map("A" -> "1")))
65 | }
66 |
67 | "appending to the body using a string representation" in {
68 | val message = Message("a" , Map("A" -> "1"))
69 | message.appendToBody("b") must equal(Message("ab", Map("A" -> "1")))
70 | }
71 |
72 | "setting the body" in {
73 | val message = Message("a" , Map("A" -> "1"))
74 | message.setBody("b") must equal(Message("b", Map("A" -> "1")))
75 | }
76 |
77 | "setting a headers set" in {
78 | val message = Message("a" , Map("A" -> "1"))
79 | message.setHeaders(Map("C" -> "3")) must equal(Message("a", Map("C" -> "3")))
80 | }
81 |
82 | "adding a header" in {
83 | val message = Message("a" , Map("A" -> "1"))
84 | message.addHeader("B" -> "2") must equal(Message("a", Map("A" -> "1", "B" -> "2")))
85 | }
86 |
87 | "adding a headers set" in {
88 | val message = Message("a" , Map("A" -> "1"))
89 | message.addHeaders(Map("B" -> "2")) must equal(Message("a", Map("A" -> "1", "B" -> "2")))
90 | }
91 |
92 | "removing a header" in {
93 | val message = Message("a" , Map("A" -> "1", "B" -> "2"))
94 | message.removeHeader("B") must equal(Message("a", Map("A" -> "1")))
95 | }
96 |
97 | "setting an exception" in {
98 | val exception = new Exception("test")
99 | val message = Message("a").setException(exception)
100 | message.exception must equal(Some(exception))
101 | Message("a").exception must equal(None)
102 | }
103 |
104 | "clearing an exception" in {
105 | val exception = new Exception("test")
106 | val message = Message("a").setException(exception)
107 | message.exceptionHandled.exception must equal(None)
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/scalaz-camel-akka/src/main/scala/akka/actor/Sender.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package akka.actor
17 |
18 | import java.net.InetSocketAddress
19 | import java.util.concurrent.atomic.AtomicReference
20 |
21 | import akka.dispatch.{CompletableFuture, MessageDispatcher, MessageInvocation}
22 |
23 | import scalaz.camel.core.Conv._
24 | import scalaz.camel.core.Message
25 |
26 | /**
27 | * Sender used for communication with actors via !. Replies to this
28 | * sender are passed to continuation k.
29 | *
30 | * @author Martin Krasser
31 | */
32 | sealed class Sender(k: MessageValidation => Unit) extends ActorRef with ScalaActorRef {
33 | def start = { _status = akka.actor.ActorRefInternals.RUNNING; this }
34 | def stop = { _status = akka.actor.ActorRefInternals.SHUTDOWN }
35 |
36 | /**
37 | * Processes replies by passing it to continuation k. If the reply is of
38 | * MessageValidation then it is passed to k as isMessage then it is converted to either Success or
41 | * Failure (depending on the the message.exception value)
42 | * before passing to kSuccess(Message(body)) that
44 | * is then passed to kuri. The consumer is
35 | * registered at the CamelContext for lifecycle management. This method can be
36 | * used in context of both, started and not-yet-started CamelContext instances.
37 | *
38 | * @param uri endpoint URI.
39 | * @param p processor that is connected to the created endpoint consumer.
40 | */
41 | def createConsumer(uri: String, p: Processor): Consumer = {
42 | val service = context.asInstanceOf[ServiceSupport]
43 | val endpoint = context.getEndpoint(uri)
44 | val consumer = endpoint.createConsumer(p)
45 |
46 | context.addLifecycleStrategy(new LifecycleSync(consumer))
47 |
48 | if (service.isStarted) {
49 | endpoint.start // needed?
50 | consumer.start
51 | }
52 |
53 | consumer
54 | }
55 |
56 | /**
57 | * Creates a producer for endpoint defined by uri. The producer is
58 | * registered at the CamelContext for lifecycle management. This method can be
59 | * used used in context of both, started and not-yet-started CamelContext instances.
60 | *
61 | * @param uri endpoint URI.
62 | */
63 | def createProducer(uri: String): Producer = {
64 | val service = context.asInstanceOf[ServiceSupport]
65 | val endpoint = context.getEndpoint(uri)
66 | val producer = endpoint.createProducer
67 |
68 | context.addLifecycleStrategy(new LifecycleSync(producer))
69 |
70 | if (service.isStarted) {
71 | producer.start
72 | }
73 |
74 | producer
75 | }
76 | }
77 |
78 | /**
79 | * Manages a CamelContext
80 | *
81 | * @author Martin Krasser
82 | */
83 | trait ContextMgnt {
84 | /** Managed CamelContext */
85 | val context: CamelContext
86 |
87 | /** Starts the context */
88 | def start = context.start
89 | /** Stops the context */
90 | def stop = context.stop
91 | }
92 |
93 | /**
94 | * The Camel context and endpoint manager (implicitly) needed in context of routes.
95 | *
96 | * @author Martin Krasser
97 | */
98 | class Router(val context: CamelContext) extends EndpointMgnt with ContextMgnt {
99 | import org.apache.camel.component.direct.DirectComponent
100 |
101 | context.addComponent("direct", new DirectNostop)
102 |
103 | private class DirectNostop extends DirectComponent {
104 | override def doStop {}
105 | }
106 | }
107 |
108 | /**
109 | * @author Martin Krasser
110 | */
111 | private[camel] class LifecycleSync(service: Service) extends LifecycleStrategy {
112 | import org.apache.camel.spi.RouteContext
113 | import org.apache.camel.builder.ErrorHandlerBuilder
114 |
115 | def onContextStart(context: CamelContext) = service.start
116 | def onContextStop(context: CamelContext) = service.stop
117 |
118 | // no action by default
119 | def onThreadPoolAdd(camelContext: CamelContext, threadPool: ThreadPoolExecutor) = {}
120 | def onErrorHandlerAdd(routeContext: RouteContext, errorHandler: Processor, errorHandlerBuilder: ErrorHandlerBuilder) = {}
121 | def onRouteContextCreate(routeContext: RouteContext) = {}
122 | def onRoutesRemove(routes: Collection[Route]) = {}
123 | def onRoutesAdd(routes: Collection[Route]) = {}
124 | def onServiceRemove(context: CamelContext, service: Service, route: Route) = {}
125 | def onServiceAdd(context: CamelContext, service: Service, route: Route) = {}
126 | def onEndpointRemove(endpoint: Endpoint) = {}
127 | def onEndpointAdd(endpoint: Endpoint) = {}
128 | def onComponentRemove(name: String, component: Component) = {}
129 | def onComponentAdd(name: String, component: Component) = {}
130 | def onThreadPoolAdd(context: CamelContext, executor: ThreadPoolExecutor, s: String, s1: String, s2: String, s3: String) {}
131 |
132 | def onErrorHandlerAdd(routeContext: RouteContext, errorHandler: Processor, errorHandlerBuilder: ErrorHandlerFactory) {}
133 | }
134 |
--------------------------------------------------------------------------------
/scalaz-camel-akka/src/test/scala/scalaz/camel/akka/AkkaTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.akka
17 |
18 | import java.util.concurrent.{CountDownLatch, TimeUnit}
19 |
20 | import org.scalatest.{WordSpec, BeforeAndAfterAll}
21 | import org.scalatest.matchers.MustMatchers
22 |
23 | import akka.actor.Actor
24 | import akka.camel.{Message => Msg}
25 |
26 | import scalaz._
27 | import scalaz.concurrent.Strategy
28 |
29 | import scalaz.camel.core._
30 |
31 | /**
32 | * @author Martin Krasser
33 | */
34 | trait AkkaTest extends AkkaTestContext with WordSpec with MustMatchers with BeforeAndAfterAll {
35 | import Scalaz._
36 |
37 | override def beforeAll = router.start
38 | override def afterAll = router.stop
39 |
40 | def support = afterWord("support")
41 |
42 | "scalaz.camel.akka.Akka" should support {
43 | "1:1 in-out messaging with an actor that is accessed via the native API" in {
44 | val appender = Actor.actorOf(new AppendReplyActor("-2", 1))
45 | val route = appendToBody("-1") >=> to(appender.manage) >=> appendToBody("-3")
46 | route process Message("a") match {
47 | case Success(Message(body, _)) => body must equal("a-1-2-3")
48 | case _ => fail("unexpected response")
49 | }
50 | }
51 |
52 | "1:n in-out messaging with an actor that is accessed via the native API" in {
53 | val appender = Actor.actorOf(new AppendReplyActor("-2", 3))
54 | val route = appendToBody("-1") >=> to(appender.manage) >=> appendToBody("-3")
55 | val queue = route submitN Message("a")
56 | List(queue.take, queue.take, queue.take) foreach { e =>
57 | e match {
58 | case Success(Message(body, _)) => body must equal("a-1-2-3")
59 | case _ => fail("unexpected response")
60 | }
61 | }
62 | }
63 |
64 | "1:1 in-out messaging with an actor that is accessed via the Camel actor component" in {
65 | val greeter = Actor.actorOf(new GreetReplyActor)
66 | val route = appendToBody("-1") >=> to(greeter.manage.uri) >=> appendToBody("-3")
67 | route process Message("a") match {
68 | case Success(Message(body, _)) => body must equal("a-1-hello-3")
69 | case _ => fail("unexpected response")
70 | }
71 |
72 | }
73 |
74 | "in-only messaging with an actor that is accessed via the native API" in {
75 | val latch = new CountDownLatch(1)
76 | val appender = Actor.actorOf(new CountDownActor("a-1", latch))
77 | val route = appendToBody("-1") >=> oneway >=> to(appender.manage) >=> appendToBody("-3")
78 | route process Message("a") match {
79 | case Success(Message(body, _)) => body must equal("a-1-3")
80 | case _ => fail("unexpected response")
81 | }
82 | latch.await(5, TimeUnit.SECONDS) must be (true)
83 | }
84 |
85 | "in-only messaging with an actor that is accessed via the Camel actor component" in {
86 | val latch = new CountDownLatch(1)
87 | val appender = Actor.actorOf(new CountDownActor("a-1", latch))
88 | val route = appendToBody("-1") >=> oneway >=> to(appender.manage.uri) >=> appendToBody("-3")
89 | route process Message("a") match {
90 | case Success(Message(body, _)) => body must equal("a-1-3")
91 | case _ => fail("unexpected response")
92 | }
93 | latch.await(5, TimeUnit.SECONDS) must be (true)
94 | }
95 |
96 | "aggregation of messages" in {
97 | val f: AggregationFunction = (m1, m2) => m1.appendToBody(m2.body)
98 | val p: CompletionPredicate = m => m.bodyAs[String].length == 5
99 |
100 | val ms = for (i <- 1 to 5) yield Message("%s" format i)
101 |
102 | (aggregate using f until p) >=> appendToBody("-done") process ms match {
103 | case Success(Message(body: String, _)) => dispatchConcurrencyStrategy match {
104 | case Strategy.Sequential => body must equal ("12345-done")
105 | case _ => body.length must be (10)
106 | }
107 | case _ => fail("unexpected response")
108 | }
109 | }
110 | }
111 |
112 | class AppendReplyActor(s: String, c: Int) extends Actor {
113 | def receive = {
114 | case m: Message => for (_ <- 1 to c) self.reply(m.appendToBody(s))
115 | }
116 | }
117 |
118 | class GreetReplyActor extends Actor {
119 | def receive = {
120 | case Msg(body, _) => self.reply("%s-hello" format body)
121 | }
122 | }
123 |
124 | class CountDownActor(b: String, latch: CountDownLatch) extends Actor {
125 | def receive = {
126 | case Message(body, _) => if (body == b) latch.countDown
127 | case Msg(body, _) => if (body == b) latch.countDown
128 | }
129 | }
130 | }
131 |
132 | class AkkaTestSequential extends AkkaTest
133 | class AkkaTestConcurrent extends AkkaTest {
134 | dispatchConcurrencyStrategy = Strategy.Naive
135 | multicastConcurrencyStrategy = Strategy.Naive
136 | }
137 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/main/scala/scalaz/camel/core/Message.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.apache.camel.{Exchange, ExchangePattern, CamelContext, Message => CamelMessage}
19 |
20 | /**
21 | * An immutable representation of a Camel message.
22 | *
23 | * @author Martin Krasser
24 | */
25 | case class Message(body: Any, headers: Map[String, Any] = Map.empty) {
26 |
27 | private val SkipContextUpdate = "scalaz.camel.update.context.skip"
28 |
29 | // TODO: consider making Message a parameterized type
30 | // TODO: consider making Message an instance of Functor
31 |
32 | val context = MessageContext()
33 |
34 | override def toString = "Message: %s" format body
35 |
36 | def setBody(body: Any) = Message(body, headers, context)
37 |
38 | def setHeaders(headers: Map[String, Any]) = Message(body, headers, context)
39 |
40 | def addHeaders(headers: Map[String, Any]) = Message(body, this.headers ++ headers, context)
41 |
42 | def addHeader(header: (String, Any)) = Message(body, headers + header, context)
43 |
44 | def removeHeader(headerName: String) = Message(body, headers - headerName, context)
45 |
46 | def setContext(context: MessageContext) = Message(body, headers, context).setSkipContextUpdate(true)
47 |
48 | def setContextFrom(m: Message) = setContext(m.context)
49 |
50 | def setOneway(oneway: Boolean) = setContext(context.setOneway(oneway))
51 |
52 | def setException(e: Exception) = setContext(context.setException(Some(e)))
53 |
54 | def exception: Option[Exception] = context.exception
55 |
56 | def headers(names: Set[String]): Map[String, Any] = headers.filter(names contains _._1)
57 |
58 | def header(name: String): Option[Any] = headers.get(name)
59 |
60 | def headerAs[A](name: String)(implicit m: Manifest[A], mgnt: ContextMgnt): Option[A] =
61 | header(name).map(convertTo[A](m.erasure.asInstanceOf[Class[A]], mgnt.context) _)
62 |
63 | def bodyAs[A](implicit m: Manifest[A], mgnt: ContextMgnt): A =
64 | convertTo[A](m.erasure.asInstanceOf[Class[A]], mgnt.context)(body)
65 |
66 | // TODO: remove once Message is a Functor
67 | def bodyTo[A](implicit m: Manifest[A], mgnt: ContextMgnt): Message =
68 | Message(convertTo[A](m.erasure.asInstanceOf[Class[A]], mgnt.context)(body), headers, context)
69 |
70 | // TODO: remove once Message is a Functor
71 | def transform[A](transformer: A => Any)(implicit m: Manifest[A], mgnt: ContextMgnt) =
72 | setBody(transformer(bodyAs[A]))
73 |
74 | // TODO: remove once Message is a Functor
75 | def appendToBody(body: Any)(implicit mgnt: ContextMgnt) =
76 | setBody(bodyAs[String] + convertTo[String](classOf[String], mgnt.context)(body))
77 |
78 | private[camel] def exceptionHandled =
79 | Message(body, headers, context.setException(None))
80 |
81 | // Experimental
82 | private[camel] def setSkipContextUpdate(skip: Boolean) =
83 | if (skip) addHeader(SkipContextUpdate, skip) else removeHeader(SkipContextUpdate)
84 |
85 | // Experimental
86 | private[camel] def skipContextUpdate = header(SkipContextUpdate) match {
87 | case Some(v) => v.asInstanceOf[Boolean]
88 | case None => false
89 | }
90 |
91 | private def convertTo[A](c: Class[A], context: CamelContext)(a: Any): A =
92 | context.getTypeConverter.mandatoryConvertTo[A](c, a)
93 | }
94 |
95 | /**
96 | * An immutable representation of a Camel Exchange.
97 | *
98 | * @author Martin Krasser
99 | */
100 | case class MessageContext(oneway: Boolean, exception: Option[Exception]) {
101 | def setOneway(o: Boolean) = MessageContext(o, exception)
102 |
103 | def setException(e: Option[Exception]) = MessageContext(oneway, e)
104 | }
105 |
106 | /**
107 | * @author Martin Krasser
108 | */
109 | object Message {
110 | /** Creates a MessageConverter from a Camel message */
111 | implicit def camelMessageToConverter(cm: CamelMessage): MessageConverter = new MessageConverter(cm)
112 |
113 | /** Create a Message with body, headers and a message context */
114 | def apply(body: Any, headers: Map[String, Any], ctx: MessageContext): Message = new Message(body, headers) {
115 | override val context = ctx
116 | }
117 | }
118 |
119 | /**
120 | * @author Martin Krasser
121 | */
122 | object MessageContext {
123 | /** Creates a MessageContextConverter from a Camel exchange */
124 | implicit def camelExchangeToConverter(ce: Exchange) = new MessageContextConverter(ce)
125 |
126 | /** Create a default MessageContext with oneway set to false and no exception */
127 | def apply(): MessageContext = MessageContext(false, None)
128 | }
129 |
130 | /**
131 | * Converts between scalaz.camel.Message and org.apache.camel.Message.
132 | *
133 | * @author Martin Krasser
134 | */
135 | class MessageConverter(val cm: CamelMessage) {
136 | import scala.collection.JavaConversions._
137 | import MessageContext._
138 |
139 | def fromMessage(m: Message): CamelMessage = {
140 | cm.getExchange.fromMessageContext(m.context)
141 | cm.setBody(m.body)
142 | for (h <- m.headers) cm.getHeaders.put(h._1, h._2.asInstanceOf[AnyRef])
143 | cm
144 | }
145 |
146 | def toMessage: Message = toMessage(Map.empty)
147 | def toMessage(headers: Map[String, Any]): Message =
148 | Message(cm.getBody, cmHeaders(cm, headers), cm.getExchange.toMessageContext)
149 |
150 | private def cmHeaders(cm: CamelMessage, headers: Map[String, Any]) = headers ++ cm.getHeaders
151 | }
152 |
153 | /**
154 | * Converts between scalaz.camel.MessageContext and org.apache.camel.Exchange.
155 | *
156 | * @author Martin Krasser
157 | */
158 | class MessageContextConverter(val ce: Exchange) {
159 | def fromMessageContext(me: MessageContext) = {
160 | ce.setPattern(if (me.oneway) ExchangePattern.InOnly else ExchangePattern.InOut)
161 | ce.setException(me.exception match {
162 | case Some(e) => e
163 | case None => null
164 | })
165 | }
166 |
167 | def toMessageContext = MessageContext(
168 | if (ce.getPattern.isOutCapable) false else true,
169 | if (ce.getException == null) None else Some(ce.getException)
170 | )
171 | }
--------------------------------------------------------------------------------
/scalaz-camel-core/src/main/scala/scalaz/camel/core/Conv.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.apache.camel.{AsyncCallback, AsyncProcessor, Exchange, Processor}
19 |
20 | import scalaz._
21 |
22 | /**
23 | * Provides converters for constructing message processing routes from
24 | *
25 | * MessageProcessor)Camel object.
34 | *
35 | * @author Martin Krasser
36 | */
37 | trait Conv {
38 | import scalaz.concurrent.Strategy
39 | import Scalaz._
40 | import Message._
41 |
42 | type MessageValidation = Conv.MessageValidation
43 | type MessageProcessor = Conv.MessageProcessor
44 | type MessageRoute = Conv.MessageRoute
45 |
46 | /**
47 | * Set the message context of m2 on m1 unless an context update should be skipped.
48 | */
49 | private val updateContext = (m1: Message) => (m2: Message) =>
50 | (if (!m1.skipContextUpdate) m1.setContextFrom(m2) else m1).setSkipContextUpdate(false)
51 |
52 | /**
53 | * Concurrency strategy for dispatching messages along the processor chain (i.e. route).
54 | */
55 | protected def dispatchStrategy: Strategy
56 |
57 | /**
58 | * Semigroup to 'append' failure messages. Returns the first failure message and ignores
59 | * the second. Needed for applicative usage of MessageValidation.
60 | */
61 | implicit def ExceptionSemigroup: Semigroup[Message] = semigroup((m1, m2) => m1)
62 |
63 | /**
64 | * A continuation monad for constructing routes from CPS message processors. It applies the
65 | * concurrency strategy returned by dispatchStrategy for dispatching messages
66 | * along the processor chain (i.e. route). Success messages are dispatched to the next processor
67 | * which in turn passes its result to continuation k. Failure messages are passed
68 | * directly to continuation k (by-passing the remaining processors in the chain).
69 | */
70 | class MessageValidationResponder(v: MessageValidation, p: MessageProcessor) extends Responder[MessageValidation] {
71 | def respond(k: MessageValidation => Unit) = v match {
72 | case Success(m) => dispatchStrategy.apply(p(m, r => k(v <*> r ∘ updateContext /* experimental */)))
73 | case Failure(m) => dispatchStrategy.apply(k(Failure(m)))
74 | }
75 | }
76 |
77 | /**
78 | * Creates a message processing route component from a CPS message processor
79 | */
80 | def messageRoute(p: MessageProcessor): MessageRoute =
81 | kleisli((v: MessageValidation) => new MessageValidationResponder(v, p).map(r => r))
82 |
83 | /**
84 | * Creates a CPS message processor from a message processing route
85 | */
86 | def messageProcessor(p: MessageRoute): MessageProcessor =
87 | (m: Message, k: MessageValidation => Unit) => p apply m.success respond k
88 |
89 | /**
90 | * Creates a CPS message processor from a direct-style message processor. The created
91 | * CPS processor executes the direct-style message processor synchronously.
92 | */
93 | def messageProcessor(p: Message => Message): MessageProcessor =
94 | messageProcessor(p, Strategy.Sequential)
95 |
96 | /**
97 | * Creates an CPS message processor from a direct-style message processor. The created
98 | * CPS processor executes the direct-style processor using the concurrency strategy
99 | * s.
100 | */
101 | def messageProcessor(p: Message => Message, s: Strategy): MessageProcessor = (m: Message, k: MessageValidation => Unit) =>
102 | s.apply { try { k(p(m).success) } catch { case e: Exception => k(m.setException(e).fail) } }
103 |
104 | /**
105 | * Creates a CPS message processor from a Camel message producer obtained from an endpoint
106 | * defined by URI. This method registers the created producer at the Camel context for
107 | * lifecycle management.
108 | */
109 | def messageProcessor(uri: String, em: EndpointMgnt, cm: ContextMgnt): MessageProcessor =
110 | messageProcessor(em.createProducer(uri), cm)
111 |
112 | /**
113 | * Creates a CPS message processor from a (synchronous or asynchronous) Camel processor.
114 | */
115 | def messageProcessor(p: Processor, cm: ContextMgnt): MessageProcessor =
116 | if (p.isInstanceOf[AsyncProcessor]) messageProcessor(p.asInstanceOf[AsyncProcessor], cm)
117 | else messageProcessor(new ProcessorAdapter(p), cm)
118 |
119 | /**
120 | * Creates a CPS message processor from an asynchronous Camel processor.
121 | */
122 | def messageProcessor(p: AsyncProcessor, cm: ContextMgnt): MessageProcessor = (m: Message, k: MessageValidation => Unit) => {
123 | import org.apache.camel.impl.DefaultExchange
124 |
125 | val me = new DefaultExchange(cm.context)
126 |
127 | me.getIn.fromMessage(m)
128 |
129 | p.process(me, new AsyncCallback {
130 | def done(doneSync: Boolean) =
131 | if (me.isFailed)
132 | k(resultMessage(me).fail)
133 | else
134 | k(resultMessage(me).success)
135 |
136 | private def resultMessage(me: Exchange) = {
137 | val rm = if (me.hasOut) me.getOut else me.getIn
138 | me.setOut(null)
139 | me.setIn(rm)
140 | rm.toMessage
141 | }
142 | })
143 | }
144 |
145 | /**
146 | * An AsyncProcessor interface for a (synchronous) Camel Processor.
147 | */
148 | private class ProcessorAdapter(p: Processor) extends AsyncProcessor {
149 | def process(exchange: Exchange) = throw new UnsupportedOperationException()
150 | def process(exchange: Exchange, callback: AsyncCallback) = {
151 | try {
152 | p.process(exchange)
153 | } catch {
154 | case e: Exception => exchange.setException(e)
155 | }
156 | callback.done(true)
157 | true
158 | }
159 | }
160 | }
161 |
162 | object Conv {
163 | /**
164 | * Type of a failed or successful response message. Will be replaced by
165 | * Either[Message, Message] with scalaz versions greater than 5.0.
166 | */
167 | type MessageValidation = Validation[Message, Message]
168 |
169 | /**
170 | * Type of a (potentially asynchronous) message processor that passes a message validation
171 | * result to a continuation of type MessageValidation => Unit. A CPS message
172 | * processor.
173 | */
174 | type MessageProcessor = (Message, MessageValidation => Unit) => Unit
175 |
176 | /**
177 | * Type of a message processing route or a single route component. These can be composed
178 | * via Kleisli composition.
179 | */
180 | type MessageRoute = Kleisli[Responder, MessageValidation, MessageValidation]
181 | }
--------------------------------------------------------------------------------
/scalaz-camel-samples/src/main/scala/scalaz/camel/samples/CoreExample.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.samples
17 |
18 | import scalaz._
19 | import scalaz.camel.core._
20 | import scalaz.concurrent.Strategy
21 |
22 | // Oversimplified purchase order domain model
23 | case class PurchaseOrder(items: List[PurchaseOrderItem])
24 | case class PurchaseOrderItem(customer: Int, category: String, name: String, amount: Int)
25 | case class ValidationException(msg: String) extends Exception(msg)
26 |
27 | /**
28 | * @author Martin Krasser
29 | */
30 | object CoreExample extends Camel {
31 | import Scalaz._
32 |
33 | def main(args: Array[String]) = run
34 |
35 | def run: Unit = {
36 |
37 | // -----------------------------------------------------------------
38 | // Application setup
39 | // -----------------------------------------------------------------
40 |
41 | val executor = java.util.concurrent.Executors.newFixedThreadPool(3)
42 |
43 | // use a custom concurrency strategy for routing
44 | // messages along the message processing chain(s)
45 | dispatchConcurrencyStrategy = Strategy.Executor(executor)
46 |
47 | // setup Camel context and producer template
48 | import org.apache.camel.spring.SpringCamelContext._
49 | val context = springCamelContext("/context.xml")
50 | val template = context.createProducerTemplate
51 |
52 | // setup and start router
53 | implicit val router = new Router(context); router.start
54 |
55 | // -----------------------------------------------------------------
56 | // Application-specific message processors
57 | // -----------------------------------------------------------------
58 |
59 | // Continuation-passing style (CPS) message processor that validates order messages
60 | // in a separate thread (created by Strategy.Naive). Validation responses are sent
61 | // asynchronously via k.
62 | val validateOrder: MessageProcessor = (m: Message, k: MessageValidation => Unit) => {
63 | Strategy.Naive.apply(m.body match {
64 | case order: PurchaseOrder if (!order.items.isEmpty) => k(m.success)
65 | case _ => k(m.setException(ValidationException("invalid order")).fail)
66 | })
67 | }
68 |
69 | // Direct-style message processor that transforms an order item to a tuple. Synchronous processor.
70 | val orderItemToTuple = (m: Message) => m.transform[PurchaseOrderItem](i => (i.customer, i.name, i.amount))
71 |
72 | // -----------------------------------------------------------------
73 | // Route definitions (Kleisli composition of message processors)
74 | // -----------------------------------------------------------------
75 |
76 | // order placement route (main route)
77 | val placeOrderRoute = validateOrder >=> oneway >=> to("jms:queue:valid") >=> { m: Message => m.setBody("order accepted") }
78 |
79 | // order placement route consuming from direct:place-order endpoint (incl. error handler)
80 | from("direct:place-order") {
81 | attempt { placeOrderRoute } fallback {
82 | case e: ValidationException => { m: Message => m.setBody("order validation failed")}
83 | case e: Exception => { m: Message => m.setBody("general processing error")} >=> failWith(e)
84 | }
85 | }
86 |
87 | // order processing route
88 | from("jms:queue:valid") {
89 | split { m: Message => for (item <- m.bodyAs[PurchaseOrder].items) yield m.setBody(item) } >=> choose {
90 | case Message(PurchaseOrderItem(_, "books", _, _), _) => orderItemToTuple >=> to("mock:books")
91 | case Message(PurchaseOrderItem(_, "bikes", _, _), _) => to("mock:bikes")
92 | } >=> { m: Message => println("received order item = %s" format m.body); m }
93 | }
94 |
95 | // -----------------------------------------------------------------
96 | // Route application
97 | // -----------------------------------------------------------------
98 |
99 | import CoreExampleAsserts.assertOrderProcessed
100 |
101 | val order = PurchaseOrder(List(
102 | PurchaseOrderItem(123, "books", "Camel in Action", 1),
103 | PurchaseOrderItem(123, "books", "DSLs in Action", 1),
104 | PurchaseOrderItem(123, "bikes", "Canyon Torque FRX", 1)
105 | ))
106 |
107 | val orderMessage = Message(order)
108 |
109 |
110 | // usage of producer template (requestBody blocks)
111 | {
112 | assertOrderProcessed { Message(template.requestBody("direct:place-order", order)).success }
113 | assert(template.requestBody("direct:place-order", "wrong") == "order validation failed")
114 | }
115 |
116 | // usage of process (process blocks)
117 | {
118 | assertOrderProcessed { placeOrderRoute process orderMessage }
119 | placeOrderRoute process Message("wrong") match {
120 | case Failure(m) => assert(m.body == "wrong")
121 | case Success(m) => throw new Exception("unexpected success")
122 | }
123 | }
124 |
125 | // usage of submit (submit does not block, promise.get blocks)
126 | {
127 | implicit val strategy = Strategy.Naive // needed for creation of promise
128 | assertOrderProcessed { val promise = placeOrderRoute submit orderMessage; promise.get }
129 | }
130 |
131 | // continuation-passing style, CPS (respond does not block, latch.await blocks)
132 | {
133 | import java.util.concurrent.CountDownLatch
134 | import java.util.concurrent.TimeUnit
135 |
136 | assertOrderProcessed {
137 | var result: MessageValidation = Message("no response").fail
138 | val latch = new CountDownLatch(1)
139 |
140 | placeOrderRoute apply orderMessage.success respond { mv => result = mv; latch.countDown }
141 | latch.await(10, TimeUnit.SECONDS)
142 |
143 | result
144 | }
145 | }
146 |
147 | // -----------------------------------------------------------------
148 | // Application shutdown
149 | // -----------------------------------------------------------------
150 |
151 | router.stop
152 | executor.shutdownNow
153 | }
154 | }
155 |
156 | /**
157 | * @author Martin Krasser
158 | */
159 | object CoreExampleAsserts {
160 | import org.apache.camel.component.mock.MockEndpoint
161 |
162 | def assertOrderProcessed(validation: => Validation[Message, Message])(implicit cm: ContextMgnt) {
163 | val books = cm.context.getEndpoint("mock:books", classOf[MockEndpoint])
164 | val bikes = cm.context.getEndpoint("mock:bikes", classOf[MockEndpoint])
165 |
166 | books.reset
167 | bikes.reset
168 |
169 | books.expectedBodiesReceivedInAnyOrder((123, "Camel in Action", 1), (123, "DSLs in Action", 1))
170 | bikes.expectedBodiesReceived(PurchaseOrderItem(123, "bikes" , "Canyon Torque FRX", 1))
171 |
172 | validation match {
173 | case Success(m) => assert(m.body == "order accepted")
174 | case Failure(m) => throw new Exception("unexpected failure")
175 | }
176 |
177 | books.assertIsSatisfied
178 | bikes.assertIsSatisfied
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelAttemptTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.scalatest.{WordSpec, BeforeAndAfterAll, BeforeAndAfterEach}
19 | import org.scalatest.matchers.MustMatchers
20 |
21 | import scalaz._
22 | import scalaz.concurrent.Strategy
23 |
24 | /**
25 | * @author Martin Krasser
26 | */
27 | trait CamelAttemptTest extends CamelTestContext with WordSpec with MustMatchers with BeforeAndAfterAll with BeforeAndAfterEach {
28 | import Scalaz._
29 |
30 | override def beforeAll = router.start
31 | override def afterAll = router.stop
32 | override def afterEach = mocks.values.foreach { m => m.reset }
33 |
34 | def support = afterWord("support")
35 |
36 | "scalaz.camel.core.Camel" should support {
37 | "single routing attempts with multiple error handling routes" in {
38 | val p: Message => Message = (m: Message) => {
39 | m.body match {
40 | case "a-0" => throw new TestException1("failure1")
41 | case "b-0" => throw new TestException2("failure2")
42 | case _ => throw new Exception("failure")
43 | }
44 | }
45 |
46 | from("direct:test-2") {
47 | attempt {
48 | appendToBody("-0") >=> p
49 | } fallback {
50 | case e: TestException1 => appendToBody("-1")
51 | case e: TestException2 => appendToBody("-2")
52 | case e: Exception => appendToBody("-3")
53 | }
54 | }
55 |
56 | template.requestBody("direct:test-2", "a") must equal ("a-0-1")
57 | template.requestBody("direct:test-2", "b") must equal ("b-0-2")
58 | template.requestBody("direct:test-2", "c") must equal ("c-0-3")
59 | }
60 |
61 | "single routing attempts including reporting of causing exception" in {
62 | from("direct:test-3") {
63 | attempt {
64 | failWith(new TestException1("failed"))
65 | } fallback {
66 | case e: Exception => to("mock:mock") >=> failWith(e) /* causes producer template to throw exception */
67 | }
68 | }
69 |
70 | mock("mock").expectedBodiesReceived("a")
71 |
72 | try {
73 | template.requestBody("direct:test-3", "a")
74 | fail("exception expected")
75 | } catch {
76 | case e: Exception => {
77 | val cause = e.getCause
78 | cause.isInstanceOf[TestException1] must be (true)
79 | cause.getMessage must equal ("failed")
80 | }
81 | }
82 |
83 | mock("mock").assertIsSatisfied
84 | }
85 |
86 | "single routing attempts with failures in error handlers" in {
87 | from("direct:test-4a") {
88 | attempt {
89 | failWithMessage("failed")
90 | } fallback {
91 | case e: Exception => failWithMessage("x")
92 | }
93 | }
94 |
95 | from("direct:test-4b") {
96 | attempt {
97 | failWithMessage("failed")
98 | } fallback {
99 | case e: Exception => failWithMessage("x")
100 | }
101 | }
102 |
103 | try {
104 | template.requestBody("direct:test-4a", "a")
105 | fail("exception expected")
106 | } catch {
107 | case e: Exception => e.getCause.getMessage must equal ("x")
108 | }
109 |
110 | try {
111 | template.requestBody("direct:test-4b", "a")
112 | fail("exception expected")
113 | } catch {
114 | case e: Exception => e.getCause.getMessage must equal ("x")
115 | }
116 | }
117 |
118 | "multiple routing attempts with the original message" in {
119 | val route = appendToBody("-1") >=> attempt(3) {
120 | appendToBody("-2") >=> to("mock:mock") >=> failWith(new TestException1("error"))
121 | }.fallback {
122 | case (e, s) => orig(s) >=> retry(s)
123 | } >=> appendToBody("-3")
124 |
125 | mock("mock").expectedBodiesReceived("a-1-2", "a-1-2", "a-1-2")
126 | route process Message("a") match {
127 | case Failure(m) => m.body must equal("a-1-2")
128 | case _ => fail("failure response expected")
129 | }
130 | mock("mock").assertIsSatisfied
131 | }
132 |
133 | "multiple routing attempts with a modified message" in {
134 | val route = appendToBody("-1") >=> attempt(2) {
135 | appendToBody("-2") >=> to("mock:mock") >=> failWith(new TestException1("error"))
136 | }.fallback {
137 | case (e, s) => orig(s) >=> appendToBody("-m") >=> retry(s)
138 | } >=> appendToBody("-3")
139 |
140 | mock("mock").expectedBodiesReceivedInAnyOrder("a-1-2", "a-1-m-2")
141 | route process Message("a") match {
142 | case Failure(m) => m.body must equal("a-1-m-2")
143 | case _ => fail("failure response expected")
144 | }
145 | mock("mock").assertIsSatisfied
146 | }
147 |
148 | "multiple routing attempts with the latest message" in {
149 | val route = appendToBody("-1") >=> attempt(3) {
150 | appendToBody("-2") >=> to("mock:mock") >=> failWith(new TestException1("error"))
151 | }.fallback {
152 | case (e, s) => appendToBody("-m") >=> retry(s)
153 | } >=> appendToBody("-3")
154 |
155 | mock("mock").expectedBodiesReceivedInAnyOrder("a-1-2", "a-1-2-m-2", "a-1-2-m-2-m-2")
156 | route process Message("a") match {
157 | case Failure(m) => m.body must equal("a-1-2-m-2-m-2")
158 | case _ => fail("failure response expected")
159 | }
160 | mock("mock").assertIsSatisfied
161 | }
162 |
163 | "multiple routing attempts that succeed after retry" in {
164 | val conditionalFailure: Message => Message = (m: Message) => {
165 | if (m.body == "a-1-2") throw new TestException1("error") else m
166 | }
167 |
168 | val route = appendToBody("-1") >=> attempt(3) {
169 | appendToBody("-2") >=> to("mock:mock") >=> printMessage >=> conditionalFailure
170 | }.fallback {
171 | case (e, s) => retry(s)
172 | } >=> appendToBody("-3")
173 |
174 | mock("mock").expectedBodiesReceivedInAnyOrder("a-1-2", "a-1-2-2")
175 | route process Message("a") match {
176 | case Success(m) => m.body must equal("a-1-2-2-3")
177 | case _ => fail("success response expected")
178 | }
179 | mock("mock").assertIsSatisfied
180 | }
181 | }
182 |
183 | class TestException1(msg: String) extends Exception(msg)
184 | class TestException2(msg: String) extends Exception(msg)
185 | }
186 |
187 | class CamelAttemptTestSequential extends CamelAttemptTest
188 | class CamelAttemptTestConcurrent extends CamelAttemptTest with ExecutorMgnt {
189 | import java.util.concurrent.Executors
190 |
191 | dispatchConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
192 | multicastConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
193 | processorConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
194 |
195 | override def afterAll = {
196 | shutdown
197 | super.afterAll
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/test/scala/scalaz/camel/core/CamelTest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import org.scalatest.{WordSpec, BeforeAndAfterAll, BeforeAndAfterEach}
19 | import org.scalatest.matchers.MustMatchers
20 |
21 | import scalaz._
22 | import scalaz.concurrent.Strategy
23 |
24 | /**
25 | * @author Martin Krasser
26 | */
27 | trait CamelTest extends CamelTestContext with WordSpec with MustMatchers with BeforeAndAfterAll with BeforeAndAfterEach {
28 | import Scalaz._
29 |
30 | override def beforeAll = {
31 | from("direct:predef-1") { appendToBody("-p1") }
32 | from("direct:predef-2") { appendToBody("-p2") }
33 | router.start
34 | }
35 |
36 | override def afterAll = router.stop
37 | override def afterEach = mocks.values.foreach { m => m.reset }
38 |
39 | def support = afterWord("support")
40 |
41 | "scalaz.camel.core.Camel" should support {
42 |
43 | "Kleisli composition of CPS message processors" in {
44 | appendToBody("-1") >=> appendToBody("-2") process Message("a") must equal(Success(Message("a-1-2")))
45 | }
46 |
47 | "Kleisli composition of direct-style message processors" in {
48 | ds_appendToBody("-1") >=> ds_appendToBody("-2") process Message("a") must equal(Success(Message("a-1-2")))
49 | }
50 |
51 | "Kleisli composition of asynchonous Camel message processors" in {
52 | repeatBody >=> repeatBody process Message("a") must equal(Success(Message("aaaa")))
53 | }
54 |
55 | "Kleisli composition of synchonous Camel message processors" in {
56 | repeatBody.sp >=> repeatBody.sp process Message("a") must equal(Success(Message("aaaa")))
57 | }
58 |
59 | "Kleisli composition of Camel endpoint producers" in {
60 | to("direct:predef-1") >=> to("direct:predef-2") process Message("a") must equal(Success(Message("a-p1-p2")))
61 | }
62 |
63 | "Kleisli composition of different types of message processors" in {
64 | repeatBody >=> repeatBody.sp >=> appendToBody("-1") >=> ds_appendToBody("-2") >=> to("direct:predef-1") process
65 | Message("a") must equal(Success(Message("aaaa-1-2-p1")))
66 | }
67 |
68 | "Kleisli composition of CPS processors defined inline" in {
69 | val route = appendToBody("-1") >=> { (m: Message, k: MessageValidation => Unit) => k(m.appendToBody("-x").success) }
70 | route process Message("a") must equal(Success(Message("a-1-x")))
71 | }
72 |
73 | "Kleisli composition of direct-style processors defined inline" in {
74 | val route = appendToBody("-1") >=> { m: Message => m.appendToBody("-y") }
75 | route process Message("a") must equal(Success(Message("a-1-y")))
76 | }
77 |
78 | "failure reporting with CPS processors" in {
79 | failWithMessage("1") >=> failWithMessage("2") process Message("a") match {
80 | case Success(_) => fail("Failure result expected")
81 | case Failure(m: Message) => m.exception match {
82 | case Some(e: Exception) => e.getMessage must equal("1")
83 | case None => fail("no exception set for message")
84 | }
85 | }
86 | }
87 |
88 | "failure reporting with direct-style processors (that throw exceptions)" in {
89 | ds_failWithMessage("1") >=> ds_failWithMessage("2") process Message("a") match {
90 | case Success(_) => fail("Failure result expected")
91 | case Failure(m: Message) => m.exception match {
92 | case Some(e: Exception) => e.getMessage must equal("1")
93 | case None => fail("no exception set for message")
94 | }
95 | }
96 | }
97 |
98 | "application of routes using promises" in {
99 | // With the 'Sequential' strategy, routing will be started in the current
100 | // thread but processing may continue in another thread depending on the
101 | // concurrency strategy used for dispatcher and processors.
102 | implicit val strategy = Strategy.Sequential
103 |
104 | val promise = appendToBody("-1") >=> appendToBody("-2") submit Message("a")
105 |
106 | promise.get match {
107 | case Success(m) => m.body must equal("a-1-2")
108 | case Failure(m) => fail("unexpected failure")
109 | }
110 | }
111 |
112 | "application of routes using response queues" in {
113 | val queue = appendToBody("-1") >=> appendToBody("-2") submitN Message("a")
114 |
115 | queue.take match {
116 | case Success(m) => m.body must equal("a-1-2")
117 | case Failure(m) => fail("unexpected failure")
118 | }
119 | }
120 |
121 | "application of routes using continuation-passing style (CPS)" in {
122 | val queue = new java.util.concurrent.LinkedBlockingQueue[MessageValidation](10)
123 | appendToBody("-1") >=> appendToBody("-2") apply Message("a").success respond { mv => queue.put(mv) }
124 | queue.take match {
125 | case Success(m) => m.body must equal("a-1-2")
126 | case Failure(m) => fail("unexpected failure")
127 | }
128 | }
129 |
130 | "message comsumption from endpoints" in {
131 | from("direct:test-1") { appendToBody("-1") >=> appendToBody("-2") }
132 | template.requestBody("direct:test-1", "test") must equal ("test-1-2")
133 | }
134 |
135 | "content-based routing" in {
136 | from("direct:test-10") {
137 | appendToBody("-1") >=> choose {
138 | case Message("a-1", _) => appendToBody("-2") >=> appendToBody("-3")
139 | case Message("b-1", _) => appendToBody("-4") >=> appendToBody("-5")
140 | } >=> appendToBody("-done")
141 | }
142 | template.requestBody("direct:test-10", "a") must equal ("a-1-2-3-done")
143 | template.requestBody("direct:test-10", "b") must equal ("b-1-4-5-done")
144 | template.requestBody("direct:test-10", "c") must equal ("c-1-done")
145 | }
146 |
147 | "scatter-gather" in {
148 | val combine = (m1: Message, m2: Message) => m1.appendToBody(" + %s" format m2.body)
149 |
150 | from("direct:test-11") {
151 | appendToBody("-1") >=> scatter(
152 | appendToBody("-2") >=> appendToBody("-3"),
153 | appendToBody("-4") >=> appendToBody("-5"),
154 | appendToBody("-6") >=> appendToBody("-7")
155 | ).gather(combine) >=> appendToBody(" done")
156 | }
157 |
158 | template.requestBody("direct:test-11", "a") must equal ("a-1-2-3 + a-1-4-5 + a-1-6-7 done")
159 | }
160 |
161 | "scatter-gather that fails if one of the recipients fail" in {
162 | val combine = (m1: Message, m2: Message) => m1.appendToBody(" + %s" format m2.body)
163 |
164 | from("direct:test-12") {
165 | appendToBody("-1") >=> scatter(
166 | appendToBody("-2") >=> failWithMessage("x"),
167 | appendToBody("-4") >=> failWithMessage("y")
168 | ).gather(combine) >=> appendToBody(" done")
169 | }
170 |
171 | try {
172 | template.requestBody("direct:test-12", "a")
173 | fail("exception expected")
174 | } catch {
175 | case e: Exception => {
176 | // test passed but reported exception message can be either 'x'
177 | // or 'y' if message is distributed to destination concurrently.
178 | // For sequential multicast (or a when using a single-threaded
179 | // executor for multicast) then exception message 'x' will always
180 | // be reported first.
181 | if (multicastConcurrencyStrategy == Strategy.Sequential)
182 | e.getCause.getMessage must equal ("x")
183 | }
184 | }
185 | }
186 |
187 | "usage of routes inside message processors" in {
188 | // CPS message processor doing CPS application of route
189 | val composite1: MessageProcessor = (m: Message, k: MessageValidation => Unit) =>
190 | appendToBody("-n1") >=> appendToBody("-n2") apply m.success respond k
191 |
192 | // direct-style message processor (blocks contained until route generated response)
193 | val composite2: Message => Message = (m: Message) =>
194 | appendToBody("-n3") >=> appendToBody("-n4") process m match {
195 | case Success(m) => m
196 | case Failure(m) => throw m.exception.get
197 | }
198 |
199 | from("direct:test-20") {
200 | composite1 >=> composite2
201 | }
202 |
203 | template.requestBody("direct:test-20", "test") must equal("test-n1-n2-n3-n4")
204 | }
205 |
206 | "custom scatter-gather using for-comprehensions and promises" in {
207 | // needed for creation of response promise (can be any
208 | // other strategy as well such as Sequential or ...)
209 | implicit val strategy = Strategy.Naive
210 |
211 | // input message to destination routes
212 | val input = Message("test")
213 |
214 | // custom scatter-gather
215 | val promise = for {
216 | a <- appendToBody("-1") >=> appendToBody("-2") submit input
217 | b <- appendToBody("-3") >=> appendToBody("-4") submit input
218 | } yield a |@| b apply { (m1: Message, m2: Message) => m1.appendToBody(" + %s" format m2.body) }
219 |
220 | promise.get must equal(Success(Message("test-1-2 + test-3-4")))
221 | }
222 |
223 | "multicast" in {
224 | from("direct:test-30") {
225 | appendToBody("-1") >=> multicast(
226 | appendToBody("-2") >=> to("mock:mock1"),
227 | appendToBody("-3") >=> to("mock:mock1")
228 | ) >=> appendToBody("-done") >=> to("mock:mock2")
229 | }
230 |
231 | mock("mock1").expectedBodiesReceivedInAnyOrder("a-1-2" , "a-1-3")
232 | mock("mock2").expectedBodiesReceivedInAnyOrder("a-1-2-done", "a-1-3-done")
233 |
234 | template.sendBody("direct:test-30", "a")
235 |
236 | mock("mock1").assertIsSatisfied
237 | mock("mock2").assertIsSatisfied
238 | }
239 |
240 | "multicast with a failing destination" in {
241 | from("direct:test-31") {
242 | attempt {
243 | appendToBody("-1") >=> multicast(
244 | appendToBody("-2"),
245 | appendToBody("-3") >=> failWithMessage("-fail")
246 | ) >=> appendToBody("-done") >=> to("mock:mock")
247 | } fallback {
248 | case e: Exception => appendToBody(e.getMessage) >=> to("mock:error")
249 | }
250 | }
251 |
252 | mock("mock").expectedBodiesReceived("a-1-2-done")
253 | mock("error").expectedBodiesReceived("a-1-3-fail")
254 |
255 | template.sendBody("direct:test-31", "a")
256 |
257 | mock("mock").assertIsSatisfied
258 | mock("error").assertIsSatisfied
259 | }
260 |
261 | "splitting of messages" in {
262 | val splitLogic = (m: Message) => for (i <- 1 to 3) yield { m.appendToBody("-%s" format i) }
263 |
264 | from("direct:test-35") { split(splitLogic) >=> appendToBody("-done") >=> to("mock:mock") }
265 |
266 | mock("mock").expectedBodiesReceivedInAnyOrder("a-1-done", "a-2-done", "a-3-done")
267 |
268 | template.sendBody("direct:test-35", "a")
269 |
270 | mock("mock").assertIsSatisfied
271 | }
272 |
273 | "aggregation of messages" in {
274 |
275 | // Waits for three messages with a 'keep' header.
276 | // At arrival of the third message, a new Message
277 | // with body 'aggregated' is returned.
278 | def waitFor(count: Int) = {
279 | val counter = new java.util.concurrent.atomic.AtomicInteger(0)
280 | (m: Message) => {
281 | m.header("keep") match {
282 | case None => Some(m)
283 | case Some(_) => if (counter.incrementAndGet == count) Some(Message("aggregated")) else None
284 | }
285 | }
286 | }
287 |
288 | from("direct:test-40") {
289 | aggregate(waitFor(3)) >=> to("mock:mock")
290 | }
291 |
292 | mock("mock").expectedBodiesReceivedInAnyOrder("aggregated", "not aggregated")
293 |
294 | // only third message will make the aggregator to send a response
295 | for (i <- 1 to 5) template.sendBodyAndHeader("direct:test-40", "a", "keep", true)
296 |
297 | // ignored by aggregator and forwarded as-is
298 | template.sendBody("direct:test-40", "not aggregated")
299 |
300 | mock("mock").assertIsSatisfied
301 | }
302 |
303 | "filtering of messages" in {
304 | from("direct:test-45") {
305 | filter(_.body == "ok") >=> to("mock:mock")
306 | }
307 |
308 | mock("mock").expectedBodiesReceived("ok")
309 |
310 | template.sendBody("direct:test-45", "filtered")
311 | template.sendBody("direct:test-45", "ok")
312 |
313 | mock("mock").assertIsSatisfied
314 | }
315 |
316 | "sharing of routes" in {
317 |
318 | // nothing specific to scalaz-camel
319 | // just demonstrates function reuse
320 |
321 | val r = appendToBody("-1") >=> appendToBody("-2")
322 |
323 | from ("direct:test-50a") { r }
324 | from ("direct:test-50b") { r }
325 |
326 | template.requestBody("direct:test-50a", "a") must equal ("a-1-2")
327 | template.requestBody("direct:test-50b", "b") must equal ("b-1-2")
328 | }
329 |
330 | "preserving the message context even if a processor drops it" in {
331 | // Function that returns *new* message that doesn't contain the context of
332 | // the input message (it contains a new default context). The context of
333 | // the input message will be set on the result message by the MessageValidationResponder
334 | val badguy1 = (m: Message) => new Message("bad")
335 |
336 |
337 | // Function that returns a *new* message on which setException is called as well.
338 | // Returning a new message *and* calling either setException or setOneway required
339 | // explicit setting on the exchange from the input message as well.
340 | val badguy2 = (m: Message) => new Message("bad").setContextFrom(m).setException(new Exception("x"))
341 |
342 | val route1 = appendToBody("-1") >=> badguy1 >=> appendToBody("-2")
343 | val route2 = appendToBody("-1") >=> badguy2 >=> appendToBody("-2")
344 |
345 | route1 process Message("a").setOneway(true) match {
346 | case Failure(m) => fail("unexpected failure")
347 | case Success(m) => {
348 | m.context.oneway must be (true)
349 | m.body must equal ("bad-2")
350 | }
351 | }
352 |
353 | route2 process Message("a").setOneway(true) match {
354 | case Failure(m) => fail("unexpected failure")
355 | case Success(m) => {
356 | m.context.oneway must be (true)
357 | m.body must equal ("bad-2")
358 | }
359 | }
360 | }
361 |
362 | "proper correlation of (concurrent) request and response messages" in {
363 | def conditionalDelay(delay: Long, body: String): MessageProcessor = (m: Message, k: MessageValidation => Unit) => {
364 | if (m.body == body)
365 | processorConcurrencyStrategy.apply { Thread.sleep(delay); k(m.success) }
366 | else
367 | processorConcurrencyStrategy.apply { k(m.success) }
368 | }
369 |
370 | val r = conditionalDelay(1000, "a") >=> conditionalDelay(1000, "x") >=> appendToBody("-done")
371 |
372 | from("direct:test-55") { r }
373 |
374 | val a = Strategy.Naive.apply { template.requestBody("direct:test-55", "a") }
375 | val b = Strategy.Naive.apply { template.requestBody("direct:test-55", "b") }
376 | val x = Strategy.Naive.apply { r process Message("x") }
377 | val y = Strategy.Naive.apply { r process Message("y") }
378 |
379 | y() must equal (Success(Message("y-done")))
380 | x() must equal (Success(Message("x-done")))
381 |
382 | b() must equal ("b-done")
383 | a() must equal ("a-done")
384 | }
385 | }
386 | }
387 |
388 | class CamelTestSequential extends CamelTest
389 | class CamelTestConcurrent extends CamelTest with ExecutorMgnt {
390 | import java.util.concurrent.Executors
391 |
392 | dispatchConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
393 | multicastConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
394 | processorConcurrencyStrategy = Strategy.Executor(register(Executors.newFixedThreadPool(3)))
395 |
396 | override def afterAll = {
397 | shutdown
398 | super.afterAll
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/scalaz-camel-core/src/main/scala/scalaz/camel/core/Dsl.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2010-2011 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package scalaz.camel.core
17 |
18 | import java.util.concurrent.{BlockingQueue, CountDownLatch, TimeUnit}
19 | import java.util.concurrent.atomic.AtomicInteger
20 |
21 | import org.apache.camel.{Exchange, AsyncCallback, AsyncProcessor}
22 |
23 | import scalaz._
24 | import Scalaz._
25 | import Message._
26 |
27 | import concurrent.Promise
28 | import concurrent.Strategy
29 |
30 | /**
31 | * Message processors representing enterprise integration patterns (EIPs).
32 | *
33 | * @author Martin Krasser
34 | */
35 | trait DslEip { this: Conv =>
36 |
37 | /**
38 | * Name of the position message header needed by scatter-gather. Needed to
39 | * preserve the order of messages that are distributed to destinations.
40 | */
41 | val Position = "scalaz.camel.multicast.position"
42 |
43 | /**
44 | * Concurrency strategy for distributing messages to destinations
45 | * with the multicast and scatter-gather EIPs.
46 | */
47 | protected def multicastStrategy: Strategy
48 |
49 | /**
50 | * Creates a message processor that sets the message context's oneway field to true.
51 | */
52 | def oneway: MessageProcessor = oneway(true)
53 |
54 | /**
55 | * Creates a message processor that sets the message context's oneway field to given value.
56 | */
57 | def oneway(oneway: Boolean): MessageProcessor = messageProcessor { m: Message => m.setOneway(oneway) }
58 |
59 | /**
60 | * Creates a message processor that routes messages based on pattern matching. Implements
61 | * the content-based router EIP.
62 | */
63 | def choose(f: PartialFunction[Message, MessageRoute]): MessageProcessor =
64 | (m: Message, k: MessageValidation => Unit) => {
65 | f.lift(m) match {
66 | case Some(r) => messageProcessor(r)(m, k)
67 | case None => k(m.success)
68 | }
69 | }
70 |
71 | /**
72 | * Creates a message processor that distributes messages to given destinations. The created
73 | * processor applies the concurrency strategy returned by multicastStrategy
74 | * to distribute messages. Distributed messages are not combined, instead n responses
75 | * are sent where n is the number of destinations. Implements the static recipient-list EIP.
76 | */
77 | def multicast(destinations: MessageRoute*): MessageProcessor =
78 | (m: Message, k: MessageValidation => Unit) => {
79 | 0 until destinations.size foreach { i =>
80 | multicastStrategy.apply {
81 | destinations(i) apply m.success respond { mv => k(mv ∘ (_.addHeader(Position -> i))) }
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * Creates a message processor that generates a sequence of messages using f and
88 | * sends n responses taken from the generated message sequence. Implements the splitter EIP.
89 | */
90 | def split(f: Message => Seq[Message]): MessageProcessor =
91 | (m: Message, k: MessageValidation => Unit) => {
92 | try {
93 | f(m) foreach { r => k(r.success) }
94 | } catch {
95 | case e: Exception => k(m.setException(e).fail)
96 | }
97 | }
98 |
99 | /**
100 | * Creates a message processor that filters messages if f returns None and sends
101 | * a response if f returns Some message. Allows providers of f to
102 | * aggregate messages and continue processing with a combined message, for example.
103 | * Implements the aggregator EIP.
104 | */
105 | def aggregate(f: Message => Option[Message]): MessageProcessor =
106 | (m: Message, k: MessageValidation => Unit) => {
107 | try {
108 | f(m) match {
109 | case Some(r) => k(r.success)
110 | case None => { /* do not continue */ }
111 | }
112 | } catch {
113 | case e: Exception => k(m.setException(e).fail)
114 | }
115 | }
116 |
117 | /**
118 | * Creates a message processor that filters messages by evaluating predicate p. If
119 | * p evaluates to true a response is sent, otherwise the message is
120 | * filtered. Implements the filter EIP.
121 | */
122 | def filter(p: Message => Boolean) = aggregate { m: Message =>
123 | if (p(m)) Some(m) else None
124 | }
125 |
126 | /**
127 | * Creates a builder for a scatter-gather processor. Implements the scatter-gather EIP.
128 | *
129 | * @see ScatterDefinition
130 | */
131 | def scatter(destinations: MessageRoute*) = new ScatterDefinition(destinations: _*)
132 |
133 | /**
134 | * Builder for a scatter-gather processor.
135 | *
136 | * @see scatter
137 | */
138 | class ScatterDefinition(destinations: MessageRoute*) {
139 |
140 | /**
141 | * Creates a message processor that scatters messages to destinations and
142 | * gathers and combines them using combine. Messages are scattered to
143 | * destinations using the concurrency strategy returned by
144 | * multicastStrategy. Implements the scatter-gather EIP.
145 | *
146 | * @see scatter
147 | */
148 | def gather(combine: (Message, Message) => Message): MessageRoute = {
149 | val mcp = multicastProcessor(destinations.toList, combine)
150 | messageRoute((m: Message, k: MessageValidation => Unit) => mcp(m, k))
151 | }
152 | }
153 |
154 | /**
155 | * Creates a message processor that sets exception e on the input message and
156 | * generates a failure.
157 | */
158 | def failWith(e: Exception): MessageProcessor =
159 | (m: Message, k: MessageValidation => Unit) => k(m.setException(e).fail)
160 |
161 | // ------------------------------------------
162 | // Internal
163 | // ------------------------------------------
164 |
165 | /**
166 | * Creates a message processor that distributes messages to destinations (using multicast) and gathers
167 | * and combines the responses using an aggregator with gatherFunction.
168 | */
169 | private def multicastProcessor(destinations: List[MessageRoute], combine: (Message, Message) => Message): MessageProcessor = {
170 | (m: Message, k: MessageValidation => Unit) => {
171 | val sgm = multicast(destinations: _*)
172 | val sga = aggregate(gatherFunction(combine, destinations.size))
173 | messageRoute(sgm) >=> messageRoute(sga) apply m.success respond k
174 | }
175 | }
176 |
177 | /**
178 | * Creates an aggregation function that gathers and combines multicast responses.
179 | */
180 | private def gatherFunction(combine: (Message, Message) => Message, count: Int): Message => Option[Message] = {
181 | val ct = new AtomicInteger(count)
182 | val ma = Array.fill[Message](count)(null)
183 | (m: Message) => {
184 | for (pos <- m.header(Position).asInstanceOf[Option[Int]]) {
185 | ma.synchronized(ma(pos) = m)
186 | }
187 | if (ct.decrementAndGet == 0) {
188 | val ml = ma.synchronized(ma.toList)
189 | Some(ml.tail.foldLeft(ml.head)((m1, m2) => combine(m1, m2).removeHeader(Position)))
190 | } else {
191 | None
192 | }
193 | }
194 | }
195 | }
196 |
197 | trait DslAttempt { this: Conv =>
198 |
199 | type AttemptHandler1 = PartialFunction[Exception, MessageRoute]
200 | type AttemptHandlerN = PartialFunction[(Exception, RetryState), MessageRoute]
201 |
202 | /**
203 | * Captures the state of (repeated) message routing attempts. A retry state is defined by
204 | * rhorig used as input for the attempted routes.
217 | */
218 | def orig(s: RetryState): MessageProcessor =
219 | (m: Message, k: MessageValidation => Unit) => k(s.orig.success)
220 |
221 | /**
222 | * Creates a builder for an attempt-fallback processor. The processor makes a single attempt
223 | * to apply route r to an input message.
224 | */
225 | def attempt(r: MessageRoute) = new AttemptDefinition0(r)
226 |
227 | /**
228 | * Creates a builder for an attempt(n)-fallback processor. The processor can be used to make n
229 | * attempts to apply route r to an input message.
230 | *
231 | * @see orig
232 | * @see retry
233 | */
234 | def attempt(count: Int)(r: MessageRoute) = new AttemptDefinitionN(r, count - 1)
235 |
236 | /**
237 | * Creates a message processor that makes an additional attempt to apply s.r
238 | * (the initially attempted route) to its input message. The message processor decreases
239 | * s.count (the retry count by one). A retry attempt is only made if an exception
240 | * is set on the message an s.h (a retry handler) is defined at that exception.
241 | */
242 | def retry(s: RetryState): MessageProcessor =
243 | (m: Message, k: MessageValidation => Unit) => {
244 | s.r apply m.success respond { mv => mv match {
245 | case Success(_) => k(mv)
246 | case Failure(m) => {
247 | for {
248 | e <- m.exception
249 | r <- s.h.lift(e, s.next)
250 | } {
251 | if (s.count > 0) r apply m.exceptionHandled.success respond k else k(mv)
252 | }
253 | }
254 | }}
255 |
256 | }
257 |
258 | /**
259 | * Builder for an attempt-retry processor.
260 | */
261 | class AttemptDefinition0(r: MessageRoute) {
262 | /**
263 | * Creates an attempt-retry processor using retry handlers defined by h.
264 | */
265 | def fallback(h: AttemptHandler1): MessageProcessor =
266 | (m: Message, k: MessageValidation => Unit) => {
267 | r apply m.success respond { mv => mv match {
268 | case Success(_) => k(mv)
269 | case Failure(m) => {
270 | for {
271 | e <- m.exception
272 | r <- h.lift(e)
273 | } {
274 | r apply m.exceptionHandled.success respond k
275 | }
276 | }
277 | }}
278 | }
279 | }
280 |
281 | /**
282 | * Builder for an attempt(n)-retry processor.
283 | */
284 | class AttemptDefinitionN(r: MessageRoute, count: Int) {
285 | /**
286 | * Creates an attempt(n)-retry processor using retry handlers defined by h.
287 | */
288 | def fallback(h: AttemptHandlerN): MessageProcessor =
289 | (m: Message, k: MessageValidation => Unit) => {
290 | retry(new RetryState(r, h, count, m))(m, k)
291 | }
292 | }
293 | }
294 |
295 | /**
296 | * DSL for endpoint management.
297 | *
298 | * @author Martin Krasser
299 | */
300 | trait DslEndpoint { this: Conv =>
301 |
302 | /**
303 | * Creates a consumer for an endpoint represented by uri and connects it to the route
304 | * r. This method registers the created consumer at the Camel context for lifecycle
305 | * management.
306 | */
307 | def from(uri: String)(r: MessageRoute)(implicit em: EndpointMgnt, cm: ContextMgnt): Unit =
308 | em.createConsumer(uri, new RouteProcessor(r))
309 |
310 | /**
311 | * Creates a CPS processor that acts as a producer to the endpoint represented by uri.
312 | * This method registers the created producer at the Camel context for lifecycle management.
313 | */
314 | def to(uri: String)(implicit em: EndpointMgnt, cm: ContextMgnt): MessageProcessor = messageProcessor(uri, em, cm)
315 |
316 | private class RouteProcessor(val p: MessageRoute) extends AsyncProcessor {
317 | import RouteProcessor._
318 |
319 | /**
320 | * Synchronous message processing.
321 | */
322 | def process(exchange: Exchange) = {
323 | val latch = new CountDownLatch(1)
324 | process(exchange, new AsyncCallback() {
325 | def done(doneSync: Boolean) = {
326 | latch.countDown
327 | }
328 | })
329 | latch.await
330 | }
331 |
332 | /**
333 | * Asynchronous message processing (may be synchronous as well if all message processor are synchronous
334 | * processors and all concurrency strategies are configured to be Sequential).
335 | */
336 | def process(exchange: Exchange, callback: AsyncCallback) =
337 | if (exchange.getPattern.isOutCapable) processInOut(exchange, callback) else processInOnly(exchange, callback)
338 |
339 | private def processInOut(exchange: Exchange, callback: AsyncCallback) = {
340 | route(exchange.getIn.toMessage, once(respondTo(exchange, callback)))
341 | false
342 | }
343 |
344 | private def processInOnly(exchange: Exchange, callback: AsyncCallback) = {
345 | route(exchange.getIn.toMessage.setOneway(true), ((mv: MessageValidation) => { /* ignore any result */ }))
346 | callback.done(true)
347 | true
348 | }
349 |
350 | private def route(message: Message, k: MessageValidation => Unit): Unit = p apply message.success respond k
351 | }
352 |
353 | private object RouteProcessor {
354 | def respondTo(exchange: Exchange, callback: AsyncCallback): MessageValidation => Unit = (mv: MessageValidation ) => mv match {
355 | case Success(m) => respond(m, exchange, callback)
356 | case Failure(m) => respond(m, exchange, callback)
357 | }
358 |
359 | def respond(message: Message, exchange: Exchange, callback: AsyncCallback): Unit = {
360 | message.exception ∘ (exchange.setException(_))
361 | exchange.getIn.fromMessage(message)
362 | exchange.getOut.fromMessage(message)
363 | callback.done(false)
364 | }
365 |
366 | def once(k: MessageValidation => Unit): MessageValidation => Unit = {
367 | val done = new java.util.concurrent.atomic.AtomicBoolean(false)
368 | (mv: MessageValidation) => if (!done.getAndSet(true)) k(mv)
369 | }
370 | }
371 | }
372 |
373 | /**
374 | * DSL support classes for applying message validation responders and message processing routes.
375 | *
376 | * @see Camel.responderToResponderApplication
377 | * @see Camel.routeToRouteApplication
378 | *
379 | * @author Martin Krasser
380 | */
381 | trait DslApply { this: Conv =>
382 |
383 | /**
384 | * Applies a message validation responder r.
385 | *
386 | * @see Camel.responderToResponderApplication
387 | */
388 | class ResponderApplication(r: Responder[MessageValidation]) {
389 | /** Apply responder r and wait for response */
390 | def response: MessageValidation = responseQueue.take
391 |
392 | /** Apply responder r and wait for response with timeout */
393 | def response(timeout: Long, unit: TimeUnit): MessageValidation = responseQueue.poll(timeout, unit)
394 |
395 | /** Apply responder r and get response promise */
396 | def responsePromise(implicit s: Strategy): Promise[MessageValidation] = promise(responseQueue.take)
397 |
398 | /** Apply responder r and get response queue */
399 | def responseQueue: BlockingQueue[MessageValidation] = {
400 | val queue = new java.util.concurrent.LinkedBlockingQueue[MessageValidation](10)
401 | r respond { mv => queue.put(mv) }
402 | queue
403 | }
404 | }
405 |
406 | /**
407 | * Applies a message processing route r.
408 | *
409 | * @see Camel.routeToRouteApplication
410 | */
411 | class RouteApplication(r: MessageRoute) {
412 | /** Apply route r to message m and wait for response */
413 | def process(m: Message) =
414 | new ResponderApplication(r apply m.success).response
415 |
416 | /** Apply route r to message m and wait for response with timeout */
417 | def process(m: Message, timeout: Long, unit: TimeUnit) =
418 | new ResponderApplication(r apply m.success).response(timeout: Long, unit: TimeUnit)
419 |
420 | /** Apply route r to message m and get response promise */
421 | def submit(m: Message)(implicit s: Strategy) =
422 | new ResponderApplication(r apply m.success).responsePromise
423 |
424 | /** Apply route r to message m and get response queue */
425 | def submitN(m: Message) =
426 | new ResponderApplication(r apply m.success).responseQueue
427 |
428 | /** Apply route r to messages ms and wait for (first) response */
429 | def process(ms: Seq[Message]) =
430 | submitN(ms).take
431 |
432 | /** Apply route r to messages ms and wait for (first) response with timeout */
433 | def process(ms: Seq[Message], timeout: Long, unit: TimeUnit) =
434 | submitN(ms).poll(timeout, unit)
435 |
436 | /** Apply route r to messaged md and get response promise */
437 | def submit(ms: Seq[Message])(implicit s: Strategy) =
438 | promise(submitN(ms).take)
439 |
440 | /** Apply route r to messaged md and get response queue */
441 | def submitN(ms: Seq[Message]) = {
442 | val queue = new java.util.concurrent.LinkedBlockingQueue[MessageValidation]
443 | for (m <- ms) r apply m.success respond { mv => queue.put(mv) }
444 | queue
445 | }
446 | }
447 | }
448 |
--------------------------------------------------------------------------------