foror.ru / соучастник

В обход J2EE или обзор Tapestry 5 фреймворка на примере блога.

1. Вступление

Два года назад (2006 год) искал альтернативу PHP и своим MVC велосипедам. В то время на PHP только-только начинали появляться профессиональные фреймворки (например, релиза Zend Framework так и не дождался), пришла мода на RubyOnRails, что-то слышал о Django. Подавшись моде изучил пару книжек по RoR, но в итоге не стал связываться с Ruby, видимо остановило отличие синтаксиса от С подобных языков. Как вариант, рассматривал создание веба на С++, но посмотрев имеющиеся фреймворки - передумал. Выбирать было нечего, поэтому выбор пал на Java — технологией где-то между PHP и С++.

J2EE оказалась слишком запутанной для новичка. PHP программисту в новинку было столкнуться с десятком фреймворков, которые навязывают для создания веба на J2EE. Сервлеты, портлеты, JMS, RMI, EJB, JSP, JSF - чего там только не было! Но затем узнал об альтернативах J2EE типа Spring.

До Spring так и не дошел, почему-то показалось, что по сложности он был на уровне J2EE. Остановился на Tapestry4. Этот фреймворк сравнивали с JSF - одной из технологий J2EE для представления веба. Через Tapestry4 впервые узнал об IoC движках (в моём случае был HiveMind). Попозже узнал о Wicket позволяющий делать представление веба не хуже, чем в Tapestry4. Но затем вышла альфа версия Tapestry5, на которой окончательно остановился.

В Tapestry5 понравилось множество вещей. Во-первых, конфигурация фреймворка выполняется через аннотации — XML конфигурация сведена к минимуму. Во-вторых, динамически подхватываются изменения в шаблонах и коде, без перезагрузки контейнера сервлетов — основная проблема J2EE и других Java фреймворков. Правда работает это только для кода, про который знает Tapestry5 (компонеты, страницы и еще кое-что), но и это не плохо. И в-третьих, грамотный ООП подход, простота и наличие собственного IoC движка.

Для работы с базой данных решил использовать ORM Hibernate. Думаю это самая мощная opensource ORM из существующих в настоящее время, поэтому выбор был очевиден. А появление HibernateSearch, HibernateValidator, HibernateAnnotations — окончательно укрепили решение продолжать работать с Java без оглядки в сторону Python с Django.

За 2007-2008 года Tapestry5 была в альфе и API разработчика постоянно менялось, поэтому приходилось изучать код. С одной стороны потерял время, но с другой изучил лучше фреймворк. Накопился опыт, которым есть желание поделиться на примере разработки небольшого блога.

Исходники проекта доступны здесь. В работе можно посмотреть здесь. Для входа в панель управления логин admin и пароль admin.

2. Среда разработчика

При использовании Maven создание первого проекта будет простым. Для начала, скачайте и установите Maven, в Linux воспользуйтесь менеджером пакетов (нужна версия 2.0.8 или выше). Для Windows разархивируйте загруженный архив и добавьте MAVEN_PATH/bin в переменную окружения PATH, а также создайте переменную окружения JAVA_HOME с путём к каталогу Java (например, C:\Program Files\Java\jdk1.6.0_10). Запустите следующую команду в консоле:

mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart -DgroupId=org.example -DartifactId=myapp -DpackageName=org.example.myapp -DarchetypeVersion=5.0.16-SNAPSHOT -DremoteRepositories=http://tapestry.formos.com/maven-snapshot-repository/

Если это первый запуск Maven, то придётся подождать, пока скачаются необходимые библиотеки. По завершению команды появится каталог myapp. Это и есть первая программа на Tapestry5. Перейдите в каталог myapp и введите следующее:

mvn jetty:run

Проект доступен по адресу http://localhost:8080/myapp. Проект можно импортировать в Eclipse с установленным Maven плагином, для дальнейших экспериментов с фреймворком.

Для подключения примера блога желательно создать среду разработчика. Здесь всё сложнее... у меня пока не было возможности разобраться с Maven и написать собственный установщик. Если вы читаете статью в первый раз, то пропустите весь дальнейший текст и перейдите к следующей главе. Кроме инструкций к действию тут мало чего интересного.

И так, вам потребуются следующие программы:

Все дальнейшее рассматривается на примере Windows, хоть и работаю давно под Linux... но думаю Linux парням будет проще интерпретировать данную документацию для своей системы, чем Windows парням интерпретировать документацию для Linux.

  1. PostgreSQL требует файловую систему в формате NTFS. Во-время инсталляции выберите UTF-8 кодировку базы данных за место WIN1251. После инсталляции, через pgAdmin, создайте роль tapestry-blog с паролем tapestry-blog и базу данных tapestry-blog с соответствующим владельцем.
  2. У Jetty нет инсталлятора, разархивируйте его в каталог к вашим программам.
  3. У Eclipse также нет инсталлятора. После разархивирования, установите плагины Jetty, cкопировав в каталог Eclipse файлы jetty6/contrib/eclipse-generic-wst-plugin/site/features и jetty6/contrib/eclipse-generic-wst-plugin/site/plugins. Они копируются на соответствующие папки Eclipse. Если в вашем дистрибутиве Jetty нет плагинов, то скачайте их здесь.

    При первом запуске Eclipse нужно указать рабочий каталог, где будут размещаться проекты. Запомните этот каталог и поставьте галочку, чтобы больше не беспокоиться об этом.

    После запуска, первым делом зайдите в Windows>Preferences раздел General>Workspace и измените кодировку с Cp1251 на UTF-8.

    Добавьте сервер Jetty через File>New>Other... раздел Server>Server.

  4. Скачайте исходники проекта tapestry-blog.tar.gz и разархивируйте в каталог проектов Eclipse, выбранный вами при первом запуске. Импортируйте проекты tapestry-blog/blog/project и tapestry-blog/common/project через File>Import раздел General>Existing Projects in Workspace.
  5. Скопируйте tapestry-blog/blog/other/servlet-container/jetty/webdefault.xml в jetty6/etc и tapestry-blog/blog/other/servlet-container/jetty/blog.xml в jetty6/context. В blog.xml нужно заменить путь .../tapestry-blog/blog/web на текущий путь к проекту.
  6. Добавьте в WINDOWS/system32/drivers/etc/hosts запись 127.0.0.1 tapestry-blog.

Можно запускать Jetty, найдите вкладку Servers и стартуйте сервер. В браузере проект доступен по адресу http://tapestry-blog:8080 или 8180 для Linux. В Linux, возможно, потребуется изменить параметр -Djetty.host c 127.0.0.1 на 0.0.0.0, для этого два раза кликнете по серверу Jetty на вкладке Servers и откройте Open launch configuration. В разделе Arguments>VM Arguments отредактируйте этот параметр. Для Windows или для Linux рекомендую добавить параметры -Xms192m -Xmx384m (в VM Arguments), тем самым выделив больше памяти для Java, обеспечив более стабильную работу сервера.

Не обращайте внимание на выбрасывание следующего исключения на консоль:

org.hibernate.transaction.JDBCTransaction - exception calling user Synchronization java.lang.NullPointerException

Tapestry5 сейчас beta и там есть небольшие проблемы. В частности, интеграция с Hibernate сделана примитивно. В своих проектах использую собственную интеграцию Tapestry5 и Hibernate, поэтому нет этого исключения, но об этом ниже.

Не рекомендую разрабатывать Tapestry5 проект в Eclipse на Tomcat. В таком случае утрачивается возможность в динамическом подхвате изменений классов, в отличии от Jetty, который делает это без перезагрузки. Не исключаю возможность настройки Tomcat для работы с Tapestry5 без перезагрузки. Например, где-то в рассылке Tapestry User List некие ребята давали советы как хакнуть Tomcat, но у меня не было желания в этом разбираться.

Недавно просматривал документацию на сайте Tapestry5, где нашёл информацию об использовании Jett5 (я работаю с 6 версией), а также некий новый плагин Run-Jetty-Run. Возможно это более простой способ подключить Jetty, но я не успел в этом разобраться.

3. Структура проекта

Автор Tapestry5 рекомендует создать следующую структуру проекта:

Но в проекте блога у меня выработалась собственная структуру. Например, шаблоны находятся в одном пакете с классами. Таким образом в Eclipse удобнее перемещаться от шаблона к классу или наоборот, чем если бы шаблон находился в другом каталоге.

4. Домены

Домены и всё относящееся к ним, находятся в пакетах ru.foror.articles.tapestry.blog.domain и ru.foror.articles.common.domain. В проекте существуют следующие домены: Аккаунт, Статья, Комментарий, Тег, Категория. В текущей реализации комментарии не задействованы в представлении, поэтому вы можете реализовать эту функциональность самостоятельно.

При первом запуске блога, через ProductionOperations, в базе данных создаётся аккаунт с правами администратора, логином и паролем admin. Добавляются несколько статей с тегами и категориями.

