Portlet Development Guide Introduction to the Portlet API Edition 1.1 January 30, 2002 Authors: Stephan Hesmer Peter Fischer Ted Buckner Pervasive Computing Development 1. Abstract .............................................................................................................................. 4 1.1. 2. 3. Changes for this edition ........................................................................................... 4 Overview ............................................................................................................................. 5 2.1. Portlets and Portlet Applications ............................................................................ 6 2.2. Portlet modes ............................................................................................................ 6 2.3. Portlet states .............................................................................................................. 7 2.4. Portlet deployment descriptor ................................................................................. 7 2.5. Examples in this document - the Bookmark Portlet ............................................. 8 Portlet API - Basic elements ........................................................................................... 10 3.1. Portlet ...................................................................................................................... 10 3.1.1. getLastModified()............................................................................................. 11 3.2. The scope of the portlet objects ............................................................................. 12 3.3. PortletRequest ........................................................................................................ 12 3.3.1. Client ................................................................................................................ 13 3.4. PortletResponse ...................................................................................................... 15 3.4.1. PortletURI ........................................................................................................ 16 3.5. PortletConfig........................................................................................................... 17 3.6. PortletContext ......................................................................................................... 19 3.6.1. PortletLog ......................................................................................................... 20 4. 3.7. PortletSession .......................................................................................................... 21 3.8. PortletData .............................................................................................................. 22 3.9. PortletWindow ........................................................................................................ 24 3.10. PortletTitle .......................................................................................................... 25 3.11. the complete example ......................................................................................... 26 Java Server Pages ............................................................................................................ 30 4.1. Standard JSP Tags ................................................................................................. 30 4.1.1. <jsp:useBean> .................................................................................................. 30 4.1.2. <jsp:setProperty> ............................................................................................. 30 4.1.3. <jsp:getProperty> ............................................................................................. 31 4.1.4. <jsp:include> .................................................................................................... 31 4.1.5. <jsp:forward> ................................................................................................... 31 4.1.6. <jsp:param> ...................................................................................................... 31 4.1.7. <jsp:plugin> ..................................................................................................... 31 4.1.8. <jsp:params> .................................................................................................... 31 4.1.9. <jsp:fallback> ................................................................................................... 31 4.2. Additional Portlet Tags .......................................................................................... 31 4.2.1. <portletjsp:encodeURI> ................................................................................... 31 4.2.2. <portletjsp:text> ............................................................................................... 32 4.3. 2 Rendering the sample using a JSP ........................................................................ 32 5. 4.4. Model-View-Controller Portlets based on Actions ............................................. 34 4.5. Implicit Objects ...................................................................................................... 35 4.6. Reserved Parameter Names .................................................................................. 36 Portlet API - Advanced elements .................................................................................... 37 5.1. Portlet events .......................................................................................................... 37 5.1.1. Action events .................................................................................................... 38 5.1.2. Window events ................................................................................................. 42 5.1.3. Message events ................................................................................................. 43 5.2. Portlet Caching ....................................................................................................... 47 5.3. Portlet Services ....................................................................................................... 50 5.3.1. ContentAccessService ...................................................................................... 50 6. Portlet development issues .............................................................................................. 52 6.1. No instance and class variables ............................................................................. 52 6.2. Storing data ............................................................................................................. 52 6.3. Namespaces/Javascript .......................................................................................... 52 6.4. Multi-markup support ........................................................................................... 52 6.5. Multi-language support ......................................................................................... 53 6.6. Portlet creation guidelines ..................................................................................... 53 6.6.1. Performance issues ........................................................................................... 53 6.6.2. Guidelines for markup ...................................................................................... 54 6.6.2.1. Accessibility ............................................................................................. 54 7. Using Single Sign-On ...................................................................................................... 55 7.1. Basic authentication sample .................................................................................. 56 7.2. LTPA example ........................................................................................................ 59 3 1. Abstract The purpose of this document is to show how to develop a portlet using the Portlet API, introducing concepts of the Portlet API along the way. Elements of the Portlet API are described with the help of an example that progresses from a simple portlet with no output to a complex portlet application with more advanced features. Portlet development for the WebSphere Portal Server environment is also described. 1.1. Changes for this edition The following changes were made for edition 1.1: 4 Added a note about Java resource bundles for the example in BookmarkPortlet_05. Updated portlet deployment descriptor examples to show how to support edit mode. Updated the description of PortletData under the PortletRequest and PortletData sections. 2. Overview Portals are Web sites that serve as a starting point to information and applications on the Internet or from an intranet. Early Internet portals, such as Yahoo, Excite, and Lycos, categorized Web content and provided search facilities. Over the years, portals have evolved to provide aggregation of content from diverse sources, such as rich text, video, or XML, and personalized services, such as user customization of layout and content. To accommodate the aggregation and display of such diverse content, a portal server must provide a framework that breaks the different portal components into portlets. Each portlet is responsible for accessing content from its source (for example, a Web site, database, or email server) and transforming the content so that it can be rendered to the client. In addition, a portlet might be responsible for providing application logic or storing information associated with a particular user. The portal server provides a framework of services to make the task of writing and managing portlets easier. From a user's perspective, a portlet is a window in the portal that provides a specific service or information, for example, a calendar or news feed. From an application development perspective, portlets are pluggable modules that are designed to run inside a portlet container of a portal server, similar to a servlet running on an application server. User related data has to be stored in a session object that persists across several requests and creates, along with the portlet instance, a virtual portlet instance. Figure: Portlets in a Portlet Container There is always one instance of a portlet class per portlet configuration running inside the portlet container. This means that all class variables of a portlet are shared between multiple threads. Therefore the portlet developer has to be cautious when using them. User related data has to be stored in a session object that persists across several requests and creates together with the portlet instance a virtual portlet instance. Each portlet has the following life cycle: The portlet is constructed and initialized with the init() method. Any calls from the portlet container to the service() method are handled. The portlet is taken out of service, then destroyed with the destroy() method. 5 The service method takes a PortletRequest and a PortletResponse object as parameters. The PortletRequest allows accessing different parameters of the request as well as the PortletSession, the PortletConfig or the PortletContext. The latter is used to obtain information about the portlet container or access to resources. The PortletResponse has a method to obtain an output stream as well as methods to create URIs and to encode them as markup. A PortletURI represents a URI to a specific portlet function. The developer can add parameters and PortletActions to a PortletURI. PortletActions are portlet specific activities that need to be performed before the service method of the portlet is called. 2.1. Portlets and Portlet Applications One or more portlets compose a portlet application. Portlet applications provide no code on their own but form a logical group of portlets. Beside this more logical gain, portlets of the same portlet application can also exchange messages. In the example used in this document, when a URL link of the BookmarkPortlet is clicked, BookmarkBrowserPortlet displays the contents of the URL. Both of these portlets are packaged as part of the same portlet application. BookmarkPortlet sends the URL of the link that was clicked to BookmarkBrowserPortlet. For more information about messaging, see Message events. 2.2. Portlet modes Portlet modes allow a portlet to display a different user interface, depending on the task required of the portlet. The following modes are provided by the Portlet API: VIEW When a portlet is initially constructed on the portal page for a user, it is displayed in its view mode. This is the portlet’s normal mode of operation. HELP If this mode is supported by a portlet, the portlet provides a help page for users to obtain more information about the portlet. EDIT If this mode is supported by a portlet, the portlet provides a page for users to customize the portlet for their own needs. For example, a portlet can provide a page for users to specify their location for obtaining local weather and events. CONFIGURE If this mode is supported by a portlet, the portlet provides a page for portal administrators to configure a portlet for a user or group of users. This mode is not support by WebSphere Portal Server Version 1.2 . The Portlet API provides methods for the portlet to determine the current mode. 6 2.3. Portlet states Portlet states allow users to change how the portlet window is displayed within the portal. In a browser, users invoke these states with icons in the title bar in the same way that Windows applications are manipulated. Portlet states are maintained in the PortletWindow.State object with a boolean value. Normal When a portlet is initially constructed on the portal page, it is displayed in its normal state – arranged on the page along with other portlets. Maximized When a portlet is maximized, it is displayed in the entire body of the portal, replacing the view of other portlets. Minimized When a portlet is minimized, only the portlet title bar is displayed on the portal page. Closed When a portlet is closed, it is not displayed on the portal page. This state is not support by WebSphere Portal Server Version 1.2 . Detached When a portlet is detached, it is rendered in a secondary window. This state is not support by WebSphere Portal Server Version 1.2 . Moving When a portlet is moving, the user is changing the portlet’s position on the page. This state is not support by WebSphere Portal Server Version 1.2 . Resizing When a portlet is resized, the user is changing the portlet’s width and height. This state is not support by WebSphere Portal Server Version 1.2 . 2.4. Portlet deployment descriptor The portlet deployment descriptor is an XML document that provides configuration information about the portlet to the portal server. This information includes configuration parameters specific to a particular portlet or portlet application as well as general information that all portlets provide, such as the type of markup that the portlet supports. The portal server uses this information to provide services for the portlet. For example, if a portlet registers its support for help and edit mode in the portlet deployment descriptor, the portal server will render icons to allow the user to invoke the portlet’s help and edit pages. The Portlet API provides methods for the portlet to read its configuration information using the PortletConfig object. The portlet also has write access to this information when it is in configure mode. Simple Deployment Descriptor Sample <?xml version="1.0"?> <!DOCTYPE portlet-app PUBLIC "-//IBM//DTD Portlet Application 1.0//EN" "portlet.dtd"> <portlet-app> <portlet-app-name>Bookmark Application</portlet-app-name> <portlet> <portlet-name>Bookmark Portlet</portlet-name> 7 <portlet-class>com.mycompany.portlets.bookmark.BookmarkPortlet</portlet-class> <portlet-type>instance</portlet-type> <allows> <maximized/> <minimized/> </allows> <language locale="en"> <title>My Bookmarks</title> <title-short>Bookmarks</title-short> <description>Portlet showing your personalized bookmarks</description> <keywords>bookmarks</keywords> </language> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> </portlet> </portlet-app> 2.5. Examples in this document - the Bookmark Portlet This guide shows all functions of the Portlet API by using only one example portlet which is constantly extended during the sections, starting with a very simple empty portlet. At last, a fully functional portlet is created that demonstrates each aspect of the Portlet API and allows users to manage bookmarks. BookmarkPortlet_01 demonstrates a portlet that implements the Portlet interface and all of its methods. It does not provide any output for the portlet’s interface. BookmarkPortlet_02 calls different methods depending on the current display mode. It still does not provide any output BookmarkPortlet_02a is the same as BookmarkPortlet_02 except that it extends PortletAdapter rather than implementing the Portlet interface. BookmarkPortlet_03 uses a Java PrintWriter to provide HTML markup to the PortletResponse for view mode. BookmarkPortlet_04 retrieves configuration parameters for the portlet. BookmarkPortlet_05 uses the PortletContext object to retrieve the localized text from a resource bundle and to write to the portlet log. BookmarkPortlet_06 sets and gets session attributes for the virtual instance of the portlet. BookmarkPortlet_07 gets persistence data from the PortletData object, which is data that is stored for a particular user for each use of the portlet. BookmarkPortlet_08 tests the PortletWindow object to determine if the portlet is maximized. BookmarkPortletTitle_09 implements the PortletTitle class to allow the user to change the title as it appears in the portlet title bar. BookmarkPortlet_10(portlet.xml) shows the portlet deployment descriptor for BookmarkPortlet_08 and BookmarkPortletTitle_09. 8 BookmarkPortlet_11 uses PortletContext to include a JSP for view mode (bookmarkView.jsp) and instantiates ViewBean_11 for adding user-defined bookmarks. BookmarkPortlet_12 includes a JSP for edit mode (bookmarkEdit.jsp) and associates a PortletAction to the JSP form’s POST method. BookmarkPortletActionListener_12 listens for “add” and “removeAll” actions in BookmarkPortlet12. BookmarkPortlet_13 adds a browse action which launches its BookmarkPortletActionListener_13 to send the URL of the link to BookmarkBrowserPortlet_13, which uses its BookmarkPortletMessageListener_13 to receive the URL. 9 3. Portlet API - Basic elements This section describes the basic interfaces and methods of the Portlet API. The following figure shows a map of many of the common objects in the Portlet API. 3.1. Portlet The Portlet interface is the central abstraction of the Portlet API. All portlets implement this interface, most often by extending a class that implements the interface, such as PortletAdapter. The portlet container calls the following methods of the Portlet interface during the portlet’s life cycle: 10 init() The portlet is constructed, after portal initialization, and then initialized with the init() method. The portal always instantiates only a single instance of the portlet, and this instance is shared among all users, much the same way a servlet is shared among all users of an application server. login() After a user logs in to a portal, each portlet creates a session for the user. The combination of the physical portlet instance and the user session creates the portlet virtual instance. The start of a virtual instance is signaled by the portal calling the login() method on the portlet. This method allows the portlet to initialize the user's session instance of the portlet, for example, to store attributes in the session. service() The portal calls the service() method when the portlet is required to render it's content. During the life cycle of the portlet, the service() method is typically called many times. logout() When the user ends the session with the portal, the portal server calls the logout() method to inform the portlet that the user's session instance is terminating. The portlet should then clean up any resources for the virtual instance. destroy() When the portal is terminating, portlets are taken out of service, then destroyed with the destroy() method. Finally the portlet is garbage collected and finalized. 3.1.1. getLastModified() The portlet container provides a built-in caching mechanism to gain better performance. The Portlet interface provides the getLastModified() method, which is called by every request to enable the portlet to inform the container when the cache entry for the portlet should be invalidated and therefore the portlet’s content should be refreshed. The getLastModified() method returns the last time the content of the portlet changed, in milliseconds, between the current time and midnight, January 1, 1970 UTC (java.lang.System.currentTimeMillis()) or a negative value if it is unknown. For an example of how this method is used, see Portlet Caching . The following example shows a basic portlet that implements all functions of the Portlet interface and stores the PortletConfig in an instance variable to make it available in all methods. At this stage of development, the portlet does not provide any output. As a result, an empty portlet window is displayed in the portal. BookmarkPortlet_01.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; public class BookmarkPortlet_01 implements Portlet { private PortletConfig config = null; public void init(PortletConfig config) throws UnavailableException { // init is called when the portlet is taken in service this.config = config; } // an additional helper method to get the stored config // -> not part of the portlet interface public PortletConfig getConfig () { return config; } 11 public void login(PortletRequest request) throws PortletException { // login is called when the user is logged in and the portlet is shown on a page } public void service(PortletRequest request, PortletResponse response) throws PortletException,IOException { // service is called to render the portlet // in this case the portlet does not render anything // and an empty portlet is being shown } public void logout(PortletSession session) throws PortletException { // logout is called when the session times out or the user logs out } public void destroy() { // when the portlet is taken out of service the destroy method is called } public long getLastModified(PortletRequest request) { // we don't know the last modification time of this portlet, use default. return -1; } } 3.2. The scope of the portlet objects The following table shows the scope of portlet objects as they correspond to other elements of the Portlet API. Scope Objects PortletRequest PortletResponse PortletData PortletSession PortletConfig PortletContext Request per per All All All All Portlet portlet instance virtual instance per all User per all all Portlet Application per Legend: per, means that the object is directly related to the scope. You can read it like this and replace the cursive words with the appropriate object/scope: It exists one object per scope. -, means that the object has no relation to the scope. virtual instance, means that there is one PortletSession for each virtual instance of the portlet. portlet instance, means that there is one PortletData for each portlet occurrence on a page. 3.3. 12 PortletRequest The PortletRequest object is passed to the portlet through the login() and service() methods, providing the portlet with request-specific data and the opportunity to access further important information as listed below. Attributes Attributes are name/value pairs associated with a request. Attributes are available only for the scope of the request. The portlet can get, set, and remove attributes during one request. Parameters Parameters are name/value pairs sent by the client in the URI query string as part of a request. Often the parameters are posted from a form. Parameters are available for the scope of a specific request. The portlet can get but not set parameters from a request. Client The Client object encapsulates all information about the user agent of a specific request. Information from the Client object includes the manufacturer of the user agent or the type of markup that the client supports. User data The PortletData object represents data specific to the portlet instance that is saved to persistent store. For example, a user can set a portlet e-mail application to check for new mail every 30 minutes. This preference is stored for the instance in the PortletData object. Session The PortletSession object contains a user’s data for more than one request. In contrast with the request, which does not retain data after the request is completely processed, session attributes can be remembered/saved over more than one request. Mode Portlet.Mode provides the current or previous mode of the portlet. PortletWindow The PortletWindow object represents the state of the current portlet window. The portlet can access this object to determine if the portlet is currently maximized, minimized, or rendered in its normal view. ModeModifier This object can be used in a PortletAction to set the portlet mode to its previous or requested mode before the portlet is rendered. 3.3.1. Client The Client object encapsulates request-dependent information about the user agent of a specific request. The Client is extracted from the PortletRequest using the getClient() method. The following information can be obtained from the Client: User agent 13 The portlet can get the String sent by the user agent to identify itself to the portal. Markup name The portlet can get the String that indicates the markup language that the client supports, for example, “wml”. MIME type The portlet can get the String that indicates the MIME types supported by the client (for example: “text/vnd.wap.wml”). If the portlet supports multiple types of devices, it should get the markup name rather than the MIME type. The following table shows MIME types and their corresponding markup types. MIME types text/html text/vnd.wap.wml text/html Markup types html wml chtml Capabilities The Capability object contains more detailed information than the markup type about what the client can support, such as the level of HTML, Javascript, or WML tables. Information about the portlet’s mode retrieved from the request can be used to implement helper methods for each mode to achieve more structured code as shown in the following example. BookmarkPortlet_02.java … public void service(PortletRequest request, PortletResponse response) throws PortletException,IOException { // get the mode we are currently running in Portlet.Mode mode = request.getMode(); if (mode == Portlet.Mode.VIEW) doView (request, response); else if (mode == Portlet.Mode.EDIT) doEdit (request, response); else if (mode == Portlet.Mode.HELP) doHelp (request, response); else if (mode == Portlet.Mode.CONFIGURE) doConfigure (request, response); } public void doView (PortletRequest request, PortletResponse response) throws PortletException, IOException { // insert your view code here // get an attribute out of the request String someAttribute = (String)request.getAttribute("SomeAttribute"); // get the client of the current request Client client = request.getClient(); } public void doEdit (PortletRequest request, PortletResponse response) throws PortletException, 14 IOException { // insert your edit code here } public void doHelp (PortletRequest request, PortletResponse response) throws PortletException, IOException { // insert your help code here } public void doConfigure (PortletRequest request, PortletResponse response) throws PortletException, IOException { // insert your configure code here } … The sample code here (plus the code of the first sample shown above) is almost equivalent to the implementation of the PortletAdapter class (for more details see JavaDoc). You should not implement the Portlet interface directly in your portlets, but rather extend PortletAdapter or any other helper class that in turn implements the Portlet interface. Extending one of the helper classes helps protect your portlet from changes in the Portlet interface. Moreover it saves you the work of implementing all of the methods of the Portlet interface, even if your portlet does not need to use them all. Using the PortletAdapter class, you only have to overwrite the methods you really need. The example in BookmarkPortlet_02a provides the same functionality as BookmarkPortlet_02, but using the PortletAdapter class. BookmarkPortlet_02a.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; public class BookmarkPortlet_02a extends PortletAdapter { public void doView (PortletRequest request, PortletResponse response) throws PortletException, IOException { // get an attribute out of the request String someAttribute = (String)request.getAttribute("SomeAttribute"); // get the client of the current request Client client = request.getClient(); } } 3.4. PortletResponse The response object encapsulates information to be returned from the server to the client. PortletResponse is passed via the service method and is used by the portlet to return portlet content using a Java PrintWriter. The response also includes methods for creating the PortletURI object or qualifying portlet markup with the portlet’s namespace. 15 Use one of the following methods to create the PortletURI: createURI() - Creates a PortletURI object pointing to the calling portlet with the current mode createURI(PortletWindow.State state) - Creates a PortletURI object pointing to the calling portlet with the current mode and given portlet window state. createReturnURI() - Creates a portlet URI pointing at the caller of the portlet. For example, createReturnURI() can used to create a back button in an edit mode. Each portlet runs in its own unique namespace. encodeNamespace() is used by portlets to bring attributes in the portlet’s namespace to avoid name clashes with other portlets. Attributes can include parameter names, global variables, or javascript function names. 3.4.1. PortletURI The PortletURI object contains a URI pointing to the Portlet instance and can be further extended by adding portlet-specific parameters and by attaching actions. Actions are portlet-specific activities that need to be performed as result of the incoming request, but before the service() method of the portlet is called. For example, when a user is entering data in the portlet’s edit mode and clicks a “Save” button, the portlet needs to process the posted data before the next markup is generated. This can be achieved by adding a "Save" action to the URI that represents the "Save" button. The complete URI can be converted to a string which is ready for embedding into markup. BookmarkPortlet_03.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortlet_03 extends PortletAdapter { public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { // get the writer to output text PrintWriter writer = response.getWriter(); writer.println("<h2>This is the bookmark portlet.</h2>"); // show an image writer.println("<br>Example graphic: <img src='"+response.encodeURI("/images/java.gif")+"'>"); // output an URL pointing to this portlet with the state maximized PortletURI url = response.createURI(PortletWindow.State.MAXIMIZED); writer.println("<br><a href=\""+url.toString()+"\">Click here to maximize this portlet</a><br>"); } } 16 In BookmarkPortlet_03, the portlet writes HTML markup to the PortletResponse for view mode. Included in the markup is a link that points to the portlet’s maximized state. The portlet looks like this: 3.5. PortletConfig The PortletConfig provides the portlet with its configuration. The configuration holds information about the portlet that is valid for the portlet instance (that is, the same configuration information applies for every virtual instance of the portlet). A portlet’s configuration is initially read from the portlet deployment descriptor. This information is normally maintained by the portlet administrator. When the portlet is in view, edit, or help mode, it has only read access to its configuration. When the portlet is in configure mode, it has read and write access to its configuration. Often, the PortletConfig object is used to access portlet-specific configuration parameters using getAttribute() or the PortletContext object. Attributes are name/value pairs available for the complete life cycle of a portlet. A portlet’s attributes are defined by the <config-param> tag in the portlet deployment descriptor. The following shows the deployment descriptor for BookMarkPortlet_04. In this example, the descriptor defines five configuration parameters that the portlet can read using the getAttribute() method. portlet.xml (Deployment Descriptor) <?xml version="1.0"?> <!DOCTYPE portlet-app PUBLIC "-//IBM//DTD Portlet Application 1.0//EN" "portlet.dtd"> <portlet-app> <portlet-app-name>Bookmark Application</portlet-app-name> <context-param> <param-name>Webmaster</param-name> <param-value>userid@de.ibm.com</param-value> </context-param> <portlet> <portlet-name>Bookmark Portlet</portlet-name> <portlet-class>com.mycompany.portlets.bookmark.BookmarkPortlet_08</portlet-class> <portlet-type>instance</portlet-type> <allows> <maximized/> 17 <minimized/> </allows> <language locale="en"> <title>My Bookmarks</title> <title-short>Bookmarks</title-short> <description>Portlet showing your personalized bookmarks</description> <keywords>bookmarks</keywords> </language> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> <config-param> <param-name>BookmarkCount</param-name> <param-value>2</param-value> </config-param> <config-param> <param-name>Bookmark.Name.1</param-name> <param-value>Jetspeed</param-value> </config-param> <config-param> <param-name>Bookmark.URL.1</param-name> <param-value>http://jakarta.apache.org/jetspeed/site/index.html</param-value> </config-param> <config-param> <param-name>Bookmark.Name.2</param-name> <param-value>Google</param-value> </config-param> <config-param> <param-name>Bookmark.URL.2</param-name> <param-value>http://www.google.com</param-value> </config-param> </portlet> </portlet-app> In BookmarkPortlet_04, the doView() method retrieves the number of bookmarks, the name for each bookmark, and its URL in the portlet configuration. All of these parameters are set in the portlet deployment descriptor; although, they can be changed by the portal administrator after deployment using Portlet Administration. BookmarkPortlet_04.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortlet_04 extends PortletAdapter { public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { PrintWriter writer = response.getWriter(); writer.println("<b>Preconfigured bookmarks:</b><br>"); String preConfCount = (String)getConfig().getAttribute("BookmarkCount"); if (preConfCount!=null) { int max = Integer.parseInt(preConfCount); for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); writer.println("<br><a href=\""+url+"\">"+name+"</a>"); } } } } 18 The portlet looks like this: 3.6. PortletContext The PortletContext interface defines a portlet's view of the portlet container within each portlet that is running. The PortletContext also allows a portlet to access resources available to it. For example, using the context, a portlet can access the portlet log, access context parameters common to all portlets within the portlet application, obtain URL references to resources, or get portlet services The most important information related to the PortletContext is described in detail below: Attributes Attributes are name/value pairs available to all portlets within the portlet application. These are defined in the portlet deployment descriptor under the <context-parameter> element. For example, if a group of portlets share a context parameter called “Webmaster” that contains the portal site’s administrator email, each portlet could get that value and provide a “mailto” link in their help. Attributes of the context are stored on a single machine and are not distributed in a cluster. Log The PortletLog provides the portlet with the ability to log informational, warning, or error messages. Localized text The getText() method is used by the portlet to access resource bundles within a given locale. Resources It is through the PortletContext that a portlet can load or include resources located in the portlet’s application scope. Available methods are include() and getResourceAsStream(). The include() method is typically used to invoke JSPs for output. Messaging Through messaging, it is possible to communicate between portlets and share data or send notifications. A message is sent by using the send() method. PortletServices 19 PortletServices allow portlets to use plugable services via dynamic discovery. See Portlet Services for more information. 3.6.1. PortletLog The PortletLog provides the portlet with the ability to log informational, warning or error messages. The log is maintained by the portlet container. Whether logging is enabled or not is at the discretion of the portlet container. The following line of code shows how you would obtain the PortletLog from the PortletConfig: PortletLog log = portletConfig.getContext().getLog(); As the implementation of the PortletAdapter class does this for you, just use the adapter’s method getPortletLog(). In BookmarkPortlet_05, the getText() method is used to retrieve the localized value of the PreDefinedBookmarks parameter, which is set in a properties file, nls_bookmark_05.properties . This value is added to the PrintWriter to be rendered in the portlet as a heading. Finally, a loop generates bookmark names and links and adds each of them to the PrintWriter. BookmarkPortlet_05.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortlet_05 extends PortletAdapter { public void service(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("service doView called"); PrintWriter writer = response.getWriter(); // get PortletContext PortletContext context = getConfig().getContext(); // get localized Text String localizedText = context.getText("nls.bookmark_05","PreDefinedBookmarks",request.getLocale()); writer.println("<b>"+localizedText+"</b><br>"); String preConfCount = (String)getConfig().getAttribute("BookmarkCount"); if (preConfCount!=null) { int max = Integer.parseInt(preConfCount); for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text localizedText = context.getText("nls.bookmark_05",name,request.getLocale()); writer.println("<br><a href=\""+url+"\">"+localizedText+"</a>"); } } } } 20 Note: The samples in this document following standard Java conventions for resource bundles. In this example, the properties file should be renamed bookmark_05.properties and packaged in the /PORTLET-INF/classes/nls directory of the PAR file. For more information, see Accessing Resources in a LocationIndependent Manner in the JDK documentation. nls_bookmark_05.properties # localized text of the bookmark portlet PreDefinedBookmarks = Predefined bookmarks: Jetspeed = Jetspeed Portal Project Google = Google Search The portlet looks like this: 3.7. PortletSession The PortletSession holds user-specific data for the virtual instance of the portlet. Virtual instances differ from each other only by the data stored in their respective PortletSession or objects. The PortletSession contains transient data for the portlet virtual instance. Any persistent data must be stored using PortletData. Information stored in a portlet’s class variables is shared between all virtual instances – with read and write access. Make sure you do not use class attributes for user-specific data. On the other hand, you have to be cautious about what the portlet adds to the session, especially if the portlet ever runs in a cluster environment where the session is being serialized to a shared database. Everything being stored in the session must be serializable, too. Like the HttpSession, a PortletSession is not available on an anonymous page. During login, a PortletSession is automatically created for each portlet on a page. To get a PortletSession, the getSession() method (available from the PortletRequest) has to be used. The method returns the current session or, if there is no current session and the given parameter “create” is true, it creates one and returns it. In BookmarkPortlet_06, the session is retrieved in the login() method. Then the BookmarkCount parameter is retrieved and associated with the session. The BookmarkCount value is retrieved from the session (if the user is logged in) or from the PortletConfig (if the user is not logged in and no session exists). BookmarkPortlet_06.java 21 package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortlet_06 extends PortletAdapter { public void login(PortletRequest request) throws PortletException { // login is called when the user is logged in and the portlet is shown on a page getPortletLog().debug("login called"); // get PortletSession PortletSession session = request.getSession(); String preConfCount = (String)getConfig().getAttribute("BookmarkCount"); session.setAttribute("BookmarkCount", preConfCount); } public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("service doView called"); PrintWriter writer = response.getWriter(); // get PortletContext PortletContext context = getConfig().getContext(); // get localized Text String localizedText = context.getText("nls.bookmark_05","PreDefinedBookmarks",request.getLocale()); writer.println("<b>"+localizedText+"</b><br>"); int max = 0; String preConfCount = null; // get PortletSession, only when logged in PortletSession session = request.getSession(false); // when valid session is returned, get the counter from the session if (session!=null) preConfCount = (String)session.getAttribute("BookmarkCount"); // when counter could not be gotten from the session load it from the // configuration (only on default page) if (preConfCount==null) preConfCount = (String)getConfig().getAttribute("BookmarkCount"); // convert counter into int if (preConfCount!=null) max = Integer.parseInt(preConfCount); for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text localizedText = context.getText("nls.bookmark_05",name,request.getLocale()); writer.println("<br><a href=\""+url+"\">"+localizedText+"</a>"); } } } nls_bookmark_05.properties # localized text of the bookmark portlet PreDefinedBookmarks = Predefined bookmarks: Jetspeed = Jetspeed Portal Project Google = Google Search 3.8. 22 PortletData The PortletData holds portlet instance specific data for the virtual instance of the portlet. For each occurrence on a page there is a portlet instance. This means, when a portlet is on a group page, PortletData holds group specific data, and when a portlet is on a user page, PortletData holds user specific data. The PortletData contains persistent information about the virtual instance of a portlet while the PortletSession contains only the transient data of the virtual instance. The PortletData stores attributes as name/value pairs. The portlet can get, set, and remove attributes during one request. To commit the changes, the store() method has to be called. Portions of the doView() method from BookmarkPortlet_07 are shown in the following example. This portlet is like BookmarkPortlet_06 with the addition of a new section that shows a list of user-defined bookmarks. The bookmarks are retrieved using the getAttribute() method from the PortletData object. The bookmarks are stored in the portlet’s edit mode, which is shown further in this document in bookmarkEdit.jsp and using an ActionListener. BookmarkPortlet_07.java … public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { … for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text localizedText = context.getText("nls.bookmark_05",name,request.getLocale()); writer.println("<br><a href=\""+url+"\">"+localizedText+"</a>"); } // show a list with user defined bookmarks // get PortletData holding user related data PortletData data = request.getData(); // get localized Text localizedText = context.getText("nls.bookmark_07","UserDefinedBookmarks",request.getLocale() ); writer.println("<br><br><b>"+localizedText+"</b><br>"); max = 0; String userConfCount = (String)data.getAttribute("BookmarkCount"); // convert counter into int max = Integer.parseInt(userConfCount); for (int i=1; i<=max; i++) { String name = (String)data.getAttribute("Bookmark.Name."+i); String url = (String)data.getAttribute("Bookmark.URL."+i); writer.println("<br><a href=\""+url+"\">"+name+"</a>"); } … 23 The portlet looks like this: 3.9. PortletWindow The PortletWindow represents the window that encloses a portlet. The portlet window can send events on manipulation of its various window controls, like when the user clicks minimize or close. The portlet, in turn, can interrogate the window about its current visibility state. For example, a portlet may render its content differently depending on whether its window is maximized or not. The PortletWindow is available from the PortletRequest object. In BookmarkPorlet_08, the PortletWindow is used to determine if the window is maximized. Further on in the loop that generates the bookmarks for output, the URL is also displayed if the window is maximized. If not, only the bookmark name is displayed. The same code is used that displays the user-defined bookmarks. BookmarkPortlet_08.java … public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("service doView called"); // check the current state of the portlet // if it is maximized we will also output the URL of the bookmark boolean isMaximized = request.getWindow().isMaximized(); PrintWriter writer = response.getWriter(); // get PortletContext PortletContext context = getConfig().getContext(); // get localized Text String localizedText = context.getText("nls.bookmark_07","PreDefinedBookmarks",request.getLocale()); writer.println("<b>"+localizedText+"</b><br>"); int max = 0; String preConfCount = null; // get PortletSession, only when logged in PortletSession session = request.getSession(false); // when valid session is returned, get the counter from the session if (session!=null) preConfCount = (String)session.getAttribute("BookmarkCount"); // when counter could not be get from the session load it from the configuration (only on default page) if (preConfCount==null) preConfCount = (String)getConfig().getAttribute("BookmarkCount"); // convert counter into int if (preConfCount!=null) max = Integer.parseInt(preConfCount); 24 for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text localizedText = context.getText("nls.bookmark_07",name,request.getLocale()); if (isMaximized) writer.println("<br><a href=\""+url+"\">"+localizedText+" - "+url+"</a>"); else writer.println("<br><a href=\""+url+"\">"+localizedText+"</a>"); } // show a list with user defined bookmarks // get PortletData holding user related data PortletData data = request.getData(); // get localized Text localizedText = context.getText("nls.bookmark_07","UserDefinedBookmarks",request.getLocale()); writer.println("<br><br><b>"+localizedText+"</b><br>"); max = 0; String userConfCount = (String)data.getAttribute("BookmarkCount"); // convert counter into int max = Integer.parseInt(userConfCount); for (int i=1; i<=max; i++) { String name = (String)data.getAttribute("Bookmark.Name."+i); String url = (String)data.getAttribute("Bookmark.URL."+i); if (isMaximized) writer.println("<br><a href=\""+url+"\">"+name+" - "+url+"</a>"); else writer.println("<br><a href=\""+url+"\">"+name+"</a>"); } } … The maximized portlet looks like this: 3.10. PortletTitle The PortletTitle interface is used to allow the portlet title, as it is displayed in the title bar, to be changed based on a condition (for example, the type of device used to access the portal) or user input (for example, a preference that the user sets on the edit page). The class implementing PortletTitle is specified in the <title-class> element of the portlet deployment descriptor. If <title-class> is not specified, the portlet will display the title specified on the <title> element (under <language>). The PortletTitle has the same life cycle as the portlet itself. BookmarkPortletTitle_09.java 25 package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortletTitle_09 implements PortletTitle { private PortletConfig config = null; public void init(PortletConfig config) { // init is called when the portlet is taken in service this.config = config; } public void service(PortletRequest request, PortletResponse response) throws PortletException,IOException { // service is called to render the portlet title PrintWriter writer = response.getWriter(); // get session. when user is not logged in we get "null" PortletSession session = request.getSession(false); if (session!=null) { String userConfCount = (String)session.getAttribute("UserDefBookmarkCount"); // when counter could not be get from the session load it from the configuration once if (userConfCount==null) { PortletData data = request.getData(); userConfCount = (String)data.getAttribute("BookmarkCount"); } writer.print("Bookmark Portlet ("+userConfCount+" bookmark(s))"); } else { // return static title writer.print(config.getTitle(request.getLocale(),request.getClient())); } } public void destroy() { // when the portlet is taken out of service the destroy method is called } } The portlet’s title bar looks like this: 3.11. the complete example BookmarkPortlet_08.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortlet_08 extends PortletAdapter { public void login(PortletRequest request) throws PortletException { // login is called when the user is logged in and the portlet is shown on a page getPortletLog().debug("login called"); // get PortletSession PortletSession session = request.getSession(false); 26 // when valid session is returned, get the counter from the session if (session!=null) { String preConfCount = (String)getConfig().getAttribute("BookmarkCount"); session.setAttribute("BookmarkCount", preConfCount); } } public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("service doView called"); // check the current state of the portlet // if it is maximized we will also output the URL of the bookmark boolean isMaximized = request.getWindow().isMaximized(); PrintWriter writer = response.getWriter(); // get PortletContext PortletContext context = getConfig().getContext(); // get localized Text String localizedText = context.getText("nls.bookmark_07","PreDefinedBookmarks",request.getLocale()); writer.println("<b>"+localizedText+"</b><br>"); int max = 0; String preConfCount = null; // get PortletSession, only when logged in PortletSession session = request.getSession(false); // when valid session is returned, get the counter from the session if (session!=null) preConfCount = (String)session.getAttribute("BookmarkCount"); // when counter could not be get from the session load it from the configuration (only on default page) if (preConfCount==null) preConfCount = (String)getConfig().getAttribute("BookmarkCount"); // convert counter into int if (preConfCount!=null) max = Integer.parseInt(preConfCount); for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text localizedText = context.getText("nls.bookmark_07",name,request.getLocale()); if (isMaximized) writer.println("<br><a href=\""+url+"\">"+localizedText+" - "+url+"</a>"); else writer.println("<br><a href=\""+url+"\">"+localizedText+"</a>"); } // *** new section // show a list with user defined bookmarks // get PortletData holding user related data PortletData data = request.getData(); // get localized Text localizedText = context.getText("nls.bookmark_07","UserDefinedBookmarks",request.getLocale()); writer.println("<br><br><b>"+localizedText+"</b><br>"); max = 0; String userConfCount = (String)data.getAttribute("BookmarkCount"); // convert counter into int max = Integer.parseInt(userConfCount); for (int i=1; i<=max; i++) { String name = (String)data.getAttribute("Bookmark.Name."+i); String url = (String)data.getAttribute("Bookmark.URL."+i); if (isMaximized) writer.println("<br><a href=\""+url+"\">"+name+" - "+url+"</a>"); else writer.println("<br><a href=\""+url+"\">"+name+"</a>"); } } 27 } BookmarkPortletTitle_09.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlet.*; import java.io.IOException; import java.io.PrintWriter; public class BookmarkPortletTitle_09 implements PortletTitle { private PortletConfig config = null; public void init(PortletConfig config) { // init is called when the portlet is taken in service this.config = config; } public void service(PortletRequest request, PortletResponse response) throws PortletException,IOException { // service is called to render the portlet title PrintWriter writer = response.getWriter(); PortletData data = request.getData(); String userConfCount = (String)data.getAttribute("BookmarkCount"); if (userConfCount!=null) writer.print("My Bookmarks ("+userConfCount+" bookmark(s))"); else // return static title writer.print(config.getTitle(request.getLocale(),request.getClient())); } public void destroy() { // when the portlet is taken out of service the destroy method is called } } nls_bookmark_07.properties # localized text of the bookmark portlet PreDefinedBookmarks = Predefined bookmarks: Yahoo = My Yahoo Germany UserDefinedBookmarks = User defined bookmarks: BookmarkPortlet_10.portlet.xml portlet.xml (Deployment Descriptor) <?xml version="1.0"?> <!DOCTYPE portlet-app PUBLIC "-//IBM//DTD Portlet Application 1.0//EN" "portlet.dtd"> <portlet-app> <portlet-app-name>Bookmark Application</portlet-app-name> <context-param> <param-name>Webmaster</param-name> <param-value>userid@de.ibm.com</param-value> </context-param> <portlet> <portlet-name>Bookmark Portlet</portlet-name> <portlet-class>com.mycompany.portlets.bookmark.BookmarkPortlet_08</portlet-class> <portlet-type>instance</portlet-type> <title-class>com.mycompany.portlets.bookmark.BookmarkPortletTitle_09</title-class> <allows> <maximized/> <minimized/> </allows> 28 <language locale="en"> <title>My Bookmarks</title> <title-short>Bookmarks</title-short> <description>Portlet showing your personalized bookmarks</description> <keywords>bookmarks</keywords> </language> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> <config-param> <param-name>BookmarkCount</param-name> <param-value>2</param-value> </config-param> <config-param> <param-name>Bookmark.Name.1</param-name> <param-value>Jetspeed</param-value> </config-param> <config-param> <param-name>Bookmark.URL.1</param-name> <param-value>http://jakarta.apache.org/jetspeed/site/index.html</param-value> </config-param> <config-param> <param-name>Bookmark.Name.2</param-name> <param-value>Google</param-value> </config-param> <config-param> <param-name>Bookmark.URL.2</param-name> <param-value>http://www.google.com</param-value> </config-param> </portlet> </portlet-app> Now that we are able to show user defined bookmarks we need to enable the user to add his favorite bookmarks. This is done by implementing the portlet’s edit mode within which the portlet instance can be customized. To render the edit mode, we will use a portlet JSP which will be explained in detail in the following chapter. 29 4. Java Server Pages 4.1. Standard JSP Tags The portlet container redefines the semantics of some of the standard JSP tags in JSPs that are invoked through the PortletContext.include method. The following sections explain the semantics of each JSP tag in comparison to its behavior when the JSP would be invoked through the ServletContext.include method. 4.1.1. <jsp:useBean> When a JSP is executed in the portlet container, the semantics of the scope attribute of the <jsp:useBean> tag are different from JSPs running in a servlet container: Page scope: For both the servlet container and the portlet container, the page scope means that the bean is only visible in the scope of the JSP itself. Request scope: In a servlet container, the request scope means that the bean is stored in the HttpServletRequest while in a portlet container it means that the bean is visible in the PortletRequest. Session scope: In a servlet container, the session scope means that the bean is stored in the HttpSession that is accessible to all servlets in the web application while in a portlet container, the session scope means that the bean is stored in the PortletSession that is only accessible for portlets within the same portlet application. Application scope: In a servlet container, the application scope means that the bean is stored in the application context, (that is, it is visible to all servlets in a web application). In a portlet container, the application scope means that the bean is stored in the PortletContext, (that is, only visible to portlets in a portlet application). Scope Page Request Session Application Portlet API semantics Visible in the JSP itself PortletRequest PortletSession Portlet Application Servlet API semantics Visible in the JSP itself (Http)ServletRequest HttpSession Web Application 4.1.2. <jsp:setProperty> When used in a JSP included by a portlet, the <jsp:setProperty> tag works in the context of the portlet container. The following tag <jsp:setProperty name=”javaBean” property=”*” /> results in the bean properties being set from the PortletRequest, not from the ServletRequest that would be used in the servlet container. Other forms of the <jsp:setProperty> tag work exactly as described in the JSP 1.1 specification, which means they set values for the properties of beans with a name as specified in a prior <jsp:useBean> tag. 30 4.1.3. <jsp:getProperty> Works exactly as defined in the JSP 1.1 specification - it gets a value from a bean with a name specified in a prior <jsp:useBean> tag. 4.1.4. <jsp:include> In the portlet container, the <jsp:include> tag includes a JSP that also belongs to the portlet application, providing it with a PortletRequest/PortletResponse pair instead of a ServletRequest/ServletResponse pair. To include servlets or JSPs that run in the same web application as the portal, but outside the portlet application, a service has to be used. 4.1.5. <jsp:forward> The <jsp:forward> action is not allowed in portlets, as portlets are being displayed as parts of larger pages and have no control over the output stream. Using this tag in a JSP that is invoked from the portlet container throws an IllegalStateException. 4.1.6. <jsp:param> The <jsp:param> tag allows to specify parameters to be added to the portlet request when including a JSP. 4.1.7. <jsp:plugin> Works as described in the JSP 1.1 specification. 4.1.8. <jsp:params> Works as described in the JSP 1.1 specification. 4.1.9. <jsp:fallback> Works as described in the JSP 1.1 specification. 4.2. Additional Portlet Tags In addition to the standard JSP tags that can be used in JSPs displayed by portlets, the portlet container provides some additional tags for use in portlet JSPs. To make these additional tags available in a JSP, the following directive is required at the beginning of the JSP: <%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletjsp" %> 4.2.1. <portletjsp:encodeURI> 31 The <portletjsp:encodeURI> tag encodes a given URI and writes the result to the output stream. The URI is encoded so that it can be used in links within portlets and always points to the portlet that invoked the JSP. <portletjsp:encodeURI path=URI /> 4.2.2. <portletjsp:text> The <portletjsp:text> tag allows convenientl access to resource bundles. It works as follows: <portletjsp:text key=keyname bundle=resourcebundle /> 4.3. Rendering the sample using a JSP BookmarkPortlet_11.java package com.mycompany.portlets.bookmark; … public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("service doView called"); // check the current state of the portlet // if it is maximized we will also output the URL of the bookmark boolean isMaximized = request.getWindow().isMaximized(); ViewBean_11 bean = new ViewBean_11(); int max = 0; String preConfCount = (String)getConfig().getAttribute("BookmarkCount"); // convert counter into int if (preConfCount!=null) max = Integer.parseInt(preConfCount); for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); // get localized Text String localizedText = getConfig().getContext().getText("nls.bookmark_07",name,request.getLocale()); if (isMaximized) bean.addPreDefinedBookmark(localizedText+" - "+url,url); else bean.addPreDefinedBookmark(localizedText,url); } // get PortletData holding user related data PortletData data = request.getData(); max = 0; String userConfCount = (String)data.getAttribute("BookmarkCount"); // convert counter into int if (userConfCount!=null) max = Integer.parseInt(userConfCount); for (int i=1; i<=max; i++) { String name = (String)data.getAttribute("Bookmark.Name."+i); String url = (String)data.getAttribute("Bookmark.URL."+i); if (isMaximized) bean.addUserDefinedBookmark(name+" - "+url,url); else bean.addUserDefinedBookmark(name,url); } 32 request.setAttribute("viewBean",bean); getConfig().getContext().include("/PORTLET-NF/jsp/bookmarkView.jsp",request,response); } } ViewBean_11.java package com.mycompany.portlets.bookmark; import java.util.Hashtable; import java.util.Enumeration; public class ViewBean_11 { private Hashtable preDefinedBookmarks = new Hashtable(); private Hashtable userDefinedBookmarks = new Hashtable(); public ViewBean_11() { } public void addPreDefinedBookmark(String name, String url) { preDefinedBookmarks.put(name,url); } public void addUserDefinedBookmark(String name, String url) { userDefinedBookmarks.put(name,url); } public Enumeration getPreDefiniedNames() { return preDefinedBookmarks.keys(); } public String getPreDefiniedURL(String name) { return (String)preDefinedBookmarks.get(name); } public Enumeration getUserDefiniedNames() { return userDefinedBookmarks.keys(); } public String getUserDefiniedURL(String name) { return (String)userDefinedBookmarks.get(name); } } bookmarkView.jsp <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <%@ page session="false" %> <%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletAPI" %> <jsp:useBean id="viewBean" class="com.mycompany.portlets.bookmark.ViewBean_11" scope="request"/> <META http-equiv="Content-Type" content="text/html"> <META http-equiv="Content-Style-Type" content="text/css"> <TABLE class="Portlet" width="100%" border="0" cellspacing="0" cellpadding="0"> <TR> <TD> <b><portletAPI:text key="PreDefinedBookmarks" bundle="nls.bookmark_07" /></b><br> <% java.util.Enumeration bookmarks = viewBean.getPreDefiniedNames(); while (bookmarks.hasMoreElements()) { String name = (String)bookmarks.nextElement(); %> 33 <br><a href="<%=viewBean.getPreDefiniedURL(name)%>"><%=name%></a> <% } %> </TD> </TR> <!-- Empty row --> <TR> <TD>&nbsp;</TD> </TR> <TR> <TD> <% java.util.Enumeration userBookmarks = viewBean.getUserDefiniedNames(); if (userBookmarks.hasMoreElements()) { %> <b><portletAPI:text key="UserDefinedBookmarks" bundle="nls.bookmark_07" /></b><br> <% } while (userBookmarks.hasMoreElements()) { String name = (String)userBookmarks.nextElement(); %> <br><a href="<%=viewBean.getUserDefiniedURL(name)%>"><%=name%></a> <% } %> </TD> </TR> </TABLE> The portlet looks like this: 4.4. Model-View-Controller Portlets based on Actions When portlets are written according to the Model-View-Controller pattern, the portlet can use the PortletResponse.createURI method to create a PortletURI object and use the PortletURI.addAction method to associate actions and parameters with the URI. In order to make such a PortletURI object available to a JSP, the portlet can pass the URI object in the PortletRequest. For the following example, the bookmarkEdit.jsp gets the URI from EditBean_12 and uses it as the action target in a form (see BookmarkPortlet_12 example further in this document). BookmarkPortlet_12.java package com.mycompany.portlets.bookmark; … public void doEdit(PortletRequest request, PortletResponse response) throws PortletException,IOException { EditBean_12 bean = new EditBean_12(); PortletURI addURI = response.createURI(); DefaultPortletAction addAction = new DefaultPortletAction("add"); addURI.addAction(addAction); bean.setAddURI(addURI.toString()); … request.setAttribute("editBean",bean); 34 getConfig().getContext().include("/PORTLET-INF/jsp/bookmarkEdit.jsp", request,response); } } bookmarkEdit.jsp … <jsp:useBean id="editBean" class="com.mycompany.portlets.bookmark.EditBean_12" scope="request"/> … Please add a new bookmark here:<br> <FORM method='POST' action='<%=editBean.getAddURI()%>'> Name: <INPUT name='bookmark.name'><br> URL: <INPUT name='bookmark.url'><br> <INPUT type='submit' name='addButton' value='Add'><br> … Potentially, there may be different portlets using the same bean names to put beans into the portlet session. As portlet applications are isolated from each other by usage of separate portlet sessions and classloaders, WebSphere Portal Server will handle this case without problems. The portlet looks like this: 4.5. Implicit Objects Similarly to JSPs invoked by servlets, JSPs invoked by portlets can access implicitly defined objects. The following table shows the implicit objects that are available for JSPs invoked by portlets and the type of each of these objects. For comparison, the last column of the table shows the types that these objects would have in the servlet container. Implicit Object Type in Portlet Container Type in Servlet Container Config PortletConfig ServletConfig request PortletRequest (Http)ServletRequest Response PortletResponse (Http)ServletResponse Session PortletSession HttpSession Application PortletContext ServletContext pageContext PageContext PageContext Out JspWriter JspWriter Page Object Object 35 In error pages, the following implicit object is available additionally: Implicit Object Portlet Container Exception Throwable 4.6. Servlet Container Equivalent Throwable Reserved Parameter Names The following parameter names may not be used in portlet JSPs without namespace encoding as they are reserved by the WPS framework: 36 template screen action names starting with a point ‘.’ names starting with an underscore ‘_’ 5. Portlet API - Advanced elements This chapter describes features of the Portlet API that are implemented by more advanced portlets. You should already be familiar with the basic elements before you use the features described in this chapter. Advanced features include capturing and processing portlet events, caching, and portlet services. In this chapter, the bookmark portlet example is expanded to take advantage of the advanced features. We introduce an edit mode to let the user define user-specific bookmarks. Then BookmarkPortlet is modified to listen for action events (when the user clicks a bookmark link) and sends the bookmark URL to the BookmarkBrowserPortlet. This new portlet listens for message events and displays the appropriate URL content using the ContentAccessService. BookmarkPortlet caches its output and uses this output until either the cache expires or the portlet’s window state changes. 5.1. Portlet events Portlet events contain information about an event to which a portlet might need to respond. For example, when a user clicks a link or button, this generates an action event. To receive notification of the event, the portlet must have an event listener registered in the portlet deployment descriptor. In the Portlet API, there are three types of events: ActionEvents: generated when an HTTP request is received by the portlet container that is associated with an action, such as when the user clicks a link. MessageEvents: generated when another portlet within the portlet application sends a message. WindowEvents: generated when the user changes the state of the portlet window. The portlet container delivers all events to the respective event listeners (and thereby the portlets) before generating the new content that the event requires. Should a listener, while processing the event, find that another event needs to be generated, that event will be queued by the portlet container and delivered at a point of time that is at the discretion of the portlet container. It is only guaranteed that it will be delivered and that it will happen before the content generation phase. No further events will be delivered once the content generation phase has started. For example, messages cannot be sent from within the service() methods. The resulting message event will not be delivered and essentially discarded. The event listener is specified in the portlet’s deployment descriptor. The listener can access its Portlet object and corresponding PortletRequest from the event and respond using the PortletSession object. 37 5.1.1. Action events An ActionEvent is sent to the related portlet when an HTTP request is received that is associated with a PortletAction. PortletActions can be linked to URLs by using the PortletAPI. Normally, PortletActions are linked with http references or buttons in html forms enabling the portlet programmer to implement different processing paths for different user selections. The ActionEvent of the clicked link, then carries the associated PortletAction back to the portlet which in turn is able to process different paths on behalf of the PortletAction. A portlet action can carry any information. It should, however, not store a request, response, or session object. This information is part of the action event that will be sent to the registered action listener(s). PortletAction An object with the type PortletAction has to be implemented which will be linked to the URL and will be passed via the ActionEvent. For example, BookmarkPortlet_12 instantiates the DefaultPortletAction class and links the action to the URL by the action’s name. The following shows how to create an DefaultPortletAction: DefaultPortletAction addAction = new DefaultPortletAction("add"); Creating a URI including a PortletAction A URI that references a PortletAction is created by using the class PortletURI and its addAction() method. The following snippet shows how to include the PortletAction to an URI: PortletURI addURI = response.createURI(); DefaultPortletAction addAction = new DefaultPortletAction("add"); addURI.addAction(addAction); ActionEvent An ActionEvent is sent by the portlet container when an HTTP request is received that is associated with one more actions and can be used to get the associated PortletAction, the portlet which sent the action, and the PortletRequest. The following shows the JSP and how it uses the URL, including the PortletAction. When the user clicks the “add” button, a new HTTP request is sent to the action URL of the FORM. <FORM method='POST' action='<%=editBean.getAddURI()%>'> Name: <INPUT name='bookmark.name'><br> URL: <INPUT name='bookmark.url'><br> <INPUT type='submit' name='addButton' value='Add'><br> </FORM> 38 ActionListener The ActionListener interface must be implemented if a portlet wishes to receive action events. The registration is done in the portlet descriptor. The following shows how the ActionListener differentiates between actions. public void actionPerformed (ActionEvent event) { PortletAction _action = event.getAction(); if (_action instanceof DefaultPortletAction) { … DefaultPortletAction action = (DefaultPortletAction)_action; if (action.getName().equals("add")) { The complete example: BookmarkPortlet_12.java package com.mycompany.portlets.bookmark; … public void doEdit(PortletRequest request, PortletResponse response) throws PortletException,IOException { EditBean_12 bean = new EditBean_12(); PortletURI addURI = response.createURI(); DefaultPortletAction addAction = new DefaultPortletAction("add"); addURI.addAction(addAction); bean.setAddURI(addURI.toString()); PortletURI removeAllURI = response.createURI(); DefaultPortletAction removeAllAction = new DefaultPortletAction("removeAll"); removeAllURI.addAction(removeAllAction); bean.setRemoveAllURI(removeAllURI.toString()); PortletURI returnURI = response.createReturnURI(); bean.setReturnURI(returnURI.toString()); PortletSession session = request.getSession(); // check for existing message if ((session!=null) && (session.getAttribute("message")!=null)) { bean.setMessage((String)session.getAttribute("message")); // clear message session.setAttribute("message",""); } request.setAttribute("editBean",bean); getConfig().getContext().include("/PORTLETINF/jsp/bookmarkEdit.jsp",request,response); } } EditBean.java package com.mycompany.portlets.bookmark; import java.util.Hashtable; import java.util.Enumeration; public class EditBean_12 { private String removeAllURI = ""; private String returnURI = ""; 39 private String addURI private String message = ""; = ""; public EditBean_12() { } public void setRemoveAllURI(String uri) { removeAllURI = uri; } public String getRemoveAllURI() { return removeAllURI; } public void setReturnURI(String uri) { returnURI = uri; } public String getReturnURI() { return returnURI; } public void setAddURI(String uri) { addURI = uri; } public String getAddURI() { return addURI; } public void setMessage(String msg) { message = msg; } public String getMessage() { return message; } } bookmarkEdit.jsp <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <%@ page session="false" %> <%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletAPI" %> <jsp:useBean id="editBean" class="com.mycompany.portlets.bookmark.EditBean_12" scope="request"/> <META http-equiv="Content-Type" content="text/html"> <META http-equiv="Content-Style-Type" content="text/css"> <TABLE class="Portlet" width="100%" border="0" cellspacing="0" cellpadding="0"> <TR> <TD> <%=editBean.getMessage()%> </TD> </TR> <TR> <TD> Please add a new bookmark here:<br> <FORM method='POST' action='<%=editBean.getAddURI()%>'> Name: <INPUT name='bookmark.name'><br> URL: <INPUT name='bookmark.url'><br> <INPUT type='submit' name='addButton' value='Add'><br> </FORM> <FORM method='POST' action='<%=editBean.getRemoveAllURI()%>'> <INPUT type='submit' name='removeAllButton' value='Remove All'><br> </FORM> <FORM method='POST' action='<%=editBean.getReturnURI()%>'> <INPUT type='submit' name='returnButton' value='Finish'><br> </FORM> </TD> </TR> </TABLE> 40 BookmarkPortletActionListener_12.java package com.mycompany.portlets.bookmark; import import import import org.apache.jetspeed.portlet.*; org.apache.jetspeed.portlet.event.*; org.apache.jetspeed.portlets.*; java.io.IOException; public class BookmarkPortletActionListener_12 implements ActionListener { public BookmarkPortletActionListener_12() { } public void actionPerformed (ActionEvent event) { PortletAction _action = event.getAction(); if (_action instanceof DefaultPortletAction) { PortletRequest request = event.getRequest(); PortletAdapter portlet = (PortletAdapter)event.getPortlet(); PortletLog log = portlet.getConfig().getContext().getLog(); log.debug("BookmarkPortletActionListener_12 - Action called"); DefaultPortletAction action = (DefaultPortletAction)_action; if (action.getName().equals("add")) { log.debug("BookmarkPortletActionListener_12 - adding bookmark"); PortletData data = request.getData(); String userConfCount = (String)data.getAttribute("BookmarkCount"); if (userConfCount==null) userConfCount = "0"; int max = Integer.parseInt(userConfCount); max++; String name = request.getParameter("bookmark.name"); data.setAttribute("Bookmark.Name."+max,name); data.setAttribute("Bookmark.URL."+max,request.getParameter("bookmark.url")); data.setAttribute("BookmarkCount",String.valueOf(max)); try { data.store(); request.getSession().setAttribute("message","<b>Added successfully bookmark "+name+"</b>"); } catch (IOException e) { log.error("failed to save data!",e); request.getSession().setAttribute("message","<font COLOR=#ff0000><b>Failed to add bookmark!</b></font>"); try { request.setModeModifier(Portlet.ModeModifier.CURRENT); } catch (AccessDeniedException ade) { } } } else if (action.getName().equals("removeAll")) { log.debug("BookmarkPortletActionListener_12 - removing all bookmarks"); PortletData data = request.getData(); data.removeAllAttributes(); try { data.store(); request.getSession().setAttribute("message","<b>Removed successfully all bookmarks.</b>"); } catch (IOException e) { log.error("failed to save data!",e); request.getSession().setAttribute("message","<font COLOR=#ff0000><b>Failed to remove bookmarks!</b></font>"); try { request.setModeModifier(Portlet.ModeModifier.CURRENT); } catch (AccessDeniedException ade) { 41 } } } } } } portlet.xml <?xml version="1.0"?> <!DOCTYPE portlet-app PUBLIC "-//IBM//DTD Portlet Application 1.0//EN" "portlet.dtd"> <portlet-app> <portlet-app-name>Bookmark Application</portlet-app-name> <context-param> <param-name>Webmaster</param-name> <param-value>userid@de.ibm.com</param-value> </context-param> <portlet> <portlet-name>Bookmark Portlet</portlet-name> <portlet-class>com.mycompany.portlets.bookmark.BookmarkPortlet_12</portlet-class> <portlet-type>instance</portlet-type> <title-class>com.mycompany.portlets.bookmark.BookmarkPortletTitle_09</title-class> <listener> <listener-class type="action"> com.mycompany.portlets.bookmark.BookmarkPortletActionListener_12 </listener-class> </listener> <allows> <maximized/> <minimized/> </allows> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> … The portlet looks like this: 5.1.2. Window events A WindowEvent is sent by the portlet container whenever a user clicks on one of the control buttons that change the window’s state, such as maximize, minimize or restore. For example, WindowEvents can be used to show different information for different window states as shown in BookmarkPortletWindowListener_14. 42 WindowEvent A WindowEvent is sent by the portlet container whenever the user or the portal interacts with portlet window controls. WindowListener The WindowListener interface must be registered in the portlet deployment descriptor if the portlet needs to receive window events. The registration is done in the portlet descriptor. WindowAdapter The WindowAdapter is a window listener in which all callback methods are empty implementations. The window adapter makes it more convenient for an object to listen for window events. In particular, by using the window adapter, it is possible to implement only those callback methods needed by your portlet. Without the window adapter, you must implement all callback methods, even if the method is empty. BookmarkPortletWindowListener_14.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlets.*; import org.apache.jetspeed.portlet.*; import org.apache.jetspeed.portlet.event.*; //Java stuff import java.io.IOException; public class BookmarkPortletWindowListener_14 extends WindowAdapter { public void windowMaximized (WindowEvent event) throws PortletException { setLastModified(event); } public void windowRestored (WindowEvent event) throws PortletException { setLastModified(event); } private void setLastModified(WindowEvent event) { PortletSession session = event.getRequest().getSession(false); if (session != null) { session.setAttribute(BookmarkPortlet_14.LAST_MODIFIED, new Long(System.currentTimeMillis())); } } public void windowDetached (WindowEvent event) throws PortletException { } } 5.1.3. Message events MessageEvents can be sent from one portlet to others if the recipient portlets are members of the same portlet application and are placed on the same page as the sending portlet. MessageEvents can only be sent actively to other portlets when the 43 portlet is in the event processing cycle of the portlet container, otherwise an exception is thrown. There are two different types of messages: Single addressed messages: Messages sent to a specific portlet by specifying the portlet name on the send() method. Broadcast messages: Messages sent to all portlets of the same portlet application remaining on the same page. MessageEvents are useful when changes in one portlet should be reflected in another one. For example, clicking a link in our bookmark portlet could show the web page in a bookmark browser portlet. In BookmarkPortlet_13, only the link is shown in the portlet. How to show the actual site is shown further down this guide. To achieve this, the links in the bookmark browser must not point to the web site anymore but trigger a “browse” action containing the link. So the ActionListener gets the “browse” action and sends a message to the browse portlet. PortletMessage An object with the type PortletMessage has to be implemented which will be passed via the MessageEvent. MessageEvent A MessageEvent is sent by the portlet container if one portlet sends a message to another. MessageListener The MessageListener interface must be implemented if a portlet wishes to receive message events. The registration is done in the portlet descriptor. BookmarkPortlet_13.java package com.mycompany.portlets.bookmark; … public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { … for (int i=1; i<=max; i++) { String name = (String)getConfig().getAttribute("Bookmark.Name."+i); String url = (String)getConfig().getAttribute("Bookmark.URL."+i); DefaultPortletAction browseAction = new org.apache.jetspeed.portlets.DefaultPortletAction("browse"); browseAction.addParameter("url", url); PortletURI portletURI = response.createReturnURI(); portletURI.addAction(browseAction); String actionURI = portletURI.toString(); // get localized Text localizedText = context.getText("nls.bookmark_07",name,request.getLocale()); if (isMaximized) bean.addPreDefinedBookmark(localizedText+" - "+url,actionURI); else bean.addPreDefinedBookmark(localizedText,actionURI); } 44 // *** new section // show a list with user defined bookmarks // get PortletData holding user related data PortletData data = request.getData(); max = 0; String userConfCount = (String)data.getAttribute("BookmarkCount"); // convert counter into int if (userConfCount!=null) { max = Integer.parseInt(userConfCount); // get localized Text localizedText = context.getText("nls.bookmark_07","UserDefinedBookmarks",request.getLocale()); for (int i=1; i<=max; i++) { String name = (String)data.getAttribute("Bookmark.Name."+i); String url = (String)data.getAttribute("Bookmark.URL."+i); DefaultPortletAction browseAction = new org.apache.jetspeed.portlets.DefaultPortletAction("browse"); browseAction.addParameter("url", url); PortletURI portletURI = response.createReturnURI(); portletURI.addAction(browseAction); String actionURI = portletURI.toString(); if (isMaximized) bean.addUserDefinedBookmark(name+" - "+url,actionURI); else bean.addUserDefinedBookmark(name,actionURI); } } … BookmarkPortletActionListener_13.java package com.mycompany.portlets.bookmark; package com.mycompany.portlets.bookmark; … public class BookmarkPortletActionListener_13 extends ActionAdapter { public void actionPerformed (ActionEvent event) { … } else if (action.getName().equals("browse")) { log.debug("BookmarkActionListener_13 - browse action"); String url = (String) action.getParameters().get("url"); log.debug("BookmarkPortletActionListener_13 - opening link: " + url); try { portlet.getConfig().getContext().send(null, new DefaultPortletMessage(url), request); } catch (AccessDeniedException ade) { log.error("BookmarkPortletActionListener_13 - unable to send message \"url=" + url + "\" - AccessDenied"); } } } } } BookmarkPortletMessageListener_13.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlets.*; 45 import org.apache.jetspeed.portlet.*; import org.apache.jetspeed.portlet.event.*; //Java stuff import java.io.IOException; public class BookmarkPortletMessageListener_14 extends MessageAdapter { public void messageReceived (MessageEvent event) throws PortletException { PortletMessage msg = event.getMessage(); if (msg instanceof DefaultPortletMessage) { String url = ((DefaultPortletMessage)msg).getText(); PortletAdapter portlet = (PortletAdapter)event.getPortlet(); portlet.getPortletLog().debug("BookmarkPortletMessageListener_13 messageReceived"); PortletRequest request = event.getRequest(); PortletSession session = request.getSession(); session.setAttribute("url",url); } } } BookmarkBrowserPortlet_13.java package com.mycompany.portlets.bookmark; import import import import org.apache.jetspeed.portlet.service.ContentAccessService; org.apache.jetspeed.portlets.*; org.apache.jetspeed.portlet.*; org.apache.jetspeed.portlet.event.*; //Java stuff import java.io.IOException; import java.io.PrintWriter; public class BookmarkBrowserPortlet_13 extends PortletAdapter { public void doView(PortletRequest request, PortletResponse response) throws PortletException,IOException { getPortletLog().debug("doView called"); try { PortletContext context = getConfig().getContext(); PrintWriter writer = response.getWriter(); String url = (String)request.getSession().getAttribute("url"); writer.println("<br><a href=\""+url+"\">” + url + “</a><br><br>"); } catch (Exception exc) { getPortletLog().error("ServletInvokerPortlet: An error occurred"); } } } portlet.xml <?xml version="1.0"?> <!DOCTYPE portlet-app PUBLIC "-//IBM//DTD Portlet Application 1.0//EN" "portlet.dtd"> <portlet-app> <portlet-app-name>Bookmark Application</portlet-app-name> <context-param> <param-name>Webmaster</param-name> <param-value>userid@de.ibm.com</param-value> </context-param> <portlet> <portlet-name>Bookmark Portlet 13</portlet-name> <portlet-class>com.mycompany.portlets.bookmark.BookmarkPortlet_13</portlet-class> 46 <portlet-type>instance</portlet-type> <title-class>com.mycompany.portlets.bookmark.BookmarkPortletTitle_09</title-class> <listener> <listener-class type="action"> com.mycompany.portlets.bookmark.BookmarkPortletActionListener_13 </listener-class> </listener> … </portlet> <portlet> <portlet-name>BookmarkBrowser</portlet-name> <portlet-class>com.mycompany.portlets.bookmark.BookmarkBrowserPortlet_13</portletclass> <portlet-type>instance</portlet-type> <listener> <listener-class type="message"> com.mycompany.portlets.bookmark.BookmarkPortletMessageListener_13 </listener-class> </listener> <allows> <maximized/> <minimized/> </allows> <language locale="en"> <title>BookmarkBrowser</title> <title-short>Browser</title-short> <description>Portlet showing your personalized bookmarks</description> <keywords>bookmarks,browser</keywords> </language> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> </portlet> </portlet-app> 5.2. Portlet Caching A portlet container can provide a per portlet cache which stores the content of a portlet. To configure the caching policy of a portlet, there are two properties in the portlet’s deployment descriptor: expires indicates for how long the content of a portlet should be cached until it expires. <0 The portlet provides static content and therefore does never expire =0 The portlet expires at once and is not cached at all >0 The time in seconds shared indicates if the portlet is user specific YES This portlet provides the same content for all users -> one cache entry for all users NO This portlet is user specific -> separate cache entries for each user 47 Example: Now the bookmark portlet shall be cached but needs to change its content immediately if the window state changes. It is possible for a portlet to inform the container about such a thing by implementing the getLastModified() method. The getLastModified method returns the time of the last modification to the portlet. In our case, this would be the time when the window state changed as we add more information for the maximized state. Or in case of the bookmark browser portlet, the content only changes if it receives a new URL or, in other words, if the content of the portlet’s session changes. portlet.xml … <listener> <listener-class type="action"> com.mycompany.portlets.bookmark.BookmarkPortletActionListener_13 </listener-class> </listener> <listener> <listener-class type="window"> com.mycompany.portlets.bookmark.BookmarkPortletWindowListener_14 </listener-class> </listener> <cache> <expires>-1</expires> <shared>NO</shared> </cache> <allows> <maximized/> <minimized/> </allows> <supports> <markup name="html"> <view output="fragment"/> <edit output="fragment"/> </markup> </supports> … BookmarkPortletWindowListener_14.java package com.mycompany.portlets.bookmark; import org.apache.jetspeed.portlets.*; import org.apache.jetspeed.portlet.*; import org.apache.jetspeed.portlet.event.*; //Java stuff import java.io.IOException; public class BookmarkPortletWindowListener_14 extends WindowAdapter { public void windowMaximized (WindowEvent event) throws PortletException { setLastModified(event); } public void windowRestored (WindowEvent event) throws PortletException { setLastModified(event); } private void setLastModified(WindowEvent event) { 48 PortletSession session = event.getRequest().getSession(false); if (session != null) { session.setAttribute(BookmarkPortlet_14.LAST_MODIFIED, new Long(System.currentTimeMillis())); } } public void windowDetached (WindowEvent event) throws PortletException { } } BookmarkPortlet_14.java package com.mycompany.portlets.bookmark; … public class BookmarkPortlet_14 extends PortletAdapter { public final static String LAST_MODIFIED = "com.mycompany.portlets.bookmark.lastModified"; … public long getLastModified(PortletRequest request) { PortletSession session = request.getSession(false); if (session != null) { Long lastModified = (Long) session.getAttribute(LAST_MODIFIED); if (lastModified != null) { return lastModified.longValue(); } } return -1; } } BookmarkBrowserPortlet_14.java package com.mycompany.portlets.bookmark; … public class BookmarkBrowserPortlet_14 extends PortletAdapter { … public long getLastModified(PortletRequest request) { PortletSession session = request.getSession(false); if (session != null) { String url String oldUrl Long lastModified = (String) session.getAttribute("url"); = (String) session.getAttribute("oldUrl"); = (Long) session.getAttribute("lastModified"); if (url != null) { if ((oldUrl == null) || (!url.equalsIgnoreCase(oldUrl))) { session.setAttribute("oldUrl", url); long time = System.currentTimeMillis(); session.setAttribute("lastModified", new Long(time)); return time; } return lastModified.longValue(); } } return -1; } } 49 5.3. Portlet Services To enable portlets to use plugable services via dynamic discovery, the Portlet API provides the PortletService interface. A PortletService is accessed from the PortletContext.getService() method that looks up the appropriate factory for the service, creates the service, and returns it to the portlet. Take for example a Credential Vault that enables portlets to access credentials for authentication. The CredentialVault.class would be available on the portal server but does not belong to the portlet application (that is, it is not packaged with the portlet’s PAR file). That service can be retrieved as follows: Example: Portlet using a Portlet Service import org.apache.jetspeed.portlet.service.PortletService; … PortletService portletService = (PortletService) portletContext.getService(CredentialVault.class); Various services may be implemented by different vendors, for example, a SearchService, LocationService, ContentAccessService, or a MailService. The Portlet API provides a ContentAccessService. 5.3.1. ContentAccessService To retrieve content from sources outside the intranet, usually a proxy must be used. To enable portlets to transparently make use of a proxy to access remote content, WPS provides a ContentAccessService. This service allows portlets to access content either from remote web sites or from JSPs and servlets of the whole portal web application. (In contrast, the PortletContext.include method only includes JSPs local to the portlet application.) Hint: The disadvantage when accessing internet resources directly is that the content is simply taken and written to the portlet’s output. This means that all relative links to other pages or images will be broken. This can be solved by parsing the content or use some enhanced browser portlet. Example: Portlet using ContentAccessService import org.apache.jetspeed.portlet.service.ContentAccessService; … ContentAccessService service = (ContentAccessService)portletContext.getService(ContentAccessService.class); //get an URL URL url = service.getURL(“http://www.ibm.com”, request, response); //include the content of a web site into the portlet service.include(“http://www.ibm.com”, request, response); //include a JSP or servlet belonging to the portal web application service.include(“/templates/weather.jsp”, request, response); 50 51 6. Portlet development issues 6.1. No instance and class variables Limit the use of instance or class variables in a portlet. Within WebSphere Portal Server, only a single instance of the Portlet class is instantiated. Even if different users have the same portlet on their pages, it is the same portlet object instance that generates the markup. Therefore, each portlet instance has to be treated like a singleton. The implication is that you should not use instance or class variables to store any state or other information between calls to the portlet. Being a singleton, each instance variable of a portlet effectively becomes a class variable with similar scope and the potential for problems when the variable is accessed many times in each user's context of the portlet. It is better to avoid these problems by limiting the use of instance or class variables to read only values. Otherwise, reentrance or thread safety may be compromised. There is an exception to this rule in that you can use instance variables. Variables whose values do not change can be stored safely in instance variables. For example, on initialization of the portlet, its configuration is set within the portlet. Because the configuration does not change for different users or between calls, you can store it in an instance variable. 6.2. Storing data The following lists where data is stored by a portlet, depending on how it is used. 6.3. Be careful with use of class variables, these will be shared by all users who access the portlet. Administrator's config data is stored in the portlet.xml User preferences are stored in persistence using PortletData. Portlet beans for a particular view or mode should be added to a PortletRequest Portlet beans containing global portlet information should be stored in a PortletSession Namespaces/Javascript Resource URIs should be namespace-encoded, allowing the portal to supply a direct path to the resource with respect to protected, unprotected, and NLS resource placement. You should also encode named elements (form fields, Javascript variables, etc) to avoid clashes with other elements on the portal page or in other portlets on the page. See PortletURI for more information about encoding . 6.4. 52 Multi-markup support Whenever possible, provide support for more than one type of markup (HTML, WML, and cHTML). This means that you will need to use the getMarkupName() method from the Client object to test for the markup. You must also indicate the markup types supported by your portlet in the portlet deployment descriptor. 6.5. Multi-language support To prepare your portlet for translation, be sure all strings are retrieved from a properties file using getText() (see BookmarkPortlet_05 for an example) rather than hard-coded in the portlet. Allow for string expansion in the portlet user interface for certain NLS languages. 6.6. Portlet creation guidelines 6.6.1. Performance issues The WebSphere Portal Server uses the same portlet instance to generate the markup for different pages for different users. There are a limited number of threads that perform these tasks. It is imperative to implement the portlet to do its job as quickly as possible so that response time for a whole page is optimized. Optimizations you should consider are: 1. Limit the use of the synchronized methods or synchronized code blocks in the processing of a portlet method. 2. Limit the use of complex string operations. This includes Minimize transient XSL style sheets as they cause a lot of temporary Java object instantiations Consider using a StringBuffer to replace temporary String objects. These are more efficient in processing strings in general and also don't generally instantiate other temporary String objects. 3. Limit the use of long running loops. 4. Do not create large pools of threads. 5. Minimize calls to external data sources. If possible, set your portlet to cache its output. 6. Use JSPs instead of XSL. JSP with view beans are much faster and instantiate less intermediate Java objects. The XSL engine creates a lot of temporary strings. 7. Portlet JSPs can be optimized for performance by reducing white space and using the non-transferrable JSP comment format (<%-- --%>) as opposed to the HTML comment format (<!-- -->). 8. Minimize Java object instantiations. 53 6.6.2. Guidelines for markup One of the goals in writing portlet markup is to provide a consistent, clean, and complete user interface. The portal server page is displayed using skins and themes defined by the portal administrator. Themes represent the look and feel of the portal overall, including colors and fonts. Skins represent the border rendering around an individual portlet. WebSphere Portal Server is installed with some predefined skins and themes. For portlets to appear integrated with an organization's portal or user's customized portal, they should generate markup that invokes the generic style classes for portlets, rather than using tags or attributes to specify colors, fonts, or other visual elements. See the WebSphere Portal Server InfoCenter for more information about use of style sheets, themes, and skins. Portlets are allowed to render only markup fragments, which are then assembled by the portlet framework for a complete page. Therefore, be sure to exclude all pagelevel tags, such as <html>, <head>, <body>, <wml>, and so on. Because HTML, WML, and cHTML code fragments are embedded into other code fragments, make sure that they are complete, contain no open tags, and only contain valid HTML, WML, or cHTML. This helps to prevent the portlet's HTML code, for example, from corrupting the portal's aggregated HTML code. It is recommended to use a validation tool, such as the W3C HTML Validation Service or a tool from a markup editor, such as WebSphere Studio. For specific guidelines for HTML, WML, and cHTML, see the WebSphere Portal Server InfoCenter . 6.6.2.1. Accessibility When writing the markup for a portlet, consider how the portlet will be accessed by people with disabilities. For example, people with sight limitations might not be able to distinguish certain colors or require the assistance of a reader to access the content of your portlet. Make the markup accessible: For all field prompts (labels) be sure to use the <LABEL> tag around the label for screen readers. Test the portlet with each browsers range of large and small fonts. You can find out more about accessibility guidelines the Center for Applied Special Technology (CAST) or W3C Web Accessibility Initiative . IBM provides guidelines for Web accessibility at http://www.ibm.com/able/accessweb.html . 54 7. Using Single Sign-On Single sign-on support in WebSphere Portal Server allows users to communicate through the portal server, via portlets, with many different back end systems without being prompted multiple times for a username and password by each individual portlet's back end server. WebSphere Portal Server internally makes use of the Java Authentication and Authorization Service (JAAS) API to provide a framework for integrating single sign-on into portlets and via these portlets to back end applications. After a user is authenticated, WebSphere Application Server generates a security context for the user. Websphere Portal Server uses this security context to set up (internally to the portal) a JAAS Subject containing several Principals and Private Credentials. When the portal first handles the HTTP request, it will set up the JAAS Subject and make it available as an attribute of the User, which the portlet accesses from the PortletSession object. From the Subject, a portlet can use one of the following methods to extract the Principal and Credentials to pass to its back end application. getPrincipals(java.lang.Class) Class is optional. If no argument is specified, this method returns a set containing all Principals. Use the Class.forName(name) method to generate a class argument for getPrincipals. See the following example. // Retrieve the Subject from the Session PortletSession session = request.getSession(); UserImpl user = (UserImpl) session.getUser(); Subject subject = user.getSubject(); // Get the User DN Set set = subject.getPrincipals(Class.forName("com.ibm.wps.sso.UserDNPrincipal")); UserDNPrincipal userDn = null; if (set.hasNext()) { userDn = (UserDNPrincipal) set.next(); // Got it, print out what it contains. System.out.println("The User DN is " + userDn.getName(); } The org.apache.jetspeed.portletcontainer.UserImpl class implements the org.apache.jetspeed.portlet.User interface in addition to the getSubject() method. Please see the Portlet API documentation for the User interface for more information about UserImpl. The following are some of the class names that can be used with Class.forName() on this method com.ibm.wps.sso.UserDNPrincipal returns the user's distinguished name from LDAP com.ibm.wps.sso.GroupDNPrincipal 55 returns the distinguished name of an LDAP group of which the user is a member. There are as many instances of GroupDNPrincipals as the number of LDAP groups to which the user belongs. com.ibm.wps.sso.UserIdPrincipal returns the ID with which the user logged on to the portal only if an authentication proxy was not used com.ibm.wps.sso.PasswordCredential returns the password with which the user logged on to the portal. Available only if an authentication proxy was not used. All Principals descend from java.security.Principal from the standard JDK. The portlet calls getName() to extract the information contained in these Principals. Before the portlet can extract any information from the Subject, it must first get the PortletSession, then the User, and finally the Subject. getPrivateCredentials(java.lang.Class) Class is optional. If no argument is specified, this method returns a set containing all PrivateCredentials. Also use Class.forName(name) to get a Class object from a classname string for this method. To obtain the user's CORBA credential, specify org.omg.SecurityLevel2.Credentials as the argument for Class.forName() . To compile a portlet that implements single sign-on, the following JARs must be in your CLASSPATH in addition to the JAR files listed in compiling Java source . wps_root/app/web/WEB-INF/lib/wpssso.jar; wps_root/app/web/WEB-INF/lib/um.jar; app_server/lib/personalization.jar 7.1. Basic authentication sample The following example demonstrates how to the extract the Subject from the PortletSession and then how to extract the user name and password from the Subject. The portlet then creates a Basic Authentication Header and attempts to connect to a URL protected by basic authentication. The URL is specified as a configuration parameter in the portlet deployment descriptor (portlet.xml) . For output, it displays the user name, password, and the results of the connection attempt. In the service() method, the JAAS Subject is extracted from the User object in the PortletSession by casting the User to org.apache.jetspeed.portletcontainer.UserImpl . This object has the getSubject() method. import import import import 56 java.io.*; java.net.*; java.util.*; java.security.*; import javax.security.auth.*; // for the JAAS Subject import com.ibm.wps.sso.*; // for the Principals contained in the Subject import com.ibm.ejs.security.util.Base64Coder; import org.apache.jetspeed.portlet.*; import org.apache.jetspeed.portlets.*; public class URLSSOSamplePortlet extends AbstractPortlet { /** URL To Access */ private String urlSpec = null; /** * Initialization Routine * * @param PortletConfig Portlet Configuration * @throws UnavailableException */ public void init(PortletConfig portletConfig) throws UnavailableException { super.init(portletConfig); // Get the URL urlSpec = portletConfig.getAttribute("url"); } /** * Retrieves the specified Principal from the provided Subject * * @param Subject subject * @param String Class Name * @return The values of the Principals for the given class name, null if * nothing was returned * @throws PortletException An error occured retrieving the Principal */ private String[] getPrincipalsFromSubject(Subject subject, String className) throws PortletException { try { // Get the Set of Principals Object[] principals = subject.getPrincipals(Class.forName(className)).toArray(); // Do we have any? if ((null == principals) || (0 == principals.length)) { // No principals of this class name return(null); } // Create the String Array to hold the values String[] values = new String[principals.length]; // Populate the values Array for (int current = 0; current < values.length; current++) { values[current] = ((Principal) principals[current]).getName(); } return(values); } catch (ClassNotFoundException cnfe) { throw(new PortletException("Class " + className + " could not be found", cnfe)); } } /** * Extracts the UserID and Password from the JAAS Subject, creates a Basic * Auth Header from it, and then connects to the resource. * * @param PortletRequest Request Object * @param PortletResponse Response Object * @throws PortletException * @throws IOException */ public void service(PortletRequest portletRequest, PortletResponse portletResponse) throws PortletException, IOException { 57 // Get the Writer from the Response PrintWriter writer = portletResponse.getWriter(); // Get the Portlet Session PortletSession session = portletRequest.getSession(); // Get the User Object from the Portlet Session org.apache.jetspeed.portletcontainer.UserImpl user = (org.apache.jetspeed.portletcontainer.UserImpl) session.getUser(); // Next, grab the Subject Subject subject = user.getSubject(); // Grab the UserID and Password. There will only be one of each // contained in the Subject. String[] userId = getPrincipalsFromSubject(subject, "com.ibm.wps.sso.UserIdPrincipal"); String[] password = getPrincipalsFromSubject(subject, "com.ibm.wps.sso.PasswordCredential"); // The URL writer.println("<TABLE>"); writer.println("<TR><TD><B>URL:</B></TD><TD>" +urlSpec + "</TD></TR>"); // Show the UserId and Password writer.println("<TR><TD><B>UserID:</B></TD><TD>" + userId[0] + "</TD></TR>"); writer.println("<TR><TD><B>Password:</B></TD><TD>" + password[0] + "</TD></TR>"); // Create the Basic Auth Header String basicAuth = Base64Coder.base64Encode(userId[0] + ":" + password[0]); basicAuth = "Basic " + basicAuth; writer.println("<TR><TD><B>Basic Auth Header:</B></TD><TD>" + basicAuth + "</TD></TR>"); writer.println("</TABLE><BR>"); // Create the URL for our protected resource, and get a URLConnection try { URL url = new URL(urlSpec); HttpURLConnection con = (HttpURLConnection) url.openConnection(); // Set our Basic Auth Header con.setRequestProperty("authorization", basicAuth); // Connect con.connect(); // Get the Response Code String responseMessage = con.getResponseMessage(); int responseCode = con.getResponseCode(); // Were we successful? if (HttpURLConnection.HTTP_OK == responseCode) { writer.println("<P>Successfully connected to " + urlSpec + "!</P>"); } else { writer.println("<P>Unable to successfully connect to " + urlSpec + ", HTTP Response Code = " + responseCode + ", HTTP Response Message = \"" + responseMessage + "\"</P>"); } } catch (Exception e) { writer.println("<P>Exception caught in HTTP Connection:</P><TT>"); writer.println(e.getMessage()); e.printStackTrace(writer); writer.println("</TT>"); } } } 58 7.2. LTPA example Lightweight third-party authenticaiton (LTPA) is the mechanism for providing single sign-on between WebSphere Application Server and Domino servers that reside in the same security domain. The LTPA token is carried in the HTTP request as a cookie. The example in this section assumes that: 1. The portlet is accessing a protected resource on the same WebSphere Application Server that WebSphere Portal Server is installed on. 2. If the resource resides on another WebSphere Application Server or Domino Server, then single sign-on is enabled between them. Extracting the LTPA token Object[] temp = subject.getPrivateCredentials(LTPATokenCredential.class).toArray(); LTPATokenCredential ltpaToken = (LTPATokenCredential) temp[0]; // Create the LTPA Cookie Header String cookie = "LtpaToken=" + ltpaToken.getTokenString(); // Create the URL for our protected resource, and get a URLConnection URL url = new URL("http://myserver.example.com/ltpa_protected_resource"); HttpURLConnection con = (HttpURLConnection) url.openConnection(); // Set our LTPA token Cookie con.setRequestProperty("cookie", cookie); // Connect con.connect(); End of document 59