Up: Part I

7 SiteMap

SiteMap is a very powerful part of Lift that does essentially what it says: provides a map (menu) for your site. Of course, if all it did was generate a set of links on your page, we wouldn’t have a whole chapter dedicated to it. SiteMap not only handles the basic menu generation functionality, but also provides:
The beauty of SiteMap is that it’s very easy to start out with the basic functionality and then expand on it as you grow.

7.1 Basic SiteMap Definition

Let’s start with our basic menu for PocketChange. To keep things simple, we’ll just define four menu items to begin:
  1. A home page that displays the user’s entries when the user is logged in, or a welcome page when the user is not
  2. A logout link when the user is logged in, log in and registration links and pages when the user is not
  3. Pages to view or edit the user’s profile, available only when the user is logged in
  4. A help page, available whether the user is logged in or not
We’ll assume that we have the corresponding pages, "homepage", "login", "logout", and "profile," written and functional. We’ll also assume that the help page(s) reside under the "help" subdirectory to keep things neat, and that the entry to help is /help/index.

7.1.1 The Link Class

The Link class [Y]  [Y] net.liftweb.sitemap.Loc.Link is a fundamental part of Menu definitions. The Link class contains two parameters: a List[String] of path components, and a boolean value that controls whether prefix matching is enabled. The path components represent the portion of the URI following your web context, split on the "/" character. Listing 7.1.1↓ shows how you would use Link to represent the "/utils/index" page. Of course, instead of “utils” :: “index” :: Nil, you could as easily use List(“utils”, “index”) if you prefer.
Link Path Components
val myUtilsIndex = new Link("utils" :: "index" :: Nil, false)
Prefix matching allows the path components you specify to match any longer paths as well. Following our first example, if you wanted to match anything under the utils directory (say, for access control), you would set the second parameter to true, as shown in Listing 7.1.1↓.
Link Prefix Matching
val allUtilPages = new Link("utils" :: Nil, true)

7.1.2 ExtLink

The ExtLink object can be used to create a Link instance using your own full link URL. As its name implies, it would usually be used for an external location. Listing 7.1.2↓ shows a menu item that points to a popular website.
Using ExtLink
val goodReference = Menu(Loc("reference",
                             ExtLink("http://www.liftweb.net/"),
                             "LiftWeb"))

7.1.3 Creating Menu Entries

Menu entries are created using the Menu [Z]  [Z] net.liftweb.sitemap.Menu class, and its corresponding Menu object. A Menu, in turn, holds a Loc [A]  [A] net.liftweb.sitemap.Loc trait instance, which is where most of the interesting things happen. A menu can also hold one or more child menus, which we’ll cover in Section 7.1.4↓. Note that the Loc object has several implicit methods that make defining Locs easier, so you generally want to import them into scope . The simplest way is to import net.liftweb.sitemap.Loc._, but you can import specific methods by name if you prefer. A Loc can essentially be thought of as a link in the menu, and contains four basic items:
  1. The name of the Loc: this must be unique across your sitemap because it can be used to look up specific Menu items if you customize your menu display (Section 7.2.3↓)
  2. The link to which the Loc refers: usually this will referernce a specific page, but Lift allows a single Loc to match based on prefix as well (Section 7.1.1↑)
  3. The text of the menu item, which will be displayed to the user: you can use a static string or you can generate it with a function (Section 7.2.2↓)
  4. An optional set of LocParam parameters that control the behavior and appearance of the menu item (see Sections 7.2↓,7.3↓, 7.5↓, and 7.4↓)
For our example, we’ll tackle the help page link first, because it’s the simplest (essentially, it’s a static link). The definition is shown in Listing 7.1.3↓. We’re assuming that you’ve imported the Loc implicit methods to keep things simple. We’ll cover instantiating the classes directly in later sections of this chapter.
Help Menu Definition
val helpMenu = Menu(Loc("helpHome",
                        ("help" :: "" :: Nil) -> true,
                        "Help"))
