Tuesday Dec 08, 2009

Client-side Eventing Example

In the previous blog, Eventing in AJAX portlets, we saw an overview of client side eventing mechanism in OSPC and WebSpace, which works like a simple yet powerful extension of JSR-286 eventing.

Lets look at an example to see how this all works.

EventGeneratorAjaxPortlet in this example, takes a zip code through a text field and has a button "Get GeoCode".

<input name="getbtn" id="getbtn" value="Get Geocode" type="button" onclick="<portlet:namespace/>PortletObj.processAction('<portlet:resourceURL/>')"/>

The onClick of the button calls processAction method on a javacript object called PortletObj which is prefixed with portlet's namespace. So first define a processAction javascript function just like one would have done while overriding the GenericPortlet's processAction method in the Portlet's Java class on server side.

<portlet:namespace/>PortletObj = { portletReq : null, processAction : function(updateURL) { portletReq = new XMLPortletRequest("<portlet:namespace/>"); portletReq.onreadystatechange = function() { <portlet:namespace/>PortletObj.render(); }; //collect information and prepare data to POST to server ... ... portletReq.open("POST", updateURL, true); portletReq.send(data); }, render : function() { var result = JSON.parse(portletReq.responseText, null); // process the response and modify the DOM ... ... var qName = {uri: "http://www.example.com/clientevents", name : "MyEvent" }; var eventPayload = {}; eventPayload.x = result.x; eventPayload.y = result.y; portletReq.setEvent(qName, eventPayload); } };

The processAction method, uses XMLPortletRequest which is initialized using the portlet's namespace. Portlet container automatically makes this available by adding the javascript file to the portlet during deployment. It is only required to include the script in the jsp from under <context-path>/js/ like this,

<script type="text/javascript" src="<%=renderRequest.getContextPath()%>/js/XMLPortletRequest.js"> </script>

In the example above, function names processAction and render are only a matter of following the JSR-286 pattern. The names of these functions could have been anything. More important is to note how "setEvent" is called on the XMLPortletRequest and an arbitrary event payload can be passed as a JSON object.

It is possible to use qName without the uri. The new RFE makes it possible to call setEvent with a string as an event name instead of requiring QName.

Show/Hide JSP

The serveResource method of the portlet computes latitude and longitude for the zip code, using Yahoo geocode service and returns a JSON object from its serveResource method.

Show/Hide serveResource

EventConsumerAjaxPortlet in this example, consumes the event generated by EventGeneratorAjaxPortlet, by implementing a PortletObj.processEvent javascript function. This function is called in response to the XMLPortletRequest.setEvent call in the processAction of event generator.

The wiring is taken care by Portlet Container at runtime as we will see later, but is done in portlet.xml by the developer as shown below.

<portlet> <portlet-name>EventGeneratorAjaxPortlet</portlet-name> ... ... <supported-publishing-event xmlns:x='http://www.example.com/clientevents'> <qname>x:ZipEvent</qname> </supported-publishing-event> </portlet> <portlet> <portlet-name>EventConsumerAjaxPortlet</portlet-name> ... ... <supported-processing-event xmlns:x='http://www.example.com/clientevents'> <qname>x:ZipEvent</qname> </supported-processing-event> </portlet> <event-definition xmlns:x='http://www.example.com/clientevents'> <qname>x:ZipEvent</qname> <value-type>com.sun.portlet.ClientEvent</value-type> </event-definition>

The portlets could have been packaged separately and may have separate portlet.xml. Also, since this is same as server-side eventing, it is possible to use tools like NetBeans PortalPack, and to use the eventing storyboard to wire the portlets visually instead of handcoding the xml.

Typically, this is how a consumer is implemented.

<portlet:namespace/>PortletObj = { portletReq : null, processEvent : function(eventObj) { portletReq = new XMLPortletRequest("<portlet:namespace/>"); portletReq.onreadystatechange = function() { <portlet:namespace/>PortletObj.render(); }; //process the event payload and prepare data to POST to server ... portletReq.open("POST", '<portlet:resourceURL/>', true); portletReq.send(data); }, render : function() { var response = JSON.parse(portletReq.responseText, null); // process response and modify the DOM ... } };

The namespaced PortletObj.processEvent function is mandatory and is the only forced naming convention required to be followed. It is not required to implement render function. It was possible to include all render code, instead of a call to render, in the callback function. But again, as a matter of convention and pattern, its much cleaner to implement render.

Show/Hide JSP

The processEvent function gets zip, latitude and longitude in the event payload. It in turn calls serveResouce of the portlet to get all the pictures recently uploaded in 10 mile radius of the given latitude/longitude, using Flickr's REST service.

Show/Hide serveResource

How it works
Portlet Container reads portlet.xml during deployment and recognizes client-side events from the special marker class com.sun.portlet.ClientEvent. While rendering the portlet, it generates a namespaced javascript event queue, <portlet:namespace/>EventQueue, for the generator and populates it with a list of consumers on the page. When XMLPortletRequest.setEvent is called, it in turn calls setEvent on the generator's event queue. Generator's event queue loops through consumer list and calls <portlet:namespace/>PortletObj.processEvent on each. This is why it is mandatory to implement processEvent function to be able to consume events.

Monday Nov 30, 2009

Eventing in AJAX Portlets

Also known as client side eventing, it becomes a necessity when you have multiple AJAX portlets on a portal page, and since the whole page is never submitted, the JSR-286 Portlet Spec 2.0 eventing (server-side) is not any useful.

We implemented a new eventing extension, for client side events, in Open Source Portlet Container (OSPC), through which it made its way into WebSpace 10.0.

A developer, who is writing a Portlet Spec 2.0 compliant portlet, follows a pattern, certain conventions and implements specific interfaces. Then s/he creates a deployment descriptor, packages the portlet as a webapp and deploys it.
These are the steps that a typical 2.0 portlet development process will involve:

  • implement processAction method for generator- generating portlet calls setEvent method here, to generate an event
  • implement processEvent method for consumer - called by the portlet container on a portlet which consumes this event
  • implement render method (both) - renders content
  • create portlet.xml - defines elements for eventing

For client side eventing, the developer follows exact same steps, albeit on client-side, for example in the jsp.
In javascript, the developer implements a processAction and render functions for an event generating portlet. In processAction or render function, setEvent function is called by the developer. For event consumer portlet, javascript functions processEvent and render are implemented. Then the developer writes the deployment descriptor in the same way as defined in the Portlet 2.0 spec, except that the value-type of the event is identified by a special marker class com.sun.portlet.ClientEvent.

When portlets are deployed, portlet container reads the deployment descriptors. If portlet supports publishing event and if event's value type is the special marker class, then container automatically generates a javascript "EventQueue".
When portlet container reads the deployment descriptor for a portlet that supports processing the event and if event value type is the special marker class, then container automatically puts a call to the processEvent function of this portlet in the event queue of the generating portlet.

When generating portlet calls setEvent function, with the event payload as an argument, EventQueue of the generator in turn calls processEvent function of all the portlets which support processing this event.
Thus, wiring of the portlets is handled by container in the exact same way as it would have been done for server-side eventing, from the deployment descriptor. This happens transparently to the developer. 

Salient Features of this mechanism

  • Follows convention over configuration paradigm
  • Follows Portlet 2.0 spec defined pattern and conventions, but mimics it on the client side
  • Works like an extension of the specification
  • Eliminates developer learning curve
  • Auto wiring of portlets, transparent to the developer (handled by OSPC).
  • Arbitrary Event payload (similar as on server side)
  • Each generator portlet has its own namespaced event queue (auto generated)
  • Event generator portlet remains agnostic of the consumers.
  • Since the event queue of the generator portlet is populated by the container at runtime, no other code needs to change when new consumer portlets are deployed or added to the portal page.
  • Easily toolable by existing tools, because it follows the same conventions as the tools expect for server side eventing
  • Well defined name spacing and type definitions allows for deploy time checking
  • Use of W3C defined QName allows for standard name spacing
  • XMLPortletRequest (XPR) provides a wrapper over XMLHttpRequest (XHR), thus easing development further
In my next blog, we will take a closer look through an example. In the meantime, you can also take a look at this document as well as check out Deepak Gothe's blog on XPR.

Thursday Oct 11, 2007

OpenSSO and Liferay Integration Prototype

I would prefer to write a short blog and document this somewhere rather than put all of this in a blog, but was not sure where to put it. So this has become a blogument ;)
OpenSSO is an open source project for Single Sign-On. Liferay is an open source portal from Liferay, Inc.
Liferay portal already integrates with CAS single sign-on server. This blogument describes how Liferay portal can be integrated with OpenSSO for single sign-on.

OpenSSO server
  • Download the OpenSSO server
  • For this prototype, FAM 8.0 Build 1 Zip was used. (FAM stands for Federated Access Manager)
  • Turn off the security manager. On Glassfish v2, it is off by default. On AS9.1, access the admin console and turn it off.
  • If security manager needs to be on, then server.policy must be edited as described here. You may need a few more permissions than listed here.
  • Unzip the file and deploy the deployable-war/fam.war as /opensso
  • Access the server (http://opensso-host:port/opensso) to invoke the configurator. Once configured, it will take you to login page where you can login as "amadmin" user.
OpenSSO client
There are 2 ways for an application to leverage OpenSSO as a client
  1. Using client sdk from the downloaded zip (libraries/jars/famclientsdk.jar)
  2. Using web services or REST based services.
There are advantages/disadvantages in both. Client sdk comes with its own cache and a comprehensive set of Java APIs. So you can register SSO token event listners etc. Also you need to configure AMConfig.properties into your classpath.
If you use REST based identity services, then application is responsible for maintaining its own sessions and data cache. But using REST, the client does not have build and runtime dependency on OpenSSO jars. Thanks to Aravindan for providing the info on this latest and greatest feature.

The REST based services were used for this prototype. Currently, only authenticate/authorize/attributes/log are the REST operations available. So there is no way to validate a client session with server or to get a subjectid for an authenticated user. An issue 1079 has been opened with OpenSSO for this enhancement.
For the time being, the subjectid is extracted from the sso cookie and the following REST operation is used,

This returns user details in the following form:
userdetails.token.id=Et2RTHUb+C9TTNipgRqR0MECgg=@AAJTSQACMDE=# userdetails.attribute.name=sn userdetails.attribute.value=user1 userdetails.attribute.name=cn userdetails.attribute.value=user1 userdetails.attribute.name=objectclass userdetails.attribute.value=person userdetails.attribute.value=inetorgperson userdetails.attribute.value=top userdetails.attribute.value=organizationalperson userdetails.attribute.value=inetuser userdetails.attribute.name=employeenumber userdetails.attribute.value=5 userdetails.attribute.name=uid userdetails.attribute.value=user1 userdetails.attribute.name=userpassword userdetails.attribute.value=s9qne0wEqVUbh4HQMZH+CY8yXmc= userdetails.attribute.name=givenname userdetails.attribute.value=user1 userdetails.attribute.name=mail userdetails.attribute.value=user1@fam.com userdetails.attribute.name=inetuserstatus userdetails.attribute.value=Active

It is possible to get this information in the form of xml by using this url:

Authentication filter
First, write an auth filter which redirects a non-authenticated user to the OpenSSO server's login page. An authenticated user is the one who has the sso cookie.
The code is shown below:
package com.liferay.portal.servlet.filters.sso.fam; import java.io.IOException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.Cookie; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.FilterChain; public class FAMFilter implements Filter { String loginUrl = null; String logoutUrl = null; String idServicesUrl = null; String ssoCookieName = null; public void init(FilterConfig filterConfig) throws ServletException { loginUrl = filterConfig.getInitParameter("loginUrl"); logoutUrl = filterConfig.getInitParameter("logoutUrl"); ssoCookieName = filterConfig.getInitParameter("ssoCookieName"); } public void destroy() {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // If any of the filter params are null or empty // then skip doing anything because it is misconfig if(loginUrl == null || loginUrl.length() == 0 || logoutUrl == null || logoutUrl.length() == 0 || ssoCookieName == null || ssoCookieName.length() == 0) { chain.doFilter(request, response); } HttpServletRequest httpReq = (HttpServletRequest)request; HttpServletResponse httpRes = (HttpServletResponse)response; String pathInfo = httpReq.getPathInfo(); if (pathInfo != null && pathInfo.indexOf("/portal/logout") != -1) { HttpSession httpSes = httpReq.getSession(); httpSes.invalidate(); httpRes.sendRedirect(logoutUrl); } else { if(isAuthenticated(httpReq)) { chain.doFilter(request, response); } else { httpRes.sendRedirect(loginUrl); } } } private boolean isAuthenticated(HttpServletRequest request) { boolean authenticated = false; Cookie cookie = null; Cookie[] cookies = request.getCookies(); int nCookies = cookies == null ? 0 : cookies.length; for(int i = 0; i < nCookies; i++) { if(ssoCookieName.equalsIgnoreCase(cookies[i].getName())) { cookie = cookies[i]; break; } } if(cookie != null) { authenticated = true; request.getSession().setAttribute("subjectid", cookie.getValue()); } return authenticated; } }

Next add the filter to web.xml:

<filter-name>FAM Filter</filter-name>

<filter-name>FAM Filter</filter-name>

Now write a FAMAutoLogin class which implements the AutoLogin interface provided by Liferay. This class implements the "login" method of the interface.
In the implementation, it gets the subjectid from the session (which has been stored off by the auth filter).
Then makes a REST call to get the user attribtues.
Liferay needs firstName, lastName, screenName, email for creating a user profile dynamically in its database, if one does not exist. If the authenticated user (from OpenSSO) is not found, then UserLocalServiceUtil is used to add the user to Liferay database.

The source is shown below:
package com.liferay.portal.security.auth; import java.util.Map; import java.util.HashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.liferay.portal.PortalException; import com.liferay.portal.NoSuchUserException; import com.liferay.portal.model.User; import com.liferay.portal.SystemException; import com.liferay.portal.service.UserLocalServiceUtil; import com.liferay.portal.util.PortalUtil; import com.liferay.portal.util.PrefsPropsUtil; import com.liferay.util.PwdGenerator; import com.liferay.util.ldap.LDAPUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.net.URLEncoder; import java.net.URLConnection; import java.io.BufferedReader; import java.net.URL; import java.io.InputStreamReader; import java.io.InputStream; public class FAMAutoLogin implements AutoLogin { public FAMAutoLogin() {} public String[] login(HttpServletRequest req, HttpServletResponse res) throws AutoLoginException { String[] credentials = null; long companyId = PortalUtil.getCompanyId(req); String idServicesUrl = null; try { idServicesUrl = PrefsPropsUtil.getString(companyId, "fam.idservices.url"); } catch(PortalException pe) { throw new AutoLoginException(pe); } catch(SystemException se) { throw new AutoLoginException(se); } String subjectid = (String)req.getSession().getAttribute("subjectid"); if(subjectid == null) { //this should not happen since filter will have already blocked return credentials; } String errorMsg = null; Map nameValues = new HashMap(); String line = null; try { subjectid = URLEncoder.encode(subjectid, "UTF-8"); String url = idServicesUrl + "/attributes?subjectid=" + subjectid; URL iurl = new URL(url); URLConnection connection = iurl.openConnection(); BufferedReader reader = new BufferedReader( new InputStreamReader((InputStream)connection.getContent())); while((line = reader.readLine()) != null) { //each line is returned as x=y String[] parts = line.split("="); //should not happen but we never know if(parts == null || parts.length != 2) { //skip this line continue; } String attrName = null; String attrValue = null; if(parts[0].endsWith("name")) { attrName = parts[1]; //next line must be value line = reader.readLine(); if(line == null) { //something wrong - name must be followed by value throw new AutoLoginException( "Error reading user attributes"); } //each line is returned as x=y parts = line.split("="); //should not happen but we never know if(parts == null || parts.length != 2 || !parts[0].endsWith("value")) { attrValue = null; } else { attrValue = parts[1]; } nameValues.put(attrName, attrValue); } } } catch(java.io.UnsupportedEncodingException use) { throw new AutoLoginException(use); } catch(java.net.MalformedURLException me) { throw new AutoLoginException(me); } catch(java.io.IOException ioe) { throw new AutoLoginException(ioe); } //Liferay user must have these attrs String firstName = (String)nameValues.get("cn"); String lastName = (String)nameValues.get("sn"); String screenName = (String)nameValues.get("givenname"); String email = (String)nameValues.get("mail"); if(email == null || email.length() == 0) { throw new AutoLoginException("No email set for user"); } User user = null; try { user = UserLocalServiceUtil.getUserByEmailAddress(companyId, email); } catch(NoSuchUserException nsue) { try { user = addUser(companyId, screenName, firstName, lastName, email); } catch(Exception e) { throw new AutoLoginException(e); } } catch(Exception e) { throw new AutoLoginException(e); } credentials = new String[3]; credentials[0] = String.valueOf(user.getUserId()); credentials[1] = user.getPassword(); credentials[2] = Boolean.TRUE.toString(); return credentials; } protected User addUser(long companyId, String screenName, String firstName, String lastName, String email) throws PortalException, SystemException { long creatorUserId = UserLocalServiceUtil.getDefaultUserId(companyId); User user = null; try { user = UserLocalServiceUtil.addUser(creatorUserId, companyId, true, "", "", false, screenName, email, java.util.Locale.ENGLISH, firstName, "", lastName, 0, 0, true, 1, 1, 1970, "", 0, 0, false); } catch (Exception e){ _log.error( "Problem adding user with screen name " + screenName + " and email address " + email, e); } return user; } private static Log _log = LogFactory.getLog(FAMAutoLogin.class); }

Liferay Hooks
Liferay portal provides hooks to plugin auto login classes.
Edit (or create if one does not exist in the deployment) portal-ext.properties and add the following:
auto.login.hooks=com.liferay.portal.security.auth.FAMAutoLogin,com.liferay.portal.security.auth.CASAutoLogin,com.liferay.portal.security.auth.NtlmAutoLogin,com.liferay.portal.security.auth.OpenIdAutoLogin,com.liferay.portal.security.auth.RememberMeAutoLogin fam.idservices.url=http://opessso-host:port/opensso/identity

  1. Access http://opensso-host:port/opensso/
  2. Login as amadmin
  3. Click on the root realm and goto Subjects tab
  4. Add a new user
  5. Now click on the new user's name to edit user profile
  6. Enter an email for the new user (this is important since Liferay needs email)
  7. Logout from OpenSSO and try login as the new user to verify
  8. Access Liferay portal to be redirected to OpenSSO login page
  9. Login as the new user to be redirected back to Liferay portal
  10. Accept the terms and conditions (first time only) to see the Liferay portal pages
The prototype demonstrates how Liferay can be integrated to leverage OpenSSO. For production, use of client sdk may be considered. Liferay can also integrate with LDAP and import membership information. OpenSSO can integrate with various user repositories, so similar implementation can be provided to import membership from OpenSSO user repositories. Even more desirable scenario would be if Liferay can fetch membership at runtime instead of importing it to a local datastore and then struggling to keep it in sync with corporate user repository. The auth filter can be further enhanced to allow anonymous/guest access and then writing an OpenSSO login portlet.

  1. Setting_up the Extension Environment
  2. Integrating Liferay With CAS
  3. Developing a Custom Authentication System
  4. Liferay LDAP integration
  5. OpenSSO project page

Prashant Dighe


« July 2016