alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Play Framework/Scala example source code file (ContentTypes.scala)

This example Play Framework source code file (ContentTypes.scala) is included in my "Source Code Warehouse" project. The intent of this project is to help you more easily find Play Framework (and Scala) source code examples by using tags.

All credit for the original source code belongs to Play Framework; I'm just trying to make examples easier to find. (For my Scala work, see my Scala examples and tutorials.)

Play Framework tags/keywords

anycontent, api, bodyparser, concurrent, done, int, lib, library, option, parthandler, play, play framework, right, seq, some, string

The ContentTypes.scala Play Framework example source code

/*
 * Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
 */
package play.api.mvc

import scala.language.reflectiveCalls
import java.io._
import scala.concurrent.Future
import scala.xml._
import play.api._
import play.api.libs.json._
import play.api.libs.iteratee._
import play.api.libs.iteratee.Input._
import play.api.libs.iteratee.Parsing._
import play.api.libs.Files.TemporaryFile
import MultipartFormData._
import scala.collection.mutable.ListBuffer
import java.util.Locale
import scala.util.control.NonFatal
import play.api.http.HttpVerbs
import play.utils.PlayIO

/**
 * A request body that adapts automatically according the request Content-Type.
 */
sealed trait AnyContent {

  /**
   * application/form-url-encoded
   */
  def asFormUrlEncoded: Option[Map[String, Seq[String]]] = this match {
    case AnyContentAsFormUrlEncoded(data) => Some(data)
    case _ => None
  }

  /**
   * text/plain
   */
  def asText: Option[String] = this match {
    case AnyContentAsText(txt) => Some(txt)
    case _ => None
  }

  /**
   * application/xml
   */
  def asXml: Option[NodeSeq] = this match {
    case AnyContentAsXml(xml) => Some(xml)
    case _ => None
  }

  /**
   * text/json or application/json
   */
  def asJson: Option[JsValue] = this match {
    case AnyContentAsJson(json) => Some(json)
    case _ => None
  }

  /**
   * multipart/form-data
   */
  def asMultipartFormData: Option[MultipartFormData[TemporaryFile]] = this match {
    case AnyContentAsMultipartFormData(mfd) => Some(mfd)
    case _ => None
  }

  /**
   * Used when no Content-Type matches
   */
  def asRaw: Option[RawBuffer] = this match {
    case AnyContentAsRaw(raw) => Some(raw)
    case _ => None
  }

}

/**
 * AnyContent - Empty request body
 */
case object AnyContentAsEmpty extends AnyContent

/**
 * AnyContent - Text body
 */
case class AnyContentAsText(txt: String) extends AnyContent

/**
 * AnyContent - Form url encoded body
 */
case class AnyContentAsFormUrlEncoded(data: Map[String, Seq[String]]) extends AnyContent

/**
 * AnyContent - Raw body (give access to the raw data as bytes).
 */
case class AnyContentAsRaw(raw: RawBuffer) extends AnyContent

/**
 * AnyContent - XML body
 */
case class AnyContentAsXml(xml: NodeSeq) extends AnyContent

/**
 * AnyContent - Json body
 */
case class AnyContentAsJson(json: JsValue) extends AnyContent

/**
 * AnyContent - Multipart form data body
 */
case class AnyContentAsMultipartFormData(mdf: MultipartFormData[TemporaryFile]) extends AnyContent

/**
 * Multipart form data body.
 */
case class MultipartFormData[A](dataParts: Map[String, Seq[String]], files: Seq[FilePart[A]], badParts: Seq[BadPart], missingFileParts: Seq[MissingFilePart]) {

  /**
   * Extract the data parts as Form url encoded.
   */
  def asFormUrlEncoded: Map[String, Seq[String]] = dataParts

  /**
   * Access a file part.
   */
  def file(key: String): Option[FilePart[A]] = files.find(_.key == key)
}

/**
 * Defines parts handled by Multipart form data.
 */
object MultipartFormData {

  /**
   * A part.
   */
  sealed trait Part

  /**
   * A data part.
   */
  case class DataPart(key: String, value: String) extends Part

