Skip to content

Commit

Permalink
[tutorial-4] Basic structure for the tutorial on spray magnet pattern…
Browse files Browse the repository at this point in the history
… and location header generation
  • Loading branch information
DanielaSfregola committed Nov 8, 2015
1 parent 601bc54 commit 65ad49c
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tutorial-4/project/project/target
36 changes: 36 additions & 0 deletions tutorial-4/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
enablePlugins(JavaServerAppPackaging)

name := "quiz-management-service"

version := "0.1"

organization := "com.danielasfregola"

scalaVersion := "2.11.5"

resolvers ++= Seq("Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/",
"Spray Repository" at "http://repo.spray.io")

libraryDependencies ++= {
val AkkaVersion = "2.3.9"
val SprayVersion = "1.3.2"
val Json4sVersion = "3.2.11"
Seq(
"com.typesafe.akka" %% "akka-actor" % AkkaVersion,
"io.spray" %% "spray-can" % SprayVersion,
"io.spray" %% "spray-routing" % SprayVersion,
"io.spray" %% "spray-json" % "1.3.1",
"com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
"ch.qos.logback" % "logback-classic" % "1.1.2",
"org.json4s" %% "json4s-native" % Json4sVersion,
"org.json4s" %% "json4s-ext" % Json4sVersion,
"com.typesafe.akka" %% "akka-testkit" % AkkaVersion % "test",
"io.spray" %% "spray-testkit" % SprayVersion % "test",
"org.specs2" %% "specs2" % "2.3.13" % "test"
)
}

// Assembly settings
mainClass in Global := Some("com.danielasfregola.quiz.management.Main")

jarName in assembly := "quiz-management-server.jar"
1 change: 1 addition & 0 deletions tutorial-4/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.6
7 changes: 7 additions & 0 deletions tutorial-4/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resolvers += Classpaths.typesafeResolver

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-M4")

addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.3.9")
28 changes: 28 additions & 0 deletions tutorial-4/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
akka {
loglevel = INFO
stdout-loglevel = INFO
loggers = ["akka.event.slf4j.Slf4jLogger"]
default-dispatcher {
fork-join-executor {
parallelism-min = 8
}
}
test {
timefactor = 1
}
}

spray {
can {
server {
server-header = "Quiz Management Service REST API"
}
}
}

http {
host = "0.0.0.0"
host = ${?HOST}
port = 5000
port = ${?PORT}
}
19 changes: 19 additions & 0 deletions tutorial-4/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<!--<pattern>%date{MM/dd HH:mm:ss} %-6level[%logger{0}]: %msg%n</pattern>-->
<pattern>%-6level[%logger{0}]: %msg%n</pattern>
</encoder>
</appender>

<logger name="akka" level="INFO" />
<logger name="spray" level="INFO" />

<logger name="com.danielasfregola" level="INFO" />

<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.danielasfregola.quiz.management

import akka.actor._
import akka.io.IO
import akka.pattern.ask
import akka.util.Timeout
import com.typesafe.config.ConfigFactory
import spray.can.Http

import scala.concurrent.duration._

object Main extends App {
val config = ConfigFactory.load()
val host = config.getString("http.host")
val port = config.getInt("http.port")

implicit val system = ActorSystem("quiz-management-service")
implicit val executionContext = system.dispatcher
implicit val timeout = Timeout(10 seconds)

val api = system.actorOf(Props(new RestInterface))

IO(Http).ask(Http.Bind(listener = api, interface = host, port = port))
.mapTo[Http.Event]
.map {
case Http.Bound(address) =>
println(s"REST interface bound to $address")
case Http.CommandFailed(cmd) =>
println("REST interface could not bind to " +
s"$host:$port, ${cmd.failureMessage}")
system.shutdown()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.danielasfregola.quiz.management

import com.danielasfregola.quiz.management.resources.QuestionResource
import com.danielasfregola.quiz.management.services.QuestionService
import spray.routing._

import scala.concurrent.ExecutionContext
import scala.language.postfixOps

class RestInterface(implicit val executionContext: ExecutionContext) extends HttpServiceActor with Resources {

def receive = runRoute(routes)

val questionService = new QuestionService

val routes: Route = questionRoutes

}

trait Resources extends QuestionResource
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.danielasfregola.quiz.management.resources

import com.danielasfregola.quiz.management.serializers.JsonSupport
import com.danielasfregola.quiz.management.services.{Question, QuestionService, QuestionUpdate}
import spray.routing._

import scala.concurrent.ExecutionContext

trait QuestionResource extends HttpService with JsonSupport {

implicit val executionContext: ExecutionContext

val questionService: QuestionService

def questionRoutes: Route = pathPrefix("questions") {
pathEnd {
post {
entity(as[Question]) { question =>
questionService.createQuestion(question)
???
}
}
} ~
path(Segment) { id =>
get {
complete(questionService.getQuestion(id))
} ~
put {
entity(as[QuestionUpdate]) { update =>
questionService.updateQuestion(id, update)
???
}
}
delete {
complete(questionService.deleteQuestion(id))
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.danielasfregola.quiz.management.serializers

import java.sql.Timestamp

import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JInt, JNull}

object CustomSerializers {
val all = List(CustomTimestampSerializer)
}

case object CustomTimestampSerializer extends CustomSerializer[Timestamp](format =>
({
case JInt(x) => new Timestamp(x.longValue * 1000)
case JNull => null
},
{
case date: Timestamp => JInt(date.getTime / 1000)
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.danielasfregola.quiz.management.serializers

import java.text.SimpleDateFormat

import org.json4s.ext.JodaTimeSerializers
import org.json4s.{DefaultFormats, Formats}
import spray.httpx.Json4sSupport

trait JsonSupport extends Json4sSupport {

implicit def json4sFormats: Formats = customDateFormat ++ JodaTimeSerializers.all ++ CustomSerializers.all

val customDateFormat = new DefaultFormats {
override def dateFormatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.danielasfregola.quiz.management.services

import scala.concurrent.{ExecutionContext, Future}

case class Question(id: String, title: String, text: String)

case class QuestionUpdate(title: Option[String], text: Option[String])

class QuestionService(implicit executionContext: ExecutionContext) {

var questions = Vector.empty[Question]

def createQuestion(question: Question): Future[Option[String]] = Future {
questions.find(_.id == question.id) match {
case Some(q) => None // conflict! id is already taken
case None =>
questions = questions :+ question
Some(question.id)
}
}

def getQuestion(id: String): Future[Option[Question]] = Future {
questions.find(_.id == id)
}

def updateQuestion(id: String, update: QuestionUpdate): Future[Option[String]] = {

def updateEntity(question: Question): Question = {
val title = update.title.getOrElse(question.title)
val text = update.text.getOrElse(question.text)
Question(id, title, text)
}

getQuestion(id).flatMap { maybeQuestion =>
maybeQuestion match {
case None => Future { None } // No question found
case Some(question) =>
val updatedQuestion = updateEntity(question)
deleteQuestion(id).flatMap(_ => createQuestion(updatedQuestion))
}
}
}

def deleteQuestion(id: String): Future[Unit] = Future {
questions.filterNot(_.id == id)
}


}

0 comments on commit 65ad49c

Please sign in to comment.