Building Web Services in lift

Steve Jenson

stevej@pobox.com

http://saladwithsteve.com

http://twitter.com/stevej

What makes it so easy?

[any material that should appear in print but not on the slide]

Who am I?

Let's start

GET an existing Item


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

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>
}

Create a New Item (Part 1)



// 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)


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()
  }
}

Delete an Item


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()
  }
}

Routing Requests to BlogAPI

class Boot {
  def boot {
    val apiDispatcher: LiftRules.DispatchPf = {
      case RequestMatcher(r, ParsePath("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
    })
}

Authenticating (Part 1)

// 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()
    }
  }
}

Authenticating (Part 2): getUser

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 < 2) {
      Empty
    } else {
      val email = up(0)
      val password = up(1)	
      User.find(By(User.email, email)) match {
        case Full(user) if user.validated && 
          user.password.match_?(password) => Full(user)
        case _ => Empty
      }
    }
})}

Thank you