Tapestry

Обзор фреймворка Tapestry

Tapestry - это открытый фреймворк для создания динамичных, гибких, масштабируемых веб-приложений на Java.

Он создан для разработки приложений, начиная от самых малых и заканчивая объемными приложениями с тысячами страниц, над которыми работают большие распределенные команды разработчиков.

Tapestry основан на 4-х принципах:

  1. Простота - разработка веб-приложений не должна требовать незаурядных умственных способностей
  2. Устойчивость и слаженность - что работает в компонентах, должно работать в страницах; что работает в малых приложениях, должно работать в больших; разные разработчики должны находить одни и те же решения для одних и тех же проблем
  3. Производительность и масштабируемость
  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-метода нужно перейти на новую страницу, например, страницу результата.
Для этого

  1. Нужно создать шаблон этой страницы, например, result.html.
  2. Создать класс-наследник BasePage с названием Result.
  3. Добавить чуть кода в 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();

Что почитать

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License