IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  WebSphere  >

探索 WebSphere Portal V5.1.0.1 编程模型: 第 2 部分:高级 URL 生成

实现门户导航控制

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

样例代码


级别: 初级

Stefan Behl (stefan.behl@de.ibm.com), 软件工程师, IBM
Stefan Hepper (sthepper@de.ibm.com), WebSphere Portal 编程模型架构师, IBM
Stefan Koch (stefkoch@de.ibm.com), 软件工程师, IBM
Carsten Leue (CLEUE@de.ibm.com), WebSphere Portal 运行时架构师, IBM

2006 年 6 月 29 日

本文是系列文章中的第二部分,能帮助您将 WebSphere Portal V5.1.0.1 编程模型应用到您公司的门户。在第 1 部分中,我们对此模型进行了介绍。在本部分中,我们将深入了解对传统 Web 应用程序领域的程序员形成挑战的领域:如何创建统一资源定位符(Uniform Resource Locator,URL)。门户环境对需要创建 URL 的组件有特殊要求;因此门户环境中的 URL 生成要更为复杂一些。您将了解可以如何使用 WebSphere Portal Version 5.1.0.1 系统编程接口 (SPI) 生成 URL(此类接口提供了门户 JSP 所能标记的功能之外的功能)。最后,您将了解如何在实现导航浏览路径记录的示例中使用这些 SPI。

引言

创建独立 Web 应用程序的开发人员对其应用程序的各个方面有全面的控制。应用程序开发人员可以自由地根据需要设计 URL。门户环境则不同。门户应用程序开发人员提供的组件与其他组件组合为更大的门户应用程序。因此,URL 不再能以独立的方式生成;相反,开发人员需要使用门户框架提供的机制来创建 URL。

在 WebSphere Portal 中,生成标记(因而需要能创建 URL)的主要门户组件如下所示:

  • 创建页面的总体外观以及布局的主题。
  • 创建 Portlet 窗口的外观的皮肤,包括用于更改窗口状态或 Portlet 的 Portlet 模式的按钮。
  • 在 Portlet 窗口内呈现其内容的 Portlet。

本文描述了如何通过使用 Java API 为主题和皮肤生成 URL。可以在主题和皮肤中使用门户 JSP 标记库中的 URL 标记和 URLGeneration 标记来创建 URL(请参阅参考资料 列表中的 WebSphere Portal Information Center, Portal tag library section)。这些标记涉及了大部分用例,应该将其用于处理常见用例。不过,对于需要 Java API 或其他功能的场景,可以使用称为 Navigational State SPI 的 WebSphere Portal V5.1.0.1 系统编程接口 (SPI)。此类用例之一是需要使用 URL 切换到另一个主题模板时。

需要生成指向其他 Portlet 的 URL 的 Portlet 应该使用 Portlet API(有关详细信息,请参考参考资料列表中的 JSR 168 Portlet Specification 链接)。例如,在 Portlet A 需要向不同页面上的 Portlet B 发送参数且应将用户转到此页面时,不要通过创建指向 Portlet B 的来将页面层次结构硬编码到 Portlet A 中,而应在 Portlet A 中使用属性代理基础设施触发一个相应事件。门户管理员可以将 Portlet A 的事件连接到 Portlet B 的输入。因此门户将执行到该页的自动切换。有关更多信息,请参阅参考资料Developing JSR 168 compliant cooperative portlets 内提供的参考信息。

为了尽可能多地从本文受益,您应该了解基本门户概念(请参阅第 1 部分),了解 JSP 基础知识,且熟悉 Java 编程。





回页首


状态处理回顾

WebSphere Portal V5.1 将 JSR168 中 Portlet 导航状态扩展到了门户服务器的状态。JSR168 模型支持以下 Portlet 可以使用的状态类型:

  • 持久状态,通过 PortletPreferences API 公开。Portlet 实现者可以使用首选项来通过门户跨最终用户会话存储和检索数据。
  • 会话状态,通过 PortletSession API 公开。会话状态表示在 Portlet 容器管理的有限生命周期内在服务器上保持的数据。会话状态的目的是保持那些临时性的且无法从其他状态或当前交互重新计算的信息(例如,购物车的内容)。主题和皮肤可以使用 HttpSession 来存储会话状态。
  • 导航状态,以使用 PortletRequest 的呈现或交互参数的形式公开。导航状态表示特定于 Portlet 上的当前视图的信息(例如,一个选中的选项卡)。此状态的生命周期与 Portlet 请求的生命周期相同。Portlet 容器可确保跨最终用户与其他门户构件(例如,页面上的 Portlet 或主题中的导航)的多个交互管理 Portlet 的导航状态。使用导航状态对于为 WebSphere Portal 启用浏览器“后退”和“前进”按钮非常关键。除了 JSR168 Portlet 的呈现参数外,WebSphere Portal 还管理各种导航状态信息,如页面选择、页导航树的展开状态、Portlet 模式、状态等等。所有可寻址的 Portlet 的呈现参数组合形成了需要编码到门户页面上的每个 URL 中的完整系统导航状态。


图 1. WebSphere Portal 中的各种状态
图 1. WebSphere Portal 中的各种状态

设计 Portlet 时,需要区分 Portlet 管理的各个状态,并恰当地对其进行编码。特别地,要将导航状态和会话状态设计为彼此正交,因为可能会在不同的导致状态形成的组合中访问相同的会话。如果用户使用“后退”按钮,调用包含 Portlet 具体状态的书签和打开与原始窗口共享会话的新浏览器窗口,并随后分别在两个窗口中独立导航,通常会发生这种情况。