Дальнейшая судьба базы данных зависит от параметра hbm2ddl.auto в файле src/hibernate.cfg.xml. Если он выставлен в create, то при следующем запуске сервера база данных будет пересоздана и ProductionOperations заново заполнит таблицы данными. Если в update, то обновится только структура таблиц, в зависимости от структуры доменов.

Hibernate "знает" о расположении доменов, которые он должен отобразить на таблицы базы данных. И при добавлении нового домена с отображением на базу данных нужно выполнить следующие шаги:

  1. Создать класс домена в одном из пакетов ru.foror.articles.tapestry.blog.domain или ru.foror.articles.common.domain.
  2. При необходимости реализовать интерфейсы IWithTags, ISearchResult и т.д.
  3. Добавить аннотации отображающие домен на базу данных.
  4. Добавить аннотации для валидации доменов в базе данных и генерации валидаторов на представлении клиента.
  5. При необходимости добавить аннотацию для кеширования домена.

На примере Article более подробно рассмотрены предыдущие действия:

  1. Помимо простых полей, статья имеет связи многие-ко-многим с тегами, один-ко-многим с комментариями и многие-к-одному с категориями. Автоматически добавляются стандартные get/set, hashCode и equals методы. Пишется дополнительная логика по добавлению комментариев к статье.
  2. Реализуются интерфейсы ISearchResult, IWithTags. По условиям интерфейса ISearchResult на домен добавляются аннотации @Indexed и @Field. Тем самым HibernateSearch получит информацию о том что и как индексировать для поискового индекса Lucene.
  3. Простые поля можно не аннотировать, а для сложных необходимо создать аннотации. @Entity, @Table, @Id, @ManyToOne и другие, отображают домен на базу данных.
  4. Tapestry5 имеет примитивную интеграцию с Hibernate, в том числе нет интеграции с HibernateValidator. Поэтому нужно поддерживать два типа аннотаций. Для Tapestry5 это аннотация @Validate, с помощью которой будет создан код для валидации на клиенте (через JavaScript) и сервере. Для HibernateValidator это аннотации @NotEmpty, @Length, @Pattern и другие, с помощью которых контролируются записи в таблицах базы данных.
  5. Hibernate работает с различными реализациями кеша. В моём случае используется EhCache с возможностью работать в кластере. После прикрепления аннотации @Cache домен автоматически помещается в кеш.

5. Основы Tapestry5

При работе с Tapestry5 часто приходится сталкиваться со страницами, компонентами, подмешиванием, событиями и сервисами. Каждый из этих элементов будет здесь рассмотрен.

5.1. Страница

Страница это обычный класс находящийся в пакете root.pages, где root - имя пакета размещающий классы Tapestry5. В файле web/WEB-INF/web.xml параметр tapestry.app-package задаёт этот пакет. Все классы и шаблоны, находящиеся в этом пакете, автоматически подхватывают вносимые в них изменения, без необходимости перезагружать контейнер сервлетов. Подробнее об этом смотрите в скринкасте.

Для каждой страницы создаётся несколько экземпляров её класса с помещением в пул страниц. Это позволяет использовать одну и туже страницу несколькими пользователями, без необходимости для каждого нового подключения инициализировать и создавать новый объект.

Страница выступает одновременно контроллером и представлением. Имя пакета и название класса определяют как она отобразится на URL пользователя. Например, страница root.pages.admin.grid.Articles отобразится как /admin/grid/articles. А страница root.pages.admin.form.FormArticle отображается как /admin/form/article. Префикс Form используется для создания имени страницы отличного от имени класса домена. Можно создать название страницы без префикса, но в таком случае имя страницы совпадёт с классом домена. И при объявлении переменной домена в странице, придётся писать имя класса с включением пространства имён.

Когда пользователь запрашивает страницу происходит событие activate, которое можно перехватить создав в странице метод onActivate(), либо метод с произвольным именем и аннотацией @OnEvent("activate"). Если для страницы параметры передаются в URL, то в метод добавляется сигнатура соответствующая принимаемым параметрам. Например, на запрос /admin/form/article/123 создаётся метод onActivate(long articleId) (приведение типа происходит автоматически). Если количество параметров неизвестно, то в сигнатуре объявляется Object[], либо специальный класс EventContext. Параметры типа /admin/form/article/?param1=some1¶m2=some2 не обрабатываются, но к ним всегда можно получить доступ через сервис Request. Для сохранения параметров в URL нужно перехватить событие passivate, с возвращением параметров полученных на activate.

 0: public class Category {
 1: 
 2:     ...
 3:     
 4:     @OnEvent("activate")
 5:     void selectArticles(String categoryId) {
 6:         this.categoryId = categoryId;
 7:         articles = articleOp.getArticlesByCategory(categoryId);
 8:     }
 9: 
10:     String onPassivate() {
11:         retrun categoryId;
12:     }
13: }

В простейших случаях используется аннотация @PageActivationContext на поле домена, когда в параметре передают его идентификатор. Домен будет выбран из базы данных, а passivate метод добавится автоматически (в байт-код класса страницы).

 0: public class Category {
 1: 
 2:     ...
 3: 
 4:     @Property
 5:     @PageActivationContext
 6:     private ru.foror.articles.common.domain.Category category;
 7:     
 8:     @OnEvent("activate")
 9:     void selectArticles(String categoryId) {
10:         articles = articleOp.getArticlesByCategory(categoryId);
11:     }
12: }

Страница может существовать без шаблона. В этом случае, она, либо переадресовывает на другие страницы, либо отдает произвольные данные. Например, RSS или поток байт. Для этого метод перехватывающий событие activate должен вернуть опеределённый объект, например, new TextStreamResponse("application/xml", rssData).

Можно создавать страницы не отображающие на URL, а предназначенные для наследования другими страницами. Такие страницы должны располагаться в пакете root.base с шаблоном или без него.

5.2. Шаблон

Шаблон имеет название аналогичное названию класса, для которого создаётся (страница, компонент и т.д.), располагаться в одном пакете и иметь расширение файла tml. Все HTML теги должны быть закрыты, а их параметры обязательно заключены в кавычки.

Для получения данных из объектов, в шаблоне используется конструкция вида ${article.title}. Соответственно в классе (например, страницы) должен быть метод getArticle():

 0: public class PageArticle {
 1: 
 2:   private Article article;
 3: 
 4:   void onActivate(long id) {
 5:     article = ...
 6:   } 
 7: 
 8:   public String getArticle() {
 9:     return article;
10:   }
11: }

А использование аннотации @Property позволяет опустить добавление get/set методов:

0: public class PageArticle {
1: 
2:   @Propertry
3:   private Article article;
4: 
5:   void onActivate(long id) {
6:     article = ...
7:   }
8: }

По умолчанию, в конструкции ${article.title} используется префикс prop, тем самым конструкция ${prop:article.title} аналогична предыдущей. В Tapestry5 есть несколько дополнительных префиксов типа message, literal и других. Для каждого префикса существует класс для обработки конструкции после префикса. Например, ${message:project.name} подставит на место кострукции локализованное сообщение полученное из web/WEB-INF/blog.properties:

project.name=Движок Блога
0: <html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
1: <head>
2:   <title>${message:project.name}</title>
3: </head>
4: <body>
5:   <h1>${article.title}</h1>
6:   <div class="content">${article.body}</div>
7: </body>
8: </html>

Если существующих префиксов недостаточно, то можно создать собственный, включая класс для обработки ваших конструкций. Например, Tapestry4 поддерживает OGNL, с помощью которого в шаблонах можно писать целые программы. Но в Tapestry5 OGNL не был включен в фреймворк.

5.3. Компонент

Компонент, как и страница, является простым классом, но располагается в пакете root.components, автоматически регистрируется в системе - название класса отображается на название компонента в шаблонах и также как страница может не иметь шаблона. Компонент не умеет перехватывать событие activate, а значит не может вернуть поток байт или что-то подобное. Поэтому, компонет без шаблона, либо создаёт контент через специальные сервисы, либо манипулирует HTML блоками, которые он оборачивает, или которые ему передали в параметрах.

Для передачи компоненту параметров используется аннотация @Parameter прикрепляемая к полю в классе компонента. Название поля отображается на его название в шаблоне. А тип переменной определяет значения какого класса можно передать в параметр.

К компоненту можно прикрепить аннотации @IncludeStylesheet и @IncludeJavaScriptLibrary с указанием стилей или скриптов, подключаемые автоматически при использовании этого компонента в других шаблонах.

Компонент Articles
0: public class Articles {
1:   
2:   @Property
3:   @Parameter(required=true)
4:   private List<Article> source;
5: 
6:   @Property
7:   private Article article;
8:     
9: }
Шаблон компонента Articles
0: <ul class="articles" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
1:   <li t:type="loop" source="source" value="article">
2:     <h2><t:pagelink page="Article" context="article.id">${article.title}</t:pagelink></h2>
3:     <div>${article.body}</div>
4:   </li>
5: </ul>
Использование компонента
0: ...
1: <t:articles source="articles"/>
2: ...

Когда компонент оборачивает HTML блок, как компонент Loop в примере, то используется следующая схема для организации цикла (вершины графа это методы в классе компонента Loop):

