Comunicación entre Portlets jsr286 (i)

Ya hace algunos meses que la versión 5.x de liferay. Entre las características más interesantes de la nueva versión podemos destacar la implementación de la especificación jsr286, algo así como la versión 2 del estandard de portlets. Lo más destacable de esta nueva especificación a mi modo de ver sería:

  • Los nuevos mecanismos de intercomunicación entre portlets (Listener pattern, Render public params)

  • Uso de anotaciones para los procesados de acciones, renderizado y eventos

En una serie de dos articulos voy a intentar ilustrar estas dos características creando un conjunto de portlets a modo de ejemplo de las mismas. En el primer artículo, éste que estoy escribiendo el ejemplo consistirá en comunicar dos portlets haciendo uso del Listener pattern. Además en el desarrollo de los portlets se utilizarán las anotaciones para el procesado tanto de los eventos como de las acciones.

El ejemplo base, modificado para escribir este artículo, se ha obtendido del foro de liferay y las urls de referencia seguidas para crear este articulo han sido:

Annotations (I)Annotations (II)

JSR286

PortletsEvents

Los dos portlets con el código se puden descargar de los siguientes links

EventPublisherPortlet.war

EventListenerPortlet.war

Creamos un portlet que haga las veces de publicador

Para ello en la definición del portlet, en el portlet.xml, añadimos los eventos que van a ser disparados por el portlet publicador.

<?xml version="1.0" encoding="UTF-8"?>

<portlet-app version="1.0"      xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
                                xmlns:x="http://zylk.net/events" 
                                xmlns:std="http://somestandardsbody.org/interop-events">



<portlet>

    <portlet-name>Jsr286EventPublisherPortlet</portlet-name>

    <portlet-class>mypackage.Jsr286EventPublisherPortlet</portlet-class>

        <portlet-info>

                <title>Jsr286 Event Publisher Portlet</title>

                <short-title>Jsr286EventPublisherPortlet</short-title>

        </portlet-info>

        <supports>

                <mime-type>text/html</mime-type>

                <portlet-mode>VIEW</portlet-mode>

        </supports>

        <supported-locale>en</supported-locale>
        <supported-publishing-event>

                <qname>x:contactInfo.add</qname>

        </supported-publishing-event>
        <supported-publishing-event>

                <qname>x:contactInfo.delete</qname>

        </supported-publishing-event>
</portlet>
        <default-namespace>ns.Jsr286EventPublisherPortlet</default-namespace>

        <event-definition>

                <qname>x:contactInfo.add</qname>

                <alias>std:contactInfo.add</alias>
                <value-type>java.util.Hashmap</value-type>

        </event-definition>
        <event-definition>

                <qname>x:contactInfo.delete</qname>

                <alias>std:contactInfo.delete</alias>
                <value-type>java.util.Hashmap</value-type>

        </event-definition>
</portlet-app>

 

Donde cabe destacar,

  • la definición de los espacios de nombres de la tag portlet-app

  • los eventos que el propio portlet va a dispara

  • la propia definición de los mismos

<supported-publishing-event>

        <qname>x:contactInfo.add</qname>

</supported-publishing-event>
....
<event-definition>

        <qname>x:contactInfo.add</qname>

        <alias>std:contactInfo.add</alias>
        <value-type>java.util.Hashmap</value-type>

</event-definition>

 

La tag alias de la definición se usa para una posible estandarización de los eventos en un espacios de nombres común. Esto permitirá, hipoteticamente, consumir eventos publicados por un tercero por medio de este espacio de nombres común y su alias, y avanzar así en una estructura realmente SOA dentro de las aplicaciones desplegadas como portlets en el portal.

 

Creamos el portlet que haga las veces de consumidor

Para ello en la definición del portlet, en el portlet.xml, añadimos los eventos que van a ser disparados por el portlet consumidor

<?xml version="1.0" encoding="UTF-8"?>

<portlet-app version="1.0"      xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
                                xmlns:x="http://zylk.net/events" 
                                xmlns:std="http://somestandardsbody.org/interop-events">



