Actions

Overview

EssentialAction

トレイトplay.api.mvc.EssentialAction の実体は、HTTP リクエストヘッダ play.api.mvc.RequestHeader を引数とし、Iteratee[Array[Byte], Result] を返す関数である。

trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])

Array[Byte] は HTTP ボディ部のチャンクにあたる。ストリームとしてボディ部を受け取り、どのように Result を組み立てるかを Iteratee として定義する。

Action

トレイト play.api.mvc.Action[A]EssentialAction を継承している。

すなわち RequestHeader を受け取り Iteratee[Array[Byte], Result] を返す関数であり、実装は play.api.mvc.BodyParser[+A] を介して行なっている。

trait Action[A] extends EssentialAction {
  ...
  def parser: BodyParser[A]

  def apply(request: Request[A]): Future[Result]

  def apply(rh: RequestHeader): Iteratee[Array[Byte], Result] = parser(rh).mapM {
    case Left(r) =>
      Play.logger.trace("Got direct result from the BodyParser: " + r)
      Future.successful(r)
    case Right(a) =>
      val request = Request(rh, a)
      Play.logger.trace("Invoking action with request: " + request)
      Play.maybeApplication.map { app =>
        play.utils.Threads.withContextClassLoader(app.classloader) {
          apply(request)
        }
      }.getOrElse {
        apply(request)
      }
  }
  ...
}

BodyParser の実体は RequestHeader を受け取り Itereatee[Array[Byte], Either[Result, A]] を返す関数である。

  • 失敗時の Left は、直接エラー応答の Result となる。
  • 成功時の Right は、ボディ部で、Action#apply(request: Request[A]) を介して Result を得る。

parser に、ボディ部が渡っていないのが疑問に思いがちだが、返すのは Iteratee であって、どのようにボディ部 Array[Byte] から出力 A を組み立てるかの定義だけである。ボディ部は、然るべき Enumerator からストリーム送信されるのであって BodyParser が全てのボディ部を得るのではない。

Helper Methods

ヘルパーメソッド Action.apply を使って Action#apply(request: Request[A]) を実装できる。

def noRequest: Action[AnyContent] = Action {
  Ok("Hello")
}
def withDefaultContent: Action[AnyContent] = Action { result =>
  Ok("Request: " + request)
}

明示的に Future[Result] で返したい場合は Action.async を使う。

import play.api.libs.ws._

def wsAction = Action.async {
  WS.url("http://foo.example.net").get().map { response =>
    Ok(response.body)
  }
}

Action.async は非同期処理用ではなく、明示的に Future[Result] を返したい場合のヘルパーメソッドである。いずれのヘルパーメソッドを使ったとしても、実装される Action#apply(request: Request) が返すのは Future[Result] であり、どちらも非同期に実行されることに違いはない。

Future を使えば、自動的にノンブロッキングとなるのではない。ブロックする処理を書かなければいけない場合は、コアのスレッドを占有しないように、別の ExecutionContext を使う必要がある。

import play.api.libs.concurrent.Akka

implicit val expensiveOperations =
  Akka.system.dispatchers.lookup("contexts.expensive-operations")

// OK: To avoid blocking operation, use a separate execution context.
def search: Action[AnyContent] = Action.async {
  val job1 = Promise.timeout({ ... } 5.second)
  val job2 = Promise.timeout({ ... } 5.second)
  Future.firstCompleteOf(Seq(job1, job2) map { x =>
    ...
    Ok(...)
  }) (expensiveOperations)
}

// NG: The thread of defaultContext is going to block.
def block: Action[AnyContent] = Action.async {
  Future {
    Thread.sleep(10000L)
    Ok(...)
  }
}

BodyParser

基本的な BodyParser は、あらかじめ play.api.mvc.BodyParsers.parse に定義されている。

デフォルトの BodyParserparse.anyContent が使われる。ボディ部は play.api.mvc.AnyContent になる。

def xmlFormat = Action { request =>
  request.body.asXml map { xml =>
    Ok(...)
  }.getOrElse {
    BadRequest(..)
  }
}

ヘルパーメソッド Action.apply[A](parser: BodyParser[A])(...) を使うと、ボディ部を明示的に指定できる。 不正なリクエストの場合は Action ブロックには渡らず BadReqeust 400 エラーが直接応答される。

def xmlOnly = Action(parse.xml) { request => ... }

tolerant がついているものは、リクエストヘッダのチェックを行なわず、ボディ部がパースできればエラーとならない。

def xmlOnly = Action(parse.torelantXml) { request => ... }

file / temporaryFile / multipartFormData

parse.file を用いるとボディ部をファイルに保存できる。指定した java.io.File がボディ部になる。

val parser = parse.file(to = new java.io.File("/path/to/a.txt"))

parse.temporaryFile は一時ファイルで保存できる。play.api.libs.Files.TemporaryFile がボディ部になる。TemporaryFIle のメソッドは、旧 Java File API によるもののため Deprecated となっている。

フォームからのファイルアップロード multipart/form-data 形式には、parse.multipartFormData を使う。

val upload = Action(parse.multipartFormData) { request =>
  request.body.file("pic").map { part =>
    val temp: TemporaryFile = part.ref
    val file: java.io.File = temp.file
    ...
    Ok(...)
  }.getOrElse {
    BadRequest(...)
  }
}

ファイルサイズに制限をかけたい場合は、ヘルパーメソッド parse.maxLength を使う。ボディ部は Either[MaxSizeexceeded, A] となる。

// up to 4096 bytes
val upload = Action(parse.maxLength(4096, parse.multipartFormData)) { request =>
  request.body match {
    case Left(MaxSizeExceeded(len)) => ...
    case Right(multipartFormdata) => ...
  }
}

ActionBuilder

ヘルパーオブジェクト Action の実体は、トレイト play.api.mvc.ActionBuilder[Request] である。

object Action extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A],
                     block: (Request[A]) => Future[Result]) = block(request)
}

同様に ActionBuilder を実装すれば、独自のヘルパーオブジェクトを作成できる。

object AppAction extends ActionBuilder[Request] {
  override def invokeBlock[A](request: Request[A],
                              block: (Request[A]) => Future[Result]): Future[Result] = {
    if (request.remoteAddress.equals("127.0.0.1")) {
      block(request).map { result =>
        result.withHeaders("X-UA-Compatible" -> "Chrome=1")
      }
    } else Future.successful(Forbidden) // localhost only
  }
}

val action = AppAction {
  Ok("...")
}

ActionBuilderActionBuilder#andThen で連結できる。Request は外側から、Future[Result] は内側から伝播する。

val action = (LoggingAction andThen SecureAction) {
  Ok("...")
}

以下の様に Action をラップしてもよい。

case class Logging[A](action: Action[A]) extends Action[A] {
  override def apply(request: Request[A]): Future[Result] = {
    Logger.info("...")
    action(request)
  }

  lazy val parser = action.parser
}

def action = Logging {
  Action {
    Ok("...")
  }
}

Action.async ブロック内で、元の Action#apply を呼ぶ方法でもよい。元の BodyParser もみ消さないように明示する必要がある。

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

ActionBuilder#composeActionAction をラップする関数を指定できる。共通の前後処理をパーツ化しておいて、組み替えることで、コードを再利用できる。

def onlyHttps(action: Action[A]): Action[A] = ...
def loggingErrors(action: Action[A]): Action[A] = ...

object AppAction extends ActionBuilder[Request] {
  override def invokeBlock[A](request: Request[A],
                              block: (Request[A]) => Future[Result]) = {
    block(request)
  }

  override protected def composeAction[A](action: Action[A]): Action[A] =
    onlyHttps(loggingErrors(action))
}

ActionRefiner

play.api.mvc.ActionRefiner[R, P] により ActionBuilderRequest 型を変換できる。

Future[Either[Result, P[A]]] を返す ActionRefiner#refine[A](request: R[A]) を実装する。UserRequest のコンストラクタの第一引数 Request[A] は、継承元の WrappedRequest に引き渡す引数になるので、重複してインスタンス変数に持つ必要はない。

case class User(id: Long, name: String)

object User {
  def find(id: Long): Option[User] = ???
}

class UserRequest[A](request: Request[A], val user: User) extends WrappedRequest[A](request)

val UserAction = new ActionRefiner[Request, UserRequest] {
  override protected def refine[A](request: Request[A]): Future[Either[Result, UserRequest]] =
    Future.successful {
      (for {
        userid <- request.sessions.get("userid")
        id <- java.lang.Long.parseLong(userid, 10)
        user <- User.find(id)
      } yield new UserRequest(request, user)).toRight(Forbidden)
    }
}

val action = (Action andThen UserAction) { userRequest =>
  Ok("Hello " + userRequest.user.name)
}

ActionTransformer

Request 型の変換に際して例外が起こらないなら、play.api.mvc.ActionTransformer[R, P] を使えばよい。

class UserRequest[A](request: Request[A],
                     val lang: Lang,
                     val robot: Boolean) extends WrappedRequest[A](request)
object UserAction extends ActionTransformer[Request, UserRequest] {
  override protected def transform[A](request: Request[A]): Future[UserRequest[A]] =
    Future.successful {
      val lang: Lang = ...
      val robot: Booean = ...
      new UserRequest(request, lang, robot)
    }
  }
}

ActionFilter

例外チェックのみを行ないたいなら play.api.mvc.ActionFilter[R] を使えばよい。例外時に Some[Result] を返すことで Action ブロックを経由せずに直接応答する。正常時には None を返せばよい。

object RemoteAddressFilter extends ActionFilter[Request] {
  override protected def filter[A](request: Request[A]) =
    Future.successful {
      if (request.remoteAddress.equals("127.0.0.1")) None
      else Some(Forbidden)
    }
}