Novell Home

Migrating NPS gadgets to portlets Overview

Novell Cool Solutions: Feature
By Bill Bodine

Digg This - Slashdot This

Posted: 2 Jun 2004
 

Introduction

Novell Portal Services (NPS) was a product released by Novell a few years back that enjoyed considerable success. Its success can be attributed to a few things:

  • It has a very simple setup and installation paradigm
  • It is easily administered
  • It uses eDirectory as its authorization and authentication engine allowing corporations to leverage the investment they have in their corporate directories
  • Many applications (gadgets) have been written using the NPS development kit

However, one of the concerns directed toward NPS is the fact that it does not use all industry standard protocols. The gadget development kit for example, while very similar to the industry standard portlet design, is still proprietary and different. An NPS gadget will not run in the portal environment of another vendor.

With the release of the exteNd Director 5 product, Novell has incorporated many of the strong features of NPS mentioned previously with the capabilities of exteNd Director Enterprise edition. While this is a great and successful product, it cannot run NPS gadgets. Since the number and caliber of gadgets was such a compelling benefit of NPS, this article is written to briefly describe some of the similarities between gadget and portlet (the industry standard way of developing portal applications) development and to help any interested developers see that they can, for the most part, with a simple port, convert their gadget applications to be portlets.

The critical thing to understand regarding gadget development is that there are a few Java methods that need to be implemented by the developer and that the gadget returns information to the portal server in XML documents. These documents are then transformed using xslt stylesheets to html or whatever other format should be displayed in a user's browser window.

Similarly, to develop a portlet there are very few methods that need to be implemented, and while a portlet does not always have to return xml documents to be rendered by stylesheets, it is one of the rendering mechanisms that can be used. Using exteNd Director 5, a developer has the option of using new drag and drop tools to develop portlets, and many will and should use these tools. However, in this paper, I am going to discuss the Java development of portlets specifically in order to leverage the similarities between gadget and portlet development. This is not intended to be an exhaustive look at the development of portlets, but it will be enough to highlight the similarities and give the developer a reasonable understanding of what to expect while porting their application.

First Programs

Initially I will explore how a developer will create the obligatory HelloWorld application in each of the two technologies. When creating a HelloWorld gadget, the developer will:

  1. Extend the BaseGadgetInstance object class.
    This class implements all the methods that are needed for a gadget. The developer overrides the specific methods that are needed in their particular case.
  2. Override the getData() method
    The getData() method is used to write XML data to the portal. The developer can use any api's and methods they choose to build the output, as long as the result is a serialized XML document.
  3. Create a main.xsl stylesheet.
    By default NPS uses a stylesheet named 'main.xsl', that is located in the <<web container>\webapps\nps\portal\gadgets\HelloWorld folder (if the class file is created in its own package, then the package name will precede the object class in this path)
  4. Package the gadget.
    A 'Packager' utility ships with the NPS sdk that is used to assemble all the files associated with gadget into one file. When done packaging, the gadget will have the '.npg' extension. An 'npg' file is really the same thing as a JAR file. In fact, as with a JAR file, NPG files can be viewed with the WinZIP utility.
  5. Install and run the gadget.
    Gadgets are installed with an administrative utility that accesses the .NPS file and deploys its contents to the correct locations. The gadget can be tested by either using a utility designed for testing gadgets, the GadgetRunner, or the gadget can be assigned to a portal page and accessed by an authenticated user on the portal.

Gadget HelloWorld

import com.novell.nps.gadgetManager.BaseGadgetInstance;
import com.novell.nps.gadgetManager.GadgetInstanceException; 
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedWriter;
import java.io.IOException;

public class HelloWorld extends com.novell.nps.gadgetManager.BaseGadgetInstance
{
   public void getData( HttpServletRequest req, BufferedWriter out, Document domTree )
   {
	out.write( "<HELLOWORLD>");
	out.write("Hello, World?from a Gadget");
	out.write("</HELLOWORLD>");
   }
}

Gadget Stylesheet

<xsl:stylesheet version="1.0" xmlns:xsl:=http://www.w3.org.1999/XSL/Transform>
   <xsl:template match="HELLOWORLD">
      <h1><b><xsl:value-of select="."/></b></h1>
   </xsl:template>
