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.