Oracle9iAS Portal Developer Kit
Adding Simple Caching


If you followed previous tutorial articles in this series, you will understand how to write fully-functional portlets using the PDK-Java Framework. Once the basic functionality is complete, you may want to turn your attention to portlet performance.

Caching is a technique used to enhance the performance of Web sites that include a high level of dynamic content. The idea is that the overheads involved in generating and retrieving dynamic content can be minimized by 'proxying' requests for such content through a local agent backed by a large, low-latency data store known as a cache. The cache agent responds to a request in one of two ways:

Web providers are generators of dynamic content, i.e. portlets and they are often remote from the portals they are deployed on, so it is reasonable to assume that caching might improve their performance. The architecture of Oracle9iAS Portal lends itself well to caching, since all rendering requests originate from a single 'page assembling agent' in the portal middle tier - the Parallel Page Engine (PPE). You can make the PPE cache the portlets rendered by your Web provider and re-use the cached copies to handle subsequent requests, minimizing the overhead your Web provider imposes on the page-assembly process.

The PPE can use two different caching 'schemes', each of which is more suited to certain applications. The schemes differ in the method the PPE uses to determine whether a cached copy of a page is valid:

  1. Expiry-based Caching: When a provider receives a render request, it stamps its response with an expiry time. The rendered response is stored in the cache, and is used to handle all subsequent requests for the same content until the expiry time has passed. This caching scheme is perhaps the simplest and most performant, as the test for cache validity has very little overhead and does not involve any network round-trips. It is not suited to all applications however, as it is not always possible to determine how long content will be valid for when it is generated.

  2. Validation-based Caching: When a provider receives a render request, it stamps its response with a version identifier (or E Tag). Again, the response is cached, but before the PPE can reuse the cached response to handle a request for the same content, it has to determine whether the cached version is still valid. It does so by sending the provider a render request which includes the version identifier of the previously-cached version of the response. If the provider decides that this version identifier is still valid, it immediately sends a 'lightweight' response without any content, to indicate to the PPE that it can reuse the cached version. Otherwise, the provider generates new content with a new version identifier, which the PPE will use to replace the previously-cached version. In this way, the PPE always has to 'ping' the provider in order to ensure it has up to date content. This scheme has the advantage that the provider is always in control of which cached content gets reused at any point in time, and does not have to estimate how long generated content will be valid for.

This article demonstrates how to use additional features of the PDK-Java Framework to improve the performance of your Web providers by activating both validation and expiry-based caching in the PPE. We will use the simple provider built in the previous articles to demonstrate both caching schemes. If you are interested in finding out more about how the Parallel Page Engine works, refer to the article Parallel Page Engine Architecture. For more detailed information on the Java classes used in this article, refer to the JavaDoc.

ASSUMPTIONS

  1. You have followed through and understood these articles:

  2. All the assumptions described in the above articles apply here too.

ACTIVATING CACHING

To use the caching features of Oracle9iAS Portal in your Web providers, you must first activate the middle tier cache. The cache is actually known as the PL/SQL Cache because it is the same cache used by MOD PL/SQL, the plug-in to the Oracle HTTP Server that allows database procedures, and hence database providers, to be called over HTTP. This means that a common cache is used for both Web and Database Providers, and that the same principles that apply to controlling caching in Database Providers can also be applied to Web Providers.

The article A Primer on Caching describes how to configure the PL/SQL Cache through a browser. Most importantly, you need to ensure that the:

Usually, your Oracle Portal administrator will take care of these details.

ADDING EXPIRY-BASED CACHING

