msawady’s learning memo

ただのJavaエンジニアが、学んだことをログっていくところ

【Scala】【Play Framework】ブラウザからのサーバー処理の呼び出し、ファイルの書き込み

ブラウザからのステータス更新処理を実装しました

  • Play Frameworkを用いた Todo 管理アプリの実装を進め、サーバーの更新処理を実装しました
  • あちこち触ることになり記事の内容が若干散漫ですが、ご容赦ください
  • やったこと
    • 前回作成した右クリックメニューにjQueryでのサーバー呼び出しを追加
    • 更新メソッドのルーティングを追加
    • Repository クラス分離、ファイルの書き込み処理を実装
    • Enum の書きっぷりを変更

jQuery でのサーバー呼び出し

$.getを利用する、以下のメソッドを追加します。

    function reqUpdateTodoStatus(status){
        $.get('/todo/update',
            {
                ids: getSelectedIds,
                status: status
            },
            function(){location.reload()}
        )
    }

第1引数はリクエスト先のURL, 第2引数はリクエストのパラメータ、第3引数はコールバック処理です。 コールバック処理で画面の更新(location.reload())を行うことで、即座に画面に変更内容が反映されます。

更新メソッドのルーティングを追加

config/routes に以下のrouteを追加

引数の型がStringだったら、型は省略できます。

GET     /todo/update              controllers.TodoController.update(ids, status)

Controller

  def update(ids: String, status: String) = Action {
    todoManager.update(ids.split(",").map(_.toInt).toList, status)
    Ok(todoView(todoManager.list))
  }

困っているところ

自分としては、Int、百歩譲ってStringの配列で引数を回したかったんですが、どうも"10001, 10002"のようなカンマ区切りのStringで来てしまいます。
ココらへんの記事を参考にして色々とやってみたのですが上手く行かず,,, リクエストの投げ方が悪いのか、受け取り方が悪いのか、もうちょっと試行錯誤したいと思います。

Repository クラス分離、ファイルの書き込み処理を実装

Service クラスと Repository クラスを分離し、Repository クラスにファイルの書き込み処理を実装しました。

Service

package services

import javax.inject.Inject

import com.google.inject.Singleton
import services.domain.TodoStatus
import services.repository.TodoRepository

/**
  * service class for todo management
  */
@Singleton
class TodoManager @Inject()(todoRepository: TodoRepository) {

  /**
    * get Todo List
    *
    * @return todo-list order by id
    */
  def list = todoRepository.getTodoList()

  /**
    * update todo status
    *
    * @param ids    id list of target TODO
    * @param status TodoStatus change to
    */
  def update(ids: List[Int], status: String): Unit = todoRepository.updateTodoList(ids, TodoStatus.withName(status))

}

かなりスッキリしました。基本的に、Inject している repository のメソッドをコールするだけになっています。

Repository

package services.repository

import java.io.PrintWriter

import com.google.inject.Singleton
import play.Environment
import services.domain.{Todo, TodoStatus}

import scala.collection.mutable
import scala.io.Source

/**
  * repository class for todo management
  *
  */
@Singleton
class TodoRepository() {

  private val filePath = "data/TODO.txt"
  private val encoding = "UTF-8"

  def getTodoList(): List[Todo] = {
    sortById(readTodoFile())
  }

  def updateTodoList(ids: List[Int], status: TodoStatus) = {
    val modified = readTodoFile()
    ids.foreach(id => {
      val todo: Todo = modified.get(id).get
      todo.update(status)
      modified.put(todo.getId, todo)
    })
    writeTodoFile(sortById(modified))
  }

  private def readTodoFile(): mutable.Map[Int, Todo] = {
    val map: mutable.Map[Int, Todo] = new mutable.HashMap[Int, Todo]
    val s = Source.fromFile(Environment.simple().getFile(filePath), encoding)
    val strList: List[String] = try s.getLines.toList finally s.close()
    strList.foreach(str => {
      val data: Array[String] = str split "\t"
      val todo: Todo = new Todo(data(0).toInt, TodoStatus.withName(data(1)), data(2))
      map.put(todo.getId, todo)
    })
    return map
  }

  private def writeTodoFile(list: List[Todo]) = {
    val pw = new PrintWriter(filePath, encoding)
    try list.foreach(todo => pw.write(todo.toTsvString + "\n")) finally pw.close()
  }

  private def sortById(map: mutable.Map[Int, Todo]): List[Todo] = {
    map.values.toList.sortBy(_.getId)
  }
}
  • ファイルの書き込みライブラリは scala.io に用意されていないのでjava.io.PrintWriter を利用します。(それにしても try~catchがゆるく書けて良いですね)
private def writeTodoFile(list: List[Todo]) = {
    val pw = new PrintWriter(filePath, encoding)
    try list.foreach(todo => pw.write(todo.toTsvString + "\n")) finally pw.close()
  }
  • workspace の中のファイルを読み込むために、Environment.getFile()を利用しています。 Environment.simple()でルートからの相対パスを指定してファイルを選択できます。
val s = Source.fromFile(Environment.simple().getFile("data/TODO.txt"), encoding)

Enum の書きっぷりを変更

Enumeration クラスを使うよりは sealed traitを使う方が柔軟に行けそうだったものの、JavaでいうvalueOfが無いのは辛かったので object の中に case objectを列挙し、withNameを実装しました。

package services.domain

sealed trait TodoStatus

/**
  * todo status
  */
object TodoStatus {

  case object UNDONE extends TodoStatus

  case object DOING extends TodoStatus

  case object DONE extends TodoStatus

  def withName(s: String) = s.toLowerCase match {
    case "undone" => UNDONE
    case "doing" => DOING
    case "done" => DONE
  }
}

動作確認

“UNDONE"な2つを"DOING"に。 f:id:msawady:20170820221421p:plain f:id:msawady:20170820221525p:plain

感想と、次にやること

  • パラメータの型にはハマったものの、サクッとブラウザからサーバーへのメソッドが通ったのは嬉しかったです。
    • パラメータの型については、もう少しきれいなやり方がないか頑張りたい。
    • いっそ、ReqMessageクラスを作って、Jsonで展開するほうが楽かもしれない。
  • Enumだったり、ファイルの書き込みだったり、サーバー側のScalaの知識もついたなぁと。
  • updateが出来たので、add/delete も実装したい
  • データの管理をDBにしたい、Scala書いてる方が楽しいけど、ちゃんと勉強しなければ。

以上です。今週は色々と触れて勉強になりました。