每个 Portlet 生成的链接都属于以下两种类别之一:

  • 操作链接触发 Portlet 操作阶段的调用。在此阶段,Portlet 可以自由地更改会话状态或后端状态,并在操作阶段结束时关联新呈现参数。这些呈现参数将在操作请求后的呈现请求中变为有效。此灵活性的缺点是存在性能损失,因为要在页面上的其他 Portlet 启动其呈现前调用操作阶段。只有操作完成后,这些 Portlet 才开始进行呈现。不过,如果启用了 WebSphere Portal 的 Parallel Portlet Rendering 功能,它们就可以并行呈现。操作链接应始终编码为 POST 链接,以符合针对 WWW1 的 W3C 体系结构建议规范(请参阅参考资料)。
  • 呈现链接将一组呈现参数与调用链接时发出的请求关联。由于进行了呈现链接调用,因此不会调用 Portlet 的操作阶段,Portlet 必须使用一组新的呈现参数呈现其视图。Portlet 一定不能因为调用了呈现链接而修改会话状态或后端状态。呈现链接提供了与 Portlet 交互的最有效办法(使用 JavaScript 的粗粒度客户端交互除外),适合用于仅修改导航状态的情况。应将呈现链接编码为 GET 链接(请参阅参考资料)。

与 Portlet 概念类似,主题和皮肤也可以生成承载操作/呈现链接的语义的链接,此类链接与门户 (portal) 状态(而不是 portlet 状态)的修改相关。

  • 引擎操作的链接与 Portlet 的操作链接很相似,因为二者都引用可以选择更改服务器端状态的服务器端命令。
  • 所有其他链接(例如,页选择或 Portlet 状态/模式修改)与 Portlet 的呈现链接类似。此类链接仅请求门户服务器的不同视图,而不会更改服务器的状态,应表示为 GET 链接(请参阅参考资料)。





回页首


编码导航状态

导航状态表示与特定客户机关联的门户视图。客户机可以通过与网页交互(例如,通过导航到新页面)请求(查询)不同的视图。此类型的交互并不会更改服务器上的任何信息,而仅仅请求服务器提供的新视图。因此,它是 HTTP 中的“安全”操作。此交互的特点在于,客户机可以在最近的视图中向后和向前导航;还可以给视图加书签,以便在以后通过调用浏览器书签回到这些视图。

开发人员通过将 WebSphere Portal 的导航状态编码为 URL 来实现此行为。不同的导航状态将导致产生不同的 URL。





回页首


Navigational State SPI 的分析

接下来,我们讨论新的 Navigational State SPI 的主要概念,并了解这些概念如何相互关联。此外,还将了解访问器应用程序编程接口,该 API 提供了非常方便的,允许对导航状态信息进行类型化访问。

在以下各个小节讨论的所有接口和类都打包在 com.ibm.portal.state.* 下。





回页首


表示导航状态和 URL

从 WebSphere Portal Version 5.1 开始,导航状态就表示为状态信息的层次结构。该对象模型是类似于 DOM 的文档模型,其中包含非类型化的状态信息,表示为 java.lang.String。这个设计决策基于一个假设,即典型的门户页包含很多 URL,要求按请求对此对导航状态进行序列化。基于字符串的内存表示形式支持将导航状态高效地序列化为 URL;这样可避免在序列化过程中使用对象到字符串的转换而导致的时间开销和 CPU 占用。

不过,从程序员的角度来看,使用强类型接口来访问状态信息更为方便,也更不容易出错。因此,状态处理 API 包含一组易于使用的接口(属于访问器 API),可提供对导航状态信息的类型化读写访问。请参阅使用访问器 API 读写导航状态

在新状态处理 SPI 的设计中,一种常用的模式是将只读接口从读写接口(控制器)分离开来。因此,该 API 提供了两个导航状态接口:

  • com.ibm.portal.state.StateHolder 可提供对导航状态信息的读访问
  • com.ibm.portal.state.StateHolderController 允许修改状态信息

图 2 表明了状态容器实际是非类型化文档模型的包装。之所以这样设计,是因为在根据生成的 URL 的特定语法修改当前导航状态前,必须对其进行克隆。例如,如果程序员希望创建指向特定门户页的 URL,则必须更改状态文档模型中的页选择信息;不过,此修改一定不能影响页面上的所有 URL。


图 2:包装非类型化文档模型的 StateHolder
图 2:包装非类型化文档模型的 StateHolder

状态容器的生命周期为一个请求。图 3 显示了处理请求时状态容器所经过的生命周期阶段。

  • URL 解码 期间,门户将创建状态容器,并使用从传入请求 URL 检索的导航状态对其进行填充。它会将普通的基于字符的状态表示形式从 URL 转换为层次结构对象表示形式(上面介绍的文档模型)。
  • 操作处理 期间,门户引擎和 Portlet 可以修改此状态。操作阶段处理完成后,门户会对状态进行整合,以确保不再对其进行修改。
  • 呈现 期间,属于所访问的门户页的一部分的 Portlet 和导航控件将生成 URL 并将其包含到标记中。生成的 URL 通常会包含其模板导航状态,此状态中包含经过整合的状态容器(也称为基状态)和一小部分表示各自 URL 的特定语义的状态信息(请参阅了解 URL 编码)。由于此时不再能够修改基状态,因此门户引擎将为每个新 URL 创建此基状态的一个(逻辑)副本。可以随后根据 URL 所需的语义对此基状态进行修改。维护基状态可确保不会丢失前面的交互的导航状态。


图 3. StateHolder 对象的生命周期
图 3. StateHolder 对象的生命周期

URL 由 com.ibm.portal.state.EngineURL 接口进行建模。EngineURL 表示包含导航状态的 URL(请参阅了解 URL 编码)。从相应 URL 工厂请求新 EngineURL 实例时,要指定 EngineURL 所引用的初始状态容器。通常,这个状态容器是请求特定的基状态的副本。

清单 1 提供了 EngineURL 接口的简单概况,该接口是此 API 中的中心对象。关键方法是 getState() 方法,该方法返回此特定 EngineURL 实例引用的状态容器对象。该方法向状态容器返回一个可修改的接口 (StateHolderController),以允许程序员为此 EngineURL 修改状态。有关如何修改状态的详细信息,请参阅使用访问器 API 读写导航状态

writeCopy() 和 writeDispose() 方法允许将 EngineURL 传递到给定 Writer(例如,从 HTTP 响应获得的标记 PrintWriter)。(尽管也可以使用 toString(),但不建议这样做,因为这个操作的开销相当大。)


