LoginSignup
15
13

More than 5 years have passed since last update.

Play Frameworkのソースコードリーディング Action周り(BodyParserとAnyContent)

Posted at

はじめに

本記事はPlay Frameworkのソースコードリーディング Action周り の続きとなる。

BodyParserとAnyContent

object Actionのapplyの中で以下の定義があった。

final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.anyContent)(block)

BodyParsers.parse.anyContentメソッド定義は以下となる。

/**
 * Guess the body content by checking the Content-Type header.
 */
def anyContent: BodyParser[AnyContent] = BodyParser("anyContent") { request =>
  import play.api.libs.iteratee.Execution.Implicits.trampoline
  if (request.method == HttpVerbs.GET || request.method == HttpVerbs.HEAD) {
    Play.logger.trace("Parsing AnyContent as empty")
    Done(Right(AnyContentAsEmpty), Empty)
  } else {
    val contentType: Option[String] = request.contentType.map(_.toLowerCase(Locale.ENGLISH))
    contentType match {
      case Some("text/plain") => {
        Play.logger.trace("Parsing AnyContent as text")
        text(request).map(_.right.map(s => AnyContentAsText(s)))
      }
      case Some("text/xml") | Some("application/xml") | Some(ApplicationXmlMatcher()) => {
        Play.logger.trace("Parsing AnyContent as xml")
        xml(request).map(_.right.map(x => AnyContentAsXml(x)))
      }
      case Some("text/json") | Some("application/json") => {
        Play.logger.trace("Parsing AnyContent as json")
        json(request).map(_.right.map(j => AnyContentAsJson(j)))
      }
      case Some("application/x-www-form-urlencoded") => {
        Play.logger.trace("Parsing AnyContent as urlFormEncoded")
        urlFormEncoded(request).map(_.right.map(d => AnyContentAsFormUrlEncoded(d)))
      }
      case Some("multipart/form-data") => {
        Play.logger.trace("Parsing AnyContent as multipartFormData")
        multipartFormData(request).map(_.right.map(m => AnyContentAsMultipartFormData(m)))
      }
      case _ => {
        Play.logger.trace("Parsing AnyContent as raw")
        raw(request).map(_.right.map(r => AnyContentAsRaw(r)))
      }
    }
  }
}

要はAPI利用者から特に指定がなくてもcontent-type毎にリクエストボディのパースの仕方およびその結果(AnyContentAsTextやAnyContentAsRaw等の型)を自動で算出している。

本記事ではGET呼び出しを想定しているOk("hello")を例に進んできているが、GETだとすぐ終わってしまうのでPOSTリクエストがあったことを想定し進める。特にtext(request)をピックアップして追いかける。

case Some("text/plain") => {
  Play.logger.trace("Parsing AnyContent as text")
  text(request).map(_.right.map(s => AnyContentAsText(s)))
}

一見すると上記はdef text(request: RequestHeader)というメソッドが呼ばれているように見えるがそうではない。
実際には以下が呼ばれており、

def text: BodyParser[String] = text(DEFAULT_MAX_TEXT_LENGTH)

返ってきた(生成された)BodyParserインスタンスにRequestを渡している。つまり、apply(request: RequestHeader)メソッドである。

順を追ってtext関数のその先を見ていくことにする。
なお、DEFAULT_MAX_TEXT_LENGTHは、デフォルト設定では100kb。
application.confでparsers.text.maxLength = 512kと設定することで上限を書き換え可能。

text(DEFAULT_MAX_TEXT_LENGTH)は以下。

def text(maxLength: Int): BodyParser[String] = when(
  _.contentType.exists(_.equalsIgnoreCase("text/plain")),
  tolerantText(maxLength),
  createBadResult("Expecting text/plain body")
)

リクエストヘッダのcontent-type部分をチェックしているのが分かる。
チェーンして、

def tolerantText(maxLength: Int): BodyParser[String] = BodyParser("text, maxLength=" + maxLength) { request =>
  // Encoding notes: RFC-2616 section 3.7.1 mandates ISO-8859-1 as the default charset if none is specified.

  import Execution.Implicits.trampoline
  Traversable.takeUpTo[Array[Byte]](maxLength)
    .transform(Iteratee.consume[Array[Byte]]().map(c => new String(c, request.charset.getOrElse("ISO-8859-1"))))
    .flatMap(Iteratee.eofOrElse(Results.EntityTooLarge))
}

