Lift and Content Negotiation

Overview

This is a follow up of my previous post on Lift and REST discussing the aspects of URI matching and content negotiation. Lift supports Accept header of HTTP by responding back with a representation in accordance with the media type provided in the header. However, it currently doesn’t support quality factor (q parameter) of the Accept header, out of the box. Here  I will attempt to provide an approach to provide that support and along the way let’s explore another compelling feature of the Lift’s REST support.

HTTP Accept Header

Check RFC 2616 for detailed explanation on Accept and other HTTP headers. Let’s go over this based on an example: If a client sends an Accept header something like the following —

Accept: application/xml; q=0.8, application/json

is interpreted as: I prefer JSON representation but if you don’t have it XML is my second choice.

Quality factor or q parameter is the one that’s used to specify the preference. q is a decimal ranging from 0.0 to 1.0 and is delimited with a semi-colon (;) following the media mime type. If no q parameter is specified it defaults to value of 1.0, indicating first among the options provided.

Approach

Before going into the approach for supporting the q parameter (for your Lift-based application) let’s get into one of the things that you definitely want to see in a web framework: decouple business logic from the representation. Lift doesn’t disappoint you in this area. In my case I was using the same business logic, authorizing the user and making the database lookups, returning the appropriate representation independent of the business logic.

serveJx in RestHelper is there precisely for that purpose. Following code provides the URI matching rule (matches /api/user/{user_id} for GET requests) and returns an object that’s of trait Convertable. Also, define an implicit def that converts the object to the appropriate representation (XML or JSON, in this case).

objectUserManagementServiceextendsRestHelper{

….

serveJx{

caseGet(“api”::“user”::id::_,_)=>Full(loadUser(id))

}

}

traitConvertable{

deftoXml:Node

deftoJson:JValue

}

implicitdefcvt:JxCvtPF[Convertable]={

case(JsonSelect,c,_)=>c.toJson

case(XmlSelect,c,_)=>c.toXml

}

view rawgistfile4.scalaThis Gist brought to you by GitHub.

Relevant pieces of case class User is provided below. If you are familiar with Squeryl you may have already identified the annotations provided for the constructor arguments otherwise don’t worry about it; Squeryl is an excellent Scala-based ORM (I like Squeryl quite a bit, that’s a topic for another blog post!). User implements Convertable, meaning the two representations — XML and JSON via toXml and toJson functions respectively.

caseclassUser(@Column(“first_name”)valfirstName:String,@Column(“last_name”)vallastName:String,valemail:String)

extendsUserManagementDBwithConvertable{

….

deftoXml():Node={

<user>

<id>{this.id}</id>

<firstName>{this.firstName}</firstName>

<lastName>{this.lastName}</lastName>

<email>{this.email}</email>

</user>

}

deftoJson():JValue={

Extraction.decompose(this)

}

}

view rawUser.scalaThis Gist brought to you by GitHub.

So with all the above in place, the following request would result in an XML or JSON response based on the Accept header. The logic for identifying the appropriate representation  is in the RestHelper‘s implicit def jxSel shown below. As RestHelper does not support q parameter out of the box, UserManagementService extended from RestHelper changes the behavior by overriding jxSel.

traitRestHelperextendsLiftRules.DispatchPF{

/**

* A function that chooses JSON or XML based on the request..

* Use with serveType

*/

implicitdefjxSel(req:Req):Box[JsonXmlSelect]=

if(jsonResponse_?(req))Full(JsonSelect)

elseif(xmlResponse_?(req))Full(XmlSelect)

elseNone

}

—————————————————————-

objectUserManagementServiceextendsRestHelper{

overrideimplicitdefjxSel(req:Req):Box[JsonXmlSelect]={

valprefs=ClientMediaPreferenceorderedList(req.headers(“accept”))

valpreferredMediaType=prefs(0).mediaType+“/”+prefs(0).mediaSubType

if(preferredMediaType==“text/xml”||preferredMediaType==“application/xml”)Full(XmlSelect)

elseif(preferredMediaType==“application/json”)Full(JsonSelect)

elseNone

}

}

view rawRestHelper.scalaThis Gist brought to you by GitHub.

Actual Parsing: Parsing of the Accept header and determining the representation is done using functions in the ClientMediaPreference object. Instead of embedding the code in this already lengthy post, here is a link to the gist covering the parsing logic.

Conclusion

There are a couple of areas that I still have to tighten-up the code, but that’s the general idea. One of the Todo items is to send a 406 Not Acceptable response if the representation that a client asks for is not implemented by the server.

Before closing, let’s continue checking off another item from Tilkov’s litmus test (as we did in the last post) …

Can I easily use the same business logic while returning different content types in the response?

Answer: Yes, the framework is flexible in this aspect.

Tags: