内容


通过 Web 服务 API 和 JAXB 编组与 WebSphere Process Server 交互

Comments

简介

除了传统的 Enterprise JavaBeans (EJB) 接口之外,最近增加了 Java Messaging Service (JMS) API、REST 接口和 Web 服务 API。comparison of the programming interfaces for interacting with business processes and human tasks 讨论了这些 API 的优点和缺点。

Web 服务 API 是在 WebSphere Process Server 的 6.0.2 版中引入的,它为构建与业务流程和人工任务交互的客户机应用程序提供丰富的功能。具体地说,它提供以下功能:

  • 能够与支持 Web 服务调用的任何运行时环境通信,包括 Microsoft®.NET 环境。
  • 作为 Web 服务公开底层的调用点。
  • 更好地隔离客户机和服务器。
  • 可以利用现有的行业模式以及强大的 XML 工具和运行时。

使用 EJB API 时,可以利用 远程工件装载器 从远程服务器将现有的工件装载到应用程序中。它宿主着服务器上安装的工件,让它们对相同或其他计算单元中的远程工件装载器客户机可用。客户机然后可以从远程工件装载器服务器查询或装载工件。

但是,Web 服务 API 不支持 RAL,所以在客户机上必须有适当格式的输入数据模式、输出数据模式和变量模式。对于这个问题,Java Architecture for XML Binding (JAXB) 提供一种把 XML 模式绑定到 Java 代码中的表示的简便方法。这让开发人员可以方便地把 XML 数据和处理功能合并到 Java 应用程序中,而不必详细了解 XML 本身。

本文并不是 JAXB 教程,而是讨论 JAXB 开发和运行时环境如何简化把 XML 模式定义 (XSD) 映射到 Java 的过程。本文还讨论在运行时通过 JAXB 运行时和 Java 反射动态地生成基本用户界面所需的运行时特性。

Java Architecture for XML Binding (JAXB)

可以通过 XML 模式表示业务领域对象和它们的结构关系。JAXB 引入了数据绑定 的概念,即 XML 模式与 Java 类的对应关系。

JAXB 模式编译器根据 XML 模式的结构创建 Java 类和接口(通常在开发时执行)。在运行时,使用 JAXB 库进行编组和反编组。编组(marshalling) 是把一个或多个 Java 对象转换为 XML 文档的过程,反编组(unmarshalling) 是相反的过程,即根据 XML 创建 Java 对象,见图 1。

图 1. JAXB 元素
JAXB 元素
JAXB 元素

xjc 模式编译器根据 XML 模式生成带注解的 Java 类。这个步骤通常在开发时执行,它创建一组映射到 XSD 文件中定义的元素和类型的 JavaBean。然后,通过使用 JAXB 绑定运行时 API,在 XML 实例文档和 Java 对象之间进行转换。

带注解的类包含 JAXB 运行时解析 XML(为了进行编组和反编组)所需的所有信息,还可以通过一个可选的步骤检验文档。可以把生成的类与 Java API for XML Web Services (JAX-WS) 技术和 WebSphere Process Server Web 服务 API 结合使用。

JAXB 的优点在于,它让 Java 开发人员可以访问和处理 XML 模式,而不必详细了解底层机制。另外,可以灵活地定制模式如何映射到 Java(以及反向映射),这对于复杂或经常变动的模式尤其有用,因为同步相应的 Java 定义要花费大量时间,很容易出错。

在下面几节中,讨论与开发和运行时相关的活动。但是,在讨论细节之前,我们先简要介绍一下主要构建块。

基本架构

图 2 给出一个可能出现的场景:浏览器访问部署在两个服务器上的客户机应用程序和后端 WebSphere Process Server。

图 2. 主要构建块
主要构建块
主要构建块

客户机逻辑通过代理与过程通信,代理把 Business Flow Manager (BFMIFProxy.java) 和 Human Task Manager (HTMIFProxy.java) Web 服务操作表示为 Java 接口。它们是在设置开发环境时根据导出的 WSDL 工件生成的。

跨服务器的通信必须确保安全。所有 Web 服务请求必须包含安全令牌,安全令牌代表有效的用户身份验证。Web 服务 API 支持的安全机制是用户名令牌Lightweight Third Party Authentication (LTPA)。前提条件是需要确保 Web 应用程序的安全,因此它需要显式的身份验证。对这个设置过程的详细描述,参见 Web service API - J2EE client 中的安全部分。

另外,如果客户机应用程序在单独的逻辑节点上运行,那么必须在这两个服务器之间建立单点登录,从而确保每个调用都附带 LTPA 令牌。更多信息参见 信息中心:Single sign-on

开发活动

下面讨论开发时所需的步骤。

设置 Web 服务开发环境

使用 WebSphere Process Server Web 服务 API 涉及许多步骤,比如复制关键工件、生成代理客户机和确保安全性。Web 服务代理是应用程序的主要集成点,是在 WebSphere Integration Developer 中根据导出的 WSDL 工件生成的,见图 3。

