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