JSR-168: The Portlet Specification
By Enrique Lara, OCI Software Engineer and Consultant
August 2006
Overview
JSR 168: Portlet Specification was constructed "to enable interoperability between Portlets and Portals." This article will focus on the Portlet side of things, by showing how to create a simple Portlet and deploying that into a Portal. We will then refactor and extend the Portlet to see how some of the patterns of Servlet development might be used. Along the way we will exercise different aspects of the API available to a JSR—168 Portlet developer. It is assumed that the reader is familiar with Java, Web applications, and Maven.
Hello World Portlet
We will be building a Portlet which will display random quotes by random authors. Maven 2.x will be used to construct a .war file to be deployed into the Jetspeed-2 JSR—168 compliant Portal.
Java Source
This Portlet generates an HTML fragment manually and uses the GenericPortlet
convenience class as a parent. The GenericPortlet
"...dispatches requests to the doView
, doEdit
or doHelp
method depending on the portlet mode." Our example below will only support the VIEW mode at this point. Note that the CSS class definitions would be provided by the Portal.
- // src/main/java/com/ociweb/portletapi/Quote1HelloWorldPortlet.java
- package com.ociweb.portletapi;
-
- import java.io.IOException;
- import java.io.PrintWriter;
-
- import javax.portlet.GenericPortlet;
- import javax.portlet.PortletException;
- import javax.portlet.RenderRequest;
- import javax.portlet.RenderResponse;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
-
-
- public class Quote1HelloWorldPortlet extends GenericPortlet {
- private static final Log log = LogFactory.getLog(Quote1HelloWorldPortlet.class);
-
- public void doView(RenderRequest request, RenderResponse response)
- throws PortletException, IOException {
- log.debug("doView");
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
- out.print("<div class=\"portlet-section-header\">Quote</div>");
- out.print("<div class=\"portlet-section-body\">");
- out.print("Awesome Quote<br />");
- out.print("by Author");
- out.print("</div>");
- }
- }
Web Application Configuration
A Portlet application is also a Web application — so we must define the web.xml
deployment descriptor.
- <!-- src/main/webapp/WEB-INF/web.xml -->
- <?xml version="1.0" encoding="ISO-8859-1"?>
- <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
- "http://java.sun.com/dtd/web-app_2_3.dtd">
- <web-app>
- <display-name>QuotePortlets</display-name>
- <description>Quote Portlets</description>
- </web-app>
Portlet Configuration
In addition to a web.xml
, we must define a Portlet application deployment descriptor. As described in the specification, "the portlet.xml
contains configuration information for the portlets."
- <!-- src/main/site/examples/portlet.xml -->
- <?xml version="1.0" encoding="ISO-8859-1"?>
- <portlet-app>
- <portlet>
- <portlet-name>Quote1HelloWorldPortlet</portlet-name>
- <portlet-class>com.ociweb.portletapi.Quote1HelloWorldPortlet</portlet-class>
- <expiration-cache>3600</expiration-cache>
- <supports>
- <mime-type>text/html</mime-type>
- <portlet-mode>VIEW</portlet-mode>
- </supports>
- <portlet-info><title>Quote1HelloWorldPortlet</title></portlet-info>
- </portlet>
- </portlet-app>
Building the WAR
We'll let Maven do a lot of the work of setting up the java classpath and packaging things. The Maven-2 project object model file is available in the example project.
With Maven rocking and rolling, we can create our project artifact — a .war
file — by typing the following command:
% mvn package
Deployment into Jetspeed2
Make sure that Jetspeed-2 is installed. The environment variable CATALINA_HOME
is assumed to to match the Jetspeed-2 installation directory. First, deploy the portlet:
% cp target/quote-portlets.war $CATALINA_HOME/webapps/jetspeed/WEB-INF/deploy
Then start the portal, by starting the Servlet Container.
% $CATALINA_HOME/bin/startup
Now, we create a new "page" which indicates to Jetspeed-2 that it should render our Portlet. Jetspeed-2 will also display other Portal goodies (navigation, login box, logos) on this screen:
- <!-- src/main/site/examples/quote.psml -->
- <page id="quotes">
- <defaults
- skin="orange"
- layout-decorator="tigris"
- portlet-decorator="gray-gradient"
- ></defaults>
- <title>Quote Portlet</title>
- <metadata name="title" xml:lang="es">Citas</metadata>
- <metadata name="short-title" xml:lang="es">Quotes</metadata>
- <fragment id="quotes-1" type="layout" name="jetspeed-layouts::VelocityTwoColumns">
- <fragment id="quotes-101" type="portlet" name="quote-portlets::Quote1HelloWorldPortlet">
- <property layout="TwoColumns" name="row" value="0" ></property>
- <property layout="TwoColumns" name="column" value="0" ></property>
- </fragment>
- </fragment>
- <security-constraints>
- <security-constraints-ref>public-edit</security-constraints-ref>
- </security-constraints>
- </page>
The Quote Portlet should now be viewable by navigating to the Quote Page.
Refactor our Basic Portlet
Now we will bring over some programming conventions that have made Web application development and maintenance go a lot more smoothly.
Introduce Model Tier
The first example didn't quite meet the "random" aspect of our requirements, so let's create a Quote domain object and a QuoteService (implementations available in the example project).
- // src/main/java/com/ociweb/portletapi/Quote.java
- package com.ociweb.portletapi;
-
- public interface Quote {
- public String getQuote();
- public String getAuthor();
- }
- // src/main/java/com/ociweb/portletapi/QuoteService.java
- package com.ociweb.portletapi;
-
- import java.util.List;
-
- public interface QuoteService {
- public List getRandomQuotes(int count);
- }
- // src/main/java/com/ociweb/portletapi/Quote2QuoteAPIPortlet.java
- ...
- public void doView(RenderRequest request, RenderResponse response)
- throws PortletException, IOException {
- log.debug("doView");
- response.setContentType("text/html");
- PrintWriter out = response.getWriter();
-
- QuoteService quoteService = (QuoteService) ComponentManager.getObject(QuoteService.class);
- List quotes = quoteService.getRandomQuotes(1);
- Quote quote = (Quote)quotes.get(0);
-
- out.print("<div class=\"portlet-section-header\">Quote</div>");
- out.print("<div class=\"portlet-section-body\">");
- out.print(quote.getQuote() + "<br />");
- out.print("by " + quote.getAuthor());
- out.print("</div>");
- }
- ...
Separate Presentation Logic
Now that we have an object model, maybe we can separate our data acquisition from how we want to display that to the user. In this example, let's use Java Server Pages technology (JSP) to manage the presentation details of the data.
We'll modify the Java code to delegate HTML fragment generation to a JSP.
- // src/main/java/com/ociweb/portletapi/Quote3RenderJSPPortlet.java
- ...
- QuoteService quoteService = (QuoteService) ComponentManager.getObject(QuoteService.class);
- List quotes = quoteService.getRandomQuotes(1);
- Quote quote = (Quote)quotes.get(0);
-
- request.setAttribute("quote", quote);
-
- String jspFilePath = "/WEB-INF/templates/jsp/html/view/quote3-render-jsp.jsp";
- PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(jspFilePath);
- rd.include(request,response);
- ...
The jsp uses the portlet tag library which "enables JSPs ... to have direct access to portlet specific elements." The renderRequest
used below is one such element.
- <!-- src/main/webapp/WEB-INF/templates/jsp/html/view/quote3-render-jsp.jsp -->
- <%@ page session="false" contentType="text/html" import="java.util.*,javax.portlet.*,com.ociweb.portletapi.*" %>
- <%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
- <portlet:defineObjects></portlet:defineObjects>
- <%
- Quote quote = (Quote)renderRequest.getAttribute("quote");
- %>
- <div class="portlet-section-header">Quote</div>
- <div class="portlet-section-body">
- <%= quote.getQuote() %><br />
- by <%= quote.getAuthor() %>
- </div>
Re-Build and Re-Deploy
Jetspeed-2 will re-load the portlet appliction, so the steps for pushing these changes can be as simple as repackaging the .war
file and copying this into the deployment directory. Tomcat may be up and running for this, but Jetspeed-2 seems to prefer that Tomcat is restarted after a redeployment.
% mvn package
% cp target/quote-portlets.war $CATALINA_HOME/webapps/jetspeed/WEB-INF/deploy
% $CATALINA_HOME/bin/shutdown
% $CATALINA_HOME/bin/startup
Respond to Portlet Preferences
In the next few steps we will enhance our example to take advantage of the personalization features defined in the Portlet Specification. Specifically, we will access the portlet preferences as well as allow an end user to edit these values. Additionally, we will also introduce the EDIT mode and the processAction
method.
Configuring Preferences
Instead of hard-coding the number of Quotes to display, we declare that in the Portlet deployment descriptor.
- <!-- src/main/site/examples/preferences-portlet.xml -->
- <portlet>
- ...
- <portlet-preferences>
- <preference>
- <name>nQuotes</name>
- <value>6</value>
- </preference>
- </portlet-preferences>
- ...
- </portlet>
Accessing Preferences
The Java code can access this preference information by querying the PortletRequest
.
- // src/main/java/com/ociweb/portletapi/Quote4PreferencesPortlet.java
- ...
- final QuoteService quoteService = (QuoteService) ComponentManager.getObject(QuoteService.class);
-
- public final static String PREF_NUM_QUOTES = "nQuotes";
- final static int DEFAULT_NUM_QUOTES = 5;
-
- private Integer getNumQuotes(PortletRequest request) {
- Integer nQuotes = new Integer(DEFAULT_NUM_QUOTES);
- try {
- PortletPreferences prefs = request.getPreferences();
- String strNQuotes = prefs.getValue(PREF_NUM_QUOTES, nQuotes.toString());
- nQuotes = Integer.valueOf(strNQuotes);
- } catch(Exception e) {
- log.warn("Problems obtaining preference:" + PREF_NUM_QUOTES, e);
- }
- return nQuotes;
- }
-
-
- public void doView(RenderRequest request, RenderResponse response)
- throws PortletException, IOException {
- log.debug("doView");
-
- int nQuotes = getNumQuotes(request).intValue();
- List quotes = quoteService.getRandomQuotes(nQuotes);
- request.setAttribute("quotes", quotes);
- renderJSP(request, response, "/WEB-INF/templates/jsp/html/view/quote4-preferences.jsp");
- }
- ...
We must also modify our JSP to support this change.
- <!-- src/main/webapp/WEB-INF/templates/jsp/html/view/quote4-preferences.jsp -->
- <%@ page session="false" contentType="text/html" import="java.util.*,javax.portlet.*,com.ociweb.portletapi.*" %>
- <%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
- <portlet:defineObjects></portlet:defineObjects>
- <%
- List quotes = (List)renderRequest.getAttribute("quotes");
- String header = (quotes.size() > 1) ? "Quotes" : "Quote";
- %>
- <div class="portlet-section-header"><%= header %></div>
- <div class="portlet-section-body">
- <ul>
- <%
- Iterator it = quotes.iterator();
- while(it.hasNext()) {
- Quote quote = (Quote) it.next();
- %>
- <li><%= quote.getQuote() %><br />
- by <%= quote.getAuthor() %></li>
- <% } %>
- </ul></div>
Editing Preferences
We want to let users change the number of quotes being displayed. This implies a change of state in the Portlet. The specification denotes that "commonly, during a render request, portlets generate content based on their current state." The doView
and doEdit
methods we have been using thus far originate in the render
method of the GenericPortlet
parent class. The specification describes an action request which is more geared towards state changes. The processAction
method is the part of the Portlet API called to handle such requests, and would probably be a good spot to handle the Portlet preference modifications.
- // src/main/java/com/ociweb/portletapi/Quote4PreferencesPortlet.java
- <pre xml:space="preserve">...
- public void processAction(ActionRequest request, ActionResponse response)
- throws PortletException, IOException {
- log.debug("processAction");
- String nQuotes = request.getParameter("nQuotes");
- PortletPreferences prefs = request.getPreferences();
- prefs.setValue(PREF_NUM_QUOTES, nQuotes);
- prefs.store();
- }
- ...
- </pre>
Since we will be editing the portlet, we would probably be better off exposing this functionality in the EDIT Portlet mode.
- // src/main/java/com/ociweb/portletapi/Quote4PreferencesPortlet.java
- ...
- public void doEdit(RenderRequest request, RenderResponse response)
- throws PortletException, IOException {
- log.debug("doEdit");
-
- Integer nQuotes = getNumQuotes(request);
- request.setAttribute("nQuotes", nQuotes);
- renderJSP(request, response, "/WEB-INF/templates/jsp/html/edit/quote4-preferences.jsp");
- }
- ...
Let's also add a new JSP to present an HTML form to the user. The form action
value is generated using the portlet
tag-lib. This Action URL will flag Jetspeed-2 to call the processAction
method on this Portlet before proceeding with the rendering of the page's defined Portlets.
- <!-- src/main/webapp/WEB-INF/templates/jsp/html/edit/quote4-preferences.jsp -->
- <%@ page session="false" contentType="text/html" import="java.util.*,javax.portlet.*,com.ociweb.portletapi.*" %>
- <%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
- <portlet:defineObjects></portlet:defineObjects>
- <%
- Integer nQuotes = (Integer)request.getAttribute("nQuotes");
-
- %>
- <div class="portlet-section-header">Edit</div>
- <div class="portlet-section-body">
- <form action="<portlet:actionURL ></form>" method="POST">
- Number Quotes: <input type="text" name="nQuotes" value="<%= nQuotes %>" >
- <input type="submit" name="Submit" value="Set Preference" >
- </form>
-
- </div>
In order to give the user access to this new Portlet screen, we must make sure to update Portlet deploment descriptor so that the Portal knows that EDIT is supported. Jetpseed-2 exposes this by adding an Edit link to the Portlet title bar.
- <!-- src/main/site/examples/preferences-view-mode-portlet.xml -->
- <portlet>
- ...
- <supports>
- <mime-type>text/html</mime-type>
- <portlet-mode>VIEW</portlet-mode>
- <portlet-mode>EDIT</portlet-mode>
- </supports>
- ...
- </portlet>
Validate Preference Value
When it comes to storing Portlet Preferences, the Portlet Specification defines the PreferencesValidator
interface. A class implementing this interface may be associated to the Portlet to verify the validity of the preference values.
- // src/main/java/com/ociweb/portletapi/Quote4PreferencesValidator.java
- package com.ociweb.portletapi;
-
- import java.util.ArrayList;
- import java.util.Collection;
-
- import javax.portlet.PortletPreferences;
- import javax.portlet.PreferencesValidator;
- import javax.portlet.ValidatorException;
-
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
-
- public class Quote4PreferencesValidator implements PreferencesValidator {
- private static final Log log = LogFactory.getLog(Quote4PreferencesValidator.class);
-
- public void validate(PortletPreferences preferences) throws ValidatorException {
- log.debug("validate");
-
- Collection keys = new ArrayList();
- keys.add(Quote4PreferencesPortlet.PREF_NUM_QUOTES);
-
- String value = preferences.getValue(Quote4PreferencesPortlet.PREF_NUM_QUOTES, null);
-
- if(value == null) {
- throw new ValidatorException(" Values must not be null.", keys);
- }
- if(value.length() < 1) {
- throw new ValidatorException(" Values must not be blank.", keys);
- }
- try {
- int iValue = Integer.parseInt(value);
- if(iValue < 1) {
- throw new ValidatorException(" Values must not be less than 1.", keys);
- }
- } catch(NumberFormatException e) {
- throw new ValidatorException(e, keys);
- }
-
- log.debug("validate success.");
- }
-
- }
The Portal will use the above implementation to assert that the preferences are valid.
The Portlet deployment descriptor must first declare this association.
- <!-- src/main/site/examples/preferences-val-portlet.xml -->
- <portlet>
- ...
- <portlet-preferences>
- <preference>
- <name>nQuotes</name>
- <value>6</value>
- </preference>
- <preferences-validator>com.ociweb.portletapi.Quote4PreferencesValidator</preferences-validator>
- </portlet-preferences>
- ...
- </portlet>
Summary
JSR—168 paves the way for a personalized user experience with content from a wide range of sources all centralized in one easy-to-find location. The API gives developers a roadmap for producing plug-and-play applications that can instantly add value to a Web site. There are some frameworks available to provide some of the structure available to Servlet programmers (JSF, WSRP, Struts-Bridges, Spring); however, these do seem to be in their infancy and there is a lot of room for new conventions and libraries to simplify the common cases.
References
- [1] JSR 168
http://jcp.org/en/jsr/detail?id=168 - [2] Introduction to JSR 168 — The Portlet Specification
http://developers.sun.com/prodtech/portalserver/reference/techart/jsr168 - [3] Pluto
http://portals.apache.org/pluto - [4] Jetspeed 2 Enterprise Portal
http://portals.apache.org/jetspeed-2 - [5] Jetspeed Simple Portlet Guide
http://portals.apache.org/jetspeed-2/guides/guide-simple-portlet.html - [6] Introducing the Portlet Specification, Part 1
http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-portlet.html - [7] Introducing the Portlet Specification, Part 2
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-portlet2.html - [8] JSR 168 Hello World Portlet
http://www.drrockit.com/space/JSR+168+Hello+World+Portlet
Enrique Lara would like to thank Tom Wheeler and Jeremy Ford for reviewing this article.