  /**
   * A file part.
   */
  case class FilePart[A](key: String, filename: String, contentType: Option[String], ref: A) extends Part

  /**
   * A file part with no content provided.
   */
  case class MissingFilePart(key: String) extends Part

  /**
   * A part that has not been properly parsed.
   */
  case class BadPart(headers: Map[String, String]) extends Part

  /**
   * A data part that has excedeed the max size allowed.
   */
  case class MaxDataPartSizeExceeded(key: String) extends Part
}

/**
 * Handle the request body a raw bytes data.
 *
 * @param memoryThreshold If the content size is bigger than this limit, the content is stored as file.
 */
case class RawBuffer(memoryThreshold: Int, initialData: Array[Byte] = Array.empty[Byte]) {

  import play.api.libs.Files._
  import scala.collection.mutable._

  @volatile private var inMemory: List[Array[Byte]] = if (initialData.length == 0) Nil else List(initialData)
  @volatile private var inMemorySize = initialData.length
  @volatile private var backedByTemporaryFile: TemporaryFile = _
  @volatile private var outStream: OutputStream = _

  private[play] def push(chunk: Array[Byte]) {
    if (inMemory != null) {
      if (chunk.length + inMemorySize > memoryThreshold) {
        backToTemporaryFile()
        outStream.write(chunk)
      } else {
        inMemory = chunk :: inMemory
        inMemorySize += chunk.length
      }
    } else {
      outStream.write(chunk)
    }
  }

  private[play] def close() {
    if (outStream != null) {
      outStream.close()
    }
  }

  private[play] def backToTemporaryFile() {
    backedByTemporaryFile = TemporaryFile("requestBody", "asRaw")
    outStream = new FileOutputStream(backedByTemporaryFile.file)
    inMemory.reverse.foreach { chunk =>
      outStream.write(chunk)
    }
    inMemory = null
  }

  /**
   * Buffer size.
   */
  def size: Long = {
    if (inMemory != null) inMemorySize else backedByTemporaryFile.file.length
  }

  /**
   * Returns the buffer content as a bytes array.
   *
   * This operation will cause the internal collection of byte arrays to be copied into a new byte array on each
   * invocation, no caching is done.  If the buffer has been written out to a file, it will read the contents of the
   * file.
   *
   * @param maxLength The max length allowed to be stored in memory.  If this is smaller than memoryThreshold, and the
   *                  buffer is already in memory then None will still be returned.
   * @return None if the content is greater than maxLength, otherwise, the data as bytes.
   */
  def asBytes(maxLength: Int = memoryThreshold): Option[Array[Byte]] = {
    if (size <= maxLength) {

      if (inMemory != null) {

        val buffer = new Array[Byte](inMemorySize)
        inMemory.reverse.foldLeft(0) { (position, chunk) =>
          System.arraycopy(chunk, 0, buffer, position, Math.min(chunk.length, buffer.length - position))
          chunk.length + position
        }
        Some(buffer)

      } else {
        Some(PlayIO.readFile(backedByTemporaryFile.file))
      }

    } else {
      None
    }
  }

  /**
   * Returns the buffer content as File.
   */
  def asFile: File = {
    if (inMemory != null) {
      backToTemporaryFile()
      close()
    }
    backedByTemporaryFile.file
  }

  override def toString = {
    "RawBuffer(inMemory=" + Option(inMemory).map(_.size).orNull + ", backedByTemporaryFile=" + backedByTemporaryFile + ")"
  }

}

/**
 * Default body parsers.
 */
trait BodyParsers {

  /**
   * Default body parsers.
   */
  object parse {

    /**
     * Unlimited size.
     */
    val UNLIMITED: Int = Integer.MAX_VALUE

    private val ApplicationXmlMatcher = """application/.*\+xml.*""".r

    /**
     * Default max length allowed for text based body.
     *
     * You can configure it in application.conf:
     *
     * {{{
     * parsers.text.maxLength = 512k
     * }}}
     */
    def DEFAULT_MAX_TEXT_LENGTH: Int = Play.maybeApplication.flatMap { app =>
      app.configuration.getBytes("parsers.text.maxLength").map(_.toInt)
    }.getOrElse(1024 * 100)

