Дослідження Lift

Редакція Lift 2.0

Написано

Derek Chen-Becker, Marius Danciu та Tyler Weir

Copyright © 2008, 2009, 2010, 2011 by Derek Chen-Becker, Marius Danciu, David Pollak, та Tyler Weir.
Ця робота ліцензована під Creative Commons Attribution-No Derivative Works 3.0 Unported License.
Переклад українською: Арсеній Чеботарьов, Ніжин, 2016
Домашня сторінка для Exploring Lift є http://exploring.liftweb.net. Там ви можете знайти актуальну копію тексту, а також посилання на списки розсилки, відсліджування проблем та джерельний код.
Зміст
Перелік малюнків

Присвячення

Derek хоче подякувати своїй жинці, Debbie, за її терпіння та підтримку під час написання цієї книжки. Він також хоче подякувати своїм двом синам, Dylan та Dean, за те, що підтримують речі цикавими та в перспективі.
Tyler хоче подякувати жінці, Laura, за те що надихала його.
Marius бажає подякувати його жінці, Alina, за її терплячість на протязі довгих вихідних, та мирилась з його односкладними відповідями на протязі роботи над цією книгою.

Подяки

Ця книга не була б можливою без розробників Lift и особливо David Pollak: без його в нас не було б такої можливості.
Нам також приємно подякувати спільноту Lift, так само, як наступних особистостей за значний внесок в вміст цієї книги: Adam Cimarosti, Malcolm Gorman, Doug Holton, Hunter Kelly, James Matlik, Larry Morroni, Jorge Ortiz, Tim Perrett, Tim Pigden, Dennis Przytarski, Thomas Sant Ana, Heiko Seeberger та Eric Willigers.
Величезна подяка Charles Munat за редагування цієї роботи, та до Tim Perrett за допомогу з REST API в Главі 13.

Частина I.  Основи

1 Ласкаво просимо до Lift!

Ласкаво просимо до Дослідження Lift. Ми створили цю книгу щоб просвітити вас щодо Lift, що, як ми вважаємо, є чудовим фреймворком для побудови конкурентноздатних веб застосувань.  Lift розроблений для того, щоб потужні технології зробити просто доступними, при цьому підтримуючи загальний фреймворк простим та гнучким. Це може здаватись як клише, але наш досвід роботи з Lift робить розробку приємною, оскільки він дозволяє сфокусуватись на цікавих частях кодування. Наша ціль для цієї книги кінець кінцем в тому, щоб ви були здатні та розвинути любе веб застосування, про які ви тільки можете подумати.

1.1  Чому Lift?

Для тих з вас, хто має досвід з іншими веб фреймворками, такими як Struts, Tapestry, Rails, і так далі, ви можете спитати себе, "Чому інший фреймворк? Чи Lift насправді вирішує проблеми інакше, або більш ефективно, ніж ті, що я використовував раніше?" Базуючись на нашому досвіді (та інших в зростаючій Lift спільноті), відповідь виразна, "Так!", Lift зібрав кращі ідеї з числа інших фреймворків, при цьому маючи власні іновативні ідеї. Це та комбінаія солідного підгрунтя та нових технолоній, що робить Lift таким потужним. В той же час, Lift зміг уникнути помилок, що робили в минулому інші фреймворки. В дусі “домовленості вище конфігурації”, Lift має розумні замовчання для всього, при цьому роблячи простим налаштування саме того, що вам треба: не більше і не менше. Пішли часа файлу XML після іншого файла XML, що провадять базову конфігурацію для вашого застосування. Замість цього, просте застосування на Lift потребує тільки додати LiftFilter до вашого web.xml, та додати один або більше рядків, що кажуть  Lift, в якому пакунку знаходиться ваші класи (Розділ 3.2↓). Методи, що ви кодуєте, не потрібні для реалізації специфічних інтерфейсів (що називаються трейтами), хоча є трейти підтримки, що роблять речі значно-значно простішими. Коротко кажучи, вам не треба нічого писати,  що напряму не вимагається для вирішуваної задачі. Lift призначений для роботи прямо з коробки, та щоб зробити вас таким ефективним та продуктивним, наскільки це можливо.
Одним з ключових сильних сторін Lift є ясне розділення презентації вмісту та логіки, базуючись на твердій концепції-шаблону  Model-View-Controller[A]    [A] http://java.sun.com/blueprints/patterns/MVC.html. Одна з оригінальних технологій Java веб застосувань, що доки використовується, є JSP, або Java Server Pages  [B]   [B] http://java.sun.com/products/jsp/. JSP дозволяє вам змішувати HTML та код Java напряму на сторінці. Хоча це спочатку може здатись за гарну ідею, на практиці це болюче. Покладати код на ваш рівень презентації ускладнює налаштування, та розуміння, що має відбуватись на сторінці, та робить більш складним для людей, що пишуть HTML частину, бо вміст не є валідним HTML. Хоча багато сучасних програмувацьких та HTML редакторів були модифіковані, щоб вміщати цю кашу, правильна підсвітка синтаксису та валідація не допомагають від переходу вперед та назад по одному або більше файлам, щоб слідувати за ходом сторінки. Lift приймає підхід, що на рівні презентації не повинно бути коду, та що рівень презентації має бути досить гнучким, щоб вміщувати любе можливе використання. З цього боку Lift використовує потужну систему шаблонів, à la Wicket  [C]   [C] http://wicket.apache.org/, щоб вбудовувати згенеровані користувачем дані на рівень презентації. Шаблони Lift побудований на обробці XML можливостей мови Scala[D]   [D] Не тільки Scala має екстенсивну підтримку бібліотеки для XML, але синтаксис XML насправді є частиною мови. Ми розглянемо це більш детально, коли ми дійдемо до цього, та дозволяє такі речі, як вкладені шаблони, просте введення згенерованого користувачем вмісту, та покращені можливості прикріплення даних. Для тих, хто прийшов з JSP, покращені шаблони Lift та обробка XML дозволяє вам просто писати власні бібліотеки тегів, витрачаючи тільки частину часу та зусиль.
Lift має інші переваги над багатьма іншими веб фреймворками: він розроблений спеціально для використання мови програмування Scala. Scala є відносно новою мовою програмування, розроблений Martin Odersky [E]  [E] Martin створив мову програмування Pizza, що призвело до проекту Generic Java (GJ), що був вбудований в Java 1.5. Його домашня сторінка  http://lamp.epfl.ch/~odersky/ та його група вивчення мов програмування на EPFL Switzerland. Scala компілюється в байткод Java та виконується на JVM, що означає, що ви можете задіяти екосистему Java бібліотек, так як можете з любим іншим веб фреймворком Java. В той же час Scala вводить дуже потужні можливості, розроблені щоб зробити вас, розробників, більш продуктивними. Серед ціх можливостей є екстремально багата система типів, разом з потужним виводом типів, природна обробка XML, повна підтримка для замикань та функцій як об'єктів, та екстенсивна високорівнева бібліотека. Потужність системи типів, разом з виводом типів, привело до того, люди називають Scala “статично-типована динамічна мова”  [F]   [F] http://scala-blogs.org/2007/12/scala-statically-typed-dynamic-language.html. Це означає, що ви пишете код так швидко, як ви можете з динамічно типізованими мовами (як Python, Ruby, etc.), але ви маєте безпеку типів часу компіляції, як в Java. Scala є також гибридною мовою функуціонально-програмування (FP) та об'єктно-орієнтованою мовою (OO), що означає, що в ви можете отримати потужність високорівневних функціональних мов, таких як Haskell або Scheme, при цьому зберігаючи модульність та повторне використання OO компонентів. Зокрема, концепція FP незмінності заохочується в Scala, що гарно підходить для написання високо-конкурентних програм, які досягають високої пропускної здатності. Гібридна модель також означає, що якщо ви не торкались до FP до цього, ви можете поступово наближатись до цього. За нашим досвідом, Scala дозволяє вам робити більше в Lift за допомогою меньшого числа рядів коду. Пам'ятайте, що Lift повністю про те, щоб зробити вас більш продуктивним!
Lift намагається охопити можливості в дуже компактній та прямолінійній манері. Потужна підтримка AJAX та Comet в Lift дозволяє вам використовувати можливості Web 2.0 з дуже невеликими зусиллями. Lift використовує бібліотеку Scala Actor, щоб провадити рушійний повідомленнями фреймворк для оновлень Comet. В більшості випадків, додавання підтримки Comet до сторінки потребує не більше, ніж розширити трейт[G]    [G]  Трейт є конструкцією Scala, що майже схоже на інтерфейс Java. Головна різниця в тому, що трейти можуть реалізовати методи та мати поля, щоб визначати метод відтворення вашої сторінки, та додавання додаткового виклику функції до ваших посилань, щоб направляти повідомлення оновлення. Lift обробляє все кодування бек-енд та на сторінці, щоб провадити опит Comet. Підтримка AJAX включає особливі обробники для виконання підписання форм AJAX через JSON, і майже кожна функція посилання може просто бути перетворена в версію AJAX в декілька торкань. Щоб виконати всі ці дива на стороні клієнта, Lift має ієрархію класів для інкапсуляції викликов JavaScript через JavaScript, jQuery, та YUI. Мила частина в тому, що ви також можете задіяти ці класи, так що код може бути згенерований для вас, та вам не потрібно класти логіку JavaScript в ваші шаблони.

1.2  Що потрібно знати перед початком

Перше і головне, це книга про фреймворк Lift. Є декілька речей, що ми очікуємо знання від вас, перед тим, як продовжувати:

1.3  Типографічні домовленості

Щоб краще обмінюватись концепціями та прийомами в цій книзі, ми адоптували наступні типографічні домовленості:
ClassNameМоноширинний текст використовується для позначення типів, імена класів, та іншої пов'язаної з кодом інформації.
...Крапки в лістингу коду використовуються для індикації пропущеного кода для зменшення лістингу. Якщо не вказане інше, приклади коду в цій книзі беруться з застосування PocketChange (Глава 2 на сторінці 1↓), що має повний код, доступний на GitHub.

1.4  Отримання додаткової інформаціїї щодо Lift

Lift має дуже активну спільноту користувачів та розробників. Починаючи з її створення на початку 2007, спільнота виросла до сотен членів по всьому світу. Лідер проекту, David Pollak [H]   [H] http://blog.lostlake.org/, постійно з'являється в списках розсилки, відповідає на запитання, та приймає запити на можливості. Є головна група розробників, хто робить над проектом, але доводи приймаються від кожного, хто має гарну мотивацію, та може перетворити її на гарний код. Хоча ми намагаємось викласти все, що вам треба знати, в цій книзі, є декілька додаткових ресурсів, доступних як джерело інформації щодо Lift:
  1. Перше місце, де треба шукати, це веб сайт Lift на http://liftweb.net/. Є посилання на безліч інформації на сайті. Зокрема:
    1. Lift Wiki розташований на http://www.assembla.com/wiki/show/liftweb. Wiki підтримує не тільки David, але також багато активних членів спільноти Lift, включаючи авторів. Частини книги надихнуті та позичені зі вмісту Wiki. Зокрема, там є посилання на всю згенеровану документацію, не тільки для стабільної гілки, але також для нестабільшої голови, якщо ви відчуваєте себе готовим до пригод. Є також екстенсивний розділ HowTo та статті по просунутим темам, що охоплюють значний обсяг інформації.
    2. Список розсилки http://groups.google.com/group/liftweb дуже активний, та якщо є речі, що не викладені в цій книзі, ви можете почуватись вільно запитувати питання там. Є багато дуже розумних людей, що можуть відповісти на ваші питання. Будь ласка, публікуйте окремі питання щодо книги до Lift Book Google Group на http://groups.google.com/group/the-lift-book. Будь-що інше, специфічне до Lift є чесною грою для списку розсилки.
  2. Tim Perrett, інший комітер Lift, пише книгу про Lift для Manning, що називається Lift in Action. Більше подробиць може бути знайдено на сайті сторінки http://www.manning.com/perrett/.
  3. Lift має канал IRC на irc://irc.freenode.net/lift, що звичайно має декілька людей в кожний окремий момент. Це чудове місце для балачок щодо проблем та ідей навколо Lift.

1.5  Ваше перше застосування Lift

Ми казали багато про Lift та його можливості, так що тепер докладемо зусиль, та спробуємо створити застосування. Однак перед тим, як ми почнемо, ним треба потурбуватись про попередні вимоги:
Java 1.5 JDK  Lift виконується на Scala, що робить на which runs JVM. Перша річ, що вам треба встановити, це сучасна версія Java SE JVM, доступна на http://java.sun.com/. Останні компілятори Scala змінили цільову версію Java на 1.5. Версія 1.4 все що доступна в цій якості, але ми будемо вважати, що ви використовуєте 1.5. Приклади в цій книзі були протестовані з версією Sun JDK, хоча більшість інших версій (як Blackdown або OpenJDK) повинні також робити, без, або з мінімальними, змінами. 
Maven 2  Maven є інструмент керування проектами, що має екстенсивні можливості для побудови, управління залежностями, тестування та звітності. Ми очікуємо, що ви знайомі з основним застосуванням Maven для компіляції, пакування та тестування. Якщо ви не використовували Maven перед цім, ви можете отримати короткий огляд в додатку A↓. Ви можете завантажити останню версію Maven з http://maven.apache.org/. Короткі інструкції по встановленню (досить, як для початку) є на сторінці завантаження http://maven.apache.org/download.html.
Програмний редкатор  Це не є жорсткою вимогою для цього прикладу, але ми почнемо кодувати, дуже корисно мати щось більше придатне, ніж Notepad. Якщо ви бажаєте повнофункціональну IDE, з підтримкою таких речей, як налаштування, постійна перевірка компіляції, таке інше, тоді вас зацікавлять плагіна на сайті Scala http://www.scala-lang.org/node/91. Підтримка плагінів:
Тепер, коли ми маємо передумови, прийшов час починати. Ми бажаємо задіяти архитипи Maven [I]   [I]Архитип, просто кажучи, це шаблон проекту для Maven, що провадить налаштування базових атрибутів з командного рядка. Це зробить до  99% роботи цього прикладу. Перше, змінимо каталог до того, де ми бажаємо працювати:
cd work
Далі використаємо команду Maven archetype:generate для створення скелету нашого проекту:
mvn archetype:generate -U \
  -DarchetypeGroupId=net.liftweb \
  -DarchetypeArtifactId=lift-archetype-blank \
  -DarchetypeVersion=2.0 \
  -DarchetypeRepository=http://scala-tools.org/repo-releases \
  -DgroupId=demo.helloworld \
  -DartifactId=helloworld \
  -Dversion=1.0-SNAPSHOT