清单 1:EngineURL 接口
 public interface EngineURL extends DisposableURL {	
    /** Returns a read-write interface to the state carried by this URL */
    StateHolderController getState();
    /** Specifies whether the URL should point to public /protected area */
    void setProtected(Boolean bFlag);	
    /** Specifies whether a secure connection is requested */
    void setSecure(Boolean bFlag);
    /**
     * Streams the URL to the given writer. Maintains the state of the URL
     * i.e. this method can be called multiple times.
     */
    Writer writeCopy(Writer out)
       throws IOException, OutputMediatorException, 
              PostProcessorException, DocumentModelException;
    /**
     * Streams the URL to the given writer and finally releases the state
     * of the URL. The EngineURL object must not be accessed after 
     * invoking this method
     */
    Writer writeDispose(Writer out)
       throws IOException, OutputMediatorException, PostProcessorException;
}





回页首


了解 URL 编码

WebSphere Portal V5.1 中引入的 URL 编码实现进行了性能优化,可实现以下优势:

  • 尽可能减少生成 URL 所需的时间,特别是对 URL 中应包含的导航状态进行序列化的时间。
  • 使 URL 尽可能短,以减小标记大小和符合某些浏览器强制实施的 2k 的 URL 长度限制。

为了满足这些要求,使用了称为增量编码 (Delta Encoding) 的方法来实现 URL 编码。增量编码的基本思路是这样的,每个 URL 中必须(至少从概念上应如此)包含已整合的基状态(操作阶段后即可用),除非程序员显式地将 URL 基于不同的状态。为了避免为创建的每个 URL 序列化此基状态,会在操作阶段结束后立即对其进行预先序列化。在 URL 生成期间,预先序列化的基状态将设置到每个不相关的 URL 中;状态增量执行 URL 特定的语义,单独编码到各个 URL 的路径信息中。

以下示例显示了一个绝对“增量 URL”,该 URL 的编码解码标识符 kcxml 后跟已序列化的基状态部分,而状态增量部分直接跟在增量编码解码标识符 delta/base64xml 后:

http://myserver.com:9081/wps/portal/kcxml/04_Sj9SPykssy0xPLMnMz0vM0Y_QjHvm5qf
oFuaE6KigCBh9CG/delta/base64xml/Y2BkbGBgYlrDwMDE3srCasTUq!

可以生成相对 URL 时,情况会变得更好。浏览器可以将相对 URL 附加到当前请求 URL 或 HTML 基标记的值后(如果有)。WebSphere Portal 可通过将预先序列化的基状态包含到使用 HTML 基标记的 HTML 页的标头中;具体相对 URL 仅包含状态增量。以下示例说明这个规则可如何大幅度减少标记大小。

<head>
    <base href="http://myportal.com:9081/wps/portal/!ut/p/kcxml/04_Sj9SPykssy0xPAIuz!
    </base>
</head>
<body>
...
    <td class="wpsToolBar" valign="middle" nowrap>
       <a href="delta/base64xml/L2dJQSEvUUt3QS80SVVFLzZfQkEwN1VTOVMyMzAwR0!"
          class="wpsToolBarLink">Administration</a>
    </td>
...

在有些情况下,无法使用相对 URL。例如,当协议从 http 切换到 https 时,生成的 URL 必须为绝对 URL。另外,在标记中大量使用 JavaScript 情况下,浏览器无法解析相对 URL。缺省情况下,WebSphere Portal 中禁用了相对 URL,以避免由于门户页中包含的自定义标记(如 Portlet 产生的标记)而导致此类 JavaScript 问题。不过,Navigational State SPI 提供了显式创建相对 URL 的方法(有关详细信息,请参阅使用 URLAccessorFactory 创建 URL)。





回页首


使用访问器 API 读写导航状态

访问器 API 提供对状态文档模型类型化的访问。它允许程序员方便地查询和修改导航状态信息。

图 4 说明了访问器 API 的情况,此 API 是封装对层次结构文档模型中特定接口的访问的抽象层。对于导航状态的每个方面(例如,页选择、展开状态、Portlet 状态等),访问器 API 提供了一个访问器工厂,其中包含了针对其引用的特定状态而定制的只读访问器和读写访问器。访问器直接对状态文档模型中的各个位置进行读写。它们还将执行所需的类型转换。

所需的导航状态信息位于状态文档模型中,被封装为专用访问器工厂实现。通常,访问器工厂使用路径表达式来定位特定的文档节点,或在状态文档模型中创建阶段(甚至阶段的完整路径)。定位到节点后,访问器工厂将节点引用传递给访问器(或者,如果处于初始化阶段,则传递给访问器控制器)。访问器(以及访问器控制器)实现独立于状态文档模型的结构;也就是说,即使所需的信息已移动到状态文档模型中的其他节点,也可以重用访问器。

图 4 显示了一些重要的访问器;其中省略了对应的访问器工厂。


图 4:访问器 API
图 4:访问器 API

现在,让我们更为详细地讨论一下选择访问器工厂 com.ibm.portal.state.selection.SelectionAccessorFactory 及其所有相关接口。其代码如清单 2 中所示。


清单 2:SelectionAccessorFactory 接口及相关接口
public interface SelectionAccessorFactory extends AccessorFactory {
    /** Returns a read-only accessor operating on the given state */
    SelectionAccessor getSelection(StateHolder state);
    /** Returns a read-write accessor operating on the given state */
    SelectionAccessorController getSelectionController(StateHolderController state);
}

public interface SelectionAccessor extends Accessor {
    /** Returns the object id of the currently selected portal page */
    ObjectID getSelection() throws InvalidSelectionNodeIdException;
}
public interface SelectionAccessorController extends SelectionAccessor {
    /** Selects the page that corresponds with the given id */
    void setSelection(ObjectID pageID) throws CannotInsertSelectionNodeException;
    /** Selects the page that corresponds with the given unique name */
    void setSelection(String uniqueName) throws CannotInsertSelectionNodeException;
} 

