0% found this document useful (0 votes)
623 views17 pages

A Well Behaved Portlets

This document summarizes how to create a Jetspeed portlet that functions well with other portlets on the page. It discusses designing the portlet to use a local data model and avoid potential issues like JavaScript errors from functions with duplicate names. The example portlet created is a bookmark/link farm portlet that allows users to maintain lists of URLs with descriptions and images. It follows the MVC architecture and stores data in the portlet's PSML file for persistence between sessions. The portlet provides different security levels to allow editing links versus just formatting changes.

Uploaded by

api-26854087
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
623 views17 pages

A Well Behaved Portlets

This document summarizes how to create a Jetspeed portlet that functions well with other portlets on the page. It discusses designing the portlet to use a local data model and avoid potential issues like JavaScript errors from functions with duplicate names. The example portlet created is a bookmark/link farm portlet that allows users to maintain lists of URLs with descriptions and images. It follows the MVC architecture and stores data in the portlet's PSML file for persistence between sessions. The portlet provides different security levels to allow editing links versus just formatting changes.

Uploaded by

api-26854087
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 17

A well-behaved Jetspeed portlet

Design and construct a Jetspeed portlet that plays well with others

By Bob Fleischman, JavaWorld.com, 11/29/04

Jetspeed 2.0 is scheduled for release in early 2005. However, that hardly means that there is no more
interest in Jetspeed 1.x. Jetspeed, the open source portal venture from the Jakarta Project, has been
around for several years and is in use around the world.

This article presents a working example of how to construct a Jetspeed portlet that runs efficiently,
adheres to the Model 2 architecture, and, by not interfering with additional portlets, plays well with
others. In addition, I demonstrate some simple ways to improve performance and point out the
mistakes that can cost you days of debugging time.

Before I go any further, let's get the prerequisites out of the way. This article's example is based upon
Jetspeed 1.5 and has been tested with a development version of Jetspeed 1.6, which has not been
finalized. Please note: This example is not compliant with Java Specification Request 168. JSR 168
is at the heart of Jetspeed 2.0 and is beyond this article's scope. However, a subsequent article will
demonstrate how to recast this portlet to comply with Jetspeed 2.0.

This article's code is compiled with Java 1.4.2. I run Jetspeed on Tomcat 5.x; although the latest of
version of Tomcat 4.x will probably function just as well. The HTML and JavaScript have been
tested with both Internet Explorer and Firefox. In addition, I assume the reader is familiar with
Jetspeed, has Maven installed, and has tried the simple Jetspeed tutorials.

Before we get started

Writing portlets is almost like writing servlets, and, in fact, can conform to the Model 2 architecture.
However, portlets must function as part of a larger framework, both in terms of operations and the
HTML page; therefore, it is critically important that early in your design, you consider the possibility
that your portlet might collide with other portlets on the page. For example, if two different portlets
both have a JavaScript function called "Submit_Form()", the page will generate a JavaScript error
and neither portlet will function.

An intranet portal, such as Jetspeed, allows multiple instances of the same portlet on a page, with
each assigned a unique portlet ID. Attributes of one instance can be changed without affecting the
siblings, if everything is coded correctly. For example, suppose a portlet allows a user to change the
background color. If that portlet appears three times on a portal page, a change in one of the

Hiren 1
instances' background colors should not affect the other instances—unless, by design, we want them
to change. However, without exercising care in coding the view and the controllers, it is easy to
confuse attributes and thus display incorrect data. Local data needs to be kept local.

This same portlet example exposes another potential problem on the generated HTML page: when
the Change Color button is pressed on the first instance, how do we ensure that the JavaScript and
the action controller for the second instance are not fired?

The opposite of local data is shared data. Imagine a portlet where all instances display the same data
and appear identical—for example, a Telephone List portlet. Changing the help-desk phone number
in one instance of the portlet should be reflected in all instances.

Decide early whether your portlet requires either a local or shared data model. This article's example
portlet uses a local data model. (A follow-up article will demonstrate shared data and database
access.)

Our portlet

