One of the limitations of the JSR 168 Portlet Specification is not allowing the Portlet to serve any binary content, or content that does not get marked up by the portal. Portlet developers must still develop workarounds to address this need, until the new Portlet specification (JSR 286) is finalized and implemented by different portal vendors. One work around is to use a Servlet to serve the content. This article will demonstrate how to do this.

To begin, we need to develop a Servlet which will serve image files. This is not something you can do with a JSR 168 Portlet and serves as typical example application.


package sample.imageviewer;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


public class ImageServlet extends HttpServlet {

    // HttpServlet is Serializable so, we should have a serialVersionUID
    private static final long serialVersionUID = 1L;

    public void doGet(HttpServletRequest request, HttpServletResponse response)
    {
        try {
            HttpSession session = request.getSession();
            String filename = (String) session.getAttribute("FILENAME");

            if ((filename != null) && (filename.length() > 0)) {
                response.setContentType(getServletContext().getMimeType(filename));
 
                OutputStream os = response.getOutputStream();
                byte b[] = new byte[1024];
                InputStream is = new FileInputStream(filename);
                int numRead = 0;

                while ((numRead=is.read(b)) > 0) {
                    os.write(b, 0, numRead);
                }

                os.flush();
            }
            else {
                System.err.println("ERROR! No filename detected.");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Notice the Servlet gets the name of the file from the Session, however, nowhere does this Servlet put the filename into the Session. This will be done in our Portlet. The following line of code demonstrates the line we will need to add to our Portlet.


    session.setAttribute("FILENAME", "/tmp/dog.gif", PortletSession.APPLICATION_SCOPE);

The third parameter to the setAttribute(...) method is important. This constant indicates this value should be made available to any other Servlet or Portlet in the same web application. If you do not include the third parameter, it will default to using the value PortletSession.PORTLET_SCOPE. Any value put into the session with PortletSession.PORTLET_SCOPE is not available to Servlets or other Portlets.

To be able to use the Image Servlet, we must define a <servlet> and <servlet-mapping> tags in the web.xml file.


<?xml version="1.0" encoding="ISO-8859-1"?> 
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd"> 
<web-app>
    <display-name>Image Viewer Sample Portlet Web Application</display-name> 
    <description>Image Viewer Sample Portlet Web Application</description>

    <servlet>
        <servlet-name>ImageServlet</servlet-name>
        <servlet-class>sample.imageviewer.ImageServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>ImageServlet</servlet-name>
        <url-pattern>/image</url-pattern>
    </servlet-mapping>  
</web-app>

Next let's define a JSP to be the View for our Portlet. This JSP will display a drop down combo box, with a list of image possibilities. The View should also display the currently selected image by creating an <img> tag to the servlet. The currently selected image, the list of images and the path to the Image Servlet should all be values the JSP expects to receive from the Portlet. The JSP will use the JSTL tag library for a clean looking portable JSP.


<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="portlet" uri="http://java.sun.com/portlet" %>

<portlet:defineObjects />

<form method="post" name="<portlet:namespace/>Form1" action="<portlet:actionURL/>">
    <select name="FILENAME" onChange="<portlet:namespace/>Form1.submit()">
        <c:forEach items="${IMAGES}" var="image" varStatus="status">
            <option value="<c:out value="${status.count}"/>"
                <c:if test="${image.absolutePath == FILENAME}">selected="true"</c:if> >
                <c:out value="${image.name}"/>
            </option>
        </c:forEach>
    </select>
</form>
<br/>
<img src="<c:out value="${IMAGE_SERVLET_PATH}"/>"/>

Our Portlet will need a portlet.xml file the defines the Portlet. This portlet.xml file should define two Portlet Preferences. One Portlet Preference should define the PATH to the Image Servlet. Typically we can make a good guess at what the PATH to the Image Servlet will be, however, the administrator could choose to name the web application something other then the name we distribute it under, so it would be safer to make this a configurable value. Additionally, the PATH to the directory that contains images should be specified as a Portlet Preference. This is the only value that MUST be changed per installation in order for this Portlet to function. The value in the example below, "/tmp", is not recommended, but has a slim possibility of working. It's better to set this value to the location of the images directory that ships with this example application (see attachments) or any other directory that contains one or more images.


<?xml version="1.0" encoding="UTF-8"?>
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd"
    version="1.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd
http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd">

    <portlet>
        <description xml:lang="en">Allows a user to view an image.</description>
        <portlet-name>ImageViewerPortlet</portlet-name>
        <display-name xml:lang="en">Image ViewerPortlet</display-name>
        <portlet-class>sample.imageviewer.ImageViewerPortlet</portlet-class>
        <expiration-cache>60</expiration-cache>
        <supports>
            <mime-type>text/html</mime-type>
            <portlet-mode>VIEW</portlet-mode>
        </supports>
        <supported-locale>en</supported-locale>
        <portlet-info>
            <title>Image Viewer Portlet</title>
            <short-title>Image Viewer</short-title>
            <keywords>portlet, sample, image</keywords>
        </portlet-info>
        <portlet-preferences>
            <preference>
                <name>IMAGE_SERVLET_PATH</name>
                <value>/ImageViewer/image</value>
                <read-only>true</read-only>
            </preference>
            <preference>
                <name>IMAGE_DIRECTORY</name>
                <value>/tmp</value>
                <read-only>true</read-only>
            </preference>
        </portlet-preferences>
    </portlet>
</portlet-app>

Lastly, we need the Java code for our Portlet. The following code has been tested on uPortal 2.6.0 GA and Pluto 1.1.4.


package sample.imageviewer;

import java.io.File;
import java.io.IOException;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.GenericPortlet;
import javax.portlet.PortletContext;
import javax.portlet.PortletException;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequestDispatcher;
import javax.portlet.PortletSession;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

public class ImageViewerPortlet extends GenericPortlet {

    public static final String FILENAME = "FILENAME";
    public static final String IMAGES = "IMAGES";
    public static final String IMAGE_SERVLET_PATH = "IMAGE_SERVLET_PATH";
    public static final String IMAGE_DIRECTORY = "IMAGE_DIRECTORY";
    public static final String IMAGES_JSP = "/WEB-INF/jsp/images.jsp";
    
    @Override
    public void processAction(ActionRequest request, ActionResponse response)
        throws PortletException, IOException
    {
        String chosenImageStr = request.getParameter(FILENAME);
        int chosenImage = 0;
        try { chosenImage = Integer.parseInt(chosenImageStr)-1; } 
        catch (NumberFormatException nfe) {
            // This should only happen if a programmer error occurred
            // or someone's trying to hack the site.  Simply default to 0.
            chosenImage = 0;
        }
        PortletSession session = request.getPortletSession();
        File[] images = (File[])session.getAttribute(IMAGES);
        if ((chosenImage >= 0) && (images != null) && (chosenImage < images.length)) {
            session.setAttribute(FILENAME, images[chosenImage].getAbsolutePath(),
                                 PortletSession.APPLICATION_SCOPE);
        }
    }    

    @Override
    public void doView(RenderRequest request, RenderResponse response)
        throws PortletException, IOException
    {
        PortletPreferences prefs = request.getPreferences();
        String servletPath = prefs.getValue(IMAGE_SERVLET_PATH, "/");
        String imagePath = prefs.getValue(IMAGE_DIRECTORY, "/");
        
        PortletSession session = request.getPortletSession();
        String filename = (String) session.getAttribute(FILENAME,PortletSession.APPLICATION_SCOPE);
        File[] images = (File[])session.getAttribute(IMAGES);
        if ((images == null) || (images.length == 0) ||
            (filename == null) || (filename.length() == 0))
        {
            images = getImageList(imagePath);
            session.setAttribute(IMAGES, images);
            if (images.length > 0) {
                filename = images[0].getAbsolutePath();
                session.setAttribute(FILENAME, filename,
                                     PortletSession.APPLICATION_SCOPE);
            }
        }

        PortletContext context = getPortletContext();
        PortletRequestDispatcher prd = context.getRequestDispatcher(IMAGES_JSP);
        request.setAttribute(IMAGES,images);
        request.setAttribute(FILENAME,filename);
        request.setAttribute(IMAGE_SERVLET_PATH,servletPath);
        prd.include(request, response);
    }
    
    private File[] getImageList(String directory) throws IOException {
        File dir = new File(directory);
        File[] imageList = dir.listFiles();
        if (imageList == null) {
            imageList = new File[0];
        }
        return imageList;
    }
}

Before you can start uPortal you must make one final change. In Tomcat each Connector must be modified to support an emptySessionPath. At an implementation detail level, this is the only way Pluto/Tomcat/uPortal allow the PortletSession and Servlet Session to be one in the same. The emptySessionPath setting change will make it so there is only one cookie for the entire Tomcat web container. If you serve multiple applications besides uPortal from your Tomcat instance, this may not be a change you want to make. The following example shows how to change the default port 8080 connector in a standard Tomcat install. You can find this defined in the apache-tomcat-5.5.23/conf/server.xml file.


<Connector port="8080" maxHttpHeaderSize="8192"
           maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
           enableLookups="false" redirectPort="8443" acceptCount="100"
           emptySessionPath="true"
           connectionTimeout="20000" disableUploadTimeout="true" />

If you put all of these pieces together, deploy and publish the portlet in your portal, you should see something similar to this.

Image Viewer Portlet Screenshot

In a future article, we will address how JSR 286 will allow this application to get a lot simpler.

---- Cris J H

AttachmentSize
ImageViewer.zip1.21 MB