Play 2.5でのCRUD処理およびメール送信（activatorによる開発環境の構築からketerによるデプロイまで）

# はじめに

CRUD処理およびメール送信を行うWebアプリケーションをPlay 2.5（Scala）で作成してみます。

Kona Linux UEも新しいバージョンになったことですし、今回はノートPCにKona Linux UE 2.0（Ubuntu 16.04 LTS）をインストールしただけの真っ新の状態からスタートし、最終的にAmazon EC2‎（Ubuntu 14.04 LTS）にデプロイするところまでやってみたいと思います。

なお、今回のコードは下記で公開しています。
https://github.com/imahiro-t/play-app.git

他の関数型言語のWebフレームワークでの実装はこちら
[Yesod 1.4でのCRUD処理およびメール送信（stackによる開発環境の構築からketerによるデプロイまで）](http://qiita.com/erin/items/55e0109e4be6b3257bef)
[PhoenixでのCRUD処理およびメール送信（mixによる開発環境の構築からExrmによるデプロイまで）](http://qiita.com/erin/items/49655ebb75695dab6907)

# 環境

Kona Linux UE 2.0（Ubuntu 16.04 LTS）（開発環境）
Amazon EC2‎（Ubuntu 14.04 LTS）（実行環境）

# 作成するWebアプリケーションについて

携帯電話のカレンダー機能には、予定を登録すると、その開始日時の○分前にアラームをならす機能があるかと思います。私のガラホ（AQUOS K SHF32）ではアラームは呼出音の設定と連動しているようで、普段呼出音を切ってバイブのみで使用しているので、当然アラームは鳴らず、それだけならまだしも、バイブすらしてくれないという使い勝手の悪さ。

概要は、サイトにアクセスし、件名、日付、時間、通知（○分前）、説明を登録すると、サーバ側でその通知時刻になった時点で、メールを配信してくれるというWebアプリケーションです。

![notify.png](https://qiita-image-store.s3.amazonaws.com/0/94455/97bd131b-45a5-e808-32e5-ac9e81c04052.png)
![notification.png](https://qiita-image-store.s3.amazonaws.com/0/94455/e9508556-97a2-1636-11d1-f88f692f3a43.png)


# JDK、activatorのインストール

まずは開発環境にJDKをインストールします。Play 2.5はJava 1.8が必要なのでご注意を。

```
$ sudo apt install openjdk-8-jdk
```

次にPlayの開発環境を構築するためにactivatorをインストールします。

[公式サイト](https://www.playframework.com/download)を参考にインストールしてください。

# プロジェクトの作成

```
$ activator new notify play-scala
$ cd notify
```

`activator new [プロジェクト名] [テンプレート]`はテンプレートを作成するコマンドで、今回は「notify」というプロジェクト名で、Scalaを使ったPlayアプリケーションのテンプレートを作成します。

notifyディレクトリ内にテンプレートが作成されるので、後はnotifyディレクトリ内でファイルの追加、編集を行っていくことになります。

# ModelとDAOの作成

データベースとのやり取りは「Slick」というFRMを使います。Slickは「Scala Language-Integrated Connection Kit」の略でPlayと同じLightbend社が提供しているORMならぬFRM「Functional Relational Mapping」です。

Slickを使用するため、Play2.5の[オフィシャルドキュメント](https://www.playframework.com/documentation/2.5.x/PlaySlick)を参考に下記の2点を設定します。

１．`build.sbt`に下記を追記

```scala:build.sbt
libraryDependencies += "com.typesafe.play" %% "play-slick" % "2.0.0"
libraryDependencies += "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0"
libraryDependencies += "com.h2database" % "h2" % "1.4.191"
```
※起動時にbindingのエラーが出るので「jdbc」の依存の記述はコメントアウトしてください。

２．`conf/application.conf`に下記を追記

```scala:conf/application.conf
slick.dbs {
  default.driver = "slick.driver.H2Driver$"
  default.db.driver = "org.h2.Driver"
  default.db.url = "jdbc:h2:./notify"
  default.db.url = ${?H2_DATABASE}
}
```

そして、`app/models/Notification.scala`に下記の実装を。

```scala:app/models/Notification.scala
package models

case class Notification (
  id: Option[Long],
  subject: String,
  actionDate: java.sql.Date,
  actionTime: java.sql.Time,
  notifyBefore: Int,
  summary: String,
  notificationDate: java.util.Date,
  sent: Boolean
)
```

初回起動時にテーブルを自動生成してもらうため、`conf/evolutions/default/1.sql`を用意します。

```conf/evolutions/default/1.sql
# Notification

# --- !Ups

CREATE TABLE "notification" (
    "id" bigint(20) NOT NULL AUTO_INCREMENT,
    "subject" varchar(255),
    "action_date" date,
    "action_time" time,
    "notify_before" integer,
    "summary" varchar(255),
    "notification_date" timestamp,
    "sent" boolean,
    PRIMARY KEY ("id")
);

# --- !Downs

DROP TABLE "notification";
```

続いてCRUD処理を行うDAOを`app/dao/NotificationDAO.scala`に。

```scala:app/dao/NotificationDAO.scala
package dao

import scala.concurrent.Future
import java.util.Date
import java.util.Calendar
import java.sql.Time
import java.sql.Timestamp
import javax.inject.Inject
import javax.inject.Singleton
import models.Notification
import play.api.db.slick.DatabaseConfigProvider
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import slick.driver.JdbcProfile

@Singleton
class NotificationDAO @Inject()(dbConfigProvider: DatabaseConfigProvider) {

  val dbConfig = dbConfigProvider.get[JdbcProfile]

  import dbConfig.driver.api._

  implicit def javaDateTimestampMapper = MappedColumnType.base[Date, Timestamp](
    dt => new Timestamp(dt.getTime),
    ts => new Date(ts.getTime)
  )

  private class NotificationTable(tag: Tag) extends Table[Notification](tag, "notification") {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def subject = column[String]("subject")
    def actionDate = column[java.sql.Date]("action_date")
    def actionTime = column[java.sql.Time]("action_time")
    def notifyBefore = column[Int]("notify_before")
    def summary = column[String]("summary")
    def notificationDate = column[Date]("notification_date")
    def sent = column[Boolean]("sent", O.Default(false))
    def * = (id.?, subject, actionDate, actionTime, notifyBefore, summary, notificationDate, sent) <> ((Notification.apply _).tupled, Notification.unapply)
  }

  private val notifications = TableQuery[NotificationTable]

  def getNotificationsSent(): Future[List[Notification]] =
    dbConfig.db.run(notifications.filter(n => (n.sent === true)).result).map(_.toList)

  def getNotificationsSentNotYet(): Future[List[Notification]] =
    dbConfig.db.run(notifications.filter(n => (n.sent === false)).result).map(_.toList)

  def byId(id: Long): Future[Option[Notification]] = dbConfig.db.run(notifications.filter(_.id === id).result.headOption)

  def create(notification: Notification): Future[Int] = {
    val n = notification.copy(
      notificationDate = calcNotificationDate(notification.actionDate, notification.actionTime, notification.notifyBefore),
      sent = false
    )
    dbConfig.db.run(notifications += n)
  }

  def update(notification: Notification): Future[Int] = {
    dbConfig.db.run(notifications.filter(_.id === notification.id).map(
      n => (
        n.subject,
        n.actionDate,
        n.actionTime,
        n.notifyBefore,
        n.summary,
        n.notificationDate,
        n.sent
        )
    ).update(
      notification.subject,
      notification.actionDate,
      notification.actionTime,
      notification.notifyBefore,
      notification.summary,
      calcNotificationDate(notification.actionDate, notification.actionTime, notification.notifyBefore),
      false
     )
    )
  }

  def getSendList(): Future[List[Notification]] =
    dbConfig.db.run(notifications.filter(n => (n.sent === false) && (n.notificationDate < new Date)).result).map(_.toList)

  def sent(id: Long): Future[Int] =
    dbConfig.db.run(notifications.filter(_.id === id).map(n => (n.sent)).update(true))

  def delete(id: Long): Future[Int] =
    dbConfig.db.run(notifications.filter(_.id === id).delete)

  private def calcNotificationDate(actionDate: java.sql.Date, actionTime: Time, notifyBefore: Int): Date = {
    val cl = Calendar.getInstance
    val clTmp = Calendar.getInstance
    cl.setTimeInMillis(actionDate.getTime())
    clTmp.setTimeInMillis(actionTime.getTime())
    cl.set(Calendar.HOUR_OF_DAY, clTmp.get(Calendar.HOUR_OF_DAY))
    cl.set(Calendar.MINUTE, clTmp.get(Calendar.MINUTE))
    cl.add(Calendar.MINUTE, -notifyBefore)
    cl.getTime
  }

}
```

概要としては、

1. Modelである`Notification`を型に持った`Table`をスーパークラスにして`NotificationTable`を作成。
2. それを元に`TableQuery`を作成。
3. Slickが`java.util.Date`に対応していなため、対応している`java.sql.Timestamp`との相互変換のための`implicit def javaDateTimestampMapper`を作成。
4. 後はCRUDの実装

といった流れになります。

Slickでは、FRMの思想として、TableQueryをScalaのコレクション感覚で操作することになるため、queryといった変数名ではなくnotificationsのようにリストっぽい変数名にすることがポイントです。

# Controller（ハンドラー）の作成

Playでは後述するルーティングの設定により、URLパターンから該当するハンドラーが呼び出されることになります。今回は２つのハンドラーを作成します。

まずはハンドラーで共通して利用するフォームとModelとのマッピングを行うオブジェクトを作成します。

`app/controllers/Forms.scala`に下記の実装を。

```scala:app/controllers/Forms.scala
package controllers

import models.Notification

import java.sql.Time
import java.util.Date

import play.api.data.Form
import play.api.data.Forms._

case class NotificationForm(command: Option[String], notification: Notification)

object Forms {
  def notificationForm = Form(
    mapping(
      "command" -> optional(text),
      "db" ->
        mapping(
          "id" -> optional(longNumber),
          "subject" -> nonEmptyText,
          "actionDate" -> sqlDate("yyyy-MM-dd"),
          "actionTime" -> date("HH:mm"),
          "notifyBefore" -> number,
          "summary" -> text
        )
        ((id, subject, actionDate, actionTime, notifyBefore, summary)
        => new Notification(id, subject, actionDate, actionTime.convert, notifyBefore, summary, new Date, false))
        ((n: Notification) => Some((n.id, n.subject, n.actionDate, n.actionTime.convert, n.notifyBefore, n.summary)))
    )(NotificationForm.apply)(NotificationForm.unapply)
  )

  implicit class DateToTimeConversion(date: Date) {
    def convert: Time = {
      if (date != null) new Time(date.getTime())
      else new Time(System.currentTimeMillis())
    }
  }

  implicit class TimeToDateConversion(time: Time) {
    def convert: Date = {
      if (time != null) new Date(time.getTime())
      else new Date(System.currentTimeMillis())
    }
  }

}
```

今回、フォームに「command」というModelとは直接関係のない隠しフィールドを設けてハンドラー内の処理を振り分けるため、ネストしたマッピングを使用しています。また、「notificationDate」や「sent」のようにModelには存在するけどフォームには存在しない項目があるため、Model側のapplyメソッドとunapplyメソッドを使用せずに別途用意しています。

次にそれぞれのハンドラーを記述します。

１つ目は（一覧表示付きの）登録画面がGETとPOSTで呼び出されたときのハンドラーです。

`app/controllers/NotifyC.scala`に下記の実装を。

```scala:app/controllers/NotifyC.scala
package controllers

import javax.inject.{Inject, Singleton}

import dao.NotificationDAO
import play.api.i18n.{I18nSupport, MessagesApi}
import play.api.mvc.{Action, Controller}
import play.api.libs.concurrent.Execution.Implicits.defaultContext

@Singleton
class NotifyC @Inject() (val messagesApi: MessagesApi, dao: NotificationDAO) extends Controller with I18nSupport {

  def getNotify = Action.async { implicit request =>
    dao.getNotificationsSent().flatMap(notificationsSent =>
      dao.getNotificationsSentNotYet().map(notificationsSentNotYet =>
        Ok(views.html.notifications(request.flash.get("message").getOrElse(""), notificationsSent, notificationsSentNotYet, Forms.notificationForm))
      )
    )
  }

  def postNotify = Action.async { implicit request =>
    Forms.notificationForm.bindFromRequest.fold(
      formWithErrors => {
        dao.getNotificationsSent().flatMap(notificationsSent =>
          dao.getNotificationsSentNotYet().map(notificationsSentNotYet =>
            BadRequest(views.html.notifications("", notificationsSent, notificationsSentNotYet, formWithErrors))
          )
        )
      },
      formValue => {
        dao.create(formValue.notification).map(_ =>
          Redirect("/notify").flashing("message" -> ("[" + formValue.notification.subject + "]" + "を作成しました。"))
        )
      }
    )
  }

}
```

GETで呼び出されたときは、getNotifyが呼ばれ、データベースから通知予定のレコードと、通知済みのレコードを取得し、登録画面を表示しています。

POSTで呼び出されたときは、postNotifyが呼ばれ、フォームの入力内容を元にデータベースにレコードを追加しています。その後、登録画面にリダイレクトしています。

２つ目は編集画面がGETとPOSTで呼び出されたときのハンドラーです。

`app/controllers/NotificationC.scala`に下記の実装を。

```scala:app/controllers/NotificationC.scala
package controllers

import javax.inject.{Inject, Singleton}

import dao.NotificationDAO
import play.api.i18n.{I18nSupport, MessagesApi}
import play.api.mvc.{Action, Controller}
import play.api.libs.concurrent.Execution.Implicits.defaultContext

import scala.concurrent.Future

@Singleton
class NotificationC @Inject() (val messagesApi: MessagesApi, dao: NotificationDAO) extends Controller with I18nSupport {

  def getNotification(id: Long) = Action.async {
    dao.byId(id).map(
      option => option match {
        case Some(notification) => Ok(views.html.notification("", Forms.notificationForm.fill(NotificationForm(None, notification))))
        case None => Redirect("/notify")
      }
    )
  }

  def postNotification(id: Long) = Action.async { implicit request =>
    Forms.notificationForm.bindFromRequest.fold(
      formWithErrors => {
        Future(BadRequest(views.html.notification("ERROR", formWithErrors)))
      },
      formValue => {
        formValue.command match {
          case Some("update") => dao.update(formValue.notification).map(_ =>
            Redirect("/notify").flashing("message" -> ("[" + formValue.notification.subject + "]" + "を更新しました。"))
          )
          case Some("delete") => dao.delete(formValue.notification.id.getOrElse(0)).map(_ =>
            Redirect("/notify").flashing("message" -> ("[" + formValue.notification.subject + "]" + "を削除しました。"))
          )
          case _ => Future(Redirect("/notify"))
        }
      }
    )
  }

}
```

GETで呼び出されたときは、getNotificationが呼ばれ、データベースからidに該当するレコードを取得し、編集画面を表示しています。

POSTで呼び出されたときは、postNotificationが呼ばれ、フォームからの"command"パラメータにより、更新処理か削除処理を行い、登録画面にリダイレクトしています。

CRUDの思想に従って、GETは表示のみを、POSTで作成、更新、削除を行っています。編集画面からのPOSTは更新と削除があるため、「command」を隠しフィールドとして持ち、処理を振り分けています。

# Viewの作成

まずは共通画面を`app/views/main.scala.html`に。（bootstrapの記述を入れただけです。）

```scala:app/views/main.scala.html
@(title: String)(content: Html)

<!DOCTYPE html>

<html lang="jp">
<head>
    <title>@title</title>

    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <!-- Optional theme -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>

    <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
    <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
    <meta name="viewport" content="width=device-width">
</head>
<body>
@content
</body>
</html>
```

次に登録画面を`app/views/notifications.scala.html`に。

```scala:app/views/notifications.scala.html
@(message: String, notificationsSent: List[models.Notification], notificationsSentNotYet: List[models.Notification]
  , myForm: Form[NotificationForm])(implicit messages: Messages)

@main("登録画面") {
<div class="container">
<h1>登録画面</h1>
<p>@message</p>
<ul>
    @for(notification <- notificationsSentNotYet) {
    <li>
        <a href="/notify/@notification.id"> @notification.subject </a>
    </li>
    }
</ul>
<hr>
<p>
@helper.form(action=routes.NotifyC.postNotify) {
    @(helper.inputText (
        field = myForm("db.subject"),
        '_label -> "件名"
    ))
    @(helper.inputDate(
        field = myForm("db.actionDate"),
        '_label -> "日付"
    ))
    @helper.input(myForm("db.actionTime"), '_label -> "時間") { (id, name, value, args) =>
        <input type="time" name="@name" id="@id" value="@value" @toHtmlArgs(args)>
    }
    @(helper.select (
        field = myForm("db.notifyBefore"),
        options = Seq("5" -> "5分前", "10" -> "10分前", "15" -> "15分前", "20" -> "20分前"
        , "30" -> "30分前", "45" -> "45分前", "60" -> "1時間前"),
        args = 'size -> 1,
        '_label -> "通知"
    ))
    @(helper.textarea (
        field = myForm("db.summary"),
        '_label -> "説明"
    ))
    <button>登録</button>
}
</p>
<hr>
<ul>
    @for(notification <- notificationsSent) {
    <li>
        <a href="/notify/@notification.id"> @notification.subject </a>
    </li>
    }
</ul>
</div>
}
```

そして編集画面を`app/views/notification.scala.html`に。

```scala:app/views/notification.scala.html
@(message: String, myForm: Form[NotificationForm])(implicit messages: Messages)

@main("編集画面") {
<div class="container">
<h1>編集画面</h1>
<p>@message</p>
<p>
@helper.form(action=routes.NotificationC.postNotification(myForm("db.id").value.getOrElse("0").toLong)) {
    <input type="hidden" name="command" value="update">
    <input type="hidden" name="db.id" value="@myForm("db.id").value">
    @(helper.inputText (
        field = myForm("db.subject"),
        '_label -> "件名"
    ))
    @(helper.inputDate(
        field = myForm("db.actionDate"),
        '_label -> "日付"
    ))
    @helper.input(myForm("db.actionTime"), '_label -> "時間") { (id, name, value, args) =>
        <input type="time" name="@name" id="@id" value="@value" @toHtmlArgs(args)>
    }
    @(helper.select (
        field = myForm("db.notifyBefore"),
        options = Seq("5" -> "5分前", "10" -> "10分前", "15" -> "15分前", "20" -> "20分前"
        , "30" -> "30分前", "45" -> "45分前", "60" -> "1時間前"),
        args = 'size -> 1,
        '_label -> "通知"
    ))
    @(helper.textarea (
        field = myForm("db.summary"),
        '_label -> "説明"
    ))
    <div>
        <input type="submit" value="更新">
        <input type="button" value="削除" onclick="if (confirm('削除しますか?')) {this.form.command.value = 'delete';this.form.submit();}">
    </div>
}
</p>
<hr>
<p><a href="/notify">HOME</a></p>
</div>
}
```

削除ボタンが押された際に、隠しフィールドの"comannd"を"delete"にしてPOSTしています。

最後にCSSです。

```css:public/stylesheets/main.css
h1 {
    background-color: #6666aa;
    color: #ccccff;
}
input#subject {
    width: 280px;
}
textarea#summary {
    width: 280px;
}
dl {
  margin-bottom: 5px !important;
}
dt,dd {
  line-height: 1 !important;
}
ul {
  padding-left: 20px !important;
}
```

# タイマーの作成

通知時刻にメール送信を行うため、1分間隔でデータベースを検索し、該当するレコードがある場合に、データベースの更新、メールの送信を行います。今回、メールの送信はActorが担当することになります。

メール送信はPlayのプラグインで提供されているplay-mailerを使用するので、まずは下記設定を追加します。

１．`build.sbt`に下記を追記

```scala:build.sbt
libraryDependencies += "com.typesafe.play" %% "play-mailer" % "5.0.0-M1"
```

２．`conf/application.conf`に下記を追記

```scala:conf/application.conf
play.mailer {
  host = "localhost"
  host = ${?MAIL_HOST}
  port = 25
  port = ${?MAIL_PORT}
}
notify.mail {
  from = "NOTIFY SENDER <from@xxxx.xx>"
  from = ${?MAIL_FROM}
  to = "to@xxxx.xx"
  to = ${?MAIL_TO}
}
```

メール送信を行うActorは下記のようになります。

```scala:app/actors/NotifyActor.scala
package actors

import dao.NotificationDAO
import akka.actor._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.mailer.{MailerClient, Email}
import com.typesafe.config.ConfigFactory

import play.api.Logger

object NotifyActor {
  def props(dao: NotificationDAO, mc: MailerClient) = Props(classOf[NotifyActor], dao, mc)
}

class NotifyActor(dao: NotificationDAO, mc: MailerClient) extends Actor {

  val config = ConfigFactory.load()
  val mailFrom = config.getString("notify.mail.from")
  val mailTo = config.getString("notify.mail.to")

  def receive = {
    case "NOTIFY" =>
      Logger.debug((new java.util.Date).toString)
      Logger.debug("Message(NOTIFY) received")
      dao.getSendList().map { notifications =>
        Logger.debug("notifications.size -> " + notifications.size)
        for (notification <- notifications) {
          Logger.debug("notification.id -> " + notification.id)
          val email = Email(
            notification.subject,
            mailFrom,
            Seq(mailTo),
            bodyText = Some("[件名]" +
              "\n" +
              notification.subject +
              "\n" +
              "[日付]" +
              "\n" +
              notification.actionDate +
              "\n" +
              "[時間]" +
              "\n" +
              notification.actionTime +
              "\n" +
              "[通知]" +
              "\n" +
              notification.notificationDate +
              "\n" +
              "[説明]" +
              "\n" +
              notification.summary)
          )
          mc.send(email)
          Logger.debug("Mail sent")
          dao.sent(notification.id.getOrElse(0))
          Logger.debug("Model(notification) updated")
        }
      }
  }
}
```

Actorにメッセージを送るタイマーは下記のようになります。

```scala:app/services/NotifyTimer.scala
package services

import dao.NotificationDAO
import actors.NotifyActor

import javax.inject.Inject
import javax.inject.Singleton

import java.util.Calendar

import akka.actor._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

import play.api.libs.mailer._

import play.api.Logger

@Singleton
class NotifyTimer @Inject() (dao: NotificationDAO, system: ActorSystem, mc: MailerClient) {

  Logger.info("start Actaor")
  val notifyActor = system.actorOf(NotifyActor.props(dao, mc), "notify-actor")
  var cl = Calendar.getInstance
  cl.set(Calendar.SECOND, 0)
  cl.set(Calendar.MILLISECOND, 0)
  cl.add(Calendar.MINUTE, 1)
  system.scheduler.schedule(
    (cl.getTimeInMillis - System.currentTimeMillis).milliseconds,
    1.minutes,
    notifyActor,
    "NOTIFY"
  )
  Logger.info("Actor has started")

}
```

アプリケーション起動時にタイマーをスタートするためDIの登録を行います。

```scala:app/Module.scala
import com.google.inject.AbstractModule

import services.NotifyTimer

class Module extends AbstractModule {

  override def configure() = {
    bind(classOf[NotifyTimer]).asEagerSingleton()
  }

}
```

# ルーティング

ルーティング（URLパターンからハンドラーへの対応付け）を設定します。

```scala:conf/routes
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /notify                     controllers.NotifyC.getNotify
POST    /notify                     controllers.NotifyC.postNotify
GET     /notify/:id                 controllers.NotificationC.getNotification(id: Long)
POST    /notify/:id                 controllers.NotificationC.postNotification(id: Long)

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

# 動作確認

`$ activator run`

`http://localhost:9000/notify`にアクセスして動作確認してみてください。

# keterによるデプロイ

今回は、HaskellのYesodというWebフレームワークを使ったWebアプリをデプロイする際にお世話になったKeterを使ってデプロイしてみます。

Keterは、もともとはYesodのデプロイツールとして開発されているのですが、起動するポート番号を環境変数で指定できるWebアプリであれば、特に言語を問わず利用できるようです。

Keterは、ホスト名によるアクセスに対して、特定のWebアプリにフォワードするリバースプロキシの機能を持っているので、例えば、同一サーバ上で、`http://play.hostname.xxx/notify`へのアクセスをPlayに、`http://yesod.hostname.xxx/notify`へのアクセスをYesodに振り分けるといった使い方もできます。

また、他の機能としては、Webアプリの自動起動、プロセス監視、リビルドによる再起動などの機能もついています。

# keterの実行ファイルの作成

keterはHaskellで作成されていますので、まずはHaskellの開発環境を構築するためにstackをインストールします。

[公式サイト](http://docs.haskellstack.org/en/stable/README/)を参考に使用するOSに合わせてインストールしてください。

次に、HaskellのGHCのインストールとともにketerの実行ファイルを作成します。

```
$ stack install keter --install-ghc
```

作成された実行ファイル`keter`をKona LinuxやAmazon EC2‎の`/usr/bin/`等に放り込み、実行権限を与えます。

# プロジェクトのビルドファイルの作成

まずはactivatorでプロジェクトのビルドファイルを作成します。

```
$ activator dist
```

`target/universal/`に作成された`notify-1.0-SNAPSHOT.zip`を解凍して作業フォルダに移動します。

作業フォルダ内に`config`フォルダを作成し、プロジェクトのketerファイルを作成するためのコンフィグレーションを`keter.yml`に設定します。

```[作業フォルダ]/config/keter.yml
stanzas:
  - type: webapp
    exec: ../notify-1.0-SNAPSHOT/bin/notify
    args: []
    hosts:
      - localhost
      - aws.hostname.xxx
    requires-secure: false
```

`notify-1.0-SNAPSHOT/conf/application.conf`の設定を変更します。

```scala:[作業フォルダ]/notify-1.0-SNAPSHOT/conf/application.conf
play.crypto.secret = "mypassword"

play.evolutions {
  # You can disable evolutions for a specific datasource if necessary
  db.default.enabled = false
}
```

keter側ではフォワードするポートを環境変数「PORT」で指定してくるので、`notify-1.0-SNAPSHOT/bin/notify`の下部にある「Main script」内で起動時のパラメータを追加します。

```scala:[作業フォルダ]/notify-1.0-SNAPSHOT/bin/notify
addJava "-Dhttp.port=$PORT"
addJava "-Dplay.crypto.secret=mypassword"
```

作業フォルダ内は下記の構成となります。

[作業フォルダ]
－　config
　　－　keter.yml
－　notify-1.0-SNAPSHOT
　　－　bin
　　－　conf
　　－　lib
　　－　share

作業フォルダ内で、プロジェクトのketerファイルを作成します。

```
$ tar czf ../notify.keter *
```

# keterの設定

Kona LinuxやAmazon EC2‎に下記のディレクトリ構成を作ります。

```
$ sudo mkdir -p /opt/keter/etc
$ sudo mkdir -p /opt/keter/incoming
$ sudo mkdir -p /opt/keter/db
$ sudo chown $USER.$USER /opt/keter/etc
$ sudo chown $USER.$USER /opt/keter/incoming
$ sudo chown $USER.$USER /opt/keter/db
```

作成した`notify.keter`をKona LinuxやAmazon EC2‎の`/opt/keter/incoming`にコピーします。

動作確認時に自動で作成された`notify.mv.db`をKona LinuxやAmazon EC2‎の`/opt/keter/db`にコピーします。

次にketerを実行するためのコンフィグレーションを設定します。

```/opt/keter/etc/keter-config.yaml
root: /opt/keter

listeners:
  - host: "*4"
    port: 3000

setuid: ubuntu

env:
  H2_DATABASE: jdbc:h2:/opt/keter/db/notify
  MAIL_HOST: localhost
  MAIL_PORT: "25"
  MAIL_FROM: NOTIFY SENDER <from@xxxx.xx>
  MAIL_TO: to@mydomain.xxx
```

デーモンとして起動するためのコンフィグレーションを設定します。

※Ubuntu 16.04の場合

```/etc/systemd/system/keter.service
[Unit]
Description=Keter Deployment Handler
After=local-fs.target network.target

[Service]
ExecStart=/usr/bin/keter /opt/keter/etc/keter-config.yaml

[Install]
WantedBy=multi-user.target
```

※Ubuntu 14.04の場合

```/etc/init/keter.conf
description "Keter Deployment Handler"

start on (net-device-up and local-filesystems and runlevel [2345])
stop on runlevel [016]
respawn

console output

exec keter /opt/keter/etc/keter-config.yaml
```

起動して確認します。

※Ubuntu 16.04の場合
`sudo systemctl start keter`

※Ubuntu 14.04の場合
`sudo start keter`

`http://aws.hostname.xxx:3000/notify`にアクセスして動作確認してみてください。

# さいごに

駆け足でactivatorによる開発環境の構築からketerによるデプロイまで見てきました。

Yesodでの実装を挟んだことで、Play2.4で作成した時よりも、それぞれの責務が分かりやすい実装になったように思います。

DIでタイマーを起動するようにしたことで、ControllerとActorの依存もなくなり、また、Yesodのテンプレートを参考にハンドラーを分けることで、メタボ気味だったControllerがすっきりとしました。

Playだけでは決して接点のないketerとの組み合わせもなかなか面白かったです。

keterにはリビルドによる再起動の機能があるので、プロジェクトを修正した場合でも、再度`notify.keter`を作成して`/opt/keter/incoming`に放り込むだけで、自動的にデプロイして再起動してくれるので便利だと思いました。