Monday, 24 March 2008

PortalDelegateServlet – Servlet Session Sharing in Liferay

After finishing my blog entry Liferay Session Sharing Demystified, i found a forum post which pointed to the liferay Jira of LEP-2297. Turns out Liferay 4.3.x has built in support for running servlets from within the Portal context! Yes... yet another wonderful but undocumented feature in Liferay.

In a nutshell, you configure your servlet to register itself with the built in PortalDelegatorServlet. This servlet (in the portal’s context) makes a cross-context call to your servlet, and provides your servlet with the portal session.

From a session sharing point of view, this behaves exactly the same as portlets using the portal session (i.e. portlets set to non-private session)

  • full access to portal attributes
  • full access to non-private portlet attributes, regardless of WAR file
  • Shared attributes set by the servlet will be made visible to the private portlets
  • no access to the WAR session attributes (servlets and private portlets in the same war)

Configuration Example

The example configuration in the Jira is based on the code that was submitted, so here is an example based on the Liferay’s distribution. In your servlet’s web.xml file, configure the following servlet.

<servlet>
  <servlet-name>springdemo</servlet-name>
  <servlet-class>
    com.liferay.portal.kernel.servlet.PortalDelegateServlet
  </servlet-class>
  <init-param>
    <param-name>servlet-class</param-name>
    <param-value>
      org.springframework.web.servlet.DispatcherServlet
    </param-value>
  </init-param>
  <init-param>
    <param-name>sub-context</param-name>
    <param-value>downloads</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

This example registers your servlet example.FooServlet under the key foo.do. (If a sub-context is not provided, the servlet-name will be used as the key instead.) When a request is made to /delegate/foo.do , the PortalDelegateServlet will process the URL, lookup the key foo.do, find your servlet, and make a cross context call to your servlet to run the service method. If you don’t like the /delegate servlet-mapping, you can change this in the portal’s ROOT.war web.xml.

Limitation

One confusing thing is that the sub-context is used as the key to store the servlet, while only the first token of the URI is used as the key to lookup the servlet. Which means if you try to set the sub-context to myapp/foo.do then navigate to the URI of /delegate/myapp/foo.do , it won’t work, because the PortalDelegateServlet will attempt to lookup the map using the value myapp, and find nothing. Bottom line: the sub-context must be only single level deep.

Using Spring DispatcherServlet

If you use Spring as your front-controller, you can configure more sophisticated URLs. The DispatcherServlet can be configured for use with the PortalDelegateServlet as follows:

<servlet>
  <servlet-name>springdemo</servlet-name>
  <servlet-class>
    com.liferay.portal.kernel.servlet.PortalDelegateServlet
  </servlet-class>
  <init-param>
    <param-name>servlet-class</param-name>
    <param-value>
   org.springframework.web.servlet.DispatcherServlet
 </param-value>
  </init-param>
  <init-param>
    <param-name>sub-context</param-name>
    <param-value>downloads</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

Then in the springdemo-servlet.xml, you will have something like this, to configure a mapping to the URL of /delegate/downloads/getdocument.do

<bean id="myDownloadController" class="example.SpringServletController" />
<bean id="urlMapping" 
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  <property name="mappings">
    <props>
      <prop key="/downloads/getdocument.do">myDownloadController</prop>  
    </props>
  </property>
</bean>

Caveats

You will need to be careful if you are using third party libraries like Spring. These libraries don’t expect you to be invoking a cross context call, so anything can happen!

For example, when wiring up the Spring DispatcherServlet, writing directly to the response output was fine, but returning a ModelAndView didn’t work. Further investigation revealed that InternalResourceViewResolver was looking in ROOT.war to resolve the location of the view. Which means if the prefix was /WEB-INF/jsp/ , you would have to put this inside /ROOT.war/WEB-INF/jsp , which is clearly not workable.

However, servlets are usually deployed in the portal is to enable some sort of file download, which requires you to write directly to the response output stream anyway, and for this use case the PortalDelegateServlet is the perfect tool.

Sunday, 23 March 2008

Liferay Session Sharing Demystified

Liferay’s session sharing mechanism has always been a bit of a mystery. As with most things Liferay, documentation is minimal, and leaves a lot of questions to be answered. I’d like to share with you what I’ve learnt after testing a bunch of scenarios, and hopefully this will be good reference material for all of you developing portlets on Liferay.

My scenarios focus on how Liferay’s sessions behave for different combinations of the private-session-attributes settings; between the portal, portlets (in the same WAR, and across different WARs) and the servlets in these WARs. This also means that only APPLICATION_SCOPE attributes are relevant to the discussion.

Testing was performed on Liferay 4.3.6, and consisted of basic JSR-168 portlets , and basic Servlet Spec servlets. To emulate code running in the portal scope, a simple servlet Filter was configured on the Portal application.

Private Session True

This is the default setting, in which each WAR has its own session. The session within each WAR is private, and nothing is shared between individual WARs. Portlets and Servlet within the same WAR file will be able to share the session, because they are in the same context. So far, this conforms to the Servlet spec.

Liferay provides an additional functionality under this setting. As the official FAQ states, it also allows shared (namespaced) attributes set by the portal to be visible from portlets. Any session attribute with the configured prefix (e.g. LIFERAY_SHARED_) will be copied over and be visible by the portlets, hence granting private portlets read-access to session attributes set by the portal. This is illustrated in diagram 1.

Diagram 1

Private Session False

All portlets under this setting will share the same session, and also share this same session with the portal. This is the easiest way for portlets in individual WAR files to communicate with each other.

However, the downside to this is that servlets in these WAR files will not be able to communicate with the portlets at all. (This is a question often raised in the forums, and one I struggled with for a while to figure out). The most convenient way to think of this is that portlets with this setting use the portal session, and have no access to the WAR session. This is illustrated in diagram 2.

Diagram 2

Mixed Scenarios

Diagram 3 depicts the interaction between a private and non-private portlet in individual WAR files. Portlet A, being non-private, has full communication with the portal, but cannot talk to Servlet A. Portlet B, being private, has full communication with Servlet B, but only has read-only access to Portal shared attributes.

You will notice arrow from Portlet A to Portlet B is dotted because this communication happening indirectly through the portal session. Portlet A is using the portal session, so any shared attributes that it sets will also be copied over to Portlet B, and can be read.

Diagram 3

Diagram 4 expands on diagram 3, and adds another private portlet to WAR B. Both of the portlets in B are private, meaning they are using the WAR session, so they can freely communicate with each other and the servlet. Same as the previous example, they will be able to read shared attributes set by the portal, or by any non-private portlet like Portlet A.

Diagram 4

Diagram 5 is a bit more interesting. Here, Portlet B has been changed to non-private, meaning it will use the portal session. That is why it can now freely communicate with the portal and Portlet A. However, just like anything else using the Portal session, it cannot read the WAR session of B anymore, even though it is in the same WAR! While this may seem a little counter-intuitive, it is consistent with the behaviour we have seen so far.

Diagram 5

Copying shared attributes to a Servlet

Update: Liferay has a built in mechanism for calling servlets from the portal context, giving them access to the portal session.

You may have noticed so far that the servlets cannot directly read the shared attributes of the portal session. But you can get around this by using a private portlet as a proxy. Diagram 6 shows the flow of how a shared attribute x, set by non-private Portlet A, will be stored in the portal session and subsequently copied to private Portlet B. Portlet B, having write access to WAR session B, can set another attribute y to the same value as x, and y can be read by the servlet.

Diagram 6

Overwriting shared attributes from private portlets?

We know that any session attributes set by private portlets cannot be seen by the portal, or by any other non-private portlets. But what happens if the private portlet sets an attribute of the same key as the shared attribute?

Diagram 7

In Diagram 7, the flow is depicted more accurately by showing two sessions – the Portal session and the WAR session of B. I will walk through the numbered scenario as follows:

  1. Non-private Portlet A sets a session attribute LIFERAY_SHARED_foo to “alice” . This is stored in the portal session.
  2. Private Portlet B reads the attribute LIFERAY_SHARED_foo. Since this is a shared attribute, it has read access, and returns the value “apple” .
  3. Private Portlet B2 now sets the same session attribute LIFERAY_SHARED_foo to “bob” . Because it is a private portlet, the value is written to the WAR session.
  4. When Portlet B tries to read the attribute again, it returns the value “bob”. This shows that values in the WAR session override those copied from the portal. If at this point Portlet A were to set the attribute again, Portlet B cannot see the change.
  5. Portlet B2 now removes the session attribute LIFERAY_SHARED_foo
  6. When Portlet B tries to read the attribute again, it returns the value “alice” from the portal session.

This example shows that when private portlets tries to read an attribute, it will first read the attribute from the WAR session. If it can’t be found, it will try to read from any shared attributes copied from the Portal session.

Summary

I am not a core Liferay developer, so I’m not sure what the underlying implementation really is. However, my observations are consistent with the following summary:

  • Non-private portlets read and write to the Portal session.
  • Private portlets write to their own WAR session.
  • Private portlets try to read from their own WAR session first, then looks up any shared attributes copied from the Portal session.
  • Servlets only have access to the WAR session, and cannot directly access the Portal session. In order to read shared session attributes, servlets need a private portlet in the same WAR file to copy it for them. Or you can configure your servlet to use the portal session by using Liferay's PortalDelegateServlet mechanism.

I hope this has helped you in your understanding of Liferay’s session sharing mechanism. If you are a core Liferay developer, please let me know if I have made any mistakes, or if you have anything else to add. I’d be keen to know if this is still relevant to Liferay 4.4.x