For our example portlet, we want to create a better link-farm, or bookmark, portlet. A rudimentary
bookmark portlet ships with Jetspeed. However, according to some discussions in the newsgroups, it
might be removed from the distribution. A link farm contains a list of hypertext links (<A href=>
tags) that jump the user to different Websites.

A bookmark portlet can be used in many ways: as a list of search engines, local restaurants, or, as
developed for a law firm, a list of Websites associated with the California courts. Bringing these sites
together in an easy-to-use format makes for an extremely powerful portlet. Experts in different fields
in your organization can keep the farms fresh by updating the links, while normal users have read-
only access. Though this type of portlet is frequently used on public pages, users can also add a
bookmark portlet to their personal homepages as well.

Our business problem is fairly simple: Design a portlet that allows users to maintain lists of URLs
that are associated with an optional description and image. In addition, only properly authorized
users are allowed to add, edit, delete, and reposition the URLs. The portlet should also support two
levels of security. Users with Level 1 security are allowed to make cosmetic changes, such as the
number of columns of links to display or how much, if any, of the optional text to display. Users with
Level 2 security have the right to edit the underlying data, the links themselves.

In addition, it must be possible to instantiate this portlet several times on the same portal page—this
is not a specific business requirement, but rather a universal Jetspeed design consideration. This
requirement may sound simple, but, as I discuss later, if a portlet's presentation is not properly
designed, it does not play well with others, resulting in JavaScript and HTML errors.

Figure 1 shows a typical Jetspeed page with two instances of the LinkFarm portlet. One instance
houses Search Engine links, while the second contains links to entertainment sites.

Hiren 2
Figure 1. A sample page showing two instances of the LinkFarm portlet. Click on thumbnail to
view full-sized image.

Our portlet adheres to the Model 2 architecture, an implementation of the Model-View-Controller


architecture. MVC not only helps a developer design a portlet in clean, succinct pieces, it also helps
a developer more easily distribute responsibilities and apply modifications later.

In designing a solution to our business problem, we have chosen the path that requires no third-party
software or toolsets, and achieves our goals in the simplest way possible.

One of our business prerequisites requires the links to persist between user sessions. We have chosen
to store the links in the portlet page's PSML (Portlet Structure Markup Language) file (if you are
unfamiliar with PSML see Resources). Storing data on PSML pages has pros and cons:

• Pros: It is simple. It requires no third-party software such as a database engine, and finally, it
requires no knowledge of or reliance upon a directory structure.
• Cons: While the portlet can be moved on the page, it can't be moved to another page. PSML
entries describe the portlets on a particular page. Therefore, creating a stock portlet
prepopulated with links inherited from the LinkFarm portlet and allowing people to add it to
their own page would prove difficult. This problem has two possible solutions: Implement
export/import functionality—that is, teach the portlet how to dump out, or read in, batches of
links. Or move to database- or file-based persistence. However, in this article, we forego such
functionality and stick with the simple, basic approach.

Parts of the puzzle

Our choice of the Model 2 architecture dictates that we break our design into three basic groups: the
models, the views, and the controller. Before we can begin writing code, we must outline the
components we need to build—one of the major commandments of good portlet construction: "Thou
shall design before thou codes."

Model

Hiren 3
For the models, we will need the classes listed below. (The full source code is available in
Resources. However, I do not recommend jumping to the code just yet. I'll go over it in more detail
later.) I've used the class names so you can easily locate the specific code. All models are in the
com.automateddocsys.portal.modules.actions.portlets.linkfarm package.

• LinkFarmLink: This contains the information for a single link. It has the URL, optional URL
to an image, name (or short description), and long description. In addition, it has a position
property—an integer, which is used to order the links for presentation purposes and identify
them for editing. In this model, URL and image link are of the type URI.
• DisplayLink: To work well with the edit view, we create a class where all of the properties
are strings. We use the Builder pattern to create DisplayLink from LinkFarmLinks.
• LinkFarmLinks: This class, which we inherited from ArrayList, has a clearly defined
purpose: it maintains the list of links presented in any given instantiation of our LinkFarm
portlet. In addition, it has some convenience methods. Swap() exchanges two links in the list.
We use this method when the user moves the items on the presentation editing screen. Our
particular implementation relies heavily on the absence of gaps in the link list's position IDs.
The position IDs are required to identify a given link for editing and deleting. The
Renumber() function ensures the links are consecutively numbered.