    // -- Text parser

    /**
     * Parse the body as text without checking the Content-Type.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    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))
    }

    /**
     * Parse the body as text without checking the Content-Type.
     */
    def tolerantText: BodyParser[String] = tolerantText(DEFAULT_MAX_TEXT_LENGTH)

    /**
     * Parse the body as text if the Content-Type is text/plain.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def text(maxLength: Int): BodyParser[String] = when(
      _.contentType.exists(_.equalsIgnoreCase("text/plain")),
      tolerantText(maxLength),
      createBadResult("Expecting text/plain body")
    )

    /**
     * Parse the body as text if the Content-Type is text/plain.
     */
    def text: BodyParser[String] = text(DEFAULT_MAX_TEXT_LENGTH)

    // -- Raw parser

    /**
     * Store the body content in a RawBuffer.
     *
     * @param memoryThreshold If the content size is bigger than this limit, the content is stored as file.
     */
    def raw(memoryThreshold: Int): BodyParser[RawBuffer] = BodyParser("raw, memoryThreshold=" + memoryThreshold) { request =>
      import play.core.Execution.Implicits.internalContext // Cannot run on same thread as may need to write to a file
      val buffer = RawBuffer(memoryThreshold)
      Iteratee.foreach[Array[Byte]](bytes => buffer.push(bytes)).map { _ =>
        buffer.close()
        Right(buffer)
      }
    }

    /**
     * Store the body content in a RawBuffer.
     */
    def raw: BodyParser[RawBuffer] = raw(memoryThreshold = 100 * 1024)

    // -- JSON parser

    /**
     * Parse the body as Json without checking the Content-Type.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def tolerantJson(maxLength: Int): BodyParser[JsValue] =
      tolerantBodyParser[JsValue]("json", maxLength, "Invalid Json") { (request, bytes) =>
        // Encoding notes: RFC 4627 requires that JSON be encoded in Unicode, and states that whether that's
        // UTF-8, UTF-16 or UTF-32 can be auto detected by reading the first two bytes. So we ignore the declared
        // charset and don't decode, we passing the byte array as is because Jackson supports auto detection.
        Json.parse(bytes)
      }

    /**
     * Parse the body as Json without checking the Content-Type.
     */
    def tolerantJson: BodyParser[JsValue] = tolerantJson(DEFAULT_MAX_TEXT_LENGTH)

    /**
     * Parse the body as Json if the Content-Type is text/json or application/json.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def json(maxLength: Int): BodyParser[JsValue] = when(
      _.contentType.exists(m => m.equalsIgnoreCase("text/json") || m.equalsIgnoreCase("application/json")),
      tolerantJson(maxLength),
      createBadResult("Expecting text/json or application/json body")
    )

    /**
     * Parse the body as Json if the Content-Type is text/json or application/json.
     */
    def json: BodyParser[JsValue] = json(DEFAULT_MAX_TEXT_LENGTH)

    /**
     * Parse the body as Json if the Content-Type is text/json or application/json,
     * validating the result with the Json reader.
     *
     * @tparam A the type to read and validate from the body.
     * @param reader a Json reader for type A.
     */
    def json[A](implicit reader: Reads[A]): BodyParser[A] =
      BodyParser("json reader") { request =>
        import play.api.libs.iteratee.Execution.Implicits.trampoline
        json(request) mapM {
          case Left(simpleResult) =>
            Future.successful(Left(simpleResult))
          case Right(jsValue) =>
            jsValue.validate(reader) map { a =>
              Future.successful(Right(a))
            } recoverTotal { jsError =>
              val msg = s"Json validation error ${JsError.toFlatForm(jsError)}"
              createBadResult(msg)(request) map Left.apply
            }
        }
      }

    // -- Empty parser

    /**
     * Don't parse the body content.
     */
    def empty: BodyParser[Unit] = BodyParser("empty") { request =>
      Done(Right(()), Empty)
    }

    // -- XML parser