Когда циклы не нужны, можно поманипулировать блоками контента. Например:

 0: /**
 1:  * Компонент для создания табов.
 2:  * 
 3:  * @author Alexey Pomogaev foror@mail.com
 4:  */
 5: public class Tab {
 6:     
 7:     /**
 8:      * Блок для отображения выбранного таба.
 9:      */
10:     @Parameter(required = true)
11:     private Block selected;
12:     
13:     /**
14:      * Блок для отображения не выбранного таба.
15:      */
16:     @Parameter(required = true)
17:     private Block unselected;
18:     
19:     /**
20:      * Объект идентифицирующий таб.
21:      */
22:     @Parameter(required = true)
23:     private Object to;
24:     
25:     /**
26:      * Объект идентифицирующий текущий выбранный таб.
27:      */
28:     @Parameter(required = true)
29:     private Object choose;
30:     
31:     Block beginRender() {
32:         if (to.equals(choose)) {
33:             return selected;
34:         }
35:         
36:         return unselected;
37:     }
38: }

Шаблон компоненту не нужен, поэтому можно сразу использовать его в других шаблонах:

0: <t:tab to="literal:MENU-ABOUT" choose="currentMenu">
1:     <t:parameter name="selected"><strong>${message:menu.about}</strong></t:parameter>
2:     <t:parameter name="unselected"><t:pagelink page="About">${message:menu.about}</t:pagelink></t:parameter>
3: </t:tab>

Подробнее про компоненты смотрите в скринкасте.

5.4. Подмешивание

Подмешивание позволяет добавлять к компонентам дополнительную функциональность. Лучше всего понять их возможности на примере подмешивания Autocomplete, входящий в Tapestry5 фреймворк. Autocomplete добавляет к компонентам типа TextField (поле ввода текста) функционалость для вывода дополнительных результатов, совпадающие с первыми буквами введенного слова. К HTML коду TextField, через подмешивание, добавляется HTML и JavaScript код Autocomplete, тем самым расширяется функциональность компонента.

0:   <t:textfield 
1:       t:id="additionTags" 
2:       value="additionTags"  
3:       size="50"    
4:       label="message:tag.addition"
5:       t:mixins="autocomplete" 
6:       frequency="0.3" 
7:       tokens="prop:tagDelimeter"/>

Можно, конечно, реализовать компонент с функциональностью текстового поля и подмешивателя. Но когда появляется новое текстовое поле, предположим с другим стилем, предназначенное, например, для ввода определенных типов данных. Ему также может потребоваться функциональность Autocomplete. Придёться дублировать код. Но когда есть подмешивание, нужная функциональность выносится в отдельный класс и шаблон подмешивателя и агрегируется с компонентами, которым она необходима.

Подмешиватель является обычным классом как компоненты и страницы и располагается в пакете root.mixins.

5.5. Событие

В компонентах, подмешивателях и страницах можно создавать и перехватывать события. Также события могут приходить от клиента.

Событие клиента задаётся в URL. Например, /start:myevent. Для перехвата, в странице Start, добавляется метод с аннотацией @OnEvent("myevent"), либо без аннотации, но с названием onMyEvent(). Как и при событии activate, можно передавать в метод параметры из URL и возвращать произвольный контент. Например, на AJAX запрос вернуть JSON объект.

0: public class Start {
1: 
2:     ...
3: 
4:     // будет запущен на запрос http://mysite.ru/start:update/100/article
5:     JSONObject onUpdate(long id, String type) {
6:         return new JSONObject(...);
7:     } 
8: }

События создаваемые в коде используется в основном для уведомления страницы о этапах работы компонента. Например, стандартный компонент Form генерирует события prepare, validate, success, failure и т.д. Их можно перехватить и повлиять на дальнейшую работу системы.

5.6. Сервис

Сервисы это обычные классы подключаемые к странице, компоненту или подмешивателю через аннотацию @Inject и предоставляющие какие-либо услуги. В Tapestry5 есть множество сервисов. Часто используемые из них это Messages позволяющий получить доступ к локализованным сообщениям. ComponentResoureces для создания URL'ов к страницам и событиям или получение ссылок на компоненты в шаблоне. HibernateSessionManager для доступа к сессии Hibernate.

Можно создать собственные сервисы. Как правило это DAO (Data Access Object) сервисы, через которые страницы работают с базой данных.