Here we’ve named the menu item "helpHome." We can use this name to refer back to this menu item elsewhere in our code. The second parameter is a Pair[List[String],Boolean] which converts directly to a Link class with the given parameters (see Section 7.1.1↑ above). In this instance, by passing in true, we’re saying that anything under the help directory will also match. If you just use a List[String], the implicit conversion is to a Link with prefix matching disabled. Note that SiteMap won’t allow access to any pages that don’t match any Menu entries, so by doing this we’re allowing full access to all of the help files without having to specify a menu entry for each. The final parameter, "Help," is the text for the menu link, should we choose to generate a menu link from this SiteMap entry.

7.1.4 Nested Menus

The Menu class supports child menus by passing them in as final constructor parameters. For instance, if we wanted to have an "about" menu under Help, we could define the menu as shown in Listing 7.1.4↓.
Nested Menu Definition
val aboutMenu = Menu(Loc("about", "help" :: "about" :: Nil, "About"))
val helpMenu = Menu(Loc(...as defined above...), aboutMenu)
When the menu is rendered it will have a child menu for About. Child menus are only rendered by default when the current page matches their parent’s Loc. That means that, for instance the following links would show in an "About" child menu item:
But the following would not:
We’ll cover how you can customize the rendering of the menus in Section 7.2.3↓.

7.1.5 Setting the Global SiteMap

Once you have all of your menu items defined, you need to set them as your SiteMap. As usual, we do this in the Boot class by calling the setSiteMap method on LiftRules, as shown in Listing 7.1.5↓. The setSiteMap method takes a SiteMap object that can be constructed using your menu items as arguments.
Setting the SiteMap
LiftRules.setSiteMap(SiteMap(homeMenu, profileMenu, ...))
When you’re dealing with large menus, and in particular when your model objects create their own menus (see MegaProtoUser, Section 8.2.8↓ ), then it can be more convenient to define List[Menu] and set that. Listing 7.1.5↓ shows this usage.
Using List[Menu] for SiteMap
val menus = Menu(Loc("HomePage", "", "Home"),...) ::
            ...
            Menu(...) :: Nil
LiftRules.setSiteMap(SiteMap(menus : _*))
The key to using List for your menus is to explicitly define the type of the parameter as "_*" so that it’s treated as a set of varargs instead of a single argument of type List[Menu].

7.2 Customizing Display

There are many cases where you may want to change the way that particular menu items are displayed. For instance, if you’re using a Menu item for access control on a subdirectory, you may not want the menu item displayed at all. We’ll discuss how you can control appearance, text, etc. in this section.

7.2.1 Hidden

The Hidden LocParam does exactly what it says: hides the menu item from the menu display. All other menu features still work. There is a variety of reasons why you might not want a link displayed. A common use, shown in Listing 7.2.1↓, is where the point of the item is to restrict access to a particular subdirectory based on some condition. (We’ll cover the If tag in Section 7.3.1↓.)
Hidden Menus
val receiptImages =
  Menu(Loc("receipts", 
          ("receipts" :: Nil) -> true,
          "Receipts",
          Hidden, If(...)))
Note that in this example we’ve used the implicit conversion from Pair[String,Boolean] to Link to make this Menu apply to everything under the "receipts" directory.

7.2.2 Controlling the Menu Text

The LinkText class is what defines the function that will return the text to display for a given menu item. As we’ve shown, this can easily be set using the implicit conversion for string → LinkText from Loc. As an added bonus, the implicit conversion actually takes a by-name String for the parameter. This means that you can just as easily pass in a function to generate the link text as a static string. For example, with our profile link we may want to make the link say "<username>’s profile". Listing 7.2.2↓ shows how we can do this by defining a helper method, assuming that there’s another method that will return the current user’s name (we use the ubiquitous Foo object here).
Customizing Link Text
def profileText = Foo.currentUser + "’s profile"
val profileMenu = Menu(Loc("Profile", 
                           "profile" :: Nil,
                           profileText, ...))
Of course, if you want you can construct the LinkText instance directly by passing in a constructor function that returns a NodeSeq. The function that you use with LinkText takes a type-safe input parameter, which we’ll discuss in more detail in Section 7.6.2↓.