图 3. 生成 Web 服务代理
生成 Web 服务代理
生成 Web 服务代理

Developing client applicationsWeb service API - J2EE client - Version 7.0 解释了这些步骤。既然已经准备好了基本的开发环境,我们就来看看另一个主要构建块 JAXB。

示例模式

正如前面提到的,创建 Web 服务的最佳实践是自顶向下方法,即根据 WSDL 和模式创建 Web 服务。对于本文,我们使用一个大家都了解但不一定喜欢的模式类型:税收模式。图 4 给出 WebSphere Integration Developer 模式编辑器中的主要类型。

图 4. 示例 XML 模式
示例 XML 模式
示例 XML 模式

XML 模式代表数据或信息模型,因此是所有软件开发项目中的关键工件。XML 模式往往是解决方案的主要相关人员进行彻底讨论和分析的结果。它必须与过程和领域模型及用例一起维护。

在早期设计和开发阶段,模式很可能会发生变化,相应的 Java 类必须反映这些变化。可以使用 JAXB 简化这个映射过程。

另外,您可能无法直接控制模式,因为它们是由其他组织定义或由专有工具(比如 WebSphere Business Modeler)生成的。在这些情况下,您只能去适应模式,使它们 “映射更友好”,例如采用能够明确地映射到 Java 类的形式。JAXB 提供强大的定制功能,让这个过程更轻松。

绑定模式文档

用命令行工具 xjc 或在 WebSphere Integration Developer 环境中直接生成 JAXB 绑定类。要想生成相应的 Java 类,进入 J2EE 透视图,右键单击模式文档并选择 Generate > Java,见图 5。

图 5. 生成 Java 类
生成 Java 类
生成 Java 类

在后面的向导中,选择 JAXB Schema to Java Bean 并单击 Next。"XSD to Java" 屏幕出现,见图 6。

图 6. XSD to Java 向导
XSD to Java 向导
XSD to Java 向导

这个面板提供以下选项:

  1. Generate schema library 允许定制模式文件到项目的映射(在下一个面板上)。
  2. Target Java Container 列表中,指定用于包含生成的 Java bean 的项目或文件夹。
  3. Target Package 框中,输入 Java 包的名称或接受默认值。
  4. 一个重要的选项是 Binding Files,它定制 XML 模式组件与其 Java 表示之间的默认映射。在 Binding Files 面板上单击 Add,就可以选择外部绑定声明文件。根据约定,绑定文件的扩展名是 .xjb,可以使用任何文本编辑器创建。后面讨论这些文件的格式和内容。
  5. 单击 Finish

JAXB 绑定编译器把 XML 模式转换为一组相应的 Java 类,它们与模式中描述的结构匹配。它们包含 JAXB 运行时环境解析和重建原 XML 表示所需的所有信息。这显著简化了数据编程模型,让开发人员可以简便地执行对象实例化以及使用 getter 和 setter 方法。不需要通过编写代码在 XML 格式和 Java 应用程序之间进行数据转换。

通过考察 “Case” 类型详细看看这个映射,见图 7。

图 7. Case XSD 类型
Case XSD 类型
Case XSD 类型
清单 1. Case XSD 源代码
  <xsd:complexType name="Case">
    <xsd:sequence>
    	<xsd:element maxOccurs="1" minOccurs="1" name="Number"
    		type="xsd:string" />
    	<xsd:element maxOccurs="1" minOccurs="1" name="Amount"
    		type="xsd:decimal" />
    	<xsd:element maxOccurs="1" minOccurs="1" name="Created"
    		type="xsd:date" />
    	<xsd:element maxOccurs="1" minOccurs="1" name="Sequence"
    		type="xsd:integer" />
    	<xsd:element name="Status">
    		<xsd:simpleType>
    			<xsd:restriction base="xsd:string">
    				<xsd:enumeration value="Open" />
    				<xsd:enumeration value="Closed" />
    				<xsd:enumeration value="Forwarded" />
    			</xsd:restriction>
    		</xsd:simpleType>
    	</xsd:element>
    	<xsd:element name="Product">
    		<xsd:simpleType>
    			<xsd:restriction base="xsd:string">
    				<xsd:enumeration value="IncomeTax" />
    				<xsd:enumeration value="BusinessTax" />
    			</xsd:restriction>
    		</xsd:simpleType>
    	</xsd:element>
            <xsd:element name="Approved" type="xsd:boolean"></xsd:element>
            <xsd:element maxOccurs="1" minOccurs="1" name="Taxation"
    		type="bo:Taxation" />
    	<xsd:element maxOccurs="1" minOccurs="0" name="Objection"
    		type="bo:Objection" />
    	<xsd:element maxOccurs="unbounded" minOccurs="0" name="History"
    		type="bo:Reference" />
    </xsd:sequence>
    </xsd:complexType>

从模式到 Java 类的 默认数据类型绑定 基本上符合预期。清单 1 中的模式生成清单 2 所示的 Java 代码(为了便于阅读,删除了注解)。

清单 2. Case Java 类
publicclass Case {
    protected String number;
    protected BigDecimal amount;
    protected XMLGregorianCalendar created;
    protected BigInteger sequence;
    protected String status;
    protected String product;
    protected boolean approved;
    protected Taxation taxation;
    protected Objection objection;
    protected List<Reference> history;

// Getter and Setter
…
}

大多数类型映射到同名的 Java 简单类型。明显的例外是小数和整数类型。它们与未指定小数范围和总位数的数字匹配,最好的 Java 匹配分别是 java.math.BigDecimalKjava.math.BigIntegerjavax.xml.datatype.XMLGregorianCalendar

尽管一些处理金额的应用程序需要很高的精度,但是您可能希望使用简单的整数序列,这更便于在代码中处理。最容易的修改方法是使用内置的 xsd:int 类型修改 XSD 类型,或者引入具有适当限制的新的 xsd:simpleType

常见的其他需求包括覆盖默认的绑定规则(由于包名等命名需求或命名冲突)、覆盖默认的映射 enums 以使它们类型安全或者添加文档。一种实现方法是 内联定制

但是,这些方法都要求您能够直接控制模式。如果不是这种情况,应该使用外部定制。

ObjectFactory 可以方便地创建组成树中子元素的对象。根据定义,新创建的 JAXB 对象是 “浅对象”;也就是说,如果它的树结构中包含复杂的类型,那么并不创建这些对象,它们仅仅是 null。元素对象工厂的方法或 Java 类的构造器都不提供标准的树构建算法。这在构造树的方式方面提供了灵活性,但是也意味着由您负责构造树。我们将在 创建子树 一节中详细讨论这个主题。

外部绑定定制

如果无法直接控制 XSD 文件、要处理的模式文档很大或者只是为了清晰起见想分为两个文件,那么可以把绑定规则放在单独的外部文件中。绑定定制文件也是 XML 模式文档,扩展名通常是 .xjb。定制文件的一般语法见清单 3。

清单 3. 绑定文件语法
<jaxb:bindings schemaLocation = "xsd:anyURI">
   <jaxb:bindings schemaLocation=”file.xsd” node="xsd:string">
      <binding declaration>
   <jaxb:bindings>
</jaxb:bindings>

其中:

  • schemaLocation 引用远程模式。
  • node 是一个 XPath 1.0 表达式,它指定与给定的绑定声明相关联的模式节点(对 XPath 的详细说明参见 XML Path Language)。
  • <binding declaration> 控制如何生成 JAXB 数据绑定工件。

绑定声明与一个范围相关联,见图 8。

图 8. 绑定范围
绑定范围
绑定范围

详细讨论参见 Customization syntax。假设希望把生成的类的名称由 “Case” 改为 “Special Case”,见清单 4。

清单 4. 定制示例
<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
               jaxb:version="2.0">
  <jaxb:bindings schemaLocation="Businessitems.xsd" node="/xsd:schema">
    <jaxb:bindings node="xsd:complexType[@name='Case']">        <jaxb:class name="SpecialCase" />
    </jaxb:bindings>
  </jaxb:bindings>
</jaxb:bindings>

现在,使用 types.xjb 绑定文件消除比较怪的 Java 类型,这很简单,见清单 5。

清单 5. 类型定制
<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
               jaxb:version="2.0">
  <jaxb:bindings schemaLocation="Businessitems.xsd" node="/xsd:schema">
    <jaxb:globalBindings>
  <jaxb:javaType name="java.lang.Float" xmlType="xsd:decimal" /><jaxb:javaType name="java.lang.Integer" xmlType="xsd:integer" /><jaxb:javaType name="java.util.Date" xmlType="xsd:date" />
    </jaxb:globalBindings>
  </jaxb:bindings>
</jaxb:bindings>

同样,可以把 XSD 枚举转换为类型安全的 Java enum 类型,见清单 6。

清单 6. 枚举定制
<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
               jaxb:version="2.0">
  <jaxb:bindings schemaLocation="Businessitems.xsd">
    <jaxb:bindings node="//xsd:element[@name='Status']/xsd:simpleType">
        <jaxb:typesafeEnumClass name="Status"/>
    </jaxb:bindings>
    <jaxb:bindings node="//xsd:element[@name='Product']/xsd:simpleType">
        <jaxb:typesafeEnumClass name="Product"/>
    </jaxb:bindings>
  </jaxb:bindings>
</jaxb:bindings>

用绑定文件 enums.xjbtypes.xjb 重新生成 Java 类,见图 9。

图 9. XSD to Java 向导和绑定文件
XSD to Java 向导和绑定文件
XSD to Java 向导和绑定文件

