Обзор фреймворка Tapestry
Tapestry - это открытый фреймворк для создания динамичных, гибких, масштабируемых веб-приложений на Java.
Он создан для разработки приложений, начиная от самых малых и заканчивая объемными приложениями с тысячами страниц, над которыми работают большие распределенные команды разработчиков.
Tapestry основан на 4-х принципах:
- Простота - разработка веб-приложений не должна требовать незаурядных умственных способностей
- Устойчивость и слаженность - что работает в компонентах, должно работать в страницах; что работает в малых приложениях, должно работать в больших; разные разработчики должны находить одни и те же решения для одних и тех же проблем
- Производительность и масштабируемость
- Обеспечение обратной связи
Эти четыре принципа обеспечивают основную идею фреймворка - самое правильное решение должно быть самым простым.
Создание приложений при помощи Tapestry включает в себя:
- создание HTML шаблонов (*.html)
- создание файлов спецификаций компонентов, которые будут использоваться в шаблонах (*.page)
- создание бизнес-логики (*.java)
Из этих трех частей (*.html, *.page, *.java) Tapestry создает объект, представляющий страницу.
Обычно это экземпляр класса-наследника org.apache.tapestry.html.BasePage, у которого есть свойства и методы.
Свойства описываются в файлах спецификаций (или прямо в шаблоне, или в Java классе, здесь нет строго требования к тому, как это лучше делать), а методы реализуются в Java классе.
Таким образом достигается главное преимущество данного фреймворка - объектно-ориентриванный поход к созданию веб-приложений, позволяющий легко добавлять новый функционал.
Компоненты страницы
Страница состоит из компонентов, которые описываются в файле спецификаций.
Доступ к ним из шаблона осуществляется по id-шкам. Для этого служит префикс jwcid.
jwcid – Java Web Component id
jwcid:componentId – означает компонент с id = componentId, который описан в файле спецификаций *.page.
Доступ к методам класса, реализующего бизнес-логику страницы, осуществляется при помощи префикса ognl.
ognl – Object Graph Navigation Language
ognl:stockId – означает вызов метода getStockId()
Поодержка шаблона проектирования Model-View-Controller
Tapestry реализует шаблон MVC (Модель - Вид - Контроллер). Согласно этому шаблону,
- модель - это файл спецификаций *.page
- вид - это шаблон *.html
- контроллер - это класс, реализующий бизнес-логику *.java
Способы объявления компонентов в шаблонах
Явный
<?xml version="1.0"?>
<!DOCTYPE page-specification PUBLIC
"-Apache Software FoundationTapestry Specification 4.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
<page-specification>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="ognl:stockId"/>
</component>
</page-specification>
Здесь указаны два компонента:
- форма с action-методом onOk()
- текстовое поле ввода
Так выглядит файл шаблона:
<html>
<form jwcid="stockQuoteForm">
<input type="text" jwcid="stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
Неявный
Удаляем из файла Home.page все объявления компонентов:
<page-specification class="com.ttdev.stockquote.Home">
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Указываем эти компоненты прямо в шаблоне:
<html>
<form jwcid="stockQuoteForm@Form" listener="listener:onOk">
<input type="text" jwcid="stockId@TextField" value="ognl:stockId"/>
<input type="submit" value="OK"/>
</form>
</html>
Неявное указание компонентов проще, т.к. не нужно создавать второй файл, все в одном.
Но зато для раздельной работы дизайнера с программистом удобнее явное указание компонентов, т.к. дизайнер может по ошибке удалить указание компонента, которое сложнее восстановить с нуля, чем просто посмотреть его id-шку в *.page и прописать ее в шаблоне.
Как сделать перенаправление с одной страницы на другую
Допустим после выполнения action-метода нужно перейти на новую страницу, например, страницу результата.
Для этого
- Нужно создать шаблон этой страницы, например, result.html.
- Создать класс-наследник BasePage с названием Result.
- Добавить чуть кода в action-метод, который позволит вернуть страницу результата
Эти «чуть кода» могут выглядеть так:
public class Home extends BasePage {
…
public String onOk(IRequestCycle cycle) {
return "Result";
}
}
или так (чтобы передать параметр):
public class Home extends BasePage {
private String stockId;
…
public IPage onOk(IRequestCycle cycle) {
int stockValue = stockId.hashCode() % 100;
Result resultPage = (Result) cycle.getPage("Result");
resultPage.setStockValue(stockValue);
return resultPage;
}
}
Кстати, такой способ передачи параметров от одной страницы к другой в Tapestry носит название шаблона bucket brigade («пожарная цепочка»).
Существует еще один способ вызова страницы результата – просто указать эту страницу в файле *.page при помощи такой строчки:
<inject property="resultPage" type="page" object="Result"/>
Файл Home.page будет выглядеть так:
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
Необходимо отметить, что умный Tapestry сам создаст необходимый объект, который будет содержать метод getResultPage(). Нам лишь только нужно объявить абстрактный метод getResultPage():
public abstract class Home extends BasePage {
private String stockId;
abstract public Result getResultPage();
public String getStockId() {
return "MSFT";
}
public void setStockId(String stockId) {
this.stockId = stockId;
}
public IPage onOk(IRequestCycle cycle) {
int stockValue = stockId.hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
}
О безопасной передаче данных
В приведенных выше примерах мы не заботимся о начальном значении параметра stockId, поэтому если страница с результатом закэширована, любой пользователь может увидеть ее.
Лучше делать так:
добавляем свойство stockValue в файл спецификаций нашей страницы результата
<page-specification class="com.ttdev.stockquote.Result">
<property name="stockValue"/>
<component id="stockValue" type="Insert">
<binding name="value" value="stockValue"/>
</component>
</page-specification>
а умный Tapestry сам создаст класс-потомок Result, в который добавит необходимые методы инициализации, установления и получения данного свойства:
public class ResultEnhanced extends Result {
private XXX stockValue;
protected void initialize() {
stockValue = <default value for type XXX>;
}
public XXX getStockValue() {
return stockValue;
}
public void setStockValue(XXX stockValue) {
this.stockValue = stockValue;
}
}
Проблема с начальным значением при загрузке страницы решена – поле автоматически очищается.
А нам можно убрать ненужный теперь код из Result.java:
public abstract class Result extends BasePage {
int stockValue;
public int getStockValue() {
return stockValue;
}
public void setStockValue(int stockValue) {
this.stockValue = stockValue;
}
abstract public void setStockValue(int stockValue);
}
Оставим только setStockValue, т.к. он вызывается из Home.java.
Теперь для полной уверенности в безопасности передачи данных нужно убрать поле stockId из Home.java и добавить свойство stockId в файл спецификаций Home.page.
Если мы все же хотим задать какое-либо начальное значение полю, это тоже можно сделать:
<property name="stockId" initial-value="literal:MSFT"/>
Использование Java аннотаций для добавления страниц и свойств
В примере ниже свойство stockId и страница Result указаны в спецификациях страницы:
<page-specification class="com.ttdev.stockquote.Home">
<inject property="resultPage" type="page" object="Result"/>
<property name="stockId" initial-value="literal:MSFT"/>
<component id="stockQuoteForm" type="Form">
<binding name="listener" value="listener:onOk"/>
</component>
<component id="stockId" type="TextField">
<binding name="value" value="stockId"/>
</component>
</page-specification>
При помощи аннотаций это можно сделать так:
public abstract class Home extends BasePage {
@InjectPage("Result")
abstract public Result getResultPage();
@InitialValue("literal:MSFT")
abstract public String getStockId();
public IPage onOk(IRequestCycle cycle) {
int stockValue = getStockId().hashCode() % 100;
Result resultPage = getResultPage();
resultPage.setStockValue(stockValue);
return resultPage;
}
}
Где можно посмотреть, какие компоненты есть у Tapestry и как их использовать
Для этого существует документация.
JavaDocs для Tapestry содержит полный список компонентов.
Проверка вводимых данных. Трансляторы и валидаторы
Tapestry содержит несколько трансляторов, например, для проверки ввода числа, даты…
В примере ниже указано, что данное поле ввода обязательно для заполнения, в него должно быть введено число, целое или дробное, минимальное значение которого 0.
[You must enter {0}!] означает, что если пользователь ничего не введет, то будет выдано следующее сообщение: «You must enter Weight!».
<component id="weight" type="TextField">
<binding name="value" value="weight"/>
<binding name="translator"
value="translator:number,pattern=#.#"/>
<binding name="validators"
value="validators:required[You must enter {0}!],min=0"/>
<binding name="displayName" value="literal:Weight"/>
</component>
Можно создавать собственные валидаторы, для этого создать класс, реализующий интерфейс Validator.
Работа с сессиями
В Tapestry существует два уровня доступности объектов:
- объекты уровня приложения, которые доступны всем пользователям
- объекты уровня сессии, доступные в рамках текущей сессии пользователя
Для инициализации начальных данных приложения используется Hivemind.
Создадим файл hivemodule.xml в папке src/META-INF:
<?xml version="1.0"?>
<module id="com.ttdev.shop" version="1.0.0">
<contribution configuration-id="tapestry.state.ApplicationObjects">
<state-object name="cart" scope="session">
<create-instance class="java.util.ArrayList"/>
</state-object>
</contribution>
</module>
Укажем абстрактный метод пучения объекта cart:
public abstract class ProductDetails extends BasePage {
private String productId;
@InjectState("cart")
public abstract List getCart();
public void addToCart() {
System.out.println("Trying to add " + productId + " to cart");
getCart().add(productId);
}
…
}
Или укажем его в файле спецификаций для ProductDetails:
<page-specification class="com.ttdev.shop.ProductDetails">
<inject property="cart" type="state" object="cart"/>
…
</page-specification>
Тогда в Java классе аннотация не нужна:
@InjectState("cart")
public abstract List getCart();
Что почитать
- Enjoying Web Development with Tapestry, автор - Kent Tong.
- http://tapestry.apache.org/
- Статьи Александра Колесникова и других авторов: http://tapestry.apache.org/articles.html