Skip to content

Commit

Permalink
Add simple rest service implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
oermolaev committed Jul 26, 2013
1 parent 3f01e0e commit 757e8b2
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 0 deletions.
26 changes: 26 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import AssemblyKeys._

name := "rest"

version := "1.0"

scalaVersion := "2.10.2"

libraryDependencies ++= Seq(
"io.spray" % "spray-can" % "1.1-M8",
"io.spray" % "spray-http" % "1.1-M8",
"io.spray" % "spray-routing" % "1.1-M8",
"net.liftweb" %% "lift-json" % "2.5.1",
"com.typesafe.slick" %% "slick" % "1.0.1",
"mysql" % "mysql-connector-java" % "5.1.25",
"com.typesafe.akka" %% "akka-actor" % "2.1.4",
"com.typesafe.akka" %% "akka-slf4j" % "2.1.4",
"ch.qos.logback" % "logback-classic" % "1.0.13"
)

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

assemblySettings
8 changes: 8 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resolvers ++= Seq(
"Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/",
"Sonatype releases" at "https://oss.sonatype.org/content/repositories/releases/"
)

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.0-SNAPSHOT")

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.0")
17 changes: 17 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
akka {
loglevel = DEBUG
event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
}

service {
host = "localhost"
port = 8080
}

db {
host = "localhost"
port = 3306
name = "rest"
user = "root"
password = null
}
18 changes: 18 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{1} - %msg%n</pattern>
</encoder>
</appender>

<logger name="akka" level="INFO"/>
<logger name="scala.slick" level="INFO"/>

<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>

</configuration>
19 changes: 19 additions & 0 deletions src/main/scala/com/sysgears/example/boot/Boot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.sysgears.example.boot

import akka.actor.{Props, ActorSystem}
import akka.io.IO
import com.sysgears.example.config.Configuration
import com.sysgears.example.rest.RestServiceActor
import spray.can.Http

object Boot extends App with Configuration {

// create an actor system for application
implicit val system = ActorSystem("rest-service-example")

// create and start rest service actor
val restService = system.actorOf(Props[RestServiceActor], "rest-endpoint")

// start HTTP server with rest service actor as a handler
IO(Http) ! Http.Bind(restService, serviceHost, servicePort)
}
36 changes: 36 additions & 0 deletions src/main/scala/com/sysgears/example/config/Configuration.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.sysgears.example.config

import com.typesafe.config.ConfigFactory
import util.Try

/**
* Holds service configuration settings.
*/
trait Configuration {

/**
* Application config object.
*/
val config = ConfigFactory.load()

/** Host name/address to start service on. */
lazy val serviceHost = Try(config.getString("service.host")).getOrElse("localhost")

/** Port to start service on. */
lazy val servicePort = Try(config.getInt("service.port")).getOrElse(8080)

/** Database host name/address. */
lazy val dbHost = Try(config.getString("db.host")).getOrElse("localhost")

/** Database host port number. */
lazy val dbPort = Try(config.getInt("db.port")).getOrElse(3306)

/** Service database name. */
lazy val dbName = Try(config.getString("db.name")).getOrElse("rest")

/** User name used to access database. */
lazy val dbUser = Try(config.getString("db.user")).toOption.orNull

/** Password for specified user and database. */
lazy val dbPassword = Try(config.getString("db.password")).toOption.orNull
}
163 changes: 163 additions & 0 deletions src/main/scala/com/sysgears/example/dao/CustomerDAO.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.sysgears.example.dao

import com.sysgears.example.config.Configuration
import com.sysgears.example.domain._
import java.sql._
import scala.Some
import scala.slick.driver.MySQLDriver.simple.Database.threadLocalSession
import scala.slick.driver.MySQLDriver.simple._
import slick.jdbc.meta.MTable