7.2.3 Using <lift:Menu>

So far we’ve covered the Scala side of things. The other half of the magic is the special <lift:Menu> tag. It’s this tag that handles the rendering of your menus into XHTML. The Menu tag uses a built-in snippet [B]  [B] net.liftweb.builtin.snippet.Menu to provide several rendering methods. The most commonly used method is the Menu.builder snippet. This snippet renders your entire menu structure as an unordered list (<ul> in XHTML). Listing 7.2.3↓ shows an example of using the Menu tag to build the default menu (yes, it’s that easy).
Rendering with <lift:Menu.title>
<div class="menu">
  <lift:Menu.builder />
</div>
Of course, Lift offers more customization on this snippet than just emitting some XHTML. By specifying some prefixed attributes on the tag itself, you can add attributes directly to the menu elements. The following prefixes are valid for attributes:
The suffix of the attributes represents the name of the HTML attribute that will be added to that element, and can be anything. It will be passed directly through. For instance, we can add CSS classes to our menu and elements fairly easily, as shown in Listing 7.2.3↓. Notice that we also add a little JavaScript to our current menu item.
Using Attribues with Menu.builder
<lift:Menu.builder
  li:class="menuitem"
  li_item:class="selectedMenu"
  li_item:onclick="javascript:alert(’Already selected!’);" />
In addition to rendering the menu itself, the Menu class offers a few other tricks. The Menu.title snippet can be used to render the title of the page, which by default is the name parameter of the Loc for the menu (the first parameter). If you write your own Loc implementation (Section 7.6↓), or you use the Title LocParam (Section 7.4.3↓), you can overide the title to be whatever you’d like. Listing 7.2.3↓ shows how you use Menu.title. In this particular example the title will be rendered as "Home Page".
Rendering the Menu Title
// In Boot:
val MyMenu = Menu(Loc("Home Page", "index" :: Nil, "Home"))
// In template (or wherever)
<title><lift:Menu.title/></title>
The next snippet in the Menu class is item. The Menu.item snippet allows you to render a particular menu item by specifying the name attribute (matching the first parameter to Loc). As with Menu.builder, it allows you to specify additional prefixed attributes for the link to be passed to the emitted item. Because it applies these attributes to the link itself, the only valid prefix is "a". Additionally, if you specify child elements for the snippet tag, they will be used instead of the default link text. Listing 7.2.3↓ shows an example using our "Home Page" menu item defined in Listing 7.2.3↑. As you can see, we’ve added some replacement text as well as specifying a CSS class for the link.
Using Menu.item
<lift:Menu.item name="Home Page"
  a:class="homeLink">
  <b>Go Home</b>
</lift:Menu.item>
The final snippet that the Menu class provides is the Menu.group method. We’re going to cover the use of Menu.group in detail in Section 7.5.2↓.

7.3 Access Control

So far we’ve covered how to control the display side of Menus; now we’ll take a look at some of the plumbing behind the scenes. One important function of a Menu is that it controls access to the pages in your application. If no Menu matches a given request, then the user gets a 404 Not Found error. Other than this binary control of "matches → display" and "doesn’t match → don’t display", SiteMap provides for arbitrary access checks through the If and Unless LocParams.

7.3.1 If

The If LocParam takes a test function, () ⇒ Boolean, as well as failure message function, () ⇒ LiftResponse, as its arguments. When the Loc that uses the If clause matches a given path, the test function is executed, and if true then the page is displayed as normal. If the function evaluates to false, then the failure message function is executed and its result is sent to the user. There’s an implicit conversion in Loc from a String to a response which converts to a RedirectWithState instance (Section 3.9↑). The redirect is to the location specified by LiftRules.siteMapFailRedirectLocation, which is the root of your webapp ("/") by default. If you want, you can change this in LiftRules for a global setting, or you can provide your own LiftResponse. Listing 7.3.1↓ shows a revision of the profile menu that we defined in Listing 7.2.2↑, extended to check whether the user is logged in. If the user isn’t logged in, we redirect to the login page.
Using the If LocParam
val loggedIn = If(() => User.loggedIn_?,
                  () => RedirectResponse("/login"))