    /**
     * Parse the body as Xml without checking the Content-Type.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def tolerantXml(maxLength: Int): BodyParser[NodeSeq] =
      tolerantBodyParser[NodeSeq]("xml", maxLength, "Invalid XML") { (request, bytes) =>
        val inputSource = new InputSource(new ByteArrayInputStream(bytes))

        // Encoding notes: RFC 3023 is the RFC for XML content types.  Comments below reflect what it says.

        // An externally declared charset takes precedence
        request.charset.orElse(
          // If omitted, maybe select a default charset, based on the media type.
          request.mediaType.collect {
            // According to RFC 3023, the default encoding for text/xml is us-ascii. This contradicts RFC 2616, which
            // states that the default for text/* is ISO-8859-1.  An RFC 3023 conforming client will send US-ASCII,
            // in that case it is safe for us to use US-ASCII or ISO-8859-1.  But a client that knows nothing about
            // XML, and therefore nothing about RFC 3023, but rather conforms to RFC 2616, will send ISO-8859-1.
            // Since decoding as ISO-8859-1 works for both clients that conform to RFC 3023, and clients that conform
            // to RFC 2616, we use that.
            case mt if mt.mediaType == "text" => "iso-8859-1"
            // Otherwise, there should be no default, it will be detected by the XML parser.
          }
        ).foreach { charset =>
            inputSource.setEncoding(charset)
          }
        Play.XML.load(inputSource)
      }

    /**
     * Parse the body as Xml without checking the Content-Type.
     */
    def tolerantXml: BodyParser[NodeSeq] = tolerantXml(DEFAULT_MAX_TEXT_LENGTH)

    /**
     * Parse the body as Xml if the Content-Type is application/xml, text/xml or application/XXX+xml.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def xml(maxLength: Int): BodyParser[NodeSeq] = when(
      _.contentType.exists { t =>
        val tl = t.toLowerCase(Locale.ENGLISH)
        tl.startsWith("text/xml") || tl.startsWith("application/xml") || ApplicationXmlMatcher.pattern.matcher(tl).matches()
      },
      tolerantXml(maxLength),
      createBadResult("Expecting xml body")
    )

    /**
     * Parse the body as Xml if the Content-Type is application/xml, text/xml or application/XXX+xml.
     */
    def xml: BodyParser[NodeSeq] = xml(DEFAULT_MAX_TEXT_LENGTH)

    // -- File parsers

    /**
     * Store the body content into a file.
     *
     * @param to The file used to store the content.
     */
    def file(to: File): BodyParser[File] = BodyParser("file, to=" + to) { request =>
      import play.core.Execution.Implicits.internalContext
      Iteratee.fold[Array[Byte], FileOutputStream](new FileOutputStream(to)) { (os, data) =>
        os.write(data)
        os
      }.map { os =>
        os.close()
        Right(to)
      }
    }

    /**
     * Store the body content into a temporary file.
     */
    def temporaryFile: BodyParser[TemporaryFile] = BodyParser("temporaryFile") { request =>
      Iteratee.flatten(Future {
        val tempFile = TemporaryFile("requestBody", "asTemporaryFile")
        file(tempFile.file)(request).map(_ => Right(tempFile))(play.api.libs.iteratee.Execution.trampoline)
      }(play.core.Execution.internalContext))
    }

    // -- FormUrlEncoded

    /**
     * Parse the body as Form url encoded without checking the Content-Type.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def tolerantFormUrlEncoded(maxLength: Int): BodyParser[Map[String, Seq[String]]] =
      tolerantBodyParser("urlFormEncoded", maxLength, "Error parsing application/x-www-form-urlencoded") { (request, bytes) =>
        import play.core.parsers._
        FormUrlEncodedParser.parse(new String(bytes, request.charset.getOrElse("utf-8")),
          request.charset.getOrElse("utf-8"))
      }

    /**
     * Parse the body as form url encoded without checking the Content-Type.
     */
    def tolerantFormUrlEncoded: BodyParser[Map[String, Seq[String]]] = tolerantFormUrlEncoded(DEFAULT_MAX_TEXT_LENGTH)

    /**
     * Parse the body as form url encoded if the Content-Type is application/x-www-form-urlencoded.
     *
     * @param maxLength Max length allowed or returns EntityTooLarge HTTP response.
     */
    def urlFormEncoded(maxLength: Int): BodyParser[Map[String, Seq[String]]] = when(
      _.contentType.exists(_.equalsIgnoreCase("application/x-www-form-urlencoded")),
      tolerantFormUrlEncoded(maxLength),
      createBadResult("Expecting application/x-www-form-urlencoded body")
    )

    /**
     * Parse the body as form url encoded if the Content-Type is application/x-www-form-urlencoded.
     */
    def urlFormEncoded: BodyParser[Map[String, Seq[String]]] = urlFormEncoded(DEFAULT_MAX_TEXT_LENGTH)

    // -- Magic any content

    /**
     * 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)))
          }
        }
      }
    }

    // -- Multipart

    /**
     * Parse the content as multipart/form-data
     */
    def multipartFormData: BodyParser[MultipartFormData[TemporaryFile]] = multipartFormData(Multipart.handleFilePartAsTemporaryFile)

    /**
     * Parse the content as multipart/form-data
     *
     * @param filePartHandler Handles file parts.
     */
    def multipartFormData[A](filePartHandler: Multipart.PartHandler[FilePart[A]]): BodyParser[MultipartFormData[A]] = BodyParser("multipartFormData") { request =>
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      val handler: Multipart.PartHandler[Either[Part, FilePart[A]]] =
        Multipart.handleDataPart.andThen(_.map(Left(_)))
          .orElse({ case Multipart.FileInfoMatcher(partName, fileName, _) if fileName.trim.isEmpty => Done(Left(MissingFilePart(partName)), Input.Empty) }: Multipart.PartHandler[Either[Part, FilePart[A]]])
          .orElse(filePartHandler.andThen(_.map(Right(_))))
          .orElse { case headers => Done(Left(BadPart(headers)), Input.Empty) }

      Multipart.multipartParser(handler)(request).map { errorOrParts =>
        errorOrParts.right.map { parts =>
          val data = parts.collect { case Left(DataPart(key, value)) => (key, value) }.groupBy(_._1).mapValues(_.map(_._2))
          val az = parts.collect { case Right(a) => a }
          val bad = parts.collect { case Left(b @ BadPart(_)) => b }
          val missing = parts.collect { case Left(missing @ MissingFilePart(_)) => missing }
          MultipartFormData(data, az, bad, missing)

        }
      }
    }

    object Multipart {

      def multipartParser[A](partHandler: Map[String, String] => Iteratee[Array[Byte], A]): BodyParser[Seq[A]] = parse.using { request =>

        val maybeBoundary = for {
          mt <- request.mediaType
          (_, value) <- mt.parameters.find(_._1.equalsIgnoreCase("boundary"))
          boundary <- value
        } yield ("\r\n--" + boundary).getBytes("utf-8")

        maybeBoundary.map { boundary =>

          BodyParser { request =>

            import play.api.libs.iteratee.Execution.Implicits.trampoline

            val CRLF = "\r\n".getBytes
            val CRLFCRLF = CRLF ++ CRLF

            val takeUpToBoundary = Enumeratee.takeWhile[MatchInfo[Array[Byte]]](!_.isMatch)

            val maxHeaderBuffer = Traversable.takeUpTo[Array[Byte]](4 * 1024) transform Iteratee.consume[Array[Byte]]()

            val collectHeaders = maxHeaderBuffer.map { buffer =>
              val (headerBytes, rest) = Option(buffer).map(b => b.splitAt(b.indexOfSlice(CRLFCRLF))).get

              val headerString = new String(headerBytes, "utf-8").trim
              val headers = headerString.lines.map { header =>
                val key :: value = header.trim.split(":").toList
                (key.trim.toLowerCase, value.mkString.trim)
              }.toMap

              val left = rest.drop(CRLFCRLF.length)
              (headers, left)
            }

            val readPart = collectHeaders.flatMap { case (headers, left) => Iteratee.flatten(partHandler(headers).feed(Input.El(left))) }

            val handlePart = Enumeratee.map[MatchInfo[Array[Byte]]](_.content).transform(readPart)

            Traversable.take[Array[Byte]](boundary.size - 2).transform(Iteratee.consume()).flatMap { firstBoundary =>

              Parsing.search(boundary) transform Iteratee.repeat {

                takeUpToBoundary.transform(handlePart).flatMap { part =>
                  Enumeratee.take(1)(Iteratee.ignore[MatchInfo[Array[Byte]]]).map(_ => part)
                }

              }.map(parts => Right(parts.dropRight(1)))

            }

          }

        }.getOrElse(parse.error(createBadResult("Missing boundary header")(request)))

      }