</xsl:stylesheet>

When developing a portlet the developer will:

  1. Extend the GenericPortlet class.
    This is an abstract class and is very similar to the BaseGadgetInstance class. A developer needs to override a few of its methods to get a properly working portlet. For example, if the doView() method is not overwritten then the default implementation just throws an exception.
  2. Override the doView() method.
    This method serves the same purpose and is used in essentially the same way as is the getData() method for gadgets. While it can output various types (HTML, XHTML, XML), for this example, its output will be a serialized XML document.
  3. Create an xslt stylesheet ( noting that data can be returned in other methods, but we are exploring stylesheets in this paper).
    In this example you could literally use the very same stylesheet that was used for the gadget hello world program. For exteNd director, when using the ExpressPortal, the portlet stylesheets are found in: ExpressPortal.spf\WEB-INF\lib\(ExpressPortal_resource.jar)\portal-style\BooksXMLPortlet_HTML.xsl
  4. Create the JAR file.
    As was mentioned earlier, JARs and NPGs serve the same purpose but are built using different tools. The Director Designer tool will build the jar files and make them ready for deployment.
  5. Deploy and run the portlet.
    Portlets are deployed into a web container as are all other J2EE web components. Director Designer can be used for developing and deploying web components. When using the Designer, portlets are automatically deployed to the portal when built.
    After a developer has used Director Designer to develop and deploy a portlet to an exteNd 5 Application Server, a "Portlet Definition" and a "Portlet Registration" will exist on the Application Server. Administrators can, through the Director Administration Console (DAC), create many registrations for a given definition. The reason to create more than one would usually deal with preferences and settings that may be different in one registration than they are in another. It is these Portlet Registrations that the administrator assigns to pages within their portal.

Portlet HelloWorld

import java.io.PrintStream;
import java.io.PrintWriter; 
import javax.portlet;
import com.sssw.fw.api.*;;
import com.sssw.portal.api.*;
import com.novell.afw.portlet.api.EbiPortletConstants;

public class HelloWorld extends GenericPortlet
{
   public void init() throws PortletException
   {
   }
   public void doView(RenderRequest request, RenderResponse response ) 
	throws IOException
   {
      try {
         PortletURL renderUrl = response.createRenderURL();
         RenderUrl.setPortletMode( PortletMode.VIEW);

         response.setContentType( EbiPortletConstants.MIME_TYPE_XML );
         PrintWriter writer = response.getWriter();
         writer.println("<HELLOWORLD>");
         writer.println("Hello, World?from a Portlet");
         writer.println("</HELLOWORLD>");
      }
      catch ( Throwable e ) {
         new PortletException( e );
      }
}

Portlet Stylesheet

<xsl:stylesheet version="1.0" xmlns:xsl:=http://www.w3.org.1999/XSL/Transform>
   <xsl:template match="HELLOWORLD">
      <h1><b><xsl:value-of select="."/></b></h1>
   </xsl:template>
</xsl:stylesheet>

This is a very simple program, but it does show the similarities between gadgets and portlets. You can see that once you output your data as a serialized XML document, the stylesheet used to render the document, in this case, is identical for both.

Second Program - accepting input from a portal user

The HelloWorld program is nice for a simple example. However, there are some critical pieces that it does not include. For example, most portlets (and gadgets) need to receive input of some kind from users of their application. To do this, Gadget developers override the "processRequest" method. Usually the following signature is used:

void processRequest( javax.servlet.http.HttpServletRequest )

This method is used to set state and perform any actions that may need to occur within the gadget. When a user submits data to a gadget the portal first calls the processRequest() method and then calls the getData() method to render what has been set in the processRequest() method.

While it is possible to create a complete gadget by overriding the getData and processRequest methods, many developers have chosen to use an additional feature available in the NDK that lets them build special purpose methods for gadget code. This is done with the addActionListener() method. It has the following signature:

void addActionListener( java.lang.Object, java.lang.String )

If the following method was called from within the init method in the gadget code:

addActionListener( this, "Browse" )

then the developer would also implement the following two additional methods.

getBrowseData(HttpServletRequest req, BuffererdWriter out, Document domDocument);
onBrowseAction( HttpServletRequest req )

Using this, if a user of the gadget submits a form to the portal that references the 'Browse' action, then the developer is able to have isolated code that is written for that specific task. It makes for nice readable code.

Portlet's accept user input in very much the same way as gadgets. It has already been shown that the doView() method is used to render content much like the getData() method is in Gadgets. Likewise, the processAction() method, with the following signature, serves the same purpose as the processRequest() method for gadgets.

void processAction( ActionRequest, ActionResponse )

The ActionRequest and ActionResponse classes are very similar to the HttpServletRequest and HttpServletResponse classes used by Gadgets and servlets. The main thing that does not exist in the portlet specification is an equivalent to the addActionListener() method. For that reason, gadget developers porting their code to be portlets will need to adjust their code enough so that the doView() and processAction() methods will deal with all the data input and output from their portlet.

Consider the following simple example that allows a user to click either of two buttons to display a list of fiction or non-fiction books.

BooksPortlet

package com.novell.books;

// Java imports
import java.io.PrintStream;
import java.io.PrintWriter;
import javax.portlet.*;

// Portal/Framework imports
import com.sssw.fw.api.*;
import com.sssw.portal.api.*;

// Portlet API imports
import com.novell.afw.portlet.api.EbiPortletConstants;

// xml imports
import com.novell.afw.util.EboXmlUtil;
import org.w3c.dom.*;

/* BooksPortlet  */
public class BooksPortlet extends GenericPortlet {
   // an Instance of a log for error/trace reporting
   private static EbiLog m_log =
  com.sssw.fw.log.EboLogFactory.getLog(com.sssw.fw.log.EboLogFactory.PORTLET);