/**
* Provides DAL for Customer entities for MySQL database.
*/
class CustomerDAO extends Configuration {

// init Database instance
private val db = Database.forURL(url = "jdbc:mysql://%s:%d/%s".format(dbHost, dbPort, dbName),
user = dbUser, password = dbPassword, driver = "com.mysql.jdbc.Driver")

// create tables if not exist
db.withSession {
if (MTable.getTables("customers").list().isEmpty) {
Customers.ddl.create
}
}

/**
* Saves customer entity into database.
*
* @param customer customer entity to
* @return saved customer entity
*/
def create(customer: Customer): Either[Failure, Customer] = {
try {
val id = db.withSession {
Customers returning Customers.id insert customer
}
Right(customer.copy(id = Some(id)))
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Updates customer entity with specified one.
*
* @param id id of the customer to update.
* @param customer updated customer entity
* @return updated customer entity
*/
def update(id: Long, customer: Customer): Either[Failure, Customer] = {
try
db.withSession {
Customers.where(_.id === id) update customer.copy(id = Some(id)) match {
case 0 => Left(notFoundError(id))
case _ => Right(customer.copy(id = Some(id)))
}
}
catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Deletes customer from database.
*
* @param id id of the customer to delete
* @return deleted customer entity
*/
def delete(id: Long): Either[Failure, Customer] = {
try {
db.withTransaction {
val query = Customers.where(_.id === id)
val customers = query.run.asInstanceOf[List[Customer]]
customers.size match {
case 0 =>
Left(notFoundError(id))
case _ => {
query.delete
Right(customers.head)
}
}
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Retrieves specific customer from database.
*
* @param id id of the customer to retrieve
* @return customer entity with specified id
*/
def get(id: Long): Either[Failure, Customer] = {
try {
db.withSession {
Customers.findById(id).firstOption match {
case Some(customer: Customer) =>
Right(customer)
case _ =>
Left(notFoundError(id))
}
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Retrieves list of customers with specified parameters from database.
*
* @param params search parameters
* @return list of customers that match given parameters
*/
def search(params: CustomerSearchParameters): Either[Failure, List[Customer]] = {
implicit val typeMapper = Customers.dateTypeMapper

try {
db.withSession {
val query = for {
customer <- Customers if {
Seq(
params.firstName.map(customer.firstName is _),
params.lastName.map(customer.lastName is _),
params.birthday.map(customer.birthday is _)
).flatten match {
case Nil => ConstColumn.TRUE
case seq => seq.reduce(_ && _)
}
}
} yield customer

Right(query.run.toList)
}
} catch {
case e: SQLException =>
Left(databaseError(e))
}
}

/**
* Produce database error description.
*
* @param e SQL Exception
* @return database error description
*/
protected def databaseError(e: SQLException) =
Failure("%d: %s".format(e.getErrorCode, e.getMessage), FailureType.DatabaseFailure)

/**
* Produce customer not found error description.
*
* @param customerId id of the customer
* @return not found error description
*/
protected def notFoundError(customerId: Long) =
Failure("Customer with id=%d does not exist".format(customerId), FailureType.NotFound)
}
41 changes: 41 additions & 0 deletions src/main/scala/com/sysgears/example/domain/Customer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.sysgears.example.domain

import scala.slick.driver.MySQLDriver.simple._

/**
* Customer entity.
*
* @param id unique id
* @param firstName first name
* @param lastName last name
* @param birthday date of birth
*/
case class Customer(id: Option[Long], firstName: String, lastName: String, birthday: Option[java.util.Date])

/**
* Mapped customers table object.
*/
object Customers extends Table[Customer]("customers") {

def id = column[Long]("id", O.PrimaryKey, O.AutoInc)

def firstName = column[String]("first_name")

def lastName = column[String]("last_name")

def birthday = column[java.util.Date]("birthday", O.Nullable)

def * = id.? ~ firstName ~ lastName ~ birthday.? <>(Customer, Customer.unapply _)

implicit val dateTypeMapper = MappedTypeMapper.base[java.util.Date, java.sql.Date](
{
ud => new java.sql.Date(ud.getTime)
}, {
sd => new java.util.Date(sd.getTime)
})

val findById = for {
id <- Parameters[Long]
c <- this if c.id is id
} yield c
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sysgears.example.domain

import java.util.Date

/**
* Customers search parameters.
*
* @param firstName first name
* @param lastName last name
* @param birthday date of birth
*/
case class CustomerSearchParameters(firstName: Option[String] = None,
lastName: Option[String] = None,
birthday: Option[Date] = None)
41 changes: 41 additions & 0 deletions src/main/scala/com/sysgears/example/domain/Failure.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.sysgears.example.domain

import spray.http.{StatusCodes, StatusCode}

/**
* Service failure description.
*
* @param message error message
* @param errorType error type
*/
case class Failure(message: String, errorType: FailureType.Value) {

/**
* Return corresponding HTTP status code for failure specified type.
*
* @return HTTP status code value
*/
def getStatusCode: StatusCode = {
FailureType.withName(this.errorType.toString) match {
case FailureType.BadRequest => StatusCodes.BadRequest
case FailureType.NotFound => StatusCodes.NotFound
case FailureType.Duplicate => StatusCodes.Forbidden
case FailureType.DatabaseFailure => StatusCodes.InternalServerError
case _ => StatusCodes.InternalServerError
}
}
}

/**
* Allowed failure types.
*/
object FailureType extends Enumeration {
type Failure = Value

val BadRequest = Value("bad_request")
val NotFound = Value("not_found")
val Duplicate = Value("entity_exists")
val DatabaseFailure = Value("database_error")
val InternalError = Value("internal_error")
}

Loading

0 comments on commit 757e8b2

Please sign in to comment.