val profileMenu = Menu(Loc("Profile", 
                           "profile" :: Nil,
                           profileText, loggedIn))

7.3.2 Unless

The Unless LocParam is essentially the mirror of If. The exact same rules apply, except that the page is displayed only if the test function returns false. The reason that there are two classes to represent this behavior is that it’s generally clearer when a predicate is read as "working" when it returns true.

7.4 Page-Specific Rendering

Page specific rendering with SiteMap is an advanced technique that provides a lot of flexibility for making pages render differently depending on state.

7.4.1 The Template Parameter

Generally, the template that will be used for a page is derived from the path of the request. The Template LocParam, however, allows you to completely override this mechanism and provide any template you want by passing in a function () ⇒ NodeSeq. Going back to our example menus (Section 7.1↑), we’d like the welcome page to show either the user’s entries or a plain welcome screen depending on whether they’re logged in. One approach to this is shown in Listing 7.4.1↓. In this example, we create a Template class that generates the appropriate template and then bind it into the home page menu Loc. (See the Lift API for more on the Template class.)
Overriding Templates
val homepageTempl = Template({ () =>
  <lift:surround with="default" at="content">
  { if (User.loggedIn_?) { 
      <lift:Entries.list /> 
    } else {
      <lift:embed what="welcome" />
    }
  }
  </lift:surround>
})
val homeMenu = Menu(Loc("Home Page",
                        "" :: Nil,
                        "Home Page", homepageTempl))

7.4.2 The Snippet and LocSnippets Parameters

Besides overriding the template for a page render (admittedly, a rather coarse approach), SiteMap has two mechanisms for overriding or defining the behavior of specific snippets. The first, Snippet, allows you to define the dispatch for a single snippet based on the name of the snippet. Listing 7.4.2↓ shows how we could use Snippet to achieve the same result for the home page rendering as we just did with the Template parameter. All we need to do is use the <lift:homepage> snippet on our main page and the snippet mapping will dispatch based on the state. (Here we’ve moved the welcome text into a Utils.welcome snippet.)
Using the Snippet LocParam
val homeSnippet = Snippet("homepage",
  if (User.loggedIn_?) {
    Entries.list _
  } else {
    Utils.welcome _
  })
val homeMenu = Menu(Loc("Home Page",
                        "" :: Nil,
                        "Home Page", homeSnippet))
The LocSnippets trait extends the concept of Snippet to provide a full dispatch partial function. This allows you to define multiple snippet mappings associated with a particular Loc. To simplify things, Lift provides a DispatchLocSnippets trait that has default implementations for apply and isDefinedAt; that means you only need to provide a dispatch method implementation for it to work. Listing 7.4.2↓ shows an example of using DispatchLocSnippets for a variety of snippets.
Using LocSnippets
val entrySnippets = new DispatchLocSnippets {
  def dispatch = {
    case "entries" => Entries.list _
    case "add" => Entries.newEntry _
  }
}

7.4.3 Title

As we mentioned in Section 7.2.3↑, the Title LocParam can be used to provide a state-dependent title for a page. The Title case class simply takes a function (T) ⇒ NodeSeq, where T is a type-safe parameter (we’ll cover this in Section 7.6.2↓). Generally you can ignore this parameter if you want to, which is what we do in Listing 7.4.3↓.
Customizing the Title
val userTitle = Title((_) => 
  if (User.loggedIn_?) {
    Text(User.name + "’s Account")
  } else {
    Text("Welcome to PocketChange")
  })
val homeMenu = Menu(Loc("Home Page",
                        "" :: Nil,
                        "Home Page", homepageTempl, userTitle))

7.5 Miscellaneous Menu Functionality

These are LocParams that don’t quite fit into the other categories.

7.5.1 Test

