Oracle9iAS Portal Developer Kit
How to Use the Preference Store API (PDK-Java v2)


There are many reasons why a portlet might need to make use of a persistent store. You may have already met one of these in the article Adding Customization (PDK-Java v2), i.e. to support the Edit and Edit Defaults render modes, a portlet must remember customizations made by each of its users on each of its instances on a Portal page. For this most popular of persistence requirements, the PortletPersonalizationManager class is provided. It is a high-level persistence mechanism, tailor-made to the specific purposes of persisting portlet-instance data.

However, a portlet's persistence needs may not necessarily end at remembering customizations to individual portlet instances. For example, what if a provider was to support a 'switch' that activated a certain feature across all of its portlets? Or what if a portlet was to allow contribution to its content by all of its users? In situations such as these, you can use the PDK-Java's general purpose persistence mechanism, the Preference Store.

In actual fact, the PDK-Java's default PortletPersonalizationManager implementation, PrefStorePersonalizationManager, is built on top of a Preference Store, so you may have been using one all along without knowing it! By accessing the Preference Store API directly, you have at your disposal a lightweight, yet flexible mechanism for persisting, categorizing and managing data associated with almost any object in the PDK-Java v2 Framework.

This article is intended as an introduction to the Preference Store API. It gives an overview of the API and its basic functions, and then follows through the implementation of an example portlet which makes use of the API. For more detailed information on the Java classes used in this article, refer to the Javadoc.

INTRODUCING THE PREFERENCE STORE API

Preference Store Components

The Preference Store API consists of three main components. These are:

The subsections following describe these components in more detail.

The PreferenceStore Class

A preference store is represented by the PreferenceStore class. PDK-Java v2 offers two alternative implementations of this abstract class:

As both classes implement the same generic mechanism, it means that you can code your portlet without imposing any assumptions on the storage medium it will use. Furthermore, you can switch from using one implementation to another without having to alter any code.

A ProviderDefinition can potentially own multiple PreferenceStores, perhaps of different implementations, each of which must be identified by a unique name. One of these is designated the default preference store, and can be accessed through ProviderDefinition.getPreferenceStore(). This is implicitly the first PreferenceStore in the ProviderDefinition, unless it is named explicitly by calling ProviderDefinition.setPrefStoreName() (perhaps indirectly through a <prefStoreName> element in an XML provider definition).

Similarly, each PortletDefinition has a default preference store, which is implicitly the default preference store of its owning ProviderDefinition, or can be be set explicitly through a setPrefStoreName() method.

The PreferenceDataObject Class

A PreferenceStore persists data held in objects which implement the PreferenceDataObject interface. As well as being a 'container' for data, a PreferenceDataObject must be able to read and write its own contents to and from InputStreams and OutputStreams. The default implementation of this interface, NameValuePreferenceDataObject should be flexible enough for most storage purposes, and offers the advantage that it uses a persistence mechanism that is less brittle than native Java serialization.

The Preference Class

Each PreferenceDataObject is stored under a 'key' represented by a Preference. The Preference class contains accessors for the three components of this key, which are outlined below.

  1. Reference path: Categorizes the data according to a context path and preference name.

  2. Preference type: Ensures uniqueness of path naming schemes.

  3. Data class name: Records the class name of the PreferenceDataObject to be stored or retrieved.

The main component of the Preference key is the reference path. A reference path is a string consisting of one or more path components, each delimited by a '/' character. A path component is a string of one or more characters, not including the '/' character. It is illegal for a path component to be equal to the strings "." or "..".

The final path component in a reference path corresponds to the preference name, while the zero or more components that precede it make up its context path. Like a file path, which is composed of a directory and filename, the context path, like a directory, should be used to group or categorize a set of related Preferences together, while the name, like a filename, should uniquely identify the Preference within its context path. For example, a PrefStorePersonalizationManager persists user-specific customizations to portlet instances using Preferences with a reference path of the following form:

<provider id>/<portlet id>/<portlet instance name>/<user name>

