Simply Lift by David Pollak - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

12

"qnty":4

13

}

The XML example is pretty much the same, except we coerse the response to Box[Node] which

RestHelper converts into an XmlResponse:

1

case "simple3" :: "item" :: itemId :: Nil XmlGet _ =>

2

for {

3

item <- Item.find(itemId) ?~ "Item Not Found"

4

} yield item: Node

Which results in the following:

1

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -i -H "Accept: application/xml" http://localhost:8080/simple3/item/1234

2

HTTP/1.1 200 OK

3

Expires: Wed, 9 Mar 2011 01:48:38 UTC

4

Content-Length: 230

5

Cache-Control: no-cache; private; no-store

6

Content-Type: text/xml; charset=utf-8

7

Pragma: no-cache

8

Date: Wed, 9 Mar 2011 01:48:38 UTC

9

X-Lift-Version: Unknown Lift Version

10

Server: Jetty(6.1.22)

11

12

<?xml version="1.0" encoding="UTF-8"?>

13

<item>

14

<id>1234</id>

15

<name>Cat Food</name>

16

<description>Yummy, tasty cat food</description>

17

<price>4.25</price>

18

<taxable>true</taxable>

19

<weightInGrams>1000</weightInGrams>

20

<qnty>4</qnty>

21

</item>

5.3. MAKING IT EASIER WITH RESTHELPER

55

Okay... that’s simpler because we define stuff in the serve block and the conversions from

JValue and Node to the right response types is taken care of. Just to be explicit about where

the implicit conversions are defined, they’re in the Item singleton:

1

/**

2

* Convert an item to XML

3

*/

4

implicit def toXml(item: Item): Node =

5

<item>{Xml.toXml(item)}</item>

6

7

8

/**

9

* Convert the item to JSON format. This is

10

* implicit and in the companion object, so

11

* an Item can be returned easily from a JSON call

12

*/

13

implicit def toJson(item: Item): JValue =

14

Extraction.decompose(item)

Okay, so, yippee skippy, we can do simpler REST. Let’s keep looking at examples of how we can

make it even simpler. This example uses extractors rather than doing the explicit Item.find:

1

serve {

2

// Prefix notation

3

case JsonGet("simple4" :: "item" :: Item(item) :: Nil, _) =>

4

// no need to explicitly create a LiftResponse

5

// Just make it JSON and RestHelper does the rest

6

item: JValue

7

8

// infix notation

9

case "simple4" :: "item" :: Item(item) :: Nil XmlGet _ =>

10

item: Node

11

}

If you like DRY and don’t want to keep repeating the same path prefixes, you can use prefix, for

example:

1

// serve a bunch of items given a single prefix

2

serve ( "simple5" / "item" prefix {

3

// all the inventory

4

case Nil JsonGet _ => Item.inventoryItems: JValue

5

case Nil XmlGet _ => Item.inventoryItems: Node

6

7

// a particular item

8

case Item(item) :: Nil JsonGet _ => item: JValue

9

case Item(item) :: Nil XmlGet _ => item: Node

10

})

The above code will list all the items in response to /simple5/item and will serve a specific item

in response to /simple5/item/1234, as we see in:

1

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item

2

[{

56

CHAPTER 5. HTTP AND REST

3

"id":"1234",

4

"name":"Cat Food",

5

"description":"Yummy, tasty cat food",

6

"price":4.25,

7

"taxable":true,

8

"weightInGrams":1000,

9

"qnty":4

10

},

11

...

12

,{

13

"id":"1237",

14

"name":"Sloth Food",

15

"description":"Slow, slow sloth food",

16

"price":18.33,

17

"taxable":true,

18

"weightInGrams":750,

19

"qnty":62

20

}]

21

22

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple5/item/1237

23

{

24

"id":"1237",

25

"name":"Sloth Food",

26

"description":"Slow, slow sloth food",

27

"price":18.33,

28

"taxable":true,

29

"weightInGrams":750,

30

"qnty":62

31

}

In the above examples, we’ve explicitly coersed the results into a JValue or Node depending

on the request type. With Lift, it’s possible to define a conversion from a given type to response

types (the default response types are JSON and XML) based on the request type and then define

the request patterns to match and RestHelper takes care of the rest (so to speak.) Let’s define

the conversion from Item to JValue and Node (note the implicit keyword, that says that the

conversion is available to serveJx statements:

1

implicit def itemToResponseByAccepts: JxCvtPF[Item] = {

2

case (JsonSelect, c, _) => c: JValue

3

case (XmlSelect, c, _) => c: Node

4

}

This is pretty straight forward. If it’s a JsonSelect, return a JValue and if it’s an XmlSelect,

convert to a Node.

This is used in the serveJx statement:

1

serveJx[Item] {

2

case "simple6" :: "item" :: Item(item) :: Nil Get _ => item

3

case "simple6" :: "item" :: "other" :: item :: Nil Get _ =>

4

Item.find(item) ?~ "The item you're looking for isn't here"

5

}

5.4. A COMPLETE REST EXAMPLE

57

So /simple6/item/1234 will match and result in an Item being returned and based on the

above implicit conversion, we turn the Item into a JValue or Node depending on the Accepts

header and then convert that to a () => Box[LiftResponse]. Let’s see what curl has to say

about it:

1

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl http://localhost:8080/simple6/item/1237

2

{

3

"id":"1237",

4

"name":"Sloth Food",

5

"description":"Slow, slow sloth food",

6

"price":18.33,

7

"taxable":true,

8

"weightInGrams":750,

9

"qnty":62

10

}

11

12

dpp@raptor:~/proj/simply_lift/samples/http_rest$ curl -H "Accept: application/xml" http://localhost:8080/simple6/item/1234

13

<?xml version="1.0" encoding="UTF-8"?>

14

<item>

15

<id>1234</id>

16

<name>Cat Food</name>

17

<description>Yummy, tasty cat food</description>

18

<price>4.25</price>

19

<taxable>true</taxable>

20

<weightInGrams>1000</weightInGrams>

21

<qnty>4</qnty>

22

</item>

Note also that /simple6/item/other/1234 does the right thing. This is because the path is 4

elements long, so it won’t match the first part of the pattern, but does match the second part of the

pattern.

Finally, let’s combine serveJx and it’s DRY helper, prefixJx.

1

serveJx[Item] {

2

"simple7" / "item" prefixJx {

3

case Item(item) :: Nil Get _ => item

4

case "other" :: item :: Nil Get _ =>

5

Item.find(item) ?~ "The item you're looking for isn't here"

6

}

7

}

5.4

A complete REST example

The above code gives us the bits and pieces that we can combine into a full fledged REST service.

Let’s do that combination and see what such a service looks like:

Listing 5.4: FullRest.scala

1

package code

2

package lib

58

CHAPTER 5. HTTP AND REST

3

4

import model._

5

6

import net.liftweb._

7

import common._

8

import http._

9

import rest._

10

import util._

11

import Helpers._

12

import json._

13

import scala.xml._

14

15

/**

16

* A full REST example

17

*/

18

object FullRest extends RestHelper {

19

20

// Serve /api/item and friends

21

serve( "api" / "item" prefix {

22

23

// /api/item returns all the items

24

case Nil JsonGet _ => Item.inventoryItems: JValue

25

26

// /api/item/count gets the item count

27

case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)

28

29

// /api/item/item_id gets the specified item (or a 404)

30

case Item(item) :: Nil JsonGet _ => item: JValue

31

32

// /api/item/search/foo or /api/item/search?q=foo

33

case "search" :: q JsonGet _ =>

34

(for {

35

searchString <- q ::: S.params("q")

36

item <- Item.search(searchString)

37

} yield item).distinct: JValue

38

39

// DELETE the item in question

40

case Item(item) :: Nil JsonDelete _ =>

41

Item.delete(item.id).map(a => a: JValue)

42

43

// PUT adds the item if the JSON is parsable

44

case Nil JsonPut Item(item) -> _ => Item.add(item): JValue

45

46

// POST if we find the item, merge the fields from the

47

// the POST body and update the item

48

case Item(item) :: Nil JsonPost json -> _ =>

49

Item(mergeJson(item, json)).map(Item.add(_): JValue)

50

51

// Wait for a change to the Items

52

// But do it asynchronously

53

case "change" :: Nil JsonGet _ =>

54

RestContinuation.async {

55

satisfyRequest => {

56

// schedule a "Null" return if there's no other answer

5.4. A COMPLETE REST EXAMPLE

59

57

// after 110 seconds

58

Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)

59

60

// register for an "onChange" event. When it

61

// fires, return the changed item as a response

62

Item.onChange(item => satisfyRequest(item: JValue))

63

}

64

}

65

})

66

}

The whole service is JSON only and contained in a single serve block and uses the prefix helper

to define all the requests under /api/item as part of the service.

The first couple of patterns are a re-hash of what we’ve already covered:

1

// /api/item returns all the items

2

case Nil JsonGet _ => Item.inventoryItems: JValue

3

4

// /api/item/count gets the item count

5

case "count" :: Nil JsonGet _ => JInt(Item.inventoryItems.length)

6

7

// /api/item/item_id gets the specified item (or a 404)

8

case Item(item) :: Nil JsonGet _ => item: JValue

The next is a search feature at /api/item/search. Using a little Scala library fun, we create a

list of the request path elements that come after the search element and all the query parameters

named q. Based on these, we search for all the Items that match the search term. We wind up with

a List[Item] and we remove duplicates with distinct and finally coerse the List[Item] to

a JValue:

1

// /api/item/search/foo or /api/item/search?q=foo

2

case "search" :: q JsonGet _ =>

3

(for {

4

searchString <- q ::: S.params("q")

5

item <- Item.search(searchString)

6

} yield item).distinct: JValue

Next, let’s see how to delete an Item:

1

// DELETE the item in question

2

case Item(item) :: Nil JsonDelete _ =>

3

Item.delete(item.id).map(a => a: JValue)

The only real difference is we’re looking for a JsonDelete HTTP request.

Let’s see how we add an Item with a PUT:

1

// PUT adds the item if the JSON is parsable

2

case Nil JsonPut Item(item) -> _ => Item.add(item): JValue

Note the Item(item) -> _ after JsonPut.

The extraction signature for JsonPut is

(List[String], (JValue, Req)). The List[String] part is simple... it’s a List that

60

CHAPTER 5. HTTP AND REST

contains the request path. The second part of the Pair is a Pair itself that contains the JValue and

the underlying Req (in case you need to do something with the request itself). Because there’s

a def unapply(in:

JValue):

Option[Item] method in the Item singleton, we can ex-

tract (pattern match) the JValue that is built from the PUT request body. This means if the user

PUTs a JSON blob that can be turned into an Item the pattern will match and we’ll evaluate the

right hand side of the case statement which adds the Item to inventory. That’s a big ole dense pile

of information. So, we’ll try it again with POST.

1

case Item(item) :: Nil JsonPost json -> _ =>

2

Item(mergeJson(item, json)).map(Item.add(_): JValue)

In this case, we’re match a POST on /api/item/1234 that has some parsable JSON in the POST

body. The mergeJson method takes all the fields in the found Item and replaces them with any

of the fields in the JSON in the POST body. So a POST body of {"qnty":

123} would replace

the qnty field in the Item. The Item is then added back into the backing store.

Cool. So, we’ve got a variety of GET support in our REST service, a DELETE, PUT and POST. All

using the patterns that RestHelper gives us.

Now we have some fun.

One of the features of Lift’s HTML side is support for Comet (server push via long-polling.) If

the web container supports it, Lift will automatically use asynchronous support. That means that

during a long poll, while no computations are being performed related to the servicing of the

request, no threads will be consumed. This allows lots and lots of open long polling clients. Lift’s

REST support includes asynchronous support. In this case, we’ll demonstrate opening an HTTP

request to /api/item/change and wait for a change to the backing store. The request will be

satisfied with a change to the backing store or a JSON JNull after 110 seconds:

1

case "change" :: Nil JsonGet _ =>

2

RestContinuation.async {

3

satisfyRequest => {

4

// schedule a "Null" return if there's no other answer

5

// after 110 seconds

6

Schedule.schedule(() => satisfyRequest(JNull), 110 seconds)

7

8

// register for an "onChange" event. When it

9

// fires, return the changed item as a response

10

Item.onChange(item => satisfyRequest(item: JValue))

11

}

12

}

If we receive a GET request to /api/item/change, invoke RestContinuation.async. We

pass a closure that sets up the call. We set up the call by scheduling a JNull to be sent after 110

seconds. We also register a function which is invoked when the backing store is changed. When

either event (110 seconds elapses or the backing store changes), the functions will be invoked and

they will apply the satifyRequest function which will invoke the continuation and send the

response back to the client. Using this mechanism, you can create long polling services that do not

consume threads on the server. Note too that the satisfyRequest function is fire-once so you

can call it lots of times, but only the first time counts.

5.5. WRAP UP

61

5.5

Wrap Up

In this chapter, we’ve covered how you create web services in Lift. While there is a lot of implicit

conversion stuff going on under the covers in RestHelper, the resulting code is pretty easy to

read, create, and maintain. At the core, you match an incoming request against a pattern, if the

pattern matches, evaluate the expression on the right hand side of the pattern.

62

CHAPTER 5. HTTP AND REST

Chapter 6

Wiring

Interactive web applications have many interdependent components on a single web page. For

example (and this is the example we’ll use for this chapter), you may have a shopping cart in your

application. The shopping cart will contain items and quantities. As you add/remove items from

the cart, the cart should update, along with the sub-total, the tax, the shipping and the grand total.

Plus, the count of the items in the cart may be displayed on some pages without the cart contents.

Keeping track of all of these dependencies for all the different page layouts is pretty tough work.

When it comes to updating the site, the team must remember where all of the items are and how

to update them and if they get one wrong, the site looks broken.

Lift’s Wiring provides a simply solution to managing complex dependencies on a single page and

on multiple tabs. Lift’s Wiring allows you to declare the formulaic relationships among cells (like a

spreadsheet) and then the user interface components (yes, there can be more than one component)

associated with each cell. Lift will automatically update the dependent user interface components

based on change in the predicates. Lift will do this on initial page render and with each Ajax

or Comet update to the page. Put another way, Wiring is like a spreadsheet and the page will

automatically get updated when any of the predicate values change such that the change results

in a change in the display value.

6.1

Cells

Like a spreadsheet, Lift’s Wiring is based on Cells. Cells come in three types: ValueCell, Dy-

namicCell, and FuncCell.

A ValueCell contains a value that is entered by a user or depends on some user action. A

ValueCell may represent the items in our shopping cart or the tax rate.

A DynamicCell contains a value that changes every time the cell is accessed. For example, a

random number or the current time.

A FuncCell has a value based on a formula applied to the value or other cells.

Let’s see some code that demonstrates this:

1

val quantity = ValueCell(0)

2

val price = ValueCell(1d)

3

val total = price.lift(_ * quantity)

63

64

CHAPTER 6. WIRING

We define two ValueCells, one for quantity and the other for price. Next, define the total

by “lifting” the price in a formula that multiplies it by quantity. Let’s see how it works in the

console:

1

scala> import net.liftweb._

2

import net.liftweb._

3

4

scala> import util._

5

import util._

6

7

scala> val quantity = ValueCell(0)

8

quantity: net.liftweb.util.ValueCell[Int] = ValueCell(0)

9

10

scala> val price = ValueCell(0d)

11

price: net.liftweb.util.ValueCell[Double] = ValueCell(0.0)

12

13

scala> val total = price.lift(_ * quantity)

14

total: net.liftweb.util.Cell[Double] = FuncCell1(ValueCell(0.0),<function1>)

15

16

scala> total.get

17

res1: Double = 0.0

18

19

scala> quantity.set(10)

20

res2: Int = 10

21

22

scala> price.set(0.5d)

23

res3: Double = 0.5

24

25

scala> total.get

26

res4: Double = 5.0

Okay... pretty nifty... we can define relationships that are arbitrarily complex between Cells and

they know how to calculate themselves.

6.2

Hooking it up to the UI

Now that we can declare relationships among cells, how do we associate the value of Cells with

the user interface?

Turns out that it’s pretty simple:

1

"#total" #> WiringUI.asText(total)

We associate the element with id="total" with a function that displays the value in total.

Here’s the method definition:

1

/**

2

* Given a Cell register the

3

* postPageJavaScript that will update the elem