Expiry-based caching is one of the simplest caching schemes to implement, and can be activated declaratively in your XML provider definition. You can set an expiry time for the output of any ManagedRenderer you utilize by setting its pageExpires property to the number of minutes you want the output to be cached for. For example, suppose we want portlet output in the About render mode to be cached for one minute.

  1. Using your favorite text editor, open the most recent version of your XML provider definition, i.e. the one created in the article Adding Customization.

  2. Set the pageExpires property of the aboutPage element to 1.

    Note: Changes required for provider.xml are shown in bold. The aboutPage declaration must be converted into 'extended form', so we have control over more properties than just its name.

    <?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
    <?providerDefinition version="2.0"?>
    <!DOCTYPE provider [
     <!ENTITY virtualRoot "/MyProvider/">
     <!ENTITY physicalRoot "C:\MyProvider\htdocs\">
    ]>
    <provider class="oracle.portal.provider.v1.http.DefaultProvider">
       <session>true</session>
       <portlet class="oracle.portal.provider.v1.http.DefaultPortlet">
          <id>1</id>
          <name>MyFirstPortlet</name>
          <title>My first portlet</title>
          <description>My first ever portlet, using my own custom renderer, implementing extra render modes!</description>
          <timeout>10</timeout>
          <timeoutMessage>Timed out waiting for MyFirstPortlet</timeoutMessage>
          <hasHelp>true</hasHelp>
          <hasAbout>true</hasAbout>
          <showDetails>true</showDetails>
          <showEdit>true</showEdit>
          <showEditDefault>true</showEditDefault>
          <renderer class="oracle.portal.provider.v1.RenderManager">
             <contentType>text/html</contentType>
             <appPath>&virtualRoot;MyFirstPortlet</appPath>
             <appRoot>&physicalRoot;MyFirstPortlet</appRoot>
             <showPage class="MyCustomRenderer"/>
             <editPage class="MyEditRenderer" />
             <editDefaultsPage class="MyEditRenderer" />
             <helpPage>help.html</helpPage>
             <aboutPage class="oracle.portal.provider.v1.http.JspRenderer">
               <name>about.jsp</name>
               <pageExpires>1</pageExpires>
             </aboutPage>
             <showDetailsPage class="oracle.portal.provider.v1.http.Servlet20Renderer">
                <servletClass>MyShowDetailsServlet</servletClass>
             </showDetailsPage>
          </renderer>
          <personalizationManager class="oracle.portal.provider.v1.FilePersonalizationManager">
             <dataClass>MyPersonalizationObject</dataClass>
             <useHashing>true</useHashing>
          </personalizationManager>
       </portlet>
    </provider>
  3. Save the file using the same filename as before, e.g. C:\MyProvider\provider.xml.

  4. To prove that the About page is cached for one minute, add a line to about.jsp that displays the current time.

    Additions are detailed below in bold.

<%@ page language = "java" import = "oracle.portal.provider.v1.*, oracle.portal.provider.v1.http.*" %>
<%@ page import = "java.util.Date, java.text.DateFormat" %>

<%
    PortletRenderRequest pr =
        (PortletRenderRequest)request.getAttribute(HttpProvider.PORTLET_RENDER_REQUEST);
    String user = pr.getUser().getName();
    DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, pr.getLocale());
    String time = df.format(new Date());
%>

<P>Hi <%= user %> !</P>
<P>This is the <I>about</I> mode of your first portlet.</P>
<P>RenderManager has invoked JspRenderer based on the .jsp extension.</P>
<P><I>This information is correct as of <%=time%>.</I></P>

ADDING VALIDATION-BASED CACHING