<portlet>

    <portlet-name>Jsr286EventListenerPortlet</portlet-name>

    <portlet-class>mypackage.Jsr286EventListenerPortlet</portlet-class>

        <portlet-info>

                <title>Jsr286 Event Listener Portlet</title>

                <short-title>Jsr286EventListenerPortlet</short-title>

        </portlet-info>

        <supports>

                <mime-type>text/html</mime-type>

                <portlet-mode>VIEW</portlet-mode>

        </supports>

        <supported-locale>en</supported-locale>



        <supported-processing-event>

                <qname>x:contactInfo.add</qname>

        </supported-processing-event>           
        <supported-processing-event>

                <qname>x:contactInfo.delete</qname>

        </supported-processing-event>

</portlet>

        

        <default-namespace>ns.Jsr286EventListenerPortlet</default-namespace>

        <event-definition>

                <qname>x:contactInfo.add</qname>

                <alias>std:contactInfo.add</alias>
                <value-type>java.util.Hashmap</value-type>

        </event-definition>
        <event-definition>

                <qname>x:contactInfo.delete</qname>

                <alias>std:contactInfo.delete</alias>
                <value-type>java.util.Hashmap</value-type>

        </event-definition>     

</portlet-app>

Al igual que en el caso anterior hay una parte en la que se define el namespace de los eventos, debe ser igual en ambos casos para que los eventos lanzados por pueden ser consumidos, otra parte donde se definen los eventos que se van a poder consumir y su definición.

Hay que tener en cuenta que como estos eventos van a ser pasados entre los portlets y estos puden encontrarse en entornos distribuidos todo eventos definido deber ser serializable (The Java Portlet Specification requires that such payload classes support Java serialization as well as XML serialization by using Java XML Binding (JAXB) annotations), algo similar a lo que sucede con los web-services y los marshallers.

Una vez definidos los eventos que van a ser disparados y/o consumidos podemos pasar a crear las clases y los jsps necesarios en cada portlet. Hay que tener en cuenta que en este ejemplo vamos a utilizar el nuevo modelo de anotaciones de la especificacion de los portlets para procesar las peticiones.

Creamos la clase mypackage.Jsr286EventPublisherPortlet

public class Jsr286EventPublisherPortlet extends GenericPortlet {

        

        @RenderMode(name="view")

        public void viewNormal(RenderRequest request, RenderResponse response) throws PortletException, IOException {

                getPortletContext().getRequestDispatcher("/xhtml/view.jsp").forward(request, response);

        }



        /**

         * This method processes the "contactInfo.add" action.

         * The form parameters are added to the Hashtable object that will be sent as the event value

         */

        @ProcessAction(name="contactInfo.add")

        public void addContact(ActionRequest request, ActionResponse response) throws PortletException, IOException {

                HashMap<String, String> contactInfo = new HashMap<String, String>();

                

                // Get form parameters

                String name = request.getParameter("name");

                String email = request.getParameter("email");

                

                // Populate the hashmap

                contactInfo.put("name", isEmpty(name) ? "anonymous":name);

                contactInfo.put("email", isEmpty(email) ? "No email":email);

                

                // Send the event using the appropriate QName

                response.setEvent(new QName("http://zylk.net/events", "contactInfo.add"), contactInfo);

        }

        /**

         * This method processes the "contactInfo.delete" action.

         * The form parameters are added to the Hashtable object that will be sent as the event value

         */

        @ProcessAction(name="contactInfo.delete")

        public void deleteContact(ActionRequest request, ActionResponse response) throws PortletException, IOException {

                HashMap<String, String> contactInfo = new HashMap<String, String>();

                

                // Get form parameters

                String name = request.getParameter("name");

                String email = request.getParameter("email");

                

                // Populate the hashmap

                contactInfo.put("name", isEmpty(name) ? "anonymous":name);

                contactInfo.put("email", isEmpty(email) ? "No email":email);

                

                // Send the event using the appropriate QName

                response.setEvent(new QName("http://zylk.net/events", "contactInfo.delete"), contactInfo);

        }



        public Map<String, String[]> getContainerRuntimeOptions() {

                return null;

        }

        

        /**

         * Evaluate if the string is empty, i.e. null or length to 0

         * @param s The string to evaluate

         * @return true if the string has a length > 0 (spaces are ignored)

         */

        private boolean isEmpty(String s) {

                return s == null || s.trim().length() == 0;

        }

}

Evidentemente esta clase extiende de GenericPortlet, los métodos que nos interesan son deleteContact y addContact. Son interesantes porque van a procesar las acciónes siguientes contactInfo.add y contactInfo.delete además. Como podemos apreciar la definición de este flujo se realiza por medio de anotaciones, ya no es necesario usar varios ifs dentro del metodo processAction.

Creamos la clase mypackage.Jsr286EventListenerPortlet

public class Jsr286EventListenerPortlet extends GenericPortlet {



        public Map<String, String[]> getContainerRuntimeOptions() {

                return null;

        }



        @RenderMode(name="view")

        public void viewNormal(RenderRequest request, RenderResponse response) throws PortletException, IOException {   

                getPortletContext().getRequestDispatcher("/xhtml/view.jsp").forward(request, response);

        }



        /*
         * This method processes received events with the following QName

         */

        @ProcessEvent(qname="{http://zylk.net/events}contactInfo.add")

        public void processEventAdd(EventRequest request, EventResponse response) throws PortletException, IOException {

                // Let's store the event value into the portlet's session (we assume it is never null)

                HashMap<String, String> eventValue = (HashMap<String, String>) request.getEvent().getValue();

                ContactInfoBean contactInfo = new ContactInfoBean();



                contactInfo.setName(eventValue.get("name"));

                contactInfo.setEmail(eventValue.get("email"));

                

                request.getPortletSession().setAttribute("contactInfo", contactInfo);

        }

        /*
         * This method processes received events with the following QName

         */

        @ProcessEvent(qname="{http://zylk.net/events}contactInfo.delete")

        public void processEventDelete(EventRequest request, EventResponse response) throws PortletException, IOException {

                // Let's drop the object from session

                request.getPortletSession().setAttribute("contactInfo", null);

        }



}

Donde lo importante es destacar que por medio de las anotaciones estamos procesando, en dos métodos diferentes, los eventos que hemos decidido que el portlet consuma.

JSPs

Como últimos dos pasos los jsps de renderizado, el del publicador simplemente va a tener dos botones para los dos tipos de eventos que va a publicar y el del consumidor un poco de lógica para mostar una u otra cosa dependiendo si el último evento consumido ha sido de un tipo o de otro

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

<portlet:defineObjects />



<div>

        <p>Use the form below to create a contact. The data is sent over to other portlets into a HashMap.</p>

        <%-- The action name must be passed 
as a param, not as an actionURL attribute. Fix will come with Liferay 5.0.2 --%>

        <form   
method='post' action='
<portlet:actionURL><portlet:param name="javax.portlet.action" value="contactInfo.add" /></portlet:actionURL>'>

                <fieldset>

                        <table>

                                <tbody>

                                <tr>

                                        <th><label>Contact name:</label></th>

                                        <td><input name="name" /></td>

                                </tr>

                                <tr>

                                        <th><label>Contact e-mail:</label></th>

                                        <td><input name="email" /></td>

                                </tr>

                                <tr>

                                 <td colspan="2"><input type="submit" value="Add contact" /></td>

                                </tr>

                                </tbody>

                        </table>

                </fieldset>

        </form>

        <form   method='post' 
action='<portlet:actionURL>
<portlet:param name="javax.portlet.action" value="contactInfo.delete" /></portlet:actionURL>'>

                <fieldset>

                        <table>

                                <tbody>

                                <tr>

                                        <td colspan="2"><input type="submit" value="Delete contact" /></td>

                                </tr>

                                </tbody>

                        </table>

                </fieldset>

        </form>

</div>

y el del consumidor

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

<%@page import="mypackage.ContactInfoBean"%>

<portlet:defineObjects/>



<%!ContactInfoBean contactInfo = null;%>

<% 

//retrieve the object from the session
if(renderRequest.getPortletSession().getAttribute("contactInfo") != null)
{

        contactInfo = (ContactInfoBean) renderRequest.getPortletSession().getAttribute("contactInfo");
}



if (contactInfo != null) { %>

        <div>

                <table>

                <tbody>

                        <tr>

                                <th>Name:</th>

                                <td><%=contactInfo.getName() %></td>

                        </tr>

                        <tr>

                                <th>Email:</th>

                                <td><%=contactInfo.getEmail() %></td>

                        </tr>

                </tbody>

                </table>

        </div>

<% } else {

        %><p>No contact information.</p><%

}

%>

 

00

More Blog Entries

0 Comments