      type PartHandler[A] = PartialFunction[Map[String, String], Iteratee[Array[Byte], A]]

      def handleFilePartAsTemporaryFile: PartHandler[FilePart[TemporaryFile]] = {
        handleFilePart {
          case FileInfo(partName, filename, contentType) =>
            val tempFile = TemporaryFile("multipartBody", "asTemporaryFile")
            import play.core.Execution.Implicits.internalContext
            Iteratee.fold[Array[Byte], FileOutputStream](new java.io.FileOutputStream(tempFile.file)) { (os, data) =>
              os.write(data)
              os
            }.map { os =>
              os.close()
              tempFile
            }
        }
      }

      case class FileInfo(partName: String, fileName: String, contentType: Option[String])

      object FileInfoMatcher {

        private def split(str: String) = {
          var buffer = new StringBuffer
          var escape: Boolean = false
          var quote: Boolean = false
          val result = new ListBuffer[String]

          def addPart() = {
            result += buffer.toString().trim
            buffer = new StringBuffer
          }

          str foreach { c =>
            c match {
              case '\\' =>
                buffer.append(c)
                escape = true
              case '"' =>
                buffer.append(c)
                if (!escape)
                  quote = !quote
                escape = false
              case ';' =>
                if (!quote) {
                  addPart
                } else {
                  buffer.append(c)
                }
                escape = false
              case _ =>
                buffer.append(c)
                escape = false
            }
          }

          addPart
          result.toList
        }

        def unapply(headers: Map[String, String]): Option[(String, String, Option[String])] = {

          val keyValue = """^([a-zA-Z_0-9]+)="(.*)"$""".r

          for {
            value <- headers.get("content-disposition")

            values = split(value).map(_.trim).map {
              // unescape escaped quotes
              case keyValue(key, value) => (key.trim, value.trim.replaceAll("""\\"""", "\""))
              case key => (key.trim, "")
            }.toMap

            _ <- values.get("form-data");

            partName <- values.get("name");

            fileName <- values.get("filename");

            contentType = headers.get("content-type")

          } yield ((partName, fileName, contentType))
        }
      }

      def handleFilePart[A](handler: FileInfo => Iteratee[Array[Byte], A]): PartHandler[FilePart[A]] = {
        case FileInfoMatcher(partName, fileName, contentType) =>
          val safeFileName = fileName.split('\\').takeRight(1).mkString
          import play.api.libs.iteratee.Execution.Implicits.trampoline
          handler(FileInfo(partName, safeFileName, contentType)).map(a => FilePart(partName, safeFileName, contentType, a))
      }

      object PartInfoMatcher {

        def unapply(headers: Map[String, String]): Option[String] = {

          val keyValue = """^([a-zA-Z_0-9]+)="(.*)"$""".r

          for {
            value <- headers.get("content-disposition")

            values = value.split(";").map(_.trim).map {
              case keyValue(key, value) => (key.trim, value.trim)
              case key => (key.trim, "")
            }.toMap

            _ <- values.get("form-data");

            partName <- values.get("name")

          } yield (partName)
        }
      }

      def handleDataPart: PartHandler[Part] = {
        case headers @ PartInfoMatcher(partName) if !FileInfoMatcher.unapply(headers).isDefined =>
          import play.api.libs.iteratee.Execution.Implicits.trampoline
          Traversable.takeUpTo[Array[Byte]](DEFAULT_MAX_TEXT_LENGTH)
            .transform(Iteratee.consume[Array[Byte]]().map(bytes => DataPart(partName, new String(bytes, "utf-8"))))
            .flatMap { data =>
              Cont({
                case Input.El(_) => Done(MaxDataPartSizeExceeded(partName), Input.Empty)
                case in => Done(data, in)
              })
            }
      }

