3 Lift Fundamentals
In this chapter we will cover some of the fundamental aspects of writing a lift application, including the architecture of the Lift library and how it processes requests.
3.1 Entry into Lift
The first step in Lift’s request processing is intercepting the HTTP request. Originally, Lift used a
java.servlet.Servlet instance to process incoming requests. This was changed to use a
java.servlet.Filter instance because this allows the container to handle any requests that Lift does not (in particular, static content). The filter acts as a thin wrapper on top of the existing
LiftServlet (which still does all of the work), so don’t be confused when you look at the Lift API and see both classes (
LiftFilter and
LiftServlet). The main thing to remember is that your
web.xml↓ should specify the filter and not the servlet, as shown in Listing
3.1↓.
LiftFilter Setup in web.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">
<web-app>
<filter>
<filter-name>LiftFilter</filter-name>
<display-name>Lift Filter</display-name>
<description>The Filter that intercepts lift calls</description>
<filter-class>net.liftweb.http.LiftFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LiftFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
A full
web.xml example is shown in Section
G.1.5 on page 1↓. In particular, the filter-mapping (lines 13-16) specifies that the Filter is responsible for everything. When the filter receives the request, it checks a set of rules to see if it can handle it. If the request is one that Lift handles, it passes it on to an internal
LiftServlet instance for processing; otherwise, it chains the request and allows the container to handle it.
3.2 Bootstrap ↓
When Lift starts up an application there are a number of things that you’ll want to set up before any requests are processed. These things include setting up a site Menu (called SiteMap, Chapter
7↓), URL rewriting (Section
3.7↓), custom dispatch (Section
3.8↓), and classpath search (Section
3.2.1↓). The Lift servlet looks for the
bootstrap.liftweb.Boot↓ class and executes the
boot method in the class. You can also specify your own Boot instance by using the
bootloader↓ init param for the
LiftFilter as shown in Listing
3.2↓
Overriding the Boot Loader Class
<filter>
... filter setup here ...
<init-param>
<param-name>bootloader</param-name>
<param-value>foo.bar.baz.MyBoot</param-value>
</init-param>
</filter>
Your custom boot class class must subclass Bootable and implement the boot method. The boot method will only be run once, so you can place any initialization calls for other libraries here as well.
3.2.1 Class Resolution
As part of our discussion of the Boot class, it’s also important to explain how Lift determines where to find classes for Views and Snippet rendering when using implicit dispatch (we’ll cover this in Section
5.2.1 on page 1↓). The
LiftRules.addToPackages method tells lift which Scala packages to look in for a given class. Lift has implicit extensions to the paths you enter: in particular, if you tell Lift to use the
com.pocketchangeapp package, Lift will look for View classes (Section ???)under
com.pocketchangeapp.view , Comet classes (Section
11.5↓) under
com.pocketchange.comet, and Snippet classes (Chapter
5↓) under
com.pocketchangeapp.snippet. The
addToPackages method should almost always be executed in your Boot class. A minimal Boot class would look like:
class Boot {
def boot = {
LiftRules.addToPackages("com.pocketchangeapp")
}
}
3.3 A Note on Standard Imports
For the sake of saving space, the following import statements are assumed for all example code throughout the rest of the book:
Standard Import Statements
import net.liftweb.common._
import net.liftweb.http._
import S._
import net.liftweb.util._
import Helpers._
import scala.xml._
3.4 Lift’s Main Objects
Before we dive into Lift’s fundamentals, we want to briefly discuss three objects you will use heavily in your Lift code. We’ll be covering these in more detail later in this chapter and in further chapters, so feel free to skip ahead if you want more details.
The
net.liftweb.http.S object represents the state of the current request (according to David Pollak, “S” is for Stateful). As such, it is used to retrieve information about the request and modify information that is sent in the response. Among other things, it can be used for notices (Section
B↓) , cookie management (Section
3.10↓), localization/internationalization (Chapter
D↓) and redirection (Section
3.9↓).
The
net.liftweb.http.SHtml object’s main purpose is to define HTML generation functions, particularly those having to do with form elements. We cover forms in detail in Chapter
6↓). In addition to normal form elements, SHtml defines functions for AJAX and JSON form elements (Chapters
11↓ and
10↓, respectively).
The net.liftweb.http.LiftRules object is where the vast majority of Lift’s global configuration is handled. Almost everything that is configurable about Lift is set up based on variables in LiftRules. Because LiftRules spans such a diverse range of functionality, we won’t be covering LiftRules directly, but as we discuss each Lift mechanism we’ll touch on the LiftRules variables and methods related to the configuration of that mechanism.
3.5 The Rendering Process
The rest of this chapter, as well as the next few chapters, are dedicated to the stages of rendering in Lift. We’ll start here by giving a brief overview of the processes by which Lift transforms a request into a response
↓. We’re only going to touch on the major points here, although the steps we do discuss will be in the order that Lift performs them. A much more detailed tour of the pipeline is given in Section
9.2↓. Starting from the initial invocation on a request, Lift will:
-
Perform any configured URL rewriting. This is covered in Section 3.7↓.
-
Execute any matching custom dispatch functions. This is split into both stateless and stateful dispatch, and will be covered in more detail in Section 3.8↓.
-
Perform automatic processing of Comet and AJAX requests (Chapter 11↓).
-
Perform SiteMap setup and matching. SiteMap, covered in Chapter 7↓, not only provides a nice site-wide menu system, but can also perform security control, URL rewrite, and other custom functionality.
-
Locate the template XHTML to use for the request. This is handled via three mechanisms:
-
Checking the LiftRules.viewDispatch↓↓ RulesSeq to see if any custom dispatch rules have been defined. We cover custom view dispatch in Section 9.2 on page 1↓.
-
If there is no matching viewDispatch, locate a template↓ file that matches and use it. We’ll cover templates, and how they’re located, in Section 4.1↓.
-
If no templates files can be located, attempting to locate a view↓ based on implicit dispatch. We’ll cover views in Section 4.4↓.
-
Process the template, including embedding of other templates (Section 4.5.7↓), merging <head/> elements from composited templates (Section ↓), and executing snippet functions (Chapter 5↓).
The rest of this chapter will be devoted in part to the early stages of the rendering pipeline, as well as some notes on some general functionality in Lift like redirects
↓.
3.6 Notices, Warnings, and Error Messages
Feedback to the user is important. The application must be able to notify the user of errors, warn the user of potential problems, and notify the user when system status changes. Lift provides a unified model for such messages that can be used for static pages as well as for AJAX and Comet calls. We cover messaging support in Appendix
B↓.
3.7 URL Rewriting
Now that we’ve gone over Templates, Views, Snippets, and how requests are dispatched to a Class.method, we can discuss how to intercept requests and handle them the way we want to. URL rewriting
↓↓ is the mechanism that allows you to modify the incoming request so that it dispatches to a different URL. It can be used, among other things, to allow you to:
-
Use user-friendly, bookmarkable URLs like http://www.example.com/budget/2008
-
Use short URLs instead of long, hard to remember ones, similar to http://tinyurl.com
-
Use portions of the URL to determine how a particular snippet or view responds. For example, you could make it so that a user’s profile is displayed via a URL such as
http://someplace.com/user/derek instead of having the username sent as part of a query string.
The mechanism is fairly simple to set up. We need to write a partial function from a RewriteRequest to a RewriteResponse to determine if and how we want to rewrite particular requests. Once we have the partial function, we modify the
LiftRules.rewrite configuration to hook into Lift’s processing chain. The simplest way to write a partial function is with Scala’s match statement, which will allow us to selectively match on some or all of the request information. (Recall that for a partial function, the matches do not have to be exhaustive. In the instance that no RewriteRequest matches, no RewriteResponse will be generated.) It is also important to understand that when the rewrite functions run, the Lift session has not yet been created. This means that you generally can’t set or access properties in the S object. RewriteRequest is a
case object that contains three items: the parsed path, the request type and the original HttpServletRequest
↓ object. (If you are not familiar with case classes, you may wish to review the Scala documentation for them. Adding the
case modifier to a class results in some nice syntactic conveniences.)
The parsed path of the request is in a ParsePath
↓ case class instance. The ParsePath class contains
-
The parsed path as a List[String]
-
The suffix of the request (i.e. “html”, “xml”, etc)
-
Whether this path is root-relative path. If true, then it will start with /<context-path>, followed by the rest of the path. For example, if your application is deployed on the app context path (“/app”) and we want to reference the file <webapp-folder>/pages/index.html, then the root-relative path will be /app/pages/index.html.
-
Whether the path ends in a slash (“/”)
The latter three properties are useful only in specific circumstances, but the parsed path is what lets us work magic. The path of the request is defined as the parts of the URI between the context path and the query string. The following table shows examples of parsed paths for a Lift application under the “myapp” context path:
The RequestType maps to one of the five HTTP methods: GET, POST, HEAD, PUT and DELETE. These are represented by the corresponding GetRequest, PostRequest, etc. case classes, with an UnknownRequest case class to cover anything strange.
The flexibility of Scala’s matching system is what really makes this powerful. In particular, when matching on Lists, we can match parts of the path and capture others. For example, suppose we’d like to rewrite the
/account/<account name> path so that it’s handled by the
/viewAcct template as shown in Listing
3.7↓. In this case we provide two rewrites. The first matches /account/<account name> and redirects it to the /viewAcct template, passing the acctName as a “name” parameter. The second matches /account/<account name>/<tag>, redirecting it to /viewAcct as before, but passing both the “name” and a “tag” parameter with the acctName and tag matches from the ParsePath, respectively. Remember that the underscore (_) in these matching statements means that we don’t care what that parameter is, i.e., match anything in that spot.
LiftRules.rewrite.append {
case RewriteRequest(
ParsePath(List("account",acctName),_,_,_),_,_) =>
RewriteResponse("viewAcct" :: Nil, Map("name" -> acctName))
case RewriteRequest(
ParsePath(List("account",acctName, tag),_,_,_),_,_) =>
RewriteResponse("viewAcct" :: Nil, Map("name" -> acctName,
"tag" -> tag)))
}
The RewriteResponse
↓ simply contains the new path to follow. It can also take a Map
↓ that contains parameters that will be accessible via S.param
↓↓ in the snippet or view. As we stated before, the LiftSession (and therefore most of S) isn’t available at this time, so the Map is the only way to pass information on to the rewritten location.
We can combine the ParsePath matching with the RequestType and HttpServletRequest to be very specific with our matches. For example, if we wanted to support the DELETE HTTP verb for a RESTful
↓ interface through an existing template, we could redirect as shown in Listing
3.7↓.
A Complex Rewrite Example
LiftRules.rewrite.append {
case RewriteRequest(ParsePath("username" :: Nil, _, _, _),
DeleteRequest,
httpreq)
if isMgmtSubnet(httpreq.getRemoteHost()) =>
RewriteResponse("deleteUser" :: Nil, Map("username" -> username))
}
We’ll go into more detail about how you can use this in the following sections. In particular, SiteMap
↓ (Chapter
7↓) provides a mechanism for doing rewrites combined with menu entries.
3.8 Custom Dispatch Functions
Once the rewriting phase is complete (whether we pass through or are redirected), the next phase is to determine whether there should be a custom dispatch for the request. A custom dispatch allows you to handle a matching request directly by a method instead of going through the template lookup system. Because it bypasses templating, you’re responsible for the full content of the response. A typical use case would be a web service
↓ returning XML or a service to return, say, a generated image or PDF. In that sense, the custom dispatch mechanism allows you to write your own “sub-servlets” without all the mess of implementing the interface and configuring them in web.xml
↓.
As with rewriting, custom dispatch is realized via a partial function. In this case, it’s a function of type PartialFunction[Req,() ⇒ Box[LiftResponse]] that does the work. The Req is similar to the RewriteRequest case class: it provides the path as a List[String], the suffix of the request, and the RequestType. There are three ways that you can set up a custom dispatch function:
-
Globally, via LiftRules.dispatch↓↓
-
Globally, via LiftRules.statelessDispatchTable↓↓
-
Per-Session, via S.addHighLevelSessionDispatcher↓↓
If you attach the dispatch function via LiftRules.dispatch or S.addHighLevelSessionDispatcher, then you’ll have full access to the S object, SessionVars and LiftSession; if you use LiftRules.statelessDispatchTable instead, then these aren’t available. The result of the dispatch should be a function that returns a Box[LiftResponse]. If the function returns Empty, then Lift returns a “404 Not Found” response.
As a concrete example, let’s look at returning a generated chart image from our application. There are several libraries for charting, but we’ll take a look at JFreeChart in particular. First, let’s write a method that will chart our account balances by month for the last year:
def chart (endDate : String) : Box[LiftResponse] = {
// Query, set up chart, etc...
val buffered = balanceChart.createBufferedImage(width,height)
val chartImage = ChartUtilities.encodeAsPNG(buffered)
// InMemoryResponse is a subclass of LiftResponse
// it takes an Array of Bytes, a List[(String,String)] of
// headers, a List[Cookie] of Cookies, and an integer
// return code (here 200 for HTTP 200: OK)
Full(InMemoryResponse(chartImage,
("Content-Type" -> "image/png") :: Nil,
Nil,
200))
}
Once we’ve set up the chart, we use the ChartUtilities helper class from JFreeChart to encode the chart into a PNG byte array. We can then use Lift’s InMemoryResponse
↓ to pass the encoded data back to the client with the appropriate Content-Type
↓ header. Now we just need to hook the request into the dispatch table from the Boot class as shown in Listing
3.8↓. In this instance, we want state so that we can get the current user’s chart. For this reason, we use
LiftRules.dispatch as opposed to
LiftRules.statelessDispatch. Because we’re using a partial function to perform a Scala match operation, the case that we define here uses the
Req object’s
unapply method, which is why we only need to provide the
List[String] argument.
Hooking Dispatch into Boot
LiftRules.dispatch.append {
case Req("chart" :: "balances" :: endDate :: Nil, _, _) =>
Charting.chart(endDate) _
}
As you can see, we capture the endDate parameter from the path and pass it into our chart method. This means that we can use a URL like http://foo.com/chart/balances/20080401 to obtain the image. Since the dispatch function has an associated Lift session, we can also use the S.param method to get query string parameters, if, for example, we wanted to allow someone to send an optional width and height:
val width = S.param(“width”).map(_.toInt) openOr 400
val height = S.param(“height”).map(_.toInt) openOr 300
Or you can use a slightly different approach by using the Box.dmap method:
val width = S.param(“width”).dmap(400)(_.toInt)
val height = S.param(“height”).dmap(300)(_.toInt)
Where dmap is identical with map function except that the first argument is the default value to use if the
Box is
Empty. There are a number of other ListResponse subclasses to cover your needs, including responses for XHTML, XML, Atom, Javascript, CSS, and JSON. We cover these in more detail in Section
9.4↓.
3.9 HTTP Redirects
HTTP redirects are an important part of many web applications. In Lift there are two main ways of sending a redirect to the client:
-
Call S.redirectTo. When you do this, Lift throws an exception and catches it later on. This means that any code following the redirect is skipped. If you’re using a StatefulSnippet (Section 5.3.3↓), use this.redirectTo so that your snippet instance is used when the redirect is processed.
Important: if you use S.redirectTo within a try/catch block, you’ll need to make sure that you aren’t catching the redirect exception (Scala uses unchecked exceptions), or test for the redirect’s exception and rethrow it. Ifyou mistakenly catch the redirect exception, then no redirect will occur.
-
When you need to return a LiftResponse, you can simply return a RedirectResponse or a RedirectWithState response.
The RedirectWithState response allows you to specify a function to be executed when the redirected request is processed. You can also send Lift messages (notices, warnings, and errors) that will be rendered in the redirected page, as well as cookies to be set on redirect. Similarly, there is an overloaded version of S.redirectTo that allows you to specify a function to be executed when the redirect is processed.
Cookies are a useful tool when you want data persisted across user sessions. Cookies are essentially a token of string data that is stored on the user’s machine. While they can be quite useful, there are a few things that you should be aware of:
-
The user’s browser may have cookies disabled, in which case you need to be prepared to work without cookies or tell the user that they need to enable them for your site
-
Cookies are relatively insecure. There have been a number of browser bugs related to data in cookies being read by viruses or other sites
-
Cookies are easy to fake, so you need to ensure that you validate any sensitive cookie data
Using Cookies in Lift is very easy. In a stateful context, everything you need is provided by a few methods on the S object:
addCookie Adds a cookie to be sent in the response
deleteCookie Deletes a cookie (technically, this adds a cookie with a maximum age of zero so that the browser removes it). You can either delete a cookie by name, or with a Cookie object
findCookie Looks for a cookie with a given name and returns a Box[Cookie]. Empty means that the cookie doesn’t exist
receivedCookies Returns a List[Cookie] of all of the cookies sent in the request
responseCookies Returns a List[Cookie] of the cookies that will be sent in the response
If you need to work with cookies in a stateless context, many of the ListResponse classes (Section
9.4↓) include a List[Cookie] in their constructor or
apply arguments. Simply provide a list of the cookies you want to set, and they’ll be sent in the response. If you want to delete a cookie in a LiftResponse, you have to do it manually by adding a cookie with the same name and a
maxage of zero.
3.11 Session and Request State
Lift provides a very easy way to store per-session and per-request data through the SessionVar and RequestVar classes. In true Lift fashion, these classes provide:
-
Type-safe access to the data they hold
-
A mechanism for providing a default value if the session or request doesn’t exist yet
-
A mechanism for cleaning up the data when the variable’s lifecycle ends
Additionally, Lift provides easy access to HTTP request parameters
↓ via the S.param method, which returns a Box[String]. Note that HTTP request parameters (sent via GET or POST) differ from RequestVars in that query parameters are string values sent as part of the request; RequestVars, in contrast, use an internal per-request Map so that they can hold any type, and are initialized entirely in code. At this point you might ask what RequestVars can be used for. A typical example would be sharing state between different snippets, since there is no connection between snippets other than at the template level.
SessionVars and RequestVars are intended to be implemented as singleton objects so that they’re accessible from anywhere in your code. Listing
3.11↓ shows an example definition of a RequestVar used to hold the number of entries to show per page. We start by defining the object as extending the RequestVar. You must provide the type of the RequestVar so that Lift knows what to accept and return. In this instance, the type is an Int. The constructor argument is a by-name parameter which must evaluate to the var’s type. In our case, we attempt to use the HTTP request variable “pageSize,” and if that isn’t present or isn’t an integer, then we default to 25.
class AccountOps {
object pageSize extends RequestVar[Int](S.param("pageSize").map(_.toInt) openOr 25)
...
}
Accessing the value of the
RequestVar is done via the
is method. You can also set the value using the
apply method, which in Scala is syntactically like using the
RequestVar as a function. Common uses of apply in Scala include array element access by index and companion object methods that can approximate custom constructors. For example, the
Loc object (which we’ll cover in Chapter
7↓), has an overloaded
apply method that creates a new
Loc class instance based on input parameters.
// get the value contained in the AccountOps.pageSize RequestVar
query.setMaxResults(AccountOps.pageSize.is)
// Change the value of the RequestVar. The following two lines
// of code are equivalent:
AccountOps.pageSize(50)
AccountOps.pageSize.apply(50)
In addition to taking a parameter that defines a default value for setup, you can also clean up the value when the variable ends it lifecycle. Listing
3.11↓ shows an example of opening a socket and closing it at the end of the request. This is all handled by passing a function to the
registerCleanupFunc method. The type of the function that you need to pass is
CleanUpParam ⇒ Unit, where
CleanUpParam is defined based on whether you’re using a
RequestVar or a
SessionVar. With
RequestVar,
CleanUpParam is of type
Box[LiftSession], reflecting that the session may not be in scope when the cleanup function executes. For a
SessionVar the
CleanUpParam is of type
LiftSession, since the session is always in scope for a
SessionVar (it holds a reference to the session). In our example in Listing
3.11↓ we simply ignore the input parameter to the cleanup function, since closing the socket is independent of any session state. Another important thing to remember is that you’re responsible for handling any exceptions that might be thrown during either default initialization or cleanup.
Defining a Cleanup Function
object mySocket extends RequestVar[Socket](new Socket("localhost:23")) {
registerCleanupFunc(ignore => this.is.close)
}
The information we’ve covered here is equally applicable to SessionVars; the only difference between them is the scope of their respective lifecycles.
Another common use of RequestVar is to pass state around between different page views (requests). We start by defining a RequestVar on an object so that it’s accesible from all of the snippet methods that will read and write to it. It’s also possible to define it on a class if all of the snippets that will access it are in that class. Then, in the parts of your code that will transition to a new page you use the overloaded versions of SHtml.link or S.redirectTo that take a function as a second argument to “inject” the value you want to pass via the RequestVar. This is similar to using a query parameter on the URL to pass data, but there are two important advantages:
-
You can pass any type of data via a RequestVar, as opposed to just string data in a query parameter.
-
You’re really only passing a reference to the injector function, as opposed to the data itself. This can be important if you don’t want the user to be able to tamper with the passed data. One example would be passing the cost of an item from a “view item” page to an “add to cart” page.
Listing
3.11↓ shows how we pass an Account from a listing table to a specific Account edit page using
SHtml.link, as well as how we could transition from an edit page to a view page using
S.redirectTo. Another example of passing is shown in Listing
12.1.3 on page 1↓.
Passing an Account to View
class AccountOps {
...
object currentAccountVar extends RequestVar[Account](null)
...
def manage (xhtml : NodeSeq) ... {
...
User.currentUser.map({user =>
user.accounts.flatMap({acct =>
bind("acct", chooseTemplate("account", "entry", xhtml),
...
// The second argument injects the "acct" val back
// into the RequestVar
link("/editAcct", () => currentAccountVar(acct), Text("Edit"))
})
})
...
}
def edit (xhtml : NodeSeq) : NodeSeq = {
def doSave () {
...
val acct = currentAccountVar.is
S.redirectTo("/view", () => currentAccountVar(acct))
}
...
}
}
One important thing to note is that the injector variable is called in the scope of the
following request. This means that if you want the value returned by the function at the point where you call the link or redirectTo, you’ll need to capture it in a val. Otherwise, the function will be called
after the redirect or link, which may result in a different value than you expect. As you can see in Listing
3.11↑, we set up an acct val in our doSave method prior to redirecting. If we tried to do something like
S.redirectTo("/view", () => currentAccountVar(currentAccountVar.is))
instead, we would get the default value of our RequestVar (null in this case).
3.12 Conclusion
We’ve covered a lot of material and we still have a lot more to go. Hopefully this chapter provides a firm basis to start from when exploring the rest of the book.
(C) 2012 Lift 2.0 EditionWritten by Derek Chen-Becker, Marius Danciu and Tyler Weir