To prevent one reference path naming scheme from 'clashing' with another in the preference store, the Preference key contains an additional preference type component, which should group together all reference paths of the same 'type'. A naming scheme similar to the one used for Java package names should be used to ensure uniqueness of preference types. For example, the preference type used by PrefStorePersonalizationManager for user-specific customizations is the following:

oracle.portal.provider.v2.user

The third component of the Preference key is the data class name, which records the class name of the PreferenceDataObject to be stored or retrieved. This component is only mandatory if the Preference is to be read from storage, as it can be automatically determined for data objects provided for writing to storage.

Preference Store Operations

As well as providing methods for storing, retrieving, and destroying data stored under individual Preferences, the PreferenceStore class also contains methods for operating 'collectively' on an entire set of Preferences under given context path. These 'set' operations are also capable of filtering their Preference sets by preference type, thus ensuring that unrelated Preferences do not get included unintentionally.

These methods can be used to powerful effect, since they can be made to operate on different subsets of the same data, simply by varying the length of the context path passed in. To illustrate, consider a set of portlet customizations managed by a PrefStorePersonalizationManager, using reference paths with the form above.

CASE STUDY: THE GUEST BOOK PORTLET

This section follows through the development of a simple portlet that relies on a Preference Store. The portlet is called the "Guest Book Portlet", and allows each user who visits it to add their own comment to a list of comments contributed by other users. The Guest Book Portlet can be added to multiple pages in the same Portal, and each instance of the portlet allows contribution to the same list of comments.

This is clearly not a suitable application for a PortletPersonalizationManager, since contributions made by each user are made visible to all other users, and are not even specific to a single instance of the portlet. In this case, the requirement is to persist data which is specific to a single registered provider instance and portlet definition.

The portlet must be able to perform the following tasks:

  1. Load up the list of comments persisted for a particular registered provider instance.

  2. Render the appropriate comment list in the portlet body.

  3. Persist a new comment each time one is submitted.

  4. Remove the persisted comments when the portlet is deregistered for a particular provider instance.

The subsections following dissect the portlet code to explain how it uses a PreferenceStore to achieve these tasks.

The complete source code for this portlet is included in the PDK-Java v2 distribution. The relevant files are:

The portlet is also included in a pre-configured sample provider within jpdk.ear, with a service ID of urn:prefstore. Consult the installation guide for details on how to register this provider on Oracle Portal.

Loading Guest Book Entries

First, the portlet needs the ability to retrieve its list of persisted guest book entries. Since this list is specific to the portlet, but shared across all of its instances, the PortletDefinition is the obvious object to extend with this capability. The portlet therefore uses its own extension to DefaultPortletDefinition, defined as follows:

public class GuestBookPortletDefinition extends DefaultPortletDefinition

{
    private static final String GUEST_BOOK_PREF_TYPE = "oracle.portal.sample.v2.guestbook";

    private static final List sGuestBookPrefTypes = 

        Collections.singletonList(GUEST_BOOK_PREF_TYPE);

    private Map mGuestBookInstances = new HashMap();
    ...

}

Note that the class defines the following data members:

Next, the class needs a means of generating appropriate reference paths for the Preferences to be stored and retrieved. The Guest Book Portlet uses a separate Preference for each entry it stores, and groups these preferences under a context path which categorizes the data together according to the guest book instance it should be added to. Therefore, all reference paths for the guest book portlet take the following form:

<provider id>/<portlet id>/<sequence number>

where the preference name, <sequence number>, is a generated integer greater than or equal to zero, denoting the entry's position in the list.

As the Preferences in each guest book instance need to be operated on collectively, the context path part of the reference path is the most important part. The class therefore contains a getGuestBookContextPath() method, to generate the context path to the collection of Preferences corresponding to a guest book instance, using the same naming scheme above. The method is listed below.


    protected String getGuestBookContextPath(ProviderInstance pi)

    {

        return pi.getProviderId() + 

            PreferenceStore.PATH_SEPARATOR_CHAR +

            getId();

    }

