my.life.logging.Blog

Создание портлета для Liferay с помощью Wicket. Часть 3. Разработка функциональности портлета.

Данное руководство детально описывает процесс создания портлета для Liferay портала cредствами фреймворка Wicket.

Все части руководства:

  1. Портлетный проект для Liferay портала
  2. Конфигурация Wicket-портлета, или web.xml, portlet.xml и т.д., и т.п.
  3. Разработка функциональности Wicket-портлета
  4. Если используется Spring Framework

Исходный код разрабатываемого примера доступен на GitHub.


Три портлетных режима

Согласно портлетной спецификации, портлет может функционировать в трех различных режимах:

  • режиме просмотра (view mode),
  • режиме редактирования (edit mode),
  • режиме справки (help mode).

Несмотря на всю негибкость подобного технического решения (ведь вполне справедливыми будут вопросы: “Разве нельзя найти лучшего места для документации, чем режим справки портлета?”, “А почему нельзя задавать параметры всех портлетов централизовано из одного места?”), мне лично идея изменения настроек портлета в режиме редактирования понравилась. Быть может, потому что подобная функциональность очень хорошо подходила для той задачи, которую мне пришлось решать. И вообще если рассматривать создание какой-нибудь CMS, то для настройки отдельных функциональных блоков на странице возможности их перехода в режим редактирования вполне достаточно.

Собственно, предлагаю и в нашем импровизированном портлете реализовать все три режима работы портлета. Если уж у нас получится реализовать все три режима для одного портлета, то реализация отдельных режимов просмотра для двух разных портлетов (например, одного административного портлета, используемого для редактирования настроек второго, пользовательского портлета) не должна составить большого труда.

Чтобы расчистить дорогу на подступах к непосредственной реализации кода портлетных режимов (в нашем случае все три портлетных режима будут реализовываться отдельными страницами Wicket), подредактируем файл portlet.xml и код класса приложения AchievementPortletApp.

В файле portlet.xml в первую очередь передадим классу Wicket-портлета параметры инициализации, указывающие по каким URL’ам будет доступен каждый из режимов портлета:

portlet.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
<portlet-class>org.apache.wicket.protocol.http.portlet.WicketPortlet</portlet-class>
<init-param>
  <name>wicketFilterPath</name>
  <value>/achievement</value>
</init-param>
<init-param>
          <name>viewPage</name>
          <value>/achievement/view</value>
</init-param>
<init-param>
          <name>editPage</name>
          <value>/achievement/edit</value>
</init-param>
<init-param>
          <name>helpPage</name>
          <value>/achievement/help</value>
</init-param>
...

Как несложно догадаться, параметр viewPage – это URL страницы режима просмотра, параметр editMode – URL страницы режима редактирования, параметр helpMode – соответственно URL страницы режима справки.

Затем чуть ниже в portlet.xml немного расширим список поддерживаемых MIME-типов (тег <supports />):

portlet.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<supports>
          <mime-type>text/html</mime-type>
          <portlet-mode>VIEW</portlet-mode>
          <portlet-mode>EDIT</portlet-mode>
          <portlet-mode>HELP</portlet-mode>
</supports>
<supports>
          <mime-type>text/xml</mime-type>
          <portlet-mode>VIEW</portlet-mode>
          <portlet-mode>EDIT</portlet-mode>
          <portlet-mode>HELP</portlet-mode>
</supports>
...

Как видно, с помощью тегов <portlet-mode />; мы указываем поддержку всех трех режимов для каждого из MIME-типов. Причем MIME-тип “text/xml” нам нужен для будущей поддержки AJAX в нашем портлете. Но всему свое время…

В методе init() класса приложения AchievementPortletApp мы привязываем страницы режимов к тем самым URL’ам, которые мы указали в параметрах инициализации класса портлета в файле portlet.xml:

AchievementPortletApp.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AchievementPortletApp extends WebApplication {

    @Override
    protected void init() {
        mountBookmarkablePage("/view", AchievementViewPage.class);
        mountBookmarkablePage("/edit", AchievementEditPage.class);
        mountBookmarkablePage("/help", AchievementHelpPage.class);
    }

    @Override
    public Class<? extends Page> getHomePage() {
        PortletRequestContext prc = (PortletRequestContext) RequestContext.get();
        if (prc.getPortletRequest().getPortletMode() == PortletMode.EDIT) {
            return AchievementEditPage.class;
        } else if (prc.getPortletRequest().getPortletMode() == PortletMode.HELP) {
            return AchievementHelpPage.class;
        } else {
            return AchievementViewPage.class;
        }
    }
}