As described earlier, we decided to persist the LinkFarmLinks to the PSML. PSML files are
referred to as registries with registry entries; reading to and reading from PSML is
accomplished by setting and getting attributes of the PortletConfigState, which I discuss
in more detail when we actually examine the code.

• LinkFarmPSMLPersister: This persistence class contains two methods, getLinks() and


saveLinks(). In the real world, we would implement this class as an interface, thereby
allowing the controller to change to database, file, or Web service persistence by simply
changing the reference to one class.

View

Four views make up the lifecycle of our LinkFarm portlet. We start with the basic view (see Figure
2), which presents the links to the user. This view can be modified to vary the number of columns
and to include the long description and/or the image. (Note: The actual implementations of the image
URLs have been omitted from this example's code because they made it too complex.)

Hiren 4
Figure 2. Jetspeed standard customization view. Click on thumbnail to view full-sized image.

Our second view, the AttributesCustomization view, is provided for free from the Jetspeed
framework and allows users with proper security to edit parameter attributes not marked as "hidden."
(For more on registry attributes, see Resources.) In our example, we expose columns, view, and
editable—see the MyPortlets.xreg file in the source code:

<parameter name="columns" value="4" type="int" hidden="false"


cachedOnName="true" cachedOnValue="true"/>
<parameter name="editable" value="true" type="boolean" hidden="false"
cachedOnName="true" cachedOnValue="true"/>
<parameter name="view" value="condensed" type="style" hidden="false"
cachedOnName="true" cachedOnValue="true">

Actually, our view attribute is a bit more complicated then shown here. We want to use a drop-down
list-box on the Customize form. See the properties file for a complete example of how to set up the
parameters for the drop-down function. A user with proper security can select the small pencil icon
in the portlet's upper right-hand corner to bring up the AttributesConfiguration screen.

The DataConfiguration view is where the actual link data is displayed for editing, see Figure 3.

Hiren 5
Figure 3. Our portlet in configuration mode. Notice the icons for up, down, edit, and delete.
Click on thumbnail to view full-sized image.

In addition to merely displaying the data, this view also contains icons for moving the links up and
down, editing, and deleting. Plus, another button adds new links.

Finally, we have the SingleLink view. Just as it sounds, this view presents and collects data on a
single link, see Figure 4.

Figure 4. Adding and editing information for one link. Click on thumbnail to view full-sized
image.

We could have combined several of these views in the same Velocity file and controlled the output
with various #IF, #ELSE, and #END statements. But doing so is simply not good, clean design and
would have violated another of the basic commandments: "Thou shall keep the simple things simple.
There shall be no more complexity than required."

Controller

Our portlet's controller has a critical job that it implements on class MyLinkFarmAction, which is
inherited from GenericMVCAction. If you are unfamiliar with actions, see Resources. In brief,
actions are classes that fulfill the controller role in the Model 2 architecture. Our simple portlet has a
single controller, which, via a switchboard and some other techniques, selects the proper view and
assembles the models for presentation. A more detailed explanation of this class follows below.

Putting it all together

Hiren 6
Now the fun begins. We know our environment, we've documented our business problem, and we've
outlined our design. It's time to begin construction.

Maven

Maven supports a Jetspeed plug-in for generating the basic framework for creating a new portlet. See
Resources for detailed instructions on setting up and using Maven. This article's source code is based
upon the directory structure and files created by the Maven plug-in. If you want to follow along with
our code, name your instance HomeSpot.

Before we start dissecting the code, let's cover some best practices for constructing portlets.

Logging

Good coding, good code maintenance, and easy debugging all rely on good information. The best
way in a Jetspeed portlet to gather information about what the portlet is doing is to use the logging
systems. Many are tempted to use System.out to send messages to the console. Don't. While that
approach may appear easy, it hogs resources, quickly leads to overly confusing output, and, under
the service-based servlet containers such as Tomcat 5.x, no traditional console exists. All of the
System.out traffic is captured in a file. So, if you have to find and open a file anyway, use a log file.

Using a log file in Jetspeed is easy. Follow these four steps:

1. Create a log4j properties.merge file


2. Import the correct units into your class
3. Instantiate the logger
4. Make log calls

The Jetspeed Maven plug-in merges files in the WEB-INF/conf with a .merge extension. For
example, the file log4j.properties.merge will be combined with the Jetspeed
log4j.properties file in the final configuration.

Here is our log4j.properties.merge file:

#
# Filter OneStop Logging
#
log4j.category.com.automateddocsys = DEBUG, linkfarm, stdout
log4j.additivity.com.automateddocsys = false
#
# linkfarm.log
#
log4j.appender.linkfarm = org.apache.log4j.FileAppender
log4j.appender.linkfarm.file = ${webappRoot}/WEB-INF/log/linkfarm.log
log4j.appender.linkfarm.layout = org.apache.log4j.PatternLayout
log4j.appender.linkfarm.layout.conversionPattern = %d [%t] %-5p (%F:%L) - %m%n

Hiren 7
log4j.appender.linkfarm.append = false

Include the following lines in your action class, or any other class where you need logging:

// The two imports needed


import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;

Then add this code to your class:

Private static final JetspeedLogger logger =


JetspeedLogFactoryService.getLogger("com.automateddocsys");

Insert the name of your class or your company in place of com.automateddocsys. This name should
match the name in your log4j.properties file.

Finally, to place entries in the log, use the following code:

logger.debug("in addOne") or logger.error("Error in adding link");

The JetspeedLogFactoryService, which appears in the code above, is the default implementation
of Jetspeed's logging service. According to the comments in its source code, it "initializes the
underlying logging implementation and acts as a factory for loggers."

Understand the logging levels—error, warn, info, debug—and use them carefully and correctly. The
log4J.properties file specifies what level of logging to commit to the logs and which output
device(s) you want to use. Only logging at or above the level specified will actually be written to the
log files.

Log files are kept in \WEB-INF\logs.

Caching

Jetspeed, Turbine, and the Servlet API offer several options for storing objects in memory for quick
and easy retrieval of instantiated objects. The LinkFarm portlet has simple caching needs: Collect the
links from the PSML page and hold them in the LinkFarmLinks object during the life of the session.
In addition, we must pass information, such as portlet state and error messages, between iterations of
the portlet rendering.

The LinkFarm portlet has one additional twist: multiple instances of the LinkFarm portlet exist, each
with a different list of links. If the list is stored with the name LinkFarmLinks, the various instances
will write over the others' data. When working with portlets, always assume multiple instances of the
portlet with different data can occur, as discussed previously.

Hiren 8
Jetspeed features the PortletSessionState class to store data for the life of the session. This class
offers two methods, setAttibute(Rundata, String Object) and setAttribute(Portlet
[VelocityPortlet], Rundata, String, Object). You must use the latter. Both methods work
similarly; they store the Object data in the User object. The difference is the name associated with
the Object. The second method appends to the name a portlet ID, a unique ID assigned by Jetspeed,
thus guaranteeing uniqueness.

Our models

The classes that make up the model portion in the LinkFarm portlet are Jetspeed-, portlet-, and even
servlet-neutral, as good model classes should be. Models encapsulate data and enforce some data
integrity. The persistence class is the exception to this rule. Since the design persists the local links
data to the PSML file, it becomes necessary to include references to the Portlet, Rundata, and
Context objects.

The links are stored as attributes for an entry in the PSML file. Every attribute can have a name and
a value; the link data is packed into the value using a pipe-delimited format. XML could be used, but
it produces a more complicated solution, and, in this situation, simpler is better.

Here is an example of the links stored in the PSML file:

<parameter name="link0" value="0|Jakarta|http://jakarta.apache.org||"/>


<parameter name="link1" value="1|Jetspeed|http://jakarta.apache.org/portals||"/>

One problem remains: how do we know how many links, and how do we retrieve them when we
could potentially have an unlimited number of links. We solve this issue by adding an attribute called
numberOfLinks and then naming each link according to its number. The following code, clipped
from getAllLinks(), shows how the links are retrieved from the PSML file:

maxLinkNumber = Integer.parseInt(portlet.getAttribute(NUMBER_OF_LINKS, "0",


rundata));
allLinks = new LinkFarmLinks();
// Only process the list if we have any entries
if (maxLinkNumber > 0) {
for (int index = 0; index < maxLinkNumber; index++) {
try {
tempLink = LinkFarmLink.readLink(
portlet.getAttribute("link" + String.valueOf(index), null, rundata));
} catch (NullPointerException e) {
tempLink = null;
}
} catch (Exception e) {
e.printStackTrace();
}
if (tempLink == null) {
try{
tempLink = new LinkFarmLink(
index,

Hiren 9
"MISSING LINK " + String.valueOf(index),
"htp://www.badlink.com",
"",
"MISSING LINK " + String.valueOf(index));
} catch (Exception e) {
e.printStackTrace();
}
}
logger.debug(tempLink.writeLink());
allLinks.add(tempLink);
}
}

Saving the links uses similar code:

// First remove all the existing links


int numberOfLinks = 0;
LinkFarmLink tempLink = null;
logger.debug("In saveAllLinks for " + portlet.getID());
numberOfLinks = Integer.parseInt(portlet.getAttribute(NUMBER_OF_LINKS, "0",
rundata));
for (int i = 1; i <= numberOfLinks; i++) {
portlet.setAttribute("link" + String.valueOf(i), null, rundata);
}
// Now write out the allLinks array
List allLinks = getAllLinks(portlet, context, rundata);
numberOfLinks = allLinks.size();
// Save the number of links
portlet.setAttribute(NUMBER_OF_LINKS, String.valueOf(allLinks.size()), rundata);
for (int i = 0; i < numberOfLinks; i++) {
LinkFarmLink link = (LinkFarmLink) allLinks.get(i);
portlet.setAttribute("link" + String.valueOf(i), link.writeLink(), rundata);
}

Both of these methods are in the LinkFarmPSMLPersister class.

Controller and view

To understand how the views operate, it is helpful to understand the type and nature of the objects in
the context. Therefore, I discuss the controller class before dissecting the views.

A brief word on naming conventions: We preface any parameter passed into a method with the letter
p to better understand what originated from outside sources. The exception to this rule is Rundata,
Context, and Portlet. The Turbine action model requires us to name our action methods
doXyyyzzz(), where X is the only allowable uppercase letter in the name. The remainder of the
name must be in lowercase letters; for example: doUpdateedit() and doUpdateconfig().

