Pull to refresh

Основы портлетов

Reading time24 min
Views30K
Привет хабралюди! Сегодня я хочу рассказать об одной интересной технологии, с которой познакомился совсем недавно — это технология портлетов. Хотя на хабре уже есть пара упоминаний о портлетах, но там ничего внятного я не нашел. Поэтому решил написать свою статью, где хочу показать на практике как программировать портлеты. При этом попутно вставляя какие-то теоретические сведения. А принимая во внимание, то, что документации на русском крайне мало, то рассказать об этом хочется вдвойне :)

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

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

Портлет — это отдельное небольшое веб-приложение, которое выполняется на портале, портал в свою очередь агрегирует один или несколько портлетов на отдельной веб странице, которые обычно настраиваются для для отдельных пользователей и групп портала. В итоге мы получаем обычную веб-страницу, наполненную несколькими веб-приложениями. И еще, портлеты имеют много аналогий с сервлетами, я думаю, что многие, кто вообще в курсе дела, это заметят. Лишний раз проводить аналогии и останавливаться на этом не буду.

В этой статье буду описывать разработку портлетов в рамках спецификации JSR 168, но не буду рассказывать о выборе портала, о его установке, деплое портлетов и т.п. Сразу скажу, что разрабатывал приложение на IBM Web Sphere Portal 6. Если кто-то расскажет об особенностях разработки и деплоя на других порталах типа Jboss Portal или еще какого, то буду очень признателен :)

Итак приступим к разработке, а разрабатывать мы будем… на самом деле долго думал, что лучше привести как пример, хотел, чтобы пример не был чем-то тривиальным типа Hello World, хотел чтобы портлету нашлось практическое применение и выбор мой пал на портлет для агрегации RSS. А что? Актульно!

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

Сперва давайте создадим список классов, библиотек и jsp-шек, которые потребуются нам для нашего приложения:

1) HabraRssPortlet — собственно сам класс портлета.
2) RssUtil — класс для работы с RSS.
3) rssutils — библиотека (конечно можно и самому парсер написать, но это займет время и наверняка сразу хорошо не получится — проверено опытом)
3) HabraRssPortletView.jsp — для отображения Rss.
4) HabraRssPortletEdit.jsp — для конфигурирования портлета пользователем.
5) HabraRssPortletHelp.jsp — где будет что-то типа «About»
6) portlet.xml и web.xml — ну я думаю понятно для чего…

Погнали!

Хочу начать с дескриптора развертывания portlet.xml дабы объяснить некоторые моменты, которые понадобятся нам позже, дескриптор web.xml приводит тут не буду, там все до боли просто. Если кому-то интересно, то его можно всегда взглянуть в аттаче.

