diff --git a/maven/api/pom.xml b/maven/api/pom.xml deleted file mode 100644 index ad52103..0000000 --- a/maven/api/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - api - jar - Akka Patterns - API - - - org.cakesolutions.akkapatterns - core - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - domain - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - test - 0.1.RELEASE-SNAPSHOT - test - - - - cc.spray - spray-server - - - cc.spray - spray-util - - - cc.spray - spray-base - - - - com.typesafe.akka - akka-actor_${scala.version} - - - - net.liftweb - lift-json_2.9.1 - - - - junit - junit-dep - test - - - org.specs2 - specs2_${scala.version} - test - - - diff --git a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-UUID-get.json b/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-UUID-get.json deleted file mode 100644 index c6e54b3..0000000 --- a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-UUID-get.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "firstName":"Jan", - "lastName":"Machacek", - "email":"janm@cakesolutions.net", - "id":"00000000-0000-0000-0000-000000000000", - "addresses":[ - {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"}, - {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"} - ] -} \ No newline at end of file diff --git a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-get.json b/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-get.json deleted file mode 100644 index b9f1d65..0000000 --- a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-get.json +++ /dev/null @@ -1,10 +0,0 @@ -[{ - "firstName":"Jan", - "lastName":"Machacek", - "email":"janm@cakesolutions.net", - "id":"00000000-0000-0000-0000-000000000000", - "addresses":[ - {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"}, - {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"} - ] -}] \ No newline at end of file diff --git a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-post.json b/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-post.json deleted file mode 100644 index 945b588..0000000 --- a/maven/api/src/main/resources/org/cakesolutions/akkapatterns/api/customers-post.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "firstName":"Joe", - "lastName":"Bloggs", - "email":"joe@cakesolutions.net", - "id":"00000000-0000-0000-0100-000000000000", - "addresses":[ - {"line1":"123 Winding Road", "line2":"Cowley", "line3":"Oxford"} - ] -} \ No newline at end of file diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala deleted file mode 100644 index baa6a50..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.{ActorRef, Props} -import cc.spray._ -import http.{StatusCodes, HttpResponse} -import org.cakesolutions.akkapatterns.core.Core -import akka.util.Timeout - -trait Api { - this: Core => - - val routes = - new HomeService().route :: - //new DummyService("customers").route :: - new CustomerService().route :: - Nil - - def rejectionHandler: PartialFunction[scala.List[cc.spray.Rejection], cc.spray.http.HttpResponse] = { - case (rejections: List[Rejection]) => HttpResponse(StatusCodes.BadRequest) - } - - val svc: Route => ActorRef = route => actorSystem.actorOf(Props(new HttpService(route, rejectionHandler))) - - val rootService = actorSystem.actorOf( - props = Props(new RootService( - svc(routes.head), - routes.tail.map(svc):_* - )), - name = "root-service" - ) - -} - -trait DefaultTimeout { - final implicit val timeout = Timeout(3000) - -} \ No newline at end of file diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala deleted file mode 100644 index 0657d52..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.ActorSystem -import cc.spray.Directives -import org.cakesolutions.akkapatterns.domain.Customer -import org.cakesolutions.akkapatterns.core.application._ -import cc.spray.directives.JavaUUID -import akka.pattern.ask -import org.cakesolutions.akkapatterns.core.application.RegisterCustomer -import org.cakesolutions.akkapatterns.domain.Customer -import org.cakesolutions.akkapatterns.core.application.Get -import org.cakesolutions.akkapatterns.core.application.FindAll - -/** - * @author janmachacek - */ -class CustomerService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with Unmarshallers with DefaultTimeout with LiftJSON { - def customerActor = actorSystem.actorFor("/user/application/customer") - - val route = - path("customers" / JavaUUID) { id => - get { - completeWith((customerActor ? Get(id)).mapTo[Option[Customer]]) - } - } ~ - path("customers") { - get { - completeWith((customerActor ? FindAll()).mapTo[List[Customer]]) - } ~ - post { - content(as[RegisterCustomer]) { rc => - completeWith((customerActor ? rc).mapTo[Either[NotRegisteredCustomer, RegisteredCustomer]]) - } - } - } - -} diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/dummy.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/dummy.scala deleted file mode 100644 index 1c792b4..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/dummy.scala +++ /dev/null @@ -1,47 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import cc.spray.Directives -import akka.actor.ActorSystem -import cc.spray.http._ -import cc.spray.http.MediaTypes._ -import cc.spray.RequestContext - -class DummyService(path: String)(implicit val actorSystem: ActorSystem) extends Directives { - - val route = { - pathPrefix(path) { - x => - x.complete( - HttpResponse(StatusCodes.OK, getContent(x, `application/json`))) - } - } - - private def getContent(ctx: RequestContext, contentType: ContentType): HttpContent = { - - var filename = ctx.request.path - - if (filename.startsWith("/")) filename = filename.drop(1) - if (filename.endsWith("/")) filename = filename.dropRight(1) - filename = filename.replace("/", "-") - filename = filename + "-" + ctx.request.method.toString().toLowerCase + ".json" - - val uidRegex = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}".r - val fileContent = uidRegex.findFirstIn(filename) match { - case Some(uid) => - getFileAsString(uidRegex.replaceAllIn(filename, "UUID")) - case None => - getFileAsString(filename) - } - - HttpContent(contentType, fileContent) - } - - private def getFileAsString(filename: String): String = { - try { - scala.io.Source.fromInputStream(getClass.getResourceAsStream(filename)).mkString - } - catch { - case _: Throwable => "{body of file " + filename + " -- Missing File!}" - } - } -} diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala deleted file mode 100644 index c701411..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.ActorSystem -import cc.spray.Directives -import cc.spray.directives.Slash -import java.net.InetAddress -import akka.pattern.ask -import org.cakesolutions.akkapatterns.core.application.{PoisonPill, GetImplementation, Implementation} - -case class SystemInfo(implementation: Implementation, host: String) - -class HomeService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with DefaultTimeout with LiftJSON { - - def applicationActor = actorSystem.actorFor("/user/application") - import scala.concurrent.ExecutionContext.Implicits.global - - val route = { - path(Slash) { - get { - completeWith { - (applicationActor ? GetImplementation()).mapTo[Implementation].map { - SystemInfo(_, InetAddress.getLocalHost.getCanonicalHostName) - } - } - } - } ~ - path("poisonpill") { - post { - completeWith { - applicationActor ! PoisonPill() - - "Goodbye" - } - } - } - } - -} diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala deleted file mode 100644 index c63a351..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala +++ /dev/null @@ -1,68 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import cc.spray.typeconversion._ -import net.liftweb.json._ -import cc.spray.http.{HttpContent, ContentType} -import cc.spray.http.MediaTypes._ -import net.liftweb.json.Serialization._ -import cc.spray.http.ContentTypeRange -import java.util.UUID - -trait Marshallers extends DefaultMarshallers { - this: LiftJSON => - - implicit def liftJsonMarshaller[A <: AnyRef] = new SimpleMarshaller[A] { - val canMarshalTo = ContentType(`application/json`) :: Nil - def marshal(value: A, contentType: ContentType) = { - val jsonSource = write(value.asInstanceOf[AnyRef]) - DefaultMarshallers.StringMarshaller.marshal(jsonSource, contentType) - } - } - -} - -trait Unmarshallers extends DefaultUnmarshallers { - this: LiftJSON => - - implicit def liftJsonUnmarshaller[A <: Product : Manifest] = new SimpleUnmarshaller[A] { - val canUnmarshalFrom = ContentTypeRange(`application/json`) :: Nil - def unmarshal(content: HttpContent) = protect { - val jsonSource = DefaultUnmarshallers.StringUnmarshaller(content).right.get - parse(jsonSource).extract[A] - } - } - -} - -trait LiftJSON { - implicit def liftJsonFormats: Formats = - DefaultFormats + new UUIDSerializer + FieldSerializer[AnyRef]() - - class UUIDSerializer extends Serializer[UUID] { - private val UUIDClass = classOf[UUID] - - def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), UUID] = { - case (TypeInfo(UUIDClass, _), json) => json match { - case JString(s) => UUID.fromString(s) - case x => throw new MappingException("Can't convert " + x + " to UUID") - } - } - - def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case x: UUID => JString(x.toString) - } - } - - class StringBuilderMarshallingContent(sb: StringBuilder) extends MarshallingContext { - - def marshalTo(content: HttpContent) { - if (sb.length > 0) sb.append(",") - sb.append(new String(content.buffer)) - } - - def handleError(error: Throwable) {} - - def startChunkedMessage(contentType: ContentType) = throw new UnsupportedOperationException - } - -} \ No newline at end of file diff --git a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala b/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala deleted file mode 100644 index da6b990..0000000 --- a/maven/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala +++ /dev/null @@ -1,24 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.ActorSystem -import cc.spray.Directives -import org.cakesolutions.akkapatterns.domain.User -import org.cakesolutions.akkapatterns.core.application.{NotRegisteredUser, RegisteredUser} -import akka.pattern.ask - -/** - * @author janmachacek - */ -class UserService(implicit val actorSystem: ActorSystem) extends Directives with Marshallers with Unmarshallers with DefaultTimeout with LiftJSON { - def userActor = actorSystem.actorFor("/user/application/user") - - val route = - path("user" / "register") { - post { - content(as[User]) { user => - completeWith((userActor ? RegisteredUser(user)).mapTo[Either[NotRegisteredUser, RegisteredUser]]) - } - } - } - -} diff --git a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala b/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala deleted file mode 100644 index 9ce15ce..0000000 --- a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import org.cakesolutions.akkapatterns.domain.{User, Customer} -import cc.spray.http.HttpMethods._ -import org.cakesolutions.akkapatterns.core.application.{RegisteredCustomer, RegisterCustomer} -import java.util.UUID - -/** - * @author janmachacek - */ -class CustomerServiceSpec extends DefaultApiSpecification { - implicit val service = rootService - - "Getting a known customer works" in { - val customer = perform[Customer](GET, "/customers/00000000-0000-0000-0000-000000000000") - - customer must_== janMachacek - } - - "Finding all customers works" in { - val customers = perform[List[Customer]](GET, "/customers") - - customers must contain (janMachacek) - } - - "Registering a customer" in { - val rc = RegisterCustomer( - joeBloggs, - User(UUID.randomUUID(), "janm", "Like I'll tell you!")) - val registered = perform[RegisterCustomer, RegisteredCustomer](POST, "/customers", rc) - - (registered.customer must_== joeBloggs) and - (registered.user.username must_== "janm") and - (registered.user.password must_!= "Like I'll tell you") - } - -} diff --git a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpec.scala b/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpec.scala deleted file mode 100644 index 6d7a644..0000000 --- a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpec.scala +++ /dev/null @@ -1,18 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import cc.spray.http.HttpMethods._ -import cc.spray.http._ -import org.specs2.runner.JUnitRunner -import org.junit.runner.RunWith - -@RunWith(classOf[JUnitRunner]) -class HomeServiceSpec extends DefaultApiSpecification { - - "root URL shows the System version" in { - testRoot(HttpRequest(GET, "/"))(rootService).response.content.as[SystemInfo] match { - case Right(info) => success - case Left(failure) => anError - } - } - -} diff --git a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala b/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala deleted file mode 100644 index f241744..0000000 --- a/maven/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala +++ /dev/null @@ -1,83 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import cc.spray.test.SprayTest -import java.util.concurrent.TimeUnit -import akka.actor.ActorRef -import cc.spray.RequestContext -import cc.spray.http._ -import io.Source -import org.specs2.mutable.Specification -import org.cakesolutions.akkapatterns.test.{DefaultTestData, SpecConfiguration} -import org.cakesolutions.akkapatterns.core.Core -import concurrent.util.Duration - -trait RootSprayTest extends SprayTest { - protected def testRoot(request: HttpRequest, timeout: Duration = Duration(10000, TimeUnit.MILLISECONDS)) - (root: ActorRef): ServiceResultWrapper = { - val routeResult = new RouteResult - root ! - RequestContext( - request = request, - responder = routeResult.requestResponder, - unmatchedPath = request.path - ) - - // since the route might detach we block until the route actually completes or times out - routeResult.awaitResult(timeout) - new ServiceResultWrapper(routeResult, timeout) - } - -} - -trait JsonSource { - - def jsonFor(location: String) = Source.fromInputStream(classOf[JsonSource].getResourceAsStream(location)).mkString - - def jsonContent(location: String) = Some(HttpContent(ContentType(MediaTypes.`application/json`), jsonFor(location))) - -} - -/** - * Convenience trait for API tests - */ -trait ApiSpecification extends Specification with SpecConfiguration with RootSprayTest with Core with Api with Unmarshallers with Marshallers with LiftJSON { - - import cc.spray.typeconversion._ - - protected def respond(method: HttpMethod, url: String, content: Option[HttpContent] = None) - (implicit root: ActorRef) = { - val request = HttpRequest(method, url, content = content) - testRoot(request)(root).response - } - - protected def perform[A](method: HttpMethod, url: String, content: Option[HttpContent] = None) - (implicit root: ActorRef, unmarshaller: Unmarshaller[A]): A = { - val request = HttpRequest(method, url, content = content) - val response = testRoot(request)(root).response.content - val obj = response.as[A] match { - case Left(e) => throw new Exception(e.toString) - case Right(r) => r - } - obj - } - - protected def perform[In, Out](method: HttpMethod, url: String, in: In) - (implicit root: ActorRef, marshaller: Marshaller[In], unmarshaller: Unmarshaller[Out]): Out = { - marshaller(t => Some(t)) match { - case MarshalWith(f) => - val sb = new StringBuilder() - val ctx = new StringBuilderMarshallingContent(sb) - f(ctx)(in) - - perform[Out](method, url, Some(HttpContent(ContentType(MediaTypes.`application/json`), sb.toString()))) - case CantMarshal(_) => - throw new Exception("Cant marshal " + in) - } - } - -} - -/** - * Convenience trait for API tests; with default test data - */ -trait DefaultApiSpecification extends ApiSpecification with DefaultTestData with JsonSource \ No newline at end of file diff --git a/maven/core/pom.xml b/maven/core/pom.xml deleted file mode 100644 index fb31fa7..0000000 --- a/maven/core/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - core - jar - Akka Patterns - Core - - - com.typesafe.akka - akka-actor_${scala.version} - - - org.cakesolutions.akkapatterns - domain - 0.1.RELEASE-SNAPSHOT - - - - - - org.scalaz - scalaz-core_${scala.version} - - - - com.typesafe.akka - akka-testkit_${scala.version} - test - - - org.specs2 - specs2_${scala.version} - test - - - org.specs2 - specs2-scalaz-core_${scala.version} - test - - - junit - junit - 4.8.2 - test - - - org.cakesolutions.akkapatterns - test - 0.1.RELEASE-SNAPSHOT - - - diff --git a/maven/core/src/main/resources/META-INF/aop.xml b/maven/core/src/main/resources/META-INF/aop.xml deleted file mode 100644 index a8655ab..0000000 --- a/maven/core/src/main/resources/META-INF/aop.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/maven/core/src/main/resources/META-INF/spring/module-context.xml b/maven/core/src/main/resources/META-INF/spring/module-context.xml deleted file mode 100644 index 72fb8a6..0000000 --- a/maven/core/src/main/resources/META-INF/spring/module-context.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/maven/core/src/main/resources/application.conf b/maven/core/src/main/resources/application.conf deleted file mode 100644 index 7a5734f..0000000 --- a/maven/core/src/main/resources/application.conf +++ /dev/null @@ -1,40 +0,0 @@ -akka { - loglevel = DEBUG - - actor { - debug { - event-stream = on - receive = on - lifecycle = on - } - - default-dispatcher { - type = "Dispatcher" - - executor = "fork-join-executor" - - fork-join-executor { - # Min number of threads to cap factor-based parallelism number to - parallelism-min = 8 - # Parallelism (threads) ... ceil(available processors * factor) - parallelism-factor = 15.0 - # Max number of threads to cap factor-based parallelism number to - parallelism-max = 64 - } - - throughput = 5 - - attempt-teamwork = on - - } - } -} - -spray { - can.server { - idle-timeout = 5 s - request-timeout = 2 s - } - - io.confirm-sends = off -} \ No newline at end of file diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/application.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/application.scala deleted file mode 100644 index d7d0d2a..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/application.scala +++ /dev/null @@ -1,48 +0,0 @@ -package org.cakesolutions.akkapatterns.core.application - -import org.cakesolutions.akkapatterns.core.{Started, Stop, Start} -import akka.actor.{Props, Actor} -import org.cakesolutions.akkapatterns.domain.Configured -import com.mongodb.casbah.MongoDB - -case class GetImplementation() -case class Implementation(title: String, version: String, build: String) - -case class PoisonPill() - -class ApplicationActor extends Actor { - - def receive = { - case GetImplementation() => - val manifestStream = getClass.getResourceAsStream("/META-INF/MANIFEST.MF") - val manifest = new java.util.jar.Manifest(manifestStream) - val title = manifest.getMainAttributes.getValue("Implementation-Title") - val version = manifest.getMainAttributes.getValue("Implementation-Version") - val build = manifest.getMainAttributes.getValue("Implementation-Build") - manifestStream.close() - - sender ! Implementation(title, version, build) - - case Start() => - context.actorOf(Props[CustomerActor], "customer") - context.actorOf(Props[UserActor], "user") - - sender ! Started() - - /* - * Stops this actor and all the child actors. - */ - case Stop() => - context.children.foreach(context.stop _) - - case PoisonPill() => - sys.exit(-1) - } - -} - -trait MongoCollections extends Configured { - def customers = configured[MongoDB].apply("customers") - def users = configured[MongoDB].apply("users") - -} diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/casbah.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/casbah.scala deleted file mode 100644 index 9a04de1..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/casbah.scala +++ /dev/null @@ -1,125 +0,0 @@ -package org.cakesolutions.akkapatterns.core.application - -import com.mongodb.casbah.Imports._ -import java.util.UUID -import org.cakesolutions.akkapatterns.domain.{User, Address, Customer} - -/** - * Contains type classes that deserialize records from Casbah into "our" types. - */ -trait CasbahDeserializers { - type CasbahDeserializer[A] = DBObject => A - - /** - * Convenience method that picks the ``CasbahDeserializer`` for the type ``A`` - * @param deserializer implicitly given deserializer - * @tparam A the type A - * @return the deserializer for ``A`` - */ - def casbahDeserializer[A](implicit deserializer: CasbahDeserializer[A]) = deserializer - - private def inner[A: CasbahDeserializer](o: DBObject, field: String): A = casbahDeserializer[A].apply(o.as[DBObject](field)) - - private def innerList[A: CasbahDeserializer](o: DBObject, field: String): Seq[A] = { - val deserializer = casbahDeserializer[A] - o.as[MongoDBList](field).map { - inner => deserializer(inner.asInstanceOf[DBObject]) - } - } - - implicit object AddressDeserializer extends CasbahDeserializer[Address] { - def apply(o: DBObject) = - Address(o.as[String]("line1"), o.as[String]("line2"), o.as[String]("line3")) - } - - implicit object CustomerDeserializer extends CasbahDeserializer[Customer] { - def apply(o: DBObject) = - Customer(o.as[String]("firstName"), o.as[String]("lastName"), o.as[String]("email"), - innerList[Address](o, "addresses"), o.as[UUID]("id")) - } - - implicit object UserDeserializer extends CasbahDeserializer[User] { - def apply(o: DBObject) = User(o.as[UUID]("id"), o.as[String]("username"), o.as[String]("password")) - } - -} - -/** - * Contains type classes that serialize "our" types into Casbah records. - */ -trait CasbahSerializers { - type CasbahSerializer[A] = A => DBObject - - /** - * Convenience method that picks the ``CasbahSerializer`` for the type ``A`` - * @param serializer implicitly given serializer - * @tparam A the type A - * @return the serializer for ``A`` - */ - def casbahSerializer[A](implicit serializer: CasbahSerializer[A]) = serializer - - implicit object AddressSerializer extends CasbahSerializer[Address] { - def apply(address: Address) = { - val builder = MongoDBObject.newBuilder - - builder += "line1" -> address.line1 - builder += "line2" -> address.line2 - builder += "line3" -> address.line2 - - builder.result() - } - } - - implicit object UserSerializer extends CasbahSerializer[User] { - def apply(user: User) = { - val builder = MongoDBObject.newBuilder - - builder += "username" -> user.username - builder += "password" -> user.password - builder += "id" -> user.id - - builder.result() - } - } - - implicit object CustomerSerializer extends CasbahSerializer[Customer] { - def apply(customer: Customer) = { - val builder = MongoDBObject.newBuilder - - builder += "firstName" -> customer.firstName - builder += "lastName" -> customer.lastName - builder += "email" -> customer.email - builder += "addresses" -> customer.addresses.map(AddressSerializer(_)) - builder += "id" -> customer.id - - builder.result() - } - } - -} - -/** - * Contains convenience functions that can be used to find "entities-by-id" - */ -trait SearchExpressions { - - def entityId(id: UUID) = MongoDBObject("id" -> id) - -// def entityId(id: UUID) = MongoDBObject("id" -> id, "active" -> true) - -} - -/** - * Mix this trait into your classes to gain the functionality of the serializers, deserializers and mappers. - */ -trait TypedCasbah extends CasbahDeserializers with CasbahSerializers { - - final def serialize[A: CasbahSerializer](a: A) = casbahSerializer[A].apply(a) - - final def deserialize[A: CasbahDeserializer](o: DBObject) = casbahDeserializer[A].apply(o) - - final def mapper[A: CasbahDeserializer] = { - (o: DBObject) => deserialize[A](o) - } - -} \ No newline at end of file diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/customer.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/customer.scala deleted file mode 100644 index d6e9519..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/customer.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.cakesolutions.akkapatterns.core.application - -import akka.actor.Actor -import java.util.UUID -import org.cakesolutions.akkapatterns.domain.{User, Configured, Customer} -import com.mongodb.casbah.{MongoCollection, MongoDB} -import org.specs2.internal.scalaz.Identity -import org.cakesolutions.akkapatterns.domain - -/** - * Registers a customer and a user. After registering, we have a user account for the given customer. - * - * @param customer the customer - * @param user the user - */ -case class RegisterCustomer(customer: Customer, user: User) - -/** - * Reply to successful customer registration - * @param customer the newly registered customer - * @param user the newly registered user - */ -case class RegisteredCustomer(customer: Customer, user: User) - -/** - * Reply to unsuccessful customer registration - * @param code the error code for the failure reason - */ -case class NotRegisteredCustomer(code: String) extends Failure - -/** - * CRUD operations for the [[org.cakesolutions.akkapatterns.domain.Customer]]s - */ -trait CustomerOperations extends TypedCasbah with SearchExpressions { - def customers: MongoCollection - - def getCustomer(id: domain.Identity) = customers.findOne(entityId(id)).map(mapper[Customer]) - - def findAllCustomers() = customers.find().map(mapper[Customer]).toList - - def insertCustomer(customer: Customer) = { - customers += serialize(customer) - customer - } - - def registerCustomer(customer: Customer)(ru: RegisteredUser): Either[Failure, RegisteredCustomer] = { - customers += serialize(customer) - Right(RegisteredCustomer(customer, ru.user)) - } - -} - -class CustomerActor extends Actor with Configured with CustomerOperations with UserOperations with MongoCollections { - - protected def receive = { - case Get(id) => - sender ! getCustomer(id) - - case FindAll() => - sender ! findAllCustomers() - - case Insert(customer: Customer) => - sender ! insertCustomer(customer) - - case RegisterCustomer(customer, user) => - import scalaz._ - import Scalaz._ - - sender ! (registerUser(user) >>= registerCustomer(customer)) - - } -} diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/messages.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/messages.scala deleted file mode 100644 index 6e4b315..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/messages.scala +++ /dev/null @@ -1,42 +0,0 @@ -package org.cakesolutions.akkapatterns.core.application - -import java.util.UUID - -/** - * Base type for failures - */ -trait Failure { - /** - * The error code for the failure - * @return the error code - */ - def code: String -} - -/** - * Gets an entity identified by ``id`` - * - * @param id the identity - */ -case class Get(id: UUID) - -/** - * Finds all entities - */ -case class FindAll() - -/** - * Inserts the given entity - * - * @param entity the entity to be inserted - * @tparam A the type of A - */ -case class Insert[A](entity: A) - -/** - * Updates the given entity - * - * @param entity the entity to update - * @tparam A the type of A - */ -case class Update[A](entity: A) \ No newline at end of file diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/user.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/user.scala deleted file mode 100644 index 0a32179..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/application/user.scala +++ /dev/null @@ -1,72 +0,0 @@ -package org.cakesolutions.akkapatterns.core.application - -import akka.actor.Actor -import com.mongodb.casbah.MongoCollection -import org.cakesolutions.akkapatterns.domain -import domain.User -import com.mongodb.casbah.commons.MongoDBObject -import java.security.MessageDigest - -/** - * Finds a user by the given username - * - * @param username the username - */ -case class GetUserByUsername(username: String) - -/** - * Registers a user. Checks the password complexity and that the username is not duplicate - * - * @param user the user to be registered - */ -case class Register(user: User) - -/** - * Successfully registered a user - * - * @param user the user that's just been registered - */ -case class RegisteredUser(user: User) - -/** - * Unsuccessful registration with the error code - * @param code the error code - */ -case class NotRegisteredUser(code: String) extends Failure - - -trait UserOperations extends TypedCasbah with SearchExpressions { - def users: MongoCollection - def sha1 = MessageDigest.getInstance("SHA1") - - def getUser(id: domain.Identity) = users.findOne(entityId(id)).map(mapper[User]) - - def getUserByUsername(username: String) = users.findOne(MongoDBObject("username" -> username)).map(mapper[User]) - - def registerUser(user: User): Either[Failure, RegisteredUser] = { - getUserByUsername(user.username) match { - case None => - val hashedPassword = java.util.Arrays.toString(sha1.digest(user.password.getBytes)) - val userToRegister = user.copy(password = hashedPassword) - users += serialize(userToRegister) - Right(RegisteredUser(userToRegister)) - case Some(_existingUser) => - Left(NotRegisteredUser("User.duplicateUsername")) - } - } - -} - -class UserActor extends Actor with UserOperations with MongoCollections { - - protected def receive = { - case Get(id) => - sender ! getUser(id) - - case GetUserByUsername(username) => - sender ! getUserByUsername(username) - - case RegisteredUser(user) => - sender ! registerUser(user) - } -} diff --git a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala b/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala deleted file mode 100644 index 7074074..0000000 --- a/maven/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala +++ /dev/null @@ -1,25 +0,0 @@ -package org.cakesolutions.akkapatterns.core - -import akka.actor.{Props, ActorSystem} -import application.ApplicationActor -import akka.pattern.ask -import akka.util.Timeout -import akka.dispatch.Await - -case class Start() -case class Started() - -case class Stop() - -trait Core { - implicit def actorSystem: ActorSystem - implicit val timeout = Timeout(30000) - - val application = actorSystem.actorOf( - props = Props[ApplicationActor], - name = "application" - ) - - Await.ready(application ? Start(), timeout.duration) - -} diff --git a/maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerActorSpecx.scala b/maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerActorSpecx.scala deleted file mode 100644 index 91625c1..0000000 --- a/maven/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerActorSpecx.scala +++ /dev/null @@ -1,46 +0,0 @@ -package org.cakesolutions.akkapatterns.core - -import application.{Insert, FindAll, Get, CustomerActor} -import org.cakesolutions.akkapatterns.test.{SpecConfiguration, DefaultTestData} -import org.specs2.mutable.Specification -import akka.testkit.{ImplicitSender, TestActorRef, TestKit} -import akka.actor.ActorSystem -import org.cakesolutions.akkapatterns.domain.Customer -import org.specs2.runner.JUnitRunner -import org.junit.runner.RunWith - -/** - * @author janmachacek - */ -@RunWith(classOf[JUnitRunner]) -class CustomerActorSpecx extends TestKit(ActorSystem()) with Specification with SpecConfiguration with DefaultTestData with ImplicitSender { - val customerActor = TestActorRef[CustomerActor] - - "Getting a known customer works" in { - customerActor ! Get(janMachacek.id) - expectMsg(Some(janMachacek)) - - success - } - - "Finding all customers includes jan" in { - customerActor ! FindAll() - val customers = expectMsgType[List[Customer]] - - customers must contain (janMachacek) - } - - "Inserting a new customer must then find it" in { - customerActor ! Get(joeBloggs.id) - expectMsg(None) - - customerActor ! Insert(joeBloggs) - val insertedJoeBloggs = expectMsgType[Customer] - - customerActor ! Get(insertedJoeBloggs.id) - val loadedJoeBloggs = expectMsgType[Option[Customer]].get - - loadedJoeBloggs.firstName must_== insertedJoeBloggs.firstName - } - -} diff --git a/maven/domain/pom.xml b/maven/domain/pom.xml deleted file mode 100644 index fe0375f..0000000 --- a/maven/domain/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - domain - jar - Akka Patterns - Domain - - - org.specs2 - specs2_${scala.version} - - - - - - diff --git a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala b/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala deleted file mode 100644 index 4a186aa..0000000 --- a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala +++ /dev/null @@ -1,43 +0,0 @@ -package org.cakesolutions.akkapatterns.domain - -import collection.mutable - -private object ConfigurationStore { - val entries = mutable.Map[String, AnyRef]() - - def put(key: String, value: AnyRef) { - entries += ((key, value)) - } - - def get[A: Manifest] = { - val erasure = manifest[A].erasure - entries.values.find(x => erasure.isAssignableFrom(x.getClass)) match { - case Some(v) => Some(v.asInstanceOf[A]) - case None => None - } - } -} - -trait Configured { - - def configured[A: Manifest, U](f: A => U) = f(ConfigurationStore.get[A].get) - - def configured[A: Manifest] = ConfigurationStore.get[A].get - -} - -trait Configuration { - - final def configure[A <: AnyRef](tag: String)(f: => A) = { - val a = f - ConfigurationStore.put(tag, a) - a - } - - final def configure[A <: AnyRef](f: => A) = { - val a = f - ConfigurationStore.put(a.getClass.getName, a) - a - } - -} \ No newline at end of file diff --git a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala b/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala deleted file mode 100644 index 545d80b..0000000 --- a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala +++ /dev/null @@ -1,9 +0,0 @@ -package org.cakesolutions.akkapatterns.domain - -import java.util.UUID - -case class Customer(firstName: String, lastName: String, - email: String, addresses: Seq[Address], - id: UUID) - -case class Address(line1: String, line2: String, line3: String) \ No newline at end of file diff --git a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala b/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala deleted file mode 100644 index 66f2aa9..0000000 --- a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala +++ /dev/null @@ -1,9 +0,0 @@ -package org.cakesolutions.akkapatterns - -import java.util.UUID - -package object domain { - type Identity = UUID - - -} diff --git a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala b/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala deleted file mode 100644 index 9b0546f..0000000 --- a/maven/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala +++ /dev/null @@ -1,6 +0,0 @@ -package org.cakesolutions.akkapatterns.domain - -/** - * @author janmachacek - */ -case class User(id: Identity, username: String, password: String) \ No newline at end of file diff --git a/maven/main/pom.xml b/maven/main/pom.xml deleted file mode 100644 index 9d2c5e2..0000000 --- a/maven/main/pom.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - main - jar - Akka Patterns - Main - - - org.cakesolutions.akkapatterns - core - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - api - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - domain - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - web - 0.1.RELEASE-SNAPSHOT - - - cc.spray - spray-server - - - cc.spray - spray-io - - - com.typesafe.akka - akka-actor_${scala.version} - - - - - - - - - - maven-jar-plugin - 2.3.1 - - - - true - true - lib/ - com.orchestra.main.Main - - - ${buildNumber} ${scmBranch} - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - prepare-package - - copy-dependencies - - - ${project.build.directory}/lib - false - false - true - - - - - - - diff --git a/maven/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala b/maven/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala deleted file mode 100644 index 8f5c34b..0000000 --- a/maven/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala +++ /dev/null @@ -1,23 +0,0 @@ -package org.cakesolutions.akkapatterns.main - -import akka.actor.ActorSystem -import org.cakesolutions.akkapatterns.domain.Configuration -import org.cakesolutions.akkapatterns.core.Core -import org.cakesolutions.akkapatterns.api.Api -import org.cakesolutions.akkapatterns.web.Web - -object Main extends App { - // -javaagent:/Users/janmachacek/.m2/repository/org/springframework/spring-instrument/3.1.1.RELEASE/spring-instrument-3.1.1.RELEASE.jar -Xmx512m -XX:MaxPermSize=256m - - implicit val system = ActorSystem("AkkaPatterns") - - class Application(val actorSystem: ActorSystem) extends Core with Api with Web with Configuration { - } - - new Application(system) - - sys.addShutdownHook { - system.shutdown() - } - -} diff --git a/maven/parent/pom.xml b/maven/parent/pom.xml deleted file mode 100644 index 8ff952a..0000000 --- a/maven/parent/pom.xml +++ /dev/null @@ -1,270 +0,0 @@ - - - 4.0.0 - org.cakesolutions.akkapatterns - server-parent - pom - 0.1.RELEASE-SNAPSHOT - Akka Patterns - Parent - - scm:git:git@github.com:janm399/akka-patterns.git - - - 2.9.2 - 3.0.1 - 2.0.1 - UTF-8 - 1.0-M2 - - - - - - - net.alchim31.maven - scala-maven-plugin - 3.1.0 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - net.alchim31.maven - scala-maven-plugin - - - - compile - testCompile - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.4 - - true - true - - - - process-test-classes-dependency-analyze - - process-test-classes - - analyze-only - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.12 - - - - **/*Spec.java - - 1 - - ${settings.localRepository}/org/scala-lang/scala-reflect/${scala.version}/scala-reflect-${scala.version}.jar - - - - - org.codehaus.mojo - buildnumber-maven-plugin - 1.1 - - - validate - - create - - - - - true - false - false - ${basedir}/buildnumber.properties - - true - {0,date,yyyy-MM-dd HH:mm:ss} - - timestamp - - - - - - - - - net.alchim31.maven - scala-maven-plugin - 3.1.0 - - - - - - - - org.scala-lang - scala-library - ${scala.version} - - - - - com.typesafe.akka - akka-kernel_${scala.version} - ${akka.version} - - - com.typesafe.akka - akka-actor_${scala.version} - ${akka.version} - - - com.typesafe.akka - akka-remote_${scala.version} - ${akka.version} - - - com.typesafe.akka - akka-testkit_${scala.version} - ${akka.version} - - - - - cc.spray - spray-server - ${spray.version} - - - cc.spray - spray-io - ${spray.version} - - - cc.spray - spray-can - ${spray.version} - - - cc.spray - spray-base - ${spray.version} - - - cc.spray - spray-util - ${spray.version} - - - org.jvnet - mimepull - 1.6 - - - org.parboiled - parboiled-scala - 1.0.2 - - - - - commons-dbcp - commons-dbcp - 1.4 - - - org.hsqldb - hsqldb - 2.2.4 - - - - - net.liftweb - lift-json_2.9.1 - 2.4 - - - org.scala-lang - scala-library - - - org.scala-lang - scalap - - - - - - junit - junit-dep - 4.8.2 - test - - - - - org.apache.cassandra - cassandra-thrift - 1.1.1 - - - commons-pool - commons-pool - 1.6 - - - - - - org.specs2 - specs2_${scala.version} - 1.11 - test - - - org.specs2 - specs2-scalaz-core_${scala.version} - 6.0.1 - test - - - com.typesafe.akka - akka-testkit - test - ${akka.version} - - - - - - - - org.scala-lang - scala-library - ${scala.version} - - - diff --git a/maven/pom.xml b/maven/pom.xml deleted file mode 100644 index f717781..0000000 --- a/maven/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - 4.0.0 - org.cakesolutions.akkapatterns - server - pom - 0.1.RELEASE-SNAPSHOT - Akka Patterns - - - janmachacek - Jan Machacek - janm@cakesolutions.net - - Project Admin - Developer - - +0 - - - - - parent - - - domain - - test - - - core - api - web - - - main - - diff --git a/maven/test/pom.xml b/maven/test/pom.xml deleted file mode 100644 index 3c7e4eb..0000000 --- a/maven/test/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - test - jar - Akka Patterns - Test support - - - org.specs2 - specs2_${scala.version} - - - org.cakesolutions.akkapatterns - domain - 0.1.RELEASE-SNAPSHOT - - - - - - diff --git a/maven/test/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js b/maven/test/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js deleted file mode 100644 index 3f50eac..0000000 --- a/maven/test/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js +++ /dev/null @@ -1,14 +0,0 @@ -db.getCollectionNames().forEach(function (name) { - if (name.indexOf("system.") == -1) db.getCollection(name).remove() -}); - -db.customers.save({ - "firstName":"Jan", - "lastName":"Machacek", - "email":"janm@cakesolutions.net", - "id":UUID("00000000000000000000000000000000"), - "addresses":[ - {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"}, - {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"} - ] -}); \ No newline at end of file diff --git a/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/DefaultTestData.scala b/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/DefaultTestData.scala deleted file mode 100644 index 9434d2d..0000000 --- a/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/DefaultTestData.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.cakesolutions.akkapatterns.test - -import org.cakesolutions.akkapatterns.domain.{Address, Customer} -import java.util.UUID - - -/** - * @author janmachacek - */ -trait DefaultTestData { - val janMachacek = Customer("Jan", "Machacek", "janm@cakesolutions.net", - Address("Magdalen Centre", "Robert Robinson Avenue", "Oxford") :: - Address("Houldsworth Mill", "Houldsworth Street", "Reddish") :: Nil, - UUID.fromString("00000000-0000-0000-0000-000000000000")) - - val joeBloggs = Customer("Joe", "Bloggs", "joe@cakesolutions.net", - Address("123 Winding Road", "Cowley", "Oxford") :: Nil, - UUID.fromString("00000000-0000-0000-0100-000000000000")) - -} diff --git a/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/SpecConfiguration.scala b/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/SpecConfiguration.scala deleted file mode 100644 index a3fa0b2..0000000 --- a/maven/test/src/main/scala/org/cakesolutions/akkapatterns/test/SpecConfiguration.scala +++ /dev/null @@ -1,11 +0,0 @@ -package org.cakesolutions.akkapatterns.test - -import org.cakesolutions.akkapatterns.domain.Configuration - -/** - * @author janmachacek - */ -trait SpecConfiguration extends Configuration { - - -} diff --git a/maven/web/pom.xml b/maven/web/pom.xml deleted file mode 100644 index c526301..0000000 --- a/maven/web/pom.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - org.cakesolutions.akkapatterns - server-parent - 0.1.RELEASE-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - web - jar - Akka Patterns - Web server - - - org.cakesolutions.akkapatterns - api - 0.1.RELEASE-SNAPSHOT - - - org.cakesolutions.akkapatterns - core - 0.1.RELEASE-SNAPSHOT - - - - cc.spray - spray-can - - - cc.spray - spray-server - - - cc.spray - spray-io - - - com.typesafe.akka - akka-actor_${scala.version} - - - - diff --git a/maven/web/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala b/maven/web/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala deleted file mode 100644 index ee35db8..0000000 --- a/maven/web/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala +++ /dev/null @@ -1,36 +0,0 @@ -package org.cakesolutions.akkapatterns.web - -import akka.actor.Props -import cc.spray.can.server.HttpServer -import cc.spray.io.IoWorker -import cc.spray.io.pipelines.MessageHandlerDispatch -import org.cakesolutions.akkapatterns.core.Core -import org.cakesolutions.akkapatterns.api.Api -import cc.spray.SprayCanRootService - -trait Web { - this: Api with Core => - - // every spray-can HttpServer (and HttpClient) needs an IoWorker for low-level network IO - // (but several servers and/or clients can share one) - val ioWorker = new IoWorker(actorSystem).start() - - // create and start the spray-can HttpServer, telling it that we want requests to be - // handled by the root service actor - val sprayCanServer = actorSystem.actorOf( - Props(new HttpServer(ioWorker, MessageHandlerDispatch.SingletonHandler( - actorSystem.actorOf(Props(new SprayCanRootService(rootService)))), actorSystem.settings.config)), - name = "http-server" - ) - - // a running HttpServer can be bound, unbound and rebound - // initially to need to tell it where to bind to - sprayCanServer ! HttpServer.Bind("0.0.0.0", 8080) - - // finally we drop the api thread but hook the shutdown of - // our IoWorker into the shutdown of the applications ActorSystem - actorSystem.registerOnTermination { - ioWorker.stop() - } - -} \ No newline at end of file diff --git a/sbt/build.sbt b/sbt/build.sbt deleted file mode 100644 index 8b2699a..0000000 --- a/sbt/build.sbt +++ /dev/null @@ -1,87 +0,0 @@ -import sbtrelease._ - -/** Project */ -name := "akka-patterns" - -version := "1.0" - -organization := "org.cakesolutions" - -scalaVersion := "2.10.0" - -/** Shell */ -shellPrompt := { state => System.getProperty("user.name") + "> " } - -shellPrompt in ThisBuild := { state => Project.extract(state).currentRef.project + "> " } - -/** Dependencies */ -resolvers += "snapshots-repo" at "http://scala-tools.org/repo-snapshots" - -resolvers += "spray repo" at "http://repo.spray.io" - -resolvers += "spray nightlies" at "http://nightlies.spray.io" - -resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" - -resolvers += "Jasper Community" at "http://jasperreports.sourceforge.net/maven2" - -resolvers += "Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/" - -resolvers += "Sonatype OSS Snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/" - -resolvers += "neo4j repo" at "http://m2.neo4j.org/content/repositories/releases/" - -resolvers += "neo4j snapshot repo" at "http://m2.neo4j.org/content/groups/public/" - -libraryDependencies <<= scalaVersion { scala_version => - val sprayVersion = "1.1-20130207" - val akkaVersion = "2.1.0" - val scalazVersion = "7.0.0-M8" - Seq( - "com.typesafe.akka" %% "akka-kernel" % akkaVersion, - "com.typesafe.akka" %% "akka-actor" % akkaVersion, - "io.spray" % "spray-can" % sprayVersion, - "io.spray" % "spray-routing" % sprayVersion, - "io.spray" % "spray-httpx" % sprayVersion, - "io.spray" % "spray-util" % sprayVersion, - "io.spray" % "spray-client" % sprayVersion, - "com.aphelia" %% "amqp-client" % "1.0", - "com.rabbitmq" % "amqp-client" % "2.8.1", - "org.neo4j" % "neo4j" % "1.9-M02", - "org.scalaz" %% "scalaz-effect" % scalazVersion, - "org.scalaz" %% "scalaz-core" % scalazVersion, - "org.cakesolutions" % "scalad_2.10" % "1.0", - "net.sf.jasperreports" % "jasperreports" % "5.0.1", - "org.apache.poi" % "poi" % "3.9", - "io.spray" %% "spray-json" % "1.2.3", - "javax.mail" % "mail" % "1.4.2", - "org.specs2" % "classycle" % "1.4.1" % "test", - "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test", - "org.specs2" %% "specs2" % "1.13" % "test" - ) -} - -/** Compilation */ -javaOptions += "-Xmx2G" - -scalacOptions ++= Seq("-deprecation", "-unchecked") - -maxErrors := 20 - -pollInterval := 1000 - -logBuffered := false - -cancelable := true - -testOptions := Seq(Tests.Filter(s => - Seq("Spec", "Suite", "Unit", "all").exists(s.endsWith(_)) && - !s.endsWith("FeaturesSpec") || - s.contains("UserGuide") || - s.contains("index") || - s.matches("org.specs2.guide.*"))) - -/** Console */ -initialCommands in console := "import org.cakesolutions.akkapatterns._" - -seq(ScctPlugin.instrumentSettings : _*) diff --git a/sbt/project/plugins.sbt b/sbt/project/plugins.sbt deleted file mode 100644 index 610e31c..0000000 --- a/sbt/project/plugins.sbt +++ /dev/null @@ -1,9 +0,0 @@ -resolvers += "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/" - -resolvers += "scct-github-repository" at "http://mtkopone.github.com/scct/maven-repo" - -addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0-SNAPSHOT") - -addSbtPlugin("com.github.gseitz" % "sbt-release" % "0.6") - -addSbtPlugin("reaktor" % "sbt-scct" % "0.2-SNAPSHOT") diff --git a/sbt/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js b/sbt/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js deleted file mode 100644 index 3f50eac..0000000 --- a/sbt/src/main/resources/org/cakesolutions/akkapatterns/mongodb-base.js +++ /dev/null @@ -1,14 +0,0 @@ -db.getCollectionNames().forEach(function (name) { - if (name.indexOf("system.") == -1) db.getCollection(name).remove() -}); - -db.customers.save({ - "firstName":"Jan", - "lastName":"Machacek", - "email":"janm@cakesolutions.net", - "id":UUID("00000000000000000000000000000000"), - "addresses":[ - {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"}, - {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"} - ] -}); \ No newline at end of file diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala deleted file mode 100644 index b3f1aa8..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala +++ /dev/null @@ -1,28 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.{ActorRef, Props} -import spray._ -import routing._ -import http.{StatusCodes, HttpResponse} -import org.cakesolutions.akkapatterns.core.ServerCore -import akka.util.Timeout - -trait Api extends RouteConcatenation { - this: ServerCore => - - val routes = - new HomeService().route ~ - new CustomerService().route - - def rejectionHandler: PartialFunction[scala.List[Rejection], HttpResponse] = { - case (rejections: List[Rejection]) => HttpResponse(StatusCodes.BadRequest) - } - - val rootService = actorSystem.actorOf(Props(new RoutedHttpService(routes))) - -} - -trait DefaultTimeout { - final implicit val timeout = Timeout(3000) - -} \ No newline at end of file diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala deleted file mode 100644 index d7c6c48..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala +++ /dev/null @@ -1,37 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.ActorSystem -import spray.routing.Directives -import spray.httpx.marshalling.MetaMarshallers -import org.cakesolutions.akkapatterns.domain.{CustomerFormats, Customer} -import org.cakesolutions.akkapatterns.core.UpdateCustomer -import spray.httpx.SprayJsonSupport - -/** - * @author janmachacek - */ -class CustomerService(implicit val actorSystem: ActorSystem) extends Directives with MetaMarshallers with DefaultTimeout - with DefaultAuthenticationDirectives with CustomerFormats with SprayJsonSupport { - import akka.pattern.ask - import concurrent.ExecutionContext.Implicits.global - - def customerActor = actorSystem.actorFor("/user/application/customer") - - val route = - path("customers" / JavaUUID) { id => - get { - complete { - "Just you wait" - } - } ~ - post { - authenticate(validCustomer) { ud => - // if we authenticated only validUser or validSuperuser - handleWith { customer: Customer => - (customerActor ? UpdateCustomer(ud, customer)).mapTo[Customer] - // then this call would not type-check! - } - } - } - } -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala deleted file mode 100644 index 09981e5..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala +++ /dev/null @@ -1,41 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import java.net.InetAddress -import akka.actor.ActorSystem -import akka.pattern.ask -import spray.httpx.marshalling.MetaMarshallers -import spray.routing.Directives -import org.cakesolutions.akkapatterns.core.{ GetImplementation, Implementation } -import java.util.Date -import scala.concurrent.ExecutionContext.Implicits._ -import spray.json.DefaultJsonProtocol -import spray.httpx.SprayJsonSupport - -trait HomerServiceMarshalling extends DefaultJsonProtocol { - - implicit val ImplementationFormat = jsonFormat3(Implementation) - implicit val SystemInfoFormat = jsonFormat3(SystemInfo) - -} - -case class SystemInfo(implementation: Implementation, host: String, timestamp: Long) - -class HomeService(implicit val actorSystem: ActorSystem) extends Directives with HomerServiceMarshalling with MetaMarshallers with SprayJsonSupport with DefaultTimeout { - - def applicationActor = actorSystem.actorFor("/user/application") - - val route = { - path(Slash) { - get { - complete { - val f =(applicationActor ? GetImplementation()).mapTo[Implementation].map { - SystemInfo(_, InetAddress.getLocalHost.getCanonicalHostName, new Date().getTime) - } - - f - } - } - } - } - -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala deleted file mode 100644 index 8ef426f..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala +++ /dev/null @@ -1,60 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import org.cakesolutions.akkapatterns.domain._ -import org.cakesolutions.akkapatterns.core._ -import spray.json.DefaultJsonProtocol -import org.cakesolutions.akkapatterns.UuidFormats -import spray.httpx.SprayJsonSupport -import spray.httpx.marshalling.{CollectingMarshallingContext, MetaMarshallers, Marshaller} -import spray.http.{HttpEntity, StatusCode} - -trait AllFormats extends UserFormats { - - implicit val NotRegisteredUserFormat = jsonFormat1(NotRegisteredUser) - implicit val RegisteredUserFormat = jsonFormat1(RegisteredUser) - - implicit val AddressFormat = jsonFormat3(Address) - implicit val CustomerFormat = jsonFormat5(Customer) - implicit val RegisterCustomerFormat = jsonFormat2(RegisterCustomer) - implicit val NotRegisteredCustomerFormat = jsonFormat1(NotRegisteredCustomer) - implicit val RegisteredCustomerFormat = jsonFormat2(RegisteredCustomer) - -} - -trait Marshalling extends DefaultJsonProtocol with AllFormats with UuidFormats with SprayJsonSupport with MetaMarshallers { - /** - * Function that computers a HTTP status given value of type ``A`` - * - * @tparam A the type a - */ - type ErrorSelector[A] = A => StatusCode - - /** - * Marshaller that uses some ``ErrorSelector`` for the value on the left to indicate that it is an error, even though - * the error response should still be marshalled and returned to the caller. - * - * This is useful when you need to return validation or other processing errors, but need a bit more information than - * just ``HTTP status 422`` (or, even worse simply ``400``). - * - * @param ma the marshaller for ``A`` (the error) - * @param mb the marshaller for ``B`` (the success) - * @param esa the error selector for ``A`` so that we know what HTTP status to return for the value on the left - * @tparam A the type on the left - * @tparam B the type on the right - * @return the marshaller instance - */ - implicit def errorSelectingEitherMarshaller[A, B](implicit ma: Marshaller[A], mb: Marshaller[B], esa: ErrorSelector[A]) = - Marshaller[Either[A, B]] { (value, ctx) => - value match { - case Left(a) => - val mc = new CollectingMarshallingContext() - ma(a, mc) - ctx.handleError(ErrorResponseException(esa(a), mc.entity)) - case Right(b) => - mb(b, ctx) - } - } - -} - -case class ErrorResponseException(responseStatus: StatusCode, response: Option[HttpEntity]) extends RuntimeException diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala deleted file mode 100644 index 45bf125..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala +++ /dev/null @@ -1,29 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.Actor -import spray.routing._ -import util.control.NonFatal -import spray.http.StatusCodes._ -import spray.util.LoggingContext - -/** - * @author janmachacek - */ -class RoutedHttpService(route: Route) extends Actor with HttpService { - - implicit def actorRefFactory = context - - implicit val handler = ExceptionHandler.fromPF { - case NonFatal(ErrorResponseException(statusCode, entity)) => ctx => - ctx.complete(statusCode, entity) - - case NonFatal(e) => ctx => - ctx.complete(InternalServerError) - } - - - def receive = { - runRoute(route)(handler, RejectionHandler.Default, context, RoutingSettings.Default, LoggingContext.fromActorRefFactory) - } - -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala deleted file mode 100644 index 57f41cc..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala +++ /dev/null @@ -1,27 +0,0 @@ -package org.cakesolutions.akkapatterns.api - -import akka.actor.ActorSystem -import spray.httpx.marshalling.MetaMarshallers -import spray.routing.Directives -import org.cakesolutions.akkapatterns.domain.{UserFormats, User} -import spray.httpx.SprayJsonSupport - -/** - * @author janmachacek - */ -class UserService(implicit val actorSystem: ActorSystem) extends Directives with DefaultTimeout with UserFormats with MetaMarshallers with SprayJsonSupport { - def userActor = actorSystem.actorFor("/user/application/user") - - val route = - path("user" / "register") { - post { - entity(as[User]) { user => - complete { - // (userActor ? RegisteredUser(user)).mapTo[Either[NotRegisteredUser, RegisteredUser]] - "Wait a bit!" - } - } - } - } - -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala deleted file mode 100644 index 6b69e70..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala +++ /dev/null @@ -1,35 +0,0 @@ -package org.cakesolutions.akkapatterns.core - -import akka.actor.{Props, Actor} - -case class GetImplementation() -case class Implementation(title: String, version: String, build: String) - -class ApplicationActor extends Actor { - - def receive = { - case GetImplementation() => - val title = "Akka-Patterns" - val version = "1.0" - val build = "1.0" - - sender ! Implementation(title, version, build) - - case Start() => - val messageDelivery = context.actorOf(Props[MessageDeliveryActor], "messageDelivery") - context.actorOf(Props(new CustomerActor(messageDelivery)), "customer") - context.actorOf(Props(new UserActor(messageDelivery)), "user") - - new SanityChecks { - sender ! (if (ensureSanity) Started() else InmatesAreRunningTheAsylum) - } - - /* - * Stops this actor and all the child actors. - */ - case Stop() => - context.children.foreach(context.stop _) - - } - -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala deleted file mode 100644 index 1ca5738..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala +++ /dev/null @@ -1,65 +0,0 @@ -package org.cakesolutions.akkapatterns.core - -import akka.actor.{ActorRef, Actor} -import org.cakesolutions.akkapatterns.domain._ - -/** - * Registers a customer and a user. After registering, we have a user account for the given customer. - * - * @param customer the customer - * @param user the user - */ -case class RegisterCustomer(customer: Customer, user: User) - -/** - * Reply to successful customer registration - * @param customer the newly registered customer - * @param user the newly registered user - */ -case class RegisteredCustomer(customer: Customer, user: User) - -/** - * Reply to unsuccessful customer registration - * @param code the error code for the failure reason - */ -case class NotRegisteredCustomer(code: String) extends ApplicationFailure - -/** - * Update the customer details - * - * @param userDetail the user making the call - * @param customer the customer to be updated - */ -case class UpdateCustomer(userDetail: UserDetailT[CustomerUserKind], customer: Customer) - -/** - * CRUD operations for the [[org.cakesolutions.akkapatterns.domain.Customer]]s - */ -trait CustomerOperations { - // def customers: MongoCollection - - def getCustomer(id: CustomerReference): Option[Customer] = None - - def findAllCustomers(): List[Customer] = List() - - def insertCustomer(customer: Customer): Customer = { - //customers += serialize(customer) - customer - } - - def registerCustomer(customer: Customer)(ru: RegisteredUser): Either[ApplicationFailure, RegisteredCustomer] = { - //customers += serialize(customer) - Right(RegisteredCustomer(customer, ru.user)) - } - -} - -/** - * Performs the customer operations - */ -class CustomerActor(messageDelivery: ActorRef) extends Actor with CustomerOperations with UserOperations { - - def receive = { - case _ => // TODO: complete me by moving me to Scalad - } -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala deleted file mode 100644 index f538593..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala +++ /dev/null @@ -1,17 +0,0 @@ -package org.cakesolutions.akkapatterns.domain - -import spray.json.DefaultJsonProtocol -import org.cakesolutions.akkapatterns.UuidFormats - -case class Customer(id: CustomerReference, - firstName: String, lastName: String, - email: String, addresses: Seq[Address]) - -case class Address(line1: String, line2: String, line3: String) - -trait CustomerFormats extends DefaultJsonProtocol with UuidFormats { - - implicit val AddressFormat = jsonFormat3(Address) - implicit val CustomerFormat = jsonFormat5(Customer) - -} \ No newline at end of file diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/io.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/io.scala deleted file mode 100644 index f9e043b..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/io.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.cakesolutions.akkapatterns - -import akka.actor.{Props, Actor, ActorSystem} -import spray.io.IOExtension -import com.typesafe.config.ConfigFactory -import spray.client.HttpClient -import com.aphelia.amqp.ConnectionOwner -import com.aphelia.amqp.Amqp.ExchangeParameters -import com.rabbitmq.client.ConnectionFactory - -/** - * Instantiates & provides access to Spray's ``IOBridge``. - * - * @author janmachacek - */ -trait HttpIO { - implicit def actorSystem: ActorSystem - - lazy val ioBridge = IOExtension(actorSystem).ioBridge() // new IOBridge(actorSystem).start() - - lazy val httpClient = actorSystem.actorOf( - Props(new HttpClient(ConfigFactory.parseString("spray.can.client.ssl-encryption = on"))) - ) - -} - -/** - * Convenience ``HttpIO`` implementation that can be mixed in to actors. - */ -trait ActorHttpIO extends HttpIO { - this: Actor => - - final implicit def actorSystem = context.system -} - -/** - * Provides connection & access to the AMQP broker - */ -trait AmqpIO { - implicit def actorSystem: ActorSystem - - // prepare the AMQP connection factory - final lazy val connectionFactory = new ConnectionFactory(); connectionFactory.setHost("localhost") - // connect to the AMQP exchange - final lazy val amqpExchange = ExchangeParameters(name = "amq.direct", exchangeType = "", passive = true) - - // create a "connection owner" actor, which will try and reconnect automatically if the connection ins lost - val connection = actorSystem.actorOf(Props(new ConnectionOwner(connectionFactory))) - -} - -/** - * Convenience ``AmqpIO`` implementation that can be mixed in to actors. - */ -trait ActorAmqpIO extends AmqpIO { - this: Actor => - final implicit def actorSystem = context.system - -} \ No newline at end of file diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/marshalling.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/marshalling.scala deleted file mode 100644 index 0aa00a4..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/marshalling.scala +++ /dev/null @@ -1,14 +0,0 @@ -package org.cakesolutions.akkapatterns - -import spray.json._ -import java.util.UUID - -trait UuidFormats { - implicit object UuidJsonFormat extends JsonFormat[UUID] { - def write(x: UUID) = JsString(x toString ()) - def read(value: JsValue) = value match { - case JsString(x) => UUID.fromString(x) - case x => deserializationError("Expected UUID as JsString, but got " + x) - } - } -} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala b/sbt/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala deleted file mode 100644 index 369005b..0000000 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.cakesolutions.akkapatterns.web - -import org.cakesolutions.akkapatterns.core.ServerCore -import org.cakesolutions.akkapatterns.api.Api -import spray.io.{SingletonHandler, IOBridge} -import spray.can.server.HttpServer -import akka.actor.Props -import org.cakesolutions.akkapatterns.HttpIO - -trait Web extends HttpIO { - this: Api with ServerCore => - - // every spray-can HttpServer (and HttpClient) needs an IOBridge for low-level network IO - // (but several servers and/or clients can share one) - // val ioBridge = new IOBridge(actorSystem).start() - - // create and start the spray-can HttpServer, telling it that - // we want requests to be handled by our singleton service actor - val httpServer = actorSystem.actorOf( - Props(new HttpServer(SingletonHandler(rootService))), - name = "http-server" - ) - - // a running HttpServer can be bound, unbound and rebound - // initially to need to tell it where to bind to - httpServer ! HttpServer.Bind("localhost", 8080) - - // finally we drop the main thread but hook the shutdown of - // our IOBridge into the shutdown of the applications ActorSystem - actorSystem.registerOnTermination { - // ioBridge ! Stop - } - -} diff --git a/sbt/src/test/scala/org/cakesolutions/akkapatterns/UserActorSpec.scala b/sbt/src/test/scala/org/cakesolutions/akkapatterns/UserActorSpec.scala deleted file mode 100644 index 938189a..0000000 --- a/sbt/src/test/scala/org/cakesolutions/akkapatterns/UserActorSpec.scala +++ /dev/null @@ -1,26 +0,0 @@ -package org.cakesolutions.akkapatterns - -import core.{GetUserByUsername, UserActor, SanityChecks} -import org.specs2.mutable.Specification -import akka.actor.ActorSystem -import akka.testkit.{ImplicitSender, TestActorRef, TestKit} - -/** - * @author janmachacek - */ -class UserActorSpec extends TestKit(ActorSystem()) with Specification with SanityChecks with ImplicitSender { - sequential - ensureSanity - - val actor = TestActorRef(new UserActor(testActor)) - - "Basic user operations" should { - - "Find the root user" in { - actor ! GetUserByUsername("root") - expectMsg(Some(RootUser)) - success - } - } - -} diff --git a/sbt/src/test/scala/org/cakesolutions/akkapatterns/specifications.scala b/sbt/src/test/scala/org/cakesolutions/akkapatterns/specifications.scala deleted file mode 100644 index 83cc46b..0000000 --- a/sbt/src/test/scala/org/cakesolutions/akkapatterns/specifications.scala +++ /dev/null @@ -1,20 +0,0 @@ -package org.cakesolutions.akkapatterns - -import akka.testkit.TestKit -import akka.actor.ActorSystem -import com.typesafe.config.ConfigFactory - -/** - * @author janmachacek - */ -trait Timers { - - def timed[U](f: => U): Long = { - val startTime = System.currentTimeMillis - f - System.currentTimeMillis - startTime - } - -} - -class ConfiguredTestKit extends TestKit(ActorSystem("Test", ConfigFactory.load("server.conf"))) \ No newline at end of file diff --git a/sbt/src/test/scala/org/cakesolutions/akkapatterns/testdata.scala b/sbt/src/test/scala/org/cakesolutions/akkapatterns/testdata.scala deleted file mode 100644 index b6d287f..0000000 --- a/sbt/src/test/scala/org/cakesolutions/akkapatterns/testdata.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.cakesolutions.akkapatterns - -import domain._ -import java.util.UUID - -/** - * @author janmachacek - */ -trait TestData { - - object Users { - - def newUser(username: String): User = User(UUID.randomUUID(), username, "", "janm@cakesolutions.net", None, "F" + username, "L" + username, SuperuserKind).resetPassword("password") - } - -} diff --git a/sbt/version.sbt b/sbt/version.sbt deleted file mode 100644 index 5e469a2..0000000 --- a/sbt/version.sbt +++ /dev/null @@ -1,2 +0,0 @@ - -version in ThisBuild := "1.0" diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala similarity index 67% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala rename to server/api/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala index ad90200..33fa306 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/authentication.scala @@ -1,12 +1,14 @@ package org.cakesolutions.akkapatterns.api -import spray.routing.{RequestContext, AuthenticationFailedRejection, AuthenticationRequiredRejection} +import spray.routing.{HttpService, RequestContext, AuthenticationFailedRejection, AuthenticationRequiredRejection} import concurrent.Future import spray.routing.authentication.Authentication import java.util.UUID -import akka.actor.ActorSystem +import akka.actor.ActorRef import org.cakesolutions.akkapatterns.domain._ import org.cakesolutions.akkapatterns.core.authentication.TokenCheck +import akka.util.Timeout +import spray.http.HttpRequest /** * Mix in this trait to get the authentication directive. The ``validUser`` function can be used in Spray's @@ -30,8 +32,7 @@ import org.cakesolutions.akkapatterns.core.authentication.TokenCheck * @author janmachacek */ trait AuthenticationDirectives { - - import concurrent.ExecutionContext.Implicits.global + this: HttpService => /** * @return a `User` that has been previously identified with the `Token` we have been given. @@ -44,15 +45,32 @@ trait AuthenticationDirectives { */ private def doValidUser[A <: UserKind](map: UserDetailT[_] => Authentication[UserDetailT[A]]): RequestContext => Future[Authentication[UserDetailT[A]]] = { ctx: RequestContext => - val header = ctx.request.headers.find(_.name == "x-token") - if (header.isEmpty) - Future(Left(AuthenticationRequiredRejection("https", "zoetic"))) - else doAuthenticate(UUID.fromString(header.get.value)).map { - case Some(user) => map(user) - case None => Left(AuthenticationFailedRejection("Zoetic")) + getToken(ctx.request) match { + case None => Future(Left(AuthenticationRequiredRejection("https", "patterns"))) + case Some(token) => doAuthenticate(token) .map { + case Some(user) => map(user) + case None => Left(AuthenticationFailedRejection("Patterns")) + } } } + // http://en.wikipedia.org/wiki/Universally_unique_identifier + val uuidRegex = """^\p{XDigit}{8}(-\p{XDigit}{4}){3}-\p{XDigit}{12}$""".r + def isUuid(token: String) = token.length == 36 && uuidRegex.findPrefixOf(token).isDefined + + def getToken(request: HttpRequest): Option[UUID] = { + val query = request.queryParams.get("token") + if (query.isDefined && isUuid(query.get)) + Some(UUID.fromString(query.get)) + else { + val header = request.headers.find(_.name == "x-token") + if (header.isDefined && isUuid(header.get.value)) + Some(UUID.fromString(header.get.value)) + else + None + } + } + /** * Checks that the token represents a valid user; i.e. someone is logged in. We make no assumptions about the roles * @@ -88,16 +106,13 @@ trait AuthenticationDirectives { } } -/** - * provides a default implementation for Authentication Directives - */ -trait DefaultAuthenticationDirectives extends AuthenticationDirectives with DefaultTimeout { - this: { def actorSystem: ActorSystem } => - import concurrent.ExecutionContext.Implicits.global - import akka.pattern.ask +trait DefaultAuthenticationDirectives extends AuthenticationDirectives { + this: HttpService => - def loginActor = actorSystem.actorFor("/user/application/authentication/login") + import akka.pattern.ask + implicit val timeout: Timeout + def loginActor: ActorRef override def doAuthenticate(token: UUID) = (loginActor ? TokenCheck(token)).mapTo[Option[UserDetailT[_]]] diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala new file mode 100644 index 0000000..05fe0a8 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/boot.scala @@ -0,0 +1,41 @@ +package org.cakesolutions.akkapatterns.api + +import akka.actor.Actor +import spray._ +import routing._ +import org.cakesolutions.akkapatterns.core.CoreActorRefs +import akka.util.Timeout +import org.cakesolutions.akkapatterns.domain.Configured + +class Api extends Actor with HttpServiceActor + with CoreActorRefs + with FailureHandling + with Tracking with Configured + with EndpointMarshalling + with DefaultAuthenticationDirectives + with CustomerService + with HomeService + with UserService + { + + // used by the Akka ask pattern + implicit val timeout = Timeout(10000) + + // lets the CoreActorRef find the actor system used by Spray + // (this could potentially be a separate system) + def system = actorSystem + + val routes = + customerRoute ~ + homeRoute ~ + userRoute + + def receive = runRoute ( + handleRejections(rejectionHandler)( + handleExceptions(exceptionHandler)( + trackRequestResponse(routes) + ) + ) + ) + +} diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala new file mode 100644 index 0000000..a0c3db3 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/customer.scala @@ -0,0 +1,37 @@ +package org.cakesolutions.akkapatterns.api + +import spray.routing.HttpService +import org.cakesolutions.akkapatterns.domain.Customer +import org.cakesolutions.akkapatterns.core.CustomerController +import akka.util.Timeout +import scala.concurrent.Future +import java.util.Date + +trait CustomerService extends HttpService { + this: EndpointMarshalling with AuthenticationDirectives => + + protected val customerController = new CustomerController + + val customerRoute = + path("customers" / JavaUUID) { id => + get { + complete { + // when using controllers, we have to explicitly create the Future here + // it is not necessary to add the T information, but it helps with API documentation. + Future[Customer] { + customerController.get(id) + } + } + } ~ + authenticate(validCustomer) { ud => + post { + handleWith { customer: Customer => + // if we authenticated only validUser or validSuperuser + Future[Customer] { + customerController.update(ud, customer) + } + } + } + } + } +} diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala new file mode 100644 index 0000000..049d649 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/home.scala @@ -0,0 +1,31 @@ +package org.cakesolutions.akkapatterns.api + +import java.net.InetAddress +import org.cakesolutions.akkapatterns.core.{GetImplementation, Implementation} +import java.util.Date +import spray.routing.HttpService +import akka.util.Timeout +import akka.actor.ActorRef + +case class SystemInfo(implementation: Implementation, host: String, timestamp: Date) + +trait HomeService extends HttpService { + this: EndpointMarshalling with AuthenticationDirectives => + + import akka.pattern.ask + implicit val timeout: Timeout + def applicationActor: ActorRef + + val homeRoute = { + path(Slash) { + get { + complete { + (applicationActor ? GetImplementation()).mapTo[Implementation].map { + SystemInfo(_, InetAddress.getLocalHost.getCanonicalHostName, new Date) + } + } + } + } + } + +} diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala new file mode 100644 index 0000000..bb507e5 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/marshalling.scala @@ -0,0 +1,61 @@ +package org.cakesolutions.akkapatterns.api + +import org.cakesolutions.akkapatterns.domain._ +import org.cakesolutions.akkapatterns.core._ +import spray.json.DefaultJsonProtocol +import spray.httpx.SprayJsonSupport +import spray.httpx.marshalling.{CollectingMarshallingContext, MetaMarshallers, Marshaller} +import spray.http.{HttpEntity, StatusCode} +import org.cakesolutions.scalad.mongo.sprayjson.{DateMarshalling, UuidMarshalling} + +// Pure boilerplate - please create a code generator (I'll be your *best* friend!) +trait ApiMarshalling extends DefaultJsonProtocol + with UuidMarshalling with DateMarshalling { + this: UserFormats => + + implicit val NotRegisteredUserFormat = jsonFormat1(NotRegisteredUser) + implicit val RegisteredUserFormat = jsonFormat1(RegisteredUser) + + implicit val ImplementationFormat = jsonFormat3(Implementation) + implicit val SystemInfoFormat = jsonFormat3(SystemInfo) +} + +case class ErrorResponseException(responseStatus: StatusCode, response: Option[HttpEntity]) extends Exception + +trait EitherErrorMarshalling { + + /** + * Marshaller that uses some ``ErrorSelector`` for the value on the left to indicate that it is an error, even though + * the error response should still be marshalled and returned to the caller. + * + * This is useful when you need to return validation or other processing errors, but need a bit more information than + * just ``HTTP status 422`` (or, even worse simply ``400``). + * + * Bring an implicit instance of this method into scope of your HttpServices to get the status code. + * + * @param status the status code to return for errors. + * @param ma the marshaller for ``A`` (the error) + * @param mb the marshaller for ``B`` (the success) + * @tparam A the type on the left + * @tparam B the type on the right + * @return the marshaller instance + */ + def errorSelectingEitherMarshaller[A, B](status: StatusCode) + (implicit ma: Marshaller[A], mb: Marshaller[B]) = + Marshaller[Either[A, B]] { + (value, ctx) => + value match { + case Left(a) => + val mc = new CollectingMarshallingContext() + ma(a, mc) + ctx.handleError(ErrorResponseException(status, mc.entity)) + case Right(b) => + mb(b, ctx) + } + } +} + +trait EndpointMarshalling extends MetaMarshallers with SprayJsonSupport + with ApiMarshalling + with UserFormats with CustomerFormats + with EitherErrorMarshalling diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala new file mode 100644 index 0000000..400986b --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/services.scala @@ -0,0 +1,48 @@ +package org.cakesolutions.akkapatterns.api + +import spray.http.StatusCodes._ +import spray.http._ +import spray.routing._ +import spray.util.LoggingContext + +/** Provides a hook to catch exceptions and rejections from routes, allowing custom + * responses to be provided, logs to be captured, and potentially remedial actions. + * + * Note that this is not marshalled, but it is possible to do so allowing for a fully + * JSON API (e.g. see how Foursquare do it). + */ +trait FailureHandling { + this: HttpService => + + // For Spray > 1.1-M7 use routeRouteResponse + // see https://groups.google.com/d/topic/spray-user/zA_KR4OBs1I/discussion + def rejectionHandler: RejectionHandler = RejectionHandler.Default + + def exceptionHandler(implicit log: LoggingContext) = ExceptionHandler.fromPF { + + case e: IllegalArgumentException => ctx => + loggedFailureResponse(ctx, e, + message = "The server was asked a question that didn't make sense: " + e.getMessage, + error = NotAcceptable) + + case e: NoSuchElementException => ctx => + loggedFailureResponse(ctx, e, + message = "The server is missing some information. Try again in a few moments.", + error = NotFound) + + case t: Throwable => ctx => + // note that toString here may expose information and cause a security leak, so don't do it. + loggedFailureResponse(ctx, t) + } + + private def loggedFailureResponse(ctx: RequestContext, + thrown: Throwable, + message: String = "The server is having problems.", + error: StatusCode = InternalServerError) + (implicit log: LoggingContext) + { + log.error(thrown, ctx.request.toString()) + ctx.complete(error, message) + } + +} diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/tracking.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/tracking.scala new file mode 100644 index 0000000..7d66c54 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/tracking.scala @@ -0,0 +1,52 @@ +package org.cakesolutions.akkapatterns.api + +import java.util.{UUID, Date} +import spray.json.DefaultJsonProtocol +import org.cakesolutions.scalad.mongo.sprayjson.{SprayMongo, SprayMongoCollection, DateMarshalling, UuidMarshalling} +import org.cakesolutions.akkapatterns.domain.Configured +import com.mongodb.DB +import spray.http._ +import spray.routing._ +import scala.concurrent.Future + +case class TrackingStat(path: String, + ip: Option[String], + auth: Option[UUID], + kind: String, + timestamp: Date = new Date, + id: UUID = UUID.randomUUID()) + +trait TrackingFormats extends DefaultJsonProtocol + with UuidMarshalling with DateMarshalling { + protected implicit val TrackingStatFormat = jsonFormat6(TrackingStat) +} + +trait TrackingMongo extends TrackingFormats with Configured { + protected implicit val EndpointHitStartProvider = new SprayMongoCollection[TrackingStat](configured[DB], "tracking") +} + +trait Tracking extends TrackingMongo { + this: AuthenticationDirectives with HttpService => + + private val trackingMongo = new SprayMongo + + def trackRequestT(request: HttpRequest): Any => Unit = { + val path = request.uri.split('?')(0) // not ideal for parameters in the path, e.g. uuids. + val ip = request.headers.find(_.name == "Remote-Address").map { _.value } + val auth = getToken(request) + val stat = TrackingStat(path, ip, auth, "request") + + // the HttpService dispatcher is used to execute these inserts + Future{trackingMongo.insertFast(stat)} + + // the code is executed when called, so the date is calculated when the response is ready + (r:Any) => (Future{trackingMongo.insertFast(stat.copy(kind = "response", timestamp = new Date))}) + } + + def trackRequestResponse: Directive0 = { + mapRequestContext { ctx => + val logResponse = trackRequestT(ctx.request) + ctx.mapRouteResponse { response => logResponse(response); response} + } + } +} \ No newline at end of file diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala new file mode 100644 index 0000000..31c9f13 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/api/user.scala @@ -0,0 +1,29 @@ +package org.cakesolutions.akkapatterns.api + +import org.cakesolutions.akkapatterns.domain.User +import akka.util.Timeout +import spray.routing.HttpService +import akka.actor.ActorRef +import org.cakesolutions.akkapatterns.core.{NotRegisteredUser, RegisteredUser} + + +trait UserService extends HttpService { + this: EndpointMarshalling with AuthenticationDirectives => + + import akka.pattern.ask + implicit val timeout: Timeout + def userActor: ActorRef + + // will return code 666 if NotRegisteredUser is received + implicit val UserRegistrationErrorMarshaller = errorSelectingEitherMarshaller[NotRegisteredUser, RegisteredUser](666) + + val userRoute = + path("user" / "register") { + post { + handleWith {user: User => + (userActor ? RegisteredUser(user)).mapTo[Either[NotRegisteredUser, RegisteredUser]] + } + } + } + +} diff --git a/server/api/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala b/server/api/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala new file mode 100644 index 0000000..52e8616 --- /dev/null +++ b/server/api/src/main/scala/org/cakesolutions/akkapatterns/web/boot.scala @@ -0,0 +1,17 @@ +package org.cakesolutions.akkapatterns.web + +import org.cakesolutions.akkapatterns.core.ServerCore +import org.cakesolutions.akkapatterns.api.Api +import spray.can.server.SprayCanHttpServerApp +import akka.actor.Props + +trait Web extends SprayCanHttpServerApp { + this: ServerCore => + + override lazy val system = actorSystem + + val service = system.actorOf(Props[Api], "api") + + newHttpServer(service, name = "spray-http-server") ! Bind("0.0.0.0", 8080) + +} diff --git a/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala new file mode 100644 index 0000000..5228dd1 --- /dev/null +++ b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/CustomerServiceSpec.scala @@ -0,0 +1,45 @@ +package org.cakesolutions.akkapatterns.api + +import spray.http.StatusCodes._ +import org.cakesolutions.akkapatterns.TestCustomerData +import org.cakesolutions.akkapatterns.core.Neo4JFixtures +import org.cakesolutions.akkapatterns.MongoCollectionFixture.Fix +import java.util.UUID + + +class CustomerServiceSpec extends ApiSpecs with CustomerService with TestCustomerData with Neo4JFixtures { + + implicit val route = handled(customerRoute) + + "/customers" should { + "fail to GET an invalid customer" in { + suppressNextException + Get("/customers/invalid").failsWith(NotFound) + } + + "GET a valid customer" in new Fix("customers") { + val jan = customerController.get(TestCustomerJanId) + Get(s"/customers/${jan.id}").returns(jan) + } + + "require a valid token to POST" in new Fix("customers") { + val jan = customerController.get(TestCustomerJanId) + val update = jan.copy(lastName = "changed") + suppressNextException + Post(s"/customers/${jan.id}", update).failsWith(MethodNotAllowed) + } + + "update with a valid POST" in new Fix("customers") { + val jan = customerController.get(TestCustomerJanId) + val update = jan.copy(lastName = "changed") + + { + implicit val auth = Token(UUID.randomUUID()) // need correct token + AuthPost(s"/customers/${jan.id}", update).returns(update) + } + + Get(s"/customers/${jan.id}").returns(update) + }.pendingUntilFixed("@janm399 needs to setup a test data fixture for tokens") + + } +} diff --git a/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpecs.scala b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpecs.scala new file mode 100644 index 0000000..cbcc013 --- /dev/null +++ b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/HomeServiceSpecs.scala @@ -0,0 +1,13 @@ +package org.cakesolutions.akkapatterns.api + +class HomeServiceSpecs extends ApiSpecs with HomeService { + + implicit val route = homeRoute + + "/" should { + "return the service info" in { + Get("/").returnsA[SystemInfo] + } + } + +} diff --git a/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala new file mode 100644 index 0000000..f004dc3 --- /dev/null +++ b/server/api/src/test/scala/org/cakesolutions/akkapatterns/api/support.scala @@ -0,0 +1,167 @@ +package org.cakesolutions.akkapatterns.api + +import java.util.UUID +import spray.testkit.Specs2RouteTest +import spray.http.{StatusCode, StatusCodes, HttpRequest} +import spray.http.HttpHeaders.RawHeader +import spray.httpx.RequestBuilding +import spray.httpx.marshalling.Marshaller +import spray.httpx.unmarshalling.Unmarshaller +import spray.routing._ +import org.specs2.mutable.Specification +import akka.contrib.jul.JavaLogging +import spray.routing.{HttpService, Rejection} +import scala.reflect.ClassTag +import org.cakesolutions.akkapatterns.{NoActorSpecs, CleanMongo, ActorSpecs} +import org.cakesolutions.akkapatterns.core.{ServerCore, CoreActorRefs} +import akka.actor.ActorSystem +import scala.concurrent.duration.Duration +import java.util.concurrent.TimeUnit +import akka.util.Timeout +import akka.testkit.TestKit +import org.specs2.specification.{Step, Fragments} +import spray.util.LoggingContext +import spray.http.StatusCodes._ +import org.cakesolutions.akkapatterns.domain.Configured + +case class Token(token: UUID) + +/** Provides default and easy authentication in testkit specs + * on endpoints that use [[org.cakesolutions.akkapatterns.api.AuthenticationDirectives]] + * for authentication. + * + * implicit AuthenticationToken doesn't work because of clashes with Spray :-( + */ +trait AuthenticatedTestkit { + this: Specs2RouteTest => + + private def addAuth(request: HttpRequest)(implicit token: Token) = { + // AuthenticationDirectives expects x-token in the header, even for GET / DELETE + request.copy(headers = RawHeader("x-token", token.token.toString) :: request.headers) + } + + def AuthGet(uri: String)(implicit token: Token) = { + addAuth(RequestBuilding.Get(uri)) + } + + def AuthPost[T](uri: String, content: T) + (implicit token: Token, + marshaller: Marshaller[T]) = { + addAuth(RequestBuilding.Post(uri, content)) + } + + def AuthPut[T](uri: String, content: T) + (implicit token: Token, + marshaller: Marshaller[T]) = { + addAuth(RequestBuilding.Put(uri, content)) + } + + def AuthDelete(uri: String)(implicit token: Token) = { + // AuthenticationDirectives expects x-token in the header, not the query + addAuth(RequestBuilding.Delete(uri)) + } + +} + +// e.g. +// Get("/").succeeds() +// Get("/").returns(myExample) +// val get = Get("/").receive[MyObject] +trait RouteTestNiceness { + this: Specs2RouteTest with Specification with JavaLogging => + + implicit val routeTestTimeout = RouteTestTimeout(Duration(5, TimeUnit.SECONDS)) + + implicit class ImplicitRouteCheck(request: HttpRequest) + (implicit route: Route, timeout: RouteTestTimeout) { + + // `specs` works, but sometimes IntelliJ thinks there are problems + def specs[T](f: => T) {request ~> route ~> check(f)} + + def receive[T](implicit um: Unmarshaller[T]): T = request ~> route ~> check { + try entityAs[T] + catch { + case up: Exception => + if (up.getMessage != null && up.getMessage.startsWith("MalformedContent")) + failure(s"didn't get the response type we expected: ${response.entity}") + else throw up + } + } + + // failures here might say application/json was unexpected. That probably means the + // exception handler kicked in and gave us a failure message instead of a T. + def returns[T](expected: T)(implicit um: Unmarshaller[T]) { + receive[T] === expected + } + + def returnsA[T](implicit um: Unmarshaller[T]) = { + receive[T] + success + } + + def succeeds() {specs(status === StatusCodes.OK)} + + def succeedsWith(code: StatusCode) = specs {status === code} + + // note, the failure type may be different in production: + // we use a different exception handler in testing. + def failsWith(code: StatusCode) = succeedsWith(code) + + def rejectedAs[T <: Rejection: ClassTag] = specs {rejection must beAnInstanceOf[T]} + } +} + + +trait TestFailureHandling { + this: HttpService with Tracking => + + def handled(route: Route) = handleRejections(testRejectionHandler)(handleExceptions(testExceptionHandler)(trackRequestResponse(route))) + + @volatile private var suppressed = false + + def suppressNextException() { + suppressed = true + } + + def testRejectionHandler: RejectionHandler = RejectionHandler.Default + + def testExceptionHandler(implicit log: LoggingContext) = ExceptionHandler.fromPF { + case t: Throwable => ctx => + // this ensures we see any logs that would otherwise be swallowed + // AND allows us to suppress expected exceptions with the "suppressException" + // mutable operation + if (!suppressed) + log.error(t, ctx.request.toString()) + else + suppressed = false + ctx.complete(InternalServerError, t.toString) + } +} + + +/** Common imports for our Specs2RouteTest and fires up our Core + * Actor system. This way we can unit test the routes, but integrate + * test with Core. + */ +trait ApiSpecs extends NoActorSpecs +with Specs2RouteTest +with RouteTestNiceness +with EndpointMarshalling +with JavaLogging +with DefaultAuthenticationDirectives +with AuthenticatedTestkit +with TestFailureHandling +with ServerCore with CoreActorRefs with CleanMongo with Tracking { + this: HttpService => + + def actorSystem = system // for ServerCore + def actorRefFactory = system // for Specs2RouteTest + + + //implicit val auth = Token(... some test data here ...) + + // we have to duplicate code here from ActorSpecs because Specs2RouteTest is INCOMPATIBLE with Testkit (go figure) + sequential + implicit val timeout = Timeout(10000) + override def map(fs: => Fragments) = super.map(fs) ^ Step(system.shutdown()) +} diff --git a/sbt/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/broken.jrxml b/server/core/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/broken.jrxml similarity index 100% rename from sbt/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/broken.jrxml rename to server/core/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/broken.jrxml diff --git a/sbt/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/empty.jrxml b/server/core/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/empty.jrxml similarity index 100% rename from sbt/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/empty.jrxml rename to server/core/src/main/resources/org/cakesolutions/akkapatterns/core/reporting/empty.jrxml diff --git a/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/CoreActorRefs.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/CoreActorRefs.scala new file mode 100644 index 0000000..7466bec --- /dev/null +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/CoreActorRefs.scala @@ -0,0 +1,14 @@ +package org.cakesolutions.akkapatterns.core + +import akka.actor.ActorSystem + + +trait CoreActorRefs { + + def system: ActorSystem + + def applicationActor = system.actorFor("/user/application") + def userActor = system.actorFor("/user/application/user") + def loginActor = system.actorFor("/user/application/authentication/login") + +} diff --git a/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala new file mode 100644 index 0000000..24162cf --- /dev/null +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/application.scala @@ -0,0 +1,37 @@ +package org.cakesolutions.akkapatterns.core + +import akka.actor.{ActorLogging, Props, Actor} +import akka.routing.FromConfig + +case class GetImplementation() +case class Implementation(title: String, version: String, build: String) + +object ApplicationActor { + case class Start() + case class Stop() +} + +class ApplicationActor extends Actor with ActorLogging { + import ApplicationActor._ + + def receive = { + case GetImplementation() => + val title = "Akka-Patterns" + val version = "1.0" + val build = "1.0" + + sender ! Implementation(title, version, build) + + case Start() => + val messageDelivery = context.actorOf( + Props[MessageDeliveryActor].withRouter(FromConfig()).withDispatcher("low-priority-dispatcher"), + "messageDelivery" + ) + context.actorOf(Props(new UserActor(messageDelivery)).withRouter(FromConfig()), "user") + + + case Stop() => + context.children.foreach(context.stop _) + + } +} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/account.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/account.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/account.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/account.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/authentication.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/authentication.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/authentication.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/authentication.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/login.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/login.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/login.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/authentication/login.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala similarity index 62% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala index 5a8a063..5fedc35 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/boot.scala @@ -4,23 +4,18 @@ import akka.actor.{Props, ActorSystem} import akka.pattern.ask import akka.util.Timeout import concurrent.Await +import org.cakesolutions.akkapatterns.core.ApplicationActor.Start -case class Start() - -case object InmatesAreRunningTheAsylum -case class Started() - -case class Stop() trait ServerCore { implicit def actorSystem: ActorSystem - implicit val timeout = Timeout(30000) + implicit val timeout: Timeout val application = actorSystem.actorOf( props = Props[ApplicationActor], name = "application" ) - Await.ready(application ? Start(), timeout.duration) + application ! Start() } diff --git a/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala new file mode 100644 index 0000000..c029172 --- /dev/null +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/customer.scala @@ -0,0 +1,53 @@ +package org.cakesolutions.akkapatterns.core + +import org.cakesolutions.akkapatterns.domain._ +import org.cakesolutions.scalad.mongo.sprayjson._ + +/** + * An alternative to using Actors (see UserActor) is to have traditional controllers. + * + * The advantage is clear: much simpler code that remains typesafe vs akka.ask. + * + * The disadvantage is that controllers cannot be distributed as easily as actors. + * However, it is possible to refactor the internals of a controller to use an + * Actor implementation if that is needed. And fire and forget tasks that are done as + * part of a controller are most certainly prime candidates as Actor actions. + */ +class CustomerController extends CustomerMongo with Configured { + + val mongo = new SprayMongo + + /** + * Registers a customer and a user. After registering, we have a user account for the given customer. + * + * @param customer the customer + * @param user the user + */ + def register(customer: Customer, user: User) { + mongo.insert(customer) + ??? // still need to register the user in neo4j... I don't really get the whole user/customer distinction... + } + + // it is better to unwrap the Option here, as Option[T] endpoints are awful ... it + // is much better to catch NoElementExceptions in the FailureHandler and return an + // appropriately formatted status response. + def get(id: CustomerReference) = mongo.findOne[Customer]("id" :> id).get + + /** + * @param userDetail the user making the call + * @param customer the customer to be updated + */ + def update(userDetail: UserDetailT[CustomerUserKind], customer: Customer) = userDetail.kind match { + // this api design means that the admin user can't update customers... intentional? + case CustomerUserKind(`customer`.id) => + mongo.findAndReplace("id" :> customer.id, customer) + customer + + // this defensive coding may be unnecessary due to the way we are always called from the API + // to avoid this form of work duplication, ensure that your codebase has a clear and well documented + // policy for authentication and access checks. Indeed, the UserDetailT should not be passed around + // if authentication checks have already been performed. + case _ => throw new IllegalArgumentException(s"${userDetail.userReference} does not have access rights to any customers.") + } + +} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala similarity index 93% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala index 4fd933f..c956221 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/messagedelivery.scala @@ -1,15 +1,15 @@ package org.cakesolutions.akkapatterns.core -import spray.http.HttpRequest +import spray.http.{HttpResponse, HttpRequest} import com.typesafe.config.Config import org.cakesolutions.akkapatterns.domain._ -import spray.client.pipelining._ import java.util.Properties import akka.actor.Actor -import org.cakesolutions.akkapatterns.{ActorHttpIO, HttpIO} +import org.cakesolutions.akkapatterns.Nexmo import javax.mail.internet.{MimeMessage, InternetAddress} import javax.mail._ import scala.Some +import scala.concurrent.Future /** * The address where the message should be delivered to. We currently support emailing the message or sending it @@ -42,7 +42,9 @@ case class DeliverActivationCode(address: DeliveryAddress, userReference: UserRe * ``IOBridge`` and other Spray components. */ trait NexmoTextMessageDelivery { - this: HttpIO => + this: Actor => + + import context.dispatcher /** * Returns the API key for Nexmo. @@ -56,10 +58,6 @@ trait NexmoTextMessageDelivery { */ def apiSecret: String - import scala.concurrent.ExecutionContext.Implicits.global - - private val pipeline = sendReceive(httpClient) // makeConduit("rest.nexmo.com")) - /** * Delivers the text message ``secret`` to the phone number ``mobileNumber``. The ``mobileNumber`` needs to be in * full international format, without spaces, but without the leading "+", for example ``4477712345678`` for @@ -72,7 +70,7 @@ trait NexmoTextMessageDelivery { // http://rest.nexmo.com/sms/json?api_key=3e08b948&api_secret=584f23de&from=Cake&to=*********&text=Hello val url = "/sms/json?api_key=%s&api_secret=%s&from=Zoetic&to=%s&text=%s" format (apiKey, apiSecret, mobileNumber, secret) val request = HttpRequest(spray.http.HttpMethods.POST, url) - pipeline(request) onSuccess { + Nexmo.sendReceive(context.system)(request) onSuccess { case response => // Sort out the response. Maybe bang to health agent if we're out of credits or some such } @@ -167,7 +165,7 @@ trait SimpleEmailMessageDelivery { /** * Delivers the secret to the address */ -class MessageDeliveryActor extends Actor with ActorHttpIO with NexmoTextMessageDelivery with SimpleEmailMessageDelivery +class MessageDeliveryActor extends Actor with NexmoTextMessageDelivery with SimpleEmailMessageDelivery with ConfigEmailConfiguration { def config = context.system.settings.config diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/neo4j.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/neo4j.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/neo4j.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/neo4j.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/datasource.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/datasource.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/datasource.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/datasource.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/jasperreports.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/jasperreports.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/jasperreports.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/jasperreports.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/package.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/package.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/package.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/package.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/user.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/user.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/user.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/reporting/user.scala diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/user.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/core/user.scala similarity index 100% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/user.scala rename to server/core/src/main/scala/org/cakesolutions/akkapatterns/core/user.scala diff --git a/server/core/src/main/scala/org/cakesolutions/akkapatterns/io.scala b/server/core/src/main/scala/org/cakesolutions/akkapatterns/io.scala new file mode 100644 index 0000000..5c15c74 --- /dev/null +++ b/server/core/src/main/scala/org/cakesolutions/akkapatterns/io.scala @@ -0,0 +1,67 @@ +package org.cakesolutions.akkapatterns + +import akka.actor.{Props, Actor, ActorSystem} +import spray.io.IOExtension +import com.typesafe.config.ConfigFactory +import com.github.sstone.amqp.ConnectionOwner +import com.github.sstone.amqp.Amqp.ExchangeParameters +import com.rabbitmq.client.ConnectionFactory +import spray.can.client.HttpClient +import akka.actor._ +import spray.client.HttpConduit +import spray.can.client.DefaultHttpClient +import akka.spray.ExtensionActorRef + +abstract class SendReceive(server: String, port: Int = 443, sslEnabled: Boolean = true) extends ExtensionId[ExtensionActorRef] { + + def createExtension(system: ExtendedActorSystem) = { + val client = DefaultHttpClient(system) + val conduitName = "http-conduit-" + port + "-" + + (if (sslEnabled) "ssl" else "plain") + + "-" + server + val conduit = system.actorOf( + props = Props( + new HttpConduit(client, server, port = port, sslEnabled = sslEnabled) + ), + name = conduitName + ) + new ExtensionActorRef(conduit) + } + + def sendReceive(system: ActorSystem) = HttpConduit.sendReceive(get(system)) + +} + +object Foursquare extends SendReceive("api.foursquare.com") +object ITunes extends SendReceive("buy.itunes.apple.com") +object ITunesSandbox extends SendReceive("sandbox.itunes.apple.com") +object Facebook extends SendReceive("graph.facebook.com") +object Twitter extends SendReceive("api.twitter.com") +object Nexmo extends SendReceive("rest.nexmo.com") + + + +/** + * Provides connection & access to the AMQP broker + */ +trait AmqpIO { + implicit def actorSystem: ActorSystem + + // prepare the AMQP connection factory + final lazy val connectionFactory = new ConnectionFactory(); connectionFactory.setHost("localhost") + // connect to the AMQP exchange + final lazy val amqpExchange = ExchangeParameters(name = "amq.direct", exchangeType = "", passive = true) + + // create a "connection owner" actor, which will try and reconnect automatically if the connection ins lost + val connection = actorSystem.actorOf(Props(new ConnectionOwner(connectionFactory))) + +} + +/** + * Convenience ``AmqpIO`` implementation that can be mixed in to actors. + */ +trait ActorAmqpIO extends AmqpIO { + this: Actor => + final implicit def actorSystem = context.system + +} \ No newline at end of file diff --git a/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerSpecs.scala b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerSpecs.scala new file mode 100644 index 0000000..8860b2d --- /dev/null +++ b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/CustomerSpecs.scala @@ -0,0 +1,31 @@ +package org.cakesolutions.akkapatterns.core + +import org.cakesolutions.akkapatterns.{TestCustomerData, NoActorSpecs, CleanMongo, ActorSpecs} +import org.cakesolutions.akkapatterns.domain.{CustomerUserKind, UserDetailT, CustomerMongo} +import org.cakesolutions.akkapatterns.MongoCollectionFixture.Fix +import java.util.UUID + + +class CustomerSpecs extends NoActorSpecs with CleanMongo with CustomerMongo with TestCustomerData with Neo4JFixtures { + + neo4jFixtures + + val controller = new CustomerController + + "Customer actor" should { + "be updateable" in new Fix("customers") { + val jan = controller.get(TestCustomerJanId) + val auth = UserDetailT(RootUser.id, CustomerUserKind(jan.id)) + + controller.update(auth, jan.copy(lastName = "changed")) + controller.get(TestCustomerJanId) !== jan + } + + "not allow the wrong user to update" in new Fix("customers") { + val jan = controller.get(TestCustomerJanId) + val bad = UserDetailT(RootUser.id, CustomerUserKind(UUID.randomUUID())) + + controller.update(bad, jan.copy(lastName = "changed")) must throwA[IllegalArgumentException] + } + } +} diff --git a/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/UserActorSpec.scala b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/UserActorSpec.scala new file mode 100644 index 0000000..37a68f6 --- /dev/null +++ b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/UserActorSpec.scala @@ -0,0 +1,23 @@ +package org.cakesolutions.akkapatterns.core + +import akka.testkit.TestActorRef +import org.cakesolutions.akkapatterns.{MongoCollectionFixture, TestMongo, ActorSpecs} +import org.cakesolutions.akkapatterns.domain.{SuperuserKind, User} +import org.cakesolutions.akkapatterns.MongoCollectionFixture.Fix + +class UserActorSpec extends ActorSpecs with Neo4JFixtures { + + neo4jFixtures + + val actor = TestActorRef(new UserActor(testActor)) + + "Basic user operations" should { + "Find the root user" in { + actor ! GetUserByUsername("root") + expectMsgType[Option[User]] should beLike { + case Some(user) if user.kind == SuperuserKind => ok + } + } + } + +} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/sanity.scala b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/neo4jsupport.scala similarity index 79% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/core/sanity.scala rename to server/core/src/test/scala/org/cakesolutions/akkapatterns/core/neo4jsupport.scala index 85d4a16..3843dc6 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/core/sanity.scala +++ b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/neo4jsupport.scala @@ -3,11 +3,8 @@ package org.cakesolutions.akkapatterns.core import org.cakesolutions.akkapatterns.domain.{SuperuserKind, User, UserFormats} import java.util.UUID -/** - * Initial system sanity checks - */ -trait SanityChecks extends TypedGraphDatabase with UserFormats with SprayJsonNodeMarshalling - with UserGraphDatabaseIndexes { +// TODO https://github.com/janm399/akka-patterns/issues/35 +trait Neo4JFixtures extends TypedGraphDatabase with UserFormats with SprayJsonNodeMarshalling with UserGraphDatabaseIndexes { val RootUserPassword = "*******" val RootUser = User(UUID.fromString("a3372060-2b3b-11e2-81c1-0800200c9a66"), "root", "", "janm@cakesolutions.net", None, "Jan", "Machacek", SuperuserKind).resetPassword(RootUserPassword) @@ -27,8 +24,8 @@ trait SanityChecks extends TypedGraphDatabase with UserFormats with SprayJsonNod } } - def ensureSanity: Boolean = synchronized { + def neo4jFixtures: Boolean = synchronized { ensureUserSanity } -} +} \ No newline at end of file diff --git a/sbt/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala similarity index 84% rename from sbt/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala rename to server/core/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala index c7c847e..d519545 100644 --- a/sbt/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala +++ b/server/core/src/test/scala/org/cakesolutions/akkapatterns/core/reporting/ReportRunnerSpec.scala @@ -2,13 +2,12 @@ package org.cakesolutions.akkapatterns.core.reporting import org.specs2.mutable.Specification import org.specs2.execute.Result -import org.cakesolutions.akkapatterns.TestData import java.io.FileOutputStream /** * @author janmachacek */ -class ReportRunnerSpec extends Specification with TestData with ReportFormats { +class ReportRunnerSpec extends Specification with ReportFormats { val runner = new ReportRunner with JRXmlReportCompiler with ClasspathResourceReportLoader @@ -23,7 +22,10 @@ class ReportRunnerSpec extends Specification with TestData with ReportFormats { } "simple report" in { - runReport("empty.jrxml", EmptyExpression, ProductListParameterExpression(Users.newUser("janm") :: Users.newUser("anirvanc") :: Nil)) + runReport("empty.jrxml", EmptyExpression, ProductListParameterExpression( +// Users.newUser("janm") :: Users.newUser("anirvanc") :: Nil + Nil + )) } def runReport(source: String, parametersExpression: Expression, dataSourceExpression: DataSourceExpression): Result = { diff --git a/server/domain/src/main/resources/application.conf b/server/domain/src/main/resources/application.conf new file mode 100644 index 0000000..b99a31f --- /dev/null +++ b/server/domain/src/main/resources/application.conf @@ -0,0 +1,96 @@ +akka { + event-handlers = ["akka.contrib.jul.JavaLoggingEventHandler"] + loglevel = DEBUG + stdout-loglevel = WARNING + log-config-on-start = off + + actor { + debug { +# receive = on +# autoreceive = on +# lifecycle = on +# fsm = on +# event-stream = on + unhandled = on + } + + deployment { + "/application/*" { + router = "round-robin" +# nr-of-instances = 20 + resizer { + lower-bound = 1 + upper-bound = 40 + } + } + } + + default-dispatcher { + type = "Dispatcher" + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 4 + parallelism-factor = 10 + parallelism-max = 64 + } + throughput = 10 + } + + } +} + +low-priority-dispatcher { + type = "Dispatcher" + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 2 + parallelism-factor = 2 + parallelism-max = 8 + } + throughput = 1 +} + + +test { + db { + mongo { + name: "patterns-test" + connections: 10 + concern: ACKNOWLEDGED + hosts { + "localhost": 27017 + } + } + cassandra { + cluster: "patterns-test" + connections: 10 + hosts { + "localhost": 9160 + } + } + } +} + +main { + db { + mongo { + name: "patterns" + connections: 50 + concern: ACKNOWLEDGED + hosts { + "mongodb.host.1": 27017 + "mongodb.host.2": 27017 + "mongodb.host.3": 27017 + } + } + cassandra { + cluster: "patterns" + connections: 50 + hosts { + "cassandra.host.1": 9160 + "cassandra.host.2": 9160 + "cassandra.host.3": 9160 + } + } + } +} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala similarity index 65% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala rename to server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala index b4dfb17..94fd559 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/configuration.scala @@ -2,6 +2,9 @@ package org.cakesolutions.akkapatterns.domain import collection.mutable import reflect.{ClassTag, classTag} +import io.{Codec, Source} +import org.springframework.core.io.DefaultResourceLoader +import java.io.InputStream /** * Stores the configuration @@ -44,4 +47,20 @@ trait Configuration { a } -} \ No newline at end of file +} + +trait Resources { + + protected def readResource(resource: String) = new DefaultResourceLoader().getResource(resource).getInputStream + + protected implicit class StreamString(stream: InputStream) { + @deprecated("use Scala IO", "HEAD") + def mkString = + try Source.fromInputStream(stream)(Codec.UTF8).mkString + finally stream.close() + } + +// PathMatchingResourcePatternResolver + +} + diff --git a/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala new file mode 100644 index 0000000..3fd6871 --- /dev/null +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/customer.scala @@ -0,0 +1,25 @@ +package org.cakesolutions.akkapatterns.domain + +import spray.json.DefaultJsonProtocol +import org.cakesolutions.scalad.mongo.sprayjson.{SprayMongoCollection, UuidMarshalling} +import com.mongodb.DB + +case class Customer(id: CustomerReference, + firstName: String, lastName: String, + email: String, addresses: Seq[Address]) + +case class Address(line1: String, line2: String, line3: String) + +trait CustomerFormats extends DefaultJsonProtocol with UuidMarshalling { + + implicit val AddressFormat = jsonFormat3(Address) + implicit val CustomerFormat = jsonFormat5(Customer) + +} + +trait CustomerMongo extends CustomerFormats { + this: Configured => + import org.cakesolutions.scalad.mongo.sprayjson._ + + protected implicit val CustomerProvider = new SprayMongoCollection[Customer](configured[DB], "customers", "id":>1) +} \ No newline at end of file diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala similarity index 61% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala rename to server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala index a4f86f4..7634f30 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/domain.scala @@ -16,6 +16,11 @@ package object domain { /** * Type alias for customer identity + * + * NOTE: consider the alternative, using a `case class CustomerReference(id: UUID)` + * which is slightly more verbose but ensures type safety throughout the code. + * If your code has lots of UUIDs, you'll be *really* glad of type safe ids, + * trust me! */ type CustomerReference = UUID diff --git a/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/nosql.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/nosql.scala new file mode 100644 index 0000000..9f46e4c --- /dev/null +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/nosql.scala @@ -0,0 +1,43 @@ +package org.cakesolutions.akkapatterns.domain + +import _root_.me.prettyprint.hector.api.{HConsistencyLevel, ConsistencyLevelPolicy} +import com.mongodb.MongoOptions +import _root_.me.prettyprint.cassandra.service.{OperationType, CassandraHostConfigurator} +import me.prettyprint.hector.api.factory.HFactory +import Settings.{Mongo, Cassandra} +import scala.collection.JavaConversions._ + +trait NoSqlConfig { + + protected def cassandra(config: Cassandra) = { + val configurator = new CassandraHostConfigurator() + configurator.setHosts(config.hosts) + configurator.setMaxActive(config.connections) + HFactory.getOrCreateCluster(config.cluster, configurator) + } + + + protected def mongo(config: Mongo) = { + val options = new MongoOptions + options.connectionsPerHost = config.connections + val m = new com.mongodb.Mongo(config.hosts, options) + m.setWriteConcern(config.concern) + m.getDB(config.name) + } +} + +object ConsistencyPolicy extends ConsistencyLevelPolicy { + def get(op: OperationType): HConsistencyLevel = { + HConsistencyLevel.LOCAL_QUORUM + } + + def get(op: OperationType, cfName: String): HConsistencyLevel = { + (op, cfName) match { + case (OperationType.READ, "countIndex") => HConsistencyLevel.ONE + case (OperationType.READ, "count") => HConsistencyLevel.ONE + case (OperationType.WRITE, "countIndex") => HConsistencyLevel.LOCAL_QUORUM + case (OperationType.WRITE, "count") => HConsistencyLevel.LOCAL_QUORUM + case _ => HConsistencyLevel.LOCAL_QUORUM + } + } +} \ No newline at end of file diff --git a/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/settings.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/settings.scala new file mode 100644 index 0000000..3932585 --- /dev/null +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/settings.scala @@ -0,0 +1,65 @@ +package org.cakesolutions.akkapatterns.domain + +import com.typesafe.config.ConfigFactory +import com.mongodb.{ServerAddress, WriteConcern} +import scala.collection.JavaConversions._ +import akka.contrib.jul.JavaLogging + +object Settings extends JavaLogging { + + // https://groups.google.com/d/topic/scala-user/wzguzEJtLaI/discussion + private val overrides = ConfigFactory.load("local") + private val config = overrides.withFallback(ConfigFactory.load()) + + private def unmerged(path: String) = + if (overrides.hasPath(path)) overrides.getConfig(path) + else config.getConfig(path) + + case class Cassandra(cluster: String, connections: Int, hosts: String) + object Cassandra { + def apply(base: String) = { + val c = config.getConfig(base) + val cluster = c.getString("cluster") + val connections = c.getInt("connections") + val hosts = unmerged(base + ".hosts").entrySet().map{e => + e.getKey.replaceAll("\"", "") + ":" + e.getValue.unwrapped() + }.mkString(",") + new Cassandra(cluster, connections, hosts) + } + } + + case class Mongo(name: String, connections: Int, hosts: List[ServerAddress], concern: WriteConcern) + object Mongo { + def apply(base: String) = { + val c = config.getConfig(base) + val name = c.getString("name") + val connections = c.getInt("connections") + val concern = WriteConcern.valueOf(c.getString("concern")) + val hosts = unmerged(base + ".hosts").entrySet().map{e => + new ServerAddress(e.getKey.replaceAll("\"", ""), e.getValue.unwrapped().asInstanceOf[Integer]) + }.toList + new Mongo(name, connections, hosts, concern) + } + } + + case class Db(cassandra: Cassandra, mongo: Mongo) + case class Main(db: Db) + + val main = try Main( + Db( + Cassandra("main.db.cassandra"), + Mongo("main.db.mongo") + ) + ) catch { + case t: Throwable => log.error(t, "Settings.main") ; throw t + } + + val test = try Main( + Db( + Cassandra("test.db.cassandra"), + Mongo("test.db.mongo") + ) + ) catch { + case t: Throwable => log.error(t, "Settings.test") ; throw t + } +} diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala similarity index 72% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala rename to server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala index 997dbb0..3381378 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala +++ b/server/domain/src/main/scala/org/cakesolutions/akkapatterns/domain/user.scala @@ -1,10 +1,11 @@ package org.cakesolutions.akkapatterns.domain import spray.json._ -import org.cakesolutions.akkapatterns.UuidFormats +import com.mongodb.DB +import org.cakesolutions.scalad.mongo.sprayjson.UuidMarshalling /** - * The user record, which stores the identtiy, the username and the password + * The user record, which stores the identity, the username and the password * * @author janmachacek */ @@ -29,7 +30,7 @@ case class CustomerUserKind(customerReference: CustomerReference) extends UserKi case object GuestUserKind extends UserKind /** - * The user detail about an authenticated user. It contains the user ``id`` and the detailm which further refines + * The user detail about an authenticated user. It contains the user ``id`` and the details which further refines * the kind of user we're dealing with. * * @param userReference the identity of the authenticated user @@ -38,12 +39,13 @@ case object GuestUserKind extends UserKind */ case class UserDetailT[A <: UserKind](userReference: UserReference, kind: A) -/** - * Trait that contains the [[spray.json.JsonFormat]] instances for the user - * management - */ -trait UserFormats extends DefaultJsonProtocol with UuidFormats { +// Spray JSON marshalling for the User hierarchy +trait UserFormats extends DefaultJsonProtocol with UuidMarshalling { + + // the penalty we pay for a type hierarchy is an overly complex + // marshalling format. It is often worth considering a data + // model that does not enforce a hierarchy. implicit object UserKindFormat extends JsonFormat[UserKind] { private val Superuser = JsString("superuser") private val Customer = JsString("customer") @@ -70,4 +72,12 @@ trait UserFormats extends DefaultJsonProtocol with UuidFormats { implicit val UserFormat = jsonFormat8(User) -} \ No newline at end of file +} + +// this is how we would use scalad for the user database, but we're actually using Neo4J +//trait UserMongo extends UserFormats { +// this: Configured => +// import org.cakesolutions.scalad.mongo.sprayjson._ +// +// protected implicit val UserProvider = new SprayMongoCollection[User](configured[DB], "users", "id":>1, "username":> 1) +//} \ No newline at end of file diff --git a/server/domain/src/main/xsd/PurchaseOrder.xsd b/server/domain/src/main/xsd/PurchaseOrder.xsd new file mode 100644 index 0000000..b393dab --- /dev/null +++ b/server/domain/src/main/xsd/PurchaseOrder.xsd @@ -0,0 +1,66 @@ + + + + + Purchase order schema for Example.com. + Copyright 2000 Example.com. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/logging.properties b/server/logging.properties new file mode 100644 index 0000000..8a0c5df --- /dev/null +++ b/server/logging.properties @@ -0,0 +1,29 @@ +# to enable this file, you must pass the following parameters to the JVM +# +# -Djava.util.logging.config.file=logging.properties +# +# It should be obvious in IntelliJ's Run settings how to do this +# (might need to add ../ if running from a module). +# +# Maven is tricky and it reported that setting MAVEN_OPTS will work. +# +# http://stackoverflow.com/questions/14269492 +# +# In SBT should work with our build.scala. + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.filter = com.github.fommil.logging.ClassnameFilter +java.util.logging.ConsoleHandler.formatter = com.github.fommil.logging.CustomFormatter +com.github.fommil.logging.CustomFormatter.format = %L: %m [%c] (%n) %e %E %S + +com.github.fommil.logging.CustomFormatter.stackExclude = \ + spray. scala. akka. com.mongodb. org.specs2. scalaz. \ + org.jetbrains. com.intellij java. sun. com.sun + + +akka.level = CONFIG +org.cakesolutions.level = INFO + +.level = WARNING diff --git a/sbt/src/main/resources/madouse.jpg b/server/main/src/main/resources/madouse.jpg similarity index 100% rename from sbt/src/main/resources/madouse.jpg rename to server/main/src/main/resources/madouse.jpg diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala b/server/main/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala similarity index 90% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala rename to server/main/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala index eedd5e4..3194264 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala +++ b/server/main/src/main/scala/org/cakesolutions/akkapatterns/main/ClientDemo.scala @@ -1,19 +1,13 @@ package org.cakesolutions.akkapatterns.main import akka.actor.{Props, ActorSystem} -import com.aphelia.amqp.{ChannelOwner, ConnectionOwner} -import com.aphelia.amqp.Amqp._ import akka.util.Timeout import com.rabbitmq.client.{DefaultConsumer, Channel, Envelope, ConnectionFactory} import com.rabbitmq.client.AMQP.BasicProperties -import com.aphelia.amqp.Amqp.ReturnedMessage -import com.aphelia.amqp.Amqp.Publish -import com.aphelia.amqp.Amqp.ChannelParameters -import scala.Some -import com.aphelia.amqp.RpcClient.Request -import com.aphelia.amqp.Amqp.QueueParameters -import com.aphelia.amqp.Amqp.Delivery import akka.actor.SupervisorStrategy.Stop +import com.github.sstone.amqp._ +import com.github.sstone.amqp.RpcClient._ +import com.github.sstone.amqp.Amqp._ /** * @author janmachacek diff --git a/sbt/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala b/server/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala similarity index 56% rename from sbt/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala rename to server/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala index 27253ba..75224c4 100644 --- a/sbt/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala +++ b/server/main/src/main/scala/org/cakesolutions/akkapatterns/main/Main.scala @@ -1,17 +1,24 @@ package org.cakesolutions.akkapatterns.main import akka.actor.ActorSystem -import org.cakesolutions.akkapatterns.domain.Configuration +import org.cakesolutions.akkapatterns.domain.{Settings, NoSqlConfig, Configuration} import org.cakesolutions.akkapatterns.core.ServerCore import org.cakesolutions.akkapatterns.web.Web import org.cakesolutions.akkapatterns.api.Api +import akka.util.Timeout object Main { def main(args: Array[String]) { implicit val system = ActorSystem("AkkaPatterns") - class Application(val actorSystem: ActorSystem) extends ServerCore with Configuration with Api with Web + class Application(val actorSystem: ActorSystem) extends Configuration with NoSqlConfig with ServerCore with Web { + + implicit val timeout = Timeout(30000) + + configure(mongo(Settings.main.db.mongo)) + } + new Application(system) diff --git a/server/project/build.properties b/server/project/build.properties new file mode 100644 index 0000000..21e2c5d --- /dev/null +++ b/server/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.12.2 \ No newline at end of file diff --git a/server/project/build.scala b/server/project/build.scala new file mode 100644 index 0000000..0ac02e4 --- /dev/null +++ b/server/project/build.scala @@ -0,0 +1,148 @@ +import sbt._ +import Keys._ +import sbtscalaxb.Plugin._ +import ScalaxbKeys._ +import net.virtualvoid.sbt.graph.Plugin._ +import org.scalastyle.sbt._ +import com.typesafe.sbt.SbtStartScript + +// to sync this project with IntelliJ, run the sbt-idea plugin with: sbt gen-idea +// +// to set user-specific local properties, just create "~/.sbt/my-settings.sbt", e.g. +// javaOptions += "some cool stuff" +// +// This project allows a local.conf on the classpath (e.g. domain/src/main/resources) to override settings, e.g. +// +// test.db.mongo.hosts { "Sampo.home": 27017 } +// test.db.cassandra.hosts { "Sampo.home": 9160 } +// main.db.mongo.hosts = ${test.db.mongo.hosts} +// main.db.cassandra.hosts = ${test.db.cassandra.hosts} +// +// mkdir -p {domain,core,api,main,test}/src/{main,test}/{java,scala,resources}/org/cakesolutions/akkapatterns +// +// the following were useful for writing this file +// http://www.scala-sbt.org/release/docs/Getting-Started/Multi-Project.html +// https://github.com/sbt/sbt/blob/0.12.2/main/Build.scala +// https://github.com/akka/akka/blob/master/project/AkkaBuild.scala +object PatternsBuild extends Build { + + override val settings = super.settings ++ Seq( + organization := "org.cakesolutions.patterns", + version := "1.0-SNAPSHOT", + scalaVersion := "2.10.1" + ) + + lazy val defaultSettings = Defaults.defaultSettings ++ graphSettings ++ Seq( + scalacOptions in Compile ++= Seq("-encoding", "UTF-8", "-target:jvm-1.6", "-deprecation", "-unchecked"), + javacOptions in Compile ++= Seq("-source", "1.6", "-target", "1.6", "-Xlint:unchecked", "-Xlint:deprecation", "-Xlint:-options"), + // https://github.com/sbt/sbt/issues/702 + javaOptions += "-Djava.util.logging.config.file=logging.properties", + javaOptions += "-Xmx2G", + outputStrategy := Some(StdoutOutput), + fork := true, + maxErrors := 1, + resolvers ++= Seq( + Resolver.mavenLocal, + Resolver.sonatypeRepo("releases"), + Resolver.typesafeRepo("releases"), + "Spray Releases" at "http://repo.spray.io", + Resolver.typesafeRepo("snapshots"), + Resolver.sonatypeRepo("snapshots"), + "Jasper Community" at "http://jasperreports.sourceforge.net/maven2" + // resolvers += "neo4j repo" at "http://m2.neo4j.org/content/repositories/releases/" + ), + parallelExecution in Test := false + ) ++ ScctPlugin.instrumentSettings ++ scalaxbSettings ++ ScalastylePlugin.Settings + + def module(dir: String) = Project(id = dir, base = file(dir), settings = defaultSettings) + import Dependencies._ + + // https://github.com/eed3si9n/scalaxb/issues/199 + lazy val domain = module("domain") settings( + libraryDependencies += java_logging, // will upgrade to scala_logging when released + libraryDependencies += akka_contrib, + libraryDependencies += akka, + libraryDependencies += scala_io_core, + libraryDependencies += scala_io_file, + libraryDependencies += scalad, + libraryDependencies += hector, + libraryDependencies += spring_core, + libraryDependencies += specs2 % "test", + packageName in scalaxb in Compile := "org.cakesolutions.patterns.domain.soap", + sourceGenerators in Compile <+= scalaxb in Compile + ) + + lazy val test = module("test") dependsOn (domain) settings ( + libraryDependencies += specs2 % "compile", + libraryDependencies += cassandra_unit, + libraryDependencies += neo4j, + libraryDependencies += akka_testkit % "compile" + ) + + lazy val core = module("core") dependsOn(domain, test % "test") settings ( + libraryDependencies += spray_client, + libraryDependencies += amqp, + libraryDependencies += rabbitmq, + libraryDependencies += mail, + libraryDependencies += neo4j, + libraryDependencies += scalaz_effect, + libraryDependencies += jasperreports, + libraryDependencies += poi + ) + + lazy val api = module("api") dependsOn(core, test % "test") settings( + libraryDependencies += spray_routing, + libraryDependencies += spray_testkit % "test" + ) + + lazy val main = module("main") dependsOn(api, test % "test") + + lazy val root = Project(id = "parent", base = file("."), settings = defaultSettings) settings ( + ScctPlugin.mergeReportSettings: _* + ) settings ( + SbtStartScript.startScriptForClassesSettings: _* + ) settings ( + mainClass in (Compile, run) := Some("org.cakesolutions.akkapatterns.main.Main") + ) aggregate ( + domain, test, core, api, main + ) dependsOn (main) // yuck +} + +object Dependencies { + // to help resolve transitive problems, type: + // `sbt dependency-graph` + // `sbt test:dependency-tree` + val bad = Seq( + ExclusionRule(name = "log4j"), + ExclusionRule(name = "commons-logging"), + ExclusionRule(organization = "org.slf4j") + ) + + val akka_version = "2.1.2" + val spray_version = "1.1-M7" + + val java_logging = "com.github.fommil" % "java-logging" % "1.0" + val scalad = "org.cakesolutions" %% "scalad" % "1.3.0-SNAPSHOT" // https://github.com/janm399/scalad/issues/7 + val akka = "com.typesafe.akka" %% "akka-actor" % akka_version + val akka_contrib = "com.typesafe.akka" %% "akka-contrib" % akka_version intransitive()// JUL only + val akka_testkit = "com.typesafe.akka" %% "akka-testkit" % akka_version + val scalaz_effect = "org.scalaz" %% "scalaz-effect" % "7.0.0-M9" + val spring_core = "org.springframework" % "spring-core" % "3.1.4.RELEASE" excludeAll (bad: _*) + // beware Hector 1.1-2 and Guava 14: https://github.com/hector-client/hector/pull/591 + val guava = "com.google.guava" % "guava" % "13.0.1" // includes Cache + val jsr305 = "com.google.code.findbugs" % "jsr305" % "2.0.1" // undeclared dep of Guava + val hector = "org.hectorclient" % "hector-core" % "1.1-2" excludeAll (bad: _*) + val spray_routing = "io.spray" % "spray-routing" % spray_version + val spray_client = "io.spray" % "spray-client" % spray_version + val spray_testkit = "io.spray" % "spray-testkit" % spray_version + val cassandra_unit = "org.cassandraunit" % "cassandra-unit" % "1.1.2.1" excludeAll (bad: _*) + val specs2 = "org.specs2" %% "specs2" % "1.13" + val amqp = "com.github.sstone" %% "amqp-client" % "1.1" + val rabbitmq = "com.rabbitmq" % "amqp-client" % "2.8.1" + val neo4j = "org.neo4j" % "neo4j" % "1.9.M05" + val jasperreports = "net.sf.jasperreports" % "jasperreports" % "5.0.1" excludeAll (bad: _*) + val poi = "org.apache.poi" % "poi" % "3.9" + val mail = "javax.mail" % "mail" % "1.4.2" + val scala_io_core = "com.github.scala-incubator.io" %% "scala-io-core" % "0.4.2" + val scala_io_file = "com.github.scala-incubator.io" %% "scala-io-file" % "0.4.2" +} \ No newline at end of file diff --git a/server/project/plugins.sbt b/server/project/plugins.sbt new file mode 100644 index 0000000..cc48177 --- /dev/null +++ b/server/project/plugins.sbt @@ -0,0 +1,17 @@ +resolvers += "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/releases/" + +addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.2.0") + +addSbtPlugin("org.scalaxb" % "sbt-scalaxb" % "1.0.1") + +//addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.0.0") + +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.1") + +resolvers += "SCCT Snapshots" at "http://mtkopone.github.com/scct/maven-repo" + +addSbtPlugin("reaktor" % "sbt-scct" % "0.2-SNAPSHOT") // https://github.com/mtkopone/scct/issues/43 + +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.2.0") + +addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.7.0") \ No newline at end of file diff --git a/server/provisioning/gatling/README.md b/server/provisioning/gatling/README.md new file mode 100644 index 0000000..ba0e611 --- /dev/null +++ b/server/provisioning/gatling/README.md @@ -0,0 +1,7 @@ +Tested with Gatling 1.4.x + +0. Ensure your databases are correctly configured for running the main method. +1. Download and extract [Gatling](https://github.com/excilys/gatling/wiki/Downloads). +2. Create and copy `test.xml` into `user-files/request-bodies`. +3. Copy the `patterns` folder into `user-files/simulations`. +4. Run `bin/gatling.sh`. diff --git a/server/provisioning/gatling/patterns/AkkaPatternsGatling.scala b/server/provisioning/gatling/patterns/AkkaPatternsGatling.scala new file mode 100644 index 0000000..cb22b29 --- /dev/null +++ b/server/provisioning/gatling/patterns/AkkaPatternsGatling.scala @@ -0,0 +1,25 @@ +package patterns + +import com.excilys.ebi.gatling.core.Predef._ +import com.excilys.ebi.gatling.http.Predef._ +import com.excilys.ebi.gatling.jdbc.Predef._ +import com.excilys.ebi.gatling.http.Headers.Names._ +import akka.util.duration._ +import bootstrap._ + +// https://github.com/excilys/gatling/wiki/First-Steps-with-Gatling +// https://github.com/excilys/gatling/wiki/Advanced-Usage +class AkkaPatternsGatling extends Simulation { + val httpConf = httpConfig.baseURL("http://localhost:8080").disableFollowRedirect + // val headers_login = Map("Content-Type" -> "application/json") + val exampleScn = scenario("Example Scenario") + .exec( + http("make_request") + .get("/") + // uncomment get() and swap for post() as needed + // .post("/").fileBody(test.xml") + ) + + setUp(exampleScn.users(15000).ramp(100).protocolConfig(httpConf)) +} + diff --git a/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/common.js b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/common.js new file mode 100644 index 0000000..47a4c24 --- /dev/null +++ b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/common.js @@ -0,0 +1,30 @@ +// adapted from https://github.com/mongodb/mongo-csharp-driver/blob/master/uuidhelpers.js + +HexToBase64 = function(hex) { + var base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var base64 = ""; + var group; + for (var i = 0; i < 30; i += 6) { + group = parseInt(hex.substr(i, 6), 16); + base64 += base64Digits[(group >> 18) & 0x3f]; + base64 += base64Digits[(group >> 12) & 0x3f]; + base64 += base64Digits[(group >> 6) & 0x3f]; + base64 += base64Digits[group & 0x3f]; + } + group = parseInt(hex.substr(30, 2), 16); + base64 += base64Digits[(group >> 2) & 0x3f]; + base64 += base64Digits[(group << 4) & 0x3f]; + base64 += "=="; + return base64; +} +JUUID = function (uuid) { + var hex = uuid.replace(/[{}-]/g, ""); // remove extra characters + var msb = hex.substr(0, 16); + var lsb = hex.substr(16, 16); + msb = msb.substr(14, 2) + msb.substr(12, 2) + msb.substr(10, 2) + msb.substr(8, 2) + msb.substr(6, 2) + msb.substr(4, 2) + msb.substr(2, 2) + msb.substr(0, 2); + lsb = lsb.substr(14, 2) + lsb.substr(12, 2) + lsb.substr(10, 2) + lsb.substr(8, 2) + lsb.substr(6, 2) + lsb.substr(4, 2) + lsb.substr(2, 2) + lsb.substr(0, 2); + hex = msb + lsb; + var base64 = HexToBase64(hex); + return new BinData(3, base64); +} + diff --git a/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/customers.js b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/customers.js new file mode 100644 index 0000000..aebba90 --- /dev/null +++ b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/customers.js @@ -0,0 +1,13 @@ +db.customers.drop(); +db.customers.save([ + { + "firstName":"Jan", + "lastName":"Machacek", + "email":"janm@cakesolutions.net", + "id":JUUID("122fa630-92fd-11e2-9e96-0800200c9a66"), + "addresses":[ + {"line1":"Magdalen Centre", "line2":"Robert Robinson Avenue", "line3":"Oxford"}, + {"line1":"Houldsworth Mill", "line2":"Houldsworth Street", "line3":"Reddish"} + ] + } +]); diff --git a/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/users.js b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/users.js new file mode 100644 index 0000000..cc9378e --- /dev/null +++ b/server/test/src/main/resources/org/cakesolutions/akkapatterns/testdata/users.js @@ -0,0 +1,31 @@ +// we're using Neo4J for the users. but if we wanted to use Mongo instead, this is some useful test data + +db.users.drop(); +db.users.save([ +{ + id: JUUID("994fc1f0-90a9-11e2-9e96-0800200c9a66"), + username: "guest", + hashedPassword: "", + email: "johndoe@example.com", + firstName: "John", + lastName: "Doe", + kind: {kind: "guest"} +}, { + id: JUUID("7370f980-90aa-11e2-9e96-0800200c9a66"), + username: "customer", + hashedPassword: "", + email: "johndoe@example.com", + mobile: "07777777777", + firstName: "John", + lastName: "Doe", + kind: {kind: "customer", value: {customerReference: JUUID("67c2b250-92f2-11e2-9e96-0800200c9a66")}} +},{ + id: JUUID("c0a93190-90aa-11e2-9e96-0800200c9a66"), + username: "root", + hashedPassword: "", + email: "johndoe@example.com", + firstName: "John", + lastName: "Doe", + kind: {kind: "guest"} +} +]); \ No newline at end of file diff --git a/server/test/src/main/scala/org/cakesolutions/akkapatterns/specifications.scala b/server/test/src/main/scala/org/cakesolutions/akkapatterns/specifications.scala new file mode 100644 index 0000000..8b2590e --- /dev/null +++ b/server/test/src/main/scala/org/cakesolutions/akkapatterns/specifications.scala @@ -0,0 +1,116 @@ +package org.cakesolutions.akkapatterns + +import domain._ +import org.cassandraunit.DataLoader +import org.cassandraunit.dataset.yaml.ClassPathYamlDataSet +import com.mongodb.DB +import me.prettyprint.hector.api.Cluster +import org.specs2.mutable.Specification +import collection.JavaConversions._ +import org.specs2.specification.{SpecificationStructure, Step, Fragments} +import akka.contrib.jul.JavaLogging +import org.specs2.control.StackTraceFilter +import akka.testkit.TestKit +import akka.actor.ActorSystem +import akka.util.Timeout +import org.cakesolutions.scalad.mongo.sprayjson.SprayMongo + + +/** Convenient parent for all Specs, ensuring that exceptions are (mostly) correctly + * logged. This is necessary because Specs2 tries to do its own exception logging + * and ends up duplicating a lot of functionality already provided by J2SE. + * + * Provides access to a 'log' field. + * + * NOTE: don't forget to add `sequential` if your specs depend on ordering. + */ +abstract class NoActorSpecs extends Specification with JavaLogging { + // change Specification to SpecificationWithJUnit for JUnit integration (not needed with SBT anymore) + + args.report(traceFilter = LoggedStackTraceFilter) +} + +/** Convenient parent for Specs that test an Actor: logging is enabled, timeouts are set, specs + * run sequentially and the actor system is closed down after all specs have run. + * + * The logging backend helps to catch a lot of root causes, as typically a failed actor + * spec will result in a timeout in the spec plus a hidden-away Akka log revealing the true + * exception. + */ +//@RunWith(classOf[JUnitRunner]) +abstract class ActorSpecs extends TestKit(ActorSystem()) with Specification with JavaLogging { + + args.report(traceFilter = LoggedStackTraceFilter) + + sequential + + implicit def self = testActor + + implicit val timeout = Timeout(10000) + + // https://groups.google.com/d/topic/specs2-users/PdCeX4zxc0A/discussion + override def map(fs: => Fragments) = super.map(fs) ^ Step(system.shutdown()) +} + + +/** Provides access to a test MongoDB instance. + */ +trait TestMongo extends Configuration with Configured with NoSqlConfig with Resources { + this: Specification with JavaLogging => + + configure(mongo(Settings.test.db.mongo)) + + val mongo = new SprayMongo + + def resetMongo() { + val db = configured[DB] + log.debug(s"resetting ${db.getName}") + db.dropDatabase() + } +} + +/** Provides access to a test MongoDB instance that is cleaned up before any specs are run. + * Note that Specs will break if SBT runs them in parallel, so ensure `parallelExecution in Test := false`. + */ +trait CleanMongo extends TestMongo { + this: Specification with JavaLogging => + + // https://groups.google.com/d/topic/specs2-users/PdCeX4zxc0A/discussion + override def map(fs: => Fragments) = Step(resetMongo()) ^ fs +} + + +/** Convenient mixin that provides access to a cleanly prepared (before any spec is run) Cassandra. + */ +trait TestCassandra extends SpecificationStructure with Configuration with Configured with NoSqlConfig with Resources { + this: Specification with JavaLogging => + + configure(cassandra(Settings.test.db.cassandra)) + + def resetCassandra() { + log.info("resetting cassandra") + val cassandraBase = new ClassPathYamlDataSet("org/cakesolutions/akkapatterns/test/cassandra-base.yaml") + val cluster = configured[Cluster] + val name = cluster.describeClusterName() + val host = cluster.getKnownPoolHosts(false).head.getHost + new DataLoader(name, host).load(cassandraBase) + } +} + +trait CleanCassandra extends TestCassandra { + this: Specification with JavaLogging => + + override def map(fs: => Fragments) = super.map(fs) ^ Step(resetCassandra()) +} + +// from com.github.fommil.scala-logging +object LoggedStackTraceFilter extends StackTraceFilter with JavaLogging { + def apply(e: Seq[StackTraceElement]) = Nil + + override def apply[T <: Exception](e: T): T = { + log.error(e, "Specs2") + // this only works because log.error will construct the LogRecord instantly + e.setStackTrace(new Array[StackTraceElement](0)) + e + } +} \ No newline at end of file diff --git a/server/test/src/main/scala/org/cakesolutions/akkapatterns/testdata.scala b/server/test/src/main/scala/org/cakesolutions/akkapatterns/testdata.scala new file mode 100644 index 0000000..77c0826 --- /dev/null +++ b/server/test/src/main/scala/org/cakesolutions/akkapatterns/testdata.scala @@ -0,0 +1,61 @@ +package org.cakesolutions.akkapatterns + +import domain._ +import org.specs2.mutable.Before +import com.mongodb.{BasicDBList, DBObject, DB} +import com.mongodb.util.JSON +import scala.collection.JavaConversions._ +import akka.contrib.jul.JavaLogging +import java.util.UUID + +/* + * The idea with test data is to provide + * + * 1. Fixtures - can be attached to specs (individual or for a whole file) + * which define the state of the database at the beginning + * of the spec examples. Typically these are loaded per example. + * 2. Identifiers - can be used to lookup data that is inserted into the DB + * by the fixtures. Typically these are mixed in to specs. + * 3. Data Instances - which is suitable for programmatically inserting into + * the database. Typically these are mixed in to specs. + */ + +object MongoCollectionFixture { + class Fix(names: String*) extends MongoCollectionFixture(true, names:_*) + + class ContinueFix(names: String*) extends MongoCollectionFixture(false, names:_*) +} + +/** + * Fixture that evaluates named files (in Mongo Javascript format) from the classpath. + * + * @param clean if true, will drop the database before running the fixture + * @param names + */ +class MongoCollectionFixture(clean: Boolean, names: String*) extends Configured with Resources with JavaLogging with Before { + override def before() { + if (clean) + configured[DB].dropDatabase() + + val header = readResource(s"classpath:/org/cakesolutions/akkapatterns/testdata/common.js").mkString + names.foreach { + name => + configured[DB].eval( + header + + readResource(s"classpath:/org/cakesolutions/akkapatterns/testdata/${name}.js").mkString + ) + } + } +} + + +//trait TestUserData { +// val TestGuestUserId = UUID.fromString("994fc1f0-90a9-11e2-9e96-0800200c9a66") +// val TestCustomerUserId = UUID.fromString("7370f980-90aa-11e2-9e96-0800200c9a66") +// val TestAdminUserId = UUID.fromString("c0a93190-90aa-11e2-9e96-0800200c9a66") +//} + +trait TestCustomerData { + val TestCustomerJanId = UUID.fromString("122fa630-92fd-11e2-9e96-0800200c9a66") + +} diff --git a/sbt/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala b/server/test/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala similarity index 65% rename from sbt/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala rename to server/test/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala index 0a762d8..3acc4d9 100644 --- a/sbt/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala +++ b/server/test/src/test/scala/org/cakesolutions/akkapatterns/ArchitectureSpec.scala @@ -1,10 +1,10 @@ package org.cakesolutions.akkapatterns -import org.specs2.mutable.Specification import org.specs2.specification.Analysis import org.specs2.analysis.ClassycleDependencyFinder +import com.mongodb.DB -class ArchitectureSpec extends Specification with Analysis with ClassycleDependencyFinder { +class ArchitectureSpec extends NoActorSpecs with Analysis with ClassycleDependencyFinder with TestMongo { "The architecture" should { "Have properly defined layers" in { @@ -20,4 +20,10 @@ class ArchitectureSpec extends Specification with Analysis with ClassycleDepende } } + "The mongo database" should { + "be configured" in { + configured[DB] must not beNull + } + } + } diff --git a/server/test/src/test/scala/org/cakesolutions/akkapatterns/TestDataSpecs.scala b/server/test/src/test/scala/org/cakesolutions/akkapatterns/TestDataSpecs.scala new file mode 100644 index 0000000..7bc8f43 --- /dev/null +++ b/server/test/src/test/scala/org/cakesolutions/akkapatterns/TestDataSpecs.scala @@ -0,0 +1,24 @@ +package org.cakesolutions.akkapatterns + +import org.cakesolutions.akkapatterns.domain._ +import com.mongodb.DB +import org.cakesolutions.scalad.mongo.sprayjson._ +import java.util.UUID +import org.cakesolutions.akkapatterns.MongoCollectionFixture._ + +class TestDataSpecs extends NoActorSpecs with CleanMongo with CustomerMongo with TestCustomerData { + + "Mongo Test Data" should { + "be clean at the beginning" in { + configured[DB].getCollectionNames.size === 0 + } + + "customers fixture should attach" in new Fix("customers") { + mongo.count[Customer]() must beGreaterThan(0L) + mongo.findOne[Customer]("id" :> TestCustomerJanId) must beLike { + case Some(customer) if customer.firstName == "Jan" => ok + } + mongo.findOne[Customer]("id" :> UUID.randomUUID()) === None + } + } +}