Hiren 10
The MyLinkFarmAction class, the controller, handles all requests for the LinkFarm portlet regardless
of the view, with the exception of the Customize view, which Jetspeed handles. There are two
schools of thought concerning how to handle a portlet that has several different views; some argue
for a separate action class for each view, others prefer to lump them together. We choose the latter.
There is enough of an overlap in the code for loading the context, and the simplicity of copying one
class file to add the portlet makes the decision easy.

The portlet's state—which is determined by the controller, in doBuildNormalConfig()—establishes


which view displays. The state is stored in the portlet-specific cache using
PortletSessionState.setAttribute(portlet, rundata, PORTLET_STATE, portletState).

The ability to maintain state for a portlet instance demonstrates why we must establish a clear line
between different instances of the same portlet. For example, setting the state of the restaurants link-
farm to config should not affect the entertainment link-farm. The portlet's state is stored as an
attribute with PortletSessionState.setAttribute(portlet, rundata, PORTLET_STATE,
portletState). The constant PORTLET_STATE is declared at the top of the class.

The portlet state can take on one of four states, which are also defined in constants at the top of the
class:

public static final String PORTLET_STATE_NORMAL = "normal";


public static final String PORTLET_STATE_CONFIG = "config";
public static final String PORTLET_STATE_EDIT = "edit";
public static final String PORTLET_STATE_ADD = "add";

The method doBuildNormalConfig() uses a switchboard and the portlet state to determine which
view to present as shown in the following:

