Content negotiation is the process of selecting the best representation for a given response when there are multiple representations available.
It can either be server-driven or agent-driven or a combination of both, which is called transparent negotiation. Play provides support for server-driven negotiations. This is handled by the rendering trait and is extended by the controller trait. The controller trait is the one where the controller objects in a Play app extend.
Let's look at the Rendering
trait:
trait Rendering { object render { //Tries to render the most acceptable result according to the request's Accept header value. def apply(f: PartialFunction[MediaRange, Result])(implicit request: RequestHeader): Result = { def _render(ms: Seq[MediaRange]): Result = ms match { case Nil => NotAcceptable case Seq(m, ms @ _*) => f.applyOrElse(m, (m: MediaRange) => _render(ms)) } // "If no Accept header field is present, then it is assumed that the client accepts all media types." val result = if (request.acceptedTypes.isEmpty) _render(Seq(new MediaRange("*", "*", Nil, None, Nil))) else _render(request.acceptedTypes) result.withHeaders(VARY -> ACCEPT) } /**Tries to render the most acceptable result according to the request's Accept header value. * This function can be used if you want to do asynchronous processing in your render function. */ def async(f: PartialFunction[MediaRange, Future[Result]])(implicit request: RequestHeader): Future[Result] = { def _render(ms: Seq[MediaRange]): Future[Result] = ms match { case Nil => Future.successful(NotAcceptable) case Seq(m, ms @ _*) => f.applyOrElse(m, (m: MediaRange) => _render(ms)) } // "If no Accept header field is present, then it is assumed that the client accepts all media types." val result = if (request.acceptedTypes.isEmpty) _render(Seq(new MediaRange("*", "*", Nil, None, Nil))) else _render(request.acceptedTypes) result.map(_.withHeaders(VARY -> ACCEPT)) } } }
The _render
method defined in the apply
method calls the partial f
function on the accept headers in the request. If f
is not defined for the any of the accept headers, a response with status code 406 is forwarded. If it's not, the result of f
for the first accept header for which f
is defined, is returned.
Since the controller extends the rendering trait, we can use the render object within our Action definition. For example, we might have an Action, which gets the configuration in JSON and XML after reading it from a file with an XML format, depending on the accept headers in the request. Let's see how this is done:
def getConfig = Action { implicit request => val xmlResponse: Node = <metadata> <company>TinySensors</company> <batch>md2907</batch> </metadata> val jsonResponse = Json.obj("metadata" -> Json.arr( Json.obj("company" -> "TinySensors"), Json.obj("batch" -> "md2907")) ) render { case Accepts.Xml() => Ok(xmlResponse) case Accepts.Json() => Ok(jsonResponse) } }
In this snippet, Accepts.Xml()
and Accepts.Json()
are Play's helper methods that check to see if the request accepts the response of the application/xml
and applicat
ion/json
types, respectively. There are currently four predefined accepts and these are tabulated here:
Request accept helper |
Accept header value |
---|---|
XML |
|
JSON |
|
HTML |
|
JavaScript |
|
This is facilitated by the RequestExtractors
trait and the AcceptExtractors
trait. RequestExtractors
is also extended by the controller trait. Let's look at the extractor traits here:
trait RequestExtractors extends AcceptExtractors { //Convenient extractor allowing to apply two extractors. object & { def unapply(request: RequestHeader): Option[(RequestHeader, RequestHeader)] = Some((request, request)) } } //Define a set of extractors allowing to pattern match on the Accept HTTP header of a request trait AcceptExtractors { //Common extractors to check if a request accepts JSON, Html, etc. object Accepts { import play.api.http.MimeTypes val Json = Accepting(MimeTypes.JSON) val Html = Accepting(MimeTypes.HTML) val Xml = Accepting(MimeTypes.XML) val JavaScript = Accepting(MimeTypes.JAVASCRIPT) } } //Convenient class to generate extractors checking if a given mime type matches the Accept header of a request. case class Accepting(val mimeType: String) { def unapply(request: RequestHeader): Boolean = request.accepts(mimeType) def unapply(mediaRange: play.api.http.MediaRange): Boolean = mediaRange.accepts(mimeType) }
From this code, all that we need to define a custom accepts is the value we would expect the request's accept header to have. For example, to define a helper for image/png
, we use this code:
val AcceptsPNG = Accepting("image/png")
We also notice that
RequestExtractors
has an &
object, and we can use this when we wish to send the same response to multiple accept types. So, in the getConfig
method shown in the preceding code, if the same response response is sent for application/json
and text/javascript
, we will modify it as follows:
def fooBar = Action { implicit request => val xmlResponse: Node = <metadata> <company>TinySensors</company> <batch>md2907</batch> </metadata> val jsonResponse = Json.obj("metadata" -> Json.arr( Json.obj("company" -> "TinySensors"), Json.obj("batch" -> "md2907")) ) render { case Accepts.Xml() => Ok(xmlResponse) case Accepts.Json() & Accepts.JavaScript() => Ok(jsonResponse) } }
The render
object can be used similarly when defining an asynchronous Action.
3.145.35.247