Для сохранения глобального состояния в сессии пользователя используется сервис ApplicationStateManager. На самом деле лучше использовать аннотацию @ApplicationState на полях, которые будут инициализироваться данными из сессии. А при их изменении новые данные запишутся обратно в сессиию. Но если нужно сохранить поле в пространстве страницы, компонента или подмешивателя, то лучше использовать аннотацию @Persist.

 0: public abstract class LoginFormBase {
 1:     
 2:     @InjectComponent
 3:     private Form loginFormPanel;
 4:     
 5:     @Inject
 6:     private IAuthHelper authHelper;
 7:     
 8:     @Inject
 9:     private ILoginOperations op;
10:     
11:     @Inject
12:     private Cookies cookies;
13:     
14:     @Persist("flash")
15:     private String nickname;
16:     
17:     @Persist("flash")
18:     private String password;
19:     
20:     @Persist("flash")
21:     private boolean cookie;
22: 
23:     ...
24: }

6. Inversion of Control

IoC связывает большинство классов Tapestry5, через него настраиваются классы генерирующие байт-код для страниц и компонентов, создаётся глобальная конфигурация системы, связываются валидаторы с их классами, добавляются преобразователи к простым типам и много другое. Для полной картины рекомендую посмотреть класс TapestryModule в tapestry-core.jar.

Наличие в Tapestry5 собственного IoC движка позволяет не подключать к проекту Spring. Для тех, кто не в курсе об IoC рекомендую прочитать об этом здесь.

Настройка IoC выполняется в специальных классах-модулях. В tapestry-common это ru.foror.articles.common.tapestry.ioc.Module, а в tapestry-blog ru.foror.articles.tapestry.blog.ioc.Module.

Для создания простейших сервисов используется SerivceBinder:

0: public class Module {
1:     
2:     public static void bind(ServiceBinder binder) {
3:         binder.bind(IAccountOperations.class, AccountOperations.class);
4:         binder.bind(IArticleOperations.class, ArticleOperations.class);
5:         binder.bind(IProductionOperations.class, ProductionOperations.class);
6:     }
7: }

Первый параметр это интерфейс, а второй класс реализующий интерфейс. Когда класс добавлен к сервисам, то в нём можно создавать конструктор с интерфейсами других сервисов. При создании сервиса, IoC самостоятельно найдёт нужный коструктор и необходимые сервисы подставятся автоматически:

 0: public class AccountOperations implements IAccountOperations {
 1: 
 2:     private HibernateSessionManager sm;
 3:     
 4:     private IAuthHelper authHelper;
 5:     
 6:     private IUniversalOperations universalOp;
 7:     
 8:     public AccountOperations(HibernateSessionManager sm, IAuthHelper authHelper,
 9:             IUniversalOperations universalOp) {
10:         this.sm = sm;
11:         this.authHelper = authHelper;
12:         this.universalOp = universalOp;
13:     }
14: 
15:     ...
16: }

Если нужно самостоятельно инициализировать сервис, то создайте метод с префиксом build и названием сервиса:

 0: public class Module {
 1: 
 2:     public Configuration buildConfiguration(
 3:             @Inject @Symbol("common.config-path") 
 4:             String configPath,
 5:             
 6:             @Inject
 7:             ApplicationGlobals g) {
 8:         
 9:         try {
10:             String path = g.getServletContext().getRealPath(configPath);
11:             return new XMLConfiguration(path);
12:         } catch (ConfigurationException e) {
13:             throw new RuntimeException(e);
14:         }
15:     }
16: 
17:     ...
18: }

Иногда нужно дополнить конфигурацию сервиса, который инициализируется в другом IoC модуле. Например, нужно к Hibernate добавить слушателя на удаление статьи. Сервис инициализирующий конфигурацию Hibernate находится в другом модуле и модифицировать его не очень удобно. Поэтому делаем следующее:

 0: public class Module {
 1: 
 2:     public static void contributeHibernateSessionSource(
 3:             OrderedConfiguration<HibernateConfigurer> config,        
 4:             
 5:             @Inject
 6:             final ITagOperations tagOp) {
 7:         
 8:         config.add("DeleteEventContrb", new HibernateConfigurer() {
 9:             public void configure(Configuration configuration) {
10:                 DeleteEventListener[] stack = {new DeleteArticle(tagOp), new DefaultDeleteEventListener()};
11:                 configuration.getEventListeners().setDeleteEventListeners(stack);
12:             }
13:         });
14:     }
15: 
16:     ...
17: }

Класс реализующий сервис HibernateSessionSource имеет следующий коструктор:

0: public class HibernateSessionSourceImpl implements HibernateSessionSource {
1: 
2:     public HibernateSessionSourceImpl(Logger logger, List<HibernateConfigurer> hibernateConfigurers) {
3:         ...
4:     }
5: 
6:     ...
7: }