      def handlePart(fileHandler: PartHandler[FilePart[File]]): PartHandler[Part] = {
        handleDataPart
          .orElse({ case FileInfoMatcher(partName, fileName, _) if fileName.trim.isEmpty => Done(MissingFilePart(partName), Input.Empty) }: PartHandler[Part])
          .orElse(fileHandler)
          .orElse({ case headers => Done(BadPart(headers), Input.Empty) })
      }

    }

    // -- Parsing utilities

    /**
     * Wrap an existing BodyParser with a maxLength constraints.
     *
     * @param maxLength The max length allowed
     * @param parser The BodyParser to wrap
     */
    def maxLength[A](maxLength: Int, parser: BodyParser[A]): BodyParser[Either[MaxSizeExceeded, A]] = BodyParser("maxLength=" + maxLength + ", wrapping=" + parser.toString) { request =>
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      Traversable.takeUpTo[Array[Byte]](maxLength).transform(parser(request)).flatMap(Iteratee.eofOrElse(MaxSizeExceeded(maxLength))).map {
        case Right(Right(result)) => Right(Right(result))
        case Right(Left(badRequest)) => Left(badRequest)
        case Left(maxSizeExceeded) => Right(Left(maxSizeExceeded))
      }
    }

    /**
     * A body parser that always returns an error.
     */
    def error[A](result: Future[Result]): BodyParser[A] = BodyParser("error, result=" + result) { request =>
      import play.api.libs.iteratee.Execution.Implicits.trampoline
      Iteratee.flatten(result.map(r => Done(Left(r), Empty)))
    }

    /**
     * Allow to choose the right BodyParser parser to use by examining the request headers.
     */
    def using[A](f: RequestHeader => BodyParser[A]) = BodyParser { request =>
      f(request)(request)
    }

    /**
     * Create a conditional BodyParser.
     */
    def when[A](predicate: RequestHeader => Boolean, parser: BodyParser[A], badResult: RequestHeader => Future[Result]): BodyParser[A] = {
      BodyParser("conditional, wrapping=" + parser.toString) { request =>
        if (predicate(request)) {
          parser(request)
        } else {
          import play.api.libs.iteratee.Execution.Implicits.trampoline
          Iteratee.flatten(badResult(request).map(result => Done(Left(result), Empty)))
        }
      }
    }

    private def createBadResult(msg: String): RequestHeader => Future[Result] = { request =>
      Play.maybeApplication.map(_.global.onBadRequest(request, msg))
        .getOrElse(Future.successful(Results.BadRequest))
    }

    private def tolerantBodyParser[A](name: String, maxLength: Int, errorMessage: String)(parser: (RequestHeader, Array[Byte]) => A): BodyParser[A] =
      BodyParser(name + ", maxLength=" + maxLength) { request =>
        import play.api.libs.iteratee.Execution.Implicits.trampoline
        import scala.util.control.Exception._

        val bodyParser: Iteratee[Array[Byte], Either[Result, Either[Future[Result], A]]] =
          Traversable.takeUpTo[Array[Byte]](maxLength).transform(
            Iteratee.consume[Array[Byte]]().map { bytes =>
              allCatch[A].either {
                parser(request, bytes)
              }.left.map {
                case NonFatal(e) =>
                  Play.logger.debug(errorMessage, e)
                  createBadResult(errorMessage)(request)
                case t => throw t
              }
            }
          ).flatMap(Iteratee.eofOrElse(Results.EntityTooLarge))

        bodyParser.mapM {
          case Left(tooLarge) => Future.successful(Left(tooLarge))
          case Right(Left(badResult)) => badResult.map(Left.apply)
          case Right(Right(body)) => Future.successful(Right(body))
        }
      }
  }

}

/**
 * Defaults BodyParsers.
 */
object BodyParsers extends BodyParsers

/**
 * Signal a max content size exceeded
 */
case class MaxSizeExceeded(length: Int)

Other Play Framework source code examples

Here is a short list of links related to this Play Framework ContentTypes.scala source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.