和其他访问器工厂一样,SelectionAccessorFactory 公开了一个返回只读 SelectionAccessor 的方法和一个返回读写 SelectionAccessorController 的方法。getSelectionAccessor() 方法将只读 StateHolder 接口作为参数。getSelectionController() 要求使用读写 StateHolderController 接口。

之所以选择这种轻量级的模式(状态作为参数传入),是因为访问器操作的状态并不一定是从请求 URL 检索的特定于请求的基状态。通常,此状态是从特定 EngineURL 创建的状态克隆。可以通过对 EngineURL 对象调用 getState() 来获得 URL 特定的状态容器。清单 3 显示了如何使用 SelectionAccessorController 让所创建的 EngineURL 指向特定的门户页(例如 Stock Market 页)。


清单 3:使用 SelectionAccessorController 创建页链接
 final EngineURL url = ...;
final SelectionAccessorFactory selFct = ...;

final SelectionAccessorController selCtrl = 
    selFct.getSelectionController(url.getState());

try {
    selCtrl.setSelection("wps.StockMarket");
} catch (StateException e) {
    // error handling
    ...
} finally {
    selCtrl.dispose();
}

Accessor 基接口派生自 com.ibm.portal.Disposable 接口。强烈建议程序员通过对访问器调用 dispose() 来显式地指示何时不再需要此访问器。这就允许访问器工厂实现将访问器工厂存储在对象池中,以实现更好的性能(因为初始化开销更少,垃圾回收工作也更少)。





回页首


使用 PortalStateManagerService 生成 URL

PortalStateManagerService 是在 WebSphere Portal Version 5.1.0.1 中新引入的服务,允许程序员方便地创建 URL 来实现复杂的门户用例。

可以使用此服务进行以下任务:

  • 在 WebSphere Portal 标记库不够用的情况下(特别是 URL 标记和 URLGenerating 标记),在主题 (JSP) 内创建 URL
  • 以编程的方式在任何门户相关的构件(如自定义标记、自定义门户操作等)中创建 URL
  • 以编程方式“脱机”创建URL;即在不具有对当前 Servlet 请求的访问权限的业务组件中创建(例如,Enterprise JavaBeans)

以下各个小节描述如何获取多 PortalStateManagerService 的访问,以及如何创建 URL。最后一个小节将演示如何实现自定义浏览路径记录导航标记,该标记将使用这个新服务(属于该 SPI 的一部分)。

获取对 PortalStateManagerService 的访问

要访问 PortalStateManagerService,请使用查询名称为 portal:service/state/PortalStateManager 的 JNDI 查询。此查询将返回一个 com.ibm.portal.state.service.PortalStateManagerServiceHome 接口和一个 getter 方法,用于检索特定的 com.ibm.portal.state.service.PortalStateManagerService。

PortalStateManagerServiceHome 实例在门户生命周期中有效。因此,您应仅执行该 JNDI 查询一次,然后存储所检索到的主对象(例如,存储在实例中或静态变量中)。

与 PortalStateManagerHome 对象不同,PortalStateManagerService 具有请求范围;即仅能在一个 Servlet 请求中使用。对于所有后续 Servlet 请求,都需要从主接口请求一个新服务接口。清单 4 显示了如何获取对该服务的访问。


清单 4:如何获取对 PortalStateManagerService 的访问
/** the JNDI name to retrieve the PortalStateManagerServiceHome object */
private static final String JNDI_NAME = 
                               "portal:service/state/PortalStateManager";

/**
 * The PortalStateManagerServiceHome object to retrieve the service from 
 */
private static PortalStateManagerServiceHome serviceHome;

/** Any method processing request and response. */
public void anyMethod(final HttpServletRequest request,
                      final HttpServletResponse response) {
    try {
        // get the service from our home interface
        final PortalStateManagerService service
            = getServiceHome().getPortalStateManagerService(request, response);
       // use the service
       ...
       // indicate that we do not need it any longer
       service.dispose();
    } catch (Exception e) {
        // error handling
    }
}
/**
 * Looks up the PortalStateManagerServiceHome being valid
 * for the lifetime of the portal.
 */
private static PortalStateManagerServiceHome getServiceHome() {
    if (serviceHome == null) {
        try {
            final Context ctx = new InitialContext();
            serviceHome =
                (PortalStateManagerServiceHome) ctx.lookup(JNDI_NAME);
        } catch (Exception e) {
            // error handling
        }
    }
    return serviceHome;
} 

PortalStateManagerService 接口派生自 com.ibm.portal.Disposable 接口。强烈建议程序员通过调用 dispose 方法来显式地指示何时不再需要此服务。由于该服务具有请求范围,因此必须在最近的请求完成时释放该服务。

这个服务请求范围规则有一个例外。如果在完全从请求处理分离(甚至从门户分离)的环境或组件中使用该服务,服务的生命周期为未定义。在这种情况下,可以从主接口请求该服务,同时为请求和响应传入 null,可以在自己认为合适的时候释放它。

使用 PortalStateManagerService

com.ibm.portal.state.PortalStateManagerService 接口提供了两个方法:

StateHolderController newState()
创建新的可修改状态容器。当以编程方式构造新导航状态以作为多个 EnginURL 的基状态时,请使用此方法。
AccessorFactory getAccessorFactory(final Class cls)
提供对此服务提供的功能的访问。URLAccessorFactory 充当了决定性的角色,允许编程人员创建支持各种用例的 URL。可获得的所有其他访问器工厂都需要通过修改各自的导航状态部分来表述特定的语义。

要使用该服务,请按照以下所述进行操作:

  1. 使用 URLAccessorFactory 获取一个新 EngineURL 对象。
  2. 通过修改已与 EngineURL 关联的 StateHolderController 来指定所创建的 URL 的语义。使用通过 getAccessorFactory(Class) 方法获取的相应访问器工厂。
  3. 将 EngineURL 写入输出流(如 JSPWriter),以将其包含到标记中。

接下来,让我们看看如何使用 URLAccessorFactory 创建 URL。





回页首


使用 URLAccessorFactory 创建 URL

com.ibm.portal.state.accessors.url.URLAccessorFactory 提供了若干可用于方便地获取 EngineURL 实例的方法。

   
EngineURL newURL(HttpServletRequest request,
                 HttpServletResponse response,
                 Constants.Clone type);

程序员通常选择此方法来创建 EngineURL,因为此方法适合于所有常见用例。该方法提供了引用表示当前 Servlet 请求的导航状态的 StateHolder 的 EngineURL。type 参数指定请求特定的 StateHolder 应如何克隆,以便创建 URL。有四个预定义克隆常量:

  • SMART_COPY 指示应创建一个影子 StateHolder 副本,其中仅记录应用于此特定 EngineURL 的状态修改,而不是复制文档模型中的所有节点来构造克隆。SMART_COPY 表示缺省设置;因此,传入 null 等效于 SMART_COPY。这个克隆方法还允许生成相对“增量”URL。
  • DEEP_COPY 将导致复制请求特定的 StateHolder 的完整副本,即,将克隆每个节点以及节点层次结构。完全复制会防止生成增量 URL。
  • EMPTY_COPY 指示应清除请求特定的 StateHolder 内容,即所创建的 EngineURL 将基于空状态。与此类 URL 的交互将导致丢失以前的交互的导航状态。

EngineURL newURL(HttpServletRequest request,
                 HttpServletResponse response,
                 StateHolder state,
                 Constants.Clone type);

第二个 newURL 方法要求显式地传入 EngineURL 所基于的 StateHolder。type 参数将引用此特定 StateHolder。当要创建的 URL 应对以编程方式定义的 StateHolder 进行编码时,请使用此方法。单击此类 URL 通常将导致丢失以前与门户交互的导航状态。

EngineURL newURL(HttpServletRequest request,
                 HttpServletResponse response,
                 URLContext ctx,
                 Constants.Clone type);

下一个变体允许指定所创建的 URL 应为绝对 URL、相对于服务器的 URL 或相对 URL。可以传入和实现 URLContext 接口;例如,如果 URL 必须为绝对 URL,则 isAbsolute() 方法必须返回 True;而 isRelative() 和 isServerRelative() 必须返回 False。由于此方法并不要求使用显式的 StateHolder 参数,因此 EngineURL 将对从给定请求检索的 StateHolder 进行编码。要减小标记大小,请传入允许相对 URL 的 URLContext。有关相对 URL 的更多信息,请参阅了解 URL 编码

EngineURL newURL(HttpServletRequest request,
                 HttpServletResponse response,
                 StateHolder state,
                 URLContext ctx,
                 Constants.Clone type) 

此方法与前一方法极为相似,采用以编程方式定义的 StateHolder 作为显式参数。

EngineURL newURL(ServerContext request,
                 boolean isSecure,
                 boolean isProtected,
                 StateHolder state,
                 Constants.Clone type)
 

最后一个 newURL 方法需要使用 Servlet 请求或响应。可以使用其“脱机”生成URL;即,在对当前 Servlet 请求不具有访问权限的 EJB 或任何其他 Java 构件中生成。要显式地传入主机名、服务器端口、上下文路径和 Servlet 路径。ServerContext 接口是封装此类型信息的抽象。Boolean 参数 isSecure 指定 URL 是产生安全连接 (https) 不是非安全连接 (http)。第二个 Boolean 参数 isProtected 声明 URL 是否指向门户的受保护区域。如果为其中的任意参数传递的是 null,则将保留当前请求的属性。

获得了 EngineURL 对象后(使用上述方法之一),可以按照使用访问器 API 读写导航状态所述修改 URL 的导航状态。为了获得对各个访问器工厂的访问,将使用 getAccessorFactory(Class) 访问方法,该方法将返回一个 java.lang.Object 类型的对象。可以根据传入的 Class 对象将返回的对象强制转换为特定的访问器工厂。

清单 5 显示了使用这些方法的示例。createPortletLink 方法演示了如何创建指向给定 Portlet 的 URL,并更改其导航状态(Portlet 模式、窗口状态和呈现参数)。第二个示例 createOfflinePageLink 演示了如何在不具有访问当前请求和响应的权限的情况下创建指向具有特定主题的门户页的 URL。

每个示例方法都涉及到使用恰当的 newURL 方法从 URLAccessorFactory 获取 EngineURL,更改 URL 的导航状态以及将 URL 提供给给定的 Writer。


清单 5:创建更改 Portlet 导航状态的 URL
public Writer createPortletLink(
    final HttpServletRequest req, final HttpServletResponse res,
    final ObjectID portletWindowID, final PortletMode newMode,
    final WindowState newState, final Map renderParameters,
    final Writer writer) throws StateException, IOException {
    
    final PortalStateManagerService service
        = getServiceHome().getPortalStateManagerService(req, res);
    
     // get a URL from the URL accessor factory using request and response
    final URLAccessorFactory urlFct = (URLAccessorFactory) 
          service.getAccessorFactory(URLAccessorFactory.class);
    // the URL should be based on the current request state
    final EngineURL url = urlFct.newURL(request, response, null);
    
    // change the portlet's navigational state
    final PortletAccessorFactory portletFct = (PortletAccessorFactory)  
        service.getAccessorFactory(PortletAccessorFactory.class);
    final PortletAccessorController portletCtrl
        = portletFct.getPortletController(portletWindowID, url.getState());
    try {
        portletCtrl.setPortletMode(newMode);
        portletCtrl.setWindowState(newState);
        portletCtrl.getParameters().putAll(renderParameters);
    } finally {
        portletCtrl.dispose();
    }
    // stream the URL using the given writer
    return url.writeDispose(writer);
} 


