级别: 初级 Tim Hanis (hanistt@us.ibm.com), 高级软件工程师, IBM
2004 年 3 月 01 日 本文是为 IBM WebSphere Portal Version 5 修订而成的,文中描述了状态转移本文描述了状态转移模式,您可以把它应用到用 WebSphere Portal V5 Portlet API(也称作 Portlet API 1.2)开发的 Portlet 中。
本文是一个由两部分组成的文章序列中的第二部分,描述了如何使用在第一部分中引入的状态模式来实现Portlet。
引言
本文取代了具有相似名称的一篇文章。本文更新的目的是为了与 IBM WebSphere Portal V5.0 Portlet API 保持一致,并删除对已弃用的方法和类的引用。具体说来,就是修改了模式实现,以删除对已弃用的类
org.apache.jetspeed.portlet.DefaultPortletAction
和这些如下已弃用的方法的引用:
-
org.apache.jetspeed.portlet.PortletURI.addAction(PortletAction)
-
org.apache.jetspeed.portlet.event.ActionEvent.getAction()
同时更新了示例实现来使用 WebSphere Portal V5 开发工具,它现在是一个 J2EE
TM1.3 应用程序。
第 1 部分概述了使用状态模式来开发用于 IBM® WebSphere® Portal 的 portlet。第2部分阐述使用状态模式的示例 portlet 的一种特定的实现。这个 portlet 是为用户维护一串联系人(比如地址本)的一个简单的 portlet。作为构想出来的例子,它实现了一个很通用的应用模式。它提供了一个主视图,有时称为“搜索和浏览”,收集了各条目摘要,以便用户选择其中一项并检查所选项的细节。标准的 CRUD(创建、恢复、更新和删除)操作都是可用的。联系人列表保存在数据库中。
在本文中您会看到如何实现功能齐全的联系人列表 Portlet,特别着重于状态模式实现。本文还简要提及了其他的 portlet 要求(比如数据库访问、异常处理及配置参数)。您也会了解到如何使用 IBM WebSphere Studio(以下称为 Studio)来创建 Portlet;然而,不使用 Studio,您也可以创建 portlet。
创建样本 portlet 需要用到下列产品:
在
下载中提供了这个样本的完整实现。
这里讨论的状态模式设计为 Portlet 应用程序设计提供了一个很好的方法,它有助于组织页面转换,以及在模型-视图-控制器(Model-View-Controller,MVC)设计模式下实现职责的分离。如果您选择使用 IBM Portal API 来编写它们的 Portlet,您就需要用到这样的方法。
本文描述了一种模式实现。推荐使用 Struts 作为 Portlet 开发的框架解决方案。Jakara Struts 项目(一个 Apache SoftwareFoundation 开放源代码项目)提供了一个框架来解决 MVC 设计和页面控制流的这些相同的问题。它也提供对异常处理、错误处理、输入校验、表单和国际化的支持。您可以使用 Struts 框架来编写应用程序并将其作为 Portlet 部署。要了解相应的使用 Struts 实现的样本程序,请参阅
开发和部署作为 WebSphere Portal V5 Portlet 的 Struts 应用程序。
状态模式快速回顾
StateManagerPortlet 是主要的 portlet 类,并且是 Portlet 无关的。它用作分派器以支持驻留着 Portlet 代码的操作和状态类,特定于 Portlet 的控制器代码就存在于该类中。扩展
Action
抽象类的类实现了
actionPerformed
方法,该类执行实现特定的操作请求行为所必需的任何控制器功能。
实现状态接口的类将实现一个
performView 方法,该方法是由 StateManagerPortlet 的
service 方法调用的。该方法还包含通常驻留在这些方法中的代码。同样,这段代码也是特定于它所驻留的状态类的,因此就避免了确定请求的操作带来的所有额外的控制逻辑混乱。
应用状态模式会使实现变得简洁。而且,当您在 portlet 的各个模式之间切换时,状态总是被记住的。有了这种模式,您就可以轻松地确定您想要何时以及在何处从源检索数据,还可以确定何时应该从高速缓存检索数据。由于在门户页面刷新时不调用 actionPerformed 方法,所以您可以将数据访问代码放在操作状态中并在那里高速缓存它们。状态类可以使用高速缓存中的数据以避免刷新门户页面时多次存取数据。
建立数据库
首先,创建一个数据表来保存联系人列表数据。要创建该表,需要从 DB2 命令行运行下列 SQL 命令。您可以用您选择的名字来替换 Schema名称,但表和列的名字是固定的。您可以为该表创建一个新的数据库(数据空间),也可以把它添加到现有的数据库中。如果您使用 DB2,则在命令中心(Command Center)建立一个到适当数据库的连接,再复制下列命令,然后运行它。
CREATE TABLE "DB2ADMIN"."CONTACTS" ("OWNER" VARCHAR(64) NOT NULL,
"OID" VARCHAR(64) NOT NULL PRIMARY KEY, "FIRST_NAME" VARCHAR(32)
NOT NULL, "LAST_NAME" VARCHAR(32) NOT NULL , "EMAIL" VARCHAR(128)
NOT NULL, "TITLE" VARCHAR(64), "COMPANY" VARCHAR(64), "BUS_PHONE"
VARCHAR(32), "MOBILE_PHONE" VARCHAR(32), "FAX_PHONE" VARCHAR(32),
"WEB" VARCHAR(128), "ADDRESS1" VARCHAR(32), "ADDRESS2" VARCHAR(32),
"CITY" VARCHAR(32), "STATE" VARCHAR(2), "ZIP" VARCHAR(10), "COUNTRY"
VARCHAR(32) )
|
另外,您可以选择把一个条目添加到联系人列表数据库表中,这样您就可以在开发 Portlet 时更容易地对其进行测试。您可以执行下列 SQL 命令来创建一个新的条目。如果您出于测试的目的想用不同于
wpsadmin 的 userID 登录到 Portal,就请用这个用户 ID 来代替下面 SQL语句中的
wpsadmin 。
insert into contacts (owner, oid, first_name, last_name, email,
company, mobile_phone) values ('wpsadmin', '01', 'Tim', 'Hanis',
'hanistt@us.ibm.com', 'IBM', '919-555-9480')
|
接下来,在 WebSphere Application Server 中为您创建的联系人表所在的数据库创建数据源。这个 Portlet 是作为 J2EE 1.3 应用程序打包的,因此您需要指定 Application Server V5 数据源。J2EE Level 1.3 包括 Servlet Specification Level 2.3 和 JSP Specification Level 1.2。
- 在 Application Server 中使用
DB2 Legacy CLI-based Type 2 JDBCDriverJDBC 驱动程序并指定
Data Sources而不是
Data Source (Version 4)。
- 在 Application Server Administrative Console 上,选择
Resources =>JDBC Providers。
- 如果您已经安装了
DB2 Legacy CLI-based Type 2 JDBC Driver,就选择它。否则,就通过选择
New命令并完成下一个页面来添加它。
- 在
DB2 Legacy CLI-based Type 2 JDBC Driver页面上,确保把类路径设置为
db2java.zip 文件的正确位置。
- 选择
Data Sources,接下来选取
New以创建一个新的 V5数据源。指定数据源的名称。对于 JNDI 名,用
jdbc/ 子上下文作为名称的前缀。
例如,如果您想要一个名为
aim
的数据源 jndi,就可以输入
jdbc/aim 。Portlet在进行 JNDI 名称查找时会预先考虑
jdbc/ 子上下文。
- 如果您的数据库需要用户 id 和密码用于身份验证,就请在
Component-managedAuthentication Alias中指定一个身份验证别名。
- 如果您还没有定义一个别名,就可以在
Security => JAAS Configuration => J2CAuthentication Data中创建一个。
- 务必保存配置的更改,并测试数据源连接。
|
图 1. WebSphere Application Server Administrative Console
建立开发环境
如果您使用 WebSphere Studio 来创建这个 portlet,请确保下列 JAR 文件中构建路径中是可用的,以便 portlet 代码顺利编译。如果您在 Studio 中使用 Portal Toolkit 并创建 Portlet 应用项目,那么类路径会自动创建。不管哪种情况,您都可以创建一个 Web 项目或 Portlet 应用项目,然后导入
下载
中的 WAR文件,从而将这个 Portlet 应用程序载入 Studio。
如果您没有使用 Portal Toolkit 或者 Studio,您可以在
<WAS_ROOT>/lib 和
<WPS_ROOT>/shared/app 中找到这些文件。
之所以建立阶段需要程序调试时间,只是因为当 portlet 部署到 WebSphere Portal 环境中时所有这些 JAR文件都必须是可用的。您可以不作修改而在 Portal 中安装 Portlet war 文件。
-
SERVERJDK_50_PLUGINDIR/jre/lib/rt.jar
-
WAS_50_PLUGINDIR/lib/dynacache.jar
-
WAS_50_PLUGINDIR/lib/j2ee.jar
-
WAS_50_PLUGINDIR/lib/servletevent.jar
-
WAS_50_PLUGINDIR/lib/ivjejb35.jar
-
WAS_50_PLUGINDIR/lib/runtime.jar
-
WAS_50_PLUGINDIR/lib/ras.jar
-
WAS_50_PLUGINDIR/lib/naming.jar
-
WAS_50_PLUGINDIR/lib/utils.jar
-
WPS_V5_PLUGINDIR/portlet-api.jar
-
WPS_V5_PLUGINDIR/wpsportlets.jar
-
WPS_V5_PLUGINDIR/wps.jar
联系人列表 portlet
示例 Portlet, ContactsList,维护一个联系人列表,比如简单的地址簿。在编辑模式下,用户可以浏览联系人列表,查看一个选定的联系人的详细信息,也可以创建、删除或修改一个联系人。在配置模式下,用户可以设置数据库访问信息。虽然可用的数据源提供了包括配置模式的完整的实现,但为了设计的目的,您只需注意查看和编辑模式的实现。
从可视角度看,联系人列表 Portlet 的实现包括如下视图(页面):
-
主视图(Main view)--显示联系人列表,并带有选项来选择联系人以查看更多的信息。
-
详细信息视图(Detail view)-- 显示选定的联系人的详细信息。
-
主编辑视图(Main edit view)--显示联系人列表,并带有选项来添加、删除和修改联系人信息。
-
添加联系人视图(Add contact view)-- 显示表单视图来输入新联系人的信息。
-
修改联系人视图(Modify contact view)--显示相似的表单视图来添加现有的联系人的元素数据以供修改。
为了利用 Portlet 里的授权列表控制,要确保摘要列表和详细信息视图对用户可用并且允许Portlet 查看,以及添加、编辑和删除功能对用户可用并且允许编辑。为了使这些授权可用,摘要列表和详细信息视图页面就应由Portlet 视图模式控制。其他的页面在编辑模式下可用。
考虑构成这个Portlet 的状态和操作。将
动作看作是您要为 Portlet实现的用户功能或行为。例如您需要一个操作来添加一个联系人或者显示一个特殊的页面。将
状态看作是用户操作完成之后Portlet的情况;状态也可以表征可视化组件。例如,添加一个新联系人的操作会带来的状态是Portlet 处于编辑模式,显示联系人列表。
现在,假设您想让 portlet更加可视化。在本例中,主视图页面列出用户创建的联系人。该页面允许用户选择这些联系人中的一个来查看其附加的详细信息。
图 2. 主列表视图
接下来,看一看 portlet 如何让用户从这一列表中修改、添加以及删除联系人。主编辑页面列出联系人,让用户选择其一进行修改、添加或删除。
为了修改联系人,用户需要查看一个详细页面,在这里面他或她能够更新该联系人条目现有的任何属性。同样地,用户可以添加一个新的联系人。有一详细页面让用户输入并保存新条目的属性。保存以后,用户返回主页面。Portlet并没有实现另一视图来用于删除;替代方法是用户简单地选择一个条目并把它删除掉。删除所选条目后,用户看到同样的主页面,而联系人列表已做恰当的更新了。
联系人列表动作
因此,您将有下面这些操作:
- 显示所有联系人的主列表视图
- 显示选定的联系人的详细情况视图
- 显示联系人的编辑视图列表
- 显示视图来添加联系人
- 添加联系人
- 显示视图来修改联系人
- 修改联系人
- 删除联系人
联系人列表状态
执行这些操作会产生下列状态之一:
- 主视图
- 详细信息视图
- 主编辑视图
- 添加联系人视图
- 修改联系人视图
现在,您可以确定此应用程序的状态过渡。这些过渡是通过将适当的操作应用于当前状态实现的,这就导致应用程序要进入另一个特定的状态。对portlet有了这一层理解后,您就可以用状态模式创建应用程序了。
图3. 此应用程序的状态过渡
现在考虑通过状态模式实现的所有类是如何在一起运行的。首先,考察主要的 portlet 类,
StateManagerPortlet, 它将在下面显示。您可以看到,它由两个基本的方法来完成大部分的工作。在本示例中,这一工作包括调用恰当的操作类或状态类。操作类是由
actionPerformed 方法调用的,所以它们是作为用户操作来调用的。状态类
performView method 是在 portlet 类
service 方法得到调用时调用的。
记住,这些逻辑流是开始于用户界面(UI)的用户操作。比如,用户单击主列表视图特定的联系人来获得所选条目的详细信息。逻辑流是如何从此处开始的呢?
UI的定义如下:用户操作(单击一个联系方式条目)有一个链接,该链接是通过一个锚标记上相关联的HREF 或 form 标记上的一个 action定义的。无论这是以何种方式实现的,单击操作都会调用一个对URI 的 HTTP 请求。当开发人员为主视图的 UI 构建 JSP时,他或她将链接与一个 PortletURI 相关联,并通过标准的addAction API,将一个实现用户操作行为的具体操作类添加到PortletURI。在这个示例中,开发人员创建了一个名为DetailActionView的操作类,它负责显示所选择的联系人的详细信息视图。这个类必须和 PortletURI 相关联。我们使用 portlet taglib来实现这一点,方式是将操作类的名称添加到 PortletURI。之后,我们使用
StateManagerPortlet 类来核对这一名称,得到这个类的一个实例,并调用它的
actionPerformed 方法。
下面代码是 StateManagerPortlet类的基本功能的实现。它是一个简洁的实现且非常普通。下面的代码还说明了这个类的
service 方法,很快我们将对其进行简要的讨论。
清单 1. StateManagerPortlet 类
public class StateManagerPortlet extends AbstractPortlet
implements ActionListener {
public void service (PortletRequest request, PortletResponse response)
throws PortletException, IOException {
// Get the portlet state object from session, if not there get the
// initial state for the mode.
State nextState = (State)
session.getAttribute(request.getMode().toString());
if (nextState == null)
nextState = InitialStateManager.getInitialState(request.getMode());
// Dispatch to state
nextState.performView(request, response,
getPortletConfig().getContext());
}
public void actionPerformed(ActionEvent event) throws PortletException {
// Get the action handler class and dispatch
String actionClassName = event.getActionString();
Action action = ActionClassManager.getAction(actionClassName);
// Dispatch to that class event handler
action.actionPerformed(request, portletContext);
action.setState(request);
}
}
|
ActionClassManager负责返回一个操作子类实例,该实例给出一个类标识符,这个标识符是通过 addAction(字符串)方法添加到 PortletURI中的。我们遵循如下约定:使类标识符为全限定类名,这样我们就能够很容易地创建一个类的实例。然而,我们可以通过单独处理操作子类实例来避免不必要的对象的创建。然后我们需要 ActionClassManager来为所引用的操作子类返回单一的实例。
清单 2. ActionClassManager 类
public class ActionClassManager {
// Map of action class instances
static HashMap actionInstances = new HashMap();
/**
* Return the Action class instance for the named class.
* @param The action class name
* @return The Action instance
* @exception AIMException - Wraps ClassNotFoundException,
* IllegalAccessException, or InstantiationException
*/
public static Action getAction(String actionClassName)
throws AIMException {
Action action = null;
try {
action = (Action)actionInstances.get(actionClassName);
if (action == null) {
action = (Action) Class.forName(actionClassName).newInstance();
actionInstances.put(actionClassName, action);
}
} catch (ClassNotFoundException e) {
throw new AIMWrapperException(e.getLocalizedMessage(), e);
} catch (IllegalAccessException e) {
throw new AIMWrapperException(e.getLocalizedMessage(), e);
} catch (InstantiationException e) {
throw new AIMWrapperException(e.getLocalizedMessage(), e);
}
return action;
}
}
|
抽象的操作类定义了两个抽象的方法,因此它的子类必须实现
actionPerformed 方法和 setState 方法。当 StateManagerPortlet 类从它的
actionPerformed 方法分派处理时,操作子类的
actionPerformed 方法得到调用。
setState 方法也可由 StateManagerPortlet调用,确保操作子类的实现在该用户请求的操作处理完成之后设置下一个状态。操作类实现它自己的setState 方法,子类必须调用该方法来真正设置下一个状态。这确保了操作类和状态类知道在哪类设置和恢复下一个状态,而不需要子类知道这些机制。这些状态跨 HTTP请求,并维持在每个 portlet模式下。所以,当用户改变模式时,控制返回到最近一次访问的状态。
清单3. 操作类
public abstract class Action implements PortletAction {
public abstract void actionPerformed(PortletRequest request,
PortletContext portletContext) throws AIMException;
public abstract void setState(PortletRequest request)
throws AIMException;
public void setState(PortletRequest request, State nextState)
throws AIMException {
PortletSession session = request.getPortletSession();
session.setAttribute(request.getMode().toString(), nextState);
}
}
|
我们的 MainViewAction 类并不需要任何特定的操作处理。事实上,我们知道我们需要将联系人的列表显示在主视图上。我们能够实现在
actionPerformed 方法中创建恰当的联系人列表 Bean 的代码,将该代码设置在会话或请求对象里,然后将下一个状态设置到我们的 MainViewState 里。这个类的
performView implementation 能够简单地调用 JSP,并提交来自会话或请求的 Bean 里的列表内容。然而,我们必须注意门户页面更新,以防更新时调用的是 portlet 的 MainViewState
performView method,而非我们的 MainViewAction
actionPerformed 方法。所以,我们不能只是将 Bean 放置于请求对象里,而没有让 performView 方法重新创建该 Bean 并将新的 Bean 放在该请求对象里。我们可以在会话里再次使用该 Bean。但如果那样做的话,我们的数据将会过时。如果我们在编辑模式下做了变动,我们就需要通知该变化,并在 State 方法里刷新我们的数据 并ean。为了简化这种处理,我们会只为 MainViewState 在 State 方法里得到数据 Bean。
所以,在 MainViewState 类里,我们的
actionPerformed 方法并没有另外处理。那个类只是简单设置了 MainViewState 而已。相关的代码如下所示。
清单 4. MainViewAction 类
public class MainViewAction extends Action {
/**
* Perform action request
*/
public void actionPerformed(PortletRequest request,
PortletContext portletContext) throws AIMException {
}
/**
* Set the next Portlet state
*/
public void setState(PortletRequest request)
throws AIMException {
setState(request, new MainViewState());
}
}
|
当
actionPerformed 完成处理时,控制返回到门户容器以便其他的侦听器通知处理程序执行。然后,门户容器通过调用 StateManagerPortlet 的
service 方法继续 Portlet 请求处理。
StateManagerPortlet 的
service 方法首先试图检索以前执行的操作类设置的状态类。如果没有发现状态类引用(例如,最初的 Portlet 调用),那么就使用一个助手类 InitialStateManager 来为每个 Portlet 模式确定初始的状态类。InitialStateManager 有一个惟一的方法叫做 getInitialState,它根据 Portlet 的当前模式返回状态对象的一个实例。对于我们的联系人列表 Portlet,这个类将为视图模式返回 MainViewState 的一个实例。
State 接口有一个惟一的方法
performView method,所有的状态类都必须实现它。StateManagerPortlet 的 service 方法调用这个方法。
清单 5. InitialStateManager 类
public class InitialStateManager {
public static State getInitialState(Portlet.Mode mode)
throws AIMException {
// Return the initial VIEW State
if (mode == Portlet.Mode.VIEW)
return new MainViewState();
// Return the initial EDIT State
if (mode == Portlet.Mode.EDIT)
return new MainEditState();
// Return the initial HELP State
if (mode == Portlet.Mode.HELP)
return null;
// Return the initial CONFIGURE State
if (mode == Portlet.Mode.CONFIGURE)
return new ConfigureState();
String msg = ResourceBundle.getBundle("nls.polling").
getString("exception.initial");
throw new AIMException (msg);
}
}
|
然后,我们的 MainViewState 的
performView 方法负责在 Contacts Bean 的列表的表单中获取联系人列表。联系人列表保存在数据库里,然后传递到 JSP 以便在主视图中呈现出来。
清单 6. MainViewState 类
public class MainViewState implements State {
// Static fields
protected static MainViewState stateInstance = null;
/**
* Contact list view
*/
public void performView(PortletRequest request,
PortletResponse response,
PortletContext portletContext)
throws IOException, PortletException, AIMException {
// Get the logged in user
String user = request.getUser().getUserID();
// Get the ContactList data, put it on the request object
String datasource = portletSettings.getAttribute(DATASOURCE);
ContactListBroker broker = new ContactListBroker(datasource);
List contactList = broker.getContactList(user);
request.setAttribute(CONTACT_LIST, contactList);
// Invoke the JSP to render
portletContext.include(VIEW_LIST_JSP, request, response);
}
/**
* Return the singleton for this class
*/
public static State getInstance() {
if (stateInstance == null)
stateInstance = new MainViewState();
return stateInstance;
}
}
|
下面展示了用来呈现主列表视图的 JSP 代码。在这里,请注意锚标记上的 HREF。这个锚标签允许用户单击一个联系人条目,然后显示该联系人的详细信息视图。
用于这个标记的 HREF 使用来自 Portlet 标记库中的 createURI 标记。该标记在 URIAction 中获得参数,而 URIAction 被我们设置为操作类中用于用户单击操作的事件处理的方法的名称。
清单 7. 查看 JSP 的主要清单
<%@ page contentType="text/html"%>
<%@ page import="com.ibm.sample.contacts.beans.Contact" %>
<%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletAPI" %>
<portletAPI:init />
<jsp:useBean id="contactList" class="java.util.List" scope="request" />
<%
String WHITE = "#ffffff";
String GRAY = "#efefef";
String bgColorStr = WHITE;
String detailViewAction =
"com.ibm.sample.contacts.actions.DetailViewAction";
%>
<form method="POST" name="<portletAPI:encodeNamespace value="form"/>">
<input type="hidden" name="selectedContact">
<table border="0" width="85%" align="center"
cellspacing="0" cellpadding="0">
<thead>
<tr><td><br></td></tr>
<tr>
<td class="wpsTableHead">Name</td>
<td class="wpsTableHead">Company</td>
<td class="wpsTableHead">Email</td>
<td class="wpsTableHead">Phone</td>
</tr>
</thead>
<tbody>
<%
for (int _ind=0; _ind<contactList.size(); _ind++ ) {
Contact contact = (Contact)contactList.get(_ind);
// alternate between white and gray rows in table
if ( (_ind & 0x1) == 0x1 ) {bgColorStr = GRAY; }
else {bgColorStr = WHITE; }
%>
<tr>
<td class="wpsPortletSmText" valign="top" align="left"
bgcolor="<%= bgColorStr %>">
<a style="text-decoration: none; color:black;"
onMouseOver="this.style.color='red'"
onMouseOut="this.style.color='#000000'"
href="javascript:document.<portletAPI:encodeNamespace
value="form"/>.action=
'<portletAPI:createURI>
<portletAPI:URIAction name='<%=detailViewAction%>'/>
</portletAPI:createURI>';
document.<portletAPI:encodeNamespace
value="form"/>.selectedContact.value='<%=contact.getOid()%>';
document.<portletAPI:encodeNamespace
value="form"/>.submit()">
<%= contact.getFirstName() %>
<%= contact.getLastName() %>
</a>
</td>
<td class="wpsPortletSmText" valign="top" align="left"
bgcolor="<%= bgColorStr %>">
<%= contact.getCompany() %>
</td>
<td class="wpsPortletSmText" valign="top" align="left"
bgcolor="<%= bgColorStr %>">
<%= contact.getEmail() %>
</td>
<td class="wpsPortletSmText" valign="top" align="lef
t"
bgcolor="<%= bgColorStr %>">
<%= contact.getMobilePhone() %>
</td>
</tr>
<%
}
%>
</tbody>
</table>
</form>
|
图 4. 主列表视图
用户可以从这个主视图页面选择特定的联系人条目来获得详细信息。我们还添加了一个锚标记来将联系人条目的名(first name)和姓(last name)作为可点击的链接显示出来。当单击时,StateManagerPortlet 的
actionPerformed 方法被名为 Action 名称参数的 DetailViewAction 类调用。
actionPerformed 方法得到所选择的联系人条目的对象 id,并调用持久性类的代理来为该 id 获取实例化的联系人对象。这个联系人对象放在 State 类的会话中,用来呈现详细信息视图。如果该页面由于门户页面刷新而得以刷新,那么该 Bean 就将在会话中可用,既然该对象只会通过用户使用 Portlet 来获得更新,我们就不必担心数据过时了。
门户页面刷新的另一个关键之处是表单数据没有再次粘贴。这样,如果我们试图获得所选联系人的对象 id 作为我们的状态类的请求参数,它在门户页面刷新时将不可用。因此,为了使 Portlet 能正常运行,必须在刚开始时就检索一下数据元素,然后将其存储在某个地方,这样在随后的页面刷新时它才可以被引用。由于 actionPerformed 方法曾被调用,所以这就是存放这段代码的好地方。
在
actionPerformed 方法中保留后端数据访问可以确保我们在页面刷新时对于同一数据不需要多次访问数据库。当然,您返回到数据源进行数据刷新的时间和频率取决于 Portlet 的需求以及数据本身的因素。在这种情况下,数据不是动态的,当 Portlet 页面被刷新时,应该对其进行高速缓存。
最后,DetailViewState 被设置为这个 Portlet 的下一个状态,对于我们的 Portlet,其中的处理将继续。
清单 8. DetailViewAction 类
public class DetailViewAction extends Action {
/**
* Perform action request
*/
public void actionPerformed(PortletRequest request,
PortletContext portletContext)
throws AIMException {
// Get the userid and the selected contact oid
String userid = request.getUser().getUserID();
String oid = request.getParameter(SELECTED_CONTACT);
// Get the Contact element and put it on session
PortletSettings portletSettings = request.getPortletSettings();
String datasource = portletSettings.getAttribute(DATASOURCE);
ContactListBroker broker = new ContactListBroker(datasource);
Contact contact = broker.getContact(userid, oid);
PortletSession session = request.getPortletSession();
session.setAttribute(CONTACT, contact);
}
/**
* Set the next Portlet state... if this is a MODE change action
* (createReturnURI) then set the next state to null, to allow
* the default state in the next mode (since we do not know
* the mode we are returning to).
*/
public void setState(PortletRequest request)
throws AIMException {
setState(request, DetailViewState.getInstance());
}
}
|
DetailViewState 简单地调用 JSP 来呈现详细信息视图。JSP 从会话中获取联系人 Bean。用于这个视图的部分 JSP 如下所示。在我们的 UI 交互界面中,当用户单击 OK 按钮时,处理返回到主视图继续进行。MainViewAction 的
actionPerformed 方法通过删除我们在 DetailViewAction 类中设置的联系人对象 id 来简单地删除会话数据。
清单 9. 联系人详细信息视图 JSP
<%@ page contentType="text/html"%>
<%@ taglib uri="/WEB-INF/tld/portlet.tld" prefix="portletAPI" %>
<portletAPI:init />
<jsp:useBean id="contact" class="com.ibm.sample.contacts.beans.Contact"
scope="session" />
<%
String viewAction = "com.ibm.sample.contacts.actions.MainViewAction";
%>
<table align="left" border='0' width=400>
<form method="post" name="<portletAPI:encodeNamespace value="form"/>">
<tr>
<td class="wpsInlineHelpText" colspan="2">
<img src='<%=
response.encodeURL("/images/msg_inline_help.gif")%>'
border="0"/>
Contact Detail Information
</td>
</tr>
<tr><td colspan="2"><br/></td></tr>
<tr>
<td class="wpsTableHead">
First Name
</td>
<td class="wpsPortletSmText">
<%= contact.getFirstName() %>
</td>
</tr>
<tr>
<td class="wpsTableHead">
Last Name
</td>
<td class="wpsPortletSmText"> <%= contact.getLastName() %>
</td>
</tr>
<tr>
<td class="wpsTableHead">
Email
</td>
<td class="wpsPortletSmText">
<%= contact.getEmail() %>
</td>
</tr>
... More here ...
<tr><td colspan="2"><br/></td></tr>
<tr>
<td>
<input class="wpsButtonText"
type="submit"
value='Ok;'
onClick="document.<portletAPI:encodeNamespace
value="form"/>.action=
'<portletAPI:createReturnURI><portletAPI:URIAction
name='<%=viewAction%>'/>
</portletAPI:createReturnURI>'">
</td>
</tr>
</form>
</table>
|
图 5. 详细信息视图
这就完成了用于联系人列表 Portlet 视图模式的 UI交互流。您可以看到,流逻辑以一种组织良好的方式进行,您不需要在 Portlet 使用冗繁的控制代码。Portlet 的编辑和配置模式也以同样的方法实现。主编辑视图如下所示。
图 6. 编辑视图
完成 Portlet 实现
该应用程序的其余部分仍遵循这个模式。编辑模式的处理控制逻辑流和视图模式的完全一样。我们实现一个配置模式,它允许用户指定数据源来保存联系人数据。它将以同样的方式实现。配置模式只有一个视图来让用户访问数据源。另外有两个类用来检验数据源和保存数据源为配置数据。
下面是实现状态模式的基本步骤。
- 所执行的特定模式的初始状态类由 InitialStateManager 类提供。
- 状态类的
performView 的方法(其调用是由 StateManagerPortlet 分派的)进行任何必要的应用程序逻辑处理,然后调用它的 JSP。
- JSP 使用 PortletURI 标记来为 UI 上可点击操作生成适当的 HREF。通过 PortletURI 上的一个参数将每个用户的操作与在其中进行事件处理的操作子类的名称相关联。每个用户操作一般都指定一个不同的操作子类,该子类实现一个单一的、特定的功能。
- 当用户单击一个链接时,操作类的
actionPerformed 方法被调用(由于通过 StateManagerPortlet 分派而被再次调用)。执行操作逻辑,并设置适当的状态类以使处理继续进行。
- 当事件处理完成后,StateManagerPortlet
service 方法被调用,并再次分派给我们在 Action 事件处理阶段设置的状态类。该状态类执行应用程序逻辑并调用它的 JSP 来呈现结果。
- 当用户浏览整个 portlet 时,操作以这种方式继续。
持久性代理
访问数据库中的持久性数据需要额外的 Portlet 组件。AbstractBroker 类提供一般的 JDBC 数据库访问功能。它能够用来获取数据库的 DataSource 和 Connection。它在高速缓存中缓存 DataSource 从而避免重复的、高代价的 jndi 查找。它也提供通用代码来执行 PreparedStatement 并关闭 Connection、Statement 和 ResultSet。
ContactListBroker 类继承了 AbstractBroker。它实现特定于 Portlet 需要的数据访问方法,如 getContact 方法和 getContactList 方法。这些方法还可用于保存和删除一个联系人条目。同时它还有检验表 Schema 的功能,这样我们就可以检验用户在配置模式下指定的数据源。
异常处理
您已经为我们的 Portlet 实现了大量的异常类。基本类名为 AIMException。AIMWrapperException 是 AIMException 的子类。您可以用 AIMWrapperException 封装其他抛出的异常,以便在 StateManagerPortlet service 方法中高效地修改显示行为和管理异常处理。
AIMMessageException 是一个特殊的异常,它允许您在终止处理时向 Portlet 生成一条报告性消息或错误消息。例如,您可以抛出一个 AIMMessage 异常,用一条消息指出用户必须先登录。
actionPerformed 方法或 setState 方法中抛出的异常被捕获,并且被延迟到
service 方法。管理延迟的方法是捕获这些方法中抛出的所有异常并将异常(通常封装在一个 AIMWrapperException 中)放在请求对象上。当调用 StateManagerPortlet
service 方法时,它首先查找有没有延迟的异常,如果找到了,就从此处重新抛出并处理这些异常。
因此,所有的 Portlet 应用程序异常都是在 StateManagerPortlet
service 方法中处理的。如果发现了异常,在以下两种情况中调用异常方法:异常消息将在 portlet 中被显示;或者异常消息以及相关联的堆栈跟踪信息将被显示。将堆栈跟踪信息放在 Portlet 外有助于在开发时进行调试。由于您不想向实际应用的用户显示这种级别的详细信息,所以该行为是可配置的。这是在部署描述符里定义的
debugTrace参数。
实现类总结
| 包 | 类 | 描述 | | com.ibm.sample.contacts.actions | Action | 抽象操作类 | | MainViewAction | 处理显示主视图(联系人列表)的请求 | | DetailViewAction | 处理显示联系人详细情况信息的请求 | | MainEditAction | 处理显示主编辑视图的请求 | | AddViewAction | 处理显示添加联系人视图的请求 | | AddAction | 处理实际添加一个联系人的请求 | | ModifyViewAction | 处理显示修改联系人视图的请求 | | ModifyAction | 处理实际保存修改过的联系人的请求 | | DeleteAction | 处理实际删除一个联系人的请求 | | ConfigureAction | 处理显示数据源配置视图的请求 | | SaveConfigAction | 处理保存修改过的数据源配置设置的请求 | | VerifyConfigAction | 处理指定数据源配置设置的请求 | | com.ibm.sample.contacts.states | State | 基本状态接口 | | MainViewState | 呈现联系人列表主视图 | | DetailViewState | 呈现联系人详细信息视图 | | MainEditState | 呈现主编辑视图 | | AddViewState | 呈现添加联系人视图 | | ModifyViewState | 呈现修改联系人视图 | | ConfigureState | 呈现配置参数设置视图 | | com.ibm.sample.contacts.portlet | StateManagerPortlet | Portlet类 | | ActionClassManager | 返回给定名字引用的操作类实例 | | Constants | 定义在 Portlet 使用的常量 | | InitialStateManager | 处理初始化 Portlet 状态的请求 | | com.ibm.sample.contacts.persistence | AbstractBroker | 基本代理类 | | ContactListBroker | Portlet 特定代理功能 | | com.ibm.sample.contacts.beans | Contact | 表示联系人列表对象 | | ContactHelper | 提供添加、删除和修改联系人条目的通用功能 | | com.ibm.sample.contacts.utilities | AIMException | 基本异常类 | | AIMWrapperException | 封装另一个异常类 | | AIMMessageException | 传递流程中断信息到 UI |
结束语
Portlet 开发指导原则和示例实现提供了对 Portlet API 的很好的理解。然而,实现复杂的流程控制超出了 API 的范围。如果没有定义完善的方法来解决如何最好地实现控制逻辑这一问题,您就不能够创建这样的 Portlet,它们包含有专门定位用户请求目的的简单合理的代码。
除了重复之外,该代码使 Portlet 更难以读懂,因为您需要进入控制逻辑以获取获取 Portlet 正在做的实际工作。控制逻辑也容易出错,因为它依赖于多方面情况,比如字符串名匹配以连接事件和侦听器或侦听器操作和 Portlet 方法中的行为。为了解决控制逻辑的这些问题,您可以将您的应用程序看作是 Portlet 的操作和状态的集合。然后,通过一个定义完善的方法来进行状态转换,这样您就可以将 Portlet 应用程序中冗繁的控制逻辑代码删除掉。
开发人员也可以选择采用 struts 框架来实现他们的 Portlet。Struts 是一个 Jakarta 项目,它提供一个非常流行的开放源代码框架来建立 Web 应用程序。使用附带了 Portal 的 Struts Portlet Framework,用 struts 框架写成的 Portlet 就可以在 WebSphere Portal 上运行。Struts 提供了值得考虑的 Web 应用程序,其功能远远超过页面导航,它是进行 Portlet 开发的一个很好的选择。对 struts 不熟悉或者希望更直接访问 Portlet API 的开发人员会从用于页面导航的状态转换实现中获益,比如这里描述的轻量级的模式。这种模式也和 struts 框架一样,使面向 Model-2 Model-View-Controller 设计的应用程序框架得以改进。
本文讨论了用于 Portlet 开发的状态模式的实现,它提供了在 Portlet 中管理复杂应用程序页面流的能力。状态模式提供高效的处理和简洁的代码,有助于开发者轻松地调试、维护和增强应用程序。下面的
下载ZIP 文件中提供了这个样本的完整实现。
下载 | 名字 | 大小 | 下载方法 |
|---|
| ContactList_0312.zip | 75 KB | HTTP |
参考资料
关于作者  | |  |
Tim Hanis 是 IBM WebSphere
Portal 方面的高级软件工程师,他在 IBM Research Triangle Park Lab(位于北卡罗莱纳州的罗利)工作。
|
对本文的评价
|