Last updated March 06, 2011 11:56, by pajoma
Feedicon  

Inter-Portlet Communication with Reverse Ajax (using Spring, DWR, and Liferay)


Note: this guide has been updated. You should also check out this guide we have added to the Liferay wiki.

This guide assumes that you have setup your project according to this text. All required libraries are already in place, and the main configuration has been done. Regarding the latter, we only have to update the configuration of the portlets (we need now two portlets, since we want them communicate) and the global application context.

The task is simple: We have two portlets, and when the user clicks a link in the first portlet, the content of the second portlet should change. We have to use server-side Inter-Portlet Communication (IPC), either because we want to route all requests through our security layer, or our communication should be independent from the user (e.g. distributed to all currently connected clients). The following guide will explain how to setup a project with all the needed requirements (using maven) how to configure Spring and DWR using mostly annotations, and finally how to enable communication between all the different components.

The action (clicking the link) triggers actually communication on three channels. First, an action is send to the server (client -> portlet). Second, this action is distributed as event with a certain identifier, and an EventListener of another portlet is reacting to it (IPC). And finally, the event is sent back to a client using reverse ajax (portlet -> client. But first, let's start with setting up the project.

Updating the configuration files


Enable Direct Web Remoting (DWR)

The first thing we do is to add the DWR servlet to the Web.xml. Copy the following code (but this time we don't replace) Source: web.xml

<!-- DWR for AJAX support -->
	<servlet>
		<servlet-name>DwrServlet</servlet-name>
		<servlet-class>org.directwebremoting.spring.DwrSpringServlet
		</servlet-class>
		<init-param>
			<param-name>debug</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>activeReverseAjaxEnabled</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>pollAndCometEnabled</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>maxWaitAfterWrite</param-name>
			<param-value>-1</param-value>
		</init-param>
		  <init-param>
		    <param-name>allowGetForSafariButMakeForgeryEasier</param-name>
		    <param-value>true</param-value>
		  </init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>DwrServlet</servlet-name>
		<url-pattern>/dwr/*</url-pattern>
	</servlet-mapping>
In the web.xml we configure the DwrSpringServlet to map all requests starting with /dwr/* to the correct javascript files. We don't use the SpringDispatcherServlet, could be also an option but I guess this is a bit overhead in the Portlet environment (since we are using directly the ViewRenderer for everything else). The parameters enable reverse ajax based on poll and comet (which means we keep the connection open for a certain time frame, and request a new connection to the server after that). Check this link for a detailed discussion of these options.

In the global application context, we enable DWR. We have to update the header to include the namespaces and schema of DWR. Source: applicationContext.xml

<!-- replace the header -->
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:dwr="http://www.directwebremoting.org/schema/spring-dwr"

	xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.directwebremoting.org/schema/spring-dwr
    	http://www.directwebremoting.org/schema/spring-dwr-3.0.xsd">


<!-- add the following -->

	<dwr:configuration />

	<dwr:controller id="dwr" debug="true">
		<dwr:config-param name="activeReverseAjaxEnabled" value="true" />
		<dwr:config-param name="pollAndCometEnabled" value="true" />
		<dwr:config-param name="maxWaitAfterWrite" value="-1" />
	</dwr:controller>

	<dwr:annotation-config />
	<dwr:url-mapping />
As you might have recognized, we repeat the configuration of reverse ajax here. The options listed above work if we use a SpringDispatcherServlet, but not in the configuration listed here. Perhaps some can explain this to me...

When you deploy the portlets to Liferay, you should be able to navigate to http://localhost:8080/MyPortletName-0.0.1-SNAPSHOT/dwr/index.html and see the DWR debug page.

Configure Inter-Portlet Communication

In the portlet configuration files, we have list all the events our portlets can produce or consume. A good overview how to configure this can be found at http://developers.sun.com/portalserver/reference/techart/jsr286/jsr286.html. In this case, we define only one event "envision.link" in the namespace "http://liferay.com/events", which is fired by the EventProducer and understood by the EventConsumer.

Source: portlet.xml


<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"
	version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd 
	 					http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd">

	<portlet>
		<portlet-name>EventConsumer</portlet-name>
		<display-name>Event Consumer Portlet</display-name>

		<portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class>

		<expiration-cache>0</expiration-cache>

		<supports>
			<mime-type>text/html</mime-type>
			<portlet-mode>VIEW</portlet-mode>
		</supports>

		<supported-processing-event>
			<qname xmlns:x="http://liferay.com/events">x:envision.link</qname>
		</supported-processing-event>
	</portlet>

	<portlet>
		<portlet-name>EventProducer</portlet-name>
		<display-name>Hello Producer Link</display-name>

		<portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class>

		<supports>
			<mime-type>text/html</mime-type>
			<portlet-mode>VIEW</portlet-mode>
		</supports>


		<supported-publishing-event>
			<qname xmlns:x="http://liferay.com/events">x:envision.link</qname>
		</supported-publishing-event>
	</portlet>

	<!-- EVENTS -->
	<event-definition>
		<qname xmlns:x="http://liferay.com/events">x:envision.link</qname>
		<value-type>java.lang.String</value-type>
	</event-definition>
	
</portlet-app>

As already mentioned in the Guide for configuring Spring, we need to define the context for each portlet. In this case, we create the two files "EventProducer-portlet.xml" and "EventConsumer-portlet.xml". The content remains (as described in the other guide), but we have to update the paths for annotation scanning to point to the according packages. We additionally create the following two portlets within these packages.

Source: EventProducerPortlet.java

@Controller
@RequestMapping("VIEW")
public class EventProducerPortlet
	
	@RequestMapping  // default (action=list)  
	   public String handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception {
		return "producer";
               }
}

Source: EventConsumerPortlet.java

@Controller
@RequestMapping("VIEW")
public class EventConsumerPortlet  implements ServletContextAware {
		
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;	
         }

	@RequestMapping  // default (action=list)  
	   public String handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception {
		return "consumer";
          }
}

The EventProducer will return as default view the content of the file producer.jsp, which initially looks like this

<%@ taglib prefix="portlet" uri="http://java.sun.com/portlet_2_0"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page contentType="text/html" isELIgnored="false"%>

<a href="javascript: void(0)" > Clicking this link triggers sending an event</a>
Not really much to say about this. We have only one link, and nothing happens when we click it. Note that we are importing the portlet Tags for JSP, which should be defined in the web.xml The JSP file consumer.jsp looks a bit more interesting.

<%@ page contentType="text/html" isELIgnored="false" %>
<%@ taglib uri='http://java.sun.com/jsp/jstl/core' prefix='c'%>
<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %>

 <%-- DWR AJAX setup --%>
<script type='text/javascript' src="<%=renderRequest.getContextPath()%>/dwr/engine.js"/></script>
<script type='text/javascript' src="<%=renderRequest.getContextPath()%>/dwr/util.js"/></script>


<script type="text/javascript">
<!--
Ext.onReady(function() {
	
	if (!dwr.engine._isActiveReverseAjax) {
		dwr.engine.setActiveReverseAjax(true);
		dwr.engine.setNotifyServerOnPageUnload(true);
	};
	

	Ext.EventManager.on(window, 'beforeunload', dwr.engine._unloader);
}); 
//-->
</script>

Here comes the message: <span id="messageOut"> </span>

We already enable reverse ajax here, which is normally done using the Body-Tag of HTML (e.g. body onLoad="dwr.engine.setActiveReverseAjax(true);") In portlets we are not allowed to use this tag (and others like html or head as well). We can also not simply call window.onload, since we would then overwrite other calls of other portlets (and we should always try to avoid any potential conflicts with other portlets). In this case, we are chaining the calls, as described here.

Communication Client -> Portlet

Our first task is to enable the communication from the client to the server. As mentioned earlier, we simply want to click a link to update the content in another portlet. We don't want to rerender anything, which means we have to invoke an action request (and not a render request). On the server side, we add the following method the file "EventSenderPortlet.java":

Source: EventProducerPortlet.java

	@ActionMapping(params = "actionID=eventClicked")
	public void sendEvent(SessionStatus status, ActionRequest request, ActionResponse response) {
		QName qname = new QName("http://liferay.com/events","envision.link");
         	response.setEvent(qname, "Current Timestamp: "+System.currentTimeMillis());
        	status.setComplete();
	}
The QName has to be equal to the one defined in the portlet.xml. We simply use the current timestamp as content of the message (it better illustrates the automatic updates later on) Now we have to be sure that the link in the JSP file is actually triggering the method. We modify the producer.jsp like this:

<%@ taglib prefix="portlet" uri="http://java.sun.com/portlet_2_0"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page contentType="text/html" isELIgnored="false"%>

<portlet:defineObjects/> 
<portlet:actionURL var="sendEventURL">
	<portlet:param name="actionID" value="eventClicked" />
</portlet:actionURL>


<script type="text/javascript">
<!--
Ext.onReady(function() {
	var session = "${sendEventURL}"+"?dwrSession="+dwr.engine._scriptSessionId;

	session = session.replace(/&amp;/g, "&");
	
	Ext.get('trigger').on('click', function() {
		Ext.Ajax.request({ url : session });
	}); 
}); 
//-->
</script>
<div id="trigger">
 <a href="javascript: void(0)" > Clicking this link triggers sending an event</a>
</div>

Using the tag portlet:actionURL allows us for defining the URL which points to our server-side EventProducerPortlet.java. By adding the paramer "actionID" we can further constrain which method should be invoked (by re-using the attribute "actionID" for the annotation @ActionMapping). Clicking on the link should invoke a HTTP call to the server, we re-use the XMLHttpRequest object for this (for everything besides Internet Explorer).

This should now already work. Just use a breakpoint in the method sendEvent() to check if clicking the link is actually forwarded to this method.

Inter-Portlet Communication

The actual IPC is rather simple compared to the client-server communication based on Ajax. In the EventProducerPortlet.java, we create a new Event with the name {http://liferay.com/events}envision.link. We now simply have to make sure that someone is listening to this event. We add the following method to the EventConsumerPortlet.java to achieve this.

	@EventMapping(value ="{http://liferay.com/events}envision.link")
	public void getEvent(EventRequest request, EventResponse response) {
		  Event event = request.getEvent();
                  String message = event.getValue();
                  
                  System.out.println(message);
       }

That's it. Clicking the link should now print the message "Current Timestamp: [enter weird long number here]".

Communication Portlet -> Client

Now it's starting to get interesting. The message fired by the event should now be forwarded to the client. In fact, we want to replace the content of the element with the id "messageOut" to print this message, without requiring a rerendering of the portlet. Sending information from a server to the Browser is usually not possible. The built-in reverse ajax functionality of DWR helps us to circumvent this problem. In fact, we just have to add a few lines of code here (but the tricky part happens in the backend, well hidden away from us)

We don't have to change anything for in the consumer.jsp (since we have already enabled reverse ajax). We add the following three lines to the method getEvent() in the EventConsumerPortlet.java

The following code is deprecated Check the Lifaray wiki to see updated code examples:

                            ServerContext serverContext = ServerContextFactory.get(servletContext);
		  Collection<ScriptSession> scriptSessionsByPage = serverContext.getAllScriptSessions();
		  Util utilAll = new Util(scriptSessionsByPage);
		  utilAll.setValue("messageOut", event.getValue());

Here, we send the message out to all connected clients (try it out, clicking the link in one browser should change the content in the other browser). All we do is to change the content of the element with the identifier "messageOut" (as defined in the consumer.jsp) to the content passed on by the event.

That's it. Clicking the link should update the other portlet in view without the need to reload anything. The actual communication between the portlets happens on the server, which means we can distribute message to all connected clients or to process the event before updating the portlet's content.