2016/10/24

HTTP programming in Scala Play


如何在 scala play 中取得 broswer 的 request,並將結果回傳給 browser,scala play 是以 routes 設定為進入點,確認要使用哪一個 controller 處理哪一個 url request,最重要的是,簡化了非同步處理的處理方法,我們可以很輕鬆地就隔絕後端 DB 的處理,避免因 DB 異常影響到前端頁面卡頓的狀況。


Actions, Controllers and Results


在 Play 收到的 browser request 就是由 Action 處理,並產生 Result


plya.api.mvc.Action 就等同於 (play.api.mvc.Request => plya.api.mv.Result) 函數


controller 就是 Action generator,在 controller 中可以定義 method 為


package controllers

import play.api.mvc._

class Application extends Controller {
  def echo = Action { request =>
    Ok("Got request [" + request + "]")
  }
  
  def hello(name: String) = Action {
    Ok("Hello " + name)
  }
}

產生 Action 的範例


Action {
  Ok("Hello world")
}

Action { request =>
  Ok("Got request [" + request + "]")
}

Action { implicit request =>
  Ok("Got request [" + request + "]")
}

// 指定 BodyParser
Action(parse.json) { implicit request =>
  Ok("Got request [" + request + "]")
}



index 及 index2 會產生相同的頁面 Result


import play.api.http.HttpEntity

def index = Action {
  Result(
    header = ResponseHeader(200, Map.empty),
    body = HttpEntity.Strict(ByteString("Hello world!"), Some("text/plain"))
  )
}

def index2 = Action {
  Ok("Hello world!")
}

以下這些為各種不同類型的 play.api.mvc.Results 的範例


val ok = Ok("Hello world!")
val notFound = NotFound
val pageNotFound = NotFound(<h1>Page not found</h1>)
val badRequest = BadRequest(views.html.form(formWithErrors))
val oops = InternalServerError("Oops")
val anyStatus = Status(488)("Strange response type")



處理 Redirect


// 303 SEE_OTHER
def index = Action {
  Redirect("/user/home")
}

// 301 MOVED_PERMANENTLY
def index = Action {
  Redirect("/user/home", MOVED_PERMANENTLY)
}



如果只是定義介面,還不實作,可以用 TODO


def index(name:String) = TODO


HTTP Routing


定義 /conf/routes


Play 支援兩種 router: 預設建議使用第一種


  1. dependency injected router
  2. static router

如果要更換 router 為 static router,可以在 build.sbt 中調整設定。如果在 routes 符合兩個以上的路由,那就依照路由宣告的順序使用第一個。


routesGenerator := StaticRoutesGenerator



route 的第一個欄位是填寫 HTTP method,有以下這六種


GET, PATCH, POST, PUT, DELETE, HEAD




route 範例


# 固定路徑
GET   /clients/all          controllers.Clients.list()

# 動態取得路徑中的一部份,可以用 regular expression: [^/]+
GET   /clients/:id          controllers.Clients.show(id: Long)