В hibernateConfigurers содержится список объектов класса HibernateConfigurer, в том числе и наш аннонимный объект. По списку выполняется итерация с запуском метода configure(Configuration configuration), в который передаётся конфигурация Hibernate для её дополения. Когда дойдёт очередь до нашего анонимного объекта, выполнится метод и добавится слушатель на удаление статьи.

Подобным образом можно дополнять сервисы генерирующие байт-код для страниц, компонетов или сервисов. Начиная от работы с аннотациями и заканчивая генерацией собственного байт-кода.

7. Аутентификация и авторизация

В Tapestry5 нет собственной системы авторизации пользователя. Но с помощью IoC можно создать декораторы для перехвата сервисов активирующие страницы. Для этого в IoC модуль tapestry-common добавляются следующие методы:

 0:     /**
 1:      * Декоратор для перехвата запросов к страницам Tapestry.
 2:      */
 3:     public PageRenderRequestHandler decoratePageRenderRequestHandler(
 4:             Object delegate,
 5:             
 6:             @Inject
 7:             IAuthHelper authHelper,
 8:              
 9:             @Inject
10:             ApplicationStateManager appStateManager,
11:             
12:             @Inject
13:             Response response,
14:             
15:             @Inject
16:             LinkFactory linkFactory,
17:             
18:             @Inject
19:             RequestPageCache cache) {
20:         
21:         return new SecurityHandler((PageRenderRequestHandler) delegate, authHelper, 
22:                 appStateManager, response, linkFactory, cache);
23:     }
24:     
25:     /**
26:      * Декоратор для перехвата запросов к страницам вызванных через события Tapestry.
27:      */
28:     public ComponentEventRequestHandler decorateComponentEventRequestHandler(
29:             Object delegate,
30:             
31:             @Inject
32:             IAuthHelper authHelper,
33:              
34:             @Inject
35:             ApplicationStateManager appStateManager,
36:             
37:             @Inject
38:             Response response,
39:             
40:             @Inject
41:             LinkFactory linkFactory,
42:             
43:             @Inject
44:             RequestPageCache cache) {
45:         
46:         return new SecurityHandler((ComponentEventRequestHandler) delegate, authHelper, 
47:                 appStateManager, response, linkFactory, cache);
48:     }

Класс SecurityHandler в методе authorization(Page page) выполняет действия для авторизации пользователя. К каждой странице необходимо добавить аннотацию @Meta. Её метаданные содержат названия ролей определяющие доступ к странице. Например, для разрешения доступа гостям добавляется аннотация @Meta{"anonym=true"}. Пользователь без прав доступа перенаправляется, через метод sendRedirect, на специальную страницу с уведомлением об отсутствии прав.

8. Сторонние библиотеки

В Tapestry5 очень просто подключать сторонние библиотеки. Для этого создаётся JAR архив с классами и ресурсами библиотеки и копируется к библиотекам проекта. Компоненты и подмешиватели получают собственное пространство имен, а страницы отображаются на URL с префиксом пространства имён. Домены автоматически подхватываются Hibernate. Все IoC модули - проекта и подключаемых библиотек объединяются в один. Тем самым, все страницы сторонних библиотек будут авторизоваться также как и другие страницы проекта (см. выше про декораторы перехватывающие активацию страницы).

В примере блога подключается библиотека tapestry-common на пространство имён common. Она сделана специально для этого примера и имеет лишь часть моих наработок для Tapestry5. Например, здесь нет интеграции HibernateValidator с Tapestry5.

Пространство имён конфигурируется в модуле библиотеки:

0: public void contributeComponentClassResolver(Configuration<LibraryMapping> configuration) {
1:     configuration.add(new LibraryMapping("common", "ru.foror.articles.common.tapestry"));
2: }

9. Страницы администратора

9.1. Компонент BeanEditor

Позволяет на основе класса домена построить форму для редактирования или создания нового домена. Поля на такой форме генерируются только для простых типов (String, Integer и т.д.). Коллекции или агрегация других классов не поддерживается. В этом случае, код для их обработки добавляется вручную. А при необходимости можно переопределить простые поля. Например, для добавления WYSIWYG редактора на поле с типом String.

 0: <t:beaneditform t:id="article" object="article" add="category,tags,additionTags" exclude="created">
 1: <t:parameter name="body">
 2:   <t:label for="body"/>
 3:   <t:common.wymeditortoggler object="article" property="body"/>
 4:   <t:common.wymeditor t:id="body" value="article.body"/>  
 5: </t:parameter>
 6: <t:parameter name="category">
 7:   <t:label for="category"/>
 8:   <t:select 
 9:       t:id="category" 
10:       encoder="categoryEncoder"
11:       model="categoryModel"
12:       value="article.category"
13:       blankOption="ALWAYS"
14:       validate="required"/>
15: </t:parameter>
16: <t:parameter name="tags">
17:   <t:label for="tags"/>
18:   <t:common.multiselect 
19:       t:id="tags" 
20:       encoder="tagEncoder" 
21:       model="tagModel"
22:       value="tags"
23:       label="message:tag.plural"
24:       size="10"/>
25: </t:parameter>
26: <t:parameter name="additionTags">
27:   <t:label for="additionTags"/>
28:   <t:textfield 
29:       t:id="additionTags" 
30:       value="additionTags"  
31:       size="50"    
32:       label="message:tag.addition"
33:       t:mixins="autocomplete" 
34:       frequency="0.3" 
35:       tokens="prop:tagDelimeter"/>
36: </t:parameter>
37: </t:beaneditform>

Подробнее смотрите в скринкасте.

9.2. Компонент Grid

Grid создаётся список доменов с сортировкой и постраничным выводом. Операции сортировки и постраничного вывода можно делать через AJAX, задав параметр компонента inPlace="true". Как и в BeandEditor все поля генерируются автоматически, но можно переопределить любое поле создав другую логику вывода. Некоторые поля можно скрыть с помощью аннотации @NonVisual в классе домена, либо через параметр компонента exclude.

 0: <t:grid source="articles" row="article" inPlace="true" add="edit,delete" exclude="body">
 1:   <t:parameter name="titlecell">
 2:     <t:pagelink page="Article" context="article.id">${article.title}</t:pagelink>
 3:   </t:parameter>
 4:   <t:parameter name="editcell">
 5:     <t:pagelink page="admin/form/Article" context="article.id">${message:content.edit}</t:pagelink>
 6:   </t:parameter>
 7:   <t:parameter name="deletecell">
 8:     <t:actionlink t:id="delete" context="article.id">${message:content.delete}</t:actionlink>
 9:   </t:parameter>
10: </t:grid>

Подробнее смотрите в скринкасте.

10. Тестирование

В Tapestry5 есть собственный пакет для тестирования страниц. Он работает без контейнера сервлетов позволяя быстрее производить тестирование. Но в тоже время есть ограничения при работе с формами и JavaScript. А из-за работы вне контейнера сервлетов могут возникнуть проблемы в низкоуровневых операциях на уровне сервлетов. Поэтому лучше использовать либо библиотеку HtmlUnit, либо Selenium. Кстати, Tapestry5 использует Selenium для собственного тестирования при сборке через Maven.

11. Заключение

В последнее время, то тут, то там слышу разговоры о смерти Java. Что же, если собрались хоронить Java, то заодно хороните С, С++, да и C#, чего уж там. Но затем задумался, почему об этом говорят (обычно не вчитываюсь в такие вещи) и тут снизошло озарение — хоронят не Java, а J2EE! И знаете, тут я соглашусь. J2EE отпугивает новичков делать веб на Java - этакий мега-монстр корпоративной среды. Именно его навязывают новичкам, как самую правильную технологию для веба на Java. Но фреймворки типа Tapestry или Wicket дают возможность не заморачиваться на J2EE, делая проекты легкими в понимании для тех кто пришел с PHP. Их возможности на уровне Django и RubyOnRails позволяют также просто создавать веб на Java.

Тем не менее, Tapestry5 не охватывает такие аспекты как аутентификации, авторизация, генерация системы управления доменами как в Django и не создаёт набор стандартных страниц. С одной стороны это хорошо. Разработчик может сделать проект более гибким, особенно с учетом возможностей Tapestry5 и её IoC движка, где можно перехватить, дополнить и написать собственную реализацию сервисов фреймворка. Но с другой стороны, хочется иметь набор стандартных страниц и сервисов, от которых можно будет отталкиваться при реализации собственных.

У меня есть подобная реализация, но она писалась, зачастую, на костылях и без хорошей документации, во-время, когда я не до конца понимал возможности Tapestry5. Есть желание скомпоновать всё это заново: очевидный бред выкинуть, костыли поправить, посыпать код комментариями, сделать качественную интеграцию с Hibernate, а в вики составить хорошую документацию. Но у меня нет возможности делать это на энтузиазме. Поэтому создал фонд на собственном проекте GetDone.ru. Если удастся получить достаточное финансирование, то в первую очередь займусь этим проектом.

Также ищу добровольцев для перевода статьи на английский язык. А если удастся найти финансирование, то и дальнейшая работа по переводу документации.