Given this basic class 'infrastructure', it is now possible to define the method that actually retrieves the guest book entries, getGuestBookEntries(), as listed below.

    public List getGuestBookEntries(ProviderInstance pi)

        throws PortletException

    {

        synchronized (mGuestBookInstances)

        {

            // Load up the entries from the pref store, if they aren't yet in

            // memory

            List guestBookPrefs= (List)mGuestBookInstances.get(pi.getProviderId());

            if(guestBookPrefs == null)

            {

                PreferenceStore prefStore = getPreferenceStore();

                if(prefStore == null)

                {

                    throw new PortletException("PreferenceStore must be defined for this portlet");

                }

                guestBookPrefs = new ArrayList();

                Iterator i;

                try

                {

                    i = prefStore.list(getGuestBookContextPath(pi),

                                       sGuestBookPrefTypes);

                    Preference pref;

                    while(i.hasNext())

                    {

                        pref = (Preference)i.next();

                        pref.setDataClass(NameValuePreferenceDataObject.class);

                        prefStore.read(pref);

                        guestBookPrefs.add(pref);

                    }



                }

                catch (PreferenceStoreException pse)

                {

                    throw new PortletException(pse);

                }



                // Sort the entries in sequential order

                Collections.sort(guestBookPrefs, new Comparator ()

                    {

                        public int compare(Object o1, Object o2)

                        {

                            String name1 = ((Preference)o1).getName();

                            String name2 = ((Preference)o2).getName();

                            return name1.compareTo(name2);

                        }

                    });



                mGuestBookInstances.put(pi.getProviderId(), guestBookPrefs);

            } // end if guestBookPrefs == null

            return guestBookPrefs;

        } // end synchronized

    }