if (portletState.equalsIgnoreCase(PORTLET_STATE_NORMAL)) {
templateName = TEMPLATE_NORMAL;
setTemplate(rundata, TEMPLATE_NORMAL);
addLinksToContext(portlet, context, rundata);
} else
if (portletState.equalsIgnoreCase(PORTLET_STATE_CONFIG)) {
templateName = TEMPLATE_CONFIG;
setTemplate(rundata, TEMPLATE_CONFIG);
// Override the View
context.put(VIEW, "condensed");
addLinksToContext(portlet, context, rundata);
} else
if ((portletState.equalsIgnoreCase(PORTLET_STATE_EDIT))
|| (portletState.equalsIgnoreCase(PORTLET_STATE_ADD))) {
templateName = TEMPLATE_EDIT;
context.put("link", PortletSessionState.getAttribute(portlet, rundata,
PORTLET_MODEL_LINK));
setTemplate(rundata, TEMPLATE_EDIT);
}

Hiren 11
Portlet lifecycle

Let's now take a 20,000-foot view of how our portlet interacts with the world
around it.

Note: In the following text, references to specific sections of code are


indicated by ({code} - #). {code} is a one or two letter code referring to the
following:

1. m - MyLinkFarmAction.java
2. lp - LinkFarmPSMLPersister.java
3. vc - linkFarm1-config.vm

The referenced section of source code, which you will find in Resources is marked with // [#].

Let's assume user Jack Portal has already placed the LinkFarm portlet on his homepage. The first
time the page containing the portlet is compiled, the Jetspeed page renderer invokes the MyLinkFarm
class, which, in turn, invokes the JetspeedLogFactoryService to retrieve the logging class (m-1):

private static final JetspeedLogger logger =


JetspeedLogFactoryService.getLogger("com.automateddocsys");

After invoking the controller class, the buildNormalConfig() method is invoked.


buildNormalConfig() reads the attributes from the portlet registry and puts them onto the context
in addParamsToContext (m-2). The attributes include the number of columns of links to display, the
view style (condensed or expanded), and whether the links are editable (m-3). You will notice a call
to PortletConfigState.getConfigParameter. It is important that you supply a default value in
case the preferred parameter is absent. In addition, the portlet's unique ID is stored on the context
(m-4): context.put(ORIGINAL_PORTLET_ID, portlet.getID());. The view uses this critical ID
to ensure that the HTML entities and JavaScript functions generated by the view do not conflict with
other elements on the page.

Next, the controller looks in the portlet/session-specific cache for the portlet's state (normal, config,
edit, or add). Since this is the first invocation and no state can be found, it is set to normal and then
stored in the portlet/session-specific cache discussed above (m-5):

String portletState = (String) PortletSessionState.getAttribute(portlet,


rundata, PORTLET_STATE);
if (null == portletState) {
portletState = PORTLET_STATE_NORMAL;
PortletSessionState.setAttribute(portlet, rundata, PORTLET_STATE,
portletState);
}

Hiren 12
Because Jack is displaying this portlet for the first time, no errors exist. However, if one of the action
events has passed in an error, it is placed on the context. Errors and other information transfer
between the events and the buildNormalConfig() method via the same portlet/session-specific
cache used for storing the state (m-6):

String errMessage = (String) PortletSessionState.getAttribute(portlet,


rundata, ERR_MESSAGE);
if (null != errMessage) {
// If it exists, put it on the context and then remove it from the
SessionState
context.put(ERR_MESSAGE, errMessage);
PortletSessionState.clearAttribute(portlet, rundata, ERR_MESSAGE);
}

The error must then be removed from the cache or it will appear in every portlet rendering.

Again, because the portlet is being presented for the first time and is the most common presentation,
the view template is set to linkfarm-view (m-7): setTemplate(rundata, TEMPLATE_NORMAL);.
Different Velocity .vm files are chosen with the setTemplate() method.

And finally, the actual links are assembled, via addLinksToContext(), and placed on the context
(m-8):

private void addLinksToContext(Portlet portlet, Context context, RunData


rundata) {
context.put(RETRIEVED_LINKS, LinkFarmPSMLPersister.getAllLinks(portlet,
context, rundata));
}

In addLinksToContext(), the LinkFarmLinks persistence class


(LinkFarmPSMLPersister.getAllLinks(portlet, context, rundata)) is called to retrieve the
list of links (lp-9) from either the portlet/session-specific cache or the PSML file via
PortletConfigState.

