Request body parsers

Consider the most common POST request in any application—the request sent for logins. Will it be sufficient if the request body has the user's credentials in, say, a JSON or XML format? Will the request handler be able to extract this data and process it directly? No, since the data in the request has to be understood by the application code, it must be translated into a compatible type. For example, XML sent in a request must be translated to Scala XML for a Scala application.

There are several libraries, such as Jackson, XStream, and so on, which can be used to achieve this task, but we wouldn't need them as Play supports this internally. Play provides request body parsers to transform the request body into equivalent Scala objects for some of the frequently used content types. In addition to this, we can extend existing parsers or define new ones.

Every Action has a parser. How do I know this ? Well, the Action object, which we used to define how our app should respond, is simply an extension of the Action trait, and is defined as follows:

 trait Action[A] extends EssentialAction {   

  //Type of the request body. 
  type BODY_CONTENT = A 

  //Body parser associated with this action. 
  def parser: BodyParser[A] 

  //Invokes this action 
  def apply(request: Request[A]): Future[Result] 

       def apply(rh: RequestHeader): Iteratee[Array[Byte], Result] = parser(rh).mapM {
          case Left(r) => 
      
      Future.successful(r)
    case Right(a) => 
      val request = Request(rh, a) 
      
      Play.maybeApplication.map { app => 
        play.utils.Threads.withContextClassLoader(app.classloader) {
          apply(request)
         }
      }.getOrElse {
         apply(request)
      }
  }(executionContext)

  //The execution context to run this action in 
  def executionContext: ExecutionContext = play.api.libs.concurrent.Execution.defaultContext 

  //Returns itself, for better support in the routes file. 
  override def apply(): Action[A] = this 

  override def toString = { 
    "Action(parser="+ parser + ")" 
  } 
 
}

The apply method transforms the value returned by the parser. The value from the parser can either be a result or the request body (denoted as Either[Result,A]).

Therefore, the transformation is defined for both possible outcomes. If we pattern-match this, we get Left(r), which is a result type and Right(a), which is the request body.

The mapM method functions similarly to the map method, the only difference being, it does so asynchronously.

However, can Actions be defined even without a parser? Yes and no.

Let's look at an example Action: a POST request, which is required to subscribe to updates. This request takes the user's e-mail ID as a query parameter, which means that we will need to access the request body in order to complete the subscription for this user. First, we'll check what the request body looks like when we do not specify a parser. Create an Action subscribe in a controller, as shown here:

def subscribe = Action { 
    request => 
        Ok("received " + request.body) 
  } 

Now, add an entry for this in the routes file:

POST    /subscribe         controllers.AppController.subscribe 

After this, run the application. Send a POST request at http://localhost:9000/subscribe with the [email protected] e-mail ID using a REST client or Curl (whichever you are more comfortable with).

For example:

curl 'http://localhost:9000/subscribe' -H 'Content-Type: text/plain;charset=UTF-8' --data-binary '[email protected]' 

The response for this request will be as follows:

received AnyContentAsText([email protected])

Did you notice that our subscribe method understood that the content was text? The request body was translated as AnyContentAsText([email protected]). How did our method determine this? Isn't this the job of a parser mapped to a particular Action?

When a parser is not specified for an Action, the parser returned by the BodyParsers.parse.anyContent method is set as the parser for this Action. This is handled by the ActionBuilder, which we will see later in this chapter. The following code snippet shows one of the methods to generate an Action when no parser is given:

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

Now, let's examine what the BodyParsers.parse.anyContent method does:

def anyContent: BodyParser[ AnyContent] = BodyParser("anyContent") { request => 
      import play.api.libs.iteratee.Execution.Implicits.trampoline 
      request.contentType.map(_.toLowerCase(Locale.ENGLISH)) match { 
        case _ if request.method == HttpVerbs.GET || request.method == HttpVerbs.HEAD => { 
          Play.logger.trace("Parsing AnyContent as empty") 
          empty(request).map(_.right.map(_ => AnyContentAsEmpty)) 
        } 
        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))) 
           }
      }
     }

First of all, it checks whether the request type supports sending data along with the request. If not, it returns AnyContentAsEmpty (you can check this by changing the request type to GET in the routes file and sending a GET request), else it compares the content type Header of the request with the supported types. If a match is found, it transforms the data into the corresponding type and returns that, or else it parses it as bytes and returns play.api.mvc.RawBuffer.

Note

AnyContentAsEmpty, AnyContentAsText, AnyContentAsXml, AnyContentAsJson, AnyContentAsFormUrlEncoded, AnyContentAsMultipartFormData, and AnyContentAsRaw all extend the trait AnyContent.

So, when an Action is defined for one of the supported content types or when it's a GET/HEAD request, we need not mention the parser.

Let's see how we can access the request body in our Action. We can now updating our subscrib e method:

def subscribe = Action {
    request => 
      val reqBody: AnyContent = request.body 
      val textContent: Option[String] = reqBody.asText 
      textContent.map { 
        emailId => 
          Ok("added " + emailId + " to subscriber's list") 
      }.getOrElse { 
        BadRequest("improper request body")
      } 
  } 

In order to access the data in the request body, we need to convert it from AnyContent to Option[String] using the asText method. This would become more concise if we added the parser in the Action definition:

def subscribe = Action(parse.text) { 
    request => 
      Ok("added " + request.body + " to subscriber's list") 
  } 

The urlFormEncoded text XML parsers return standard Scala objects while the others return Play objects.

We can assume that the subscription request takes a JSON in this format:

{"emailId": "[email protected]", " interval": "month"} 

Now, we will need to modify our subscribe method to def subscribe = Action(parse.json) {, as shown here:

       request => 
      val reqData: JsValue = request.body 
      val emailId = (reqData  "emailId").as[String] 
      val interval = (reqData  "interval").as[String] 
      Ok(s"added $emailId to subscriber's list and will send updates every $interval") 
  } 

For the following request:

curl 'http://localhost:9000/subscribe' -H 'Content-Type: text/json' --data-binary '{"emailId": "[email protected]", "interval": "month"}' 

We get a response as follows:

Added [email protected] to the subscriber's list and will send updates every month

The parse.json transforms the request body to play.api.libs.json.JsValue. The operator is used to access the value of a particular key. Similarly, there is a \ operator, which can be for the value of a key, though it may not be a direct child of the current node. Play-Json has several methods that simplify the handling of data in a JSON format, such as modifying the structure, or converting it to Scala models, and so on. Play-Json is also available as a stand-alone library to enable its usage in non-Play projects. Its documentation is available at https://www.playframework.com/documentation/2.3.x/ScalaJson.

Now, let's see how to write an Action to add a new user, which takes a request of content-type multipart:

import java.io.File 
 
  def createProfile = Action(parse.multipartFormData) {
    request => 
      val formData = request.body.asFormUrlEncoded
      val email: String = formData.get("email").get(0)
      val name: String = formData.get("name").get(0)
      val userId: Long = User(email, name).save
      request.body.file("displayPic").map {
        picture => 
          val path = "/socialize/user/" 
          if (!picture.filename.isEmpty) {
            picture.ref.moveTo(new File(path + userId + ".jpeg"))
          }
          Ok("successfully added user")
      }.getOrElse {
           BadRequest("failed to add user")
      }
  }

The request has three fields: email, name, and displayPic. From the request data, we fetch the e-mail of, name, and add a new user. The User. save method adds an entry in the user table and throws an error if a user with the same e-mail ID exists. This is why the operations in the file are performed only after adding a user. The displayPic is optional; therefore, the check for its length to be greater than zero is made prior to saving the image.

Note

It is better to complete the data transactions before the file-related ones, since they may fail and file-related operations might not be required for the incorrect request. The following table shows the supported content-types, parsers, and their default conversions.

Content type

Parser

Parsed to Scala type

text/plain

text

String

application/json or text/json

json

play.api.libs.json.JsValue

application/xml, text/xml, or application/XXX+xml

xml

NodeSeq

application/form-url-encoded

urlFormEncoded

Map[String, Seq[String]]

multipart/form-data

multipartFormData

play.api.mvc.MultipartFormData[TemporaryFile]

other

raw

Play.api.mvc.RawBuffer

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.5.201