HowTo do Web Services
From Lift
Building Web Services in lift
- Steve Jenson
- http://saladwithsteve.com
- http://twitter.com/stevej
- What makes it so easy?
- Pattern Matching
- Higher-Order Functions
- Scala’s XML support
- If the first two have not already been explained then this won’t go well.
Contents |
Who am I?
* Spent too many years working on Web Services. * Blogger.com, a pioneer of web services in the blogging world. * Used XML-RPC extensively. * Wrote a simple Document Literal SOAP stack at Google. * Was in the Atom IETF Working Group. * Built Google’s first REST API. * lift is the easiest framework I’ve used for building Web Services.
Let’s start
Start with a Simple Class
package com.hellolift.api
import net.liftweb.http._
import com.hellolift.model._
import scala.xml._
class BlogAPI(val request: RequestState)
extends SimpleController {
def index: ResponseIt = {
// To be filled in.
}
def get(id: String): ResponseIt = {
// To be filled in.
}
def create: ResponseIt = {
// To be filled in.
}
def delete(id: String): ResponseIt = {
// To be filled in.
}
}
GET an existing Item
* GET /api/$itemid * Route a request to the proper method via pattern matching
def index: ResponseIt {
request match {
case RequestState(ParsePath("api" :: itemid :: Nil,
_, _), GetRequest, _, _) => get(itemid)
}
}
def get(id: String): ResponseIt = {
val it = Item.find(By(Item.id, Helpers.toLong(id)),
By(Item.author, user)
it match {
case Full(item) => AtomResponse(item.toAtom())
case Empty => NotFoundResponse()
}
}
Outputting XML
* scala.xml makes it really easy to output valid XML. * The following is in our Item model object.
def toAtom = {
val id = "http://example.com/api/" + this.id
val formatter = new
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
val updated = formatter.format(this.lastedited.is)
<entry xmlns="http://www.w3.org/2005/Atom">
<id>{id}</id>
<updated>{updated}</updated>
<author>
<name>{name}</name>
</author>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
{body}
</div>
</content>
</entry>
}
- You can just use xml.
- Much easier on the eyes
- Just as correct as DOM.
Create a New Item (Part 1)
* Longer code snippet so this is only the pattern matching * POST /api/create * Match against PostRequest
// the previous pattern matching clause is assumed
// (for space reasons)
def index: ResponseIt = {
request match {
case RequestState(ParsePath("api" :: "create"
:: Nil, _, _), PostRequest, _, _) => create
}
}
Create a new Item (Part 2)
* POST /api/create
def create: ResponseIt = {
try {
val xml = XML.load(httpRequest.getInputStream())
val body = (xml \\ "content").text
val item = Item.create.author(user).body(body)
item.save
AtomCreatedResponse(item.toAtom)
} catch {
case e: Exception => BadResponse()
}
}
* \\ searches the children for the element. * You’ve probably noticed that we are lacking authentication. * Are you wondering where user coming from? * All will be revealed!
Delete an Item
* DELETE /api/$itemid
def index: ResponseIt = {
request match {
case RequestState(ParsePath("api" :: itemid ::
Nil, _, _), DeleteRequest, _, _)
=> delete(itemid)
}
}
def delete(itemid: String): ResponseIt = {
Item.find(By(Item.id, Helpers.toLong(itemid)),
By(Item.author, user)) match {
case Full(item) => {
item.delete_!
OkResponse()
}
case _ => NotFoundResponse()
}
}
* I bet you’re getting the hang of this now.
Routing Requests to BlogAPI
* How do requests get dispatched to BlogAPI? * Dispatching happens in Boot.scala
class Boot {
def boot {
val apiDispatcher: LiftRules.DispatchPf = {
case RequestMatcher(r @ RequestState("api" ::
_ :: Nil, _) ,_) => api(r, "index")
}
LiftRules.statelessDispatchTable =
apiDispatcher orElse LiftRules.statelessDispatchTable
}
// this doesn't format well for presentations.
private def api
(request: RequestState, methodName: String)
(req: RequestState): Can[ResponseIt] =
createInvoker(methodName,
new BlogAPI(request)).flatMap(_() match {
case Full(ret: ResponseIt) => Full(ret)
case _ => Empty
})
}
* The api method is really just boilerplate and we should make it go away in lift so you don’t have to think about it. * statlessDispatchTable keeps your api requests from incurring session costs.
Authenticating (Part 1)
* The Question of the Mystery user is Answered * We want each Request to be Authenticated * Higher-Order Functions to the Rescue!
// getUser is on the next slide
def authenticated(f: User => ResponseIt) = {
getUser match {
case Full(user) => f(user)
case Empty => UnauthorizedResponse("Our Realm")
}
}
// new and improved
def get(id: String): ResponseIt {
authenticated { user => {
val it = Item.find(By(Item.id, Helpers.toLong(id)),
By(Item.author, user)
it match {
case Full(item) => AtomResponse(item.toAtom())
case Empty => NotFoundResponse()
}
}
}
* authenticated takes a function that takes a User and returns
a ResponseIt.
Authenticating (Part 2): getUser
* We read in the HTTP Authorization header and find the proper User based on it’s contents
private def getUser: Can[User] = {
Can.legacyNullTest(request.request
.getHeader(”Authorization”)).flatMap(auth => {
val up: List[String] = if (auth.length > 5) {
new String(Base64.decodeBase64(auth.substring(5,
auth.length).getBytes())).split(”:”).toList
} else {
Nil
}
if (up.length Full(user)
case _ => Empty
}
}
})}
* Again, this is mostly boilerplate, will soon be in lift. * Base64 is from Apache Commons Codec.
Thank you
* Any questions?

