Actions
Overview
- https://www.playframework.com/documentation/2.3.x/ScalaActions
- https://www.playframework.com/documentation/2.3.x/HttpApi
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 に定義されている。
デフォルトの BodyParser は parse.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("...")
}
ActionBuilder は ActionBuilder#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#composeAction に Action をラップする関数を指定できる。共通の前後処理をパーツ化しておいて、組み替えることで、コードを再利用できる。
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] により ActionBuilder の Request 型を変換できる。
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)
}
}