Test is intended to be used to ensure that a given request has the proper parameters before servicing. With Test, you provide a function, (Req) ⇒ Boolean that is passed the full Req object. Note that the test is performed when SiteMap tries to locate the correct menu, as opposed to If and Unless, which are tested after the proper Loc has been identified. Returning a false means that this Loc doesn’t match the request, so SiteMap will continue to search through your Menus to find an appropriate Loc. As an example, we could check to make sure that a given request comes from Opera (the Req object provides convenience methods to test for different browsers; see the Lift API for a full list) with the code in Listing 7.5.1↓.
Testing the Request
val onlyOpera = Test(req => req.isOpera)
val operaMenu = Menu(Loc("Opera", "opera" :: Nil, "Only Opera", onlyOpera))

7.5.2 LocGroup

The LocGroup param allows you to categorize your menu items. The Menu.group snippet (mentioned in Section 7.2.3↑) allows you to render the menu items for a specific group. A menu item may be associated with one or more groups. Simply add a LocGroup param with string arguments for the group names, as shown in Listing 7.5.2↓.
Categorizing Your Menu
val siteMenu = Menu(Loc(...,LocGroup("admin", "site")))
In your templates, you then specify the binding of the menu as shown in Listing 7.5.2↓. As you can see, we’ve also added a prefixed attribute to control the CSS class of the links ("a" is the only valid prefix), and we’ve added some body XHTML for display. In particular, the <menu:bind> tag controls where the menu items are rendered. If you don’t provide body elements, or if you provide body elements without the <menu:bind> element, your body XHTML will be ignored and the menu will be rendered directly.
Binding a Menu Group
<div class="site">
  <ul>
  <lift:Menu.group group="site"
    a:class="siteLink">
   <li><menu:bind /></li>
  </lift:Menu.group>
  </ul>
</div>

7.6 Writing Your Own Loc

As we’ve shown, there’s a lot of functionality available for your Menu items. If you need more control, though, the Loc trait offers some functionality, such as rewriting, that doesn’t have a direct correspondence in a LocParam element. The basic definition of a Loc implementation covers a lot of the same things. The following vals and defs are abstract, so you must implement them yourself:
Essentially, these mirror the params that are required when you use Loc.apply to generate a Loc. We’re going to write our own Loc implementation for our Expenses in this section to demonstrate how this works. Because this overlaps with existing functionality in the PocketChange application, we’ll be using a branch in the PocketChange app. You can pull the new branch with the command
git checkout --track -b custom-loc origin/custom-loc
You can then switch back and forth between the branches with the commands:
git checkout master
git checkout custom-loc

7.6.1 Corresponding Functions

Table 7.1↓ lists the LocParams and their corresponding methods in Loc, with notes to explain any differences in definition or usage. If yould prefer to use the LocParams instead, just define the params method on Loc to return a list of the LocParams you want.
Hidden N/A To make your Loc hidden, add a Hidden LocParam to your params method return value
If/Unless override testAccess You need to return an Either to indicate success (Left[Boolean]) or failure (Right[Box[LiftResponse]])
Template override calcTemplate Return a Box[NodeSeq]
Snippet and LocSnippets override snippets Snippet is a PartialFunction[String, Box[ParamType]), NodeSeq => NodeSeq], which lets you use the type-safe parameter to control behavior.
Title override title You can override "def title" or "def title(in: ParamType)" depending on whether you want to use type-safe parameters
Test override doesMatch_? It’s your responsibility to make sure that the path of the request matches your Loc, since this method is what SiteMap uses to find the proper Loc for a request
LocGroup override inGroup_? Nothing special here
Table 7.1 LocParam Methods in Loc

7.6.2 Type Safe Parameters

One of the nice features of Loc is that it allows you to rewrite requests in a type-safe manner. What this means is that we can define a rewrite function on our Loc instance that returns not only a standard RewriteResponse, but also a parameter that we can define to pass information back to our menu to control behavior. The reason that this is type-safe is that we define our Loc on the type of the parameter itself. For instance, let’s expand the functionality of our app so that we have a page called "acct" that shows the expense entries for a given account. We would like this page to be viewable only by the owner of the account under normal circumstances, but to allow them to share it with other members if they wish to. Let’s start by defining our type-safe parameter class as shown in Listing 7.6.2↓.
Defining AccountInfo
abstract class AccountInfo
case object NoSuchAccount extends AccountInfo
case object NotPublic extends AccountInfo
case class FullAccountInfo(account : Account,
                           entries : List[Expense]) extends AccountInfo