Maven повинен вивести декілька сторінок тексту. Він має зупинитись, та запитати підтвердити опції конфігурації, в якому випадку вам просто треба натиснути <enter>. На кінці ви повинні отримати повідомлення, що каже BUILD SUCCESSFUL. Тепер ви успішно створили ваш перший проект! Не вірите? Давайте виконаємо та переконаємось:
cd helloworld
mvn jetty:run
Maven має зпродукувати більше тексту, що завершується так
[INFO] Starting scanner at interval of 5 seconds.
Це означає, що тепер ми маємо веб сервер (Jetty [J]   [J] http://www.mortbay.org/jetty/), що виконується на порту 8080 вашої машини. Просто перейдіть на http://localhost:8080/, і ви побачите вашу першу сторінку Lift, стандартний “Hello, world!”. За допомогою всього декількох простих команд, ми побудували фунціональне (хоча і обмежене) веб застосування. Давайте пройдемо трохи глибше по деталях, та побачимо, саме як ці частини пасують одне до одного. Перше, давайте перевіримо сторінку index. Коли Lift обслуговує запит, де URL завершується на косу лінію, Lift автоматично шукає файл з назвою index.html[K]   [K] Технічно, він також шукає деякі варіації index.html, включаючи любі локалізовані версії сторінки, але ми розглянемо це пізніше в розділі в цьому каталозі. Наприклад, якщо ви спробуєте перейти до http://localhost:8080/test/, Lift буде шукати index.html в каталозі  test/ вашого проекту. Джерела HTML будуть розміщені в src/main/webapp/ в каталозі вашого проекту. Ось файл index.html з нашого проекта Hello World:
1
2
3
4
<lift:surround with="default" at="content">
  <h2>Welcome to your project!h2>
  <p><lift:helloWorld.howdy />p>
lift:surround>
Це може виглядати дивно на перший погляд. Для тих, що мають деякий досвід XML, ви можете розпізнати використання префіксних елементів. Для тих, хто не знаю знає, що таке префіксні елементи, це елемент в формі
<prefix:element>
В нашому випадку ми маємо два елементи : <lift:surround> та
<lift:helloWorld.howdy />. Lift надає особливого значення елементам, що мають префікс “lift”: вони формують базис екстенсивної підтримки шаблонів lift, що ми побачимо більш детально в розділі 4.1↓. Коли lift обробляє шаблон XML, він робить це з найширшого елемента всередину. В нашому випадку зовнішній елемент є <lift:surround with=”default” at=”content”>. Елемент <lift:surround> в основному каже Lift знайти шаблон, що вказаний атрибутом with (default, в нашому випадку) та покласти вміст нашого елемента всередину цього шаблону. Атрибут at каже Lift де в шаблоні покласти наш вміст. В Lift це “заповнення проміжків” називається біндінгом (прив'язкою), та є фундаментальною конфепцією системи шаблонів Lift. Майже все на рівні HTML/XML може розглядатись як вкладені біндінги. Перед тим, як перейти до елементу <lift:helloWorld.howdy/>, давайте поглянемо на шаблон по замовчанню. Ви можете знайти його в каталозі templates-hidden веб застосування. Подібно до WEB-INF та META-INF каталоги в веб застосуваннях Java, вміст templates-hidden не може бути доступною напряму з боку клієнтів; вони можуть, однак, бути доступними, коли воми мають посилання в елементі <lift:surround>. Ось вміст файлу default.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  
    ="content-type" content="text/html; charset=UTF-8" />
    ="description" content="" />
    ="keywords" content="" />
                 
    demo.helloworld</code><code class="scala keyword">:</code><code class="scala plain">helloworld</code><code class="scala keyword">:</code><code class="scala value">1.0</code><code class="scala plain">-SNAPSHOT
    
  
  
    :bind name="content" />
    :Menu.builder />
    :msgs/>
  
Як ви бачите з цього листингу, це відповідний файл XHTML, з тегами , , та . Це необхідно, бо Lift не додає їх автоматично. Lift просто обробляє XML з кожного шаблона, на який він натрапить. Елемент та його вміст є заглушкою; цікаві речі відбуваються в елементі . Тут є три елементи:
  1. Елемент визначає, де буде прив'язано (вставлено) вміст нашого файлу index.html. Атрибут name повинен співпадати з відповідним атрибутом at нашого елемента .
  2. Елемент   є спеціальним елементом, що будує меню, базуючись на SiteMap (що буде пояснений в главі 7↓). SiteMap є високорівневим компонентом директорії сайту, що не тільки провадить централізоване місце для визначення меню сайту, але дозволяє вам контролювати, коли відображувати окремі посилання (базуючись на, скажімо, чи користувач увійшов в систему, та яку роль він має), та провадить механізм контроля доступу рівня сторінки.
  3. Елемент дозволяє Lift (або вашому коду) відображувати повідомлення на сторінці по мірі її відображення. Це можуть бути повідомленнями статуса, повідомленнями про помилки, тощо. Lift має механізми для встановлення одного або більше повідомлень з логіки вашого коду.
Тепер давайте подивимось назад на елемент з файлу index.html. Цей елемент (та, насправді, елемент ) називається фрагментом, та має форму

Тут class є ім'ям класу Scala, визначеним в нашому проекті в пакунку demo.helloworld.snippets та method є методом, визначеним в цьому класі. Lift робить невелику трансляцію імені файлу, щоб поміняти camel-case назад до title-case, та потім шукає клас. В нашому демо клас розташований в src/main/scala/demo/helloworld/snippet/HelloWorld.scala, та показаний тут:
1
2
3
4
5
6
package demo.helloworld.snippet
class HelloWorld {
  def howdy = Welcome to helloworld at
    {new _root_.java.util.Date}
}
Як ви можете бачити, метод howdy є досить прямолінійним. Lift прикріплює результат виконання метода (в цьому випадку span) в місце, де стоїть елемент фрагменту. Є цікавим зауважити, що матод сам по собі може повертати інші елементи у вмісті, і вони також будуть оброблені. Ця рекурсивна природа композиції є частиною фундаментальної потужності Lift; це позначає, що результуючі фрагменти та уривки шаблонів у вашому застосуванні є загалом довільними. Вам не треба ніколи не писати ту ж функціональність більше одного разу.
Тепер, коли ми розглянули всі справжні елементи вмісту, фінальний уривок пазлу є клас Boot. Клас Boot відповідальний з конфігурацію і встановлення фреймворку Lift. Як ми казали раніше в цій главі, більшість Lift має розумні замовчання, так що клас Boot в головному містить тільки додатки, що вам потрібні. Клас Boot завждирозташовується в пакунку bootstrap.liftweb, та показаний тут:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package bootstrap.liftweb
import net.liftweb.util._
import net.liftweb.http._
import net.liftweb.sitemap._
import net.liftweb.sitemap.Loc._
import Helpers._
  
/**
  * Клас, що створюється рано, та дозволяє застосуванню
  * модифікувати оточення Lift
  */
class Boot {
  def boot {
    // де шукати фрагмент
    LiftRules.addToPackages("demo.helloworld")    
    // будуємо SiteMap
    val entries =
      Menu(Loc("Home", List("index"), "Home")) ::
      Nil
    LiftRules.setSiteMap(SiteMap(entries:_*))
  }
}
Є два основні базові елементи конфігурації, розміщені в методі boot. Перший є метод LiftRules.addToPackages. Він вказує lift базувати свій пошук в пакунку demo.helloworld. Це означає, що фрагменти будуть розміщені в пакунку demo.helloworld.snippets, перегляди (розділ 4.4↓) буде розміщені в пакунку demo.helloworld.views, таке інше. Якщо ви маєте більше однієї ієрархії (тобто декілька пакунків), ви можете просто викликати addToPackages декілька разів. Другий елемент в класі Boot є встановлення  SiteMenu. Очевидно, в цьому демо досить просте меню, але ми будемо розглядати більш цікаві приклади в главі SiteMap.
Тепер, коли ми розглянули базовий приклад, ми сподіваємось ви починаєте розуміти, чому Lift є таким потужним, та чому він робить вас більш продуктивним. Ми тільки пошкрябали поверхню шаблонів Lift та можливостей прив'язування, але те що ми показали є вже великий крок. Це лише десять рядків коду Scala, та майже тридціти в XML, та ми вже маємо функціональний сайт. Якщо ми бажаємо додати більше сторінок, ми вже маємо встановлений набір шаблонів по замовчанню, так що нам не треба малювати той же самий рутинний HTML декілька разів. В нашому прикладі ми напряму генеруємо для нашого фрагменту helloWorld.howdy, але в подальших прикладах ми покажемо, як просто підтягувати вміст з самого шаблону у фрагмент, та модифікувати його за потреби.
В наступних главах ми розглянемо:
Ми сподіваємось, що ви також захоплені щодо розпочати з Lift, як і ми!

2  PocketChange

Як шлях до демонстрації концепцій цієї книги, ми збираємось побудувати базове застосування, та потім розбудовувати його по мірі просування. З його роозвитком ви будете розуміти все більше про Lift. Застосування, що ми обрали, називається Expense Tracker, та присвячене персональним фінансам. Ми назвали його PocketChange.
figure images/pocketchange.png
Малюнок 2.1 Застосування PocketChange
PocketChange буде відстежувати ваші витрати, підраховувати, скільки ви наразі витрачаєте, дозволить організувати ваші дані з використанням тегів, та допоможе візуалізувати ваші дані. На протязі подальших глав книги ми додамо декілька цікавих можливостей, таких, як AJAX діаграми, та дозволимо декілька людей на один рахунок (з допомогою Comet оновлення сутностей). Крім цього ми бажаємо утримувати інтерфейс тонким та ясним.
Ми збираємось використовувати шаблон View First для розробки нашого застосування, оскільки розділення в Lift презентації та логіки через шаблони, перегляди та фрагменти добре лягає на шаблон View First. Щоб прочитати чудовий папір щодо рішень дизайну за підходом Lift до шаблонів та логіки, прочитайте статтю David Pollak: Lift View First у Wiki  [L]   [L] http://www.assembla.com/wiki/show/liftweb/View_First.
Інша важлива річ, що треба занотувати, полягає в тому, що ми збираємось зробити обліт застосування, та торкнутися багатьох деталей. Ми надамо безліч посилань на глави, де ці речі розкриваються. Ця глава насправді призначена тільки для надання вам смаку Lift, так що вільно зазирайте наперед, якщо бажаєте знати, як щось працює. Повний код джерела цілого застосування PocketChange доступний на GitHub  [M]   [M] http://github.com/tjweir/pocketchangeapp. Досить балаканини, слід вирушати!

2.1  Визначення моделі

Перший крок, що ми зробимо - це визначимо сутності бази даних, що ми збираємось використовувати в нашому застосуванні. Базова функціональність відслідкованих категоризованих витрат включає наступні елементи:
Ми почнемо з User, як показано в лістингу 2.1↓. Ми використаємо клас Lift MegaProtoUser (Розділ 8.2.8 сторінка 1↓), щоб обробляти маже все, що нам треба для керування користувачами. Наприклад, з допомогою коду, що ви бачите, ми визначаємо цілий менеджмент користувачами для нашого сайту, включаючи сторінку реєстрації, сторінку втрати пароля, та сторінку входу. Супроводжуючі меню  SiteMap (Розділ 7 сторінка 1↓) генеруються одним викликом  User.siteMap. Як ви можете бачити, ми можемо налаштувати згенерований XHTML, що призначений для сторінок управління користувачами, за допомогою декількох простих def. Можливості для налаштування, що провадить MetaMegaProtoUser, є потужними.
The PocketChange User Entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.pocketchangeapp.model
// Імпорт всіх класів-мепперів
import _root_.net.liftweb.mapper._
// Створюємо клас User розширюючи базовий клас Mapper
// MegaProtoUser, що провадить поля по замовчанню, та методи
// для користувача сайту.
class User extends MegaProtoUser[User] {
  def getSingleton = User // посилання на об'єкт-компанйон
  def allAccounts : List[Account] =
    Account.findAll(By(Account.owner, this.id))
}
// Створення об'єкта-компанйона класу User (вище).
// Цей об'єкт містить глобальні методи та полі, як find,
// dbTableName, dbIndexes, etc.
object User extends User with MetaMegaProtoUser[User] {
  override def dbTableName = "users" // визначаємо ім'я таблиці
  // Провадимо власний шаблон для сторінки-логіну
  override def loginXhtml =
    :surround with="default" at="content">
      { super.loginXhtml }
    :surround>
  // Провадимо власний шаблон для сторінки входу
  override def signupXhtml(user: User) =
    :surround with="default" at="content">
      { super.signupXhtml(user) }
    :surround>
}
Зауважте, що ми також додали службовий метод allAccounts до класу User, щоб отримувати всі рахунки для даного користувача. Ми використовуємо метод MetaMapper.findAll, щоб зробити запит по ID власника (Розділ 8.1.8 сторінка 1↓), надаючи ID користувача, як ID власника.
Визначення сутності Account трохи складніше, як показан в Лістингу 2.1↓. Тут ми визначаємо клас з первинним ключем Long , та деякими полями, асоційованими з рахунками. Ми також визначаємо деякі допомоіжні методи для поєднань об'єктів в джойни (Розділ 8.1.11 сторінка 1↓). Сутності Expense та Tag (разом з деякими допоміжними) послідують піздніше, так що ми доки їх не розглядатимемо. 
Сутність PocketChange Account
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.pocketchangeapp.model
import _root_.java.math.MathContext
import _root_.net.liftweb.mapper._
import _root_.net.liftweb.util.Empty
// Клас Account розширює, що відображується на трей в БД з РК Long
// та міксується в трейт IdPK, що додає РК "id"
class Account extends LongKeyedMapper[Account] with IdPK {
  // визначаємо синглтон
  def getSingleton = Account
  // визначаємо відношення many-to-one з User
  object owner extends MappedLongForeignKey(this, User) {
    // додаємо індекс для цього стовпчика
    override def dbIndexed_? = true
  }
  // Поле контроля доступом, відключений до розгляду SiteMap
// Власник Account зможе розшарювати перегляд на рахунок
  object is_public extends MappedBoolean(this) {
    override def defaultValue = false
  }
  // Поле для утримання балансу, до 16 розрядів (DECIMAL64) та 2 після коми
  object balance extends MappedDecimal(this, MathContext.DECIMAL64, 2)
   
  object name extends MappedString(this,100)
  object description extends MappedString(this, 300)
   
  // Службові методи для спрощеного доступу, ми дойдемо до
  // них в главі про Mapper
  def admins = AccountAdmin.findAll(By(AccountAdmin.account, this.id))
  def addAdmin (user : User) =
    AccountAdmin.create.account(this).administrator(user).save
  def viewers = AccountViewer.findAll(By(AccountViewer.account, this.id))
  def entries = Expense.getByAcct(this, Empty, Empty, Empty)
  def tags = Tag.findAll(By(Tag.account, this.id))
  def notes = AccountNote.findAll(By(AccountNote.account, this.id))
}
// Об'єкт-компан'йон
object Account extends Account with LongKeyedMetaMapper[Account] {
  // Метод для пошуку рахунку по власнику та імені
  def findByName (owner : User, name : String) : List[Account] =
    Account.findAll(By(Account.owner, owner.id.is), By(Account.name, name))
  ... ще методи ...
}

2.2 Наш перший шаблон

Наступний крок це з'ясувати, як презентувати дані користувачеві. Ми бажаємо отримати головну сторінку сайту, що відображує, в залежності від того, чи користувач увійшов в систему, або запрошення, або підсумок балансу рахунків, з місцем, щоб внести нові витрати. Лістинг 2.2↓ показує базовий шаблон, що робить саме це. Ми зберігаємо його як index.html. Уважний читач помітить, що ми маємо елемент заголовку, але не тіло. Це XHTML, так як це робить? Цей шаблон використовує тег (Розділ 4.5.17 сторінка 1↓) для встроювання себе в головний шаблон (/templates_hidden/default). Lift, насправді, робить те, що називаєтся “злиття заголовка” (Розділ сторінка 1↓) для злиття вмісту тегу head в вашому шаблоні нижче, з елементом head головного шаблона. Теги та є викликами до методів фрагментів. Фрагменти є кодом підтримки Scala, що провадить дійсну логіку сторінки. Ми розглянемо їх в наступному розділі.
The Welcome Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<lift:surround with="default" at="content">
<head>
  
  <script type="text/javascript" src="/scripts/date.js">script>
  
  
  <script type="text/javascript" src="/scripts/jquery.datePicker.js">
  script>
  <link rel="stylesheet" type="text/css" href="/style/datePicker.css" />
head>
    
    <lift:HomePage.summary>
      <div class="column span-24 bordered">
        <h2>Summary of accounts:h2>
        <account:entry>
          <acct:name /> : <acct:balance /> <br/>
        account:entry>
      div>  
      <hr />
    lift:HomePage.summary>
    <div class="column span-24">
      
      <lift:AddEntry.addentry form="POST">
        <div id="entryform">
          <div class="column span-24"><h3>Entry Formh3>
            <e:account /> <e:dateOf /> <e:desc /> <e:value />
            <e:tags/><button>Add $button>
          div>
        div>
      lift:AddEntry.addentry>
    div>
    <script type="text/javascript">
      Date.format = ’yyyy/mm/dd’;
      jQuery(function () {
        jQuery(’#entrydate’).datePicker({startDate:’00010101’,
                                        clickInput:true});
      })
    script>
lift:surround>
Як ви можете бачити, у свьому нашому шаблоні немає жодної логіки, тільки добре сформований XML та деякий JavaScript, щоб активізувати функціональність  jQuery datePicker.

2.3  Написання фрагментів

Тепер, коли ми маємо шаблон, нам треба написати фрагменти HomePage та AddEntry, так, щоб ми могли дещо робити на сайті. Перше, давайте подивимось на фрагмент HomePage, показаний на Лістингу 2.3↓. Ми пропустили стандартні заголовки Lift (Лістинг 3.3↓) щоб зберігти місце. Але ми залишили імпорт java.util.Date та всі наші класи моделей.
Визначення фрагменту Summary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.pocketchangeapp.snippet
import ... стандартні імпорти ...
import _root_.com.pocketchangeapp.model._
import _root_.java.util.Date
class HomePage {
  // User.currentUser повертає об'єкт "Box", що або Full
  // (містить User), Failure (містить помилку), або Empty.
  // Метод Scala match служить для вибору дії на основі того, чи
  // це Full. Дивіться Box в Lift API.
  def summary (xhtml : NodeSeq) : NodeSeq = User.currentUser match {
    case Full(user) => {
      val entries : NodeSeq = user.allAccounts match {
        case Nil => Text("Ви не маєте рахунків")
        case accounts => accounts.flatMap({account =>
          bind("acct", chooseTemplate("account", "entry", xhtml),
               "name" -> ={"/account/" + account.name.is}>
               "balance" -> Text(account.balance.toString))
        })
      }
      bind("account", xhtml, "entry" -> entries)
    }
    case _ => :embed what="welcome_msg" />
  }
}
Наш перший крок використати метод User.currentUser (провадиться трейтом MetaMegaProtoUser), щоб визначити, чи хтось ввійшов в систему. Цей метод повертає “Box,” що або Full (з User) або Empty. (Третій варіант Failure, але ми поки проігноруємо це). Якщо там є користувач, то він є володарем сессії і ми використовуємо метод User.allAccounts для отримання List з усіх рахунків користувача. Якщо користувач не має рахунків, ми повертаємо текстовий вузол XML, що буде закріплений там, де був наш техт в шаблоні. Якщо користувач мар рахунки, ми відображуємо їх на XHTML з використанням функції. Для кожного рахунка ми прикріплюємо ім'я рахунка де ми в шаблоні визначили тег , та баланс, де ми визначили . Результуючий List з XML NodeSeq входжень використовується для заміни елементу в шаблоні. Нарешті, ми перевіряємо випадок, коли користувач не війшов, вставляючи вміст шаблону запрошення (що може бути далі оброблений). Зауважте, що ми можемо вставляти теги Lift таким чином, і вони будуть рекурсивно оброблені.
Звичайно, це не видається дуже корисним - відтворювати баланс рахунків, коли ми не в змозі додавати витрати, так що давайте визначимо фрагмент AddEntry. Код показаний в Лістингу 2.3↓. Це виглядає інакше, ніж фрагмент HomePage, загалом тому що ми використовуємо StatefulSnippet (Розділ 5.3.3 сторінка 1↓). Первинна різниця в тому, що за допомогою StatefulSnippet використовується той же самий “примірник” фрагменту для кожного запиту в окремій сесії, так що ви можете зберігати змінні, в разі, якщо нам треба, щоб користувач щось виправив на формі. Базова структура фрагменту та ж сама, як і для підсумку: ми робимо деяку роботу (ми опишемо функцію doTagsAndSubmit через момент), та потім прив'язуються значення назад до шаблону. В цьому фрагменті, однак, ми використовуємо методи SHtml.select та SHtml.text для генерації полів форми. Поля text просто приймають початкове значення та функцію (замикання), щоб обробити отримане значення. Поле select є трохи більш складним, оскільки ви отримуєте перелік опцій, але в іншому концепція та ж сама.
Фрагмент AddEntry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.pocketchangeapp.snippet
import ... standard imports ...
import com.pocketchangeapp.model._
import com.pocketchangeapp.util.Util
import java.util.Date
/* date | desc | tags | value */
class AddEntry extends StatefulSnippet {
  // Ці мапи елементу "addentry" XML до методу "add" нижче
  def dispatch = {
    case "addentry" => add _
  }
  var account : Long = _
  var date = ""
  var desc = ""
  var value = ""
  // S.param("tag") повертає "Box" та метод "openOr" повертає або
  // вміст цього "ящика" (якщо він "Full"), або пустий String, якщо
  // Box є "Empty". Метод S.param повертає параметри, передані
  // переглядачем. В цьому випадку параметру є "tag"
  var tags = S.param("tag") openOr ""
   
  def add(in: NodeSeq): NodeSeq = User.currentUser match {
    case Full(user) if user.editable.size > 0 => {
      def doTagsAndSubmit(t: String) {
        tags = t
        if (tags.trim.length == 0)
          S.error("We’re going to need at least one tag.")
        else {
          // Трактуємо дату коректно, вона іде як yyyy/mm/dd
          val entryDate = Util.slashDate.parse(date)
          val amount = BigDecimal(value)
          val currentAccount = Account.find(account).open_!
          // Нам треба визначити останній послідовний номер та баланс
          // для дати. Цей метод повертає два значення, що кладуться
          // в entrySerial та entryBalance відповідно
          val (entrySerial, entryBalance) =
            Expense.getLastExpenseData(currentAccount, entryDate)
       
          val e = Expense.create.account(account)
                    .dateOf(entryDate)
                    .serialNumber(entrySerial + 1)
                    .description(desc)
                    .amount(BigDecimal(value)).tags(tags)
                    .currentBalance(entryBalance + amount)
          // Метод валідації повертає Nil, якщо немає помилок, та
          // повідомлення помилки, якщо помилки знайдені
          e.validate match {
            case Nil => {
              Expense.updateEntries(entrySerial + 1, amount)
              e.save
              val acct = Account.find(account).open_!
              val newBalance = acct.balance.is + e.amount.is
              acct.balance(newBalance).save
              S.notice("Entry added!")
              // Видалення стану цього фрагменту
              unregisterThisSnippet()
            }
            case x => error(x)
          }
        }
      }
      val allAccounts =
        user.allAccounts.map(acct => (acct.id.toString, acct.name))
      // Розбір через NodeSeq, переданий як "in", шукаючи теги з
      // префіксом "e". Коли знайдено, замінюємо тег на NodeSeq
      // відповідно до мари нижче (name -> NodeSeq)
      bind("e", in,
        "account" -> select(allAccounts, Empty,
                            id => account = id.toLong),
        "dateOf" -> text(Util.slashDate.format(new Date()).toString,
                         date = _,
                         "id" -> "entrydate"),
        "desc" -> text("Item Description", desc = _),
        "value" -> text("Value", value = _),
        "tags" -> text(tags, doTagsAndSubmit))
    }
    // Якщо немає користувача повертаємо пустий вузол Text
    case _ => Text("")
  }
}
Функція doTagsAndSubmit є новим додатком. Його головне призначення обробляти прислані дані, створити та перевірити сутність Expense, та потім повернутись до користувача. Цей шаблон визначення локальної функції для обробки форми є повністю загальною, на відміну до визначення метода в вашому класі. Головна причина цього в визначенні функції локально, та вона замиканнями для любих змінних, визначених в полі зору вашої функції фрагменту.

2.4 Маленька перчинка AJAX

Доки все ішло досить стандартною платою, так що просунемось трохи далі, та покажемо деякі більш складну функціональность. Лістинг 2.4↓ показує шаблон для відтворення таблиці з Expense для користувача, з опціональними датами початка та кінця. Фрагмент Accounts.detail буде визначеня пізніше в цьому розділі.
Відображення таблиці Expense
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<lift:surround with="default" at="content">
  <lift:Accounts.detail eager_eval="true">
  <div class="column span-24">
  <h2>Summaryh2>
    <table><tr><th>Nameth><th>Balanceth>tr>
      <tr><td><acct:name />td><td><acct:balance />td>tr>
    table>
  <div>
    <h3>Filters:h3>
    <table><tr><th>Start Dateth><td><acct:startDate />td>
               <th>End Dateth><td><acct:endDate />td>tr>
    table>
  div>
  <div class="column span-24" >
    <h2>Transactionsh2>
    <lift:embed what="entry_table" />
  div>
  lift:Accounts.detail>
lift:surround>
Тег (Розділ 4.5.7 на сторінці 1↓) дозволяє вам включати інший шаблон в цій точці. В нашому випадку, шаблон entry_table показаний в Лістингу 2.4↓. Це дійсно тільки фрагмент, та не призначений бути використаний окремо, бо це не повний документ  XHTML, та він не оточує себе головним шаблоном. Однак він провадить точки прикріплення, які ми можемо заповнити.
Вбудована таблиця Expense
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table class="" border="0" cellpadding="0" cellspacing="1"
    width="100%">
  <thead>
    <tr>
      <th>Dateth><th>Descriptionth><th>Tagsth><th>Valueth>
      <th>Balanceth>
    tr>
  thead>
  <tbody id="entry_table">
    <acct:table>
      <acct:tableEntry>
    <tr><td><entry:date />td><td><entry:desc />td>
        <td><entry:tags />td><td><entry:amt />td>
        <td><entry:balance />td>
    tr>
      acct:tableEntry>
    acct:table>
  tbody>
table>
Перед тим, як ми дойдемо до частини AJAX коду, давайте визначимо допоміжний метод в нашому фрагменті класу Account, показаний в Лістингу 2.4↓, щоб згенерувати таблицю XHTML з елементів, що ми будемо відображувати (звичайні імпорти опущені). Загалом, ця функція заштовхує вміст тегу (через метод  Helpers.chooseTemplate, Розділ C.8 на сторінці 1↓), та прикріплює кожний Expense з провадженого списку до нього. Як ви можете бачити в шаблоні entry_table, що відповідає до одного рядка таблиці для кожного елементу.
Допоміжна функція таблиці
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.pocketchangeapp.snippet
... imports ...
class Accounts {
  ...
  def buildExpenseTable(entries : List[Expense], template : NodeSeq) = {
    // Повторно викликає bind, для кожного Entry в entries
    entries.flatMap({ entry =>
      bind("entry", chooseTemplate("acct", "tableEntry", template),
           "date" -> Text(Util.slashDate.format(entry.dateOf.is)),
           "desc" -> Text(entry.description.is),
           "tags" -> Text(entry.tags.map(_.tag.is).mkString(", ")),
           "amt" -> Text(entry.amount.toString),
           "balance" -> Text(entry.currentBalance.toString))
   })
  }
  ...
}
Фінальна частина нашого уривка Accounts.detail, показана в Лістингу 2.4↓. Ми почнемо з деяких шаблонних викликів щоб знайти Account для перегляду, потім ми визначаємо деякі змінні, щоб зберігати стан. Важливо, що це var, так що вони можуть бути захоплені в замиканнях entryTable, updateStartDate, та updateEndDate, так само, як полями форми AJAX, що ми визначаємо. Єдина магія, що ми маємо використовувати, це генератор поля форми SHtml.ajaxText (Глава 11↓), що перетворить наші замикання оновлень в зворотні виклики AJAX. Значення, що повертаються з ціх зворотніх викликів є код JavaScript, що буде виконуватись на стороні клієнта. Ви можете бачити, що є декілька рядків кода, що ми маємо на сторінці, що будуть автоматично оновлювати нашу таблицю Expense, коли ми встановимо початкову та кінечну дату!
Наш AJAX уривок
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.pocketchangeapp.snippet
import ... standard imports ...
import com.pocketchangeapp.model._
import com.pocketchangeapp.util.Util
class Accounts {
  def detail (xhtml: NodeSeq) : NodeSeq = S.param("name") match {
    // Якщо параметр "name" був переданий переглядачем...
    case Full(acctName) => {
      // Шукаємо рахунок по імені для поточного користувача
      Account.findByName(User.currentUser.open_!, acctName) match {
        // Якщо рахунок повернено (як List)
        case acct :: Nil => {
          // Деякий стан замикання для викликів AJAX
          // Ось Lift-івський "Box" в дії: ми створюємо
          // змінні, щоб містити Date Boxes та ініціюємо його
          // як "Empty" (Empty є субкласом Box)
          var startDate : Box[Date] = Empty
          var endDate : Box[Date] = Empty
          // Допоміжний методи AJAX. Визначені тут для захоплення
          // змінних замикання вище
          def entryTable = buildExpenseTable(
            Expense.getByAcct(acct, startDate, endDate, Empty),
            xhtml)
          def updateStartDate (date : String) = {
            startDate = Util.parseDate(date, Util.slashDate.parse)
            JsCmds.SetHtml("entry_table", entryTable)
          }
          def updateEndDate (date : String) = {
            endDate = Util.parseDate(date, Util.slashDate.parse)
            JsCmds.SetHtml("entry_table", entryTable)
          }
          // Прикріплення даних до переданих в XML eелементів з
          // префіксом "acct", відповідно до мапи нижче.
          bind("acct", xhtml,
            "name" -> acct.name.asHtml,
            "balance" -> acct.balance.asHtml,
            "startDate" -> SHtml.ajaxText("", updateStartDate),
            "endDate" -> SHtml.ajaxText("", updateEndDate),
            "table" -> entryTable)
        }
        // Ім'я рахунку запроваджені, але не співпали з жодним
        // рахунком користувачів
        case _ => Text("Could not locate account " + acctName)
      }
    }
    // S.param "name" був порожнім
    case _ => Text("No account name provided")
  }
}

2.5 Висновок

Ми надіємось, що цей розділ продемонстрував, яким потужним може бути Lift, при цьому залишаючись стислим та простим до використання. Не турбуйтесь, якщо щось залишилось незрозумілим, ми пояснимо це більш детально по мірі просування далі. Ми продовжимо розширювати цей приклад на протязі книги, так що почувайтесь вільно використовувати  цю главу для посилання, або зробіть власну версію PocketChange з git репозитарія, за допомогою наступної команди (вважаємо, ви маєте встановлений git):
git clone git://github.com/tjweir/pocketchangeapp.git
Тепер час зануритись!

3 Основи Lift

В цій главі ми охопимо деякі з фундаментальних аспектів написання застосувань lift, включаючи архитектуру бібліотеки Lift, та як обробляти запити.

3.1  Введення в Lift

Перший крок в обробці запиту Lift є перехоплення запиту HTTP. В оригінала Lift використовував примірник java.servlet.Servlet для обробки надходящих запитів. Це було змінене для використання примірника java.servlet.Filter instance [N]   [N]Ви можете бачити дискусію в списці поштової розсилки Lift, що призвело до цієї зміни: http://tinyurl.com/dy9u9d, оскільки це дозволяє контейнеру обробляти любі запити, що не робить Lift (зокрема, статичний вміст). Фільтр діє як тонка огортка зверху існуючого LiftServlet (який все ще робить всю роботу), так що не будьте спантеличені, коли подивитесь на Lift API, та побачите обоє класи (LiftFilter та LiftServlet). Головна річ, що треба запам'ятати, це що ваш web.xml повинен вказувати фільтр, а не сервлет, як показано в Лістингу 3.1↓.
LiftFilter Setup in web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml version="1.0" encoding="ISO-8859-1"?>
  PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
<web-app>
  <filter>
    <filter-name>LiftFilterfilter-name>
    <display-name>Lift Filterdisplay-name>
    <description>The Filter that intercepts lift callsdescription>
    <filter-class>net.liftweb.http.LiftFilterfilter-class>
  filter>     
  <filter-mapping>
    <filter-name>LiftFilterfilter-name>
    <url-pattern>/*url-pattern>
  filter-mapping>
web-app>
Повний приклад web.xml показаний в Розділі G.1.5↓. Зокрема, відображення фільтрів (рядки 13-16) вказують, що Filter відповідальний за все. Коли фільтр отримує запит, він перевіряє набір правил, щоб побачити, чи він може їх обробити. Якщо це запит, щообробляє Lift, він передає його на внутрішній примірник LiftServlet для обробки; інакше, він сціплює запит та дозволяє контейнеру обробити його.

3.2  Розкрутка

Коли Lift стартує застосування, відбувається низка речей, що ви бажаєте встановити перед обробкою любих запитів. Ці речі включають налаштування маню сайта (називається SiteMap, Глава 7↓), перезаписування URL (Розділ 3.7↓), власний диспечер (Розділ 3.8↓), та пошук classpath (Розділ 3.2.1↓). Сервлет Lift шукає клас bootstrap.liftweb.Boot, та виконує метод boot цього класу. Ви можете вказати ваш власний примірник Boot, використовуючи параметр ініціалізації bootloader для LiftFilter, як показано В Лістингу 3.2↓
Перекриття класу завантаження
1
2
3
4
5
6
7
<filter>
  ... filter setup here ...
  <init-param>    
    <param-name>bootloaderparam-name>
    <param-value>foo.bar.baz.MyBootparam-value>
  init-param>
filter>
Ваш власний клас завантаження мусить бути суб-класом Bootable  [O]   [O] net.liftweb.http.Bootable та реалізувати метод boot. Метод boot буде виконуватись тілько один раз, так що ви можете покласти сюди також любі виклики ініціалізації для інших бібліотек.

3.2.1  Розрішення класів

Як частина нашої дискусії по класу Boot, також важливо пояснити, як Lift визначає, де знайти класи для відображення Views та Snippet, коли використовується неявна диспечеризація (ми охопимо це в Розділі 5.2.1↓). Метод LiftRules.addToPackages каже lift, які пакунки  Scala передивлятись для наданих класів. Lift має неявні розширення до шляхів, що ви введете: зокрема, якщо ви скажете Lift використовувати пакуок com.pocketchangeapp, Lift буде дивитись класи View в com.pocketchangeapp.view, Comet класи (Розділ 11.5↓) в com.pocketchangeapp.comet, та класи Snippet (Глава 5↓) в com.pocketchangeapp.snippet. Метод addToPackages повинен майже завжди виконуватись в вашому класі Boot. Мінімальний клас Boot може виглядати так:
Мінімальний клас Boot
1
2
3
4
5
class Boot {
  def boot = {
    LiftRules.addToPackages("com.pocketchangeapp")
  }
}

3.3 Зауваження щодо стандартних імпортів

В цілях збереження місця наступні твердження імпорту вважаються присутніми для всього коду прикладів на протязі залишку книги:
Стандартні твердження імпорту
1
2
3
4
5
6
7
8
9
import net.liftweb.common._
import net.liftweb.http._
import S._
import net.liftweb.util._
import Helpers._
import scala.xml._

3.4  Головні об'єкти Lift

Перед тим, як ми зануримось в основи Lift, ми бажаємо коротко обсудити три об'єкти, що ви будете щільно використовувати в вашому коді Lift. Ми охопимо їх більш детально пізніше в цій главі, та в подальших главах, так що можете вільно перейти далі, якщо бажаєте отримати більше деталей.

3.4.1  S object

Об'єкт net.liftweb.http.S представляє стан поточного запиту (згідно з David Pollak, “S” означає Stateful). Як такий, він використовується для отримання інформації щодо запиту, та модифікації інформації, що відсилається у відповідь. Окрім іншого, він може використовуватись для зауважень (Розділ B↓) , управління cookie (Розділ 3.10↓), локалізації/інтернаціолізації (Глава D↓) та перенаправлення (Розділ 3.9↓).

3.4.2  SHtml

Головне призначення об'єкта net.liftweb.http.SHtml є визначення функцій генерації HTML, зокрема тих, що мають справу з полями форми. Ми охопимо форми в деталях в Главі 6↓). На додаток до звичайних елементів форм, SHtml визначає функції для елементів форм AJAX та JSON (Главаs 11↓ та 10↓, відповідно).

3.4.3  LiftRules

Об'єкт net.liftweb.http.LiftRules є місцем, що обробляється значна більшість глобальної конфігурації Lift. Майже все, що конфігурується щодо Lift, встановлюється на основі змінних в LiftRules. Оскільки LiftRules перекриває такий різноманітний диапазон функціональності, wми не будемо розглядати  LiftRules напряму, але коли ми будемо розглядати кожний з механізмів Lift, ми будемо торкатись змінних та методів LiftRules, що мають відношення до конфігурації цього механізму.

3.5 Процес відтворення

Залишок цієї глави, так само, як декілька наступних глав, будуть присвячені стадіям відтворення Lift. Ми починаємо, надаючи короткий огляд процесу, як Lift трансформує запит в відповідь. Ми збираємось тільки доторкнутись до найважливіших точок, хоча кроки, що ми обсудимо, будуть впорядковані, як Lift виконує їх. Значно більш деталізований тур щодо конвейера наданий в Розділі 9.2↓. Починаючи з початкового запита Lift буде робити наступне:
  1. Викоанає любе сконфігуроване переписування URL. Це охоплено в Розділі 3.7↓.
  2. Виконає любі співпадаючі власні функції диспечеризації. Це поділяється на обоє, диспечеризацію з та без стану, та більш детально розкривається в Розділі 3.8↓.
  3. Виконує автоматичну обробку запитів Comet та AJAX (Глава 11↓).
  4. Виконує налаштування та співпадіння SiteMap. SiteMap, описаний в Главі 7↓, не тільки провадить гарну систему меню на рівні сайту, але може також виконувати контроль безпеки, перезапис URL, та іншу власну функціональність.
  5. Шукає шаблон XHTML, що має використовуватись для запиту. Це обробляється через три механізми:
    1. Перевірки LiftRules.viewDispatch RulesSeq, щоб почабачити, чи визначені влаані правила  диспечера. Ми розкажемо про власну диспечерізацію переглядів в Розділі 9.2↓.
    2. Якщо немає співпадінь viewDispatch, шукається файл шаблону , що співпадає, та використовується. Ми поговоримо про шаблони, та як вони шукаються, в Розділі 4.1↓.
    3. Якщо файл шаблону не був знайдений, намагаємось знайти перегляд, базуючись на неявній маршрутизації. Ми розглянемо це в  Розділі 4.4↓.
  6. Обробляємо шаблон, включаючи вбудування інших шаблонів (Розділ 4.5.7↓), злиння елементів зі скомпонованих шаблонів (Розділ ), та виконуємо функції уривка (Глава 5↓).
Залишок цієї глави буде присвячено, частково, ранішнім стадіям конвеєра відтворення, так само, як деяким зауваженням щодо головної функціональності Lift, як перенаправлення.

3.6  Повідомлення про зауваження, попередження та помилки

Зворотній зв'язок з користувачем є корисним. Застосування мають бути в змозі повідомляти користувачів про помилки, попереджати про потенційні проблеми, та повідомлять користувача, коли стан системи змінюється. Lift провадить уніфіковану модель для таких повідомлень, що може використовуватись для статичних сторінок, так само, як для викликів AJAX та Comet. Ми розкриємо підтримку повідомлень в Додатку B↓.

3.7 Переписування URL

Тепер, коли ми пройшлись по шаблонах, переглядах, уривках, та тому, як запити диспечеризуються до Class.method, ми можемо обсудити, як перехопити запити та обробити їх бажаним нам шляхом. Перезаписування URL є механізмом, що дозволяє вам модифікувати входящі запити, так що вони перенаправляються на інший URL. Це може бути застосоване, окрім іншого, щоб дозволити вам:
Механізм досить простий для налаштування. Нам треба написати часткову функію від RewriteRequest до RewriteResponse, щоб визначить, чи, та як, ми бажаємо переписати окремий запит. Коли ми маємо часткову функцію, ми модифікуємо конфігурацію  LiftRules.rewrite, щоб перехопити ланцюжок обробки Lift. Простіший шлях написати яасткову функцію є твердження порівняння Scala, що дозволить нам вибірково порівнювати деяку або всю інформацію запита. (Зауважте, що для часткової функції порівняння не мають бути вичерпні. В випадку, коли немає порівнянь RewriteRequest, RewriteResponse не буде згенеровано). Також важливо розуміти, що коли виконується функція перезаписування , сесія  Lift ще не створена. Це означає, що ви, загалом, не можете встановити або отримати доступ до властивостей в об'єкті S. RewriteRequest є case об'єктом, що містить три елементи: розібраний шлях, запитаний тип, та оригінальний HttpServletRequest object. (Якщо ви не знайомі з кейс класами, ви можете переглянути документацію Scala щодо них. Додавання case модифікатора до класа призводить до деяких милих синтаксичних домовленостей).
Розібраний шлях запиту опиняється в примірнику кейс класа ParsePath. Клас ParsePath містить
  1. Розібраний шлях, як List[String]
  2. Суфікс запита (тобто, “html”, “xml”, тощо)
  3. Чи цей шлях відносний до кореня. Якщо так, тоді він буде починатись з /, за яким слідує залишок шляху. Наприклад, якщо ваше застосування розгорнуте на шляху контексту app (“/app”), та ми бажаємо посилатись на файл /pages/index.html, тоді відносний до кореня шлях буде /app/pages/index.html.
  4. Чи він завершується на косу (“/”)
Три останні властивості корисні тільки в окремих випадках, але розібраний шлях є тим, що дозволяє нам робити магію. Шлях запиту визначений як частини URI між шляхом контексту та рядком запита. Наступна таблиця показує приклади розібраних шляхів для застосування Lift, під шляхом контекста “myapp”:
Запитаннй URL Розібраний шлях
http://foo.com/myapp/home?test_this=true List[String](“home”)
http://foo.com/myapp/user/derek List[String](“user”, “derek”)
http://foo.com/myapp/view/item/14592 List[String](“view”,”item”,”14592”)
RequestType відображує один з п'яти методів HTTP: GET, POST, HEAD, PUT та DELETE. Вони представлені відповідними GetRequest, PostRequest, тощо. кейс класами, та кейс клас UnknownRequest охоплює все незрозумуле.
Гнучкість системи співпадінь Scala - це те, що робить це потужним. Зокрема, коли порівнюються Lists, ми можемо порівняти частини шляху, та захопити інші. Наприклад, уявімо, що ми бажаємо переписати шлях /account/, так, щоб він був оброблений шаблоном /viewAcct, як показано в Лістингу 3.7↓. В цьому випадку ми провадимо два перезаписи. Перший співпадає з /account/, та перенаправляє до шаблону /viewAcct, передаючи acctName як параметр “name”. Другий співпадає з /account//, пересилаючи його на /viewAcct, як раніше, але передаючи обоє параметри,  “name” та “tag”, з acctName та tag співпадіннями з ParsePath, відповідно. Пам'ятайте, що підкреслення (_) в ціх твердженнях співпадіння означає, що ми не опікуємось, що це за параметр, тобто, що тут співпало.
Простий приклад перезапису
1
2
3
4
5
6
7
8
9
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)))
}
RewriteResponse просто містить новий шлях, яким треба слідувати. Він також приймає Map, що містить параметри, що будуть доступні через S.param в уривку або перегляді. Як ми вже встановити, LiftSession (і, таким чином, більшість S) не доступна на цей час, так що Map є єдиним шляхом передати інформацію до перезаписаного місця.
Ми можемо комбінувати співпадіння ParsePath з  RequestType та HttpServletRequest, щоб бути дуже специфічним щодо співпадінь. Наприклад, якщо ми бажаємо підтримати дієслово DELETE HTTP для RESTful [P]   [P]  інтерфейса через існуючий шаблон, ми можемо переписати, як показано в Лістингу 3.7↓.
Складний приклад переписування
1
2
3
4
5
6
7
LiftRules.rewrite.append {
  case RewriteRequest(ParsePath("username" :: Nil, _, _, _),
                      DeleteRequest,
                      httpreq)
                      if isMgmtSubnet(httpreq.getRemoteHost()) =>
       RewriteResponse("deleteUser" :: Nil, Map("username" -> username))
}
Ми дійдемо до деталей щодо того, як ви можете використовувати це в наступних розділах. Зокрема, SiteMap (Глава 7↓) провадить механізм для виконання переписування, в комбінації з елементами меню.

3.8 Власні функції диспечеризації

Коли фаза переписування завершена (чи ми пройшли скрізь, чи перенаправлені), наступна фаза є визначення, чи ми потребуємо власну диспечеризацію для запиту. Власна диспечеризація дозволяє вам обробляти співпадаючі запити напряму в методі, замість проходження через систему пошуку шаблонів. Оскільки це обходить шаблони, ви відповідальні за весь вміст відповіді. Типовим прикладом використання може бути веб сервіс, що повертає XML, або сервіс, що повертає, скажімо, згенероване зображення або PDF. В цьому сенсі механізм власного диспечера дозволяє вам писати ваші власні “суб-сервлети”, без всього безладу реалізації інтерфейса, та конфігурації їх в web.xml.
Як і з переписуванням, власна диспечеризація реалізована через часткову функцію. В цьому випадку, це функція типу PartialFunction[Req,()  ⇒ Box[LiftResponse]], що виконує всю роботу. Req подібне до кейс класу RewriteRequest: він провадить шлях як List[String], суфікс запиту, та RequestType. Є три шляхи, що ви можете встановити функцію власної диспечеризації:
  1. Глобально, через LiftRules.dispatch
  2. Глобально, через LiftRules.statelessDispatchTable
  3. По-сессійно, через S.addHighLevelSessionDispatcher
Якщо ви прикріплюєте функцію диспечеризації через  LiftRules.dispatch або S.addHighLevelSessionDispatcher, тоді ви маєте повний доступ до об'єкта S, SessionVar та LiftSession; якщо ви замість цього використовуєте LiftRules.statelessDispatchTable, тоді вони не доступні. Результат диспечеризації повинен бути функцією, що повертає Box[LiftResponse]. Якщо функція повертає Empty, тоді Lift повертає відповідь “404 Not Found”.
Як конкретний приклад, давайте подивимось на повернення згенерованого зображення графіка з нашого застосування. Є декілька бібліотек для малювання діаграм, але ми, зокрема, поглянемо на  JFreeChart. Перше, давайте напишемо метод, що буде відображати наш баланс рахунку по місяціх минулого року:
Метод малювання діаграми
1
2
3
4
5
6
7
8
9
10
11
12
13
def chart (endDate : String) : Box[LiftResponse] = {
  // запит, встановлення, тощо...
  val buffered = balanceChart.createBufferedImage(width,height)
  val chartImage = ChartUtilities.encodeAsPNG(buffered)
  // InMemoryResponse є субкласом LiftResponse
  // він приймає Array з Bytes, List[(String,String)] з
  // заголовків, List[Cookie] з кукіз, та ціле як
  // код повернення (тут 200 для HTTP 200: OK)
  Full(InMemoryResponse(chartImage,
                        ("Content-Type" -> "image/png") :: Nil,
                        Nil,
                        200))
}
Як тільки ми встановили діаграму, ми використовуємо  клас-допоміжник ChartUtilities  з JFreeChart щоб закодувати діаграму в байтовий масив PNG. Ми можемо потім використати Lift InMemoryResponse для передачі закодованих даних назад з відповідним заголовком Content-Type. Тепер нам тільки треба перехопити запит в таблиці диспечеризації від Boot класа, як показано в Лістингу  3.8↓. В цьому випадку ми бажаємо наголосити, що ми можемо отримати діаграму поточного користувача. З цієї причини ми використовуємо LiftRules.dispatch на відміну від LiftRules.statelessDispatch. Оскільки ми використовуємо часткову функцію для виконання операції співпадіння Scala, випадок, який ми визначаємо тут, використовує метод unapply  Req об'єкта, ось чому ми повинні провадити тільки аргумент List[String].
Перехоплення диспечеризації в Boot
1
2
3
4
LiftRules.dispatch.append {
  case Req("chart" :: "balances" :: endDate :: Nil, _, _) =>
    Charting.chart(endDate) _
}
Як ви можете бачити, ми захопили параметр endDate зі шляха, та передали його в метод діаграми. Це означає, що ми можемо використовувати URL як http://foo.com/chart/balances/20080401 для отримання зображення. Оскільки функція була асоційована з сесією Lift, ми можемо також використовувати метод S.param для отримання параметрів рядка запиту, якщо, наприклад, ми бажаємо дозволити декому надсилати опціональні ширину та висоту:
val width = S.param(“width”).map(_.toInt) openOr 400
val height = S.param(“height”).map(_.toInt) openOr 300
Або ви можете використати трохи інший підхід, використовуючи метод Box.dmap:
val width = S.param(“width”).dmap(400)(_.toInt)
val height = S.param(“height”).dmap(300)(_.toInt)
Тут dmap ідентичний з функцією map, за тією різницею, що перший аргумент є занченням по замовчанню, якщо Box є Empty. Є декілька інших субкласів ListResponse , що треба охопити для наших потреб, включаючи відповіді для XHTML, XML, Atom, Javascript, CSS, та JSON. Ми розглянемо їх більш докладно в Розділі 9.4↓.

3.9 Перенаправлення HTTP

Перенаправлення HTTP є важливою частиною багатьох веб застосувань. В Lift є два головні шляхи надсилання перенаправлення до клієнта:
  1. Виклик S.redirectTo. Коли ви робите це, Lift підіймає виключення, та потім його перехоплює. Це означає, що любий код після перенаправлення пропускається. Якщо ви використовуєте StatefulSnippet (Розділ 5.3.3↓), використовуйте this.redirectTo, так що ваш примірник уривку використовується при обробці перенаправлення.
    Важливо: якщо ви використовуєте S.redirectTo в блоці try/catch, вам треба переконатись, що ви не відловлюєте виключення перенаправлення (Scala використовує неперевірені виключення), або перевірте виключення перенаправлення, та знову підійміть його. Якщо ви помилково спіймаєте виключення перенаправлення, тоді саме перенаправленя не відбудеться.
  2. Коли вам треба повернути LiftResponse, ви можете просто повернути відповідь RedirectResponse або RedirectWithState.
Відповідь RedirectWithState дозволяє вам вказати функцію, що треба виконати, коли обробляється перенаправлений запит. Ви можете також надіслати повідомлення Lift (повідомлення. попередження або помилки), що будуть відображені на перенаправленій сторінці, так само, як кукіз, що будуть відіслані в перенаправлення. Подбіним чином є перевантажена версія S.redirectTo, що дозволяє вам вказати функцію, що має бути виконана, коли буде оброблятись переназначення.

3.10 Кукіз

Кукіз  [Q]   [Q] http://java.sun.com/products/servlet/2.2/javadoc/javax/servlet/http/Cookie.html є корисним інструментом, коли ви бажаєте сталих даних між сесіями користувача. Кукіз в основі є токеном з рядка даних, що зберігається на машині користувача. Хоча вони досить корисні, є декілька речей, що ви повинні мати на увазі:
  1. Переглядач користувача може відключити кукіз, і в такому випадку вам треба приготуватись для роботи без кукіз, або сповістити користувача, що йому потрібно ввімкнути кукіз для вашого сайта
  2. Кукіз відносно небезпечні  [R]   [R]  Дивітьсяhttp://www.w3.org/Security/Faq/wwwsf2.html (Q10) та http://www.cookiecentral.com/faq/ щодо деталей про кукіз, та їх проблеми з безпекою.. Є декіка багів переглядачів, пов'язаних з даними в кукіз, які можуть читати віруси або інші сайти
  3. Кукіз просто підробити, так що вам треба переконатись, що ви перевірили любі чултиві дані з кукіз
Використання Cookies в Lift є дуже простим. В контексті зі станом, все, що вам треба, провадиться декількома методами на об'єкті S:
addCookie  Додає кукі, що буде відіслана у відповіді
deleteCookie  Видаляє кукі (технічно, це додає кукі з максимальним часом життя ноль, так що переглядач видаляє її). Ви можете або видалити кукі по імені, або через об'єкт Cookie
findCookie  Шукає кукі за наданим ім'ям, та повертає Box[Cookie]. Empty означає, що кукі не існує
receivedCookies  Повертає List[Cookie] з усіх кукі, надісланих у запиті
responseCookies Повертає List[Cookie] з кукіз, що будуть надіслані у відповіді
Якщо вам треба робити з кукіз в контексті без стану, багато з класів ListResponse (Розділ 9.4↓) включають List[Cookie] в свої конструктори або аргументи apply. Просто надайте список кукіз, що ви бажаєте встановити, та вони будуть надіслані у відповідь. Якщо ви бажаєте видалити кукі з LiftResponse, ви маєте зробити це вручну, додавши кукі з тим же ім'ям, та maxage рівним нулю.

3.11 Стан сесії та запиту 

Lift провадить дуже простий шлях для зберігання даних рівня сесії та запиту, через класи SessionVar та RequestVar. В стилі Lift, ці класи провадять:
Додатково, Lift провадить простий доступ до параметрів запиту HTTP через метод S.param, що повертає Box[String]. Зауважте, що параметри запиту  HTTP (надіслані через GET або POST) відрізняються від RequestVars в тому, що параметри запиту є значеннями рядків, надісланих як частина запиту; RequestVars, на відміну, використовують внутрішній Map для кожного запиту, так що вони можуть містити любий тип, та ініціалізуються повністю в коді. В цій точці ви можете запитати, для чого можна застосувати RequestVars. Типовий приклад є розділення стану між різними уривками, оскільки немає іншого зв'язку між ними, ніж на рівні шаблону.
SessionVars та RequestVars призначені бути реалізованими як об'єкти-синглтони, так що вони доступні будь-де у вашому коді. Лістинг 3.11↓ показує приклад визначення RequestVar, що використовується для зберігання числа елементів, що треба відображувати на сторінці. Ми починаємо, визначаючи об'єкт, як такий, що розширює RequestVar. Ви повинні провадити тип RequestVar, так що Lift знає, що сприймати та повертати. В цьому разі тип є Int. Пргумент конструктора є параметром за-ім'ям, що має обраховуватись до типу var. В нашому випадку ми намагаємось використати змінну запиту HTTP “pageSize,” та якщо вона не присутня або це не ціле, тоді ми беремо замовчання 25.
Визначення RequestVar
1
2
3
4
class AccountOps {
  object pageSize extends RequestVar[Int](S.param("pageSize").map(_.toInt) openOr 25)
  ...
}
Доступ до значення RequestVar робиться через метод is. Ви можете також встановити значення, використовуючи метод apply, що в Scala синтаксично подібне до використання RequestVar як функції. Загальні використання apply в Scala аключають доступ до елементів масиву по індексу, та методу об'єкта-компанйона, що можуть апроксимувати власні конструктори. Наприклад, об'єкт  Loc (що ми розглянемо в Главі 7↓), має перевантажений метод apply, що створює новий примірник класу Loc, базуючись на вхідних параметрах.
Доступ до RequestVar
1
2
3
4
5
6
7
// отримуємо значення з AccountOps.pageSize RequestVar
query.setMaxResults(AccountOps.pageSize.is)
// Змінюємо значення RequestVar. Два наступні рядка
// кода є еквівалентними:
AccountOps.pageSize(50)
AccountOps.pageSize.apply(50)
На додаток до прийняття параметрів, що визначають значення по замовчанню, ви також можете очистит значення, коли змінна закінчує життєвий цикл. Лістинг 3.11↓ показує приклад відкриття сокета, та його зачинення в кінці запиту. Все це обробляється передачею функції до метода registerCleanupFunc. тип функції, що вам треба передати, є CleanUpParam  ⇒ Unit, де CleanUpParam визначений базуючись на тому, чи ви використовуєте RequestVar або  SessionVar. З RequestVar, CleanUpParam є типу Box[LiftSession], відображуючи те, що сесія може не бути в полі зору, коли виконується функція очищення. ДЛя SessionVar CleanUpParam є типу LiftSession, оскільки сесія завжди в полі зору для SessionVar (він містить посилання на сесію). В нашому прикладі в Лістингу3.11↓ ми просто ігноруємо вхідні параметри до функції очищення, оскільки закриття сокету є незалежним від жодного стану сесії. Інша важлива річ, що треба запам'ятати, те що ви відповідальні за обробку любих виключень, що можуть піднятись на протязі або ініціалізації, або очистки по замовчанню.
Визначення функції очищення
1
2
3
object mySocket extends RequestVar[Socket](new Socket("localhost:23")) {
  registerCleanupFunc(ignore => this.is.close)
}
Інформація, що ми тут розкрили, також стосуєтся і до SessionVars; єдина різниця між ними є поле зору іх відповідних життєвих циклів.
Інше загальне застосування RequestVar є передача стану між різними переглядами сторінки (запитами). Ми починаємо, визначивши RequestVar на об'єкті, так що воно доступне з усіх методів уривка, що будуть читати або записувати його. Також можливо визначити його на класі, якщо всі уривки, що будуть отримувати доступ до нього, є в цьому класі . Тоді, в частинах вашого кода, що буде переходити на нову сторінку, ви використовуєте перевантажені версії SHtml.link або S.redirectTo, що приймає функцію як другий аргумент щоб “інжектувати” значення, яке ви бажаєте передати через RequestVar. Це подібне до використання параметрів запиту на URL для передачі даних, але є дві важливі переваги:
  1. Ви можете передати любий тип даних через  RequestVar, на відміну від тільки рядків даних в параметрі запиту.
  2. Ви насправді передаєте посилання на функцію інжектора, на відміну від самих даних. Це може біти важливо, якщо ви не бажаєте, щоб користувач бавився з переданими даними. Одним прикладом може бути передача вартості товару зі сторінки “перегляд товару” до сторінки “додати до корзини”.
Лістинг 3.11↓ показує, як передавати Account з таблиці переліку до окремої сторінки редагування Account, з використанням SHtml.link, так само, та як ми можемо перейти від сторінки редагування до сторінки перегляду з використанням S.redirectTo. Інший приклад передачі показаний на Лістингу 12.1.3↓.
Передача Account до View
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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),
          ...
          // Другий аргумент робить інжекцію "acct" val
          // назад до RequestVar
          link("/editAcct", () => currentAccountVar(acct), Text("Edit"))
      })
    })
    ...
  }
  def edit (xhtml : NodeSeq) : NodeSeq = {
    def doSave () {
      ...
      val acct = currentAccountVar.is
      S.redirectTo("/view", () => currentAccountVar(acct))
    }
    ...
  }
}
Одна важлива річ, що треба занотувати, полягає в тому, що змінна інжетора викликається в полі зору наступного запиту. Це означає, що якщо ви бажаєте значення, повернуте функцією, в точці, де ви викликаєте link або redirectTo, вам треба захопити її в val. Інакше функція буде визиватись після перенаправлення або пов'язання, що може призвести до іншого значення, ніж ви очікували. Як ви можете бачити з Лістинга 3.11↑, ми встановили val в вашому методі doSave перед перенаправленням. Якщо ви спробуєте щось подібне до 
S.redirectTo("/view", () => currentAccountVar(currentAccountVar.is))
замість цього, ми можемо отримати значення по замовчанню для нашої RequestVar (в цьому випадку null).

3.12 Висновок  

Ми розглянули багато матеріалу, та все ще багато залишилось. Надіємось, ця глава провадить надійний базис для початку, перед дослідженням залишку книги.

4 Шаблони в Lift

An XHTML page, being the central component of a web application, is likewise the central component of Lift’s request processing. In Lift, we go a step further and utilize a flexible yet powerful templating engine that allows us to compose an XHTML page not only out of one or more XML files, but also from methods that can programmaticaly generate template XML. Additionally, Lift 2.2 brings designer-friendly templates (Розділ 4.2↓) and HTML5 support (Розділ 4.3↓). Designer-friendly templates, in particular, can simplify working with a designer because they allow templates to be fully valid XHTML or HTML5.
In this chapter we’ll discuss template capabilities and syntax, including built-in tags provided by Lift that perform special template processing (Розділ 4.5↓). We will also cover how you can write your own View classes, Scala code that can programmatically generate template XML (Розділ 4.4↓). We’ll finish up the chapter with some discussion on miscellaneous templating functionality.

4.1  Template XML

Templates form the backbone of Lift’s flexibility and power. A template is an XML document that contains Lift-specific tags, see 4.5↓, as well as whatever content you want returned to the user.
A note on nomenclature: typically when people discuss “templates” in books or on the mailing list they’re talking about XML files. We’ll cover programmatic generation of template XML in Розділ 4.4↓.
Lift includes several built-in XML tags for specific actions. These utilize prefixed XML elements and are of the form . Lift also allows you to define your own tags, which are called snippets (Глава 5↓). These user-defined tags are linked to Scala methods and these methods can process the XML contents of the snippet tag, or can generate their own content from scratch. A simple template is shown in Listing 4.1↓.
A Sample Template
1
2
3
4
<lift:surround with="default" at="content">
  <head><title>Hello!title>head>
  <lift:Hello.world />
lift:surround>
Notice the tags that are of the form which in this case are and . These are two examples of Lift-specific tags. We’ll discuss all of the tags that users will use in Розділ 4.5↓, but let’s briefly discuss the two shown here. We use the built-in tag (Розділ 4.5.17↓) to make Lift embed our current template inside the “default” template. We also use tag (aliased to Hello.world) to execute a snippet that we defined. In this instance, we execute the method world in the class Hello to generate some content.

4.1.1  Locating Template XML

During request processing, Lift first tries to match against the LiftRules.viewDispatch function to see if an explicit View method is defined for the request. If there isn’t a viewDispatch match, then Lift next tries to locate a file in the template directory tree (typically in a WAR archive) that matches the request. Lift tries several suffixes (html, xhtml, htm, and no suffix) and also tries to match based on the client’s Accept-Language header. The pattern Lift uses is:
[_][.]
Because Lift will implicitly search for suffixes, it’s best to leave the suffix off of your links within the web app. If you have a link with an href of /test/template.xhtml, it will only match that file, but if you use /test/template for the href and you have the following templates in your web app:
then Lift will use the appropriate template based on the user’s requested language if a corresponding template is available. For more information regarding internationalization please see Appendix D↓. In addition to normal templates, your application can make use of hidden templates. These are templates that are located under the /templates-hidden directory of your web app. Technically, Lift hides files in any directory ending in “-hidden”, but templates-hidden is somewhat of a de facto standard. Like the WEB-INF directory, the contents cannot be directly requested by clients. They can, however, be used by other templates through mechanisms such as the and tags (Розділ 4.5.7↓). If a static file can’t be located then Lift will attempt to locate a View class (Розділ 4.4↓) that will process the request. If Lift cannot locate an appropriate template based on the request path then it will return a 404 to the user.

4.1.2  Processing Template XML

Once Lift has located the correct template, the next step is to process the contents. It is important to understand that Lift processes XML tags recursively, from the outermost tag to the innermost tag. That means that in our example Listing 4.1↑, the surround tag gets processed first. In this case the surround loads the default template and embeds our content at the appropriate location. The next tag to be processed is the snippet. This tag is essentially an alias for the lift:snippet tag (specifically, ) , and will locate the Hello class and execute the world method on it. If you omit the “method” part of the type and only specify the class ( or ), then Lift will attempt to call the render method of the class.
To give a more complex example that illustrates the order of tag processing, consider Listing 4.1.2↓. In this example we have several nested snippet tags, starting with . Listing 4.1.2↓ shows the backing code for this example. Snippets are covered in more detail in Глава 5↓.
A Recursive Tag Processing Example
1
2
3
4
5
6
7
8
9
<lift:A.snippet>
  <p>Hello, <A:name />!p>
  <p>
    <lift:B.snippet>
      <B:title />
      <lift:C.snippet />
    lift:B.snippet>
  p>
lift:A.snippet>
The first thing that happens is that the contents of the tag are passed as a NodeSeq argument to the A.snippet method. In the A.snippet method we bind, or replace, the tag with an XML Text node of “The A snippet”. The rest of the input is left as-is and is returned to Lift for more processing. Lift examines the returned NodeSeq for more lift tags and finds the tag. The contents of the tag are passed as a NodeSeq argument to the B.snippet method, where the tag is bound with the XML Text node “The B snippet”. The rest of the contents are left unchanged and the transformed NodeSeq is returned to Lift, which scans for and finds the tag. Since there are no child elements for the tag, the C.snippet method is invoked with an empty NodeSeq and the C.snippet returns the Text node “The C snippet”.
The Recursive Tag Snippets Code
1
2
3
4
5
6
7
8
9
10
11
12
... standard Lift imports ...
class A {
  def snippet (xhtml : NodeSeq) : NodeSeq =
    bind("A", xhtml, "name" -> Text("The A snippet"))
}
class B {
  def snippet (xhtml : NodeSeq) : NodeSeq =
    bind("B", xhtml, "title" -> Text("The B snippet"))
}
class C {
  def snippet (xhtml : NodeSeq) : NodeSeq = Text("The C snippet")
}
While the contents of the A.snippet tag are passed to the A.snippet method, there’s no requirement that the contents are actually used. For example, consider what would happen if we swapped the B and C snippet tags in our template, as shown in Listing 4.1.2↓. In this example, the C.snippet method is called before the B.snippet method. Since our C.snippet method returns straight XML that doesn’t contain the B snippet tag, the B snippet will never be executed! We’ll cover how the eager_eval tag attribute can be used to reverse this behavior in Розділ 5.3.4↓.
The Swapped Recursive Snippet Template
1
2
3
4
5
6
7
8
9
10
11
12
13
<lift:A.snippet>
  <p>Hello, <A:name />!p>
  <p>
    <lift:C.snippet>
      <lift:B.snippet>
        <B:title />
      lift:B.snippet>
    lift:C.snippet>
  p>
lift:A.snippet>
<p>Hello, The A snippetp>
<p>The C snippetp>
As you can see, templates are a nice way of setting up your layout and then writing a few methods to fill in the XML fragments that make up your web applications. They provide a simple way to generate a uniform look for your site, particularly if you assemble your templates using the surround and embed tags. If you’d like programmatic control over the template XML used for a particular request, you’ll want to use a View, which is discussed in the next section.

4.2  Designer-Friendly Templates

New in Lift 2.2 is the ability to use fully valid XHTML (or HTML5, which we’ll discuss in Розділ 4.3↓) for your templates. There are a number of features involved in designer-friendly templates (or DFTs for short), so let’s go through each one.

4.2.1  Determining the Content Element

In XML-based templates, the entire XML file is considered to hold the contents of the template. In DFTs, we want to be able to include the full XHTML or HTML5 markup, including tags like , , etc. without necessarily including all of that in the output of the template (for example, in an embedded template). Lift supports choosing a child element of the template to represent the actual contents via the use of one of two related mechanisms.
The first mechanism is to put a lift:content_id attribute on the HTML element, as shown in Listing 4.2.1↓. The drawback to this approach is that you have to specify the “lift” namespace in the html tag or you might get validation errors.
Assigning a Content ID on the HTML element
1
2
3
4
5
6
7
8
9
10
11
12
13
14
       xmlns:lift="http://liftweb.net"
       lift:content_id="real_content">
   <head>
    <title>Not reallytitle>
   head>
   <body>
     <div id="real_content">
       <h1>Welcome to your project!h1>
     div>
   body>
 html>
The second, safer approach, is to specific the lift:content_id marker as part of the body element’s class attribute, as shown in Listing 4.2.1↓.
Assigning a Content ID in the Body class
1
2
3
4
5
6
7
8
9
10
11
12
   <head>
    <title>Not reallytitle>
   head>
   <body class="lift:content_id=real_content">
     <div id="real_content">
       <h1>Welcome to your project!h1>
     div>
   body>
 html>

4.2.2  Invoking Snippets Via the Class Attribute

In XML-based templates, Lift looks for tags with the “lift” prefix to process. In DFTs, the class attribute is used instead for invocation. This form of invocation is discussed in more detail in Розділ 5.1 on page 1↓.

4.2.3  Binding via CSS transforms

Lift 2.2 introduces a new feature for binding values into snippet markup by using CSS id and class attributes instead of prefixed XML elements. This support is detailed in Розділ 5.3.2 on page 1↓.

4.3  HTML5 Support

4.4  Views

We just discussed Templates and saw that through a combination of an XML file, Lift tags, and Scala code we can respond to requests made by a user. You can also generate an XHTML response entirely in code using a View. Custom dispatch is a similar method which can be used to programmatically return any kind of response (not just XHTML), and is covered in more depth in Розділ 3.8↑.
A view function is a normal Scala method of type ()  ⇒ scala.xml.NodeSeq. The NodeSeq that’s returned from a view is processed for template tags in the same way that XML loaded from a static file would be. As we showed in Розділ 3.5↑, there are two ways that a View can be invoked. The first is by defining a partial function for LiftRules.viewDispatch, which allows you to dispatch to any statically-available method (i.e. on an object, not a class), or to a LiftView (explained in a moment) object for any arbitrary request path. The second way that a View can be invoked is by reflection: if the first element of the request path matches the class name of the View (as defined in Розділ 3.2.1↑), then the second element is used to look up the View function depending on which trait the View class implements. For performance reasons, explicit dispatch via LiftRules.viewDispatch is recommended because reflection incurs a significant cost for each request. When you use LiftRules.viewDispatch, you need to provide an instance of scala.lang.Either to differentiate the dispatch type: a scala.lang.Left indicates a method returning a Box[NodeSeq], while a scala.lang.Right indicates a LiftView object. If you want to dispatch a request to a LiftView object, the match in the LiftRules.viewDispatch is made on all path components except the last one (e.g. List.init), and that object’s dispatch method is checked against the last component of the path to further determine which method on the object will handle the request. Listing 4.4↓ shows how we can define an RSS view as a feed using explicit dispatch. Note that we use extraction on the requested path in this case to provide account-specific feeds and a security token to prevent feed browsing.
Explicit View Dispatch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// In Boot.boot:
LiftRules.viewDispatch.append {
  // This is an explicit dispatch to a particular method based on the path
  case List("Expenses", "recent", acctId, authToken) =>
    Left(() => Full(RSSView.recent(acctId, authToken)))
  // This is a dispatch via the same LiftView object. The path
  // "/Site/news" will match this dispatch because of our dispatch
  // method defined in RSSView. The path "/Site/stuff/news" will not
  // match because the dispatch will be attempted on List("Site","stuff")
  case List("Site") => Right(RSSView)
}
// Define the View object:
object RSSView extends LiftView {
  def dispatch = {
    case "news" => siteNews
  }
  def recent(acctId : String, authToken : String)() : NodeSeq = {
     // User auth, account retrieval here
     ...
     :surround with="rss" at="content">
       ...
     :surround>
  }
  // Display a general RSS feed for the entire site
  def siteNews() : NodeSeq = { ... }
}
If you want to use reflection for dispatch then there are two traits that you can use when implementing a view class: one is the LiftView trait, the other is the InsecureLiftView trait, both under the net.liftweb.http package. As you may be able to tell from the names, we would prefer that you extend the LiftView trait. The InsecureLiftView determines method dispatch by turning a request path into a class and method name. For example, if we have a path /MyStuff/enumerate, then Lift will look for a class called MyStuff in the view subpackage (class resolution is covered in Розділ 3.2.1↑) and if it finds MyStuff and it has a method called enumerate, then Lift will execute the enumerate method and return its result to the user. The main concern here is that Lift uses reflection to get the method with InsecureLiftView, so it can access any method in the class, even ones that you don’t intend to make public. A better way to invoke a View is to extend the LiftView trait, which defines a dispatch partial function. This dispatch function maps a string (the “method name”) to a function that will return a NodeSeq. Listing 4.4↓ shows a custom LiftView class where the path /ExpenseView/enumerate will map to the ExpenseView.doEnumerate method. If a user attempts to go to /ExpenseView/privateMethod they’ll get a 404 because privateMethod is not defined in the dispatch method. If, however, our ExpenseView class implemented the InsecureLiftView trait and someone visited /ExpenseView/privateMethod, we would lose our hard drive (on Unix at least).
Dispatch in LiftView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ExpenseView extends LiftView {
  override def dispatch = {
    case "enumerate" => doEnumerate _
  }
  def doEnumerate () : NodeSeq =
    ...
    :surround with="default" at="content">
     { expenseItems.toTable }
    :surround>
  }
  def privateMethod () : NodeSeq = {
    Runtime.getRuntime.exec("rm -rf /")
  }
}
A major difference between Views and other programmatic rendering approaches (such as Custom Dispatch) is that the NodeSeq returned from the View method is processed for template tags including surrounds and includes, just as it would be for a snippet. That means that you can use the full power of the templating system from within your View, as shown in Listing 4.4↑’s doEnumerate method.
Since you can choose not to include any of the pre-defined template XHTML, you can easily generate any XML-based content, such as Atom or RSS feeds, using a View.

4.5  Tags

In the earlier sections on Templates and Views we briefly touched on some of Lift’s built-in tags, namely, and />. In this section we’ll go into more detail on those tags as well as cover the rest of Lift’s tags.

4.5.1  a

The tag is used internally by SHtml.a to create an anchor tag that will call an AJAX function when clicked. This tag is generally not used directly by developers. See Розділ 11.4 on page 1↓ for more details.

4.5.2  bind

Usage: 
The tag is used as a placeholder for insertion of content within included templates when using the and tags. See Розділ 4.7↓ for examples and discussion.

4.5.3  bind-at

Usage: contents
The tag is used to replace named tags within and tags. See Розділ 4.7↓ for examples and discussion.

4.5.4  children

Usage: ...multiple xml nodes here...
The purpose of the tag is to allow you to create fragment templates with more than one root element that still parse as valid XML. For example, Listing 4.5.4↓ shows a template that we might want to embed into other templates. The problem is that XML requires a single root element, and in this case we have two.
A Non-Conforming XML Fragment
1
2
<div>First Divdiv>
<div>Second Divdiv>
By using the tag, as shown in Listing 4.5.4↓, we have a valid XML file. Lift essentially replaces the tag with its contents.
A Conforming XML Fragment
1
2
3
4
<lift:children>
<div>First Divdiv>
<div>Second Divdiv>
lift:children>

4.5.5  comet

Usage: 
The tag embeds a Comet actor into your page. The class of the Comet actor is specified by the type attribute. The name attribute tells Lift to create a unique instance of the Comet actor; for example, you could have one Comet actor for site updates and another for admin messages. The contents of the tag are used by the Comet actor to bind a response. Listing 4.5.5↓ shows an example of a Comet binding that displays expense entries as they’re added. Comet is covered in more detail in Глава 11↓.
Account Entry Comet
1
2
3
4
5
6
7
<div class="accountUpdates">
  <lift:comet type="AccountMonitor">
    <ul><account:entries>
      <li><entry:time/> : <entry:user /> : <entry:amount />li>
    account:entries>ul>
  lift:comet>
div>
As we mention in the embed tag documentation, mixing Comet with AJAX responses can be a bit tricky due to the embedded JavaScript that Comet uses.

4.5.6  CSS

Usage: 
       
The tag is used to insert the blueprint  [S]   [S] http://www.blueprintcss.org/ and (optionally) fancyType   [T]   [T] http://anthonygthomas.com/2010/02/15/blueprint-optional-fancy-type-plugin/ CSS stylesheets

4.5.7  embed

Usage: 
The embed tag allows you to embed a template within another template. This can be used to assemble your pages from multiple smaller templates, and it also allows you to access templates from JavaScript commands (Глава 10↓). As with the surround tag, the template name can be either the base filename or a fully-qualified path.
Note that if you use the embed tag to access templates from within a JsCmd (typically an AJAX call), any JavaScript in the embedded template won’t be executed. This includes, but is not limited to, Comet widgets.

4.5.8  form

4.5.9  HTML5

4.5.10  ignore

4.5.11  lazy-load

4.5.12  loc

4.5.13  Menu

4.5.14  Msgs

4.5.15  SkipDocType

4.5.16  snippet

The snippet tag is covered in detail in Розділ 5.1 on page 1↓, part of the chapter on snippets.

4.5.17  surround

Usage: 
         children
       
The surround tag surrounds the child nodes with the named template. The child nodes are inserted into the named template at the binding point specified by the at parameter (we’ll cover the bind tag in Розділ 4.5.2↑). Typically, templates that will be used to surround other templates are incomplete by themselves, so we usually store them in the /templates-hidden subdirectory so that they can’t be accessed directly. Having said that, “incomplete” templates may be placed in any directory that templates would normally go in. The most common usage of surround is to permit you to use a “master” template for your site CSS, menu, etc. An example use of surround is shown in Listing 4.5.17↓. We’ll show the counterpart master template in the section on the bind tag. Note also that the surrounding template name can be either a fully-qualified path (i.e. “/templates-hidden/default”), or just the base filename (“default”). In the latter case, Lift will search all subdirectories of the app root for the template. By default, Lift will use “/templates-hidden/default” if you don’t specify a with attribute, so Listings 4.5.17↓ and 4.5.17↓ are equivalent.
Surrounding Your Page
1
2
3
<lift:surround with="default" at="content">
  <p>Welcome to PocketChange!p>
lift:surround>
Surrounding with the default template
1
2
3
<lift:surround at="content">
  <p>Welcome to PocketChange!p>
lift:surround>
Note that you can use multiple surround templates for different functionality, and surrounds can be nested. For example, you might want to have a separate template for your administrative pages that adds a menu to your default template. In that case, your admin.html could look like Listing 4.5.17↓. As you can see, we’ve named our bind point in the admin template “content” so that we keep things consistent for the rest of our templates. So if, for example, we were going to nest the template in Listing 4.5.17↑ above into the admin.html template in Listing 4.5.17↓, all we’d need to do is change it’s with attribute from “default” to “admin.”
Adding an Admin Menu
1
2
3
4
<lift:surround with="default" at="content">
  <lift:Admin.menu />
  <lift:bind name="content" />
lift:surround>
You cannot have a hidden template with the same name as a sub-directory of your webapp directory. For example, if you had an admin.html template in /templates-hidden, you could not also have an admin directory.

4.5.18  tail

4.5.19  TestCond

4.5.20  with-param

4.5.21  with-resource-id

4.5.22  VersionInfo

4.5.23  XmlGroup

4.6  Head and Tail Merge

Another feature of Lift’s template processing is the ability to merge the HTML head element in a template with the head element in the surrounding template. In our example, Listing 4.1↑, notice that we’ve specified a head tag inside the template. Without the head merge, this head tag would show up in the default template where our template gets bound. Lift is smart about this, though, and instead takes the content of the head element and merges it into the outer template’s head element. This means that you can use a surround tag to keep a uniform default template, but still do things such as changing the title of the page, adding scripts or special CSS, etc. For example, if you have a table in a page that you’d like to style with jQuery’s TableSorter, you could add a head element to insert the appropriate script:
Using Head Merge
1
2
3
4
<lift:surround with="default" at="foo">
<head><script src="/scripts/tablesorter.js" type="text/javascript" /><head>
...
lift:surround>
In this manner, you’ll import TableSorter for this template alone.

4.7  Binding

5  Snippets

Put simply, a snippet is a Scala method that transforms input XML into output XML. Snippets act as independent (or dependent, if you want) pieces of logic that you insert into your page to perform rendering. As such, snippets form the backbone of Lift’s View-First rendering architecture. Although snippets aren’t the only mechanism Lift has for rendering page views (see Views, Розділ 4.4 on page 1↑, Custom Dispatch, Розділ 3.8 on page 1↑, or even the REST API, Глава 15↓), they’re so widely used and so important that we feel they warrant their own chapter.
In this chapter we will cover the ins and outs of snippets, from the snippet tag that you place in your templates, through how the snippet method is resolved, to the snippet method definition itself. We’ll also cover related topics and some advanced functionality in snippets for those looking to push Lift’s boundaries.

5.1 The Snippet Tag

Usage: 
       
       
The snippet tag is what you use to tell Lift where and how to invoke a snippet method on given XML content. The most important part of the tag is the snippet name, which is used to resolve which snippet method will process the snippet tag contents. We’ll cover how the snippet name is resolved to a concrete method in section 5.2↓.
Note that there is a shorthand for the type attribute simply by appending the snippet name after the lift: prefix. If you use this shorthand, make sure to avoid naming your snippets the same as Lift’s built-in tags, such as surround, children, embed, etc.
In addition to the the type attribute, Lift will process several other options:
form  If the form attribute is included with a value of either “POST” or “GET”, then an appropriate form tag will be emitted into the XHTML using the specified submission method. If you omit this tag from a snippet that generates a form, the form elements will display but the form won’t submit.
multipartThe multipart attribute is a boolean (the default is false, specify “yes”, “true” or “1” to enable) that specifies whether a generated form tag should be set to use multipart form submission. This is most typically used for file uploads (Розділ 6.4↓). If you don’t also specify the form attribute then this won’t do anything.
eager_evalThe eager_eval attribute is a boolean (the default is false, specify “yes”, “true” or “1” to enable) that controls the order of processing for the snippet tag contents. Normally, the snippet is processed and then the XML returned from the snippet is further processed for Lift tags. Enabling eager_eval reverses this order so that the contents of the snippet tag are processed first. We cover this in more detail with an example in Розділ 5.3.4↓.
With Lift 2.2’s Designer-Friendly Templates (Розділ 4.2 on page 1↑), you can also specify a snippet tag as part of the class attribute for a given element. Attributes for snippets invoked in this manner are passed via a query string. Listing 5.1↓ shows an example of how we can use the standard lift:surround processing by modiying the class of our content element.
Invoking Snippets Via the Class Attribute
1
2
3
4
5
6
7
8
9
10
11
12
"-//W3C//DTD XHTML 1.0 Transitional//EN"
   
    >Not really
   
   class="lift:content_id=real_content">
     
class="lift:surround?with=default;at=content" id="real_content">
       1>Welcome to your project!1>
     
   
 

5.2  Snippet Dispatch

The first step taken by Lift when evaluating a snippet tag is to resolve what snippet method will actually process the content. There are several mechanisms that are used to resolve the method, but they can be broken down into two main approaches: dispatch via reflection and explicit dispatch. In addition, Lift allows per-request remapping of snippet names via S.mapSnippet. We’ll cover each in the following sections.

5.2.1  Implicit Dispatch Via Reflection

The simplest, and default, approach to resolving snippet names is to use implicit dispatch via reflection. When using implicit dispatch, Lift will use the snippet name specified in the snippet tag to first locate a class. Lift will then either instantiate a class, or if it’s a stateful snippet (we’ll cover stateful snippets in Розділ 5.3.3↓), retrieve the current instance. One Lift has a class instance, it uses the snippet name to further determine which method in the class to execute. There are three ways to specify this:
  1. Via the type attribute on the snippet tag. The value should be “ClassName:method” for the particular snippet method you want to have handle the tag
  2. Via a tag suffix of Class.method. This is the same as specifying the type=”Class:method” attribute
  3. Via a tag suffix of just Class. This will use the render method on the specified class to handle the tag
Classes are resolved as specified in Розділ 3.2.1↑.
The most important thing to remember when using implicit dispatch is that your snippet classes must be members of a snippet subpackage as registered by LiftRules.addToPackages. For example, if you have LiftRules.addToPackages(“com.foo”) in your Boot.boot method, snippets should be members of com.foo.snippet.
Listing 5.2.1↓ shows three equivalent snippet tags. Note: these are only equivalent because the method name is “render.” If we had chosen a different method, e.g., “list,” then the third example below will still call a “render” method.
It’s important to note that with pure implicit dispatch, Java’s reflection allows access to any method on the enclosing class, no matter what the protection on the method is set to (e.g. private, protected). Because of this, it’s possible to invoke private and protected methods via implicit dispatch, which could be a security concern.This is one reason that we recommend using either DispatchSnippet or explicit dispatch for production sites. We’ll cover both of these approaches momentarily.
Another important note is that lookup via reflection is relatively expensive  [U]   [U] See http://www.jguru.com/faq/view.jsp?EID=246569 for a more thorough explanation operation, yet another reason that we recommend explicit dispatch for production sites.
Snippet Tag Equivalence
1
2
3
:snippet type="MyClass:render" />
:MyClass.render />
:MyClass />
In addition to “pure” implicit dispatch, you can exert a little more control on which method in a given class handles a snippet by implementing the net.liftweb.http.DispatchSnippet trait. This trait contains a single method, dispatch, of type PartialFunction[String,  NodeSeq ⇒ NodeSeq] that maps the method name (the “method” part of “Class.method” or “Class:method” as described above) to a particular method. Only method names defined in the dispatch PartialFunction can be executed; any methods that aren’t covered by the partial function will result in a snippet failure. Listing 5.2.1↓ shows how you can control the dispatch by providing a custom dispatch def.
Using DispatchSnippet to Control Snippet Method Selection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.foo.snippet
import scala.xml.{NodeSeq,Text}
import net.liftweb.http.DispatchSnippet
class SomeSnippetClass extends DispatchSnippet {
  def dispatch : DispatchIt = {
    // We have to use a partially-applied (trailing "_") version
    // of the functions that we dispatch to
    case "foo" => myFooMethod _
    case "bar" => someOtherBarMethod _
    case _ => catchAllMethod _
  }
  def myFooMethod (xhtml : NodeSeq) : NodeSeq = { ... }
  def someOtherBarMethod (xhtml : NodeSeq) : NodeSeq = { ... }
  def catchAllMethod(xhtml : NodeSeq) : NodeSeq = Text("You’re being naughty!")
}
To summarize, implicit dispatch is the default method by which Lift resolves snippet tag names to the actual class and method that will process the snippet tag contents. Although implicit dispatch is simple to use and works well, security concerns lead us to recommend the use of the DispatchSnippet trait. Even with DispatchSnippet, however, the implicit class resolution still uses reflection, so if you’re trying to make things performant you should use explicit dispatch instead.

5.2.2  Explicit Dispatch

Explicit dispatch allows you to have direct control over which methods will be executed for a given snippet name. There are two ways that you can define snippet name to method mappings: via LiftRules.snippetDispatch, which points Lift to DispatchSnippet instances, and LiftRules.snippets, which points Lift directly at methods.
Let’s first take a look at LiftRules.snippetDispatch, the more generic option. When a snippet tag is encountered with a snippet name of the form A.B or A:B, Lift will take the first portion (A) and use that as the lookup for snippetDispatch. The PartialFunction needs to return an instance of DispatchSnippet, so typically you will implement your explicit dispatch snippets using an object instead of a class. Listing 5.2.2↓ shows how we define our object. Note that the dispatch method will be executed with the “B” portion of the snippet name (as we defined above) as its argument. Other than the fact that it’s an object, the definition is essentially identical to our implicit dispatch class in Listing 5.2.1↑.
Defining an Explicit Snippet Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// The package *doesn’t* need to be "snippet" because there’s
// no reflection involved here
package com.foo.logic
import scala.xml.{NodeSeq,Text}
import net.liftweb.http.DispatchSnippet
object HelloWorld extends DispatchSnippet {
  // We define dispatch as a val so that it doesn’t get re-created
  // on each request
  val dispatch : DispatchIt = {
    case name => render(name) _
  }
  def render (name : String)(ignore : NodeSeq) : NodeSeq =
    Text("Hello, world! Invoked as " + name)
}
Now that we have our snippet object, we can bind it to a particular snippet name in our Boot.boot method, as shown in Listing 5.2.2↓. It’s interesting to note that this is actually how Lift defines many of its tags, such as , , and . In our case, we’ve bound our snippet object to , and because our DispatchSnippet uses a simple variable binding for its dispatch method case, we can invoke the same snippet with , , or even , and the snippet will tell us what name it was invoked with ( will invoke with the name “render”, following Lift’s normal snippet tag conventions). Noe that if you’re setting up a dispatch for a StatefulSnippet, return a new instance of your StatefulSnippet class. StatefulSnippet instances will properly register themselves ahead of the snippetDispatch partial function on each successive request.
Binding Our Explicit Snippet Object
1
2
3
4
5
6
7
8
9
10
11
class Boot {
  def boot {
    ...
    LiftRules.snippetDispatch.append {
      case "HelloWorld" => com.foo.logic.HelloWorld
      // For StatefulSnippets, return a *new instance*
      case "HelloConversation" =>
        new com.foo.logic.StatefulHelloWorld
    }
  }
}
Now let’s look at LiftRules.snippets. This is a more fine-grained approach to explicit dispatch that doesn’t require the DispatchSnippet trait. Instead, we bind a list of snippet name components corresponding to the parts of the snippet name separated by either “:” or “.”, and point it directly at a given snippet method. Assuming we’re using the same snippet object in Listing 5.2.2↑, we can bind the tag by setting up LiftRules.snippets in our Boot.boot method as shown in Listing 5.2.2↓. Notice that in order to bind the same way that we did with snippetDispatch, we need two lines to match the un-suffixed and suffixed versions. If you omit the un-suffixed line you will get a snippet failure.
Explicitly Binding a Snippet Method
1
2
3
4
5
6
7
8
9
10
11
import com.foo.logic
class Boot {
  def boot {
    ...
    LiftRules.snippets.append {
      // Matches a tag without a suffix ()
      case List("HelloWorld") => HelloWorld.render("no name") _
      case List("HelloWorld", name) => HelloWorld.render(name) _
    }
  }
}

5.2.3  Per-request Remapping

The final piece of snippet mapping that we want to discuss is per-request remapping. The S.mapSnippet method allows you to modify which snippet method will service a given snippet tag within your page processing. For example, Listing 5.2.3↓ shows how we can conditionally “blank” a snippet based on logic in a second snippet. This functionality isn’t used frequently as the other types of snippet dispatch, but it’s here in case you need it.
Remapping A Snippet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import scala.xml.NodeSeq
import net.liftweb.http.S
class Display {
  def header (xhtml : NodeSeq) : NodeSeq = {
    ...
    // If simple is set, we don’t display complexStuff
    S.param("simple").foreach {
      S.mapSnippet("complexStuff", ignore => Text(""))
    }
  }
  def complexStuff (xhtml : NodeSeq) : NodeSeq = {
    ...
  }
}

5.3  Snippet Methods

Now that we’ve examined how Lift determines which snippet to execute, let’s look at what a snippet method actually does. A snippet method is essentially a transform, taking a single scala.xml.NodeSeq argument and returning a NodeSeq.
Note: Although Scala can often infer return types, it’s important to explicitly specify the return type of your snippet methods as NodeSeq. Failure to do so may prevent Lift from locating the snippet method if you’re using implicit dispatch (Розділ 5.2.1↑), in which case the snippet won’t execute!
The argument passed to the snippet method is the XML content of the snippet tag. Because Lift processes XML from the root element down to the child elements (outside-in), the contents of the snippet tag aren’t processed until after the snippet method processes them. You may reverse the order of processing by specifying the eager_eval attribute on the tag (Розділ 5.3.4↓). As an example, let’s say we wanted a snippet that would output the current balance of our ledger, shown in Listing 5.3↓. We simply return an XML Text node with the formatted balance. Note that the XML result from a snippet is itself processed recursively, so the lift:Util.time snippet will be processed after our snippet method returns.
Returning Tags from a Snippet
1
2
3
4
5
class Ledger {
  def balance (content : NodeSeq) : NodeSeq =
    

{currentLedger.formattedBalance}

      as of :Util.time />

}
It is this hierarchical processing of template tags that makes Lift so flexible. For those of you coming to Lift with some JSP experience, Lift is designed to let you write something similar to tag libraries, but that are much more powerful and much simpler to use.

5.3.1  Binding Values in Snippets

So far we’ve shown our snippets generating complete output and ignoring the input to the method. Lift actually provides some very nice facilities for using the input NodeSeq within your snippet to help keep presentation and code separate. First, remember that the input NodeSeq consists of the child elements for the snippet tag in your template.
Snippet Tag Children
1
2
3
:Ledger.balance>
  :balance/> as of :time />
:Ledger.balance>
For example, given a template containing the snippet tag shown in Listing 5.3.1↑, the Ledger.balance method receives
 as of 
as its input parameter. This is perfectly correct XML, although it may look a little strange at first unless you’ve used prefixed elements in XML before. The key is that Lift allows you to selectively “bind”, or replace, these elements with data inside your snippet. The Helpers.bind [V]   [V] net.liftweb.util.Helpers. Technically the bind method is overloaded, and can even fill in values for the lift:bind tag, but this is advanced usage and we’re not going to cover that here. method takes three arguments:
  1. The prefix for the tags you wish to bind, in this instance, “ledger”
  2. The NodeSeq that contains the tags you wish to bind
  3. One or more BindParam elements that map the tag name to a replacement value
While you can create your own BindParam instances by hand, we generally recommend importing Helpers._, which among other things contains a convenient implicit conversion to BindParam using the “->” operator. With this knowledge in hand, we can change our previous definition of the balance method in Listing 5.3↑ to that in Listing 5.3.1↓ below.
Binding the Ledger Balance
1
2
3
4
5
6
class Ledger {
  def balance (content : NodeSeq ) : NodeSeq =
    bind ("ledger", content,
          "balance" -> Text(currentLedger.formattedBalance),
          "time" -> Text((new java.util.Date).toString))
}
As you can see here, we actually gain a line of code over our previous effort, but the trade-off makes it far simpler for us to change the layout just by editing the template.
One last aspect of binding that we want to discuss is that any attributes set on the input elements that are being bound will be discarded if you use the “->” binding operator. See Розділ 5.4↓ for more details on how you manipulate attributes in bindings, including how you can retain attributes on binding elements from your templates by using the “-%>” binding operator instead.

5.3.2  CSS Selector Transforms

In addition to the binding support detailed in Розділ 5.3.1↑, Lift 2.2 introduces binding via CSS transforms as part of its support for designer friendly templates. These allow you to bind values into template XHTML (or HTML5, see Розділ 4.3 on page 1↑) by using the attributes on specific elements. Let’s start by looking at a basic example, corresponding to the examples in Розділ 5.3.1↑.
Listing 5.3.2↓ shows a Designer-Friendly version of Listing 5.3.1↑. You can see that we’re invoking the Ledger.balance snippet via the class attribute, and we’ve specified the binding elements as normal elements with id attributes.
A Simple CSS Snippet
1
2
3
class="lift:Ledger.balance">
  ="balance">$0 as of ="time">midnight
Now, we need to perform the CSS transform within our snippet. The binding implicits for CSS transforms are found on the net.liftweb.util.BindHelpers object/trait, so you should import it (in particular, the strToCssBindPromoter method). Listing 5.3.2↓ shows how we modify the snippet in Listing 5.3.1↑ to utilize the new CSS transform.
Binding the Ledger Balance with CSS
1
2
3
4
5
6
import net.liftweb.util.BindHelpers._
class Ledger {
  def balance = "#balance" #> currentLedger.formattedBalance &
    "#time" #> (new java.util.Date).toString
}
As you can see in this example, CSS transforms are comprised of three parts: the transform selector, the transform operator (#>), and the right hand side value. This value can be a number of different things, which we’ll cover in Розділ 5.3.2.2↓, but in our case we’re using a MappedField and a String. Additionally, you can chain transforms together with the & operator.

5.3.2.1  CSS Selector Syntax

The selector syntax is based on a subset of CSS, so if you already know that you’re well on your way. The syntax can operate on elements based on id or class, and can also operate on attributes of those elements. Let’s look at the basic syntax:
The element matching the selector is replaced by the result of processing the replacement. That means that in the example of Listing 5.3.2↑ the span elements will be replaced with straight Text elements, resulting in the markup shown in Listing 5.3.2.1↓ (in other words, no remaining markup).
Sample CSS Transform Result
1
$12.42 as of Fri Jan 14 08:29:50 MST 2011
You can further refine the replacement with an optional qualifier. We’ve already seen how omitting the qualifer results in wholesale replacement of the matching element, but there are a few additional options:

5.3.2.2  Right Hand Side Values

The right hand side of a CSS transform operates on the selected element to either transform or replace it. It can be one of:

5.3.3  Stateless versus Stateful Snippets

The lifecycle of a snippet is stateless by default. That means that for each request, Lift creates a new instance of the snippet class to execute (or uses the same staic method if using explicit dispatch, Розділ 5.2.2↑). Any changes you make to instance variables will be discarded after the request is processed. If you want to keep some state around, you have a couple of options:
Using a StatefulSnippet is very similar to using a normal snippet but with the addition of a few mechanisms. First, the StatefulSnippet trait extends DispatchSnippet (see Розділ 5.2.1↑), allowing you to define which methods handle which snippets based on the dispatch method. Because Scala allows defs to be implemented by vars in subclasses, we can redefine the dispatch behavior as a result of snippet processing.
Another thing to remember when using StatefulSnippets is that when you render a form, a hidden field is added to the form that permits the same instance of the StatefulSnippet that created the form to be the target of the form submission. If you need to link to a different page, but would like the same snippet instance to handle snippets on that page, use the StatefulSnippet.link method (instead of SHtml.link); similarly, if you need to redirect to a different page, the StatefulSnippet trait defines a redirectTo method. In either of these instances, a function map is added to the link or redirect, respectively, that causes the instance to be reattached.
When might you use a stateful snippet? Consider a multi-part form where you’d like to have a user enter data over several pages. You’ll want the application to maintain the previously entered data while you validate the current entry, but you don’t want to have to deal with a lot of hidden form variables. Using a StatefulSnippet instance greatly simplifies writing the snippet because you can keep all of your pertinent information around as instance variables instead of having to insert and extract them from every request, link, etc.
Listing 5.3.3↓ shows an example of a stateful snippet that handles the above example. Note that for this example, the URL (and therefore, the template) don’t change between pages. The template we use is shown in Listing 5.3.3↓. Remember to call unregisterThisSnippet() when you’re finished with your workflow in order to stop the current instance from being used.
Using a StatefulSnippet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
... standard Lift imports ...
import scala.xml.Text
class BridgeKeeper extends StatefulSnippet {
  // Define the dispatch for snippets. Note that we are defining
  // it as a var so that the snippet for each portion of the
  // multi-part form can update it after validation.
  var dispatch : DispatchIt = {
    // We default to dispatching the "challenge" snippet to our
    // namePage snippet method. We’ll update this below
    case "challenge" => firstPage _
  }
  // Define our state variables:
  var (name,quest,color) = ("","","")
  // Our first form page
  def firstPage (xhtml : NodeSeq) : NodeSeq = {
    def processName (nm : String) {
      name = nm
      if (name != "") {
        dispatch = { case "challenge" => questPage _ }
      } else {
        S.error("You must provide a name!")
      }
    }
    bind("form", xhtml,
         "question" -> Text("What is your name?"),
         "answer" -> SHtml.text(name, processName))
  }
  def questPage (xhtml : NodeSeq) : NodeSeq = {
    def processQuest (qst : String) {
      quest = qst
      if (quest != "") {
        dispatch = {
          case "challenge" if name == "Arthur" => swallowPage _
          case "challenge" => colorPage _
        }
      } else {
        S.error("You must provide a quest!")
      }
    }
    bind("form", xhtml,
         "question" -> Text("What is your quest?"),
         "answer" -> SHtml.text(quest, processQuest))
  }
  def colorPage (xhtml : NodeSeq) : NodeSeq = {
    def processColor (clr : String) {
      color = clr
      if (color.toLowercase.contains "No,") {
        // This is a cleanup that removes the mapping for this
        // StatefulSnippet from the session. This will happen
        // over time with GC, but it’s best practice to manually
        // do this when you’re finished with the snippet
        this.unregisterThisSnippet()
        S.redirectTo("/pitOfEternalPeril")
      } else if (color != "") {
        this.unregisterThisSnippet()
        S.redirectTo("/scene24")
      } else {
        S.error("You must provide a color!")
      }
    }
    bind("form", xhtml,
         "question" -> Text("What is your favorite color?"),
         "answer" -> SHtml.text(color, processColor))
  }
  // and so on for the swallowPage snippet
  ...
}
The StatefulSnippet Example Template
1
2
3
4
5
6
<lift:surround with="default" at="content">
  <lift:BridgeKeeper.challenge form="POST">
    <form:question /> : <form:answer /> <br />
    <input type="submit" value="Answer" />
  lift:BridgeKeeper.challenge>
lift:surround>
If you’re using implicit dispatch (Розділ 5.2.1↑), then you’re done. If you want to use explicit dispatch, however, you need to do a little more work than usual in the LiftRules.snippetDispatch setup. Listing 5.3.3↓ shows how we can bind our own StatefulSnippet classes without using reflection.
Explicit Dispatch with Stateful Snippets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In your boot method:
LiftRules.snippetDispatch.append {
  // S.snippetForClass checks to see if an instance has already
  // registered. This is the case after form submission or when
  // we use the StatefulSnippet.link or .redirectTo methods
  case "BridgeKeeper" => S.snippetForClass("TestHello") openOr {
    // If we haven’t already registered an instance, create one
    val inst = new com.test.TestHello
    // The name is what Lift uses to locate an instance (S.snippetForClass)
    // We need to add it so that the Stateful callback functions can
    // self-register
    inst.addName("TestHello")
    // Register this instance for the duration of the request
    S.overrideSnippetForClass("TestHello", inst)
    inst
  }

5.3.4  Eager Evaluation

As we mentioned in Розділ 5.3↑, Lift processes the contents of a snippet tag after it processes the tag itself. If you want the contents of a snippet tag to be processed before the snippet, then you need to specify the eager_eval attribute on the tag:
...
This is especially useful if you’re using an embedded template (Розділ 4.5.7↑). Consider Listing 5.3.4↓: in this case, the eager_eval parameter makes Lift process the tag before it executes the Hello.world snippet method. If the “formTemplate” template looks like Listing 5.3.4↓, then the Hello.world snippet sees the and XML tags as its NodeSeq input. If the eager_eval attribute is removed, however, the Hello.world snippet sees only a tag that will be processed after it returns.
Embedding and eager evaluation
1
2
3
<lift:Hello.world eager_eval="true">
  <lift:embed what="formTemplate" />
lift:Hello.world>
The formTemplate template
1
2
3
4
<lift:children>
<hello:name />
<hello:time />
lift:children>

5.4 Handling XHTML Attributes in Snippets

It’s a common requirement that elements contain XHTML attributes to control things like style, provide an id, register javascript event handlers, and other functionality. Lift provides two main approaches to applying attributes to elements either in your snippet code or directly in the XHTML template.

5.4.1 Direct Manipulation in Code

You can apply attributes directly to XHTML elements using the “%” operator to apply a
scala.xml.UnprefixedAttribute instance [W]   [W] there’s a corresponding PrefixedAttribute as well to an element. Lift’s net.liftweb.util.Helpers trait contains an implicit conversion from a Pair[String,_] to an UnprefixedAttribute called pairToUnprefixed that allows you to use a simpler syntax. You may chain invocations of “%” to apply multiple attributes. For example, Listing 5.4.1↓ shows how you can apply an “id” and “class” attribute to a text box and to a normal paragraph.
Applying Attributes with %
1
2
val myInput = SHtml.text("", processText(_)) % ("id" -> "inputField") %
  ("class" -> "highlighted")
Note that the % metadata mechanism is actually part of the Scala XML library. Specifically, scala.xml.Elem has a % method that allows the user to update the attributes on a given XML element by passing in a scala.xml.UnprefixedAttribute. We suggest reading more about this in the Scala API documents, or in the Scala XML docbook at http://burak.emir.googlepages.com/scalaxbook.docbk.html.

5.4.2 XHTML Attribute Pass-through

The second main approach to modifying XHTML attributes is to specify them directly in your templates. This has the benefit of allowing your template designers to directly manipulate things like style-related attributes and keeping the markup and the logic separate. Listing 5.4.2↓ shows how you can utilize the “-%>” binding operator instead of “->” to preserve attributes.
Snippet mixin attributes
1
2
3
4
5
6
7
8
9
10
11
12
13
// the markup
:Ledger.balance>
  :time id="myId"/>
:Ledger.balance>
// The snippet class
class Ledger {
  def balance (content : NodeSeq ) : NodeSeq = {
    bind ("ledger", content,
          "time" -%> {(new java.util.Date).toString})
  }
}
The resulting node will be something like
Sat Mar 28 16:43:48 EET 2009
In addition to the “-%>” binding operator, there is also the “_id_>” operator, which uses the element’s name as its “id” attribute. Listing shows a snippet method using the “_id_>” attribute and Listing shows the resulting markup.
Binding with the _id_> operator
1
2
def idByName (xhtml : NodeSeq) : NodeSeq =
  bind("example", xhtml, "name" _id_> Fred)
Markup bound using _id_>
1
2
3
4
5
6
7
<lift:HelloWorld.idByName>
  Hi, <example:name />
lift:HelloWorld.idByName>
Hi, <span id="name">Fredspan>

6 Forms in Lift

In this chapter we’re going to discuss the specifics of how you generate and process forms with Lift. Besides standard GET/POST form processing, Lift provides AJAX forms (Глава 11↓) as well as JSON form processing (Розділ 10.4.1↓), but we’re going to focus on the standard stuff here. We’re going to assume that you have a general knowledge of basic HTML form tags as well as how CGI form processing works.

6.1  Form Fundamentals

Let’s start with the basics of Lift form processing. A form in Lift is usually produced via a snippet that contains the additional form attribute. As we mentioned in Розділ 5.1↑, this attribute takes the value GET or POST, and when present makes the snippet code embed the proper form tags around the snippet HTML. Listing 6.1↓ shows an example of a form that we will be discussing throughout this section.
An Example Form Template
1
2
3
4
:Ledger.add form="POST">
  :description /> :amount />
  :submit />
:Ledger.add>
The first thing to understand about Lift’s form support is that you generally don’t use the HTML tags for form elements directly, but rather you use generator functions on
net.liftweb.http.SHtml. The main reason for this is that it allows Lift to set up all of the internal plumbing so that you keep your code simple. Additionally, we use Lift’s binding mechanism (Розділ 5.3.1↑) to “attach” the form elements in the proper location. In our example in Listing 6.1↑, we have bindings for a description field, an amount, and a submit button.
Our next step is to define the form snippet itself. Corresponding to our example template is Listing 6.1↓. This shows our add method with a few vars to hold the form data and a binding to the proper form elements. We’ll cover the processEntryAdd method in a moment; for now let’s look at what we have inside the add method.
An Example Form Snippet
1
2
3
4
5
6
7
8
9
10
11
def add (xhtml : NodeSeq) : NodeSeq = {
  var desc = ""
  var amount = "0"
  def processEntryAdd () { ... }
  bind("entry", xhtml,
       "description" -> SHtml.text(desc, desc = _),
       "amount" -> SHtml.text(amount, amount = _),
       "submit" -> SHtml.submit("Add", processEntryAdd))
}
First, you may be wondering why we use vars defined inside the method. Normally, these vars would be locally scoped (stack-based) and would be discarded as soon as the method returns. The beauty of Scala and Lift is that the right hand argument of each of the SHtml functions is actually a function itself. Because these functions, also known as anonymous closures, reference variables in local scope, Scala magically transforms them to heap variables behind the scenes. Lift, in turn, adds the function callbacks for each form element into its session state so that when the form is submitted, the appropriate closure is called and the state is updated. This is also why we define the processEntryAdd function inside of the add method: by doing so, the processEntryAdd function also has access to the closure variables. In our example, we’re using Scala’s placeholder “_” shorthand [X]   [X] For more details on placeholders, see the Scala Language Specification, section 6.23 to define our functions. Your description processing function could also be defined as:
newDesc => description = newDesc
One important thing to remember, however, is that each new invocation of the add method (for each page view) will get its own unique instance of the variables that we’ve defined. That means that if you want to retain values between submission and re-rendering of the form, you’ll want to use RequestVars (Розділ 3.11↑) or a StatefulSnippet (Розділ 5.3.3↑) instead . Generally you will only use vars defined within the snippet method when your form doesn’t require validation and you don’t need any of the submitted data between snippet executions. An example of using RequestVars for your form data would be if you want to do form validation and retain submitted values if validation fails, as shown in Listing 6.1↓. In this instance, we set an error message (more in Глава B↓). Since we don’t explicitly redirect, the same page is loaded (the default “action” for a page in Lift is the page itself) and the current RequestVar value of description is used as the default value of the text box.
Using RequestVars with Forms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object description extends RequestVar("")
object amount extends RequestVar("0")
def add (xhtml : NodeSeq) : NodeSeq = {
  def processEntryAdd () =
    if (amount.toDouble <= 0) {
      S.error("Invalid amount")
    } else {
      // ... process Add ...
      redirectTo(...)
    }
  bind("entry", xhtml,
       "description" -> SHtml.text(description.is, description(_)),
  ...
}
The next thing to look at is how the form elements are generated. We use the SHtml helper object to generate a form element of the appropriate type for each variable. In our case, we just want text fields for the description and amount, but SHtml provides a number of other form element types that we’ll be covering later in this section. Generally, an element generator takes an argument for the initial value as well as a function to process the submitted value. Usually both of these arguments will use a variable, but there’s nothing stopping you from doing something such as
“description” -> SHtml.text(“”, println(“Description = “ + _))
Finally, our submit function executes the partially applied processEntryAdd function, which, having access to the variables we’ve defined, can do whatever it needs to do when the submit button is pressed.

6.2 Attributes for Form Elements

In addition to the approaches shown in Розділ 5.4↑, the SHtml generator functions allow you to apply attributes by passing the attribute name/value pairs as final arguments. This is usually simpler, and in some cases is much simpler than using the “%” operator directly. For example, checkbox and radio form elements are acutally returned as ChoiceHolder instances, which do not directly support the “%” operator. Listing 6.2↓ shows how to apply the same attributes as Listing 5.4.1↑ using the varargs approach.
Applying Attributes as Varargs
1
2
val myInput = SHtml.text("", processText(_), "id" -> "inputField",
  "class" -> "highlighted")

6.3 An Overview of Form Elements

Now that we’ve covered the basics of forms, we’re going to go into a little more detail for each form element generator method on SHtml. The a method (all 3 variants) as well as the ajax* methods are specific to AJAX forms, which are covered in detail in Глава 11↓. The json* methods are covered in Розділ 10.4.1↓. We’ll be covering the fileUpload method in detail in Розділ 6.4↓. One final note before we dive in is that most generator methods have an overload with a trailing asterisk (i.e. hidden_*); these are generally equivalent to the overloads without an asterisk but are intended for Lift’s internal use.

6.3.1  checkbox

The checkbox method generates a checkbox form element, taking an initial Boolean value as well as a function (Boolean)  ⇒ Any that is called when the checkbox is submitted. If you’ve done a lot of HTML form processing you might wonder how this actually occurs, since an unchecked checkbox is not actually submitted as part of a form. Lift works around this by adding a hidden form element for each checkbox with the same element name, but with a false value, to ensure that the callback function is always called. Because more than one XML node is returned by the generator, you can’t use the % metadata mechanism to set attributes on the check box element. Instead, use attribute pairs as arguments to the generator function as outlined in Розділ 5.4.1↑.
For example, Listing 6.3.1↓ shows a checkbox with an id of “snazzy” and a class attribute set to “woohoo.”
A Checkbox Example
1
2
SHtml.checkbox_id(false, if (_) frobnicate(),
                  Full("snazzy"), "class" -> "woohoo")

6.3.2  hidden

The hidden method generates a hidden form field. Unlike the HTML hidden field, the hidden tag is not intended to hold a plain value; rather, in Lift it takes a function () ⇒ Any argument that is called when the form is submitted. As with most of the other generators, it also takes a final varargs sequence of Pair[String,String] attributes to be added to the XML node. Listing 6.3.2↓ shows an example of using a hidden field to “log” information. (When the form is submitted, “Form was submitted” will be printed to stdout. This can be a useful trick for debugging if you’re not using a full-blown IDE.)
A Hidden Example
1
SHtml.hidden(() => println("Form was submitted"))

6.3.3  link

The link method generates a standard HTML link to a page (an tag, or anchor), but also ensures that a given function is executed when the link is clicked. The first argument is the web context relative link path, the second argument is the () ⇒ Any function that will be executed when the link is clicked, and the third argument is a NodeSeq that will make up the body of the link. You may optionally pass one or more Pair[String,String] attributes to be added to the link element. Listing 6.3.3↓ shows using a link to load an Expense entry for editing from within a table. In this case we’re using a RequestVar to hold the entry to edit, so the link function is a closure that loads the current Expense entry. This combination of link and RequestVars is a common pattern for passing objects between different pages.
A Link Example
1
2
3
4
5
6
7
8
9
10
11
12
13
object currentExpense extends RequestVar[Box[Expense]](Empty)
def list (xhtml : NodeSeq) : NodeSeq = {
  ...
  val entriesXml =
    entries.map(entry =>
      bind("entry", chooseTemplate("expense", "entries", xhtml),
        ...
        "edit" -> SHtml.link("/editExpense",
          () => currentExpense(Full(entry)),
          Text("Edit")))
  )
}

6.3.4  text and password

The text and password methods generate standard text and password input fields, respectively. While both take string default values and (String) ⇒ Any functions to process the return, the password text field masks typed characters and doesn’t allow copying the value from the box on the client side. Listing 6.3.4↓ shows an example of using both text and password for a login page.
A Text Field Example
1
2
3
4
5
6
7
8
def login(xhtml : NodeSeq) : NodeSeq = {
  var user = ""; var pass = "";
  def auth () = { ... }
  bind("login", xhtml,
       "user" -> SHtml.text(user, user = _, "maxlength" -> "40")
       "pass" -> SHtml.password(pass, pass = _)
       "submit" -> SHtml.submit("Login", auth))
}
Alternatively, you might want the user (but not the password) to be stored in a RequestVar so that if the authentication fails the user doesn’t have to retype it. Listing 6.3.4↓ shows how the snippet would look in this case.
A RequestVar Text Field Example
1
2
3
4
5
6
7
8
9
object user extends RequestVar[String]("")
def login(xhtml : NodeSeq) : NodeSeq = {
  var pass = "";
  def auth () = { ... }
  bind("login", xhtml,
       "user" -> SHtml.text(user.is, user(_), "maxlength" -> "40"),
       "pass" -> SHtml.password(pass, pass = _),
       "submit" -> SHtml.submit("Login", auth))
}

6.3.5  textarea

The textarea method generates a textarea HTML form element. Generally the functionality mirrors that of text, although because it’s a textarea, you can control width and height by adding cols and rows attributes as shown in Listing 6.3.5↓. (You can, of course, add any other HTML attributes in the same manner.)
A Textarea Example
1
2
3
4
var noteText = ""
val notes =
  SHtml.textarea(noteText, noteText = _,
                 "cols" -> "80", "rows" -> "8")

6.3.6  submit

Submit generates the submit form element (typically a button). It requires two parameters: a String value to use as the button label, and a function () ⇒ Any that can be used to process your form results. One important thing to note about submit is that form elements are processed in the order that they appear in the HTML document. This means that you should put your submit element last in your forms: any items after the submit element won’t have been “set” by the time the submit function is called. Listings 6.3.4↑ and 6.3.4↑ use the SHtml.submit method for the authentication handler invocation.

6.3.7  multiselect

Up to this point we’ve covered some fairly simple form elements. Multiselect is a bit more complex in that it doesn’t just process single values. Instead, it allows you to select multiple elements out of an initial Seq and then process each selected element individually. Listing 6.3.7↓ shows using a multiselect to allow the user to select multiple categories for a ledger entry. We assume that a Category entity has an id synthetic key as well as a String name value. The first thing we do is map the collection of all categories into pairs of (value, display) strings. The value is what will be returned to our processing function, while the display string is what will be shown in the select box for the user. Next, we turn the current entry’s categories into a Seq of just value strings, and we create a Set variable to hold the returned values. Finally, we do our form binding. In this example we use a helper function, loadCategory (not defined here), that takes a String representing a Category’s primary key and returns the category. We then use this helper method to update the Set that we created earlier. Note that the callback function will be executed for each selected item in the multiselect, which is why the callback takes a String argument instead of a Set[String]. This is also why we have to use our own set to manage the values. Depending on your use case, you may or may not need to store the returned values in a collection.
Using multiselect
1
2
3
4
5
6
7
8
def mySnippet ... {
  val possible = allCategories.map(c => (c.id.toString, c.name))
  val current = currentEntry.categories.map(c => c.id.toString)
  var updated = Set.empty[Category]
  bind (...,
    "categories" ->
      SHtml.multiselect(possible, current, updated += loadCategory(_)))
}

6.3.8  radio

The radio method generates a set of radio buttons that take String values and return a single String (the selected button) on form submission. The values are used as labels for the Radio buttons, so you may need to set up a Map to translate back into useful values. The radio method also takes a Box[String] that can be used to pre-select one of the buttons. The value of the Box must match one of the option values, or if you pass Empty no buttons will be selected. Listing 6.3.8↓ shows an example of using radio to select a color. In this example, we use a Map from color names to the actual color values for the translation. To minimize errors, we use the keys property of the Map to generate the list of options.
Using radio for Colors
1
2
3
4
5
6
import java.awt.Color
var myColor : Color = _
val colorMap = Map("Red" -> Color.red,
                   "White" -> Color.white,
                   "Blue" -> Color.blue)
val colors = SHtml.radio(colorMap.keys.toList, Empty, myColor = colorMap(_))

6.3.9  select

The select method is very similar to the multiselect method except that only one item may be selected from the list of options. That also means that the default option is a Box[String] instead of a Seq[String]. As with multiselect, you pass a sequence of (value, display) pairs as the options for the select, and process the return with a (String) ⇒ Any function. Listing 6.3.9↓ shows an example of using a select to choose an account to view.
A select Example
1
2
3
4
5
var selectedAccount : Account = _
val accounts = User.accounts.map(acc => (acc.id.toString, acc.name))
val chooseAccount =
  SHtml.select(accounts, Empty,
               selectedAccount = loadAccount(_), "class" -> "myselect")
An important thing to note is that Lift will verify that the value submitted in the form matches one of the options that was passed in. If you need to do dynamic updating of the list, then you’ll need to use untrustedSelect (Розділ 6.3.11↓).

6.3.10  selectObj

One of the drawbacks with the select and multiselect generators is that they deal only in Strings; if you want to select objects you need to provide your own code for mapping from the strings. The selectObj generator method handles all of this for you. Instead of passing a sequence of (value string, display string) pairs, you pass in a sequence of (object, display string) pairs. Similarly, the default value is a Box[T] and the callback function is (T) ⇒ Any , where T is the type of the object (selectObj is a generic function). Listing 6.3.10↓ shows a reworking of our radio example (Listing 6.3.8↑) to select Colors directly. Note that we set the select to default to Color.red by passing in a Full Box.
Using selectObj for Colors
1
2
3
4
5
6
7
8
9
10
11
... standard Lift imports ...
import _root_.java.awt.Color
class SelectSnippet {
  def chooseColor (xhtml : NodeSeq) : NodeSeq = {
    var myColor = Color.red
    val options = List(Color.red, Color.white, Color.blue)
    val colors = SHtml.selectObj(options, Full(myColor), myColor = _)
    bind(...)
  }
}

6.3.11  untrustedSelect

The untrustedSelect generator is essentially the same as the select generator, except that the value returned in the form isn’t validated against the original option sequence. This can be useful if you want to update the selection on the client side using JavaScript.

6.4  File Uploads

File uploads are a special case of form submission that allow the client to send a local file to the server. This is accomplished by using multipart forms. You can enable this by setting the multipart attribute on your snippet tag to true. Listing 6.4↓ shows how we can add a file upload to our existing expense entry form so that users can attach scanned receipts to their expenses. We modify our template to add a new form, shown below. Note the multipart=”true” attribute.
File Upload Template
  ... existing form fields ...
  
  ...
:AddEntry.addEntry>
1
2
3
4
5
6
7
:AddEntry.addEntry form="POST" multipart="true">
  ... existing headers ...
  
Receipt (JPEG or PNG) :receipt />
On the server side, Listing 6.4↓ shows how we modify the existing addEntry snippet to handle the (optional) file attachment. We’ve added some logic to the existing form submission callback to check to make sure that the image is of the proper type, then we use the SHtml file upload generator with a callback that sets our fileHolder variable. The callback for the fileUpload generator takes a FileParamHolder, a special case class that contains information about the uploaded file. The FileParamHolder case class has four parameters:
nameThe name of the form field that this file is associated with, as sent by the client
mimeTypeThe mime type as sent by the client
filenameThe filename as sent by the client
fileAn Array[Byte] containing the uploaded file contents
In our example, we want to save the file data into a MappedBinary field on our expense entry. You could just as easily process the data in place using a scala.io.Source or
java.io.ByteArrayInputStream, or output it using a java.io.FileOutputStream.
File Upload Snippet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class AddEntry {
  ...
  // Add a variable to hold the FileParamHolder on submission
  var fileHolder : Box[FileParamHolder] = Empty
  ...
  def doTagsAndSubmit (t : String) {
    ...
    val e : Expense = ...
    // Add the optional receipt if it’s the correct type
    val receiptOk = fileHolder match {
      // An empty upload gets reported with a null mime type,
      // so we need to handle this special case
      case Full(FileParamHolder(_, null, _, _)) => true
      case Full(FileParamHolder(_, mime, _, data))
        if mime.startsWith("image/") => {
          e.receipt(data).receiptMime(mime)
          true
        }
      case Full(_) => {
        S.error("Invalid receipt attachment")
        false
      }
      case _ => true
    }
    (e.validate, receiptOk) match {
    ...
    }
    ...
  }
  bind("e", in,
       ...
       "receipt" -> SHtml.fileUpload(fileHolder = _),
       "tags" -> SHtml.text(tags, doTagsAndSubmit))
  }
}
By default, Lift will utilize the InMemoryFileParamHolder to represent uploaded file data. This implementation reads the uploaded data directly into memory (you retrieve the byte array with the file val). If you would prefer to have Lift write uploaded data to disk and then give you a server-local filename to work with, you can use the LiftRules.handleMimeFile configuration hook to instead use the OnDiskFileParamHolder, as shown in Listing 6.4↓. The OnDiskFileParamHolder class has an additional property, localFile, that is a java.io.File object for the temporary upload file.
Using OnDiskFileParamHolder
1
2
// in Boot.boot:
LiftRules.handleMimeFile = OnDiskFileParamHolder.apply

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:
  • Access control mechanisms that deal not only with whether a menu item is visible, but also whether the page it points to is accessible
  • Grouping of menu items so that you can easily display portions of menus where you want them
  • Nested menus so you can have hierarchies
  • Request rewriting (similar to Розділ 3.7↑)
  • State-dependent computations for such things as page titles, page-specific snippets, etc.
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
1
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
1
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
1
2
3
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 Розділ 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 (Розділ 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 (Розділ 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 (Розділ 7.2.2↓)
  4. An optional set of LocParam parameters that control the behavior and appearance of the menu item (see Розділs 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
1
2
3
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 Розділ 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
1
2
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:
  • /help/index
  • /help/usage
But the following would not:
  • /index
  • /site/example
We’ll cover how you can customize the rendering of the menus in Розділ 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
1
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, Розділ 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
1
2
3
4
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 Розділ 7.3.1↓.)
Hidden Menus
1
2
3
4
5
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 "’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
1
2
3
4
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 Розділ 7.6.2↓.

7.2.3  Using

So far we’ve covered the Scala side of things. The other half of the magic is the special 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 (
    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
1
2
3
<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:
  • ul - Adds the specified attribute to the
      element that makes up the menu
    • li - Adds the specified attribute to each
    • element for the menu
    • li_item - Adds the specified attribute to the current page’s menu item
    • li_path - Adds the specified attribute to the current page’s breadcrumb trail (the breadcrumb trail is the set of menu items that are direct ancestors in the menu tree)
    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
    1
    2
    3
    4
    <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 (Розділ 7.6↓), or you use the Title LocParam (Розділ 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
    1
    2
    3
    4
    // In Boot:
    val MyMenu = Menu(Loc("Home Page", "index" :: Nil, "Home"))
    // In template (or wherever)
    <lift</code><code class="scala keyword">:</code><code class="scala plain">Menu.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
    1
    2
    3
    4
    <lift:Menu.item name="Home Page"
      a:class="homeLink">
      <b>Go Homeb>
    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 Розділ 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 (Розділ 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
    1
    2
    3
    4
    5
    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 (Розділ 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    val homepageTempl = Template({ () =>
      :surround with="default" at="content">
      { if (User.loggedIn_?) {
          :Entries.list />
        } else {
          :embed what="welcome" />
        }
      }
      :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 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    1
    2
    3
    4
    5
    6
    val entrySnippets = new DispatchLocSnippets {
      def dispatch = {
        case "entries" => Entries.list _
        case "add" => Entries.newEntry _
      }
    }

    7.4.3  Title

    As we mentioned in Розділ 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 Розділ 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    1
    2
    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 Розділ 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
    1
    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 tag controls where the menu items are rendered. If you don’t provide body elements, or if you provide body elements without the element, your body XHTML will be ignored and the menu will be rendered directly.
    Binding a Menu Group
    1
    2
    3
    4
    5
    6
    7
    8
    class="site">
      
      :Menu.group group="site"
        a:class="siteLink">
       
  • :bind />
  •   :Menu.group>
      

    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:
    • def name: the name that can be used to retrieve the menu via Menu.item
    • def link: the actual link; you can use the implicit conversions from List[String] or Pair[List[String],Boolean], or you can create the Link object yourself
    • def text: the text that will be displayed to the user; you can use the implicit conversion from String, or you can provide your own LinkText instance
    • def params: must return a List[LocParam] that is used to control behavior as we’ve shown in the previous sections
    • def defaultParams: used for type-safe rewriting, which we’ll cover in Розділ 7.6.2↓
    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
    1
    2
    3
    4
    5
    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
    1
    2
    3
    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/". 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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
    1
    2
    3
    4
    5
    6
    :surround with="default" at="content">
      :entries  eager_eval="true">
        1>:Menu.title />1>
        :embed what="entry_table" />
      :entries>
    :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
    1
    2
    3
    4
    5
    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.

    8 The Mapper and Record Frameworks

    In our experience, most webapps end up needing to store user data somewhere. Once you start working with user data, though, you start dealing with issues like coding up input forms, validation, persistence, etc. to handle the data. That’s where the Mapper and Record frameworks come in. These frameworks provides a scaffolding for all of your data manipulation needs. Mapper is the original Lift persistence framework, and it is closely tied to JDBC for its storage. Record is a new refactorization of Mapper that is backing-store agnostic at its core, so it doesn’t matter whether you want to save your data to JDBC, JPA, or even something such as XML. With Record, selecting the proper driver will be as simple as hooking the proper traits into your class.
    The Record framework is relatively new to Lift. The plan is to move to Record as the primary ORM framework for Lift sometime post-1.0. Because Record is still under active design and development, and because of its current “moving target” status, this chapter is mostly going to focus on Mapper. We will, however, provide a few comparitive examples of Record functionality to give you a general feel for the flavor of the changes. In any case, Mapper will not go away even when record comes out, so you can feel secure that any code using Mapper will be viable for quite a while.

    8.1  Introduction to Mapper and MetaMapper

    Let’s start by discussing the relationship between the Mapper and MetaMapper traits (and the corresponding Record and MetaRecord). Mapper provides the per-instance functionality for your class, while MetaMapper handles the global operations for your class and provides a common location to define per-class static specializations of things like field order, form generation, and HTML representation. In fact, many of the Mapper methods actually delegate to methods on MetaMapper. In addition to Mapper and MetaMapper, there is a third trait, MappedField, that provides the per-field functionality for your class. In Record, the trait is simply called “Field”. The MappedField trait lets you define the individual validators as well as filters to transform the data and the field name. Under Record, Field adds some functionality such as tab order and default error messages for form input handling.

    8.1.1  Adding Mapper to Your Project

    Since Mapper is a separate module, you need to add the following dependency to your pom.xml to access it:
    Mapper POM Dependency
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <project ...>
      ...
      <dependencies>
        ...
        <dependency>
          <groupId>net.liftwebgroupId>
          <artifactId>lift-mapperartifactId>
          <version>1.0version>
        dependency>
      dependencies>
      ...
    project>
    You’ll also need the following import in any Scala code that uses Mapper:
    Mapper Imports
    1
    import _root_.net.liftweb.mapper._

    8.1.2  Setting Up the Database Connection

    The first thing you need to do is to define the database connection. We do this by defining an object called DBVendor (but you can call it whatever you want). This object extends the net.liftweb.mapper.ConnectionManager trait and must implement two methods: newConnection and releaseConnection. You can make this as sophisticated as you want, with pooling, caching, etc., but for now, Listing 8.1.2↓ shows a basic implementation to set up a PostgreSQL driver.
    Setting Up the Database
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    .. standard Lift imports ...
    import _root_.net.liftweb.mapper._
    import _root_.java.sql._
    object DBVendor extends ConnectionManager {
      // Force load the driver
      Class.forName("org.postgresql.Driver")
      // define methods
      def newConnection(name : ConnectionIdentifier) = {
        try {
          Full(DriverManager.getConnection(
               "jdbc:postgresql://localhost/mydatabase",
               "root", "secret"))
        } catch {
          case e : Exception => e.printStackTrace; Empty
        }
      }
      def releaseConnection (conn : Connection) { conn.close }
    }
    class Boot {
      def boot {
        ...
        DB.defineConnectionManager(DefaultConnectionIdentifier, DBVendor)
      }
    }
    A few items to note:
    1. The name parameter for newConnection can be used if you need to have connections to multiple distinct databases. One specialized case of this is when you’re doing DB sharding (horizontal scaling). Multiple database usage is covered in more depth in Розділ 8.3.1↓
    2. The newConnection method needs to return a Box[java.sql.Connection]. Returning Empty indicates failure
    3. The releaseConnection method exists so that you have complete control over the lifecycle of the connection. For instance, if you were doing connection pooling yourself you would return the connection to the available pool rather than closing it
    4. The DB.defineConnectionManager call is what binds our manager into Mapper. Without it your manager will never get called

    8.1.3  Constructing a Mapper-enabled Class

    Now that we’ve covered some basic background, we can start constructing some Mapper classes to get more familiar with the framework. We’ll start with a simple example of a class for an expense transaction from our PocketChange application with the following fields:
    • Date
    • Description: a string with a max length of 100 chars
    • Amount: a decimal value with a precision of 16 digits and two decimal places
    • A reference to the Account that owns the transaction
    Given these requirements we can declare our Expense class as shown in Listing 8.1.3↓.
    Expense Class in Mapper
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import _root_.java.math.MathContext
    class Expense extends LongKeyedMapper[Expense] with IdPK {
      def getSingleton = Expense
      object dateOf extends MappedDateTime(this)
      object description extends MappedString(this,100)
      object amount extends MappedDecimal(this, MathContext.DECIMAL64, 2)
      object account extends MappedLongForeignKey(this, Account)
    }
    For comparison, the Record version is shown in Listing 8.1.3↓. This example already shows some functionality that hasn’t been ported over to Record from Mapper; among other things, the IdPK trait, and foreign key fields (many to one mappings) are missing. The other minor differences are that the getSingleton method has been renamed to meta, and the Field traits use different names under the Record framework (i.e. DateTimeField vs MappedDateTime).
    Entry Class in Record
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import _root_.java.math.MathContext
    import _root_.net.liftweb.record._
    class Expense extends KeyedRecord[Expense,Long] {
      def meta = Expense
      def primaryKey = id
      object id extends LongField(this) with KeyField[Long,Expense]
      object dateOf extends DateTimeField(this)
      object description extends StringField(this, 100)
      object amount extends DecimalField(this, MathContext.DECIMAL64, 2)
      object account extends LongField(this)
    }
    As you can see, we’ve set Expense to extend the LongKeyedMapper and IdPK traits and we’ve added the fields required by our class. We would like to provide a primary key for our entity; while not strictly necessary, having a synthetic primary key often helps with CRUD operations. The LongKeyedMapper trait accomplishes two objectives: it tells Lift that we want a primary key defined and that the key should be a long. This is basically a shortcut for using the KeyedMapper[Long,Expense] trait. When you use the KeyedMapper trait you need to provide an implementation for the primaryKeyField def, which must match the type of the KeyedMapper trait and be a subtype of IndexedField. The IdPK trait handles the implementation, but note that IdPK currently only supports Long keys. Mapper supports both indexed Longs and Strings, so if you want Strings you’ll need to explicitly use KeyedMapper[String,...] and provide the field definition yourself. It’s possible to use some other type for your primary key, but you’ll need to roll your own (Розділ 8.2.7↓). Technically Int indexes are supported as well, but there is no corresponding trait for an Int foreign key. That means that if you use an Int for the primary key, you may not be able to add a relationship to another object (Розділ 8.1.4↓), unless you write your own. Record is a little more flexible in primary key selection because it uses, in effect, a marker trait (KeyField) to indicate that a particular field is a key field. One thing to note is that in the Mapper framework, the table name for your entity defaults to the name of the class (Expense, in our case). If you want to change this, then you just need to override the dbTableName def in your MetaMapper object.
    Looking at these examples, you’ve probably noticed that the fields are defined as objects rather than instance members (vars). The basic reason for this is that the MetaMapper needs access to fields for its validation and form functionality; it is more difficult to cleanly define these properties in the MetaMapper if it had to access member vars on each instance since a MetaMapper instance is itself an object. Also note that MappedDecimal is a custom field type [C]   [C] The authors are working on adding this to the core library soon after Lift 1.0, which we’ll cover in Розділ 8.2.7↓.
    In order to tie all of this together, we need to define a matching LongKeyedMetaMapper object as the singleton for our entity, as shown in Listing 8.1.3↓. The Meta object (whether MetaMapper or MetaRecord) is where you define most behavior that is common across all of your instances. In our examples, we’ve decided to name the meta object and instance class the same. We don’t feel that this is unclear because the two together are what really define the ORM behavior for a “type.”
    EntryMeta object
    1
    2
    3
    object Expense extends Expense with LongKeyedMetaMapper[Expense] {
      override def fieldOrder = List(dateOf, description, amount)
    }
    In this instance, we’re simply defining the order of fields as they’ll be displayed in XHTML and forms by overriding the fieldOrder method. The default behavior is an empty list, which means no fields are involved in display or form generation. Generally, you will want to override fieldOrder because this is not very useful. If you don’t want a particular field to show up in forms or XHTML output, simply omit it from the fieldOrder list.
    Because fields aren’t actually instance members, operations on them are slightly different than with a regular var. The biggest difference is how we set fields: we use the apply method. In addition, field access can be chained so that you can set multiple field values in one statement, as shown in Listing 8.1.3↓:
    Setting Field Values
    1
    2
    myEntry.dateOf(new Date).description("A sample entry")
    myEntry.amount(BigDecimal("127.20"))
    The underlying value of a given field can be retrieved with the is method (the value method in Record) as shown in Listing 8.1.3↓.
    Accessing Field Values in Record
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // mapper
    val tenthOfAmount = myEntry.amount.is / 10
    val formatted = String.format("%s : %s",
                                  myEntry.description.is,
                                  myEntry.amount.is.toString)
    // record
    if (myEntry.description.value == "Doughnuts") {
      println("Diet ruined!")
    }

    8.1.4  Object Relationships

    Often it’s appropriate to have relationships between different entities. The archetypical example of this is the parent-child relationship. In SQL, a relationship can be defined with a foreign key that associates one table to another based on the primary key of the associated table. As we showed in Listing 8.1.3↑, there is a corresponding MappedForeignKey trait, with concrete implementations for Long and String foreign keys. Once we have this defined, accessing the object via the relationship is achieved by using the obj method on the foreign key field. Note that the obj method returns a Box, so you need to do some further processing with it before you can use it. With the foreign key functionality you can easily do one-to-many and many-to-one relationships (depending on where you put the foreign key). One-to-many relationships can be achieved using helper methods on the “one” side that delegate to queries. We’ll cover queries in a moment, but Listing 8.1.4↓ shows examples of two sides of the same relationship.
    Accessing Foreign Objects
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Expense extends LongKeyedMapper[Expense] with IdPK {
     ...
      object account extends MappedLongForeignKey(this, Account)
      def accountName =
        Text("My account is " + (account.obj.map(_.name.is) openOr "Unknown"))
    }
    class Account ... {
      ...
      def entries = Expense.findAll(By(Expense.account, this.id))
    }
    If you want to do many-to-many mappings you’ll need to provide your own “join” class with foreign keys to both of your mapped entities. An example would be if we wanted to have tags (categories) for our ledger entries and wanted to be able to have a given entry have multiple tags (e.g., you purchase a book for your mother’s birthday, so it has the tags Gift, Mom, and Books). First we define the Tag entity, as shown in Listing8.1.4↓ .
    Tag Entity
    1
    2
    3
    4
    5
    6
    7
    class Tag extends LongKeyedMapper[Tag] with IdPK {
      def getSingleton = Tag
      object name extends MappedString(this,100)
    }
    object Tag extends Tag with LongKeyedMetaMapper[Tag] {
      override def fieldOrder = List(name)
    }
    Next, we define our join entity, as shown in Listing 8.1.4↓. It’s a LongKeyedMapper just like the rest of the entities, but it only contains foreign key fields to the other entities.
    Join Entity
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class ExpenseTag extends LongKeyedMapper[ExpenseTag] with IdPK {
      def getSingleton = ExpenseTag
      object tag extends MappedLongForeignKey(this,Tag)
      object expense extends MappedLongForeignKey(this,Expense)
    }
    object ExpenseTag extends ExpenseTag with LongKeyedMetaMapper[ExpenseTag] {
      def join (tag : Tag, tx : Expense) =
        this.create.tag(tag).expense(tx).save
    }
    To use the join entity, you’ll need to create a new instance and set the appropriate foreign keys to point to the associated instances. As you can see, we’ve defined a convenience method on our Expense meta object to do just that. To make the many-to-many accessible as a field on our entities, we can use the HasManyThrough trait, as shown in Listing 8.1.4↓.
    HasManyThrough for Many-to-Many Relationships
    1
    2
    3
    4
    class Expense ... {
      object tags extends HasManyThrough(this, Tag,
        ExpenseTag, ExpenseTag.tag, ExpenseTag.expense)
    }
    A similar field could be set up on the Tag entity to point to entries. It’s important to note a few items:
    • The only way to add new entries is to directly construct the ExpenseTag instances and save them (either directly or via a helper method). You can’t make any modifications via the HasManyThrough trait
    • Although the field is defined as a query, the field is actually lazy and only runs once. That means if you query it and then add some new ExpenseTag instances, they won’t show up in the field contents
    If you want a way to retrieve the joined results such that it pulls fresh from the database each time, you can instead define a helper join method as shown in Розділ8.1.11 on page 1↓.

    8.1.5  Indexing

    It’s often helpful to add indexes to a database to improve performance. Mapper makes it easy to do most simple indexing simply by overriding the dbIndexed_? def on the field. Listing 8.1.5↓ shows how we would add an index to our Expense.account field.
    Indexing a Field
    1
    2
    3
    4
    5
    class Expense ... {
      object account extends ... {
        override def dbIndexed_? = true
      }
    }
    Mapper provides for more complex indexing via the MetaMapper.dbIndexes def combined with the Index, IndexField and BoundedIndexField case classes. Listing 8.1.5↓ shows some examples of how we might create more complex indices.
    More Complex Indices
    1
    2
    3
    4
    5
    6
    7
    object Expense extends ... {
      // equivalent to the previous listing
      override dbIndexes = Index(IndexField(account)) :: Nil
      // equivalent to "create index ... on transaction_t (account, description(10))"
      override dbIndexes = Index(IndexField(account),
                                 BoundedIndexField(description,10))
    }

    8.1.6  Schema Mapping

    The Mapper framework makes it easy not only to define domain objects, but also to create the database schema to go along with those objects. The Schemifier object is what does all of the work for you: you simply pass in the MetaMapper objects that you want the schema created for and it does the rest. Listing 8.1.6↓ shows how we could use Schemifier to set up the database for our example objects. The first argument controls whether an actual write will be performed on the database. If false, Schemifier will log all of the DDL statements that it would like to apply, but no changes will be made to the database. The second argument is a logging function (logging is covered in Appendix E↓). The remaining arguments are the MetaMapper objects that you would like to have schemified. You need to be careful to remember to include all of the objects, otherwise the tables won’t be created.
    Using Schemifier
    1
    Schemifier.schemify(true, Log.infoF _, User, Expense, Account, Tag, ExpenseTag)
    As we mentioned in Розділ 8.1.3↑, you can override the default table name for a given Mapper class via the dbTableName def in the corresponding MetaMapper. The default table name is the name of the Mapper class, except when the class name is also an SQL reserved word; in this case, a “_t” is appended to the table name. You can also override individual column names on a per-field basis by overriding the dbColumnName def in the field itself. Like tables, the default column name for a field will be the same as the field name as long as it’s not an SQL reserved word; in this case a “_c” is appended to the column name. Listing 8.1.6↓ shows how we could make our ExpenseTag.expense field map to “expense_id”.
    Setting a Custom Column Name
    1
    2
    3
    4
    5
    class ExpenseTag ... {
      object expense extends ... {
        override def dbColumnName = "expense_id"
      }
    }

    8.1.7  Persistence Operations on an Entity

    Now that we’ve defined our entity we probably want to use it in the real world to load and store data. There are several operations on MetaMapper that we can use :
    create  Creates a new instance of the entity
    save  Saves an instance to the database.
    delete  Deletes the given entity instance
    count  Returns the number of instances of the given entity. An optional query criteria list can be used to narrow the entities being counted
    countByInsecureSQL  Similar to count, except a raw SQL string can be used to perform the count. The count value is expected to be in the first column and row of the returned result set. An example would be
    • Expense.countByInsecureSQL(“select count(amount) “ +
        “from Expense where amount > 20”, ...)
      
      We’ll cover the IHaveValidatedThisSQL parameter in a moment.
    There are also quite a few methods available for retrieving instances from the database. Each of these methods comes in two varieties: one that uses the default database connection, and one that allows you to specify the connection to use (Розділ 8.3.1 on page 1↓). The latter typically has “DB” appended to the method name. The query methods on MetaMapper are:
    findAll  Retrieves a list of instances from the database. The method is overloaded to take an optional set of query criteria parameters; these will be covered in detail in their own section, 8.1.8↓.
    findAllByInsecureSQL  Retrieves a list of instances based on a raw SQL query. The query needs to return columns for all mapped fields. Usually you can use the BySQL QueryParameter to cover most of the same functionality.
    findAllByPreparedStatement  Similar to findAllByInsecureSQL except that prepared statements are used, which usually means that the driver will handle properly escaping arguments in the query string.
    findAllFields  This allows you to do a normal query returning only certain fields from your Mapper instance. For example, if you only wanted the amount from the transaction table you would use this method. Note that any fields that aren’t specified in the query will return their default value. Generally, this method is only useful for read access to data because saving any retrieved instances could overwrite real data.
    findMap*  These methods provide the same functionality as the non-Map methods, but take an extra function argument that transforms an entity into a Box[T], where T is an arbitrary type. An example would be getting a list of descriptions of our transactions:
    • Expense.findMap(entry => Full(entry.description.is))
      
    The KeyedMapperClass adds the find method, which can be used to locate a single entity based on its primary key. In general these operations will be supported in both Record and Mapper. However, because Record isn’t coupled tightly to a JDBC backend some of the find methods may not be supported directly and there may be additional methods not available in Mapper for persistence. For this reason, this section will deal specifically with Mapper’s persistence operations.

    Creating an Instance

    Once we have a MetaMapper object defined we can use it to create objects using the create method. You generally don’t want to use the “new” operator because the framework has to set up internal data for the instance such as field owner, etc. This is important to remember, since nothing will prevent you from creating an instance manually: you may just get errors when you go to use the instance. The join method in Listing 8.1.4↑ shows an example of create usage.

    Saving an Instance

    Saving an instance is as easy as calling the save method on the instance you want to save. Optionally, you can call the save method on the Meta object, passing in the instance you want to save. The save method uses the the saved_? and clean_? flags to determine whether an insert or update is required to persist the current state to the database, and returns a boolean to indicate whether the save was successful or not. The join method in Listing 8.1.4↑ shows an example of save usage.

    Deleting an Instance

    There are several ways to delete instances. The simplest way is to call the delete_! method on the instance you’d like to remove. An alternative is to call the delete_! method on the Meta object, passing in the instance to delete. In either case, the delete_! method returns a boolean indicating whether the delete was successful or not. Listing 3 on page 1↓ shows an example of deleting instances.
    Example Deletion
    1
    2
    3
    if (! myExpense.delete_!) S.error("Couldn’t delete the expense!")
    //or
    if (! (Expense delete_! myExpense)) S.error(...)
    Another approach to deleting entities is to use the bulkDelete_!! method on MetaMapper. This method allows you to specify query parameters to control which entities are deleted. We will cover query parameters in Розділ 8.1.8↓ (an example is in Listing 8.1.9 on page 1↓).

    8.1.8  Querying for Entities

    There are a variety of methods on MetaMapper for querying for instances of a given entity. The simplest method is findAll called with no parameters. The “bare” findAll returns a List of all of the instances of a given entity loaded from the database. Note that each findAll... method has a corresponding method that takes a database connection for sharding or multiple database usage (see sharding in Розділ 8.3.1↓). Of course, for all but the smallest datasets, pulling the entire model to get one entity from the database is inefficient and slow. Instead, the MetaMapper provides “flag” objects to control the query.
    The ability to use fine-grained queries to select data is a fundamental feature of relational databases, and Mapper provides first-class support for constructing queries in a manner that is not only easy to use, but type-safe. This means that you can catch query errors at compile time instead of runtime. The basis for this functionality is the QueryParam trait, which has several concrete implementations that are used to construct the actual query. The QueryParam implementations can be broken up into two main groups:
    1. Comparison - These are typically items that would go in the where clause of an SQL query. They are used to refine the set of instances that will be returned
    2. Control - These are items that control things like sort order and pagination of the results
    Although Mapper provides a large amount of the functionality in SQL, some features are not covered directly or at all. In some cases we can define helper methods to make querying easier, particularly for joins (Розділ 8.1.11↓).

    8.1.9  Comparison QueryParams

    The simplest QueryParam to refine your query is the By object and its related objects. By is used for a direct value comparison of a given field: essentially an “=” in SQL. For instance, Listing 8.1.9↓ shows how we can get all of the expenses for a given account.
    Retrieving by Account ID
    1
    val myEntries = Expense.findAll(By(Expense.account, myAccount.id))
    Note that our By criterion is comparing the Expense.account field to the primary key (id field) of our account instead of to the account instance itself. This is because the Expense.account field is a MappedForeignKey field, which uses the type of the key instead of the type of the entity as its underlying value. In this instance, that means that any queries using Expense.account need to use a Long to match the underlying type. Besides By, the other basic clauses are:
    • NotBy - Selects entities whose queried field is not equal to the given value
    • By_>- Selects entities whose queried field is larger than the given value
    • By_<- Selects entities whose queried field is less than the given value
    • ByList - Selects entities whose queried field is equal to one of the values in the given List. This corresponds to the “field IN (x,y,z)” syntax in SQL.
    • NullRef - Selects entities whose queried field is NULL
    • NotNullRef - Select entities whose queried field is not NULL
    • Like - Select entities whose queried field is like the given string. As in SQL, the percent sign is used as a wildcard
    In addition to the basic clauses there are some slightly more complex ways to control the query. The first of these is ByRef, which selects entities whose queried field is equal to the value of another query field on the same entity. A contrived example would be if we define a tree structure in our table and root nodes are marked as having themselves as parents:
    An Example of ByRef
    1
    2
    // select all root nodes from the forest
    TreeNode.findAll(ByRef(TreeNode.parent,TreeNode.id))
    The related NotByRef tests for inequality between two query fields.
    Getting slightly more complex, we come to the In QueryParameter, which is used just like an “IN” clause with a subselect in an SQL statement. For example, let’s say we wanted to get all of the entries that belong to tags that start with the letter “c”. Listing 8.1.9↓ shows the full breakdown.
    Using In
    1
    2
    3
    4
    5
    val cExpenses =
      ExpenseTag.findAll(
        In(ExpenseTag.tag,
           Tag.id,
           Like(Tag.name, "c%"))).map(_.expense.obj.open_!).removeDuplicates
    Note that we use the List.removeDuplicates method to make sure that the List contains unique entities. This requires overriding the equals and hashCode methods on the Expense class, which we show in Listing 8.1.9↓. In our example we’re using the primary key (id field) to define object “identity”.
    Overriding equals and hashcode on the Expense entity
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Expense ... {
      ...
      override def equals (other : Any) = other match {
        case e : Expense if e.id.is == this.id.is => true
        case _ => false
      }
      override def hashCode = this.id.is.hashCode
      ...
    }
    We use the ByRef params to do the join between the many-to-many entity on the query. Related to In is InRaw, which allows you to specify your own SQL subquery for the “IN” portion of the where clause. Listing 8.1.9↓ shows an example of how we could use InRaw to find Tags for expense entries made in the last 30 days.
    Using InRaw
    1
    2
    3
    4
    5
    6
    7
    def recentTags = {
      val joins = ExpenseTag.findAll(
        InRaw(ExpenseTag.expense,
              "select id from Expense where dateOf > (CURRENT_DATE - interval ’30 days’)",
              IHaveValidatedThisSQL("dchenbecker", "2008-12-03"))
      joins.map(_.expense.obj.open_!).removeDuplicates
    }
    Here things are starting to get a little hairy. The InRaw only allows us to specify the subquery for the IN clause, so we have to do some postprocessing to get unique results. If you want to do this in the query itself you’ll have to use the findAllByInsecureSql or findAllByPreparedStatement methods, which are covered later in this section on page number 1↓. The final parameter for InRaw, IHaveValidatedThisSQL acts as a code audit mechanism that says that someone has checked the SQL to make sure it’s safe to use. The query fragment is added to the master query as-is: no escaping or other filtering is performed on the string. That means that if you take user input. then you need to be very careful about it or you run the risk of an SQL injection attack on your site.
    The next QueryParam we’ll cover is BySql, which lets you use a complete SQL fragment that gets put into the where clause. An example of this would be if we want to find all expense entries within the last 30 days, as shown in Listing 8.1.9↓. Again, the IHaveValidatedThisSQL case class is required as a code audit mechanism to make sure someone has verified that the SQL used is safe.
    Using BySql
    1
    2
    3
    val recentEntries = Expense.findAll(
      BySql("dateOf > (CURRENT_DATE - interval ’30 days’)",
            IHaveValidatedThisSQL("dchenbecker","2008-12-03"))
    The tradeoff with using BySql is that you need to be careful with what you allow into the query string. BySql supports parameterized queries as shown in Listing 8.1.9↓, so use those if you need to have dynamic queries. Whatever you do, don’t use string concatenation unless you really know what you’re doing.
    Parameterized BySql
    1
    2
    val amountRange = Expense.findAll(
      BySql("amount between ? and ?", lowVal, highVal))
    As we mentioned in Розділ 3 on page 1↑, we can use the query parameters to do bulk deletes in addition to querying for instances. Simply use the QueryParam classes to constrain what you want to delete. Obviously, the control params that we’ll cover next make no sense in this context, but the compiler won’t complain. Listing 8.1.9↓ shows an example of deleting all entries older than a certain date.
    Bulk Deletion
    1
    2
    def deleteBefore (date : Date) =
      Expense.bulkDelete_!!(By_<(Expense.dateOf, date))

    8.1.10  Control QueryParams

    Now that we’ve covered the selection and comparison QueryParams, we can start to look at the control params. The first one that we’ll look at is OrderBy. This operates exactly like the order by clause in SQL, and allows you to sort on a given field in either ascending or descending order. Listing 8.1.10↓ shows an example of ordering our Expense entries by amount. The Ascending and Descending case objects are in the net.liftweb.mapper package. The OrderBySql case class operates similarly, except that you provide your own SQL fragment for the ordering, as shown in the example. Again, you need to validate this SQL.
    OrderBy Clause
    1
    2
    3
    4
    5
    6
    val cheapestFirst =
      Expense.findAll(OrderBy(Expense.amount,Ascending))
    // or
    val cheapestFirst =
      Expense.findAll(OrderBySql("amount asc"),
        IHaveValidatedThisSQL("dchenbecker", "2008-12-03"))
    Pagination of results is another feature that people often want to use, and Mapper provides a simple means for controlling it with two more QueryParam classes: StartAt and MaxRows, as shown in Listing 8.1.10↓. In this example, we take the offset from a parameter passed to our snippet, with a default of zero.
    Pagination of Results
    1
    2
    val offset = S.param("offset").map(_.toLong) openOr 0
    Expense.findAll(StartAt(offset), MaxRows(20))
    An important feature of the methods that take QueryParams is that they can take multiple params, as shown in this example. A more complex example is shown in Listing 8.1.10↓. In this example, we’re querying with a Like clause, sorting on the date of the entries, and paginating the results, all in one statement!
    Multiple QueryParams
    1
    2
    3
    4
    Expense.findAll(Like(Expense.description, "Gift for%"),
                    OrderBy(Expense.dateOf,Descending),
                    StartAt(offset),
                    MaxRows(pageSize))
    Another useful QueryParam is the Distinct case class, which acts exactly the same way as the DISTINCT keyword in SQL. One caveat is that Mapper doesn’t support explicit joins, so this restricts the situations in which you can use Distinct. The final “control” QueryParam that we’ll cover is PreCache. It’s used when you have a mapped foreign key field on an entity. Normally, when Mapper loads your main entity it leaves the foreign key field in a lazy state, so that the query to get the foreign object isn’t executed until you access the field. This can obviously be inefficient when you have many entities loaded that you need to access, so the PreCache parameter forces Mapper to preload the foreign objects as part of the query. Listing 8.1.10↓ shows how we can use PreCache to fetch an Expense entry as well as the account for the entry.
    Using PreCache
    1
    2
    3
    def loadExpensePlusAccount (id : Long) =
      Expense.findAll(By(Expense.id, id),
                        PreCache(Expense.account))

    8.1.11  Making Joins a Little Friendlier

    If you prefer to keep your queries type-safe, but you want a little more convenience in your joins between entities, you can define helper methods on your entities. One example is finding all of the tags for a given Expense, as shown in Listing 1↓. Using this method in our example has an advantage over using HasManyThrough: hasManyThrough is a lazy value that will only retrieve data from the database once per request. Using a findAll will retrieve data from the database every time. This may be important if you add data to the database during a request, or if you expect things to change between queries.
    Join Convenience Method
    1
    2
    def tags =
      ExpenseTag.findAll(By(ExpenseTag.expense, this.id)).map(_.tag.obj.open_!)

    8.2  Utility Functionality

    In addition to the first-class persistence support in Mapper and Record, the frameworks provide additional functionality to make writing data-driven applications much simpler. This includes things such as automatic XHTML representation of objects and support for generating everything from simple forms for an individual entity to a full-fledged CRUD  [D]   [D] An acronym (Create, Read, Update and Delete) representing the standard operations that are performed on database records. Taken from http://provost.uiowa.edu/maui/Glossary.html. implementation for your entities.

    8.2.1  Display Generation

    If you want to display a Mapper instance as XHTML, simply call the asHtml method (toXHtml in Record) on your instance. The default implementation turns each field’s value into a Text node via the toString method and concatenates the results separated by newlines. If you want to change this behavior, override the asHtml on your field definitions. For example, if we wanted to control formatting on our dateOf field, we could modify the field as shown in Listing 8.2.1↓.
    Custom Field Display
    1
    2
    3
    4
    5
    6
    7
    import _root_.java.text.DateFormat
    ...
    object dateOf extends MappedDateTime(this) {
      final val dateFormat =
        DateFormat.getDateInstance(DateFormat.SHORT)
      override def asHtml = Text(dateFormat.format(is))
    }
    Note that in Record, dateOf contains a java.util.Calendar instance and not a
    java.util.Date, so we would need to use the getTime method on the value. Two similar methods, asJSON and asJs, will return the JSON and JavaScript object representation of the instance, respectively.

    8.2.2  Form Generation

    One of the biggest pieces of functionality in the Mapper framework is the ability to generate entry forms for a given record. The toForm method on Mapper is overloaded so that you can control how your form is created. All three toForm methods on Mapper take a Box[String] as their first parameter to control the submit button; if the Box is Empty, no submit button is generated, otherwise, the String contents of the Box are used as the button label. If you opt to skip the submit button you’ll need to provide it yourself via binding or some other mechanism, or you can rely on implicit form submission (when the user hits enter in a text field, for instance). The first toForm method simply takes a function to process the submitted form and returns the XHTML as shown in Listing 8.2.2↓:
    Default toForm Method
    1
    myEntry.toForm(Full("Save"), { _.save })
    As you can see, this makes it very easy to generate a form for editing an entity. The second toForm method allows you to provide a URL which the Mapper will redirect to if validation succeeds on form submission (this is not provided in Record). This can be used for something like a login form, as shown in Listing 8.2.2↓:
    Custom Submit Button
    1
    myEntry.toForm (Full("Login"), "/member/profile")
    The third form of the toForm method is similar to the first form, with the addition of “redo” snippet parameter. This allows you to keep the current state of the snippet when validation fails so that the user doesn’t have to re-enter all of the data in the form.
    The Record framework allows for a little more flexibility in controlling form output. The MetaRecord object allows you to change the default template that the form uses by setting the formTemplate var. The template may contain any XHTML you want, but the toForm method will provide special handling for the following tags:
      The label for the field with the given name will be rendered here.
      The field itself (specified by the given name) will be rendered here. Typically this will be an input field, although it can be anything type-appropriate. For example, a BooleanField would render a checkbox.
      Any messages, such as from validation, for the field with the given name will be rendered here.
    As an example, if we wanted to use tables to lay out the form for our ledger entry, the row for the description field might look like that in Listing 8.2.2↓:
    Custom Form Template
    1
    2
    3
    4
    5
    6
    <tr>
      <th><lift:field_label name="description" />th>
      <td><lift:field name="description" />
          <lift:field_msg name="description" />td>
    tr>
    Technically, the field_msg binding looks up Lift messages (Глава B↓) based on the field’s uniqueId, so you can set your own messages outside of validation using the S.{error, notice, warning} methods as shown in Listing 8.2.2↓:
    Setting Messages via S
    1
    2
    3
    S.warning(myEntry.amount.uniqueFieldId,
              "You have entered a negative amount!")
    S.warning("amount_id", "This is brittle")
    For most purposes, though, using the validation mechanism discussed in the next section is the appropriate way to handle error checking and reporting.

    8.2.3  Validation

    Validation is the process of checking a field during form processing to make sure that the submitted value meets requirements. This can be something as simple as ensuring that a value was submitted, or as complex as comparing multiple field values together. Validation is achieved via a List of functions on a field that take the field value as input and return a List[FieldError] (Box[Node] in Record). To indicate that validation succeeded, simply return an empty List, otherwise the list of FieldErrors you return are used as the failure messages to be presented to the user. A FieldError is simply a case class that associates an error message with a particular field. As an example, let’s say we don’t want someone to be able to add an Expense entry for a date in the future. First, we need to define a function for our dateOf field that takes a Date as an input (For Record, java.util.Calendar, not Date, is the actual value type of DateTimeField) and returns the proper List. We show a simple function in Listing 8.2.3↓. In the method, we simply check to see if the millisecond count is greater than “now” and return an error message if so.
    Date Validation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import _root_.java.util.Date
    class Expense extends LongKeyedMapper[Expense] with IdPK {
      ...
      object dateOf extends MappedDateTime(this) {
        def noFutureDates (time : Date) = {
          if (time.getTime > System.currentTimeMillis) {
            List(FieldError(this, "You cannot make future expense entries"))
          } else {
            List[FieldError]()
          }
        }
      }
      ...
    }
    The first argument for the FieldError is the field itself, so you could use the alternate definition shown in Listing 8.2.3↓ if you would prefer to define your validation functions elsewhere (if they’re common to more than one entity, for example).
    Alternate Date Validation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import _root_.java.util.Date
    import _root_.net.liftweb.http.FieldIdentifier
    object ValidationMethods {
      def noFutureDates (field : FieldIdentifier)(time : Date) = {
        if (time.getTime > System.currentTimeMillis) {
          List(FieldError(field, "You cannot make future expense entries"))
        } else {
          List[FieldError]()
        }
      }
      ...
    }
    The next step is to tie the validation into the field itself. We do this by slightly modifying our field definition for date to set our list of validators as shown in Listing 8.2.3↓:
    Setting Validators
    object dateOf extends MappedDateTime(this) {
    1
    2
    3
    4
    5
    6
    7
    8
      def noFutureDates (time : Date) = { ... }
      override def validations = noFutureDates _ :: Nil
    }
    // Using the alternate definition:
    object dateOf extends MappedDateTime(this) {
      override def validations = ValidationMethods.noFutureDates(dateOf) _ :: Nil
    }
    Note that we need to add the underscore for each validation function to be partially applied on the submitted value. When our form is submitted, all of the validators for each field are run, and if all of them return Empty then validation succeeds. If any validators return a Full Box, then the contents of the Box are displayed as error messages to the user.

    8.2.4  CRUD Support

    Adding CRUD support to your Mapper classes is very simple. We just mix in the
    net.liftweb.mapper.CRUDify
    trait to our meta object and it provides a full set of add, edit, list, delete and view pages automatically. Listing 8.2.4↓ shows our Expense meta object with CRUDify mixed in.
    Mixing in CRUDify
    1
    2
    3
    4
    5
    6
    object Expense extends Expense LongKeyedMetaMapper[Expense]
        with CRUDify[Long,Expense] {
      ... normal def here ...
      // disable delete functionality
      override def deleteMenuLoc = Empty
    }
    The CRUDify behavior is very flexible, and you can control the templates for pages or whether pages are shown at all (as we do in our example) by overriding defs that are provided on the CRUDify trait. In our example Listing 8.2.4↑, we disable the delete menu by overriding the
    deleteMenuLoc method to return Empty. As an added bonus, CRUDify automatically creates a set of menus for SiteMap (Глава 7↑) that we can use by appending them onto the rest of our menus as shown in Listing 8.2.4↓.
    Using CRUDify Menus
    1
    2
    3
    4
    5
    6
    7
    class Boot {
      def boot {
        ...
        val menus = ... Menu(Loc(...)) :: Expense.menus
        LiftRules.setSiteMap(SiteMap(menus : _*))
      }
    }

    8.2.5  Lifecycle Callbacks

    Mapper and Record provide for a set of callbacks that allow you to perform actions at various points during the lifecycle of a given instance. If you want to define your own handling for one of the lifecycle events, all you need to do is override and define the callback because MetaMapper already extends the LifecycleCallbacks trait. Note that there is a separate LifecycleCallbacks trait in each of the record and mapper packages, so make sure that you import the correct one. For example, if we want to notify a Comet actor whenever a new Expense entry is saved, we can change our Expense class as shown in Listing 8.2.5↓:
    Lifecycle Callbacks
    1
    2
    3
    4
    object Expense extends LongKeyedMapper[Expense] with LifecycleCallbacks {
      ...
      override def afterSave { myCometActor ! this }
    }
    The lifecycle hooks are executed at the main operations in an instance lifecycle:
    Create  When a fresh instance is first saved (corresponding to a table insert).
    Delete  When an instance is deleted.
    Save  When a new or existing instance is inserted or updated. beforeSave is always called before beforeCreate or beforeUpdate. Similarly, afterSave is always called after afterCreate or afterUpdate.
    Update  When an instance that already exists in the database is updated (corresponding to a table update).
    Validation  When form validation occurs.
    For each of these points you can execute your code before or after the operation is run.

    8.2.6  Base Field Types

    The Record and Mapper frameworks define several basic field types. The following table shows the corresponding types between Mapper and Record, as well as a brief description of each type.
    MappedBinary BinaryField Represents a byte array. You must provide your own overrides for toForm and asXHtml/asHtml for input and display
    MappedBirthYear N/A Holds an Int that represents a birth year. The constructor takes a minAge parameter that is used for validation
    MappedBoolean BooleanField Represents a Boolean value. The default form representation is a checkbox
    MappedCountry CountryField Represents a choice from an enumeration of country phone codes as provided by the net.liftweb.mapper.Countries.I18NCountry class. The default form representation is a select
    MappedDateTime DateTimeField Represents a timestamp (java.util.Calender for Record, java.util.Date for Mapper). The default form representation is a text input
    MappedDouble DoubleField Represents a Double value
    MappedEmail EmailField Represents an email address with a maximum length
    MappedEnum EnumField Represents a choice from a given scala.Enumeration. The default form representation is a select
    MappedEnumList N/A Represents a choice of multiple Enumerations. The default form representation is a set of checkboxes, one for each enum value
    MappedFakeClob N/A Fakes a CLOB value (really stores String bytes to a BINARY column)
    MappedGender N/A Represents a Gender enumeration. Display values are localized via the I18NGenders object. Internationalization is covered in appendix D↓
    MappedInt IntField Represents an Int value
    MappedIntIndex N/A Represents an indexed Int field (typically a primary key). In Record this is achieved with the KeyField trait
    MappedLocale LocaleField Represents a locale as selected from the java.util.Locale.getAvailableLocales method. The default form representation is a select
    MappedLong LongField Represents a Long value
    MappedLongForeignKey N/A Represents a mapping to another entity via the other entities Long primary key. This functionality in Record is not yet supported
    MappedLongIndex N/A Represents an indexed Long field (typically a primary key). In Record this is achieved with the KeyField trait
    MappedPassword PasswordField Represents a password string. The default form representation is a password input (obscured text)
    MappedPoliteString N/A Just like MappedString, but the default value is an empty string and the input is automatically truncated to fit the database column size
    MappedPostalCode PostalCodeField Represents a validated postal code string. The field takes a reference to a MappedCountry (CountryField in Record) at definition and validates the input string against the selected country’s postal code format
    MappedString StringField Represents a string value with a maximum length and optional default value
    MappedStringForeignKey N/A Represents a mapping to another entity via the other entities String primary key. This functionality in Record is not yet supported
    MappedStringIndex N/A Represents an indexed String field (typically a primary key). In Record this is achieved with the KeyField trait
    MappedText N/A Represents a String field that stores to a CLOB column in the database. This can be used for large volumes of text.
    MappedTextarea TextAreaField Represents a String field that will use an HTML textarea element for its form display. When you define the field you can override the textareaCols and textareaRows defs to control the dimensions of the textarea.
    MappedTimeZone TimeZoneField Represents a time zone selected from java.util.TimeZone.getAvailableIDs. The default form representation is a select
    MappedUniqueId N/A Represents a unique string of a specified length that is randomly generated. The implementation doesn’t allow the user to write new values to the field. This can be thought of as a GUID

    8.2.7  Defining Custom Field Types in Mapper

    The basic MappedField types cover a wide range of needs, but sometimes you may find yourself wanting to use a specific type. In our example, we would like a decimal value for our expense amount and account balance. Using a double would be inappropriate due to imprecision and rounding errors  [E]   [E] http://stephan.reposita.org/archives/2008/01/11/once-and-for-all-do-not-use-double-for-money/, so instead we base it on scala.BigDecimal. We’re going to provide an abridged version of the code that will end up in the Lift library. Feel free to examine the source to see the constructors and methods that we’ve omitted  [F]   [F] The code is checked into the master branch of the liftweb Git repository.. Our first task is to specify the class signature and constructors, as shown in Listing 8.2.7↓. Note that the BigDecimal we’re using here is scala.BigDecimal, not java.math.BigDecimal. We’ll cover how we make this work with JDBC (which doesn’t support scala.BigDecimal) in a moment.
    MappedDecimal Constructors
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import _root_.java.math.{MathContext, RoundingMode}
    class MappedDecimal[T <: Mapper[T]] (val fieldOwner : T,
                                         val context : MathContext,
                                         val scale : Int) extends MappedField[BigDecimal,T] {
      // ... constructor taking initial value ...
      def this(fieldOwner : T, value : BigDecimal, context: MathContext) = {
        this(fieldOwner, context, value.scale)
        setAll(value) // we’ll cover this later in this section
      }
      def this(fieldOwner : T, value : BigDecimal) = {
        this(fieldOwner, MathContext.UNLIMITED, value.scale)
        setAll(value)
      }
    The first part of the class definition is the type signature; basically the type [T <: MappedField[T]] indicates that whatever type “owns” this field must be a Mapper subclass (<: specifies an upper type bound [G]  [G] For more on type bounds, see http://www.scala-lang.org/node/136.). With our primary constructor we specify the owner mapper as well as the MathContext (this controls rounding and precision, or the total number of digits) and scale of the decimal value. The scale in BigDecimal essentially represents the number of digits to the right of the decimal point. In addition, we specify ancillary constructors to take an initial value with or without and explicit MathContext.
    Now that we have the constructors in place, there are several abstract methods on MappedField that we need to define. The first of these is a method to provide a default value. The default value is used for uninitialized fields or if validation fails. We also need to specify the class for our value type by implementing the dbFieldClass method. Listing 8.2.7↓ shows both of these methods. In our case, we default to a zero value, with the scale set as specified in the contructor. Note that BigDecimal instances are generally immutable, so the setScale method returns a new instance. We also provide the vars and methods that handle the before and after values of the field. These values are used to handle persistence state. If you change the value of the field, then the original value is held until the instance is saved to the database. The st method is used internally to set the value of the field when instances are “rehydrated” from the database.
    Setting a Default Value
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
      private val zero = BigDecimal("0")
      def defaultValue = zero.setScale(scale)
      def dbFieldClass = classOf[BigDecimal]
      // The data and orgData variables are used so that
      // we know when the field has been modified by the user
      private var data : BigDecimal = defaultValue
      private var orgData : BigDecimal = defaultValue
      private def st (in : BigDecimal) {
        data = in
        orgData = in
      }
       
      // The i_is_! and i_was_! methods are used internally to
      // keep track of when the field value is changed. In our
      // instance they delegate directly to the data and orgData
      // variables
      protected def i_is_! = data
      protected def i_was_! = orgData
      override def doneWithSave() {
        orgData = data
      }
    The next set of methods we need to provide deal with when and how we can access the data. Listing 8.2.7↓ shows the overrides that set the read and write permissions to true (default to false for both) as well as the i_obscure_! and real_i_set_! methods. The i_obscure_! method returns the a value that is used when the user doesn’t have read permissions. The real_i_set_! method is what actually stores the internal value and sets the dirty flag when the field is updated.
    Access Control
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    override def readPermission_? = true
    override def writePermission_? = true
    protected def i_obscure_!(in : BigDecimal) = defaultValue
    protected def real_i_set_!(value : BigDecimal): BigDecimal = {
      if (value != data) {
        data = value
        dirty_?(true)
      }
      data
    }
    The next two methods that we need to provide deal with actually setting the value of the field. The first is setFromAny, which takes an Any parameter and must convert it into a BigDecimal. The second, setFromString is a subset of setFromAny in that it takes a String parameter and must return a BigDecimal. Our implementation of these two methods is shown in Listing 8.2.7↓. We’ve also added a setAll and coerce method so that we have a common place to properly set scale and rounding modes on the value of the field.
    setFrom... Methods
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
      def setFromAny (in : Any) : BigDecimal =
        in match {
          case bd : BigDecimal => setAll(bd)
          case n :: _ => setFromString(n.toString)
          case Some(n) => setFromString(n.toString)
          case Full(n) => setFromString(n.toString)
          case None | Empty | Failure(_, _, _) | null => setFromString("0")
          case n => setFromString(n.toString)
        }
      def setFromString (in : String) : BigDecimal = {
        this.setAll(BigDecimal(in))
      }
      protected def setAll (in : BigDecimal) = set(coerce(in))
      // Make a separate method for properly adjusting scale and rounding.
      // We’ll use this method later in the class as well.
      protected coerce (in : BigDecimal) =
        new BigDecimal(in.bigDecimal.setScale(scale, context.getRoundingMode))
    Our implementations are relatively straightforward. The only special handling we need for setFromAny is to properly deal with Lists, Boxes, Options and the null value. The BigDecimal constructor takes Strings, so the setFromString method is easy. The only addition we make over the BigDecimal constructor is to properly set the scale and rounding on the returned value.
    Our final step is to define the database-specific methods for our field, as shown in Listing 8.2.7↓. The first method we implement is targetSQLType. This method tells Mapper what the corresponding SQL type is for our database column. The jdbcFriendly method returns a value that can be used in a JDBC statement. Here’s where we need to use the bigDecimal val on our scala.BigDecimal to obtain the real java.math.BigDecimal instance. Similarly, the real_convertToJDBCFriendly method needs to return a java BigDecimal for a given scala.BigDecimal input. The buildSet... methods return functions that can be used to set the value of our field based on different input types. These are essentially conversion functions that are used by Lift to convert data retrieved in a ResultSet into actual field values. Finally, the fieldCreatorString specifices what we would need in a CREATE TABLE statement to define this column. In this instance, we need to take into account the precision and scale. We use default precision if we’re set to unlimited, but it’s important to understand that actual precision for the default DECIMAL type varies between RDBMS vendors.
    Database-Specific Methods
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    def targetSQLType = Types.DECIMAL
    def jdbcFriendly(field : String) = i_is_!.bigDecimal
    def real_convertToJDBCFriendly(value: BigDecimal): Object = value.bigDecimal
    // The following methods are used internally by Lift to
    // process values retrieved from the database.
    // We don’t convert from Boolean values to a BigDecimal, so this returns null
    def buildSetBooleanValue(accessor : Method, columnName : String) :
      (T, Boolean, Boolean) => Unit = null
    // Convert from a Date to a BigDecimal. Our assumption here is that we can take
    // The milliseconds value of the Date.
    def buildSetDateValue(accessor : Method, columnName : String) :
        (T, Date) => Unit =
      (inst, v) =>
        doField(inst, accessor,{
          case f: MappedDecimal[T] =>
            f.st(if (v == null) defaultValue else coerce(BigDecimal(v.getTime)))
        })
    // Convert from a String to a BigDecimal. Since the BigDecimal object can
    // directly convert a String, we just pass the String directly.
    def buildSetStringValue(accessor: Method, columnName: String) :
        (T, String) => Unit =
      (inst, v) =>
        doField(inst, accessor,{
          case f: MappedDecimal[T] =>
            f.st(coerce(BigDecimal(v)))
        })
    // Convert from a Long to a BigDecimal. This is slightly more complex than
    // for a String, since we need to check for null values.
    def buildSetLongValue(accessor: Method, columnName : String) :
        (T, Long, Boolean) => Unit =
      (inst, v, isNull) =>
        doField(inst, accessor, {
          case f: MappedDecimal[T] =>
            f.st(if (isNull) defaultValue else coerce(BigDecimal(v)))
        })
    // Convert from an AnyRef (Object). We simply use the String value
    // of the input here.
    def buildSetActualValue(accessor: Method, data: AnyRef, columnName: String) :
        (T, AnyRef) => Unit =
      (inst, v) =>
        doField(inst, accessor, {
          case f: MappedDecimal[T] => f.st(coerce(BigDecimal(v.toString)))
        })
    def fieldCreatorString(dbType: DriverType, colName: String): String = {
      val suffix = if (context.getPrecision == 0) "" else {
        "(" + context.getPrecision + "," + scale + ")"
      }
      colName + " DECIMAL" + suffix
    }

    8.2.8  ProtoUser and MegaProtoUser

    In addition to all of the database-related features, Mapper contains an extra goody to help you quickly set up small sites. ProtoUser and MegaProtoUser are two built-in traits that define a simple user account. The ProtoUser trait defines some basic fields for a user: email, firstName, lastName, password and superUser (a boolean to provide basic permissions). There are also a number of defs used to format the fields for display or to provide form labels. Listing 8.2.8↓ shows an example of a ProtoUser-based Mapper class that overrides some of the formatting defs.
    A Simple ProtoUser
    1
    2
    3
    4
    class User extends ProtoUser[User] {
      override def shortName = firstName.is
      override lastNameDisplayName = "surname"
    }
    The MegaProtoUser trait, as its name implies, extends the ProtoUser trait with a whole suite of functionality. The main thrust of MegaProtoUser (and its associated meta object,
    MetaMegaProtoUser) is to automatically handle all of the scaffolding for a complete user management system, with:
    • A user registration page with configurable validation via email
    • A login page that automatically handles authentication
    • A lost password page that does reset via email
    • A change password page
    • A user edit page
    • A simple method to generate SiteMap menus for all of these pages
    Of course, you can customize any of these by overriding the associated methods on the MetaMegaProtoUser object. Listing 2.1 on page 1↑ shows an example of sprucing up the signup and login pages by overriding the loginXHtml and signupXHtml methods. Listing 8.2.8↓ shows how easy it is to then hook the MetaMegaProtoUser menus into SiteMap.
    Hooking MetaMegaProtoUser into Boot
    1
    2
    // in Boot.scala
    LiftRules.setSiteMap(SiteMap((... :: User.sitemap) :_*))

    8.3  Advanced Features

    In this section we’ll cover some of the advanced features of Mapper

    8.3.1  Using Multiple Databases

    It’s common for an application to need to access data in more than one database. Lift supports this feature through the use of overrides on your MetaMapper classes. First, we need to define the identifiers for the various databases using the ConnectionIdentifier trait and overriding the jndiName def. Lift comes with one pre-made: DefaultConnectionIdentifier. It’s jndiName is set to “lift”, so it’s recommended that you use something else. Let’s say we have two databases: sales and employees. Listing 8.3.1↓ shows how we would define the ConnectionIdentifier objects for these.
    Defining Connection Identifiers
    1
    2
    3
    4
    5
    6
    7
    object SalesDB extends ConnectionIdentifier {
      def jndiName = "sales"
    }
    object EmployeeDB extends ConnectionIdentifier {
      def jndiName = "employees"
    }
    Simple enough. Now, we need to create connection managers for each one, or we can combine the functionality into a single manager. To keep things clean we’ll use a single manager, as shown in Listing 8.3.1↓. Scala’s match operator allows us to easily return the correct connection.
    Multi-database Connection Manager
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    object DBVendor extends ConnectionManager {
      Class.forName("org.postgresql.Driver")
      
      def newConnection(name : ConnectionIdentifier) = {
        try {
          name match {
            case SalesDB =>
              Full(DriverManager.getConnection(
                "jdbc:postgresql://localhost/sales",
                "root", "secret"))
            case EmployeeDB =>
              Full(DriverManager.getConnection(
                "jdbc:postgresql://server/employees",
                "root", "hidden"))
        } catch {
          case e : Exception => e.printStackTrace; Empty
        }
      }
      def releaseConnection (conn : Connection) { conn.close }
    }
    Now that we’ve defined our connection identifiers, we need to be able to use them in our Mapper instances. There are several ways to do this. The first (simplest) way is to override the dbDefaultConnectionIdentifier method on your MetaMapper object, as shown in Listing 8.3.1↓. In this example we’re setting the MetaMapper to always use the EmployeeDB connection for all persistence operations.
    Defining the Default Connection Identifier
    1
    2
    3
    4
    5
    object EmployeeMeta extends Employee with LongKeyedMetaMapper[Employee] {
      ...
      override def dbDefaultConnectionIdentifier = EmployeeDB
      ...
    }
    The second way to utilize more than one DB is to use the “DB” version of the persistence methods, as we mentioned in Розділ 8.1.7↑. Listing 8.3.1↓ shows how we can perform a findAll with a specific connection.
    Using a Connection Identifier Directly
    1
    val employees = EmployeeMeta.findAllDb(EmployeeDB)

    8.3.2  Database Sharding

    A special case of using multiple databases is sharding [H]   [H] For more information on sharding, see this article: http://highscalability.com/unorthodox-approach-database-design-coming-shard. Sharding is a means to scale your database capacity by associating entities with one database instance out of a federation of servers based on some property of the entity. For instance, we could distribute user entites across 3 database servers by using the first character of the last name: A-H goes to server 1, I-P goes to server 2, and Q-Z goes to server 3. As simple as this sounds, there are some important factors to remember:
    • Sharding increases the complexity of your code.
    • To get the most benefit out of sharding, you need to carefully choose and tune your “selector.” If you’re not careful, you can get an uneven distribution where some servers handle significantly more load than others, defeating the purpose of sharding. The example we’ve given here of using the last name is, in practice, a very poor choice. We recommend reading http://startuplessonslearned.blogspot.com/2009/01/sharding-for-startups.html for a good overview of the pros and cons of various selector strategies.
    • When you use sharding, you can’t just use normal joins anymore because the data isn’t all within one instance. This means more work on your part to properly retrieve and associate data
    Mapper provides a handy feature for sharding that allows you to choose which database connection you want to use for a specific entity. There are two methods we can use to control the behavior: dbSelectDBConnectionForFind and dbCalculateConnectionIdentifier. dbSelect... is used to find an instance by primary key, and takes a partial function (typically a match clause) to determine which connection to use. dbCalculate... is used when a new instance is created to decide where to store the new instance. As an example, say we’ve defined two database connections, SalesA and SalesB. We want to place new instances in SalesA if the amount is > $100 and SalesB otherwise. Listing 8.3.2↓ shows our method in action.
    Sharding in Action
    1
    2
    3
    4
    5
    6
    7
    8
    class Expense extends LongKeyedMapper[Expense] {
      ... fields, etc ...
      override def dbCalculateConnectionIdentifier = {
        case n if n.amount.is > 100 => SalesA
        case _ => SalesB
      }
    }

    8.3.3  SQL-based Queries

    If, despite all that Mapper covers, you find yourself still wanting more control over the query, there are two more options available to you: findAllByPreparedStatement and findAllByInsecureSql. The findAllByPreparedStatement method allows you to, in essence, construct your query completely by hand. The added benefit of using a PreparedStatement [I]   [I] http://java.sun.com/javase/6/docs/api/java/sql/PreparedStatement.html means that you can easily include user-defined data in your queries. The findAllByPreparedStatement method takes a single function parameter. This function takes a SuperConnection  [J]   [J] Essentially a thin wrapper on java.sql.Connection, http://scala-tools.org/mvnsites/liftweb/lift-webkit/scaladocs/net/liftweb/mapper/SuperConnection.html and returns a
    PreparedStatement instance. Listing 8.3.3↓ shows our previous example in which we looked up all Tags for recent Expense entries, but here using findAllByPreparedStatement instead. The query that you provide must at least return the fields that are mapped by your entity, but you can return other columns as well (they’ll just be ignored), so you may choose to do a “select *” if you prefer.
    Using findAllByPreparedStatement
    1
    2
    3
    4
    5
    6
    7
    8
    def recentTags = Tag.findAllByPreparedStatement({ superconn =>
      superconn.connection.prepareStatement(
        "select distinct Expense.id, Tag.name" +
        "from Tag" +
        "join ExpenseTag et on Tag.id = et.tag " +
        "join Expense ex on ex.id = et.expense " +
        "where ex.dateOf > (CURRENT_DATE - interval ’30 days’)")
    })
    The findAllByInsecureSql method goes even further, executing the string you submit directly as a statement without any checks. The same general rules apply as for
    findAllByPreparedStatement, although you need to add the IHaveValidatedThisSQL parameter as a code audit check. In either case, the ability to use full SQL queries can allow you to do some very powerful things, but it comes at the cost of losing type safety and possibly making your app non-portable.
    As a last resort, Mapper provides support for non-entity SQL queries through a few methods on the DB object. The first method we’ll look at is DB.runQuery. This method allows you to provide a full SQL query string, and is overloaded to take a parameterized query. It returns a Pair[List[String],List[List[String]], with the first List[String] containing all of the column names and the second List corresponding to each row in the result set. For example, let’s say we wanted to compute the sums of each tag for a given account. Listing 8.3.3↓ shows how we could accomplish this using a parameterized query against the database.
    Using DB.runQuery
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    DB.runQuery("select Tag.name, sum(amount) from Expense ex " +
                "join ExpenseTag et on et.expense = ex.id " +
                "join Tag on et.tag = Tag.id " +
                "join Account on Account.id = ex.account " +
                "where Account.id = ? group by Tag.name order by Tag.name",
                myAccount.id)
    // might return:
    (List("tag", "sum"]),
     List(List("food","42.00"),
          List("home","75.49"),
          List("work","2.00")))
    If you need full control over the query and full access to the result set, DB provides some low-level utility methods. The most basic is DB.use, which takes a connection identifier as well as a function that takes a SuperConnection (a thin wrapper on JDBC’s connection). This forms a loan pattern  [K]   [K] http://scala.sygneca.com/patterns/loan that lets Mapper deal with all of the connection open and release details. The DB.exec method takes a provided connection and executes an arbitrary SQL statement on it, then applies a provided function to the result set. Similarly, the DB.prepareStatement method allows you to create a prepared statement and then apply a function to it. You can combine these methods to run any arbitrary SQL, as shown in Listing 8.3.3↓.
    Using DB.use
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // recompute an account balance from all of the transactions
    DB.use(DefaultConnectionIdentifier) { conn =>
      val balance =
        // Should use a prepared statement here. This is for example only
        DB.exec(conn,
          "select sum(ex.amount) from Expense ex where ex.account = "
          + myAccount.id) {
          rs =>
           if (!rs.next) BigDecimal(0)
           else (new BigDecimal(rs.getBigDecimal(1)))
        }
      DB.prepareStatement("update Account set balance = ? where Account.id = ",
                          conn) { stmt =>
        stmt.setBigDecimal(1, balance.bigDecimal)
        stmt.setLong(2, resetAccount.id)
        stmt.executeUpdate()
      }
    }

    8.4  Logging

    Logging with Mapper is covered in detail in Розділ E.4 on page 1↓.

    8.5  Summary

    In this chapter, we discussed the two major ORMs included in Lift: Mapper and Record. We’ve shown how you can define entities using the Mapper field types and how to coordinate between the entity and its Meta-object. We’ve shown how you can customize the display and schema of your behavior with custom form control, CRUD support, and indexing. And we’ve show you how to query for entities using Mapper’s type-safe query support. Finally, we showed you how you can do in-depth customization of Mapper behavior by writing your own field types, using multiple databases, and using raw SQL queries.

    Part II.  Advanced Topics

    9  Advanced Lift Architecture

    This chapter is still under active development. The contents will change.
    Congratulations! You’ve either made it through the introduction to Lift, or maybe you’ve just skipped Basics and jumped right to here to Advanced; either way, the next group of chapters will be exciting.
    In this chapter we’re going to dive into some of the advanced guts of Lift so that you have a thorough understanding of what’s going on before we explore further.

    9.1  Architectural Overview

    Before we jump into the specific details of the architecture, let’s refresh our memories. Малюнок 9.1↓ highlights the main Lift components and where they live in the ecosystem. Scala compiles down to Java bytecode, so we sit on top of the JVM. Lift Applications are typically run in a J(2)EE web container, such as Jetty or Tomcat. As we explained in section 3.1↑, Lift is set up to act as a Filter [L]   [L] http://java.sun.com/j2ee/1.4/docs/api/javax/servlet/Filter.html that acts as the entry point. Usage of the rest of the framework varies from application to application, depending on how simple or complex you make it.
    Малюнок 9.1 Architecture
    figure images/LiftArchDiagram.png
    The major components outlined in the diagram are:
    LiftCore  The engine of the framework responsible for request/response lifecycle, rendering pipeline, invoking user’s functions etc. We don’t directly cover the core in this book since essentially all of the functionality that we do cover sits on top of the core
    SiteMap  Contains the web pages for a Lift application (chapter7↑)
    LiftRules  Allows you to configure Lift. We cover this in various sections throughout the book
    LiftSession  The session state representation (section 9.5↓)
    S The stateful object impersonating the state context for a given request/response lifecycle (section 9.7↓)
    SHtml  Contains helper functions for XHtml artifacts (chapters 6↑ and 11↓)
    Views  LiftView objects impersonating a view as a XML content. Thus pages can be composed from other sources not only from html files. (section 4.4↑)
    LiftResponse  Represents the abstraction of a response that will be propagated to the client. (section 9.4↓)
    Comet  Represents the Comet Actors layer which allows the sending of asynchronous content to the browser (section 11.5↓)
    ORM - Either Mapper or Record - The lightweight ORM library provided by Lift. The Mapper framework is the proposed ORM framework for Lift 1.0 and the Record framework will be out for next releases. (chapter 8↑)
    HTTP Auth  - You can use either Basic or Digest HTTP authentication in your Lift application. This provides you more control as opposed to web-container’s HTTP authentication model. (section 9.9↓)
    JS API  The JavaScript abstraction layer. These are Scala classes/objects that abstract JavaScript artifacts. Such objects can be combined to build JavaScript code (chapter 10↓)
    Utils  Contains a number of helper functions that Lift uses internally and are available to your application

    9.2 The Request/Response Lifecycle

    We briefly discussed the Request/Response Liftcycle in section 3.5↑, and now we’re going to cover it in depth. This will serve not only to familiarize you with the full processing power of Lift, but also to introduce some of the other advanced topics we’ll be discussing in this and later chapters.
    One important thing we’d like to mention is that most of the configurable properties are in LiftRules, and are of type RulesSeq. With a RulesSeq you essentially have a list of functions or values that are applied in order. RulesSeq defines a prepend and append method that allows you to add new configuration items at the beginning or end of the configuration, respectively. This allows you to prioritize things like partial functions and compose various methods together to control Lift’s behavior. You can think of a RulesSeq as a Seq on steroids, tweaked for Lift’s usage.
    The following list outlines, in order, the process of transforming a Request into a Response. We provide references to the sections of the book where we discuss each step in case you want to branch off.
    1. Execute early functions: this is a mechanism that allows a user function to be called on the HttpServletRequest before it enters the normal processing chain. This can be used for, for example, to set the XHTML output to UTF-8. This is controlled through LiftRules.early
    2. Perform URL Rewriting, which we already covered in detail in section 3.7↑. Controlled via LiftRules.rewrite, this is useful for creating user-friendly URLs, among other things. The result of the transformation will be checked for possible rewrites until there are no more matches or it is explicitly stopped by setting the stopRewriting val in ReqwriteResponse to true. It is relevant to know that you can have rewriter functions per-session hence you can have different rewriter in different contexts. These session rewriters are prended to the LiftRules rewriters before their application.
    3. Call LiftRules.onBeginServicing hooks. This is a mechanism that allows you to add your own hook functions that will be called when Lift is starting to process the request. You could set up logging here, for instance.
    4. Check for user-defined stateless dispatch in LiftRules.statelessDispatchTable. If the partial functions defined in this table match the request then they are used to create a LiftResponse that is sent to the user, bypassing any further processing. These are very useful for building things like REST APIs. The term stateless refers to the fact that at the time the dispatch function is called, the stateful object, called S, is not available and the LiftSession is not created yet. Custom dispatch is covered in section 3.8↑
    5. Create a LiftSession. The LiftSession holds various bits of state for the request, and is covered in more detail in section 9.5↓.
    6. Call LiftSession.onSetupSession. This is a mechanism for adding hook functions that will be called when the LiftSession is created. We’ll get into more details when we discuss Lift’s session management in section 9.5↓.
    7. Initialize the S object (section 3.4.1↑). The S object represents the current state of the Request and Response.
    8. Call any LoanWrapper instances that you’ve added through S.addAround. A LoanWrapper is a way to insert your own processing into the render pipeline, similar to how Filter works in the Servlet API. This means that when your LoanWrapper implementation is called, Lift passes you a function allowing you to chain the processing of the request. With this functionality you can execute your own pre- and post-condition code. A simple example of this would be if you need to make sure that something is configured at the start of processing and cleanly shut down when processing terminates. LoanWrappers are covered in section 9.6.1↓
    9. Process the stateful request
      1. Check the stateful dispatch functions defined in LiftRules.dispatch. This is similar to the stateless dispatch in step #4 except that these functions are executed in the context of a LiftSession and an S object (section 3.4.1↑). The first matching partial function is used to generate a LiftResponse that is returned to the client. If none of the dispatch functions match then processing continues. Dispatch functions are covered in section 3.8↑. This flow is wrapped by LiftSession.onBeginServicing/onEndServicing calls
      2. If this is a Comet request, then process it and return the response. Comet is a method for performing asynchronous updates of the user’s page without a reload. We cover Comet techniques in chapter 11↓
      3. If this is an Ajax request, execute the user’s callback function; the specific function is mapped via a request parameter (essentially a token). The result of the callback is returned as the response to the user. The response can be a JavaScript snippet, an XML construct or virtually any LiftResponse. For an overview of LiftResponse please see section 9.4↓. This flow is wrapped by LiftSession.onBeginServicing/onEndServicing calls.
      4. If this is a regular HTTP request, then:
        1. Call LiftSession.onBeginServicing hooks. Mostly “onBegin”/”onEnd” functions are used for logging. Note that the LiftRules object also has onBeginServicing and onEndServicing functions but these are “wrapping” more Lift processing and not just statefull processing.
        2. Check the user-defined dispatch functions that are set per-session (see S.addHighLevelSessionDispatcher). This is similar to LiftRules.dispatch except that you can have different functions set up for a different session depending on your application logic. If there is a function applicable, execute it and return its response. If there is no per-session dispatch function, process the request by executing the Scala function that user set up for specific events (such as when clicking a link, or pressing the submit button, or a function that will be executed when a form field is set etc.). Please see SHtml obejct 3.4.2↑.
        3. Check the SiteMap and Loc functions. We cover SiteMap extensively in chapter 7↑.
        4. Lookup the template based on the request path. Lift will locate the templates using various approaches:
          1. Check the partial functions defined in LiftRules.viewDispatch. If there is a function defined for this path invoke it and return an Either[() ⇒ Can[NodeSeq],LiftView]. This allows you to either return the function for handling the view directly, or delegate to a LiftView subclass. LiftView is covered in section 4.4↑
          2. If no viewDispatch functions match, then look for the template using the ServletContext’s getResourceAsStream.
          3. If Lift still can’t find any templates, it will attempt to locate a View class whose name matches the first component of the request path under the view folder of any packages defined by LiftRules.addToPackages method. If an InsecureLiftView class is found, it will attempt to invoke a function on the class corresponding to the second component of the request path. If a LiftView class is found, it will invoke the dispatch method on the second component of the request path.
        5. Process the templates by executing snippets, combining templates etc.
          1. Merge elements, as described in section e
          2. Update the internal functions map. Basically this associates the user’s Scala functions with tokens that are passed around in subsequent requests using HTTP query parameters. We cover this mechanism in detail in section 9.3↓
          3. Clean up notices (see S.error, S.warning, S.notice) since they were already rendered they are no longer needed. Notices are covered in section B↓.
          4. Call LiftRules.convertResponse. Basically this glues together different pieces if information such as the actual markup, the response headers, cookies, etc into a LiftResponse instance.
          5. Check to see if Lift needs to send HTTP redirect. For an overview please see 3.9↑
        6. Call LiftSession.onEndServicing hooks, the counterparts to LiftSession.onBeginServicing
      5. Call LiftRules.performTransform. This is actually configured via the LiftRules.responseTransformers RulesSeq. This is a list of functions on LiftResponse  ⇒ LiftResponse that allows the user to modify the response before it’s sent to the client
    10. Call LiftRules.onEndServicing hooks. These are the stateless end-servicing hooks, called after the S object context is destroyed.
    11. Call any functions defined in LiftRules.beforeSend. This is the last place where you can modify the response before it’s sent to the user
    12. Convert the LiftResponse to a raw byte stream and send it to client as an HTTP response.
    13. Call any functions defined in LiftRules.afterSend. Typically these would be used for cleanup.
    We realize that this is a lot of information to digest in one pass, so as we continue to cover the specific details of the rendering pipeline you may want to keep a bookmark here so that you can come back and process the new information in the greater context of how Lift is working.
    Tyler Weir has created a set of diagrams on the following two pages that outline Lift’s processing at the global level and also for HTTP requests in particular. For the visually-oriented these may explain things a bit better.


    figure images/lift_request_processing_global.png
    Малюнок 9.2 Lift Global Request Processing
    The “Process HTTP request” step is expanded on the following page.


    figure images/lift_request_processing_http.png
    Малюнок 9.3 Lift HTTP Request Processing


    9.3  Lift Function Mapping

    As we mentioned in section 6.1↑, lift utilizes scala closures and functions for almost all processing of client data. Because of this, Lift’s ability to associate functions with specific form elements, AJAX calls, etc, is critical to its operation. This association of functions, commonly known as “mapping” is handled through a combination of request parameters, Scala closures and Session data. We feel that understanding how mapping works is important if you want to work on advanced topics.
    At its most basic, mapping of functions is just that; a map of the user’s currently defined functions. To simplify things, Lift actually uses one of four subclasses of AFuncHolder [M]   [M] net.liftweb.http.S.AFuncHolder:
    BinFuncHolder  used for binding functions for file uploading. It will hold a FileParamHolder  ⇒ Any function, which is used to process the file data after upload (section 6.4↑)
    SFuncHolder  used for binding String ⇒ Any functions. This function corresponds to a single HTTP query parameter, except that the parameter name is unique to this request (we’ll cover naming shortly)
    LFuncHolder  used for binding List[String] ⇒ Any functions. This is essentially the same as SFuncHolder but for multiple values
    NFuncHolder  used for binding () ⇒ Any functions. Typically these are used for event callabcks (such as form submission)
    Wherever Lift takes a function callback it is converted to one of these types behind the scenes. Also on the backend, each function is assigned a token ID (generated by Helpers.nextFuncName), which is then added to the session, typically via S.addFunctionMap or S.mapFunc. The token is generally used as the form element name so that the tokens for a given form are passed back to Lift when the form is submitted; in AJAX, the token is used as an HTTP query parameter of the AJAX callback from the client JavaScript code. In either case, Lift processes the query parameters within LiftSession.runParams and executes each associated function in the function mapping.
    As a concrete example, let’s look at a simple binding in a form. Listing 9.3↓ shows a small example snippet that will request a person’s name and print it out when the person clicks the submit button.
    Function binding snippet
    1
    2
    3
    4
    5
    6
    7
    8
    def greet (xhtml : NodeSeq) : NodeSeq = {
      var name = ""
      def process() = {
        println(name)
      }
      bind("form", xhtml, "name" -> SHtml.text(name, name = _),
                          "greet" -> SHtml.submit("Greet", process))
    }
    Listing 9.3↓ shows the corresponding template using our sample snippet.
    Function binding template
    1
    2
    3
    4
    5
    :surround with="default" at="content">
      :Test.greet form="GET">
        :name /> :greet />
      :Test.greet>
    :surround>
    Finally, listing 9.3↓ shows an example of the resulting HTML that’s generated when a user views the template. As you can see, each of the elements with callbacks has a corresponding form element with a token ID for the name value. Since we’ve used the GET CGI method here (we usually recommend using POST in the real world), when we submit the form our URL would look like /greet.html?F541542594358JE2=...&F541542594359PM4=Greet. For SFuncHolder mappings the value of the request parameter is passed directly. For NFuncHolders the presence of the token in the query parameter list is enough to fire the function. For BinFuncHolder and LFuncHolder mappings some additional processing is performed to coerce the submitted values into proper values for the functions to handle.
    Function binding result
    1
    2
    3
    4
    ="get" action="/greet.html">
      ="F541542594358JE2" type="text" value=""/>
      ="F541542594359PM4" type="submit" value="Greet"/>
    Normally you do not have to directly deal with the function holder classes, since the generator functions in SHtml handle that internally. However, if you’re in a situation when you need to bind functions by yourself (such as building your own widget where SHtml doesn’t provided needed elements), you can use the previously mentioned S.addFunctionMap or S.mapFunc to do the “registration” for you.

    9.4  LiftResponse in Detail

    In some cases, particularly when using dispatch functions (section 3.8↑), you may want explicit control over what Lift returns to the user. The LiftResponse trait is the base of a complete hierarchy of response classes that cover a wide variety of functionality, from simply returning an HTTP status code to returning a byte stream or your own XML fragments. In this section we’ll cover some of the more common classes.

    9.4.1  InMemoryResponse

    The InMemoryResponse allows you to return an array of bytes directly to the user along with a set of HTTP headers, cookies and a response code. An example of using InMemoryResponse was given in section 3.8↑, showing how we can directly generate a chart PNG in memory and send it to the user. This is generally useful as long as the data you need to generate and send is relatively small; when you start getting into larger buffers you can run into memory constraints as well as garbage collection pressure if you’re serving a large number of requests.

    9.4.2  StreamingResponse

    The StreamingResponse class is similar to the InMemoryResponse, except that instead of reading from a buffer, it reads from an input object. The input object is not required to be a subclass of java.io.InputStream, but rather is only required to implement the method “def read(buf: Array[Byte]): Int”  [N]   [N] This is done with Scala’s structural typing, which we don’t cover in this book. For more info, see http://scala.sygneca.com/patterns/duck-typing-done-right, or the Scala Language Spec, section 3.2.7. This allows you to essentially send back anything that can provide an input stream. Additionally, you can provide a () ⇒ Unit function (cleanup, if you will) that is called when the input stream is exhausted. As an example, let’s look at how we could stream a file from our WAR back to the client. Listing 9.4.2↓ shows how we can retrieve the input stream from our classloader and then send it directly to the user. Note that you must know the size of the file you’re streaming before sending it.
    Streaming download method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def sendFile () : Box[LiftResponse] = {
      // Locate the file and process it
      LiftRules.getResource("/some-file.txt").map { url =>
        val input = url.openStream()
        val filesize = ... // must compute or predetermine this.
        StreamingResponse(inPipe,
                          () => { input.close },
                          filesize,
                          (Content-Type -> "text/plain") :: Nil,
                          Nil,
                          200)
      }
    }
    Note that we use the cleanup function to close the input stream once we’re done so that we make sure to release resources.

    9.4.3  Hierarchy

    The Lift framework makes a lot of things really easy and it provides extremly useful abstractions as you may have already discovered. Responses to clients are also abstacted by LiftResponse trait. There are numerous response types and here is the simplified view of the class hierarchy:
    • LiftResponse
      • BasicResponse
        • InMemoryResponse
        • StreamingResponse
      • JSonResponse
      • RedirectResponse
        • RedirectWithState
      • ToResponse
        • XhtmlRespomse
        • XmlResponse
        • XmlMimeResponse
        • AtomResponse
        • OpenSearchResponse
        • AtomCreatedResponse
        • AtomCategoryResponse
        • AtomServiceResponse
        • CreatedResponse
      • OkResponse
      • PermRedirectResponse
      • BadResponse
      • UnauthorizedResponse
      • UnauthorizedDigestResponse
      • NotFoundResponse
      • MethodNotAllowedResponse
      • GoneResponse
    We won’t get into details right now on what exactly each and every class/object does, although their purpose is given away by their names. It is important to know that whenever you need to return a LiftResponse reference from one of your functions, for example LiftRules.dispatch you can you can use one of these classes. Lift doesn’t really provide the HttpServletResponse object, instead all responses are impersonated by a LiftResponse instance and it content (the actual payload, http headers, content-type, cookies etc.) is written internally by Lift to the container’s output stream.
    Still let’s take a look at a few examples

    9.4.4  RedirectWithState

    RedirectWithState example
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Assume you boot function
    import MessageState._
    ...
    def boot = {
    LiftRules.dispatch.prepend {
      case Req("redirect1" :: _, _, _) => () =>
        Full(RedirectWithState("/page1", "My error" -> Error))
      case Req("redirect2" :: _, _, _) => () =>
        Full(RedirectWithState("/page2",
                               RedirectState(() => println("Called on redirect!"),
                                             "My error" -> Error)))    
    }
    First of all we added a DispatchPF function that pattern matches for paths starting with redirect1 and redirect2. Let’s see what happens in each case.
    • redirect1 - We are returning a RedirectWithState response. It will do HTTP redirect towards /page1 and the state is impersonated by the tuple “MyError” -> Error. Because MessageState object holds an implicit conversion function from Tuple2 to MessageState it suffices to just provide the tuple here. Essentially we are saying here that when the browser sends the redirect request to server we already have an Error notice set up and the tag from your /page1 will show this “My error” error message.
    • redirect2 - Similarly it does an HTTP redirect to browser towards your /page2. But we are passing now a RedirectState object. This object holds a () => Unit function that will be executed when browser send the redirect request and the Notices impersonated by a repeated parameter (String, NoticeType.Value)*. In fact the mapping between the actual message and its type: Notice, Warning or Error.

    9.4.5  XmlResponse

    XmlResponse example
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Assume you boot function
    def boot = {
    LiftRules.dispatch.prepend {
      case Req("rest" :: Nil, _, _) => () => Full(XmlResponse(
            
                John
                Jane
            
        ))
    }
    When you are receiving a request with the path /rest the code is returning an XML response. The content-type and everything else is taken care of by XmlResponse. You can build much more complex REST API’s an return XML response which is probably mot commonly used.

    9.5  Session Management

    Lift is a stateful framework and naturally this state needs to be managed. You may already be familiar with HttpSession and and how a J(2)EE web container identifies an HttpSession; either by a JSESSIONID cookie or by a JSESSIONID URI sequence (in case of URL rewriting). Similarly, Lift uses a LiftSession reference which is not actually “persisted” in HttpSession. As a matter of fact Lift does not really use the HttpSession provided by the web container to maintain conversational state, but rather uses a bridge between the HttpSession and the LiftSession. This bridge is impersonated by SessionToServletBridge class which implements javax.servlet.http.HttpSessionBindingListener and javax.servlet.http.HttpSessionActivationListener and works like this:
    1. When receiving an HTTP Request and there was no stateless dispatch function to execute, Lift does the stateful processing. But before doing that it checks to see if there is a LiftSession associated with this HTTP session ID. This mapping is kept on a SessionMaster Scala actor.
    2. If there is no associated LiftSession in the SessionMaster actor, create it and add a SessionToServletBridge attribute on HttpSession. This will make Lift aware of the session when the container terminates the HttpSession or when the HTTP session is about to be passivated or activated.
    3. When the container terminates the HTTP session, SessionToServletBridge sends a message to the SessionMaster Actor to terminate the LiftSession, which includes the following steps:
      1. Call any defined LiftSession.onAboutToShutdownSession hooks
      2. Send a ShutDown message to all Comet Actors pertaining to this session
      3. Clean up any internal LiftSession state
      4. Call LiftSession.onShutdownSession hooks
    The SessionMaster Actor is also protected by another watcher Actor. This watcher Actor receives the Exit messages of the watched Actors. When it receives an Exit message it will call the users’ failure functions and restart the watched actor (Please see ActorWatcher.failureFuncs).
    Even while Lift is handling session management you still have the ability to manually add attributes to the HttpSession object. We do not recommend this unless you really must. A simpler way to keep your own session variables, is to use SessionVars. For more details about SessionVar please see the fundamental chapter 3.11↑
    The next question would probably be “So we have internal session management, how do we cope with that in a clustered environment? ... how are sessions replicated?” the answer is, they aren’t. There is no intention to use the web container’s session replication as these technologies appears to be inferior to other solutions on the market. Relying on Java serialization brings a lot of performance concerns and alternative technologies have been investigated and they are still under investigation. Until there is a standard session replication technology you can still cluster you application using “sticky session”. This meas that all requests pertaining to a HTTP session must be processed by the same cluster node. This can be done by software or hardware load balancers, as they would dispatch the requests based on JSESSIONID cookie. Another approach is that the dispatching is done based on some URI or query parameters. For example, a query parameter like serverid=1 is configured in the load balancer to always be dispatched to the node 1 of the cluster, and so on. There are some downsides for the sticky session approach. For instance you are logged in the application and do your stuff. Suddenly the node designated to your session crashes. At this moment you lost your session. The next subsequent request would be automatically dispatched by the load balancer to another cluster node and depending how your application is built this may mean that you need to log in again or if part of the state was persisted in DB you may resume your work from some point avoiding re-login ... but this is application specific behavior that is beyond the scope of this discussion. The advantages of sticky sessions are related with application performance since in this model the state does not need to be replicated in all cluster nodes which for significant state information can be quite time/resources consuming.

    9.5.1  Lift garbage collection

    As you have seen, Lift tailors Scala functions with client side artifacts (XHTML input elements, Ajax requests etc.). Naturally these functions are kept into the session state. Also for every rendered page, a page ID is generated and functions bound for these pages as asociated with this page ID. In order to prevent accumulation of such mappings, Lift has a mechanism of purging unused functions. Basically the idea is
    1. On client side, a script periodically sends to the server an Ajax request impersonating a lift GC request.
    2. On service side Lift updates the timestamps of the functions associated with this page ID. The functions older then LiftRules.unusedFunctionsLifeTime (default value is 10 minutes) become eligible for garbage collection as they are de-referenced from the current session. The frequency of such Ajax requests is given by LiftRules.liftGCPollingInterval. By default it is set to 75 seconds.
    3. Each Ajax request contains includes the page ID as new function may be bound as a result of processing the Ajax request, dependin on the application code. Such function that are dynamically bound are automatically associated with the same page ID.
    You can of course turn off this garbage collection mechanism by setting LiftRules.enableLiftGC = false typically in your Boot. You can also fine tune the garbage collection mechanims to fit your application needs, by changing the default LiftRules variables.
    LiftRules gabage collection variables
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**   
     * By default lift uses a garbage-collection mechanism of removing
     * unused bound functions from LiftSesssion
     * Setting this to false will disable this mechanims and there will
     * be no Ajax polling request attempted.   
     */
    var enableLiftGC = true;
    /**   
     * If Lift garbage collection is enabled, functions that are not seen
     * in the page for this period of time (given in milliseonds) will be
     * discarded, hence eligible for garbage collection. The default value
     * is 10 minutes.   
     */  
    var unusedFunctionsLifeTime: Long = 10 minutes
    /**   
     * The polling interval for background Ajax requests to prevent
     * functions of being garbage collected.  
     * Default value is set to 75 seconds.
     */  
    var liftGCPollingInterval: Long = 75 seconds
    /**  
     * The polling interval for background Ajax requests to prevent functions
     * of being garbage collected. 
     * This will be applied if the Ajax request will fail. Default value is
     * set to 15 seconds. 
     */
    var liftGCFailureRetryTimeout: Long = 15 seconds

    9.6  Miscellaneous Lift Features

    In this section we will discuss various features that can prove helpful in building rich Lift applications.

    9.6.1  Wrapping Lift’s processing logic

    Lift provides the ability to allow user functions to be part of processing lifecycle. In these cases Lift allows you to provide your own functions and the actual Lift’s processing function is passed to your function. Hence your own function is responsible of calling the actual Lift’s processing logic.
    But let’s see how exactly you can do this.
    LoanWrapper example
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Boot {  
      def boot {   
        ...  
        S.addAround(new LoanWrapper { // Y  
          def apply[T](f: => T): T = {  
            println("Y -> hello to the request!"
            val result = f // Let Lift do normal request processing.  
            println("Y -> goodbye!"
            result  
          }  
        })  
        S.addAround(new LoanWrapper { // X  
          def apply[T](f: => T): T = {  
            println("X -> hello to the request!")  
            val result = f // Let Lift do normal request processing.  
            println("X -> goodbye!")  
            result
          }  
        })  
     
    The code looks pretty straight-forward in the sense that we add two LoanWrapper instances to the S object. (Note that we’re using the S object not LiftRules meaning that LoanWrappers are applicable only for stateful processing. See 9.2↑ for when exactly LoanWrappers are invoked.)
    So let’s see what happens when the above code processess a request from a client. You can think of the invocation sequence as X(Y(f)) where f is the Lift function that impersonates the core processing. Therefore you’ll see the following output in the console:
    1
    2
    3
    4
    5
    X -> hello to the request!
    Y -> hello to the request!
    Y -> goodbye!
    X -> goodbye!
    This feature allows you use a resource before Lift does and release them after Lift has finished processing the stateful request and before the LiftResponse object is constructed.

    9.6.2  Passing Template Parameters to Snippets

    In addition to the standard attributes for snippets, outlined in Розділ 5.1↑, you can set your own attributes on the snippet element. Attributes used in this manner are called “parameters”. Listing 9.6.2↓ shows us setting a default parameter on our Ledger.balance snippet.
    Defining a Snippet Parameter
    1
    2
    3
    :Ledger.balance default="10">
      :balance/> as of :time />
    :Ledger.balance>
    The S.attr function allows us to access all parameters defined on the snippet element itself, as shown in Listing 9.6.2↓.
    Accessing a Snippet Parameter
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Ledger {
      def balance (content : NodeSeq ) : NodeSeq = {
        val dflt = S.attr("default") openOr "0";
        bind ("ledger", content,
              "balance" -> Text(currentLegdger.formattedBalance),
              "time" -> Text((new java.util.Date).toString))
      }
    }

    9.6.3  Computing Attributes with Snippets

    You can use snippets to compute tag attributes, as shown in Listing 9.6.3↓:
    Using a Snippet to Compute an Attribute
    1
    2
    3
    4
    5
    6
    7
    8
    // In your page you can have
    :snippet="MyDivThing:calcDir"> ...
    ...
    // Your snippet
    class MyDivThing {  
      def calcDir = new UnprefixedAttribute("dir", "rtl", Null)
    }

    9.6.4  Processing Element Attributes

    Now we have seen how we can pass xml parameters to snippets but what if we want to pass parameters on the nodes that will be bound? For instance, we may want to pass the am/pm information on the time element such as:
     
    
    to control the time display format. Listing 9.6.4↓ shows how we can use the BindHelpers object to retrieve the current element’s attributes.
    Retrieving Element Attributes with BindHelpers
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Ledger {
      def balance (content : NodeSeq ) : NodeSeq = {
        val dflt = S.attr("default") openOr "0";
        bind ("ledger", content,
              "balance" -> Text(currentLegdger.formattedBalance),
              "time" -> {
                node: NodeSeq => println(BindHelpers.attr("ampm"));
                Text((new java.util.Date).toString))
              })
      }
    }
    You can use the BindHelpers object for obtaining information about node attributes. This context is maintained internally using ThreadLocals and closures. Note that the context is cleared after the bind method is executed. In our example above for “time” node we are actually binding a function that takes the child nodes of the node. When our function is called by Lift we can access the BindHelpers, such ass the attributes of the current node. The sequence -> is turned into a BindParam object using implicit conversions. It is important to note that BindParam.calcValue function is called in the correct context so that BindHelpers can be safely used.

    9.7  Advanced S Object Features

    The S, or Stateful, object is a very important part of Lift. The S context is created when a client request is recieved that needs to be handled as a stateful reuest. Please see 9.2↑ for more details on the state creation and handling. The actual state information is kept inside the S object using ThreadLocal [O]  [O] java.lang.ThreadLocal variables since S is a singleton. This means that if you have any code that is executed in the stateful context you can safely use any S object goodies, which include:

    9.7.1  Managing cookies

    You can retrieve cookies from the request or set cookies to be sent in the response. Cookies are covered in section 3.10↑.

    9.7.2  Localization and Internationalization

    Localization (also called L10N) and Internationalization (also called I18N) are very important aspects of many web applications that deal with different languages. These topics are covered in chapter D↓.

    9.7.3  Managing the Timezone

    The S.timeZone function returns the current timezone as computed by the
    LiftRules.timeZoneCalculator function. By default, the LiftRules method simply executes TimeZone.getDefault, but you can provide your own Box[HttpServletRequest]  ⇒ TimeZone partial function to define your own behavior. Examples would include allowing users to choose their own timezone, or to use geographic lookup of the user’s IP address.

    9.7.4  Per-session DispatchPF functions

    You can set DispatchPF functions that operate in the context of a current session. Essentially you can bind DispatchPF functions with a given name. Relevant functions are:
    • S.highLevelSessionDispatcher - returns a List[LiftRules.DispatchPF]
    • S.highLevelSessionDispatchList - returns a List[DispatchHolder]
    • S.addHighLevelSessionDispatcher - maps a name with a given DispatchPF
    • S.removeHighLevelSessionDispatcher - removes the DispatchPF given its name
    • S.clearHighLevelSessionDispatcher - removes all DispatchPF associations

    9.7.5  Session re-writers

    Session re-writers are per session functions that allow you to modify a HTTP request (URI, query parameters etc.) before the request is actually processed. This is similar with LiftRules.rewrite variable but you can apply rewriters per a given session. Hence you can have different rewrites in diferent contexts. The relevant functions are:
    • S.sessionRewriter - returns a List[RewriteHolder]
    • S.addSessionRewriter - maps a LiftRules.RewritePF with a given name
    • S.removeSessionRewriter - removes a rewriter by a name
    • S.clearSessionRewriter - remove all session rewriters.

    9.7.6  Access to HTTP headers

    Accessing HTTP header parameters from the request and adding HTTP header parameters to the HTTP response represent very common operations. You can easily perform these operations using the following functions:
    • S.getHeaders - returns a List[(String, String)] containing all HTTP headers grouped by name and value pair
    • S.setHeader - sets a HTTP header parameter by specifying the name and value pair

    9.7.7  Manage the document type

    You can also read and write the XML document type set for the current response. You can use the following functions:
    • S.getDocType - returns the doc type that was set forthe current response
    • S.setDocType - sets a document type for the curent response object.

    9.7.8  Other functions

    • Access to the raw HttpServletRequest and HttpSession if you really need it.
    • Managing the function map. The function map generates an association between a String and a function. This string represents a query parameter that when Lift receives upon a HTTP request, it will execute your function. Normally these names are auto-generated by Lift but you can also provide you own name. Please see 9.3↑ for more details.
    • Managing wrappers - see 9.6.1↑
    • Managing notices - see 3.6↑
    • Managing HTTP redirects - see S.redirectTo functions and 9.4↑
    • Using XML attibutes of a snippet - see

    9.8  ResourceServer

    ResourceServer is a Lift component that manages the serving of resources like JS, CSS etc. Well the web container can do that right? ... still container does not serve these resources if they are inside jar files. The default URI path for serving such resources is given by LiftRules.resourceServerPath variable which by default it is set to “classpath”. The folder location where the resource is looked up inside jar files is given by ResourceServer.baseResourceLocation variable which by default it is set to “toserve”. Let’s assume the following folder structure inside you Lift project:
    lift-proj/src/main/resources/toserve/css/mystyle.css
    Maven will create the toserver folder in the jar/war file generated. Then in your web page you add something like:
    Because the first URI part matches with LiftRules.resourceServerPath Lift will tell ResouceServer to load this resource from ’toserve’ folder. But it will fail. There is one thing left to do. We need to tell ResouceServer to allow the loading of mystyle.css resource. We can do this from Boot by calling:
    ResourceServer.allow {
    case "css" :: _ => true
    }
    We basically told Lift here to allow any resource found in css folder under toserve. Note that toserver comes from ResourceServer.baseResourceLocation which can be changed.

    9.9  HTTP Authentication

    HTTP authentication is described by RFC 2617  [P]   [P] http://www.isi.edu/in-notes/rfc2617.txt. It describes the means of protecting server resources and allowing access only to authorized entities. As you may know, any J(2)EE web container provides HTTP authentication support using JAAS [Q]   [Q] Java Authentication and Authorization Service. More information can be found at http://java.sun.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html. However, this approach has limitations. For example, if you provide your own LoginModule or CallbackHandler implementation this will not be loaded by the web application classloader but instead by the container classloader (at least in tomcat). This can lead to dependency loading issues since the web application classloader sits below the container’s classloader in the delegation chain. Lift, however, provides supports for both basic and digest authentications via a simplified, scala-oriented API that you can use directly. This API provides not only direct support for the HTTP authentication mechanisms, but also a path and role based authorization mechanism. The following sections show how we use basic authentication to protect our REST API (Глава 15 on page 1↓).

    9.9.1  Determining which Resources to Protect

    The first thing we need to do is tell Lift which resources are protected by authentication. This is done by configuring LiftRules.httpAuthProtectedResources with one or more PartialFunction[Req,Box[Role]] [R]   [R] net.liftweb.http.auth.Role to match on the request. Listing 9.9.1↓ shows the PartialFunction defined in our DispatchRestAPI object (Розділ 15.4.1 on page 1↓) used to protect our REST API from unauthorized access.
    Defining Protected Resources
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // We explicitly protect GET and PUT requests in our REST API
    import net.liftweb.http.auth.AuthRole
    def protection : LiftRules.HttpAuthProtectedResourcePF = {
      case Req(List("api", "account", accountId), _, PutRequest) =>
         Full(AuthRole("editAcct:" + accountId))
      case Req(List("api", "account", accountId), _, GetRequest) =>
         Full(AuthRole("viewAcct:" + accountId))
      // If the account is public, don’t enforce auth
      case Req(List("api", "expense", Expense(e, true)), _, GetRequest) => Empty
      case Req(List("api", "expense", Expense(e, _)), _, GetRequest) =>
        Full(AuthRole("viewAcct:" + e.account.obj.open_!.id))
    }
    The PartialFunction matches on the Req and can either return an Empty, indicating that the given request does not require authentication, or a Full[Role], that indicates which Role a user requires to be authorized to access the given resource. One important thing to remember is that HTTP authentication and SiteMap access control (Розділ 7.3 on page 1↑) are synergistic, so make sure that you configure both properly. We will discuss Roles further in Розділ 9.9.3↓, but for now you can simply consider them as String attributes associated with the current session. Once we’ve defined which resources are to be protected, we need to hook our PartialFunction into LiftRules in the Boot.boot method, shown in Listing 9.9.1↓.
    Hooking Resource Protection
    1
    2
    // Hook in our REST API auth
    LiftRules.httpAuthProtectedResource.append(DispatchRestAPI.protection)

    9.9.2  Providing the Authentication Hook

    After we’ve defined what resources we want to protect, we need to configure the LiftRules.authentication function to perform the actual authentication. Lift supports both HTTP Basic and Digest authentication schemes, which we’ll cover in the next two sections.
    Note that in these examples we use stateful dispath (Розділ 3.8 on page 1↑) since the User.logUserIn method utilizes a backing SessionVar. If you use stateless dispatch you will need to provide your own RequestVars to store the current user and roles.

    9.9.2.1  HTTP Basic Authentication

    HTTP Basic authentication is provided by the net.liftweb.http.auth.HttpBasicAuthentication implementation class, constructed using the authentication realm name as well as a PartialFunction[(String, String, Req), Boolean] that actually does the authentication. The tuple passed to the PartialFunction consists of the attempted username password, and the request object (Req). It’s your responsibility to return true or false to indicate whether the provided credentials succeed. Listing 9.9.2.1↓ shows the code in Boot.boot that PocketChange uses to perform authentication based on the user’s email address and password. Note that when authentication succeeds for a given user not only do we return true, but we set the user as logged in (via User.logUserIn) and we compile a set of all of the Roles that the user so that Lift knows which protected resources the user may access. The net.liftweb.http.auth.userRoles RequestVar is a built-in construct in Lift that the authentication backend uses for bookkeeping.
    Performing Basic Authentication
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import net.liftweb.http.auth.{AuthRole,HttpBasicAuthentication,userRoles}
    LiftRules.authentication = HttpBasicAuthentication("PocketChange") {
      case (userEmail, userPass, _) => {
        logger.debug("Authenticating: " + userEmail)
        User.find(By(User.email, userEmail)).map { user =>
          if (user.password.match_?(userPass)) {
            logger.debug("Auth succeeded for " + userEmail)
            User.logUserIn(user)
            // Compute all of the user roles
            userRoles(user.editable.map(acct => AuthRole("editAcct:" + acct.id)) ++
                      user.allAccounts.map(acct => AuthRole("viewAcct:" + acct.id)))
            true
          } else {
            logger.warn("Auth failed for " + userEmail)
            false
          }
        } openOr false
      }
    }

    9.9.2.2  HTTP Digest Authentication

    HTTP Digest authentication is provided by the net.liftweb.http.auth.HttpDigestAuthentication implementation class. Like Basic authentication, the HttpDigestAuthentication instance is constructed with a realm name and a PartialFunction, but in this case the PartialFunction uses a tuple of (String,Req,(String) ⇒ Boolean). The first parameter is still the username, and the second parameter is the request instance, but the third parameter is a function that will compute and compare the digest for authentication based on a plaintext password. This means that if we want to use Digest authentication, we need to be able to retrieve a plaintext password for the user from the database somehow. Listing 9.9.2.2↓ shows how we could do this in PocketChange if we modified the User.password field to simply be a MappedString.
    Performing Digest Authentication
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import net.liftweb.http.auth.{AuthRole,HttpBasicAuthentication,userRoles}
    LiftRules.authentication = HttpBasicAuthentication("PocketChange") {
      case (userEmail, _, authenticates) => {
        logger.debug("Authenticating: " + userEmail)
        User.find(By(User.email, userEmail)).map { user =>
          if (authenticates(user.password.is)) {
            logger.debug("Auth succeeded for " + userEmail)
            User.logUserIn(user)
            // Compute all of the user roles
            userRoles(user.editable.map(acct => AuthRole("editAcct:" + acct.id)) ++
                      user.allAccounts.map(acct => AuthRole("viewAcct:" + acct.id)))
            true
          } else {
            logger.warn("Auth failed for " + userEmail)
            false
          }
        } openOr false
      }
    }
    Another important factor with Digest authentication is that it uses nonces  [S]   [S] http://en.wikipedia.org/wiki/Cryptographic_nonce for authenticating the client, and the nonces have a limited lifetime. The default nonce lifetime is 30 seconds, but you can configure this by overriding the HttpDigestAuthentication.nonceValidityPeriod method.

    9.9.3  Role Hierarchies

    So far we’ve discussed Roles as essentially flat constructs. A Role, however, is an n-ary tree structure, meaning that when we assign a Role to a protected resource we can actually provide a hierarchy. Малюнок 9.4↓ shows an example of one such hierarchy. In this example, the Admin is the “superuser” role for admins, and can do what any sub-role can do and more. The Site-Admin can monitor the application, the User-Admin can manage users, and then we specify a set of location-specific roles: the Romania-Admin that can manage users from Romania, US-Admin that can manage users from US and UK-Admin that can only manage users from UK. With this hierarchy a User-Admin can manage users from anywhere but a Site-Admin can not manage any users. A Romania-Admin can’t monitor the site, nor it can manage the US or UK users.
    figure images/roles.png
    Малюнок 9.4 Roles hierarchy example
    Given this Role hierarchy, Listing 9.9.3↓ shows how we can implement this in our code by creating our Role hierarchy and then using the Role.getRoleByName method to locate the proper Role when we perform authentication. In this example we’re restricting access to the /users/ro path to only users with the “Romania-Admin” role. However, our fictional “John” user is assigned the “User-Admin” role, so he will be able to access that path.
    Using Role Hierarchies
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import auth._
    class Boot {
      def boot = {
        ...
        val roles =
          AuthRole("Admin",
            AuthRole("Site-Admin"),
            AuthRole("User-Admin",
                AuthRole("Romania-Admin"),
                AuthRole("US-Admin"),
                AuthRole("UK-Admin")
            )
        )
        LiftRules.protectedResource.append {   
          case (ParsePath("users" :: "ro" :: _, _, _, _)) =>
            roles.getRoleByName("Romania-Admin")
        }
        
        LiftRules.authentication = HttpBasicAuthentication("lift") { 
          case ("John", "12test34", req) =>
            println("John is authenticated !")
            userRoles(AuthRole("User-Admin"))
            true   
        
       ...
      }
    }

    10 Lift and JavaScript

    In this chapter we’ll be discussing some of the techniques that Lift provides for simplifying and abstracting access to JavaScript on the client side. Using these facilities follows Lift’s model of separating code from presentation by allowing you to essentially write JavaScript code in Scala. Lift also provides a layer that allows you to use advanced JavaScript functionality via either the JQuery [T]   [T] http://jquery.com/ or YUI [U]   [U] http://developer.yahoo.com/yui/ user interface libraries.

    10.1  JavaScript high level abstractions

    You may have noticed that Lift already comes with rich client side functionality in the form of AJAX and COMET support (chapter 11↓). Whenever you use this support, Lift automatically generates the proper