Adding validation-based caching requires slightly more effort, but gives you explicit control over exactly which requests to your provider are cache hits. This time, we extend our provider so that its Show page is only regenerated after customizations have been made which alter the way it is displayed. First, we make the portlet keep track of the last time it was customized in each user session. Then, we use this as the version identifier for the portlet's Show mode output for each user's requests.

  1. Using your favorite text editor, open the Java class MyEditRenderer which you created to handle portlet Edit and Edit Defaults modes.

  2. Update the file so that it can keep track of the last time the portlet was customized in the current session.

    The time can be stored inside a Date object attached to the session. In the sample below we have added a static utility method getLastEditTime() that allows access to this object in the session, so it can be retrieved by the Show mode renderer. See the article Adding Session Storage for more information on how to use the session.

    Note: Additions to MyEditRenderer are shown in bold.

    import java.io.*;
    import java.util.Date;
    import oracle.portal.provider.v1.*;
    import oracle.portal.provider.v1.http.BaseManagedRenderer;
    
    public class MyEditRenderer extends BaseManagedRenderer
    {
        public static Date getLastEditTime(PortletRenderRequest pr)
        {
            ProviderSession session = pr.getSession();
            if(session == null)
            {
                return null;
            }
            // Generate name for edit time session value that is specific to this
            // portlet instance
            String lastEditedParam = PortletRendererUtil.portletParameter(pr.getPortletReference(),
                                                                          "lastEditedParam");
            // Make sure no one else tries to create the same object at the same
            // time
            synchronized(session)
            {
                Date lastEdited = (Date)session.getValue(lastEditedParam);
                if(lastEdited == null)
                {
                    lastEdited = new Date();
                    session.putValue(lastEditedParam, lastEdited);
                }
                return lastEdited;
            }
        }
    
        public void renderBody(PortletRenderRequest pr) throws PortletException
        {
            try 
            { 
                // Retrieve the name of the parameter used to represent the user 
                // action taken on the edit form. This name was assigned by 
                // RenderManager but can be overriden by the portlet developer. 
                String actionParam = PortletRendererUtil.getEditFormParameter(pr); 
    
                // Now retrieve the value of that action parameter. 
                String action = pr.getParameter(actionParam); 
    
                // Retrieve the data object for later use. 
                MyPersonalizationObject data = 
                    (MyPersonalizationObject)PortletRendererUtil.getEditData(pr); 
    
                // If the action was non null, then we have to save our newly 
                // customized data. Otherwise, we have to render the edit form. 
                if (action != null) 
                { 
                    // Write the personalized values into the data object. 
                    data.setPortletTitle(pr.getParameter("mfp_title")); 
                    data.putString(MyPersonalizationObject.GREETING,
                                   pr.getParameter(MyPersonalizationObject.GREETING));
    
                    // Submit the data object back to the PersonalizationManager 
                    // for storing. 
                    PortletRendererUtil.submitEditData(pr, data); 
                    // Update the edit time in the session
                    Date lastEdited = getLastEditTime(pr);
                    if(lastEdited != null)
                    {
                        lastEdited.setTime(System.currentTimeMillis());
                    }
                }
                else 
                { 
                    // Get a writer for the edit form.
                    PrintWriter out = pr.getWriter();
    
                    // Render the form.
                    out.println("<TABLE BORDER=0 CELLSPACING=0>");
                    out.println("<TR><TD>Title:</TD>");
                    out.print("<TD><INPUT TYPE=\"text\" NAME=\"mfp_title\" " +
                              "SIZE=\"50\" VALUE=\"" + data.getPortletTitle() +
                              "\" </TD></TR>");
                    out.println("<TR><TD>Greeting:</TD>");
                    out.print("<TD><INPUT TYPE=\"text\" NAME=\"" +
                              MyPersonalizationObject.GREETING + "\"  SIZE=\"50\" "+ 
                              "VALUE=\"");
                    out.print(data.getString(MyPersonalizationObject.GREETING) +
                              "\" </TD></TR>");
                    out.println("</TABLE>");
                }
            }
            catch (IOException ioe)
            {
                throw new PortletException(ioe);
            }
        }
    }
  3. Using your favorite text editor, open the Java class CustomRenderer which you created to render your portlet's output in Show mode.

  4. Update the file so that the last edit time (returned by MyEditRenderer.getLastEditTime()) is used as the version identifier for responses cached by the PPE.

    This time you will override the prepareResponse method which sets the headers on your renderer's response, so that it:

    Note: Additions to CustomRenderer are shown in bold.

    import java.io.*;
    import java.util.Date;
    import java.text.DateFormat;
    import oracle.portal.provider.v1.*;
    import oracle.portal.provider.v1.http.*;
    
    public class MyCustomRenderer extends BaseManagedRenderer
    {
    
        public boolean prepareResponse(PortletRenderRequest pr)
            throws PortletException, AccessControlException
        {
            // First, perform the default behavior of the superclass. If it returns false,
            // then the portlet body is not to be rendered, so there is no need to continue
            if(!super.prepareResponse(pr))
            {
                return false;
            }
    
            // Get the time of the last edit by this user
            Date lastEdited = MyEditRenderer.getLastEditTime(pr);
    
            // If lastEdited is null, it means that we have lost the session, so
            // we can't validate the cached version, and will always have to generate a
            // new response
            if(lastEdited == null)
            {
                return true;
            }
            // If there is a version of this content already cached, then
            // check it is still valid.
            String version = HttpPortletRendererUtil.getCachedVersion(pr);
            if (version != null && Long.parseLong(version) == lastEdited.getTime())
            {
                // Signal to the PPE that it can reuse the existing version
                HttpPortletRendererUtil.useCachedVersion(pr);
                return false;
            }
            // Otherwise, we are going to have to generate a new version of
            // the page, so use the edit time as a version identifier for the new output
            HttpPortletRendererUtil.setCachedVersion(pr,
                                                     String.valueOf(lastEdited.getTime()),
                                                     HttpProvider.USER_LEVEL);
            return true;
        }
    
        public void renderBody(PortletRenderRequest pr) throws PortletException
        {
            // Create a date formatter, and get the time of the last edit and
            // the current time.
            DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, pr.getLocale());
            Date lastEdited = MyEditRenderer.getLastEditTime(pr);
            Date thisTime = new Date();
    
            try
            {
                // Get a writer for this renderer's output
                PrintWriter out = pr.getWriter();
    
                // Generate output and send to the writer
                out.println("<b>Hi " + pr.getUser().getName() + "!</b><br>");
    
                // Retrieve the data object used to store user customizations.
                MyPersonalizationObject data =
                    (MyPersonalizationObject)PortletRendererUtil.getEditData(pr);
    
                // Display the user's personalized greeting.
                out.println(data.getString(MyPersonalizationObject.GREETING));
    
                // Display the time the portlet was last generated
                out.println("<Br><i>This portlet was last generated at " + df.format(thisTime) + ".</i><Br>");
    
                // Display the time the portlet was last edited
                if(lastEdited != null)
                {
                    out.println("<i>This portlet has not been customized in this session since ");
                    out.println(df.format(lastEdited) + ".</i>");
                }
                else
                {
                    out.println("<i>The session has become invalid.");
                    out.println("Please login again to enable validation-based caching.</i>");
                }
            } 
            catch (IOException ioe)
            {
                throw new PortletException(ioe);
            }
        }
    }
  5. Stop the Oracle HTTP Server.

  6. Follow the steps outlined in the article How to Build a Java Portlet to recompile both MyCustomRenderer.java and MyEditRenderer.java.

    Ensure these class files are in the Oracle HTTP Server CLASSPATH, i.e. the file jserv.properties contains a wrapper.classpath entry that specifies the directory containing these files. For example:

    wrapper.classpath=C:\MyProvider\MyClasses
  7. Restart the Oracle HTTP Server.

VIEWING THE PORTLET

To test the new caching features, add your portlet to a Portal page, display that page and then do the following:

  1. Click the About link.

  2. Refresh the page a couple of times and note the time stamp on the message displayed.

    Since the page expiry time was set to 1 minute, the same time stamp should be displayed for the duration of a minute.

  3. Click Close to return to your portlet's Show mode.

  4. Refresh the page a number of times.

    No matter how many times you refresh the page, the same time stamp should be displayed inside the portlet. This is because the cached version of the portlet should remain valid until the portlet is customized.

  5. Click the Customize link.

  6. Enter new values for the Title and Greeting and click OK.

    The portlet should now display your customized greeting and title. The time stamp should now be more recent and remain the same when refreshed, until you customize the portlet again.


Revision History: