First, where to put your code? The easiest method is to follow the standard SPP source code layout:
src/core/uk/ac/portal/spp/<name>/
for the core functionality
src/portlets/<name>/
for the related portlet code. Under this should be...
configuration/
for .xreg definition files
java/uk/ac/portal/spp/actions/<name>/
for the actual portlet code
velocity/<name>/
for the portlet velocity files
Write all your functionality and make it available via service facades using the stock startup mechanism. This is important: the job of a portlet should simply be to choreograph the actions of back-end services (which are then testable separately from the UI, with any luck).
Now, put the SPP Portlet boiler-plate code in place:
package uk.ac.portal.spp.actions.@@YOUR-PACKAGE@@;
import org.apache.velocity.context.Context;
import uk.ac.portal.spp.common.Log;
import uk.ac.portal.spp.common.portlet.SPPPortletState;
import uk.ac.portal.spp.common.portlet.SPPRuntimeData;
import uk.ac.portal.spp.common.portlet.SPPPortletConfig;
import uk.ac.portal.spp.glue.portlet.BaseErrorHandlingPortlet;
@@YOUR PACKAGE-SPECIFIC IMPORTS GO HERE@@
public class @@YOUR-PORTLET@@ extends BaseErrorHandlingPortlet
{
}
BaseErrorHandlingPortlet is an implementation of some error-handling methods on top of an implementation of uk.ac.portal.spp.common.portlet.SPPPortlet. Such a base class provides facilities for you to use, as well as having some conventions for you to follow.
From a UI point of view, your portlet might have a number of "views". (These are not _necessarily_ in one-to-one correspondence with velocity templates, but may be.) They represent the current "page" your portlet should display. The current "view" is called the "state switch".
In addition, your portlet has a "session" associated with it, called its "state". This is where you can store and fetch values that will persist between HTTP requests. You can set your portlet up initially by implementing the following method:
public void initialiseState(
SPPPortletConfig conf,
SPPRuntimeData in,
SPPPortletState state)
{
// To get configuration information
String xregConfigParameter = (String)
conf.getAttribute("@@YOUR XREG PARAM NAME@@");
// We may also wish to get details about the user,
// the user profile, etc.
// the "in" parameter supplies these.
state.setAttribute("attname", xregConfigParameter);
setStateSwitch(state, "MainView");
}
To produce a particular view, you have to do three things: write the code to populate a velocity context, add a call to choose the correct template, and write the velocity template itself.
You put this code into a method whose name is derived from the state switch: the following method would be called if the state switch had been set to "MainView" -
public void buildNormalMainView(SPPRuntimeData in,
Context cx,
SPPPortletState state)
{
cx.put("velocity_variable_name",
state.getAttribute("attname");
setTemplate(in, cx, "mypackage/mytemplate");
}
When the portal wants to draw your portlet, it examines the stateswitch, calls the right buildNormalXXX method, which constructs a velocity context (making variables available to the velocity template) and finally chooses which template to draw. Your velocity file itself can display the values of its supplied parameters (see example templates under the alerting portlet).
A web application is also about user interaction. How do user operations get back to the portlet? Here's a sample velocity snippet:
<form action="$spplink.event("RenameWidget")" method="POST">
<p> The widget is called
<input type="hidden" name="id" value="$widget.id" />
<input type="text" name="name" value="$widget.name" />
<a href="$spplink.event("DeleteWidget").add("id",$widget.id)">
[delete this]
</a>
<input type="submit" value="Rename" />
</p>
</form>
which looks like this:
OK, the snippet assumes our buildNormalXXX method has put an object called "widget" into the context, with methods getId() and getName(). The template can trigger two "events", which are the last piece of the lifecycle of a portlet. As you can see, events are generated using the "spplink" object. If the user submits the form, the following method in your code is called:
public void eventRenameWidget(SPPRuntimeData in,
SPPPortletState state)
{
String id = in.get("id");
String name = in.get("name");
/* Rename the widget */
...
setStateSwitch("DisplayWidget");
}
If you click on the delete link, instead the eventDeleteWidget method will be called.
The SPPPortlet supplies a number of convenience methods:
void setTemplate(SPPRuntimeData, Context, String)
we've already seen,
void setStateSwitch(SPPPortletState, String)
we've already seen
String getStateSwitch(SPPPortletState)
returns the current stateswitch
Object getService(SPPRuntimeData in, String name)
This last call can be used to get a facade instance. For example, my missing widget renaming code above might look like this:
/* rename the widget */ WidgetFacade wf = (WidgetFacade) getService(in, "spp.wf"); wf.renameWidget(id, name);
Finally, the error-handling. This may need a little sprucing up, but currently, the BaseErrorHandlingPortlet provides additional methods for getting responses to a user. These methods are as follows:
void addNotice("some notice", SPPPortletState state);
with analogous addNoticeInfo, addNoticeWarn, and addNoticeError.
If you want to utilise these methods, you can cause a list of current pending notices to appear in your velocity template by including the line
#parse("portlets/html/spp/error-handling.vm")
which does the magic.
There is a working example portlet that demonstrates all this: src/portlets/sppexample.
Here, in summary, are the steps you need to take to write an SPP portlet:
The robustness diagrams can be a big help in identifying the views and events.
The basic steps are as follows:
portlets.xreg (the default settings should be fine)spp/localisation/[hub]/portlets/spp/configuration/spp-portlets.xreg file. (See below for how to write a portlet definition).portlets/[portlet]/configuration/*.xreg files. These will also be included into your local spp.xreg file at deploy time. Overwrite them in your localisation directory if need be.ref (and instance) portlets in the .xreg files.In jetspeed portlets are defined within .xreg files. Any file with the extension .xreg in the WEB-INF/conf/ directory will be parsed by jetspeed at startup. Portlets are defined within the <portlet-entry> construction. Here's an example:
<portlet-entry name="AggregatePortlet" hidden="false"
type="abstract" application="false">
<security-ref parent="default"/>
<classname>org.apache.jetspeed.portal.portlets.AggregatePortlet</classname>
<url cachedOnURL="true"/>
</portlet-entry>
Portlets have a unique name, and a classname, which must refer to a java class that implements the org.apache.jetspeed.portal.Portlet interface.
Each portlet definition can be one of three types:
abstractinstanceclassname.ref(definitions lifted from the jetspeed site)
At present we use the Velocity type abstract portlets.
These link a name, and default parameters, to a classname, which means our ref portlet definitions don't need to include the classname parameter.
We define SPP portlets that ref the Velocity portlets, and use the action parameter to point to the SPPPortlet class in question.
See below for an example.
Jetspeed has several .xreg files installed by default:
portlets.xregadmin.xregdemo-portlets.xreg
In addition SPP portlets can be defined in spp/src/portlets/[portlet-name]/configuration/*.xreg files.
These files should just contain the <portlet-entry> XML fragments.
On deployment they are combined into a spp.xreg file and placed in the WEB-INF/conf directory.
Here's an example, an RSS feed. Note how this is a ref type portlet, with a parent Velocity.
The Velocity portlet (abstract) is defined in the portlets.xreg file.
<portlet-entry name="RDN_News" hidden="false" type="ref" parent="Velocity" application="false"> <meta-info> <title>RDN News</title> <description>The latest news at the Resource Discovery Network</description> </meta-info> <parameter name="url" value="http://www.rdn.ac.uk/news/channels/rdnrss.xml"/> <parameter name="template" value="newsfeed/newsfeed.vm" hidden="false"/> <parameter name="action" value="newsfeed.NewsFeedPortlet" hidden="false"/> <media-type ref="html"/> <category>news</category> </portlet-entry>
Notes:
ref the Velocity portlet for now. Our portlet class is referenced by the action parameter<media-type ref="html"/> entry, or they will not be available via the web interface