Now the portlet displays on the screen together with other portlets. Jack notices the absence of links
and sees just a button that says Edit Links. Jack presses that button, which was created with the
following HTML (taken from the linkfarm-view.vm file):

<form action="$jslink.getPortletById($portlet.getID())" method="post">


<input type="submit" name="eventSubmit_doUpdate" value="Edit Links">

Notice the action URL in the form tag. This is critical to ensuring that the correct firing of methods
takes place, an issue I referred to earlier. Rather than refer to the portlet by name or by PSML page,
the HTML points directly at this instance of the portlet.

Hiren 13
While it is possible to call an action directly, you should be aware that doing so will cause the action
event to fire, but it will lack access to the portlet's instance. Therefore, it has no access to any of the
data stored with the portlet and retrievable from PortletSessionState.

When Jack presses the Edit Links button, an HTTP request is sent to Jetspeed via the Turbine action
methodology, which eventually forwards to the MyLinkFarmAction class, where the doUpdate()
event is called. This simple method merely sets the portlet's state to config (m-10). Note that the
portlet's state is saved using the portlet/session-specific storage PortletSessionState. If we stored
the state in the user's temporary storage, via getUser.setTemp(), which is another way to save data
between rerenderings, we would need to develop a naming convention to prevent a portlet instance
from retrieving another portlet's state. Suppose we used
getUser.setTemp("state",portletState). Using PortletSessionState is critical or all
instances of the portlet would be set to the configuration mode.

Normal rendering then continues, and buildNormalContext() is eventually called. However,


because the portlet state is config, a different template is chosen, and the same data displays with a
different view (m-11)—see Figure 3.

This view, linkfarm1-config.vm, is the portlet's most complicated view and bears a closer look.
First, notice that all of the JavaScript functions and the form elements have $!{js_peid} in front of
their names (vc-12). This ensures the function name or element is unique within the HTML page
namespace. Even with several portlet instances or another portlet with a function or element of the
same name, the uniqueness of your function is preserved. This is especially important when your
portlet will be used with others and you have no idea what functions they will create and call.

Next, three hidden attributes are used to pass back to the controller the action taken
(edit/add/delete/up/down) and the link acted upon (vc-13):

<input type="hidden" id="linkMode" name="linkmode" value="unused">


<input type="hidden" id="linkID" name="linkID" value="-1">
## this is our protection against a refresh causing repeated deletes or adds --
we check the count against the actual count
<input type="hidden" id="linkCount" name="linkcount" value="$links.size()">

Note that the form is submitted via JavaScript rather than with the traditional Submit button, thus
avoiding the problem of a Refresh or Back button firing the URL a second time.

Jack clicks on the Add Link hyperlink, which passes a parameter of add to the submit_this()
function on the page. submit_this() sets the linkMode attribute and submits the page by
programmatically pressing the Submit button named "eventSubmit_doUpdateconfig" which, via
the Turbine action methodology, makes its way to the doUpdateconfig() method in the controller
(m-14).

This method removes the three hidden parameters, linkMode, linkID, and linkCount, from the
request. linkCount is compared with the actual number of links. If the numbers deviate, the portlet
throws an exception. Such a situation might occur if multiple browser windows are open on the same

Hiren 14
page and Jack edits the portlet in more than one place at a time. Similarly, a Refresh or Back button
could cause the same effect. By passing in the count, the method can throw an exception if the
portlet external count, passed in via the view, differs from the internal count maintained by the
model.

Next, a simple switchboard if statement is processed:

