Введение
В этой статье я хотел бы рассказать о маленьком исследовании по скрещиванию GWT и Webtop.
Вначале отвечу на законный вопрос: «Зачем это нужно?». Я преследовал несколько целей:
1) Изучить GWT.
2) Более детально изучить внутренности WDK и Webtop.
3) Возможно, найти способ создавать GWT компоненты для Webtop.
GWT был выбран потому что позволяет быстро создавать UI и клиентский код Java программистам. А так же поддерживает AJAX «из коробки».
Использовалась именно связка с Webtop, а не создание своего приложение с нуля, так как это заняло бы больше времени (поддержка Documentum сессий, изобретение замены action и precondition) и не позволило бы использовать уже написанный код на Webtop.
Граничные условия
По логике работы с объектами:
CRUD, бизнес-операции над объектами.
Возможность работы с action и, как следствие, precondition Webtop’s. Т.е. должен быть обеспечен способ вызова action и проверки precondition для action со стороны клиента.
По содержимому объектов:
Максимум 100 атрибутов. Максимум 100 repeating значений атрибута.
По UI:
GWT должен встраиваться в существующее решение на Webtop. Хотя бы на уровне отдельного компонента. Т.е. должна быть возможность вызова GWT компонента из простого Webtop компонента. (Под словом “компонент” понимается стандартный Webtop component) А также должен быть способ выхода из GWT компонента назад в вызывающий его компонент.
Замечание:
Ниже будут перечислены основные аспекты GWT, которые я буду использовать. Хочу обратить ваше внимание на то, что я не буду детально рассматривать работу с каждой технологией – статья не об этом. Если вы хотите прочитать о чём-то подробнее – ссылки в конце статьи.
UI
Для создания UI пробовал сначала простой способ GWT – a la Swing, когда widget’s создаются прямо в коде. Получилось неплохо – весь код в одном файле и сразу видно, что где создаётся. Но когда форма становится довольно большой (хотя бы больше 100 строк) и на форме появляются множество слушателей (Listener’s) событий, то код становится перегруженным и сложным для чтения.
В противовес созданию widget’s из кода GWT предлагает другой механизм – UI Builder. Основное отличие — UI описывается отдельно от логики в xml файле. Из минусов UI Builder – не все widget’s доступные в GWT доступны в палитре компонентов UI Builder. Я бы даже сказал, что поддержка widget’s в Window Builder очень ограничена. Так что в итоге если нужно сделать что то специфическое (хотя бы добавить колонку в таблицу) – придётся писать код.
Получения объектов клиентом
Всю работу с объектами Documentum будет осуществлять через вызов серверных методов. Осталось только определиться, как организовать эту работу. Первый очевидный способ – использовать стандартный RPC из GWT. Просто создаём нужный сервис, который будет отвечать за обработку какого либо объекта БД – например, сервис по работе с объектами «сотрудник» и сервис по работе с объектами «организация». Объекты в данном случае – POJO, которые передаются между клиентом и сервером. В статьях по GWT есть заметка, как организовать работу с Hibernate developers.google.com/web-toolkit/articles/using_gwt_with_hibernate?hl=ru-RU
Там рассматривается похожий способ коммуникации. Отличие (помимо того, что используется Hibernate) в использовании DTO – data transfer object. Следуя из названия и из wiki это промежуточные объекты для передачи сущностей БД клиенту. Нужны они для того, что бы минимизировать отправку ненужных данных клиенту. Т.е. есть сущность, содержащая 100 атрибутов, но использовать клиент будет только 10. Вот как раз создаётся специальный DTO для нужной сущности с этими 10 полями.
Плюсы этого способа – очень прост для понимания и реализации.
Минусы – использование DTO или, в противном случае, потенциальная передача большого количества не нужных атрибутов объекта клиенту.
Второй способ – Request Factory. Это альтернатива RPC, предназначенная как раз для выполнения CRUD операций над объектами. Основное отличие от простого вызова RPC заключается в том, что объекты, над которыми происходят операции, сразу разнесены на две категории – объекты, которыми манипулирует сервер и объекты-представление (наследники EntityProxy), которыми манипулирует клиент. В принципе, Proxy-объекты являются не чем иным, как DTO. Просто в данном случае преобразование основного объекта в DTO занимается GWT.
Дискуссия на StackOverflow о RPC и RequestFactory stackoverflow.com/questions/4119867/when-should-i-use-requestfactory-vs-gwt-rpc
Способ конечно интересный, но требует написания дополнительного кода. К тому же мне не понравилась не явная зависимость между методами наследников RequestContext и объектами, реализующими эти методы, а так же между getter & setters Proxy объектов и самими объектами. Хотя для первого случая GWT использует модуль проверки для RequestFactory который находит несоответствия на этапе компиляции.
Передача параметров между Webtop и GWT
Поискав, нашёл несколько способов передачи параметров в GWT – через JSP (вставка значений в JS с последующим парсингом), через url (парсинг get запроса) и с помощью сервлета, который будет возвращать параметры.
Для Webtop неплохо подойдёт вариант с JSP – так как в компоненте предполагается наличие jsp страницы, которая как раз будет содержать в себе код вставки параметров, а компонент позволит пробросить эти параметры в JSP.
Для этого в jsp нужно добавить этот код:
<script type="text/javascript">
var wdk2gwt_param = '${wdk2gwtParam}';
</script>
Здесь описана JS переменная wdk2gwt_param которой будет присвоено значение атрибута wdk2gwtParam.
А в код компоненты для записи параметров написать это:
@Override
public void onRender() {
getPageContext().setAttribute(“wdk2gwtParam”, “Значение переменной”);
super.onRender();
}
Здесь при выводе страницы присваиваем атрибуту wdk2gwtParam, который был выше описан в jsp, значение.
Для возможности выхода из компонента нужно добавить метод обработки события:
public void onReturnClick(Control control, ArgumentList list) {
setComponentReturn();
}
Здесь вызывается стандартный метод компонента (setComponentReturn()) который позволит вернуться к вызвавшему его компоненту.
А в GWT, указанное выше, использовать с помощью следующих методов:
public native String getParam()
/*-{
return $wnd.wdk2gwt_param;
}-*/;
public native void callPostServerEvent()
/*-{
$wnd.postServerEvent(null, null, null, ' onReturnClick ', null, null);
}-*/;
Здесь представлен стандартный способ вызова JS кода из Java кода. В методе getParam() просто возвращается значение переменной wdk2gwt_param (которая была указана в jsp). В методе callPostServerEvent() вызывается стандартная JS функция WDK предназначенная для вызова методов компоненты. В данном случае вызывается метод onReturnClick, который был описан выше. Для работы JS функции postServerEvent её сначала нужно определить. Для этого в jsp должны быть указаны следующие теги: <dmf:webform /> и <dmf:form>
В связи в этом полный код jsp будет выглядеть так:
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="/WEB-INF/tlds/dmform_1_0.tld" prefix="dmf"%>
<html>
<head>
<dmf:webform />
<title></title>
</head>
<body>
<dmf:form>
<script type="text/javascript">
var wdk2gwt_param = '${wdk2gwtParam}';
</script>
<script language="javascript" src="/Webtop/ru.tim.gwt.correspondent.Correspondent/ru.tim.gwt.correspondent.Correspondent.nocache.js"></script>
</dmf:form>
</body>
</html>
Работа с сессиями
В качестве работы с Documentum сессиями будем использовать SessionManagerHttpBinding. Он позволяет получить экземпляр IDfSessionManager текущего пользователя. Из которого можно получить саму сессию.
IDfSessionManager sessionManager = SessionManagerHttpBinding.getSessionManager();
IDfSession session = null;
try {
session = sessionManager.getSession(SessionManagerHttpBinding.getCurrentDocbase());
} catch (Exception e) {
e.printStackTrace();
} finally {
sessionManager.release(session);
}
Это код можно использовать в сервлетах GWT для получения и, последующего закрытия сессии. Конечно, нужно сделать правильный вывод в log и добавить проверку на null перед обращением к sessionManager.
Выполнение action WDK
Нужно для выполнения action и проверки precondition. Use case – проверить что указанный action с перечисленными параметрами выполняется и выполнить его.
Первая идея, которая пришла на ум, — провести reverse engineering вызовов формы. (Доков в сети нет). Покопавшись в коде, проследил цепочку вызовов методов. В итоге, для получения формы и контекста нужно много что сделать.
Второй вариант – AJAX средствами Documentum community.emc.com/docs/DOC-8012 — так называемые inline вызовы.
Т.е. суть такова – делать метод в компоненте, который будет вызываться асинхронно из клиента с параметрами – имя action и список параметров action. А результат выполнения возвращаться клиенту. Тут есть одно замечание — если в action используется методы перехода на другие компоненты: setComponentReturn, setComponentJump, setComponentNested (возможно ещё и другие), то при повторном запросе к методам компоненты будет ошибка (Component Configuration Base has not been established). Скорее всего это связано с тем, что компонент вышел из текущего состояния (перешёл на другой компонент), но при запросе серверу в параметрах указан уже устаревшее состояние компонента. Точнее, в параметрах указывается уникальная строка в пределах текущей сессии, по которой сервер восстанавливает состояние компонента. И это состояние устарело.
В принципе, более-менее рабочее решение уже найдено. Но я ещё решил проверить метод onaction класса Component. Это метод, который выполняет все action своей формы (это можно заметить по stack trace, если поставить break point в action и вызывать его). Если быть точным – этот метод вызывают все стандартные action control’s формы. Это обыкновенный метод компонента, который можно вызывать извне. И этот метод поддерживает inline вызовы. Так вот, для вызова этого метода нужно передать ему имя action и параметры для него. При первом вызове обычного action получаем ошибку – action должен реализовывать интерфейс IInlineCapableAction. Исправив это, получится успешно вызвать action. Для передачи значения из action нужно воспользоваться Map для возврата значение (6-й параметр IActionExecution. execute) и поместить туда значение с ключом RESPONSE_DATA.
Полностью код выглядит так:
Содержимое xml с описанием action.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<config version="1.0">
<scope>
<action id="test_action">
<params>
<param name="test_param" required="false"/>
</params>
<execution class="ru.tim.wdk.test.TestAction"/>
</action>
</scope>
</config>
Обратите внимание – action принимает параметр test_param.
Метод action который будет вызываться асинхронно.
public boolean execute(String actionName, IConfigElement config, ArgumentList args, Context context, Component component, Map resultMap) {
System.out.println("Action param: " + args.get("test_param"));
resultMap.put("RESPONSE_DATA", "actionResultValue");
return true;
}
Здесь выводиться параметр test_param. Выше он был описан в xml. Для вывода ответа в map результата помещается ответ actionResultValue с ключом RESPONSE_DATA. RESPONSE_DATA – жёстко закодированный ключ ответа для inline вызовов action. Я его нашёл при просмотре кода метода onaction. При inline вызове action из класса обработчика (CallbackDoneListenerWrapper) берётся значение из Map по ключу RESPONSE_DATA. При отправке ответа клиенту используется так же ключ RESPONSE_DATA (метод processInlineActionResponseData в классе Component).
Часть jsp страницы с методами отправки и получения результата выполнения action:
<script type="text/javascript">
function executeAction() {
var prefs = InlineRequestEngine.getPreferences(InlineRequestType.JSON);
prefs.setCallback(callBack);
postInlineServerEvent(null, prefs, null, null, "onaction", "action",
"test_action", "test_param", "test_param_value");
}
function callBack(data) {
if (isEventPostingLocked()) {
releaseEventPostingLock();
}
if (data) {
var result = data['RESPONSE_DATA']
window.alert(result);
}
}
</script>
Метод executeAction выполняет асинхронный запрос на сервер для выполнения action. Для обработки ответа используется функция callBack.
В методе postInlineServerEvent происходит вызов метода выполнения action – onaction. С параметрами – action, в параметре передаётся название вызываемого action. test_action – название вызываемого action. test_param – название параметра action. test_param_value – значение параметра. Если нужно добавить ещё параметров, то их надо дальше перечислять парами — <имя параметра>, <значение параметра>.
Для использования action из GWT нужно обвернуть вызов функции postInlineServerEvent из GWT метода, а функции setCallback прокинуть в качестве параметра функцию GWT.
Теперь осталось реализовать метод проверки precondition для указанных action.
Можно это сделать с помощью метода в компоненте, который будет выполнять precondition для указанных action. Сами precondition нужно выполнить с помощью функции ActionService.queryExecute(strAction, args, context, component)
Настройка среды
Для удобной работы с Webtop и Eclipse создадим Gwt проект. В org.eclipse.wst.common.project.facet.core.xml проекта (находится в папке .settings в папке проекта) добавим следующие строчки
<fixed facet="jst.web"/>
<fixed facet="java"/>
<fixed facet="wst.jsdt.web"/>
<installed facet="java" version="1.6"/>
<installed facet="jst.web" version="2.5"/>
<installed facet="wst.jsdt.web" version="1.0"/>
В итоге должно получиться следующее:
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<fixed facet="jst.utility"/>
<fixed facet="java"/>
<fixed facet="jst.web"/>
<fixed facet="wst.jsdt.web"/>
<installed facet="java" version="1.6"/>
<installed facet="jst.web" version="2.5"/>
<installed facet="wst.jsdt.web" version="1.0"/>
<installed facet="jst.utility" version="1.0"/>
</faceted-project>
Здесь мы указали, что проект будет web проектом и его можно запускать в Tomcat. Добавим в окне Java Build Path папку classes Weptop и Web App Libraries. Настроим Deployment assembly для выгрузки папки с исходниками gwt и webtop проекта и папок с web контентом gwt и weptop.
Осталась только одна проблема – настройка web.xml для возможности запуска gwt отдельно от Webtop. Так как сейчас web.xml включает в себя настройку и servlet’s gwt и servlet’s Webtop. Как вариант, можно поместить все настройки в один web.xml Webtop и выгружать только его. Для gwt оставить только его настройки. Только нужно соблюсти правильную очерёдность выгрузки – сначала выгружается Webtop, а потом gwt. При этом данные перезаписываться не будут.
Реализация
Для тестирования я сделал небольшой компонент, который выводит объект определённого типа и позволяет редактировать несколько single полей. Исходники можно скачать от сюда dl.dropbox.com/u/7519092/gwt2wdk.rar (для работы нужен Webtop)
Заключение
Основы взаимодействия между GWT и Webtop получилось сделать. Но для полноценной реализации шаблонной карточки редактирования компонента нужно сделать несколько вещей:
1) Форму с выбором объекта. В Webtop она называется selector.
2) Widget’s с поддержкой action, что бы можно было конфигурировать их для запуска action, а не писать для этого код.
3) Подключаемые компоненты для редактирования определённых атрибутов. Для примера, в Webtop их можно использовать в компоненте Properties.
Плюс ещё несколько вещей относительно GWT – проверка ввода на стороне клиента, правильное разграничение UI и событий.
Links
RequestFactory Manual developers.google.com/web-toolkit/doc/latest/DevGuideRequestFactory?hl=ru-RU#locators
RequestFactory Example (1) cleancodematters.wordpress.com/2011/06/04/tutorial-gwt-request-factory-part-i/
RequestFactory Example (2) javaasylum.blogspot.com/2010/11/gwt-21-request-factory.html
RequestFactory Example (3) turbomanage.wordpress.com/2011/03/25/using-gwt-requestfactory-with-objectify/
Автор: TimReset