这会创建包含定制类型的 Java 类和类型安全的 enum Java 类,见清单 7。

清单 7. 定制的 Java 类
publicclass Case
{
    protected String number;
    protected Float amount;
    protected Date created;
    protected Integer sequence;
    protected Case.Status status;
    protected Case.Product product;
    protectedboolean approved;
…
    publicenum Status
{
        OPEN("Open"),
        CLOSED("Closed"),
        FORWARDED("Forwarded");
        privatefinal String value;
…
}

注解

生成的类包含特殊的 JAXB 注解,它们向运行时框架提供处理相应 XML 文档所需的映射。反向映射以及覆盖 Java 到 XML 模式映射的默认绑定规则也需要注解。可以使用 javax.xml.bind.annotation 包中的所有注解。

关键的注解是 @XmlRootElement,它把顶级类映射到 Web 服务的 WSDL 使用的 XML 模式中的全局元素。但是,有时候 JAXB 编译器在猜测哪些类应该有这个注解时似乎过分聪明了。如果发生这种情况,就会显示下面的运行时错误消息:

Unable to marshal type "gov.mof.tax.businessitems.Case" as an element because it 
is missing an @XmlRootElement annotation.

因此,问题是:why does JAXB put @XmlRootElement sometimes, but not always?(为什么 JAXB 有时候放 @XmlRootElement 注解,有时候不放?) 这个帖子很有意思,但是仍然没有得到适当的解答。这个问题很烦人,因为可能经常要生成 Java 类,需要进行手工编辑以适当解决此问题。同样,可以使用外部绑定文件来解决,方法参见 How to make XJC generate XmlRootElement with an external binding file

多态的行为

我们的 XSD 使用了一个有意思的 XML 模式特性 —— 继承。文章 Polymorphic Web services 详细介绍了这个特性,解释了 XML 扩展和动态服务调用技术如何提供多态性。

我们在 JAXB 的上下文中考虑一下图 10 所示的模式。

图 10. XML 模式中的继承
XML 模式中的继承
XML 模式中的继承

BaseNotification 分别派生出 TaxNotificationPaymentNotification。它们继承基类型的所有属性和元素,并添加自己的新属性,见清单 8。

清单 8. XML 模式中的继承
<xsd:complexType abstract="true" name="BaseNotification">
		…
</xsd:complexType>
	
<xsd:complexType name="TaxNotification">
  <xsd:complexContent>
    <xsd:extension base="bo:BaseNotification">
    </xsd:extension>
  </xsd:complexContent>
</xsd:complexType>

<xsd:complexType name="PaymentNotification">
  <xsd:complexContent>
    <xsd:extension base="bo:BaseNotification">
...

JAXB 运行时通过内省注册所需的所有 Java 类。现在,如果在运行时作为基类型传递派生的子类,那么无法通过检查接口参数类型识别出子类。

为了利用继承和多态性,在 JAXB 2.1 和 Java 6.0 中引入了一个新注解 javax.xml.bind.annotation.XmlSeeAlso@XmlSeeAlso 通过引用在 JAXBContext 中添加的额外派生子类类以及根据接口参数识别出的类实现多态性,见清单 9。

清单 9. 包含继承的 Java 类
@XmlSeeAlso({
    PaymentNotification.class,
    TaxNotification.class
})
publicabstractclass BaseNotification {
   ...

运行时

完成基本设置之后,我们来看看如何让 JAXB 起作用。我们需要一个基本的业务流程,以便演示如何处理人工任务,见图 11。

图 11. 简单业务流程
简单业务流程
简单业务流程

把这个业务流程导入 WebSphere Integration Developer 并部署到 WebSphere Process Server 中。可以在本文提供的 sample_WID_project.zip 文件中找到 "TaxClient"。TaxClient 是一个 Web 项目,包含简单的用户界面,见图 12。

图 12. 基本用户界面
基本用户界面
基本用户界面

设置端点

首先,需要配置 Java Web 服务代理使用的 Web 服务端点。例如,JSP 中使用的一个 JavaBean 可以表示代理设置,可以在整个项目中共享它,见清单 10。

清单 10. 设置 Web 代理端点
<%
  String bfmEndpoint = ServicesBean.getInstance().getBfmEndpoint();
  String htmEndpoint = ServicesBean.getInstance().getHtmEndpoint();
  
  if( request.getParameter( "newBFMEndpoint" ) != null )
  {
      bfmEndpoint = request.getParameter( "newBFMEndpoint" );
      ServicesBean.getInstance().setBfmEndpoint( bfmEndpoint );
  }
  if( request.getParameter( "newHTMEndpoint" ) != null )
  {
      htmEndpoint = request.getParameter( "newHTMEndpoint" );
      ServicesBean.getInstance().setHtmEndpoint( htmEndpoint );
  }
 %>

然后,创建实际的运行时代理并保存在 Java Bean 中。在 Business Process Choreographer Explorer 中创建几个业务流程实例之后,现在可以接收并处理与人工任务相关的数据。

接收源数据

JAXBContext 类管理所有后续操作所需的绑定关系,这是 Java 应用程序的入口点。使用清单 11 所示的代码获取这个类的新实例。

清单 11. 创建 JAXB 上下文
JAXBContext jaxbContext	=
JAXBContext.newInstance( "gov.mof.tax.businessitems" );

这个调用初始化 JAXBContext 对象以管理运行时调用。传递的参数包含以冒号 (:) 分隔的 Java 包名列表(这些包包含从模式派生的类)。但是,JAXBContext.newInstance 调用的开销很大。因为根据设计它是线程安全的,创建它之后可以把它缓存在一个公共静态最终变量中,或者包装在单实例模式中以供以后访问。

现在要创建另一个主要运行时工件,反编组器。可以通过它把 XML 数据直接转换为 Java 内容对象树,见图 13。

图 13. 反编组器的作用
反编组器的作用
反编组器的作用

如果通过 @XmlSeeAlso 注解标为实例文档的根,就可以对任何全局 XML 元素进行反编组。清单 12 中的代码片段说明如何创建反编组器。如果在 contextPath 中列出,还可以跨一组模式合并全局元素。

清单 12. 创建反编组器
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

反编组器可以处理来自各种数据源的 XML 数据,包括文件、输入流、URL、DOM 对象、SAX 解析器等。在我们的示例中,数据源是通过对 WebSphere Process Server Human Task Manager (HTM) 代理执行的 claim() 调用返回的,见清单 13。

清单 13. 对数据源执行反编组
Claim claim = new Claim( );
claim.setTkiid( tkiid ); // returned via FacesContext Request Parameter
ClaimResponse response = htmProxy.claim( claim );
SOAPElement element = response.get_any();

// Traverse to SDO node
Iterator<SOAPElement> iter = element.getChildElements();
if( iter.hasNext())
{
  SOAPElement operation = iter.next();
  Class<?>	inputClass = … // via introspection from JAX-WS WSDL
  unmarshaller.setValidating( true );
  JAXBElement<?> jaxbObject = unmarshaller.unmarshal( operation,
	inputClass );
  inputObject = jaxbObject.getValue();
  if( inputObject instanceof inputClass )
  {
    ...
  }
  Iterator<SOAPElement> iterDetails = operation.getChildElements( );
  while( iterDetails.hasNext())
  {
    SOAPElement	detail = iterDetails.next();
    System.out.println( "Input " + detail.getLocalName());
    ...
  } // while
} // if

WebSphere Process Server HTM API 通过 claim() 调用返回一个人工任务的当前输入数据。这个调用的输入是一个引用特定人工任务实例的 tkiid(任务实例 ID),它返回输入数据。在我们的示例中,当用户从任务列表中选择一项时,作为隐藏的字段存储 tkiid,然后由客户机返回它。

因为返回的 SOAPElement 在底层具有对象层次结构,所以必须递归地处理树以获得实际的数据对象。通过对 JAX-WS WSDL 工件的内省,解析预期的 inputClass。为了安全起见,检查返回的对象的实例类型。

这段代码说明了 JAXB 是多么优雅、强大:一旦准备好所有工件,就很容易接收并反编组任何 XML 模式/Java 类型,不需要了解模式映射的底层机制。

检验源数据

JAXB 的有用特性之一是,在反编组操作过程中可以根据相关联的模式检验源数据。

清单 14. 检验源数据
unmarshaller.setValidating( true );

清单 14 中的这一行代码让 JAXB 运行时在数据不符合模式的情况下发出报告。规范在这个方面不明确:它要求实现报告检验错误,但是并不强制停止数据处理。这意味着没有严格地定义 JAXB 实现处理无效 XML 文档的方式。

根据常理,实现必须能够反编组有效的文档。注意,如果启用了检验,就会检查 XML 模式 facets,它限制基本数据类型。

创建子树

一旦得到任意复杂度的树的根对象,就可以开始使用它了。但是正如前面指出的,如果这是第一次调用,或者以前没有设置值,树可能还没有完全构造好。

在内存中 “预构建” Java 对象的一种方法是通过反射在树中移动,递归地解析包含的复杂类型。如果模式检验没有提供默认值,还可以设置默认值,见清单 15。

清单 15. 构建 Java 对象
public void buildDataObject( Object dataObject)
{
  …
  Method[] methods = dataObject.getClass( ).getMethods();
  for( Method method : methods )
  {
    String methodName = method.getName();
    // Setters start with "set" and have one param
    if( methodName.startsWith( "set" ))
    {
      Class[] parameters = method.getParameterTypes();
      // Check if the value is already set through the getter, (boolean ‘is')
      byte[] byteArray	= methodName.getBytes();
      String getterName	= new String( byteArray );

      if( parameters[0] == java.lang.Boolean.class ||
      parameters[0] == java.lang.Boolean.TYPE )
      {
        getterName = methodName.replaceFirst( "set", "is" );
      }
      else // turn 'setter' into 'getter'
      {
        byteArray[0] = 'g';
        getterName   = new String( byteArray );
      }
      Method getter = dataObject.getClass().getMethod( getterName, new Class[]{} );
      Object result = getter.invoke( dataObject, new Object[]{} );
      if( parameters.length == 1 )
      {
        Class parameter = parameters[0];
        if( result != null )
        {
          // If the value is already set, skip
          if( parameter == java.lang.Integer.class	||
            …
              parameter == java.lang.Enum.class	)
          {
            continue;
          }
          // If this is a complex type, recurse
          buildDataObject( result );
          continue;
        }
...

这个方便的方法从树根开始一直处理到树叶,构造并填充树。这是一种相当常见的模式。也可能有由其他系统提供的预先构建的部分树。很容易使用清单 15 所示的方法添加它们。

动态地显示模式

我们已经讨论了必须适应经常变化的 XSD,看到了 JAXB 如何帮助满足这一需求。另外,开发功能大致相同的动态用户界面也有意义:通过反射遍历 Java 对象,动态地创建代表相应输入框的 HTML 代码。

然后,可以收集用户输入,把输入发送回 WebSphere Process Server。我们先考虑清单 16 中的代码片段,它说明如何构建这样的动态呈现器。

为了了解服务的输入和输出数据类型,要引用 WSDL。一种方法是创建一个 Java 客户机,然后对创建的 Java 类进行内省。为此,找到 WSDL 并创建客户机,见图 14。

图 14. 创建 WSDL 客户机
创建 WSDL 客户机
创建 WSDL 客户机

清单 16 中的代码片段说明如何读取元数据。

清单 16. 读取 WSDL 元数据
…
// Retrieve Helper for input
// The standard 'getTypeDesc()' method provides the WSDL metadata

Class<?> helper = Class.forName( wsdlPackage + "." + operationName + "_Helper");
Method	  method = helper.getMethod( "getTypeDesc", new Class[]{} );
TypeDesc typeDesc = (TypeDesc)method.invoke( null, new Object[]{} );
FieldDesc[] fieldDesc  =  typeDesc.getFields();

for( FieldDesc field : fieldDesc )
{
…
inputName	= field.getXmlName().getLocalPart();
inputClass	= Class.forName( inputPackage + "." + field.getXmlType().getLocalPart());
…
}
// Retrieve Helper for output (Response)
...

参见 sample_WID_project.zip 文件中提供的 TaxClient 项目的 gov.mof.tax.bean 包。

清单 17. 创建表单 (1)
protected void createFormFromDataObject( Object	dataObject,
						boolean	disabled )
{
  // Custom logic to check if data objects have equal types, etc
  … 
  delegate.beginRendering(  ); 
  createForm( dataObject, null, "", htmlString, disabled);
}

用一个数据对象调用清单 17 中的主要方法 createFormFromDataObject,这个数据对象代表人工任务的输入和输出。在真实的场景中,比较复杂的人工任务可能使用不同的类型。为了处理所有类型的人工任务,需要支持数量不限的输入和输出对象。

另外,这个方法可以通过定制的逻辑检查是否可以使用来自输入对象的数据预填充输出对象。如果您愿意多下功夫,这个方法可以相当复杂。然后,这个方法调用 createForm,后者通过反射遍历树以执行实际工作。如果找到子树,递归地调用其本身,见清单 18。

清单 18. 创建表单 (2)
private void createForm( Object		dataObject,
                         String		title,
                         String		methodName,
                         StringBuffer	htmlString,
                         boolean		disabled )
{
  …
  Method[] methods = dataObject.getClass( ).getMethods();
  for( Method method : methods )
  {
    methodName = method.getName();
    Class[]	parameters	= method.getParameterTypes();
    String	label;
    // Getters start with "get" or "is" (booleans) and have no param
    if( parameters.length == 0 &&
       (methodName.startsWith( "get") || methodName.startsWith( "is" )))
    {
      …
      // Need setter when the values are retrieved from the form
      byte[] byteArray	= methodName.getBytes();
      byteArray[0]		= 's';
      methodName		= new String( byteArray );

      Object result = method.invoke( dataObject, new Object[0] );
      if( result instanceof java.lang.Integer  ||
         …
          result instanceof java.lang.Enum ) 
      {
        delegate.addField( label, result, currentSDOName, methodName, disabled );
      }
      else // if nested, recurse
      {
        createForm( result, label, methodName, htmlString, disabled );
      }
    } // if
  } // for

人工任务可能处于不同的状态。如果状态不是 “claimed”,可以把字段呈现为禁用的。一旦用户声明了人工任务,就启用字段,用户可以开始输入数据。

为了简化创建标记语言的过程,Delegate 类负责生成实际的 HTML 代码。这意味着 Delegate 将封装生成用户界面的逻辑,而上面的 “解释器” 将保持稳定。Delegate 维护元素的 Map,当需要呈现树中新的 Java 类或字段时,调用这个 Map,见清单 19。

清单 19. 委托 HMTL 创建
public class Delegate
{
  private	String	processTemplate;
  private	Map<String, StringBuffer>	fieldMap;

  …
  protected void	beginRendering( )
  {
    fieldMap = new TreeMap( );
  }

  protected void	addField(	String	fieldName,
					Object	fieldValue,
					String	sdoName,
					String	methodName,
					boolean	disabled )
  {
    StringBuffer htmlBuffer = new StringBuffer( "" );
    if( fieldValue instanceof java.lang.Integer	||
        fieldValue instanceof java.lang.Float	||
        fieldValue instanceof java.lang.String )
    {
      htmlBuffer.append( "<tr><td>" + fieldName + ":</td><td>” +
       "<input name='" + sdoName + "." + methodName +
       "' size='20' value='" + fieldValue + "'" + (disabled? " disabled" : "") + 
       "/></td></tr>\n" );
    }
    else if( fieldValue instanceof java.lang.Boolean )
    {
      …
    }
    else if( fieldValue instanceof java.lang.Enum ) 
    {
      htmlBuffer.append( "<tr><td>" + fieldName +
        ": </td><td><select name='" + sdoName + "." + 
            methodName + "' " +
         (disabled? " disabled" : "" ) + " >\n" );
      Object[] enumConstants = fieldValue.getClass().getEnumConstants();
      for( Object enumConstant : enumConstants )
      {
        Object enumValue = null;
        Method valueMethod = enumConstant.getClass().getMethod( "value", 
         new Class[]{} );
        enumValue = valueMethod.invoke( enumConstant, new Object[]{} );
        htmlBuffer.append( "<option value='" + enumValue + "'>" + 
         enumValue + "</option>\n" );
      }
      htmlBuffer.append( "</select></td></tr>\n" );
    }
    StringBuffer	sb;
    if(( sb = fieldMap.get( sdoName )) !=  null )
    {
      sb.append( htmlBuffer );
    }
    else
    {
      fieldMap.put( sdoName, htmlBuffer );
    }
  }

代码很简单,它为表单中的字段创建 HTML 输出。但是,重要的是表单元素的 fieldNames。当把输入发送回 WebSphere Process Server Human Task Manager 时,需要通过它们收集用户数据。

然后,在 JSP 或 JSF 中呈现最终的标记,见清单 20。

清单 20. 在 JSP 中呈现 HMTL 代码
<jsp:useBean id="td" scope="session" class="gov.mof.tax.bean.ToDoTaskBean" /> 
</head>

<body bgcolor="#efefef">
  <div class="formblock">
    <form>
      <% td.createInputForm(); %>
      <% out.print( td.getHtmlString() ); %>
      <h2>Possible Activities:</h2>
      <% if( td.getCompleteSuccessful() ) { %>
        <input id="close" type="submit" name="close" value="Close Window" />
      <% } else { %>
        <input id="claim" type="submit" name="claim" value="Claim"
                <%=(td.getClaimButton() == true) ? "" : "disabled"%> />
        <input id="cancel" type="submit" name="cancel" value="Cancel"
                <%=(td.getClaimButton() == false) ? "" : "disabled"%> />
        <input id="complete" type="submit" name="complete"
value="Complete" <%=(td.getClaimButton() == false) ? "" : "disabled"%> />
        <input id="tkiid" type="hidden" name="tkiid" value="<%=td.getTkiid()%>" />
      <% } %>
    </form>
  </div>
</body>

结果见图 15。

图 15. 动态的前端
动态的前端
动态的前端

字段的类型符合 XML 模式。Created 呈现为日期框,由右边的图标表示。在定制文件的帮助下,模式枚举(比如 StatusProduct)映射到 Java enums,代码可以把它们呈现为下拉菜单。Approved 属性映射到 Java Boolean,呈现为复选框。

如果使用相当复杂的 XSD,所有字段会显示在一个相当混乱的表单上。使用这样的表单很困难,很容易引起混淆。因此,我们使用 Armel Pingault 的 apTabs JavaScript 库,它允许把表单分割为多个选项卡。

为了发送回数据,必须捕捉用户输入并调用完成方法。因此,我们提供所有字段的完全限定名称,这些名称与相应的 XPath 地址相似,见清单 21(为了便于阅读,增加了缩进)。

清单 21. 显示并捕捉输入数据的 HTML 代码
...
<table border=0 bgcolor='#f2f5ff' width='98%' align='center'>
  <tr>
    <td>FirstName:</td>
    <td>
      <input name='.setObjection.setPrepared.setFirstName'
             size='20' value='Fred'/>
    </td>
  </tr>
  <tr>
    <td>Lastname:</td>
    <td>
      <input name='.setObjection.setPrepared.setLastname'
             size='20' value='Flintstone'/>
    </td>
  </tr>
...

表单底部的按钮调用相应的 WebSphere Process Server 生命周期方法。ClaimCancel 按钮的是否启用反映人工任务的当前状态,它们也决定所有 UI 元素是否启用。在这两个状态之间来回切换可以测试基本的客户机功能。

组合用户输入

现在,只需获取用户输入并将其发送回 WebSphere Process Server。完成人工任务的主要方法名为 complete() 或其变体之一。

Complete 按钮调用 getDataFromForm 方法,它采用相同的模式:调用执行实际工作的 getDatagetData 在树中移动,如果需要,递归地调用其本身,见清单 22。

清单 22. 获取用户输入
protected void getDataFromForm(	Object	dataObject,
	Map<String, String[]> map)
{
  // Traverse through request parameter
  for( String key : map.keySet())
  {
    String mapValue = map.get(key)[0];
    formsBean.getData( dataObject, key, mapValue );
  }
}

private void getData(	Object	dataObject,
				String	key,
				String	value )
{
  …
  // Parse the key to get dataObject field
  String[]	array = key.split( "\\.");
  for( int index = 0; index < array.length; index++ )
  {
    String element = array[index];
    if( element.startsWith( "set" ) || element.startsWith( "is" ))
    {
      Method[] methods = dataObject.getClass().getMethods();
      for( Method method : methods )
      {
        Class[]	parameters	= method.getParameterTypes();
        if( parameters.length == 1 && method.getName().equals( element ))
        {
          Class parameter = parameters[0];
          if( parameter == java.lang.Integer.class )
          {
            method.invoke( dataObject, Integer.valueOf( value ));
          }
          else if( parameter == java.lang.Integer.TYPE )
             …
          }
          else if( parameter.isEnum())
          {
            // Create an instance of an enum with 'fromValue' method
            Method enumMethod = parameter.getMethod( "fromValue", String.class );
            Object newEnum = enumMethod.invoke( parameter, value );
            // Call setter to set the new Enum
            method.invoke( dataObject, newEnum );
          }
          else // object is already built, get the pointer and recurse
          {
            byte[] byteArray	= element.getBytes();
            byteArray[0]		= 'g';
            String methodName	= new String( byteArray );
            Method getter		= dataObject.getClass().getMethod( methodName, 
             new Class[]{} );
            Object newDataObject = getter.invoke( dataObject, new Object[]{} );

            if( parameter != Enum.class ) // Complex types: recurse
            {
              String newArray		= "";
              for( int i = 1; i < array.length; i++ )
              {
                newArray += array[i] + ".";
              }
              getData( newDataObject, newArray, value );
            }
          }
          break;
        }
      } // for
    } // if
  } // for
}

发送用户输入

数据树现在反映用户输入,可以发送它了,见图 16。

图 16. 编组用户数据
编组用户数据
编组用户数据

JAXB 也使编组过程更容易了。创建要传递给 WebSphere Process Server Web 服务 API 的对象,然后创建一个响应 soapMessage。然后把它传递给 编组器。通过 JAX-WS 创建的运行时工件和反射获取 WSDL 元数据。

然后在 Faces 请求参数映射中移动,找到实际的用户数据,把每个元素填充到 soapMessage 中,见清单 23。

清单 23. 编组用户数据
...
CompleteWithOutput completeWithOutput = new CompleteWithOutput();
SOAPFactory soapFactory = SOAPFactory.newInstance();
SOAPElement soapMessage = soapFactory.createElement( operationName + 
       "Response", "nsprefix", wsdlNamespace );
SOAPElement topElement	= soapFactory.createElement( outputName );
soapMessage.addChildElement( topElement );

// Get http parameters from Faces Context
Map<String, String[]> map = FacesContext.getCurrentInstance().
		getExternalContext().getRequestParameterValuesMap();

getDataFromForm( inputDataObject, map );
marshaller.marshal( inputDataObject, topElement );

completeWithOutput.set_any( soapMessage );
completeWithOutput.setTkiid( tkiid );
CompleteWithOutputResponse response = htmProxy.completeWithOutput
(completeWithOutput);

这就完成了人工任务。

结束语

本文介绍了 JAXB 的基本概念以及它们如何与 WebSphere Process Server Web 服务 API 结合使用。在线路上发送的是 XML 文档,而在客户机环境中使用的是 Java 类,JAXB 以简洁优雅的方式在这两者之间建立了桥梁。

讨论的重点是 JAXB 开发和运行时环境如何简化 XSD 到 Java 映射过程,以及如何使用运行时特性开发简单的用户界面,即通过 JAXB 运行时和 Java 反射在运行时动态地生成用户界面。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere
ArticleID=499140
ArticleTitle=通过 Web 服务 API 和 JAXB 编组与 WebSphere Process Server 交互
publish-date=07052010