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="https://www2.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="https://www2.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("https://www2.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("https://www2.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="{https://www2.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="{https://www2.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><% } %>