# *id 就等同於 .+
GET   /files/*name          controllers.Application.download(name)

# 使用自訂的 regular expression,如果要自動轉型,才填寫 id:Long,否則可以省略不寫
GET   /items/$id<[0-9]+>    controllers.Items.show(id: Long)

# 可以給予參數的預設值
GET   /items/               controllers.Items.show(id: Long=10)

Manipulating results


Result 的 Content-Type


// Content-Type: text/plain
val textResult = Ok("Hello World!")

// Content-Type: application/xml
val xmlResult = Ok(<message>Hello World!</message>)

// text/html
val htmlResult = Ok(<h1>Hello World!</h1>).as("text/html")

// text/html; charset=utf-8
val htmlResult2 = Ok(<h1>Hello World!</h1>).as(HTML)



可以增加或修改 HTTP Response Header


val result = Ok("Hello World!").withHeaders(
  CACHE_CONTROL -> "max-age=3600",
  ETAG -> "xx")



設定 Response Cookie


// 增加 cookie: theme
val result = Ok("Hello world").withCookies(
  Cookie("theme", "blue"))

// 清除 cookie: theme
val result2 = result.discardingCookies(DiscardingCookie("theme"))

// 增加 cookie: theme, 同時刪除 cookie: skin
val result3 = result.withCookies(Cookie("theme", "blue")).discardingCookies(DiscardingCookie("skin"))



預設的 charset 為 utf-8,可以修改


利用 scala 的 implicit instance,可以改變 HTML 的 charset


class Application extends Controller {

  implicit val myCustomCharset = Codec.javaSupported("iso-8859-1")

  def index = Action {
    // text/html; charset=iso-8859-1 
    Ok(<h1>Hello World!</h1>).as(HTML)
  }
}

因為 HTML 是用以下的方式定義的


def HTML(implicit codec: Codec) = {
  "text/html; charset=" + codec.charset
}

Session and Flash scopes


如果需要在不同 HTTP requests 之間共用資料,可以儲存在 Session 或是 Flash scopes,Session 會在整個 user Session 都可以使用,Flash scope 只能在下一個 request 中使用。


session 名稱預設為 PLAYSESSION,可以在 application.conf 用 session.cookieName 修改,(為了簡化相容性的問題,其實可以直接改成 JSESSIONID),PLAYSESSION 有使用 private key 加密以避免 client 竄改。


application.conf 的 play.http.session.maxAge (ms) 可設定 session 存活的時間


如果要暫存一些資料,可以在 SESSION 中存入 unique ID ,然後在 play 的 cache 機制中儲存資料




session 的新增與刪除


// 存入 session
Ok("Welcome!").withSession(
  "connected" -> "user@gmail.com")

Ok("Hello World!").withSession(
  request.session + ("saidHello" -> "yes"))

// 移除 session: theme
Ok("Theme reset!").withSession(
  request.session - "theme")

// 讀取 session value,判斷 user 是否已經登入系統
def index = Action { request =>
  request.session.get("connected").map { user =>
    Ok("Hello " + user)
  }.getOrElse {
    Unauthorized("Oops, you are not connected")
  }
}

// 直接用新的 session 將舊的 session 刪除
Ok("Bye").withNewSession



// 取得 flash scope value: success
def index = Action { implicit request =>
  Ok {
    request.flash.get("success").getOrElse("Welcome!")
  }
}

// 新增 flash scope: success
def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created")
}

如果要在畫面上使用 flash scope value: success


@()(implicit flash: Flash)

@flash.get("success").getOrElse("Welcome!")

controller 必須用 implicit instance request 傳送到 views


def index = Action { implicit request =>
  Ok(views.html.index())
}

Body parsers


一般的 http request 資料會暫存記憶體中,可以用 BodyParser 來處理。如果 http request body 內容太長,就需要 以 stream 的方式來處理,以免資料需要整個暫存在記憶體中。


Play 是使用 Akka Strems 處理 stream 資料。




內建的 body parsers 可以處理 JSON, XML, forms


Action 是以 Request[A] => Result 定義的,只要有可以處理 A 的 BodyParser,就可以處理對應的 Request Content


trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

trait Request[+A] extends RequestHeader {
  def body: A
}

預設會以 request header: Content-Type 作為選用 BodyParser 的條件。例如 Content-Type 為 application/json,就會使用 JsValue 的 BodyParser,Content-Type 為 application/x-form-wwww-urlencoded,就 paring 為 Map[String, Seq[String]]




預設 body parser 會產生 AnyContent 資料型別,然後再以 asJson 轉換成 JsValue


def save = Action { request =>
  val body: AnyContent = request.body
  val jsonBody: Option[JsValue] = body.asJson

  // Expecting json body
  jsonBody.map { json =>
    Ok("Got: " + (json \ "name").as[String])
  }.getOrElse {
    BadRequest("Expecting application/json request body")
  }
}

以下是 default body parser 支援的資料對應


  • text/plain: String, asText
  • application/json: JsValue, asJson
  • application/xml, text/xml 或是 application/XXX+xml: scala.xml.NodeSeq, asXml
  • application/form-url-encoded: Map[String, Seq[String]], asFormUrlEncoded
  • multipart/form-data: MultipartFormData, asMultipartFormData
  • 其他的content: RawBuffer, asRaw



選用適當的 body parser


def save = Action(parse.json) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

可以用 curl 測試,會得到 Got: user name


curl 'http://localhost:9000/test' -H 'Content-Type:application/json' --data-binary '{"name": "user name","interval": "month"}'

如果使用 parse.tolerantJson,就會忽略 Content-Type,直接嘗試將 content body 以 json parsing


def save = Action(parse.tolerantJson) { request =>
  Ok("Got: " + (request.body \ "name").as[String])
}

以下例子可以將 request body 儲存到一個檔案中


def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
  Ok("Saved the request content to " + request.body)
}

storeInUserFile 是一個自訂的 body parser,可以從 session 取得 username,並將 body 以 user 名稱儲存為檔案


val storeInUserFile = parse.using { request =>
  request.session.get("username").map { user =>
    file(to = new File("/tmp/" + user + ".upload"))
  }.getOrElse {
    sys.error("You don't have the right to upload here")
  }
}

def save = Action(storeInUserFile) { request =>
  Ok("Saved the request content to " + request.body)
}



application.conf 中是以這個設定值,決定最大的 content length


play.http.parser.maxMemoryBuffer=128K

可以直接用 maxLength 覆寫這個設定值,parse.maxLength 可以封裝任何一個 body parser


// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
  Ok("Got: " + text)
}

// Accept only 10KB of data.
def save = Action(parse.maxLength(1024 * 10, storeInUserFile)) { request =>
  Ok("Saved the request content to " + request.body)
}



Custom body parser


note: 這部分我決定先跳過不看,等比較熟悉之後,再去了解


Actions composition


如果需要做一個 Action logging decorator,記錄所有呼叫這個 action 的 request。


首先要實作 invokeBlock method,ActionBuilder 產生的每一個 action 都會呼叫。


import play.api.mvc._

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    Logger.info("Calling action")
    block(request)
  }
}

使用剛剛的 LoggingAction


def index = LoggingAction {
  Ok("Hello World")
}

def submit = LoggingAction(parse.text) { request =>
  Ok("Got a body " + request.body.length + " bytes long")
}



用上面的寫法,會需要寫很多個 action buidler,如果要改成將既有的 Action 增加 Logging 的功能,可以用 wrapping actions 的方法實作。


import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

  def apply(request: Request[A]): Future[Result] = {
    Logger.info("Calling action")
    action(request)
  }

  lazy val parser = action.parser
}

也可以不產生 action class,直接定義 logging method


import play.api.mvc._

def logging[A](action: Action[A])= Action.async(action.parser) { request =>
  Logger.info("Calling action")
  action(request)
}



Actions 可以利用 composeAction 方法 mixed in to action builder


object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    block(request)
  }
  override def composeAction[A](action: Action[A]) = new Logging(action)
}

現在就可以這樣使用 LogginAction,或是用 mixin wrapping actions without action builder 的方法


def index = LoggingAction {
  Ok("Hello World")
}

def index = Logging {
  Action {
    Ok("Hello World")
  }
}



這個例子是修改 request,增加一個 remoteAddress 欄位


import play.api.mvc._

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
  val newRequest = request.headers.get("X-Forwarded-For").map { xff =>
    new WrappedRequest[A](request) {
      override def remoteAddress = xff
    }
  } getOrElse request
  action(newRequest)
}

這個例子是拒絕 http 連線,只允許 https 通過


import play.api.mvc._

def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
  request.headers.get("X-Forwarded-Proto").collect {
    case "https" => action(request)
  } getOrElse {
    Future.successful(Forbidden("Only HTTPS requests allowed"))
  }
}

修改 response,增加一個 response header: X-UA-Compatible


import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
  action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}



四個 pre-defined traits,實作了 ActionFunction,用來對不同資料進行處理。我們也可以實作 invokeFunction,實作自訂的 ActionFunction。


  1. ActionTransformer
    可修改 request, 增加資料
  2. ActionFilter
    選擇性攔截 request,可用來產生 error
  3. ActionRefiner
    以上兩種的 general case
  4. ActionBuilder
    以 Request 為參數,可以產生新的 actions

使用範例:Authentication


import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

object UserAction extends
    ActionBuilder[UserRequest] with ActionTransformer[Request, UserRequest] {
  def transform[A](request: Request[A]) = Future.successful {
    new UserRequest(request.session.get("username"), request)
  }
}

使用範例:增加資料到 request


// add Item to UserRequest
import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  def username = request.username
}

// looks up that item and returns Either an error (Left) or a new ItemRequest (Right)
def ItemAction(itemId: String) = new ActionRefiner[UserRequest, ItemRequest] {
  def refine[A](input: UserRequest[A]) = Future.successful {
    ItemDao.findById(itemId)
      .map(new ItemRequest(_, input))
      .toRight(NotFound)
  }
}

使用範例: Validating requests


object PermissionCheckAction extends ActionFilter[ItemRequest] {
  def filter[A](input: ItemRequest[A]) = Future.successful {
    if (!input.item.accessibleByUser(input.username))
      Some(Forbidden)
    else
      None
  }
}

利用 ActionBuilder,以及 andThen,把以上的 action function 合併在一起


def tagItem(itemId: String, tag: String) =
  (UserAction andThen ItemAction(itemId) andThen PermissionCheckAction) { request =>
    request.item.addTag(tag)
    Ok("User " + request.username + " tagged " + request.item.id)
  }

Content negotiation


這個機制可以用相同的 URI 處理不同的 content boday,例如同一個 URI 網址可以同時接受 XML, JSON。也可以利用 Accept-Language 欄位,決定回傳的 response 要用什麼語系的內容。


play.api.mvc.RequestHeader#acceptLanguages 可取得 Accept-Language 的資料


play.api.mvc.RequestHeader#acceptedTypes 可取得可接受的 request MIME types 列表,也就是 Accept 欄位。實際上 Accept 欄位並沒有確切的 MIME type,而是 media range 例如 text/*, /,controller 提供 render method,處理 media range。


val list = Action { implicit request =>
  val items = Item.findAll
  render {
    case Accepts.Html() => Ok(views.html.list(items))
    case Accepts.Json() => Ok(Json.toJson(items))
  }
}

可以使用 play.api.mvc.Accepting case class 產生特定 MIME type 的自訂 extractor。


  val AcceptsMp3 = Accepting("audio/mp3")
  render {
    case AcceptsMp3() => ???
  }

Handling errors


HTTP application 的 error 主要有兩種:client errors, server errors。


Play 會自動偵測 client errors,包含 malformed header value, unsupported content types, requests for unknown resource。也會自動處理一些 server errors,例如 code throws an exception,Play 會自動產生 error page。


以下為 custom error handler,實作了兩個 methods: onClientError 及 onServerError。


import play.api.http.HttpErrorHandler
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent._
import javax.inject.Singleton;

@Singleton
class ErrorHandler extends HttpErrorHandler {

  def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    Future.successful(
      Status(statusCode)("A client error occurred: " + message)
    )
  }

  def onServerError(request: RequestHeader, exception: Throwable) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }
}

可在 application.conf 設定 error handler


play.http.errorHandler = "com.example.ErrorHandler"



也可以利用 default error handler 延伸自己的功能。以下就是不改變 development error message,並修改特定的 onForbidden error page。


import javax.inject._

import play.api.http.DefaultHttpErrorHandler
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.routing.Router
import scala.concurrent._

@Singleton
class ErrorHandler @Inject() (
    env: Environment,
    config: Configuration,
    sourceMapper: OptionalSourceMapper,
    router: Provider[Router]
  ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) {

  override def onProdServerError(request: RequestHeader, exception: UsefulException) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }

  override def onForbidden(request: RequestHeader, message: String) = {
    Future.successful(
      Forbidden("You're not allowed to access this resource.")
    )
  }
}

References


HTTP programming

2016/10/17

Scala Play 2.5


新版本的 Scala Play 又有些不同,軟體的更新帶來了新的功能,新的想法跟做法,但就得花一些時間去了解,一個沒有變化的專案除了表示穩定,也可能是維護的人太少,沒辦法推出新功能,一個常常更新的專案,一般都是希望能夠相容舊版,無痛升級,雖然對開發者來說是福音,但也可能因為相容而自我設限,造成整個專案越來越龐大,該怎麼做沒有定論,Play Framework 就曾因為升級到 2 之後,不跟 1 相容,造成專案開發者的流失,不過都已經是過去的歷史了。


書本的內容也沒辦法隨著軟體更新就馬上更新,所幸 Play Framework 官方文件很完整,跟著文件的說明測試一次,應該就能大致掌握新版的功能。


安裝 Play


  1. 安裝 Java SDK
  2. 下載 Activator 1.3.10 minimal
  3. 將 typesafe-activator-1.3.10-minimal 資料夾的 bin 目錄放到環境變數的 PATH 中
  4. 確認可以在 console 執行 activator

產生 project


在 console 產生 project 有兩種方式,command line 或是 web ui。


  • command line

使用 activator 就可以產生 project


activator new playtest1 play-scala

切換到 playtest project folder,就可以用 activator 執行project


cd playtest1
activator

  • activator ui

執行 activator ui,就可以使用 activator 內建的網頁管理介面


activator ui

連結到 http://localhost:8888 就可以看到管理介面



選擇 Lightbend 的 Play Scala Seed 專案 template 及路徑,就可以產生 project




Play 支援了 Eclipse, IDEA, NetBeans, ENSIME 這些 IDE,跟著 Setting up your preferred IDE 的說明,就可以利用 IDE 直接產生一個新的 project。


以下記錄 IDEA 的步驟:
如果要產生一個新的 Play application


  1. New Project
  2. 選擇 Scala -> Activator
  3. 選擇一個適當的 template,最簡單的就是選擇 Lightbend 提供的 Play Scala Seed
  4. Finish

如果要 import 一個已經產生的 Play application


  1. File -> New -> Project from existing sources
  2. 選擇 project folder
  3. 選擇 Import project from external model -> SBT
  4. Finish

activator command line


cd playtest1

# 進入 activator 互動介面
activator

編譯 project


[playtest1] $ compile

執行 project,在這個模式下會啟用 auto-reload 功能,也就是 Play 會自動檢查 project source 有沒有更新,並自動重新編譯


[playtest1] $ run

執行測試程式


[playtest1] $ test

進入 scala console mode,按 Ctrl-D 就可以跳出 console mode


[playtest1] $ console

在 scala console mode,可以直接使用 play 的 classes


scala> views.html.index("Hello")
res0: play.twirl.api.HtmlFormat.Appendable =

<!DOCTYPE html>
<html lang="en">
    <head>

        <title>Welcome to Play</title>

以下這段 code 在文件中提到是可以在 console mode 中啟動 application


import play.api._
val env = Environment(new java.io.File("."), this.getClass.getClassLoader, Mode.Dev)
val context = ApplicationLoader.createContext(env)
val loader = ApplicationLoader(context)
val app = loader.load(context)
Play.start(app)

如果 debug 就要在啟動 activator 加上參數,JDPA 就會運作在 Port 9999 上


activator -jvm-debug 9999

Play console 其實就是一個 sbt console,因此我們可以用以下的方式,透過 sbt 進行 triggered execution


[playtest1] ~ compile
[playtest1] ~ run
[playtest1] ~ test

Play application file layout


app                      → app 原始程式
 └ assets                → 程式資源資料夾
    └ stylesheets        → LESS CSS 原始程式
    └ javascripts        → CoffeeScript 原始程式
 └ controllers           → app controllers
 └ models                → app business layer
 └ views                 → 網頁 templates
build.sbt                → sbt build script
conf                     → 設定檔及其他不需要編譯的資源
 └ application.conf      → app 主設定檔
 └ routes                → 路由跟程式的對應設定
dist                     → 專案發布的資料夾
public                   → 對外開放的資源資料夾
 └ stylesheets           → CSS files
 └ javascripts           → Javascript files
 └ images                → Image files
project                  → sbt 設定檔
 └ build.properties      → sbt 專案的設定檔
 └ plugins.sbt           → sbt plugins
lib                      → 不在 lib 倉庫中受管理的 libraries
logs                     → logs 資料夾
 └ application.log       → 預設的 log file
target                   → 編譯後的檔案資料夾
 └ resolution-cache      → 相關 libraries 的資料
 └ scala-2.11
    └ api                → API 文件
    └ classes            → 編譯後的 class files
    └ routes             → routes 編譯後的結果
    └ twirl              → templates 編譯後的結果
 └ universal             → app 封裝
 └ web                   → 編譯後的網頁資源
test                     → 單元測試的資料夾

Task sample project


以剛剛的 test1 為基礎,修改成一個簡單的 Task 網頁


  • 修改 conf/routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Map static resources from the /public folder to the /assets URL path
GET           /assets/*file        controllers.Assets.at(path="/public", file)

# Home page
GET           /                    controllers.TaskController.index

# Tasks
GET           /tasks               controllers.TaskController.tasks
POST          /tasks               controllers.TaskController.newTask
DELETE        /tasks/:id           controllers.TaskController.deleteTask(id: Int)

  • 新增 models/Task.scala

package models

case class Task(id: Int, name: String)

object Task {

  private var taskList: List[Task] = List()

  def all: List[Task] = {
    // 回傳整個 taskList
    taskList
  }

  def add(taskName: String) = {
    // task id 由 0 開始,如果 list 為 nonEmpty,就使用 list 中最後一個元素的 id
    val lastId: Int = if (taskList.nonEmpty) taskList.last.id else 0
    taskList = taskList ++ List(Task(lastId + 1, taskName))
  }

  def delete(taskId: Int) = {
    // filter 可取得滿足條件的所有元素
    // filterNot 可取得 不滿足 條件的所有元素
    taskList = taskList.filterNot(task => task.id == taskId)
  }
}

  • 新增 app/controllers/TaskController

package controllers

import models.Task
import play.api.mvc._


class TaskController extends Controller {

  def index = Action {
    // 將 request 由 http://localhost:9000/ redirect 到 http://localhost:9000/tasks
    Redirect(routes.TaskController.tasks)
  }

  def tasks = Action {
    // Task.all 取得整個 taskList,送入 views.html.index 產生網頁
    Ok(views.html.index(Task.all))
  }

  def newTask = Action(parse.urlFormEncoded) {
    implicit request =>
      // 取得 request 裡面的 taskName, 新增到 Task 中
      Task.add(request.body.get("taskName").get.head)
      // redirect 到 http://localhost:9000/tasks
      Redirect(routes.TaskController.index)
  }

  def deleteTask(id: Int) = Action {
    // 刪除 task id
    Task.delete(id)
    Ok
  }

}

  • views/index.scala.html

@(tasks: List[Task])

@main("Task Tracker") {

    <h2>Task</h2>
    <div>
        <form action="@routes.TaskController.newTask()" method="post">
            <input type="text" name="taskName" placeholder="Add a new Task" required>
            <input type="submit" value="Add">
        </form>
    </div>
    <div>
        <ul>
        @tasks.map { task =>
            <li>
                @task.name <button onclick="deleteTask ( @task.id) ;">Remove</button>
            </li>
        }
        </ul>
    </div>
    <script>
    function deleteTask ( id ) {
        var req = new XMLHttpRequest ( ) ;
        req.open ( "delete", "/tasks/" + id ) ;
        req.onload = function ( e ) {
            if ( req.status = 200 ) {
                document.location.reload ( true ) ;
            }
        } ;
        req.send ( ) ;

    }
    </script>
}

  • views/main.scala.html

@(title: String)(content: play.twirl.api.Html)
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
    </head>
    <body>
    @content
    </body>
</html>


Projects


Streaming examples using Play 2.5 and Akka Streams http://loicdescotte.github.io/posts/play25-akka-streams/


play-scala-akka example


Example Play Scala application showing WebSocket use with Akka actors https://www.playframework.com/documentation/2.5.x/ScalaWebSockets


Mastering Play Framework for Scala

2016/10/3

json4s: scala json library


scala 世界中,關於處理 json 的 library 很多, 這裡列出了 13 個: Scala standard libray, jawn, sphere-json, argonaut, circe, json4s, spray-json-shapeless, spray-json, lift-json, play-json, rapture,用 google trends 查了一下,json4s查詢的次數還是多一些,json4s 的目標就是提供一個單一的 AST 供其他Scala類庫來使用,以下測試如何使用 json4s。



準備使用 json4s


在 build.sbt 加上 org.json4s 的 library


val json4sVersion = "3.3.0"

libraryDependencies ++= Seq(
  // json4s
  "org.json4s" %% "json4s-native" % json4sVersion,
  "org.json4s" %% "json4s-jackson" % json4sVersion,
  "org.json4s" %% "json4s-ext" % json4sVersion
)

json4s 需要 import 以下的 packages


import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._

主要提供 parse, render 進行 json 轉換,render 需要輸入 JValue 物件,必須利用 JsonDSL 提供 implicit 轉換,parse 很明確,就是 parsing JSON 字串。


測試 json4s


import java.text.SimpleDateFormat

import org.json4s.JsonDSL._
import org.json4s._
import org.json4s.native.JsonMethods._
import org.scalatest.{FunSpec, Matchers}

class TestJson4s extends FunSpec with Matchers {
  implicit val formats = new DefaultFormats {
    //override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  }

  //implicit val formats = Serialization.formats(ShortTypeHints(List()))

  //  implicit val formats = new DefaultFormats {
  //    override val dateFormat: DateFormat = new DateFormat {
  //      override def parse(s: String): Option[Date] = Some(new Date(s.toLong * 1000))
  //      override def format(d: Date): String = (d.getTime/1000).toString
  //    }
  //  }

  case class Student(name: String, age: Int)

  describe("Testing json4s marshalling, unmarshalling") {
    it("parse json and extract to class") {
      // 將原始的 json 字串,轉換成 json4s JObject 物件
      println()
      println(">將 json 轉換成 JObject")
      val tom = parse( """ { "name" : "tom","age":23,"number":[1,2,3,4] } """)
      println(s"tom:${tom.getClass.getName}")
      println(tom)
      val tstring: String = tom.toString
      //JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))
      tstring should be("JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))")

      // 上面的轉換之後 並不是 Scala 的原生物件
      // 要用 Map[String, _] 轉換
      println()
      println(">把 JObject 轉成 Scala 物件")
      for ((k, v) <- tom.values.asInstanceOf[Map[String, _]]) {
        println(s"${k} -> ${v}:${v.getClass}")
      }
      //      name -> tom:class java.lang.String
      //      age -> 23:class scala.math.BigInt
      //      number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

      // 可以用  \  取得某個欄位的值
      println()
      println(">可以用  \\  取得某個欄位的值")
      println(s"name -> ${(tom \ "name").values}")
      println(s"age -> ${(tom \ "age").values}")
      println(s"number -> ${(tom \ "number").values}")

      //      name -> tom
      //      age -> 23
      //      number -> List(1, 2, 3, 4)

      val name = (tom \ "name").values
      name should be("tom")

      // 可直接將 tom 轉換成 Student

      val student: Student = tom.extract[Student]
      println()
      println(">可直接將 tom 轉換成 Student")
      println(student)
      // Student(tom,23)

      student.name should be("tom")

      ///// 列印 json

      println()
      println("> 列印 json 時要呼叫 compact")
      println(compact(render(Map("name" -> "tom", "age" -> "23"))))
      //结果:{"name":"tom","age":"23"}

      println()
      println("> 只支援 Tuple2[String, A] 這種格式的 tuple")
      val tuple = ("name", "tom")
      println(compact(render(tuple)))
      //结果:{"name":"tom"}


      println()
      println("> 可以用 ~ 將兩個 tuple 連接在一起")
      val tuple2 = ("name", "tom") ~ ("age", 23)
      println(tuple2)
      println(compact(render(tuple2)))
      //结果:
      //JObject(List((name,JString(tom)), (age,JInt(23))))
      //{"name":"tom","age":23}


      println()
      println("> 當 value 為 None 時,就不會序列化")
      val tuple3 = ("name", "tom") ~("age", None: Option[Int])
      println(tuple3)
      //结果:
      //JObject(List((name,JString(tom)), (age,JNothing)))
      //{"name":"tom"}


      println()
      println("> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀")
      case class Winner(id: Long, numbers: List[Int])
      case class Lotto(id: Long, winningNumbers: List[Int], winners: List[Winner], drawDate: Option[java.util.Date])

      val winners = List(Winner(23, List(2, 45, 34, 23, 3, 5)), Winner(54, List(52, 3, 12, 11, 18, 22)))
      val lotto = Lotto(5, List(2, 45, 34, 23, 7, 5, 3), winners, Option(new java.util.Date()))
      val customDateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val json =
        ("lotto" ->
          ("lotto-id" -> lotto.id) ~
            ("winning-numbers" -> lotto.winningNumbers) ~
            //("draw-date" -> lotto.drawDate.map(_.toString)) ~
            ("draw-date" -> lotto.drawDate.map( customDateFormatter.format(_) )) ~
            ("winners" ->
              lotto.winners.map { w =>
                (("winner-id" -> w.id) ~
                  ("numbers" -> w.numbers))
              }))

      println(compact(render(json)))
      // {"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 12:03:36","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}

      println(pretty(render(json)))
      //
      //      {
      //        "lotto":{
      //          "lotto-id":5,
      //          "winning-numbers":[2,45,34,23,7,5,3],
      //          "draw-date":"2016-05-19 12:03:36",
      //          "winners":[{
      //          "winner-id":23,
      //          "numbers":[2,45,34,23,3,5]
      //        },{
      //          "winner-id":54,
      //          "numbers":[52,3,12,11,18,22]
      //        }]
      //        }
      //      }


      //
      println()
      println("> 用 merge 合併兩個 json")
      val t1 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaoerban"]
    }
        """)
      val t2 = parse(
        """
    {"name":"tom",
       "age":23,
       "class":["xiaosanban"]
    }
        """)
      val t3 = t1 merge t2
      println(pretty(render(t3)))
      //      {
      //        "name":"tom",
      //        "age":23,
      //        "class":["xiaoerban","xiaosanban"]
      //      }

      println()
      println("> 用 Diff 檢查兩個 JObject 的差異")
      val Diff(changed, added, deleted) = t3 diff t1
      println("changed", changed)
      println("added", added)
      println("deleted", deleted)
      //      (changed,JNothing)
      //      (added,JNothing)
      //      (deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))


      println()
      println("> 可以將 xml 轉換成 json")
      import org.json4s.Xml.toJson
      val xml =
        <users>
          <user>
            <id>1</id>
            <name>Harry</name>
          </user>
          <user>
            <id>2</id>
            <name>David</name>
          </user>
        </users>

      val json2 = toJson(xml)
      println(pretty(render(json2)))
    }

  }
}

以下為執行的結果


>將 json 轉換成 JObject
tom:org.json4s.JsonAST$JObject
JObject(List((name,JString(tom)), (age,JInt(23)), (number,JArray(List(JInt(1), JInt(2), JInt(3), JInt(4))))))

>把 JObject 轉成 Scala 物件
name -> tom:class java.lang.String
age -> 23:class scala.math.BigInt
number -> List(1, 2, 3, 4):class scala.collection.immutable.$colon$colon

>可以用  \  取得某個欄位的值
name -> tom
age -> 23
number -> List(1, 2, 3, 4)

>可直接將 tom 轉換成 Student
Student(tom,23)

> 列印 json 時要呼叫 compact
{"name":"tom","age":"23"}

> 只支援 Tuple2[String, A] 這種格式的 tuple
{"name":"tom"}

> 可以用 ~ 將兩個 tuple 連接在一起
JObject(List((name,JString(tom)), (age,JInt(23))))
{"name":"tom","age":23}

> 當 value 為 None 時,就不會序列化
JObject(List((name,JString(tom)), (age,JNothing)))

> 處理 date 必須要加上 implicit val formats, 用 pretty 可以讓列印結果容易閱讀
{"lotto":{"lotto-id":5,"winning-numbers":[2,45,34,23,7,5,3],"draw-date":"2016-05-19 13:57:54","winners":[{"winner-id":23,"numbers":[2,45,34,23,3,5]},{"winner-id":54,"numbers":[52,3,12,11,18,22]}]}}
{
  "lotto":{
    "lotto-id":5,
    "winning-numbers":[2,45,34,23,7,5,3],
    "draw-date":"2016-05-19 13:57:54",
    "winners":[{
      "winner-id":23,
      "numbers":[2,45,34,23,3,5]
    },{
      "winner-id":54,
      "numbers":[52,3,12,11,18,22]
    }]
  }
}

> 用 merge 合併兩個 json
{
  "name":"tom",
  "age":23,
  "class":["xiaoerban","xiaosanban"]
}

> 用 Diff 檢查兩個 JObject 的差異
(changed,JNothing)
(added,JNothing)
(deleted,JObject(List((class,JArray(List(JString(xiaosanban)))))))

> 可以將 xml 轉換成 json
{
  "users":{
    "user":[{
      "id":"1",
      "name":"Harry"
    },{
      "id":"2",
      "name":"David"
    }]
  }
}
[info] TestJson4s:
[info] Testing json4s marshalling, unmarshalling
[info] - parse json and extract to class
[info] Run completed in 371 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Reference


學習和使用Scala的Json解析類庫-JSON4S