//
// Switchboard -- redirect to the proper action
//
if (linkMode.equalsIgnoreCase(ACTION_DELETE)) {
doDeletelink(rundata, context, linkid);
// After we have deleted, there is nothing else to do
} else if (linkMode.equalsIgnoreCase(ACTION_ADD)) {
createLink(rundata, context);
// Now go into Add State
portletState = PORTLET_STATE_ADD;
} else if (linkMode.equalsIgnoreCase(ACTION_EDIT)) {
getLinkToEdit(rundata, context, linkid);
// Now go into Edit State
portletState = PORTLET_STATE_EDIT;
} else if (linkMode.equalsIgnoreCase(ACTION_UP)) {
doUplink(rundata, context, linkid);
} else if (linkMode.equalsIgnoreCase(ACTION_DOWN)) {
doDownlink(rundata, context, linkid);
} else {
logger.error("Bad Link in MyLinkFarmAction doUpdateConfig --
linkMode: " + linkMode);
};

The Add and Edit actions operate almost identically, with the exception that Add generates a new
blank link (m-15) and Edit retrieves a link (m-16). This link is then put on the portlet/session safe
cache to be passed down the line. Both actions also reset the portlet state. Processing then continues
and returns to buildNormalContext().

The other options—delete, up, and down—all act directly on the model and leave the portlet state
alone. Thus, after the action, the screen finishes rendering the portlet, still in the config mode.

Had Jack pressed the Done button, the doCancelconfig() event would have fired and the portlet
state would have been reset to normal:

public void doCancelconfig(RunData rundata, Context context) {


logger.debug("in MyLinkFarmAction DoCancelConfig");
// Return to the Normal state
Portlet portlet = getPortlet(context);
String portletState = PORTLET_STATE_NORMAL;
PortletSessionState.setAttribute(portlet, rundata, PORTLET_STATE,
portletState);
}

Hiren 15
With the Add/Edit state set, the buildNormalContext() retrieves the model link from the cache,
adds it to the context, and changes the template view to linkfarm-edit (m-17):

if ((portletState.equalsIgnoreCase(PORTLET_STATE_EDIT))
|| (portletState.equalsIgnoreCase(PORTLET_STATE_ADD))) {
templateName = TEMPLATE_EDIT;
context.put("link", PortletSessionState.getAttribute(portlet, rundata,
PORTLET_MODEL_LINK));
setTemplate(rundata, TEMPLATE_EDIT);
}

Jack then sees the page displaying the add/edit link as shown in Figure 4. Upon completion of
editing, Jack presses the Update button, and eventually, via the Turbine and Jetspeed action
controller sequence discussed above, the doUpdateEdit() is called. Data from the form is scraped
from the request and used to build a DisplayLink (m-18). Using the Builder pattern avoids directly
accessing the LinkFarmLinks class's properties and is the preferred method of working with
business objects. The DisplayLink—dLink—is then used to either insert or edit a link in the
LinkFarmLinks. In either event, should an exception be thrown, an error message is placed on the
cache, and the erroneous link is replaced so that Jack can have another chance to correct and submit
it.

Assuming that no errors were thrown (m-19), the doCanceledit() method is called—the same
method called by the Cancel button on the portlet in the Edit view mode. This method resets the
portlet state to config and the process continues.

Jack modifies the data until he is pleased with both the content and data placement, at which time, he
presses the Cancel button and returns to the LinkFarm portlet's normal state.

Should Jack desire to change the presentation style, he could press the Customize icon, which
presents the Customize view.

The portlet now displays the standard view, shown in Figure 5:

Figure 5. The portlet in use. Click on thumbnail to view full-sized image.

And that ends our tour.

Hiren 16
What did we learn?

Having worked through the entire process for Jack, several important lessons become apparent.

1. Design before you code.


2. Keep the simple things simple, and introduce no more complexity then is absolutely
necessary.
3. Use the Model 2 architecture and the GenericMVCAction class.
4. Use the portlet ID, passed to your views in the context, to name all of your JavaScript
functions and HTML elements.
5. Use JetspeedLoggingFactory.getLogger, and log your error and debugging messages. Do
not use System.out.
6. Use PortletSessionState.setAttribute(Portlet [VelocityPortlet], Rundata,
String, Object) to store items on a portlet/session-specific memory cache. Do not use
setAttribute(Rundata, String, Object).

Jack is happy; the portlet does exactly what he wants. We're happy because the portlet works well
within the Jetspeed environment and can be easily ported to other clients. Our developers are happy
because the portlet has small, clearly defined parts, and modifying or enhancing the portlet will
prove straightforward.

Speaking of enhancements, as an exercise, look at the code and design methods to export or import
the links.

Hiren 17

You might also like