Что касается метода getHomePage(), то в теории он теперь вообще не должен будет использоваться, так как портал будет обращаться к нашему портлету по одному из URL’ов “/achievement/view”, “/achievement/edit” или “/achievement/help” в зависимости от режима. Но, как говорится, теория и практика одинаковы только в теории, поэтому на практике в методе getHomePage() мы еще раз постараемся вернуть нужную страницу в зависимости от режима, указанного в портлетном запросе.

Локализация

Перед тем, как перейти непосредственно к написанию кода портлетных страниц, предлагаю еще подумать о такой замечательной вещи, как локализация. Значение этих четырех символов – L10n – становится все больше с каждым днем в нашем всеуменьшающемся мире! И, согласись, нам ведь важно, чтобы рассказать во всеуслышанье о своем достижении мог не только какой-нибудь Вася из Москвы, но и Уильям из США, Пабло из Испании, и даже, может быть, Юсуф из Турции.

Как известно, Wicket уже включает в себя широкие средства для локализации приложений, так что грех будет всей этой функциональностью не воспользоваться. Но вот незадача: в Liferay локаль может быть изменена в любой момент с помощью портлета “Язык” (“Language”), а в рамках Wicket локаль пользователя устанавливается только во время первого запроса к портлету, а затем хранится в сессии. Так что, чтобы все работало корректно, у нас по сути нет другого выхода, кроме как менять локаль в сессии Wicket при каждом обращении к портлету.

Для этого реализуем обобщенный класс страницы Wicket, заключающий в себе всю базовую функциональность для локализации:

LiferayPortletPage.java
1
2
3
4
5
6
public class LiferayPortletPage extends WebPage {

    public LiferayPortletPage() {
        getSession().setLocale(getRequest().getLocale());
    }
}

В единственной строчке, ради которой был нужен этот класс, мы просто устанавливаем в сессии Wicket значение локали, взятое из портлетного запроса. Вуаля!

Теперь главное – не забыть использовать этот класс в качестве родительского при реализации страниц для портлетных режимов.

Страница для режима справки

На самом деле, режим справки HELP новой улучшенной версии нашего портлета будет аналогичен единственному режиму VIEW версии портлета, разработанной в предыдущей части данного руководства. Единственное улучшение – конечно же, поддержка локализации (не забываем про использование LiferayPortletPage в качестве родительского класса).

Далее приводятся листинги файлов страницы режима справки AchievementHelpPage.

AchievementHelpPage.java
1
2
3
4
5
6
public class AchievementHelpPage extends LiferayPortletPage {

    public AchievementHelpPage() {
        add(new Label("text", new StringResourceModel("help.text", null)));
    }
}
AchievementHelpPage.html
1
2
3
4
5
6
7
8
9
10
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="charset" content="UTF-8" />
    <title>Achievement</title>
</head>
<body>
    <div wicket:id="text"></div> 
</body>
</html>

Если теперь создать заглушки для страниц, отвечающих за режимы VIEW и EDIT, установить портлет на портал, выбрать в меню, которое открывается при нажатии на “гаечный ключ” в заголовке портлета, пункт “Помощь”, то тогда, скорее всего, можно будет увидеть следующее:

Режим портлета HELP

Модель для работы с настройками портлета

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

Но если у портлета имеются настройки, то возникает другая проблема: эти настройки нужно где-то хранить. Удобным для этого местом в рамках портлетного контекста является хранилище типа PortletPreferences. Настройки хранятся в этом хранилище по ключам и так, что однажды заданные для портлета настройки хранятся, пока их не изменят или портлет не удалят (то есть фактически сохраняются в том же месте, что и информация о портлете).

Очень хорошо! Но напомню, что в нашем случае настройками будет некоторая информация о человеке, совершившем достижение, и она будет отображаться в интерфейсе портлета в неизменном виде. Это говорит о том, что фактически настройки нашего портлета будут его данными. А что используется в Wicket для работы с данными, подскажи-ка мне, эрудированный читатель? Ну, нет же, причем здесь методы классов? Я понимаю, что ты очень хорошо в свое время зазубрил экзамен по ООП, но я имел ввиду всего лишь потомков класса Model. :)

В общем, предлагаю реализовать модель для получения настроек из хранилища типа PortletPreferences и сохранения их в хранилище. Модель должна быть наподобии той, листинг которой приведен ниже.

PortletPreferenceModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class PortletPreferenceModel extends Model<String> {

    /** Ключ настройки портлета */
    private String preferenceKey;

    /**
     * Модель, значение которой используется как значение по-умолчанию,
     * если настройка с ключом preferenceKey отсутствует в хранилище
     */
    private IModel<String> defaultValueModel;

    public PortletPreferenceModel(String preferenceKey, IModel<String> defaultValueModel) {
        if (preferenceKey == null) {
            throw new IllegalArgumentException("Portlet preference key can't be null.");
        }

        this.preferenceKey = preferenceKey;
        this.defaultValueModel = defaultValueModel != null ? defaultValueModel : new Model<String>();
    }

    @Override
    public String getObject() {
        return getPortletPreferences().getValue(this.preferenceKey, this.defaultValueModel.getObject());
    }

    @Override
    public void setObject(String value) {
        try {
            getPortletPreferences().setValue(this.preferenceKey, value);
            // обязательно сохраняем настройки после их изменения
            getPortletPreferences().store();
        } catch (PortletException e) {
            throw new RuntimeException("Error while saving portlet preference's value.", e);
        } catch (IOException e) {
            throw new RuntimeException("Error while saving portlet preference's value.", e);
        }
    }

    /**
     * Получение хранилища настроек портлета
     *
     * @return
     */
    private PortletPreferences getPortletPreferences() {
        final PortletRequestContext prc = (PortletRequestContext) RequestContext.get();
        return prc.getPortletRequest().getPreferences();
    }
}

Метод getPortletPreferences() модели получает хранилище настроек портлета из контекста. Это хранилище потом используется для получения (традиционно метод getObject()) и сохранения (традиционно метод setObject()) настроек по ключу, значение которого передается в качестве параметра конструктора. Если настройка с данным ключом отсутствует в хранилище, то возвращается значение дефолтной модели.

Создаем страницу для режима просмотра

После реализации класса модели PortletPreferenceModel создание режима просмотра для портлета – дело 10 строчек кода. Не верите? Смотрите сами. Только Wicket, только хардкор и никакого мошенничества!

AchievementViewPage.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AchievementViewPage extends LiferayPortletPage {

    public AchievementViewPage() {
        add(new Label("text", new StringResourceModel("view.text", this, null, new Object[] {
                // имя создателя портлета
                new PortletPreferenceModel("name", new StringResourceModel("defaultValue.name", this, null)),
                // наименование задания
                new PortletPreferenceModel("task", new StringResourceModel("defaultValue.task", this, null)),
                // продолжительность задания
                new PortletPreferenceModel("length", new StringResourceModel("defaultValue.length", this, null)),
                // награда
                new PortletPreferenceModel("prize", new StringResourceModel("defaultValue.prize", this, null))
        })));
    }
}
AchievementViewPage.html
1
2
3
4
5
6
7
8
9
10
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="charset" content="UTF-8" />
    <title>Achievement</title>
</head>
<body>
    <div wicket:id="text"></div> 
</body>
</html>

Чуть-чуть объясню используемые здесь фокусы локализации. В конструктор объекта StringResourceModel для получения локализованного значения по ключу “view.text” передается четвертый параметр – массив объектов. Таким образом, если по ключу “view.text” в ресурсном файле хранится строка вида

Данный портлет создан для того, чтобы сообщить, что я,
{0}, успешно справился с выполнением задания "{1}", и мне
потребовалось для этого всего {2}. За данное достижение я, пожалуй,
куплю себе {3}.

то вместо каждой из меток {0}, {1}, {2}, {3} будет использован преобразованный к строке объект массива с соответствующим индексом. В нашем случае роль объектов массива играют модели типа PortletPreferenceModel, поэтому значения для подстановки будут получены из хранилища настроек портлета по ключам “name”, “task”, “length” и “prize”. В случае же отсутствия значений по этим ключам в настройках портлета в дело вступят модели типа StringResourceModel, которые получат локализованные значения по умолчанию из ресурсного файла по ключам “defaultValue.name”, “defaultValue.task”, “defaultValue.length” и “defaultValue.prize”.

Теперь после того, как мы добавим портлет на страницу, он будет выглядеть так:

Режим портлета HELP

Значения “Создатель портлета”, “Создание портлета” и т.д. были получены из локализованного ресурсного файла, так как пока у нас нет режима редактирования, в котором мы могли бы эти значения изменить. В следующем разделе мы исправим это досадное упущение.

Создаем страницу для режима редактирования

Почти все самое вкусное, то есть режим редактирования, я не удержался и приберег напоследок.

Чтобы режим редактирования выглядел по-настоящему круто, предлагаю в качестве контрола для редактирования значений использовать добротный компонент AjaxEditableLabel (вот здесь-то нам, наконец-то, и пригодится поддержка AJAX в нашем портлете). В обычном состоянии этот компонент выглядит, как обычная надпись. Но если всего лишь один раз кликнуть по этой надписи, то она тут же чудесным образом превращается в поле для редактирования!

А вот это пример кода, создающего компонент AjaxEditableLabel для редактирования имени человека, совершившего достижение:

1
2
AjaxEditableLabel nameLabel = new AjaxEditableLabel<String>("name",
        new PortletPreferenceModel("name", new StringResourceModel("defaultValue.name", this, null)));

В качестве модели для компонента используется до боли знакомая PortletPreferenceModel без каких-либо модификаций. Думаю, то, что мы в свое время заморочились написанием кода этой модели, оправдало себя целиком и полностью.

Также считаю важным в режиме редактирования реализовать еще один полезный контрол, а именно кнопку возврата в режим просмотра после завершения редактирования. Просто дело в том, что если в Liferay 6.0.6 в настройках внешнего вида портлета отключить опцию показа обрамления, то вернуться из режима редактирования в этом случае будет практически невозможно:

Проблема возврата из режима редактирования в Liferay 6.0.6

Что касается Liferay 6.1.0, то там вернуться из режима редактирования вполне реально даже в случае остутствия рамки (обрамления) портлета. Однако, реализация отдельной кнопки в режиме редактирования для возврата в режим просмотра, на мой взгляд, будет хорошим тоном по отношению к будущему пользователю портлета.

Не стоит волноваться, реализация кнопки для перехода в режим просмотра, на самом деле, пустяковое дело. В методе onClick() класса Link, в котором происходит обработка нажатия на кнопку, просто сделаем следующие три вещи:

  1. в качестве значения будущего состояния окна портлета передадим объекту PortletResponse константу WindowState.NORMAL;
  2. в качестве значения будущего режима портлета укажем, понятное дело, PortletMode.VIEW;
  3. с помощью метода setResponsePage() зададим переход на страницу AchievementViewPage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add(new Link<Void>("view_button") {

    @Override
    public void onClick() {
        final PortletRequestContext prc = (PortletRequestContext) RequestContext.get();
        final ActionResponse response = (ActionResponse) prc.getPortletResponse();
        try {
            response.setWindowState(WindowState.NORMAL);
            response.setPortletMode(PortletMode.VIEW);
        } catch (PortletException e) {
            throw new RuntimeException("Exception while changing portlet's window state and mode", e);
        }

        setResponsePage(AchievementViewPage.class);
    }
});

Полные листинги для страницы редактирования AchievementEditPage приводятся ниже.

AchievementEditPage.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class AchievementEditPage extends LiferayPortletPage {

    public AchievementEditPage() {
        // компонент для редактирования имени создателя портлета
        add(new AjaxEditableLabel<String>("name", new PortletPreferenceModel("name", new StringResourceModel(
                "defaultValue.name", this, null))));

        // компонент для редактирования наименования задания
        add(new AjaxEditableLabel<String>("task", new PortletPreferenceModel("task", new StringResourceModel(
                "defaultValue.task", this, null))));

        // компонент для редактирования продолжительности задания
        add(new AjaxEditableLabel<String>("length", new PortletPreferenceModel("length", new StringResourceModel(
                "defaultValue.length", this, null))));

        // компонент для редактирования награды
        add(new AjaxEditableLabel<String>("prize", new PortletPreferenceModel("prize", new StringResourceModel(
                "defaultValue.prize", this, null))));

        // кнопка для возврата в режим просмотра
        add(new Link<Void>("view_button") {

            @Override
            public void onClick() {
                final PortletRequestContext prc = (PortletRequestContext) RequestContext.get();
                final ActionResponse response = (ActionResponse) prc.getPortletResponse();
                try {
                    response.setWindowState(WindowState.NORMAL);
                    response.setPortletMode(PortletMode.VIEW);
                } catch (PortletException e) {
                    throw new RuntimeException("Exception while changing portlet's window state and mode", e);
                }

                setResponsePage(AchievementViewPage.class);
            }
        });
    }
}
AchievementEditPage.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="charset" content="UTF-8" />
    <title>Achievement</title>
</head>
<body>
    <div>
        <wicket:message key="edit.name.label"></wicket:message>
        <span wicket:id="name"></span>
    </div>
    <div>
        <wicket:message key="edit.task.label"></wicket:message>
        <span wicket:id="task"></span>
    </div>
    <div>
        <wicket:message key="edit.length.label"></wicket:message>
        <span wicket:id="length"></span>
    </div>
    <div>
        <wicket:message key="edit.prize.label"></wicket:message>
        <span wicket:id="prize"></span>
    </div>

    <div class="view-button-container">
        <input wicket:id="view_button" type="button" wicket:message="value:edit.view.button.text" />
    </div>
</body>
</html>

Ура! Теперь-то мы cможем редактировать значения, которые будут отображаться в режиме просмотра!

Для этого просто выберем в меню “гаечного ключа” в заголовке портлета пункт “Настроить”. Как видим, оно действительно работает:

Режим портлета EDIT

Результат

Итак, по сути вся запланированная функциональность была реализована. Чем не достижение?

Собственно, зачем тогда добру пропадать без дела? Используем его по назначению:

Режим портлета EDIT

Так-то!

Целиком исходный код примера можно увидеть в репозитории на GitHub.

Comments