Часть ядра фреймворка Play2, относящаяся собственно к обработке веб-запросов - сравнительно небольшая, и основным типом в нем является Action, т.е. команда, и некоторое количество вспомогательных типов (Request, Result, BodyParser и др.).

В самом грубом приближении, ядро Play2 представляет собой API, которое занимается преобразованием вида:

RequestHeader -> Array[Byte] -> Result 

Приведенное вычисление принимает на вход заголовок RequestHeader, затем принимает тело запроса как Array[Byte] и генерирует Result.

Этот тип предполагает вычитку всего тела запроса в память или на диск, а это не всегда хорошо с точки зрения расходования памяти.

В этом случае мы хотели бы получать тело запроса в виде блоков и обрабатывать их по мере поступления, если это необходимо.

Т.е. неплохо было бы поменять вторую стрелочку таким образом, она принимала на вход вот такие блоки и в конечном счете генерировала бы результат. И необходимый нам тип действительно существует, называется он Iteratee, и параметризуется двумя типами - тип входного параметра и тип результата.

Т.о., Iteratee[E,R] принимает на вход тип E и возвращает тип R, конкретно в данном случае - принимающий на вход Array[Byte], возвращающий Result. Т.е. мы немного меняем тип вот так:

RequestHeader -> Iteratee[Array[Byte],Result]

Первую стрелочку мы просто заменяем на Function[From,To], т.е. фактически заменяем стрелочку на символ =>: `

RequestHeader => Iteratee[Array[Byte],Result]

Как мы знаем, для более выразительного построения новых типов с использованием уже существующих мы можем использовать инфиксные операторы типов. Если мы объявим псевдоним типа Iteratee[E,R]

type ==>[E,R] = Iteratee[E,R]

То теперь мы можем использовать его в качестве инфиксного оператора типа:

RequestHeader => Array[Byte] ==> Result

Что означает следующее: на вход принимаем заголовки запроса, на вход же принимаем тело запроса в виде Array[Byte] и в финале возвращаем Result. Приблизительно таким образом объявлен трейт EssentialAction, который является базовым для всех команд (actionов) (инфиксный оператора типов там не используется, мы привели его просто для наглядности):

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

В то же время можно сказать, что тип Result является абстрактным представлением заголовков и тела ответа. В первом приближении такой тип выглядел бы вот так:

case class Result(headers: ResponseHeader, body:Array[Byte])

Но, опять же, как и в ситуации с запросом, мы бы хотели не сразу сформировать весь массив байт ответа (потому что он может быть довольно большим и занять всю память), а постепенно, блоками отдавать его клиенту. Поэтому нам неплохо было бы заменить Array[Byte] чем-то вроде генератора блоков байт.

Для этого у нас уже есть необходимый тип - Enumerator[E], который может генерировать блоки типа E, в нашем случае - Enumerator[Array[Byte]]:

case class Result(headers:ResponseHeaders, body:Enumerator[Array[Byte]])

Если же нам все-таки не нужно отсылать ответ постепенно, а мы хотим отдать все тело ответа сразу, мы можем отослать все данные в одном блоке.

Любой тип данных E, который можно сконвертировать в поток байт, или Array[Byte], может быть потенциально отдан в виде потока - за это отвечает объект типа Writeable[E], который отдается в виде implicit’ного объекта, несколько упрощенно это можно представить как:

case class Result[E](headers:ResponseHeaders, body:Enumerator[E])(implicit writeable:Writeable[E])

На самом деле, правда, writeable передается не в конструкторе Resultа, а, например, в методе-фабрика в классе Status, который тоже является Resultом.

def apply[C](content: C)(implicit writeable: Writeable[C]): Result

EssentialAction - всего лишь трейт, то есть интерфейс. В контроллерах реально будет использоваться производный от него Action, а точнее Action[A]. Action[A] наследуется от EssentialAction, при этом он уже может преобразовать типизированный Request[A], а не просто поток байт, но для типа A должен быть предоставлен BodyParser[A].

trait Action[A] extends EssentialAction {

  type BODY_CONTENT = A

  def parser: BodyParser[A]

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

  //...
}

В силу асинхронной природы Iteratee, результатом Action‘а уже является Future[Result] вместо Result. Action, вообще говоря тоже трейт, при реализации которого надо переопределить методы apply и parser.

Итак, мы установили, что (в подавляющем большинстве) запросы приложения, написанного с использованием Play, обрабатываются с помощью Action, который есть по сути функция (play.api.mvc.Request => play.api.mvc.Result). Приведем пример простейшего Actionа, т.е. “команды”

val echo = Action { request =>
  Ok("Got request [" + request + "]")
}

Тело Action‘а возвращает значение play.api.mvc.Result, представляющее HTTP ответ, отсылаемый клиенту. В данном случае Ok вернет ответ со статусом 200 OK, и типом тела ответа text/plain. Собственно, Ok - это константа

val Ok = new Status(OK)

Как мы уже говорили, фактически Status представляет собой вариант Resultа:

class Status(status: Int) extends Result { // ...

    def apply[C](content: C)(implicit writeable: Writeable[C]): Result = ...
...
}

Вообще, надо заметить, что т.н. companion-object Action наследует ActionBuilder[Request], в котором есть ряд методов-фабрик; в данном случае вызывается метод:

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

Т.е. используется BodyParsers.parse.default, который парсит тело запроса исходя из содержимого заголовка "Content-Type".

Вообще говоря, BodyParser[A] является на самом деле Iteratee[Array[Byte],A], т.е. почти то же самое, что мы видели в EssentialAction, за исключением того, что вместо Result здесь A.

Методы для создания разных видов команд (Actionов), как мы уже говорили, находятся в трейте ActionBuilder. Очевидно, что мы можем реализовать свой ActionBuilder, и использовать его для создания нужных нам видов команд. Например, пусть нам надо создать декоратор для логирования, т.е. фактически каждый вызов команды, созданной нами, будет логироваться.

Мы можем реализовать эту функциональность в методе invokeBlock, который вызывается для каждой команды, созданной ActionBuilderом:

import play.api.mvc._

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    Logger.info("Calling action")
    block(request)
  }
}

И теперь создадим логируемую команду:

def index = LoggingAction {
  Ok("Hello World")
}

Композиция команд

Иногда возникает необходимость в нескольких видах ActionBuilder‘ов, например, если у нас разные виды аутентификации. Но в то же время мы не хотели бы отказываться от нашей логируемой команды, т.е. у нас возникает необходимость скомбинировать несколько ActionBuilder‘ов.

Сделать это можно, например, вложением одной команды в другую, т.е. путем передачи некоторой команды action в наш логируемый вариант:

import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

  def apply(request: Request[A]): Future[Result] = {
    Logger.info("Calling action")
    action(request)
  }

  lazy val parser = action.parser
}

В принципе, то же самое можно сделать и без определения отдельного класса:

import play.api.mvc._

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

Есть еще в ActionBuilder такая вещь, как метод composeAction, специально созданный для этих целей:

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    block(request)
  }
  override def composeAction[A](action: Action[A]) = new Logging(action)
}

После чего мы опять-таки можем просто вызвать LoggingAction:

def index = LoggingAction {
  Ok("Hello World")
}

Ну и конечно, подмешивание команд можно делать и без создания отдельного ActionBuilderа, просто вложением команд (правда, в этом случае мы теряем преимущества повторного использования, т.е. этот вариант годится, если, например, нам нужно залогировать только одну команду):

def index = Logging {
  Action {
    Ok("Hello World")
  }
}