となり、object BodyParserを使用してBodyParserインスタンスを生成して返している。
ちなみに「tolerant」とは「寛大な、懐の深い」という意味。要はcontent-typeチェックを行わないメソッドであることを示している。

では実際にBodyParserの生成に使っているコンパニオンオブジェクト部を見てみる。

object BodyParser {
  def apply[T](debugName: String)(f: RequestHeader => Iteratee[Array[Byte], Either[Result, T]]): BodyParser[T] = new BodyParser[T] {
    def apply(rh: RequestHeader) = f(rh)
    override def toString = "BodyParser(" + debugName + ")"
  }

newしているのが分かる。

なお、BodyParserトレイト宣言部は以下。

trait BodyParser[+A] extends Function1[RequestHeader, Iteratee[Array[Byte], Either[Result, A]]] {

実体はRequestHeaderを受け取ってIteratee[Array[Byte], Either[Result, A]]を返すFunction1であることが分かる。

HTTPリクエストのボディ部も複数回のArray[Byte]で来るのでそれを処理しやすいようIterateeとなっている。
inputはArray[Byte]、outputはうまくいかなかった時がResultで、うまくパース出来た時がAとなる、Either。

これでようやく、以下のコードのtext部分が終わりとなる。

case Some("text/plain") => {
  Play.logger.trace("Parsing AnyContent as text")
  text(request).map(_.right.map(s => AnyContentAsText(s)))
}

BodyParser#apply(request: RequestHeader)、これは先ほどdef apply(rh: RequestHeader) = f(rh)と定義していることを確認した。単に関数を実行しているだけで、Iteratee[Array[Byte], Either[Result, String]]が返ってきている。
Iteratee#mapの定義は以下なので、

trait Iteratee[E, +A] {
  def map[B](f: A => B)(implicit ec: ExecutionContext): Iteratee[E, B] = this.flatMap(a => Done(f(a), Input.Empty))(ec)

map関数に渡す高階関数部ではoutput側のEitherが渡ってくることが分かる。
EitherのrightのStringインスタンスをAnyContentAsText型のインスタンスに変換している。

case class AnyContentAsText(txt: String) extends AnyContent

これで晴れて、object Actionのapplyメソッド内のBodyParsers.parse.anyContent関数の処理が終わってBodyParser[AnyContent]が渡っていく事となる。

final def apply(block: R[AnyContent] => Result): Action[AnyContent] = apply(BodyParsers.parse.anyContent)(block)

なお、先ほどスキップしたGET時のanyContentメソッドのBodyParser部分を改めて。

def anyContent: BodyParser[AnyContent] = BodyParser("anyContent") { request =>
  import play.api.libs.iteratee.Execution.Implicits.trampoline
  if (request.method == HttpVerbs.GET || request.method == HttpVerbs.HEAD) {
    Play.logger.trace("Parsing AnyContent as empty")
    Done(Right(AnyContentAsEmpty), Empty)
  } else {

GET時はrequestを受け取ってDone(Right(AnyContentAsEmpty), Empty)なIterateeとなり、BodyParser[AnyContent(実際はAnyContentAsEmpty)]が返っている。

Action

改めてActionインスタンスのapply関数。

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)
    }
}(executionContext)

BodyParserはRequestHeaderを受け取り、Iteratee[Array[Byte], Either[Result, A]]を返すFunction1であった。よってparser(rh)の結果はパースが終わったIteratee[Array[Byte], Either[Result, A]]。IterateeのmapM関数を実行すると高階関数にはEither[Result, A]が渡ってくる(この高階関数はFuture[Result]を返す必要あり)。
Left、つまりボディ部のパース失敗時はLeftにBadRequestが格納されているのでそれを返す。
Right、つまりパース成功時はAの部分が利用できるようになっている。RequestHeaderとbodyを使ってRequestを生成、その後ActionインスタンスのapplyにRequestを渡し、処理実行を行う。

このapplyは、object Actionのasync内で生成していたActionインスタンスのapply(Request)であり、ここで自分で定義したblock部が実行される。

final def async[A](bodyParser: BodyParser[A])(block: R[A] => Future[Result]): Action[A] = composeAction(new Action[A] {
  def parser = composeParser(bodyParser)
  def apply(request: Request[A]) = try {
    invokeBlock(request, block)

以上、終わり!

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13