Два года назад (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.
При использовании 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.
У 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.
Можно запускать 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, обеспечив более стабильную работу сервера.
Не обращайте внимание на выбрасывание следующего исключения на консоль:
Tapestry5 сейчас beta и там есть небольшие проблемы. В частности, интеграция с Hibernate сделана примитивно. В своих проектах использую собственную интеграцию Tapestry5 и Hibernate, поэтому нет этого исключения, но об этом ниже.
Не рекомендую разрабатывать Tapestry5 проект в Eclipse на Tomcat. В таком случае утрачивается возможность в динамическом подхвате изменений классов, в отличии от Jetty, который делает это без перезагрузки. Не исключаю возможность настройки Tomcat для работы с Tapestry5 без перезагрузки. Например, где-то в рассылке Tapestry User List некие ребята давали советы как хакнуть Tomcat, но у меня не было желания в этом разбираться.
Недавно просматривал документацию на сайте Tapestry5, где нашёл информацию об использовании Jett5 (я работаю с 6 версией), а также некий новый плагин Run-Jetty-Run. Возможно это более простой способ подключить Jetty, но я не успел в этом разобраться.
Автор Tapestry5 рекомендует создать следующую структуру проекта:
Но в проекте блога у меня выработалась собственная структуру. Например, шаблоны находятся в одном пакете с классами. Таким образом в Eclipse удобнее перемещаться от шаблона к классу или наоборот, чем если бы шаблон находился в другом каталоге.
/ корень проекта
production/ файлы заменяющие файлы разработчика при сборке проекта
src/ исходный код
targets/ модули для аnt скриптов
test/ тесты
web/ ресурсы проекта
WEB-INF/
other/
Домены и всё относящееся к ним, находятся в пакетах ru.foror.articles.tapestry.blog.domain и ru.foror.articles.common.domain. В проекте существуют следующие домены: Аккаунт, Статья, Комментарий, Тег, Категория. В текущей реализации комментарии не задействованы в представлении, поэтому вы можете реализовать эту функциональность самостоятельно.
При первом запуске блога, через ProductionOperations, в базе данных создаётся аккаунт с правами администратора, логином и паролем admin. Добавляются несколько статей с тегами и категориями.
Дальнейшая судьба базы данных зависит от параметра hbm2ddl.auto в файле src/hibernate.cfg.xml. Если он выставлен в create, то при следующем запуске сервера база данных будет пересоздана и ProductionOperations заново заполнит таблицы данными. Если в update, то обновится только структура таблиц, в зависимости от структуры доменов.
Hibernate "знает" о расположении доменов, которые он должен отобразить на таблицы базы данных. И при добавлении нового домена с отображением на базу данных нужно выполнить следующие шаги:
На примере Article более подробно рассмотрены предыдущие действия:
При работе с Tapestry5 часто приходится сталкиваться со страницами, компонентами, подмешиванием, событиями и сервисами. Каждый из этих элементов будет здесь рассмотрен.
Страница это обычный класс находящийся в пакете 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 с шаблоном или без него.
Шаблон имеет название аналогичное названию класса, для которого создаётся (страница, компонент и т.д.), располагаться в одном пакете и иметь расширение файла 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:
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 не был включен в фреймворк.
Компонент, как и страница, является простым классом, но располагается в пакете root.components, автоматически регистрируется в системе - название класса отображается на название компонента в шаблонах и также как страница может не иметь шаблона. Компонент не умеет перехватывать событие activate, а значит не может вернуть поток байт или что-то подобное. Поэтому, компонет без шаблона, либо создаёт контент через специальные сервисы, либо манипулирует HTML блоками, которые он оборачивает, или которые ему передали в параметрах.
Для передачи компоненту параметров используется аннотация @Parameter прикрепляемая к полю в классе компонента. Название поля отображается на его название в шаблоне. А тип переменной определяет значения какого класса можно передать в параметр.
К компоненту можно прикрепить аннотации @IncludeStylesheet и @IncludeJavaScriptLibrary с указанием стилей или скриптов, подключаемые автоматически при использовании этого компонента в других шаблонах.
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: }
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>
Подробнее про компоненты смотрите в скринкасте.
Подмешивание позволяет добавлять к компонентам дополнительную функциональность. Лучше всего понять их возможности на примере подмешивания 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.
В компонентах, подмешивателях и страницах можно создавать и перехватывать события. Также события могут приходить от клиента.
Событие клиента задаётся в 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 и т.д. Их можно перехватить и повлиять на дальнейшую работу системы.
Сервисы это обычные классы подключаемые к странице, компоненту или подмешивателю через аннотацию @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: }
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 для её дополения. Когда дойдёт очередь до нашего анонимного объекта, выполнится метод и добавится слушатель на удаление статьи.
Подобным образом можно дополнять сервисы генерирующие байт-код для страниц, компонетов или сервисов. Начиная от работы с аннотациями и заканчивая генерацией собственного байт-кода.
В 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, на специальную страницу с уведомлением об отсутствии прав.
В 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: }
Позволяет на основе класса домена построить форму для редактирования или создания нового домена. Поля на такой форме генерируются только для простых типов (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>
Подробнее смотрите в скринкасте.
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>
Подробнее смотрите в скринкасте.
В Tapestry5 есть собственный пакет для тестирования страниц. Он работает без контейнера сервлетов позволяя быстрее производить тестирование. Но в тоже время есть ограничения при работе с формами и JavaScript. А из-за работы вне контейнера сервлетов могут возникнуть проблемы в низкоуровневых операциях на уровне сервлетов. Поэтому лучше использовать либо библиотеку HtmlUnit, либо Selenium. Кстати, Tapestry5 использует Selenium для собственного тестирования при сборке через Maven.
В последнее время, то тут, то там слышу разговоры о смерти Java. Что же, если собрались хоронить Java, то заодно хороните С, С++, да и C#, чего уж там. Но затем задумался, почему об этом говорят (обычно не вчитываюсь в такие вещи) и тут снизошло озарение — хоронят не Java, а J2EE! И знаете, тут я соглашусь. J2EE отпугивает новичков делать веб на Java - этакий мега-монстр корпоративной среды. Именно его навязывают новичкам, как самую правильную технологию для веба на Java. Но фреймворки типа Tapestry или Wicket дают возможность не заморачиваться на J2EE, делая проекты легкими в понимании для тех кто пришел с PHP. Их возможности на уровне Django и RubyOnRails позволяют также просто создавать веб на Java.
Тем не менее, Tapestry5 не охватывает такие аспекты как аутентификации, авторизация, генерация системы управления доменами как в Django и не создаёт набор стандартных страниц. С одной стороны это хорошо. Разработчик может сделать проект более гибким, особенно с учетом возможностей Tapestry5 и её IoC движка, где можно перехватить, дополнить и написать собственную реализацию сервисов фреймворка. Но с другой стороны, хочется иметь набор стандартных страниц и сервисов, от которых можно будет отталкиваться при реализации собственных.
У меня есть подобная реализация, но она писалась, зачастую, на костылях и без хорошей документации, во-время, когда я не до конца понимал возможности Tapestry5. Есть желание скомпоновать всё это заново: очевидный бред выкинуть, костыли поправить, посыпать код комментариями, сделать качественную интеграцию с Hibernate, а в вики составить хорошую документацию. Но у меня нет возможности делать это на энтузиазме. Поэтому создал фонд на собственном проекте GetDone.ru. Если удастся получить достаточное финансирование, то в первую очередь займусь этим проектом.
Также ищу добровольцев для перевода статьи на английский язык. А если удастся найти финансирование, то и дальнейшая работа по переводу документации.