清单 6:脱机创建本地化的门户页链接
public Writer createOfflinePageLink(
    final ServerContext serverCtx, final boolean isSecure,
    final boolean isProtected, final ObjectID pageID,
    final Locale locale, final Writer writer)
    throws StateException, IOException {
    
    // get the service via the home interface
    final PortalStateManagerService service
        = getServiceHome().getPortalStateManagerService(null, null);
    
    // get a URL from the URL accessor factory
    final URLAccessorFactory urlFct = (URLAccessorFactory)
        service.getAccessorFactory(URLAccessorFactory.class);
    final EngineURL url = urlFct.newURL(serverCtx, isSecure, isProtected,
        service.newState(), null);
    
    // change the page selection and the locale
    final SelectionAccessorFactory selFct = (SelectionAccessorFactory) 
        service.getAccessorFactory(SelectionAccessorFactory.class);
    final ThemeTemplateAccessorFactory themeTemplateFct = 
      (ThemeTemplateAccessorFactory) service.getAccessorFactory
      (ThemeTemplateAccessorFactory.class); 
    final SelectionAccessorController selCtrl
        = selFct.getSelectionController(url.getState());
    final ThemeTemplateAccessorController themeTemplateCtrl = 
      themeTemplateFct.getThemeTemplateAccessorController(url.getState()); 

    
    try {
        selCtrl.setSelection(pageID);
        themeTemplateCtrl.setThemeTemplate(themeTemplate); 
    } finally {
        // dispose the controllers
        selCtrl.dispose();
        themeTemplateCtrl.dispose();
    }

为了支持更为复杂的用例,PortalStateManagerService 还提供了其他访问器工厂。下面的列表对可用访问器工厂进行了描述。所有访问器工厂都属于 com.ibm.portal.state.accessors.* 包的一部分。

SelectionAccessorFactory
SelectionAccessorFactory 提供了读写门户页选择信息的访问器。要创建指向另一个页面的 URL,需要从工厂请求 SelectionAccessorController,以便基于与所创建的 EngineURL 关联的状态设置新选择。
PortletAccessorFactory
PortletAccessorFactory 提供了读写门户相关导航状态信息的访问器。这包括 Portlet 模式、窗口状态和呈现参数。特别地,PortletAccessorController 可用于更改给定 Portlet 的导航状态(如 Portlet 模式)。
PortletTargetAccessorFactory
PortletTargetAccessorFactory 提供了读写 Portlet 操作相关信息的访问器。特别地,PortletAccessorController 可用于将 Portlet 声明为操作的目标对象。这就允许程序员创建触发 Portlet 操作的 URL。
SoloAccessorFactory
SoloAccessorFactory 提供引用 Portlet 的竖井 (Solo) 视图访问器。SoloAccessorController 可以用于创建激活/取消激活特定 Portlet 的竖井视图的 URL。
ThemeTemplateAccessorFactory
ThemeTemplateAccessorFactory 支持读写主题模板信息。特别地,ThemeTemplateAccessorController 可以用于创建切换到特定主题模板的 URL。
ExpansionStatesAccessorFactory
ExpansionStatesAccessorFactory 提供读取展开状态信息的访问器,即用于确定导航树中的给定导航节点是展开还是折叠的信息。ExpansionStatesAccessorController 通常用于生成切换特定导航节点的展开状态的切换 URL。
ShowToolsAccessorFactory
ShowToolsAccessorFactory 提供读写“显示工具”相关信息的访问器。ShowToolsAccessorController 通常用于创建与 Portlet 窗口结合在一起来提供移动/删除相应 Portlet 等功能的 URL。
StatePartitionAccessorFactory
StatePartitionAccessorFactory 提供读写状态分割标识符的访问器。StatePartitionAccessorController 可以用于将状态分割标识符包含到导航状态中。新状态分割标识符应包含到打开新浏览器窗口的 URL 中。
EngineActionAccessorFactory
EngineActionAccessorFactory 提供应用于创建引擎操作 URL 的控制器。特别地,EngineActionAccessorController 允许设置操作参数。请注意,此访问器工厂并不提供只读访问器,因为引擎操作的执行是由门户全权进行管理的。





回页首


示例:主题的浏览路径记录导航控件

为了演示如何使用该 API,让我们了解一下如何在自定义 JSP 标记中使用它。

图 5 显式了一个示例页面,其中包含一个浏览路径记录导航控件,可将该控件用于主题和皮肤中。浏览路径记录导航定位当前选定的页面,并显示属于从内容根到选定的页面的导航路径的所有页面的标题。这些标题呈现为页链接,允许用户直接导航到当前页的各个父级页面。在图 5 中,用户已选择了 Company Tracker 页。


图 5:在主题中使用浏览路径记录导航
图 5:在主题中使用浏览路径记录导航

清单 7 显式了如何在主题 JSP 中包含浏览路径记录导航。在此示例中,选择使用的是 WebSphere Portal 的缺省主题 (Default.jsp)。


清单 7:在 JSP 中包括浏览路径记录
<td colspan="2" class="breadcrumb" >
    <crumbtrail:loop var="selection">
        &nbsp;>&nbsp;;
        <a href='<crumbtrail:url node="<%= selection %>" 
                                 allowRelative="true"/>'>
            <wps:title varname="<%=selection%>"/>
        </a> 
    </crumbtrail:loop><br>
</td>

此清单中使用了两个自定义 JSP 标记来应用浏览路径记录。这两个标记都属于所包含的带 crumbtrail 前缀的标记库。

  • LoopTag (crumbtrail:loop) 封装导航路径的查询。

    在其内部,将对已标识的导航路径进行遍历,并将每个已访问的导航节点保存在称为 selection 的脚本变量中。LoopTag 并不产生任何标记输出。

  • UrlTag (crumbtrail:url) 负责创建指向当前导航节点(即当前页)的 URL。它将使用 tag 属性节点获取相应的导航节点。可以直接将 selection 脚本变量的值赋给它 (node="<%= selection %>")。

    在其内部,将使用 PortalStateManagerService 创建 URL,并使用相应的 JSPWriter 将 URL 写入到标记中。因此,URL 可以直接作为 HTML href 属性包含到标记中。所创建的链接的本地化显示名是使用 portal title 标记确定的。

在以下部分,我们将更详细地讨论 UrlLTag。为了简单起见,将不对 LoopTag 进行详细说明,因为它并不使用新的 SPI;它仅仅使用 Model SPI 来遍历导航模型,从而构建浏览路径记录。有关详细信息,请参阅参考资料中的 WebSphere Portal V 5.1.0.1 Information Center, Model SPI Overview section 链接。

在 URL 标记内访问 PortalStateManagerService

获取对 PortalStateManagerService 访问中所述,可以从使用 JNDI 检索的 PortalStateManagerHome 对象获取 PortalStateManagerService。

清单 8 显示了如何使用名为 UrlTag 的自定义标记实现服务检索。


清单 8:在标记中获取对 PortalStateManagerService 的访问
public class UrlTag implements Tag

  /** the JNDI name to retrieve the PortalStateManagerServiceHome object */
  private static final String JNDI_NAME = 
      "portal:service/state/PortalStateManager";

  /** the PortalStateManagerServiceHome object to retrieve the service from */
  private static PortalStateManagerServiceHome serviceHome;

  /** the PortalStateManagerService for the current request */
  private PortalStateManagerService service;

  /**
   * Initializes the tag on a per page basis. Gets the request-specific
   * PortalStateManagerService from the service home.
   */
  public void setPageContext(final PageContext pc) {
      pageContext = pc;
      try {
          service = getServiceHome().getPortalStateManagerService(
              (HttpServletRequest)pageContext.getRequest(),
              (HttpServletResponse)pageContext.getResponse());
      } catch (Exception e) {
          logger.log(Level.WARNING,"Exception occured", e);
      }
  }
/**
   * Looks up the PortalStateManagerServiceHome being valid
   * for the lifetime of the portal.
   */
  private static PortalStateManagerServiceHome getServiceHome() {
      if (serviceHome == null) {
          try {
              final Context ctx = new InitialContext();
              serviceHome =
                  (PortalStateManagerServiceHome) ctx.lookup(JNDI_NAME);
          } catch (Exception e) {
              logger.log(Level.WARNING,"Exception occured",e);
          }
      }
      return serviceHome;
  }

  /**
   * @see javax.servlet.jsp.tagext.Tag#release()
   */
  public void release() {
      service.dispose();
      service = null;
  } 
 

PortalStateManagerServiceHome 对象在门户生命周期中有效。因此,将仅使用 JNDI 检索一次,并随后存储在静态变量 serviceHome 中。查询逻辑在私有方法 getServiceHome() 中实现。PortalStateManagerService 在 setPageContext() 方法中请求,每次页面访问仅调用该方法一次。因此,当每个页面上多次调用 URLTag 时(本例中就是如此),可以重用相同的服务实例。此解决方案之所以正确,是因为页范围就指示了所需的请求范围。

在 release() 方法内,标记通过调用其 dispose() 方法显式地指示不再需要该服务了。处理了 URLTag 的所有调用后,JSP 实现调用 release() 方法来指示标记的生命周期结束。

创建导航链接

每个 URL 表示在 URLTag 的 doStartTag() 方法内创建了浏览路径记录导航内的一个页面链接。清单 9 显式了所需的代码。


清单 9:在 doStartTag 内创建导航链接
 /** The navigation node to operate on */
private NavigationNode iNode;

/** Flag indicating whether this tag may create a relative URL */
private Boolean iAllowRelativeURL;

/** URL context that allows for relative URLs */
private static final URLContext ALLOW_RELATIVE_URL = new RelativeURLContext();

/** URL context that disallows relative URLs */
private static final URLContext DISALLOW_RELATIVE_URL = new NonRelativeURLContext();

/** Sets the navigation node to operate on. */
public void setNode(final NavigationNode node) {
    iNode = node;
}

/** Specifies whether the URL to be created may be relative or not. */
public void setAllowRelativeURL(final String allowRelative) {
    iAllowRelativeURL = Boolean.valueOf(allowRelative);
}

/**
 * Create an EngineURL which points to the portal page represented by iNode.
 * Finally write URL to the responsible JSPWriter and indicate that no tag
 * body processing is required.
 */
public int doStartTag() throws JspException {
    if (iNode != null) {
       try {
            // get the factory providing EngineURLs
            final URLAccessorFactory urlAccessorFactory = 
                (URLAccessorFactory) service.getAccessorFactory 
                    (URLAccessorFactory.class);            
            // request an EngineURL 
            final EngineURL selectionURL;
            // request and response
            final HttpServletRequest request =
                (HttpServletRequest) pageContext.getRequest();   
            final HttpServletResponse response =
                (HttpServletResponse) pageContext.getResponse();   
            // check whether we can create a relative URL
            if (iAllowRelativeURL == null) {
                // the server should decide
                selectionURL = urlAccessorFactory.newURL(
                    request, response, null);
            } else if (iAllowRelativeURL.booleanValue()) {
                // relative URLs are allowed
                selectionURL = urlAccessorFactory.newURL(
                    request, response, ALLOW_RELATIVE_URL, null);
            } else {
                // relative URLs are not allowed
                            selectionURL = urlAccessorFactory.newURL(
                    request, response, DISALLOW_RELATIVE_URL, null);
            }
            // get the selection accessor factory    
            final SelectionAccessorFactory selectionAccessorFactory = 
                (SelectionAccessorFactory) service.getAccessorFactory
                    (SelectionAccessorFactory.class);           
            // get the selection controller which operates on the URL-
            // specific state which has been derived from the request state 
            final SelectionAccessorController selectionAccessorController = 
                selectionAccessorFactory.getSelectionController
                    (selectionURL.getState());
            // set the ObjectID of iNode
            selectionAccessorController.setSelection(iNode.getObjectID());
            // we do not need the controller any more
            selectionAccessorController.dispose();				
            // stream the URL to the obtained JSPWriter and finally dispose
            // the EngineURL object
            selectionURL.writeDispose(pageContext.getOut());
        } catch (Exception e) {
            logger.log(Level.WARNING,"Exception occured", e);
        }
    }
    // no body processing required	
    return SKIP_BODY;
}

/** Reset our instance variables to enable tag pooling. */
public int doEndTag() throws JspException {
    iNode = null;
    iAllowRelativeURL = null;
    return EVAL_PAGE;
}

/** URL context implementation that also allows for relative URLs */
private static class RelativeURLContext implements URLContext, Serializable {
    /** @see com.ibm.portal.state.accessors.url.URLContext#isAbsolute() */
    public boolean isAbsolute() {
        return true;
    }
    /** @see com.ibm.portal.state.accessors.url.URLContext#isRelative() */
    public boolean isRelative() {
        return true;
    }
    /** @see com.ibm.portal.state.accessors.url.URLContext#isServerRelative() */
    public boolean isServerRelative() {
        return true;
    }
}

/** URL context implementation that enforces non-relative URLs */
private static class NonRelativeURLContext extends RelativeURLContext {
    /** @see com.ibm.portal.state.accessors.url.URLContext#isRelative() */
    public boolean isRelative() {
        return false;
    }
} 

URLTag 具有两个实例变量:iAllowRelative 和 iNode。Boolean 实例变量 iAllowRelative 指示标记是否可以创建相对 URL。对应的(可选)标记属性是 allowRelative。缺省情况下,并未设置 iAllowRelative(等于 null),指示服务器应决定是否可以生成相对 URL。决定 URL 是否可为相对的,极大地依赖于将 URL 包括到标记中的方式。在我们的示例中,可以使用相对 URL,因为所创建的导航 URL 是作为链接标记 (Anchor Tag) 的 href 属性包括进来的。尽可能使用相对 URL,以减少浏览路径记录控件的标记大小。

用于计算 iAllowRelative 变量的代码属于 doStartTag 方法的一部分。将使用 newURL(HttpServletRequest, HttpServletResponse, Constants.Clone) 方法(需要由门户服务器进行决定的情况)或 newURL(HttpServletRequest, HttpServletResponse, URLContext, Constants.Clone) 方法(显式地禁用或启用了相对 URL 生成的情况)从 URLAccessorFactory 请求 EngineURL。在后一种情况,传递给 newURL 方法的 URLContext 参数为 RelativeURLContext 内部类的实例(允许生成相对 URL)或 NonRelativeURLContext 的实例(强制要求使用非相对 URL)。

NavigationNode 类型的实例变量 iNode 表示标记引用的页。JSP 中使用的对应(强制)标记属性是 node。为了让所创建的 EngineURL 选择此页,要调用 SelectionAccessorController 的 setSelection 方法,并传入导航节点 iNode 的 ObjectID。

doStartTag 方法结束时,会通过调用 selectionURL.writeDispose(pageContext.getOut()) 将所创建的页选择 URL 直接传递给 JSP writer。





回页首


结束语

导航状态表示与特定客户机关联的门户视图。从 WebSphere Portal V5.1 开始,导航状态就可以编码到 URL 中,从而支持各种重要的功能。用户可以使用浏览器的“后退”或“前进”按钮在以前访问过的视图中向前和向后导航。用户可以在以后单击浏览器书签来回到特定的门户视图。

将导航状态有效编码到 URL 中的工作是由 Navigational State SPI 实现内部完成的。根据用例不同,程序员只需要使用访问器 API 在所创建的 URL 对象上设置相应的导航状态即可。访问器 API 提供了直观的类型化接口来读写特定的导航状态信息(如页选择、主题、Portlet 相关的主题等等),从而将程序员从执行类型转换的工作中解放出来。

PortalStateManagerService 是一个非常易于使用的门户服务,可为主题程序员提供对 Navigational State SPI 的访问,从而允许实现各种用例,如开发增强缺省门户 URL 标记提供的功能的自定义标记、脱机创建 URL(如在 EJB 中)等。

在本系列的下一个部分,我们将介绍如何将 WebSphere Portal 集成到您现有的安全环境中。我们将讨论单点登录(Single-Sign-On,SSO)和登录/注销行为自定义等主题。





回页首


已知问题

要在 WebSphere Portal Version 5.1.0.1 和 Version 5.1.0.2 中利用相对 URL,需要安装 APAR PK15894 修补程序。






回页首


下载

描述名字大小下载方法
Code samplescrumbtrail.zip12 KB  FTP|HTTP
关于下载方法的信息


参考资料



作者简介

Stefan Behl 的照片

Stefan Behl 是位于德国 Boeblingen 的 IBM Development Laboratory 的一名软件工程师。他于 2003 年加入 Workplace and Portal Foundation Development,在 Portal Engine 团队工作。他的主要工作重点是导航状态处理和页面聚合。Stefan 曾在德国斯图加特大学学习软件工程,获得了计算机科学的毕业证。


Stefan Hepper 是负责 WebSphere Portal、Workplace Client 及 Server 编程模型和公共 API 的架构师。他是 Java Portlet Specification V 1.0 (JSR 168) 的负责人之一,目前正在负责 V 2.0 (JSR 286) 的工作。Stefan 发起了 Apache Pluto 项目,该项目为 JSR 168 提供了参考实现。他曾在各种国际会议(如 JavaOne)发表演讲,曾发表了多篇论文,并且是《Pervasive Computing》(Addison-Wesley,2001 年)和《Portlets and Apache Portals》(下载手稿,Manning,2005 年)的合著者。Stefan 毕业于德国卡尔斯鲁厄大学计算机科学专业,于 1998 年加入 IBM B?blingen Development Laboratory。


Stefan Koch 的照片

Stefan Koch 是位于德国 Boeblingen 的 IBM Development Laboratory 的一名软件工程师。作为 WebSphere Portal Engine Team 的成员,他负责门户标记和 URL 生成问题。Stefan 曾在德国 University of Applied Sciences Osnabrueck 学习信息工程专业。


Carsten Leue 的照片

Carsten Leue 博士在是位于德国 Boeblingen 的 IBM Development Laboratory 的 WebSphere Portal 团队的一名架构师。他具有 6 年软件开发领域的经验,获得了德国德国海德堡大学的物理博士学位。他于 2000 年加入 IBM,由于他有图像处理方面的技术背景,所以当时参与解决一个门户解决方案的识别问题,后来转到 WebSpherePortal 任规范编辑,参与撰写 WSRP OASIS 标准。通过担任首席程序员,Carsten 对 WebSphere Portal 有了深刻的了解,现在他主要从事状态处理和配置管理领域的 Foundation 体系结构工作。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款