We define a few case classes to indicate various states. The FullAccountInfo holds the account itself as well as some flags for behavior. Now that we have our parameter type, we can start to define our Loc, as shown in Listing 7.6.2↓.
Defining a Type-Safe Loc
class AccountLoc extends Loc[AccountInfo] {
...
}
Assuming that an Account instance has a unique string ID, we would like to use URL rewriting so that we can access a ledger via "/acct/<unique id>". Our rewrite function, shown in Listing 7.6.2↓, handles a few different things at once. It handles locating the correct account and then checking the permissions if everything else is OK.
The Rewrite Function
override def rewrite = Full({
  case RewriteRequest(ParsePath(List("acct", aid), _, _, _), _, _) => {
    Account.findAll(By(Account.stringId, aid)) match {
      case List(account) if account.is_public.is => {
        (RewriteResponse("account" :: Nil),
         FullAccountInfo(account, account.entries))
      }
      case List(account) => {
        (RewriteResponse("account" :: Nil),
         NotPublic)
      }
      case _ => {
        (RewriteResponse("account" :: Nil),
         NoSuchAccount)
      }
    }
  }
})
Now that we’ve defined the transformation from URL to parameter, we need to define the behaviors based on that parameter. The account page will show a list of expense entries only if the account is located and is public. For this example we’ll use a single template and we’ll change the snippet behavior based on our parameter, as shown in Listing 7.6.2↓.
Defining Snippet Behavior
override def snippets = {
  case ("entries", Full(NoSuchAccount)) => {ignore : NodeSeq =>
    Text("Could not locate the requested account")}
  case ("entries", Full(NotPublic)) => {ignore : NodeSeq =>
    Text("This account is not publicly viewable")}
  case ("entries", Full(FullAccountInfo(account, List()))) => {ignore : NodeSeq =>
    Text("No entries for " + account.name.is)}
  case ("entries", Full(FullAccountInfo(account, entries))) =>
    Accounts.show(entries) _ 
}
In this example, we simply return some text if the Account can’t be located, isn’t public, or doesn’t have any Expense entries. Remember that this function needs to return a snippet function, which expects a NodeSeq parameter. This is why we need to include the ignore parameter as part of our closures. If our Account does have entries, we return a real snippet method defined in our Accounts object. In our template, we simply use an entries snippet tag, as shown in Listing 7.6.2↓.
Our Public Template
<lift:surround with="default" at="content">
  <lift:entries  eager_eval="true">
    <h1><lift:Menu.title /></h1>
    <lift:embed what="entry_table" />
  </lift:entries>
</lift:surround> 
We’re using our embedded table template for the body of the table along with the eager_eval attribute so that we can use the same markup for all occurrences of our expense table display. We can also define the title of the page based on the title parameter, as shown in Listing 7.6.2↓.
Defining the Title
override def title(param : AccountInfo) = param match {
  case FullAccountInfo(acct, _) => 
    Text("Expense summary for " + acct.name.is)
  case _ => Text("No account")
}

7.6.3 Dynamically Adding Child Menus

TBW

7.6.4 Binding Your Custom Loc

7.7 Conclusion

As we’ve shown in this chapter, SiteMap offers a wide range of functionality to let you control site navigation and access. You can customize the display of your individual items using the LinkText LocParam as well as through the functionality of the built-in Menu builder and item snippets. You can use the If and Unless LocParams to control access to your pages programmatically, and you can use the Test LocParam to check the request before it’s even dispatched. Page-specific rendering can be customized with the Template, Snippet, and LocSnippet LocParams, and you can group menu items together via the LocGroup LocParam. Finally, you can consolidate all of these functions by writing your own Loc trait subclass directly, and gain the additional benefit of type-safe URL rewriting. Together these offer a rich set of tools for building your web site exactly they way you want to.
Up: Part I

(C) 2012 Lift 2.0 EditionWritten by Derek Chen-Becker, Marius Danciu and Tyler Weir