portlet.xml
<?xml version="1.0" encoding="UTF-8"?><br><portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" version="1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" id="ru.habrahabr.rssportlet.HabraRssPortlet.a97fbe5bd1"><br>    <portlet><br>        <portlet-name>HabraRssPortlet</portlet-name><br>        <display-name>HabraRssPortlet</display-name><br>        <display-name xml:lang="ru">HabraRssPortlet</display-name><br>        <portlet-class>ru.habrahabr.rssportlet.HabraRssPortlet</portlet-class><br>        <init-param><br>            <name>wps.markup</name><br>            <value>html</value><br>        </init-param><br>        <expiration-cache>0</expiration-cache><br>        <supports><br>            <mime-type>text/html</mime-type><br>            <portlet-mode>view</portlet-mode><br>            <portlet-mode>edit</portlet-mode><br>            <portlet-mode>config</portlet-mode><br>            <portlet-mode>help</portlet-mode><br>        </supports><br>        <supported-locale>ru</supported-locale><br>        <resource-bundle>ru.habrahabr.rssportlet.nl.HabraRssPortletResource</resource-bundle><br>        <portlet-info><br>            <title>HabraRssPortlet</title><br>            <short-title>HabraRssPortlet</short-title><br>            <keywords>HabraRssPortlet</keywords><br>        </portlet-info><br>        <portlet-preferences><br>            <br>            <preference><br>                <name>updateFrequency</name><br>                <!-- in minutes --><br>                <value>10</value><br>            </preference><br>            <preference><br>                <name>itemQuantity</name><br>                <value>3</value><br>            </preference><br>            <preference><br>                <name>rssLinks</name><br>                <value>www.vz.ru/rss.xml</value><br>            </preference><br><br>        </portlet-preferences><br>    </portlet><br>    <custom-portlet-mode><br>        <portlet-mode>config</portlet-mode><br>    </custom-portlet-mode><br></portlet-app><br><br>* This source code was highlighted with Source Code Highlighter.


  • portlet-app — главный элемент дескриптора развертывания, должен содержать как минимум два атрибута: версия и идентификатор + сслки на схемы, описывающие данный xml. Так же внутри элемента portlet-app могут содержаться объявления портлетов, кастомные режимы и состояния портлета.
  • portlet — данный элемент представляет класс портлета и все его метаданные
  • portlet-name — имя портлета
  • display-name — отображаемое имя портлета
  • portlet-class — класс портлета, указывается вместе со структурой пакетов
  • init-param — параметры инициализации портлета
  • expiration-cache — таймаут кэша
  • supports — поддерживаемые режимы и mime типы
  • supported-locale — поддерживаемые локали
  • resource-bundle — класса отвечающий за метаданные, которые находятся в bundle
  • portlet-info — информация о портлете. Если присутствует bundle, то эту информацию можно вынести в него.
  • portlet-preferences — являются неким хранилищем, именно там мы и будем сохранять ссылки на Rss, период обновления и количество новостей с каждого канала. Опциональным для этого элемента является элемент preferences-validator, который в свою очередь занимается проверкой preferences. Хочу заметить, preferences не обязательно указывать в дескрипторе, их можно создать и программно во время работы программы.
  • custom-portlet-mode — кастомные режимы портлетов, тут я для примера включил режим config, хотя использовать его не будем


Ниже приведен основной класс нашего приложения — HabraRssPortlet, который является потомком класса GenericPortlet, а он в свою очередь является абстрактным классом и обеспечивает дефолтную имплементацию интерфейса Portlet. Все тот же GenericPortlet обеспечивает поддержку несколько режимов: VIEW, EDIT, HELP, еще иногда бывают кастомные режимы типа CONFIG, но это уже зависит от конкретной реализации портала. Для конечного пользователя все три режима чаще всего представлены различной отображаемой информацией и доступным функционалом в том или ином режиме. Переход между режимами осуществляется либо через меню портлета, либо через кнопки/ссылки. Обычно у любого класса портлета перегружаются несколько методов, чаще всего это методы doView, doEdit и doHelp, каждый из которых и отвечает за соответствующий режим.

HabraRssPortlet.java
package ru.habrahabr.rssportlet;

import java.io.*;
import java.util.*;
import javax.portlet.*;

public class HabraRssPortlet extends GenericPortlet {

  public static final String JSP_FOLDER    = "/_HabraRssPortlet/jsp/";
  public static final String VIEW_JSP     = "HabraRssPortletView";
  public static final String EDIT_JSP     = "HabraRssPortletEdit";
  public static final String HELP_JSP     = "HabraRssPortletHelp";
  public static final String CONFIG_JSP    = "HabraRssPortletConfig";
  public static final String RSS_LINKS    = "rssLinks";
  public static final String UPDATE_FREQUENCY = "updateFrequency";
  public static final String ITEM_QUANTITY  = "itemQuantity";
  public static final String NEW_RSS_LINK   = "newRssLink";
  public static final String ADD_NEW_RSS_LINK = "addNewRssLink";
  public static final String DELETE_RSS_LINK = "deleteRssLink";
  public static final String RSS_LINKS_LIST  = "rssLinksList";
  public static final String FREQUENCY     = "frequency";
  public static final String QUANTITY     = "quantity";
  public static final String SET_FREQUENCY  = "setFrequency";
  public static final String SET_QUANTITY   = "setQuantity";
  public static final String ERROR_MESSAGE  = "errorMessage";
  public static final String UPDATE_FEEDS   = "updateFeeds";
  public static final String EDIT_BACK     = "editBack";

  private static HashMap rssMap    = new HashMap();
  private static HashMap updateTimeMap = new HashMap();

  public void doView(RenderRequest request, RenderResponse response)throws PortletException, IOException {
    response.setContentType(request.getResponseContentType());
    PortletPreferences preferences = request.getPreferences();
    
    if (!rssMap.containsKey(request.getUserPrincipal().getName())){
      rssMap.put(request.getUserPrincipal().getName(), new ArrayList());
      updateTimeMap.put(request.getUserPrincipal().getName(), new Long(0));
      
    }
        
    if (RssUtil.checkUpdateTime(getLastUpdateTime(request), preferences)) {
      ArrayList rssItems = (ArrayList)rssMap.get(request.getUserPrincipal().getName());
      rssItems.clear();
      rssItems = RssUtil.updateRssFeeds(preferences);
      System.out.println("News for user " + request.getUserPrincipal().getName() + " updated!");
      rssMap.put(request.getUserPrincipal().getName(), rssItems);
      setLastUpdateTime(request);
      
    }
        
    int interval = Integer.parseInt(preferences.getValue(UPDATE_FREQUENCY, "60"));
    System.out.println("Time befor update (in seconds) = " + ((interval * 60) - ((System.currentTimeMillis() - getLastUpdateTime(request).longValue()) / 1000 )));
    
    request.setAttribute("rssMap", rssMap.get(request.getUserPrincipal().getName()));
    PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(getJspFilePath(request, VIEW_JSP));
    rd.include(request, response);
  }

  public void doEdit(RenderRequest request, RenderResponse response)
      throws PortletException, IOException {
    response.setContentType(request.getResponseContentType());
    PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(
        getJspFilePath(request, EDIT_JSP));
    rd.include(request, response);
  }

  protected void doHelp(RenderRequest request, RenderResponse response)
      throws PortletException, IOException {
    response.setContentType(request.getResponseContentType());
    PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(getJspFilePath(request, HELP_JSP));
    rd.include(request, response);
  }

  public void processAction(ActionRequest request, ActionResponse response)
      throws PortletException, java.io.IOException {
    PortletPreferences preferences = request.getPreferences();

    if (request.getParameter(ADD_NEW_RSS_LINK) != null) {
      if (request.getParameter(NEW_RSS_LINK).trim().length() > 0) {
        String[] links = (String[]) preferences.getValues(HabraRssPortlet.RSS_LINKS, null);
        String newLink = request.getParameter(NEW_RSS_LINK);
        preferences.setValues(RSS_LINKS, addToArray(links, newLink));
        preferences.store();
        System.out.println(">>>>>>>>>>>>>>>> New rss link added " + newLink);
      } else {
        response.setRenderParameter(ERROR_MESSAGE, "Link must be not empty!");
      }
    }

    if (request.getParameter(DELETE_RSS_LINK) != null) {
      if (request.getParameter(RSS_LINKS_LIST) != null
          && request.getParameter(RSS_LINKS_LIST).trim().length() > 0 ) {
        String[] links = (String[]) preferences.getValues(
            HabraRssPortlet.RSS_LINKS, null);
        String deletedLink = request.getParameter(RSS_LINKS_LIST);
        if (isFound(links, deletedLink)) {
          preferences.setValues(RSS_LINKS, deleteFromArray(links, deletedLink));
          preferences.store();
          System.out.println(">>>>>>>>>>>>>>>> Rss link deleted " + deletedLink);
        }
      } else {
        response.setRenderParameter(ERROR_MESSAGE, "Choose a link!");
      }
    }

    if (request.getParameter(SET_FREQUENCY) != null) {
      if (request.getParameter(FREQUENCY).trim().length() > 0
          && isDigit(request.getParameter(FREQUENCY))) {
        String frequency = request.getParameter(FREQUENCY);
        preferences.setValue(UPDATE_FREQUENCY, frequency);
        preferences.store();
        System.out.println(">>>>>>>>>>>>>>>> Frequency updated " + frequency);
      } else {
        response.setRenderParameter(ERROR_MESSAGE, "Set valid frequency!");
      }
    }

    if (request.getParameter(SET_QUANTITY) != null) {
      if (request.getParameter(QUANTITY).trim().length() > 0
          && isDigit(request.getParameter(QUANTITY))) {
        String quantity = request.getParameter(QUANTITY);
        preferences.setValue(ITEM_QUANTITY, quantity);
        preferences.store();
        System.out.println(">>>>>>>>>>>>>>>> Quantity updated " + quantity);
      } else {
        response.setRenderParameter(ERROR_MESSAGE, "Set valid quantity!");
      }
    }

    if (request.getParameter(UPDATE_FEEDS) != null) {
      ArrayList rssItems = (ArrayList)rssMap.get(request.getUserPrincipal().getName());
      rssItems.clear();
      rssItems = RssUtil.updateRssFeeds(preferences);
      System.out.println("News fo user " + request.getUserPrincipal().getName() + " updated!");
      rssMap.put(request.getUserPrincipal().getName(), rssItems);
      setLastUpdateTime(request);
    }

    if (request.getParameter(EDIT_BACK) != null) {
      response.setPortletMode(PortletMode.VIEW);
    }
    
    

  }

  private static String getJspFilePath(RenderRequest request, String jspFile) {
    String markup = request.getProperty("wps.markup");
    if (markup == null)
      markup = getMarkup(request.getResponseContentType());
    return JSP_FOLDER + markup + "/" + jspFile + "."
        + getJspExtension(markup);
  }

  private static String getMarkup(String contentType) {
    if ("text/vnd.wap.wml".equals(contentType))
      return "wml";
    else
      return "html";
  }

  private static String getJspExtension(String markupName) {
    return "jsp";
  }

  private static String[] addToArray(String[] array, String s) {
    String[] ans = new String[array.length + 1];
    System.arraycopy(array, 0, ans, 0, array.length);
    ans[ans.length - 1] = s;
    return ans;
  }

  private static String[] deleteFromArray(String[] array, String s) {
    String[] ans = new String[array.length - 1];
    int addIndex = 0;
    for (int i = 0; i < array.length; i++) {
      if (array[i].equals(s)) {
        addIndex++;
        continue;
      }
      ans[i - addIndex] = array[i];
    }
    return ans;
  }

  private static boolean isFound(String[] array, String s) {
    boolean found = false;
    for (int i = 0; i < array.length; i++) {
      if (array[i].equals(s)) {
        found = true;
        break;
      }
    }
    return found;
  }

  private static boolean isDigit(String s) {
    char[] array = s.toCharArray();
    for (int i = 0; i < array.length; i++) {
      if (!Character.isDigit(array[i])) {
        return false;
      }
    }
    return true;
  }
  
  private static Long getLastUpdateTime(PortletRequest request){
    return (Long)updateTimeMap.get(request.getUserPrincipal().getName());
  }
  
  private static void setLastUpdateTime(PortletRequest request){
    updateTimeMap.put(request.getUserPrincipal().getName(), new Long(System.currentTimeMillis()));
  }
  
  
}

* This source code was highlighted with Source Code Highlighter.


Пройдемся по коду:

С набором констант все ясно, далее мы создаем два статических объекта типа HashMap, — ключом ко всем значениям обеих карт служит имя конкретного пользователя портала, значением либо объект типа ArrayList, содержащий Rss-контент, либо объект типа Long, содержащий время последнего обновлнения каналов. Так как время жизни статической переменной портлета равно времени жизни самого портлета, то я решил хранить данние именно в них, а не персистить их куда-либо, тем более я вижу мало смысла в сохранении rss'a в какую-нибудь базу данных.

Далее начинается то, о чем мы говорили чуть выше, а именно переопределение методов класса GenericPortlet:

Метод doView: от объекта запроса получаем объект типа PortletPreferences, из которого в свою очередь можем вытащить нужное нам значение или же наоборот внести в него свое значение. Потом проверяем нашу первую карту на наличие в ней ключа с именем активного пользователя, если такого ключа нет, то заносим в обе карты пары ключ-значение, в одном случае значение — это объект ArrayList, в другом — объект Long, содержащий время последнего обновления. Далее проверяем, нужен ли нам апдейт новостей. Напомню, что для всех пользователей по умолчанию установлен один канал, одно и тоже количество новостей и время их обновления (см. portlet.xml) Итак, если пользователь зашел первый раз на страничку с нашим портлетом, то для него сразу же выполнится обновление канала, полученный rss-контент заносим в наш ArrayList, который мы получили из карты с rss-контентом по имени пользователя (ключ). Далее все просто — в запрос кладем все тот же ArrayList, вызываем PortletRequestDispatcher и перенаправляемся на jsp-страничку HabraRssPortletView, котрая и будет отображать наш rss-контент:

HabraRssPortletView.jsp
<%@page session="false" contentType="text/html" pageEncoding="ISO-8859-1" import="java.util.*,javax.portlet.*,ru.habrahabr.rssportlet.*" %><br><%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %><br><%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <br><portlet:defineObjects/><br><br><br><%<br>    if (request.getAttribute("rssMap") != null){<br>    %><br>    <br>    <c:forEach var="item" items="${rssMap}"><br>        <p><br>        <b><a href="${item.link}">${item.title}</a></b><br>        <span>${item.description}</span><br>        </p><br>        <hr><br>    </c:forEach><br>    <br><%<br> } else {<br> %>Error!!!!<%<br> }<br>%><br><br>* This source code was highlighted with Source Code Highlighter.



На страничке из запроса получаем наш объект ArrayList, итерируем его с помощью JSTL и выводим наш Rss-контент.

   
Метод doEdit: тут ничего особенного нет, так же вызываем PortletRequestDispatcher и переходим на jsp-страничку HabraRssPortletEdit. Вся логика зашита именно в ней, хоть это и не есть хорошо. Мы, конечно же, без проблем межем все это исправить :) Что же происходит на этой страничке? В скриплете с помощью все того же объекта PortletPreferences мы получаем необходимые нам значения, а именно:

а)Список rss каналов.
б)Частоту обновления.
в)Количество новостей из каждого канала.

HabraRssPortletEdit.jsp (часть)
<%<br> PortletPreferences preferences = renderRequest.getPreferences();<br> if( preferences!=null ) {<br>    String frequency = (String)  preferences.getValue(HabraRssPortlet.UPDATE_FREQUENCY, "60");<br>    String quantity = (String)  preferences.getValue(HabraRssPortlet.ITEM_QUANTITY, "5");<br>    String [] links = (String[]) preferences.getValues(HabraRssPortlet.RSS_LINKS, null );<br>%><br><br>* This source code was highlighted with Source Code Highlighter.


Далее рисуем необходимые нам поля, списки и кнопочки, вставляя в них значения, полученные чуть выше, и которые мы можем потом свободно редактировать, т.е. добавлять rss-каналы, удалять их, изменять значения частоты обновления и т.д.

   
   

Метод doHelp: тут ничего интересного совсем уже нет, вызываем все тот же PortletRequestDispatcher и перенаправляемся на страничку HabraRssPortletHelp, там пишем что-угодно, хотя обычно принято писать что-то типа справочных сведений.

Метод processAction: — сердце нашего портлета, именно тут идет вся обработка взаимодействия с пользователем, в нашем случае это все сабмиты с форм, которые располежены на страничке HabraRssPortletEdit. Прошу заметить, что в этом методе используются объекты типа ActionRequest и ActionResponse, в отличии от RenderRequest и RenderResponse, которые ответственны за рендеринг портлета, эти обеъкты ответственны за взаимодействие с пользователем. Обработка заключается в простейшей валидации входных параметров, изменении и создании значений preferences. Замечу, что валидаюцию preferences можно вынести в отдельный класс, который должен имплементиться от PreferencesValidator, при этом надо не забыть прописать этот класс в дескриптор portlet.xml. Мы же сделали немного попроще, если входящие параметры нас не устраивают, то мы устанавливаем объекту типа ActionResponse параметр рендеринга, содержащий сообщение об ошибке, это сообщение будет выводиться на той же старничке.

   
Да, еще очень важно то, что изменять значения preferences можно только внутри метода processAction. Самое главное, что стоит не забывать, это после всех измененийв вызвать метод store() у объекта типа PortletPreferences, иначе изменения не сохранятся. Прощу заметить, что портал хранит preferences отдельно для каждого пользователя, поэтому если один пользователь добавит в список своих каналов еще несколько ссылок, то у других пользователей все так же и останется одна ссылка, то же касается и остальных значений preferences, они индивидуальны для каждого пользователя. Правда очень удобно? :)



Теперь давайте немного рассмотрим класс, отвечающий за RSS. Я уже упоминал выше, что я воспользовался готовой библиотекой, скачать можно тут, может она не самая лучшая, но что есть, то есть :)

RssUtil.java
package ru.habrahabr.rssportlet;<br><br>public class RssUtil {<br><br>    public static ArrayList updateRssFeeds(PortletPreferences preferences){<br>        ArrayList allItems = new ArrayList();<br>        String [] links = preferences.getValues(HabraRssPortlet.RSS_LINKS, null);<br>        int quantity = Integer.parseInt(preferences.getValue(HabraRssPortlet.ITEM_QUANTITY, "3"));<br>        for (int i=0; i < links.length; i++){<br>            String link = links[i];<br>            InputStream stream = null;<br>            try {<br>                URL s = new URL(link);<br>                URLConnection connection = s.openConnection();<br>                if (!(connection instanceof URLConnection)) {            <br>                    throw new IllegalArgumentException(s.toExternalForm() + " is not a valid HTTP Url");<br>                }<br>                stream = connection.getInputStream();<br>                try{<br>                    RssParser parser = RssParserFactory.createDefault();<br>                    Rss rss = parser.parse(stream);<br>                    Collection items = rss.getChannel().getItems();<br>                    if (items != null && !items.isEmpty()) {<br>                        Iterator it = items.iterator();<br>                        int count = 0;<br>                        while (it.hasNext() && count < quantity){<br>                            Item item = (Item) it.next();<br>                            allItems.add(item);<br>                            count++;<br>                        }<br>                    }<br>                }catch (RssParserException e) {<br>                    e.printStackTrace();<br>                }<br>            } catch(IOException e) {<br>                e.printStackTrace();<br>            }<br>        }<br>        return allItems;<br>    }<br><br>    public static boolean checkUpdateTime(Long lastUpdateTime, PortletPreferences preferences) {<br>        int interval = Integer.parseInt(preferences.getValue(HabraRssPortlet.UPDATE_FREQUENCY, "60"));<br>        if (((System.currentTimeMillis() - lastUpdateTime.longValue()) / 1000) > (interval * 60))<br>            return true;<br>        return false;<br>    }<br><br>}<br><br>* This source code was highlighted with Source Code Highlighter.



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

Ну вот вроде бы и все. Все что тут было сказано, это мое личное ИМХО. Если у кого-то возникли вопросы — задавайте в камментах :) Весь код и war-архив можно взять в аттаче :)

PS. Да, и простите за System.out.println() в коде, оставил для наглядности.

Tags:
Hubs:
Total votes 35: ↑32 and ↓3+29
Comments70

Articles