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
.
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.
Content type |
Parser |
Parsed to Scala type |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18.222.164.141