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/04

Android Studio Import Eclipse Android Project


Android 在 2015 / 6 / 26 發表的 An update on Eclipse Android Developer Tools 文章中有提到,將在 2015 年底停止更新 Eclipse 的 ADT plug-in。 但以往大部分的 Android 專案都是使用 eclipse 這個 IDE,該如何把舊的專案從 eclipse 中移植到 android studio 呢?以下就來簡單介紹一下流程

步驟一:將專案從 eclipse 匯出

  1. 將專案從 eclipse 匯出

  2. 選擇 Generate Gradle build files

  3. Import Instead 內容可以看看,下一步

  4. 選擇要匯出的專案

  5. Finish

  6. 打開專案目錄可以看到多了 gradle 相關檔案

步驟二:將專案匯入 Android Studio

  1. Import Project

  2. 切到你的專案跟目錄並開啟專案

  3. 設定 android sdk
    1. Project Structure

    2. 設定 Compile SDK / Min SDK / Target SDK



    3. Import lib ( 如果有需要,eclipse 有 link lib 的話 )



    ※ 在 libs 目錄下的 jar 不需要另外 import,因為 build.gradle 文件中已經定義好
    
    dependencies {
        compile fileTree(include: '*.jar', dir: 'libs')
    }
    

Error

  1. Android Gradle plug-in version unsupport


    sol:直接點選 Fix plugin version and sync project

References

  1. An update on Eclipse Android Developer Tools
  2. ADT Plugin Release Notes
  3. Migrate to Android Studio from Eclipse

2016/10/03

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