Again, because the portlet is generally interested in the provider instance-specific list of entries in a single 'guest book instance', the method takes as an argument the ProviderInstance whose entries are to be retrieved. Note that because there are potentially multiple threads running through the same GuestBookPortletDefinition, the method has to be synchronized on the shared mGuestBookInstances variable. The method can be outlined as follows:

  1. Check the mGuestBookInstances table for a previously-cached list of guest book entries for the given ProviderInstance. If one is present, then return this immediately.

  2. Otherwise, get hold of the default PreferenceStore for this portlet definition with getPreferenceStore().

  3. Get an Iterator over all the Preferences in the appropriate guest book instance by using getGuestBookContextPath() to generate the context path for the guest book instance and passing this in to PreferenceStore.list().

  4. For each Preference returned by the iterator, call setDataClass() to designate NameValuePreferenceDataObject as the data class and then read() to load up the data object into the Preference. Add each Preference to a list.

  5. Sort the list in order of Preference name (remember that each Preference's name corresponds to its position in the list).

  6. Cache the list in mGuestBookInstances and return it.

Persisting Guest Book Entries

Although the Guest Book Portlet caches each of its guest book instances in memory, it must still write through new entries to the PreferenceStore as they are added, so that none of the portlet data is lost, should the provider be restarted. This is the purpose of the addGuestBookEntry() method of the GuestBookPortletDefinition class, listed below. First, it appends a new Preference to the cached list returned by getGuestBookContextPath(), using the current length of the list to generate a new sequence number. Then it stores the Preference under the appropriate context path in the PreferenceStore. Note that because there are potentially multiple threads accessing the same cached list of preferences, the method is synchronized on the cached list.

    Public void addGuestBookEntry(ProviderInstance pi, NameValuePreferenceDataObject data)

        throws PortletException

    {

            List guestBookPrefs = getGuestBookEntries(pi);            



            PreferenceStore prefStore = getPreferenceStore();

            if(prefStore == null)

            {

                throw new PortletException("PreferenceStore must be defined for this portlet");

            }



            try

            {

                synchronized (guestBookPrefs)

                {

                    Preference pref = new Preference(getGuestBookContextPath(pi),

                                                     String.valueOf(guestBookPrefs.size()),

                                                     GUEST_BOOK_PREF_TYPE,

                                                     data);

                    guestBookPrefs.add(pref);

                    prefStore.write(pref, false);

                }

            }

            catch(PreferenceStoreException pse)

            {

                throw new PortletException(pse);

            }

    }

Rendering the Guest Book

Given that GuestBookPortletDefinition now has the ability to retrieve the list of Preferences corresponding to a 'guest book instance' and add new persisted entries to this list, it should now be easy to make a PortletInstance render a representation of its appropriate guest book, and include the UI to submit new entries. A JSP is used to generate the appropriate markup, and is listed below.

<%@ page import="java.text.DateFormat, java.util.Date, java.util.List,

  java.util.Iterator, oracle.portal.provider.v2.render.PortletRenderRequest,

  oracle.portal.provider.v2.render.PortletRendererUtil,

  oracle.portal.provider.v2.preference.Preference,

  oracle.portal.provider.v2.preference.NameValuePreferenceDataObject,

  oracle.portal.provider.v2.http.HttpCommonConstants,

  oracle.portal.sample.v2.devguide.prefstore.GuestBookPortletDefinition" %>

<%!

private static final String SUBMIT_PARAM = "submit";

private static final String VISITOR_NAME_KEY = "name";

private static final String DATE_KEY = "date";

private static final String COMMENT_KEY = "comment";

%>

<%

PortletRenderRequest prr = (PortletRenderRequest)

    request.getAttribute(HttpCommonConstants.PORTLET_RENDER_REQUEST);



if(prr == null)

{%>

This JSP will not run outside of Oracle Portal.

<%

  return;

}



GuestBookPortletDefinition gbpd = (GuestBookPortletDefinition)prr.getPortletDefinition();



// Handle a new submitted comment

if(prr.getQualifiedParameter(SUBMIT_PARAM) != null)

{

    NameValuePreferenceDataObject data = new NameValuePreferenceDataObject();

    DateFormat df = DateFormat.getDateTimeInstance();

    data.putString(VISITOR_NAME_KEY, prr.getUser().getName());

    data.putString(DATE_KEY, df.format(new Date()));

    data.putString(COMMENT_KEY, prr.getQualifiedParameter(COMMENT_KEY));



    gbpd.addGuestBookEntry(prr.getProviderInstance(), data);

}

%>

<form method="post" action="<%=

   PortletRendererUtil.htmlFormActionLink(prr,

    PortletRendererUtil.PAGE_LINK) %>">



<%= PortletRendererUtil.htmlFormHiddenFields(prr,

        PortletRendererUtil.PAGE_LINK) %>



  <table cellpadding="2" cellspacing="2" border="1">

    <tbody>

      <tr>

        <th valign="Top">User Name<br>

        </th>

        <th valign="Top">Date Visited<Br>

        </th>

        <th valign="Top">Comments<Br>

        </th>

      </tr>

<%

    List entries = gbpd.getGuestBookEntries(prr.getProviderInstance());

    for(Iterator i = entries.iterator(); i.hasNext();)

    {

        Preference pref = (Preference)i.next();

        NameValuePreferenceDataObject data = (NameValuePreferenceDataObject)pref.getData();      

%>

      <tr>

        <td valign="Top"><%=data.getString(VISITOR_NAME_KEY)%><Br>

        </td>

        <td valign="Top"><%=data.getString(DATE_KEY)%><Br>

        </td>

        <td valign="Top"><%=data.getString(COMMENT_KEY)%></td>

      </tr>

<%

    }

%>

      <tr>

        <td colspan="3">Please enter your comment here:<Br>

        <textarea name="<%= PortletRendererUtil.portletParameter(prr, COMMENT_KEY) %>" rows="5" cols="40">No comment.</textarea>

        <input type="submit" name="<%= PortletRendererUtil.portletParameter(prr, SUBMIT_PARAM) %>" value="Send">

        <input type="reset" value="Reset"></td>

      </tr>

    </tbody>

  </table>

</form>

The above JSP can be outlined as follows:

  1. Get the PortletDefinition from the PortletRenderRequest, and cast it into a GuestBookPortletDefinition.

  2. Use PortletRenderRequest.getQualifiedParameter() to check whether the submit button parameter is present in the request.

  3. If so, read the submitted comment out of the request, and add it to data in a NameValuePreferenceDataObject, along with the user name from the request and the current date and time.

  4. Persist the NameValuePreferenceDataObject as a new entry using GuestBookPortletDefinition.addGuestBookEntry().

  5. Generate the markup for an HTML form header. Use PortletRendererUtil.htmlFormActionLink() to generate the appropriate target URL for the portal, and use PortletRendererUtil.htmlFormHiddenFields() to ensure the appropriate 'context parameters' get submitted with the form.

  6. Generate column headings for the guest book entries.

  7. Get the list of guest book entries, using GuestBookPortletDefinition.getGuestBookEntries().

  8. For each Preference in the list, retrieve the data from its NameValuePreferenceDataObject and present it in the HTML table.

  9. Add form fields for submitting new comments, using PortletRendererUtil.portletParameter()to fully-qualify the field names. This ensures that the form parameter names are portlet instance specific, and do not conflict with another instance of the portlet on the same page.

Handling Portlet De-registration

There is one remaining data management task still to be handled by the Guest Book Portlet. This is in the case where the portlet is de-registered from a particular provider instance, and the data for the corresponding 'guest book instance' must be removed.

The portlet de-registration event is handled by the PortletDefinition.deregister() method, and hence the Guest Book Portlet can be made to carry out the task by overriding this method in GuestBookPortletDefinition. The method is listed below. It first calls the super class to carry out the default de-registration behavior, and then uses the PreferenceStore.destroy() method to destroy all the Preferences under the appropriate context path. Finally, it removes the cached version of the appropriate guest book instance from memory. Note again that because there are potentially many threads running though the GuestBookPortletDefinition, access to the shared mGuestBookInstances table must be synchronized.

    Public void deregister(ProviderInstance pi)

        throws IOException, AccessControlException

    {

        super.deregister(pi);

        PreferenceStore prefStore = getPreferenceStore();

        if(prefStore != null)

        {

            synchronized (mGuestBookInstances)

            {

                try

                {

                    prefStore.destroy(getGuestBookContextPath(pi),

                                      sGuestBookPrefTypes);

                }

                catch (PreferenceStoreException pse)

                {

                    throw new IOException(pse.toString());

                }

                mGuestBookInstances.remove(pi.getProviderId());

            }

        }

    }

Putting It All Together

To tell the PDK-Java v2 Framework how it should integrate all the technology outlined above in order to run the Guest Book Portlet, it is necessary to deploy a new instance of the PDK-Java Provider Adapter with an appropriate XML provider definition. An example of such a definition is listed below. Most importantly, it defines a PreferenceStore, specifies that GuestBookPortletDefinition should be used as the portlet definition class, and specifies the context root-relative path to guest_book.jsp in a <showPage> element. Note that this example uses a FilePreferenceStore, since most providers will have a filesystem at their disposal. However, the example could just as easily be made to use DBPreferenceStore, simply by changing the class attribute of the <preferenceStore> element.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<?providerDefinition version="3.1"?>



<provider class="oracle.portal.provider.v2.DefaultProviderDefinition">

   <session>true</session>



   <preferenceStore class="oracle.portal.provider.v2.preference.FilePreferenceStore">

      <name>prefStore1</name>

    </preferenceStore>



   <portlet class="oracle.portal.sample.v2.devguide.prefstore.GuestBookPortletDefinition">

      <id>1</id>

      <name>GuestBook</name>

      <title>Guest Book Portlet</title>

      <shortTitle>Guest Book</shortTitle>

      <description>Demonstration of using a Preference Store to drive portlet content</description>

      <timeout>100</timeout>

      <timeoutMessage>Guest Book Portlet timed out</timeoutMessage>

      <renderer class="oracle.portal.provider.v2.render.RenderManager">

        <showPage>/htdocs/prefstore/guest_book.jsp</showPage>

      </renderer>

   </portlet>

   

</provider>


Revision History: