HowTo do Web Services

From Lift

Jump to: navigation, search

Building Web Services in lift

  • 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?
Personal tools