   /* Get the initialization parameters from the portlet.xml file */
   public void init() throws PortletException {
   }
 
   /**
    * Helper method to serve up the mandatory view mode
    * @param request        an portlet request object
    * @param response       an render response object
    */
   public void doView( RenderRequest request, RenderResponse response ) throws 
  PortletException, java.io.IOException {                       
	        
  try {
       PortletURL actionUrl = response.createActionURL();
       actionUrl.setPortletMode( PortletMode.VIEW );
                    
       response.setContentType( EbiPortletConstants.MIME_TYPE_XML );
       PrintWriter writer = response.getWriter();
			
        Document baseDoc = BuildDomXml( renderUrl, request );
       // Write the output for the response
       writer.write( EboXmlUtil.serialize( baseDoc ) );
       writer.flush();
     } 
     catch ( Throwable e ) {
       // Log any errors generated
       m_log.error(e);
       new PortletException( e );
     }
   }
 
   /**
    * Process any requests that the portlet may have.
    * @param request           an action request object
    * @param actionResponse    an action response object
    */
   public void processAction (ActionRequest request, ActionResponse response) throws
  PortletException, java.io.IOException {
        
      try {
        PortletContext portletContext = getPortletContext();
			
	String buttonPushed = null;
			
	buttonPushed = request.getParameter( "GetFiction" );
	if ( buttonPushed != null ) {
		request.getPortletSession().setAttribute("TypeOfView", "FictionView" );
	}
	
	buttonPushed = request.getParameter( "GetNonFiction" );
	if ( buttonPushed != null ) {
		request.getPortletSession().setAttribute("TypeOfView", "NonFictionView" );
	}
    
            // only log items if the log level indicates we should
            if ( m_log.isTrace() ) {
                m_log.trace( "BooksPortlet in processAction method" );
            }
            // Do "save" processing here if necessary, and then
            // set the portlet mode to be "View" after completion.
            response.setPortletMode( PortletMode.VIEW );
        }
        catch ( Throwable e ) {
            // Log any errors generated
            m_log.error( e );
            new PortletException( e );
        }
    }

private Document BuildDomXml( PortletURL portletUrl, RenderRequest request ) throws
Exception {
   Document baseDoc = null;

   String typeOfView = (String)request.getPortletSession().getAttribute("TypeOfView");
   if ( typeOfView == null || typeOfView.equals("InitialView") ) {
      baseDoc = EboXmlUtil.createDocument();
      Element rootElement = baseDoc.createElement( "nobooks" );
      baseDoc.appendChild( rootElement );
      rootElement.setAttribute( "action", portletUrl.toString() );
   }
   else {
    // Create the new document
         baseDoc = EboXmlUtil.createDocument();
         Element rootElement = baseDoc.createElement ("books");
         baseDoc.appendChild( rootElement );
         rootElement.setAttribute( "action", portletUrl.toString() );

         // Now for the books
         if ( typeOfView.equals("FictionView") ) {
            rootElement.setAttribute( "booktype", "Fiction" );
            // First Harry Potter
            Element book1Element = baseDoc.createElement( "book" );
            rootElement.appendChild( book1Element );
            Element book1Author = baseDoc.createElement( "Author" );
            book1Element.appendChild( book1Author );
            book1Author.appendChild( baseDoc.createTextNode( "JK Rawlings" ) );
            Element book1Title = baseDoc.createElement( "Title" );
            book1Element.appendChild( book1Title );
            book1Title.appendChild( baseDoc.createTextNode( "Harry Potter and the
			Chamber of Secrets" ) );
            Element book1Price = baseDoc.createElement( "Price" );
            book1Element.appendChild( book1Price );
            book1Price.appendChild( baseDoc.createTextNode( "12.95" ) );
            // Now Sword of Honour
            Element book2Element = baseDoc.createElement( "book" );
            rootElement.appendChild( book2Element );
            Element book2Author = baseDoc.createElement( "Author" );
            book2Element.appendChild( book2Author );
            book2Author.appendChild( baseDoc.createTextNode( "Evelyn Waugh" ) );
            Element book2Title = baseDoc.createElement( "Title" );
            book2Element.appendChild( book2Title );
            book2Title.appendChild( baseDoc.createTextNode( "Sword of Honour" ) );
            Element book2Price = baseDoc.createElement( "Price" );
            book2Element.appendChild( book2Price );
            book2Price.appendChild( baseDoc.createTextNode( "5.95" ) );
            // Then Lord of the Rings
            Element book3Element = baseDoc.createElement( "book" );
            rootElement.appendChild( book3Element );
            Element book3Author = baseDoc.createElement( "Author" );
            book3Element.appendChild( book3Author );
            book3Author.appendChild( baseDoc.createTextNode( "JRR Tolkien" ) );
            Element book3Title = baseDoc.createElement( "Title" );
            book3Element.appendChild( book3Title );
            book3Title.appendChild( baseDoc.createTextNode( "The Lord of the Rings" ) );
            Element book3Price = baseDoc.createElement( "Price" );
            book3Element.appendChild( book3Price );
            book3Price.appendChild( baseDoc.createTextNode( "15.95" ) );
      }
      else if( typeOfView.equals("NonFictionView") ) {
            rootElement.setAttribute( "booktype", "NonFiction" );
            Element book1Element = baseDoc.createElement( "book" );
            rootElement.appendChild( book1Element );
            Element book1Author = baseDoc.createElement( "Author" );
            book1Element.appendChild( book1Author );
            book1Author.appendChild( baseDoc.createTextNode( "Nigel Rees" ) );
            Element book1Title = baseDoc.createElement( "Title" );
            book1Element.appendChild( book1Title );
            book1Title.appendChild( baseDoc.createTextNode( "Sayings of the Century" ) );
            Element book1Price = baseDoc.createElement( "Price" );
            book1Element.appendChild( book1Price );
            book1Price.appendChild( baseDoc.createTextNode( "8.95" ) );
      }
   }
	
   return baseDoc;
}
}

Books Stylesheet

<xsl:template match="books">
   <form action="{/books/@action}" method="POST" name="BookLookup">
   <table width="100%">
      <xsl:if test="@booktype='Fiction'">
         <tr><td><font size="10">Fiction Novels</font></td></tr>
      </xsl:if>
      <xsl:if test="@booktype='NonFiction'">
         <tr><td><font size="10">Non Fiction Novels</font></td></tr>
      </xsl:if>
         <tr>
            <td>
               <table border="" width="100%">
                  <xsl:apply-templates/>
               </table>
            </td>
         </tr>
         <tr><td>
         <xsl:if test="@booktype='Fiction'">
            <input class="portlet-form-button" name="GetNonFiction" style="height:14pt;"
			 type="SUBMIT" value="Get Non-Fiction Books"/>
           </xsl:if>
           <xsl:if test="@booktype='NonFiction'">
               <input class="portlet-form-button" name="GetFiction" style="height:14pt;"
			    type="SUBMIT" value="Get Fiction Books"/>
           </xsl:if>
           </td></tr>
       </table>
   </form>
</xsl:template>

<xsl:template match="book">
   <tr>
      <td><xsl:number/></td>
         <xsl:apply-templates/>
   </tr>
</xsl:template>

<xsl:template match="Author|Title|Price">
   <td>
       <xsl:value-of select="."/>
   </td>
</xsl:template>

<xsl:template match="nobooks">
   <table border="0" bordercolor="#111111" cellpadding="0" cellspacing="0" width="100%">
       <tr>
          <td valign="top" width="100%">
             <form action="{/nobooks/@action}" method="POST" name="BookLookup">
                <tr><td><h1>Pick one of the following</h1></td></tr>
                   <tr><td>
                      <input class="portlet-form-button" name="GetFiction" style=
			 "height:14pt;" type="SUBMIT" value="Get Fiction Books"/>
                      <input class="portlet-form-button" name="GetNonFiction" style=
			 "height:14pt;" type="SUBMIT" value="Get Non-Fiction Books"/>
                   </td></tr>
               </form>
          </td>
       </tr>
   </table>
</xsl:template>

There are a few points that need to be mentioned while viewing the preceding code when considered from a Gadget development perspective.

  1. Portlets have their own url.
    One of the significant differences between portlets and gadgets is that portlets are referenced by their own url, whereas gadgets are referenced by the portal's url and their own "Gadget ID". Gadget developers become quite adept at ensuring that, within their stylesheets, they correctly reference their gadget ID at all the right locations to ensure that their code will be invoked when a form is submitted.
    Portlets, however, are treated like servlets. You can see from the portlet stylesheet above that the "form" element has an "action" attribute that references the url of the portlet. The portlet java code derived this url by calling the creationActionURL() method on the RenderResponse class and attaching this to the XML document sent from the doView() method.
  2. HTTP is still the protocol.
    With the portlet development, the same methods are used to extract data from an HTTP form as are used in Gadget and Servlet development. Note the getParameter() calls that are made from within the processAction() method.
Final Points

By using what has been discussed already, a developer should have a pretty good start on how to convert their gadget code to portlet code. There are a few additional items that I will discuss here that reflect issues that many developers may come up against.

  1. Referencing images from a stylesheet
    Most portal applications, whether they be gadgets or portlets, will display images in their renderings. While the stylesheets will look similar in how these images are displayed, the key difference is in how the images are referenced. It has already been mentioned that NPS typically expects each gadget to have a 'main.xsl' stylesheet associated with it. To render a portal page, NPS includes this stylesheet with a few others (other gadgets, the portal theme, etc.) to form one big stylesheet. Included in this is a variable, Portal.ResourcePath, that the individual gadget stylesheets reference to find their resources. So the following html statement could be used to reference an image from a gadget stylesheet:

    <img border="0" height="16" width="16"
     src="{$Portal.ResourcePath}/gadgets/HelloWorldGadget/images/World.gif"/>

    Since portlet stylesheets are not included in one big stylesheet as are gadgets, resources are not accessed by a common global variable. Instead, the portlet developer should return the resource path as an attribute or element in the XML document that is output from the processAction() method. The following code shows how a context object for the current portlet can be obtained. This context is then used to retrieve the resource path that can be included in the outgoing XML document as either an attribute or an element.

    EbiPortalContext context =
    (EbiPortalContext)request.getAttribute(EbiPortletConstants.EBI_PORTAL_CONTEXT);
    String resourcePath =
    com.novell.afw.portal.aggregation.EboPortalResourceHelper.getResourceSetPath(context);

    Assuming that the preceding string was returned as an attribute named 'resourceUrl' on the root node element in your XML document, the following statements in your stylesheet could be used to reference an image for your portlet.

    <xsl:variable name="portlet.resource.path">
       <xsl:value-of select=//@resourceUrl/>
    </xsl:variable>
    <img border="0" height="16" width="16" src="{$protlet.resource.path}/images/World.gif"/>

    An additional important point on this example is that the 'World.gif' file would have been placed in the <ProjectDir>/data/images directory for us to reference it as we have here.

  2. Configuration settings
    NPS uses an XML file named 'AvailableSettings.xml' to set configuration settings on a gadget. The portal administrator can then edit these settings in the administrative console available with NPS. Individual users can also be given rights to edit certain of these values depending on assignment by the portal administrator.

    Since exteNd Director 5 and portlets follow the J2EE specification, a portlet 'deployment descriptor' is used to identify and set setting values rather than the AvailableSettings.xml file used with gadgets. When using the Director Designer tool the deployment descriptor for a HelloWorld portlet, for example, can be found in the 'Archive layout' view at <ProjectDir>/portal-portlet/HelloWorld.xml. There is a schema definition that defines what can exist in this descriptor, but the main elements of interest here are: 1) the <portal-preferences> element that is the parent element for all the preferences (settings) that will exist for this gadget. 2) the <preference> element that identifies a unique preference, 3) the <name> element that of course assigns a name to the preference and 4) the <value> element. A preference can have more than one value element. So the following is an example that could exist in the HelloWorld portlet.

    <portlet-preferences>
       <preference>
          <name>Language</name>
          <value>English</value>
          <value>Spanish</value>
       </preference>
       <preference>
          <name>Season</name>
          <value>Winter</value>
       </preference>
    <portlet-preferences>
    An administrator has the option of modifying these settings when registering a portlet in the Director Administration Console (DAC) and a user can edit these (when allowed by the administrator) by clicking the 'Edit' button in the portal window.

    For the user to be able to use the 'Edit' button requires the portlet developer to have implemented the doEdit() method in their code. Once implemented, the developer can update these values and provide any user interface to the user that is deemed appropriate for editing the portlet settings.


  3. Packaging/distributing a portlet.
    The final topic to discuss is how a portlet developer can 'package' their portlet for distribution to other portal applications. The first helpful point to understand about portal applications like the ExpressPortal application that ships with exteNd Director 5, is that they are built and deployed as either a Web Archive (WAR) or an Enterprise Archives (EAR). The Director documentation contains a good description of what WARs and EARs are, but in essence, since applications like the ExpressPortal are deployed to 'Web Containers' like the exteNd Application Server, then they are built as Web archives, or WAR files (an EAR, since it can contain WAR files, can also be deployed to a web container). WAR files typically include components that are assembled as Java Archive (JAR) files. A portlet is an example of a JAR that will be included in a WAR. So the developer of a portlet needs to package the portlet as a JAR file. This is easily done in the Director Designer by using steps similar to the following for our HelloWorld example:

    1. In the Navigation page of the Designer click the 'Resources' tab
    2. In the 'File Name:' field enter the following to perform a regular expression search that finds all the files related to our HelloWorld project
      1. HelloWorld.xml|HelloWorld.class|HelloWorld_HTML.xml
    3. Click the 'Save' button and give this new 'view' a name, ie. HelloWorldView.xml
    4. Click on the "View" tab and locate the view we just created in the drop down list box
    5. Now if you hover over the buttons that are in this pane you should see a "Export view to jar" button. Click this button and create a JAR file from this view
    Once this is completed anyone who wants to deploy your JAR just needs to "import" your JAR into their project and then redeploy their application to their application server. The "Import view jar" button is located right beside the button you used in step 5 above.
Summary

This is by no means meant to be an exhaustive look at portlet development. It should, however, be a pretty good introduction to some of the differences in gadget and portlet development. There are countless articles available on the web that cover portlet development and it would be wise for any developer, after reviewing this article, to look around as they begin their project.


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell