├── .gitignore ├── LICENSE ├── README.textile ├── project ├── ScalazCamelBuild.scala └── plugins.sbt ├── scalaz-camel-akka └── src │ ├── main │ └── scala │ │ ├── akka │ │ └── actor │ │ │ └── Sender.scala │ │ └── scalaz │ │ └── camel │ │ └── akka │ │ ├── ActorMgnt.scala │ │ ├── ActorRefx.scala │ │ ├── Akka.scala │ │ ├── Conv.scala │ │ └── Dsl.scala │ └── test │ ├── resources │ ├── akka.conf │ └── logback-test.xml │ └── scala │ └── scalaz │ └── camel │ └── akka │ ├── AkkaTest.scala │ ├── AkkaTestContext.scala │ └── AkkaTestProcessors.scala ├── scalaz-camel-core └── src │ ├── main │ └── scala │ │ └── scalaz │ │ └── camel │ │ └── core │ │ ├── Camel.scala │ │ ├── Conv.scala │ │ ├── Dsl.scala │ │ ├── Message.scala │ │ └── Router.scala │ └── test │ ├── resources │ └── context.xml │ └── scala │ └── scalaz │ └── camel │ └── core │ ├── CamelAttemptTest.scala │ ├── CamelJettyTest.scala │ ├── CamelJmsTest.scala │ ├── CamelLoadTest.scala │ ├── CamelSetupTest.scala │ ├── CamelTest.scala │ ├── CamelTestContext.scala │ ├── CamelTestProcessors.scala │ ├── ExecutorMgnt.scala │ ├── MessageTest.scala │ └── RouterTest.scala └── scalaz-camel-samples └── src ├── main ├── resources │ └── context.xml └── scala │ └── scalaz │ └── camel │ └── samples │ └── CoreExample.scala └── test └── scala └── scalaz └── camel └── samples └── CoreExampleTest.scala /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. Update 2 | 3 | Scalaz-camel is now superseded by the "streamz":https://github.com/krasserm/streamz project and its "combinators for Apache Camel":https://github.com/krasserm/streamz#combinators-for-apache-camel. 4 | 5 | h2. Introduction 6 | 7 | This project provides a domain-specific language (DSL) for "Apache Camel":http://camel.apache.org/ that is based on the "Scala":http://www.scala-lang.org/ programming language and the "Scalaz":http://code.google.com/p/scalaz/ library. It applies functional programming (FP) concepts to "enterprise integration patterns":http://www.enterpriseintegrationpatterns.com/ (EIPs) so that integration solutions can be constructed by composing EIPs with a functional DSL. 8 | 9 | The scalaz-camel DSL is an alternative to Camel's existing Java DSL and Scala DSL. It was re-designed from scratch with the goal of a better and more native integration with the Scala programming language. 10 | 11 | "Learn more ...":https://github.com/krasserm/scalaz-camel/wiki 12 | 13 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | * 46 | */ 47 | protected[akka] def postMessageToMailbox(message: Any, channel: UntypedChannel) = { 48 | import scalaz._ 49 | import Scalaz._ 50 | 51 | message match { 52 | case mv: MessageValidation => k(mv) 53 | case m: Message => m.exception match { 54 | case None => k(m.success) 55 | case Some(_) => k(m.fail) 56 | } 57 | case b => k(Message(b).success) 58 | } 59 | } 60 | 61 | def actorClass: Class[_ <: Actor] = unsupported 62 | def actorClassName = unsupported 63 | def dispatcher_=(md: MessageDispatcher): Unit = unsupported 64 | def dispatcher: MessageDispatcher = unsupported 65 | def makeRemote(hostname: String, port: Int): Unit = unsupported 66 | def makeRemote(address: InetSocketAddress): Unit = unsupported 67 | def homeAddress_=(address: InetSocketAddress): Unit = unsupported 68 | def remoteAddress: Option[InetSocketAddress] = unsupported 69 | def link(actorRef: ActorRef): Unit = unsupported 70 | def linkedActors: java.util.Map[Uuid, ActorRef] = unsupported 71 | def unlink(actorRef: ActorRef): Unit = unsupported 72 | def startLink(actorRef: ActorRef): Unit = unsupported 73 | def startLinkRemote(actorRef: ActorRef, hostname: String, port: Int): Unit = unsupported 74 | def spawn(clazz: Class[_ <: Actor]): ActorRef = unsupported 75 | def spawnRemote(clazz: Class[_ <: Actor], hostname: String, port: Int, timeout: Long): ActorRef = unsupported 76 | def spawnLink(clazz: Class[_ <: Actor]): ActorRef = unsupported 77 | def spawnLinkRemote(clazz: Class[_ <: Actor], hostname: String, port: Int, timeout: Long): ActorRef = unsupported 78 | def shutdownLinkedActors: Unit = unsupported 79 | def supervisor: Option[ActorRef] = unsupported 80 | def homeAddress: Option[InetSocketAddress] = None 81 | protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout(message: Any, timeout: Long, channel: UntypedChannel) = unsupported 82 | protected[akka] def mailbox: AnyRef = unsupported 83 | protected[akka] def mailbox_=(msg: AnyRef):AnyRef = unsupported 84 | protected[akka] def restart(reason: Throwable, maxNrOfRetries: Option[Int], withinTimeRange: Option[Int]): Unit = unsupported 85 | protected[akka] def restartLinkedActors(reason: Throwable, maxNrOfRetries: Option[Int], withinTimeRange: Option[Int]): Unit = unsupported 86 | protected[akka] def handleTrapExit(dead: ActorRef, reason: Throwable): Unit = unsupported 87 | protected[akka] def linkedActorsAsList: List[ActorRef] = unsupported 88 | protected[akka] def invoke(messageHandle: MessageInvocation): Unit = unsupported 89 | protected[akka] def remoteAddress_=(addr: Option[InetSocketAddress]): Unit = unsupported 90 | protected[akka] def registerSupervisorAsRemoteActor = unsupported 91 | protected[akka] def supervisor_=(sup: Option[ActorRef]): Unit = unsupported 92 | protected[akka] def actorInstance: AtomicReference[Actor] = unsupported 93 | 94 | private def unsupported = throw new UnsupportedOperationException("Not supported for %s" format classOf[Sender].getName) 95 | } -------------------------------------------------------------------------------- /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-akka/src/main/scala/scalaz/camel/akka/ActorRefx.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 | 20 | /** 21 | * 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/Akka.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 | 20 | import scalaz.camel.core 21 | 22 | /** 23 | * Provides the Akka-specific Camel DSL. 24 | * 25 | * @author Martin Krasser 26 | */ 27 | trait Akka extends DslEndpoint with DslEip with Conv with core.DslEndpoint with core.Conv { 28 | implicit def actorRefToActorRefx(actor: ActorRef) = new ActorRefx(actor) 29 | } 30 | -------------------------------------------------------------------------------- /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-akka/src/main/scala/scalaz/camel/akka/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.akka 17 | 18 | import scalaz.camel.core 19 | import scalaz.camel.core.Conv._ 20 | import scalaz.camel.core.Message 21 | 22 | import akka.actor.Actor 23 | import akka.actor.ActorRef 24 | 25 | /** 26 | * @author Martin Krasser 27 | */ 28 | object DslEip { 29 | type AggregationFunction = (Message, Message) => Message 30 | type CompletionPredicate = (Message) => Boolean 31 | } 32 | 33 | /** 34 | * @author Martin Krasser 35 | */ 36 | trait DslEip { this: DslEndpoint => 37 | type AggregationFunction = DslEip.AggregationFunction 38 | type CompletionPredicate = DslEip.CompletionPredicate 39 | 40 | /** 41 | * Preliminary support for actor-based aggregator EIP. 42 | */ 43 | def aggregate(implicit am: ActorMgnt) = AggregateDefinition(am) 44 | 45 | /** 46 | * Preliminary support for actor-based aggregator EIP. 47 | */ 48 | case class AggregateDefinition(am: ActorMgnt, f: AggregationFunction = (m1, m2) => m2) { 49 | def using(f: AggregationFunction) = AggregateDefinition(am, f) 50 | def until(p: CompletionPredicate): MessageProcessor = to(manage(Actor.actorOf(new Aggregator(f, p)))(am)) 51 | } 52 | 53 | private class Aggregator(f: AggregationFunction, p: CompletionPredicate) extends Actor { 54 | var current: Option[Message] = None 55 | 56 | def receive = { 57 | case message: Message => { 58 | val m = update(message) 59 | if (p(m)) self.reply(m) 60 | } 61 | } 62 | 63 | def update(message: Message): Message = { 64 | current match { 65 | case None => { current = Some(message) } 66 | case Some(m) => { current = Some(f(m, message))} 67 | } 68 | current.get 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * @author Martin Krasser 75 | */ 76 | trait DslEndpoint { this: core.DslEndpoint with Conv => 77 | /** 78 | * Binds the life cycle of 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/test/resources/akka.conf: -------------------------------------------------------------------------------- 1 | akka{ 2 | } -------------------------------------------------------------------------------- /scalaz-camel-akka/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [%4p] [%d{ISO8601}] [%t] %c{1}: %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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-akka/src/test/scala/scalaz/camel/akka/AkkaTestContext.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 scalaz.camel.core._ 19 | 20 | import org.apache.camel.impl.DefaultCamelContext 21 | 22 | /** 23 | * @author Martin Krasser 24 | */ 25 | trait AkkaTestContext extends Camel with Akka with AkkaTestProcessors { 26 | import scalaz.concurrent.Strategy._ 27 | 28 | dispatchConcurrencyStrategy = Sequential 29 | multicastConcurrencyStrategy = Sequential 30 | 31 | val context = new DefaultCamelContext 32 | val template = context.createProducerTemplate 33 | 34 | implicit val router = new Router(context) with ActorMgnt 35 | } -------------------------------------------------------------------------------- /scalaz-camel-akka/src/test/scala/scalaz/camel/akka/AkkaTestProcessors.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 scalaz.camel.core 19 | import scalaz.camel.core.{ContextMgnt, Message} 20 | 21 | /** 22 | * @author Martin Krasser 23 | */ 24 | trait AkkaTestProcessors { this: core.Conv => 25 | def appendToBody(o: Any)(implicit mgnt: ContextMgnt) = messageProcessor((m: Message) => m.appendToBody(o)) 26 | } -------------------------------------------------------------------------------- /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/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 | * 32 | * 33 | * Related implicit conversions are provided by the 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-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 | * 210 | */ 211 | case class RetryState(r: MessageRoute, h: AttemptHandlerN, count: Int, orig: Message) { 212 | def next = RetryState(r, h, count - 1, orig) 213 | } 214 | 215 | /** 216 | * Creates a message processor that extracts the original message from retry state s. 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 | -------------------------------------------------------------------------------- /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/Router.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.Collection 19 | import java.util.concurrent.ThreadPoolExecutor 20 | 21 | import org.apache.camel._ 22 | import org.apache.camel.support.ServiceSupport 23 | import org.apache.camel.spi.LifecycleStrategy 24 | import java.lang.String 25 | 26 | /** 27 | * Registers created endpoint consumers and producers at the CamelContext for 28 | * lifecycle management. 29 | * 30 | * @author Martin Krasser 31 | */ 32 | trait EndpointMgnt { this: ContextMgnt => 33 | /** 34 | * Creates a consumer for endpoint defined by uri. 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-core/src/test/resources/context.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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/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/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/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/ExecutorMgnt.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 | * @author Martin Krasser 20 | */ 21 | trait ExecutorMgnt { 22 | import java.util.concurrent.{Executors, ExecutorService} 23 | import scala.collection.mutable.Buffer 24 | 25 | // registry for thread pools 26 | private val executors = Buffer[ExecutorService]() 27 | 28 | def register(s: ExecutorService) = { executors append s; s } 29 | def shutdown = executors.foreach(_.shutdownNow) 30 | } -------------------------------------------------------------------------------- /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-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-samples/src/main/resources/context.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /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-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 | --------------------------------------------------------------------------------