摘要 数据转换总是难于实现的.XSLT (Extensible Stylesheet Language Transformations) 规范一个极好XML转换工具,但是它仅能处理输入数据是XML数据的情况, 而对于复杂转换经常是困难的,甚至是不可能的. 本文阐述了在将XML数据提供给XSLT转换器之前如何使用SAX对其进行”预处理 “. 预处理器能够处理大多数复杂的任务,从而为XSLT器减少了不少负担.本文附有几个功能齐全的例子代码 (可以从此处下载). 例子中使用的实施框架很容应用到许多不同的转换解决方案中. (5,000 字; 2005年9月5日)
在一个需要将原始的帐单数据转换为不同的帐单布局展现的项目中, 我曾经被要求做简单的数据转换.看了问题的简介后,我建议使用XSLT (Extensible Stylesheet Language Transformations).
我进一步研究需求后发现问题没有我预想的那么简单.虽然输入数据是易于管理的, 需要做简单转换的数据不能用一组静态的XSLT样式表单描述.一部分需要转换的数据是动态的,而且存储在两个分离的数据库中. 此外,为了呈现帐单的布局程序不得不对从两个数据库中取出的输入数据做相对复杂的计算. XSLT解决方案被淡忘了.
这个例子的核心问题是动态数据需要直接转换. 在理想情况下,我们从来不会面对这个问题. 输入数据的准备和 输入数珠的转换应该是清晰的分离开来的,从而所有需要转换的数据能够包含在一个单独的XSLT模板中. 不幸的是,现实项目的需求有时候是相当的奇异.
本文给出了上面描述的问题的一个解决方案.我将通过例子来展示如何利用 SAX(Simple API for XML) 来提高XSLT的适用性. 另外我将展示在输入数据和期望的输出数据都不是XML时如何使用XSLT.
XSLT简介 XSLT 是一种转换XML数据的编程语言. XSLT 表单样式可以用来将XML文档另外一种XML格式的文档, 实际上可以转换成任意其它格式. 但是XSLT可能不是一种简单易学的语言, 尤其对于那些对类似JAVA语言比较熟悉的人来说,它以一种强大而灵活的方式来完成相对复杂的数据转换.如果你对XSLT还不是很熟悉, 有很多优秀的教程可用,例如, the XML Bible 的第17章.
尽管XSLT是一种好的语言,但是用它来完成有些任务是困难的,甚至是不可能的. 对于必须对输入的XML文档的几个元素做合并计算的这种转换通常是可能行的, 但是通常极为难写. 如果需要转换的直接数据本身是动态的,那么单独使用XSLT就不够了. XSLT模板本质上是静态的.尽管可以动态的重新产生模板,我仍然不认为这是一个可行的解决方案.(如有不同观点请不吝赐教.)
试验了各种想法之后,我得出结论,用XSLT来完成复杂数据转换的最简单的办法是在将输入数据提交给XSLT转换器处理之前对输入数据进行巧妙的预处理. 这听起来很令人费解,而且效率不高, 但是结果证明使用SAX处理输入的XML数据是相当的简单容易.
SAX 是一种事件驱动的用来解析XML文档的接口. 当SAX解析器处理XML数据时,它产生解析器可以识别的XML元素的”回调(callback)”通知.例如,当解析器遇到XML开始标签时,它产生一个回调事件startElement.标签的名字和其它相关信息作为回调参数发送出去.当需要高效地界些XML时,应该使用SAX. 关于更多的SAX信息请看Sun's tutorial on JAXP. 本文中,在将数据提交给XSLT转换器之前我使用SAX来修改事件流.
SAX 和XSLT两者都包含在JAXP (Java API for XML Processing) API中, J2SE 1.4版以后的版本中已经包含这个API.
例子概要 这部分介绍文章中的例子和SAX与XSLT结合的可能性.
执行这些例子 如果你对执行这些例子不感兴趣,跳过这一部分. 但是本文严重依赖这些例子代码,因此,建议您最好看看这些代码.这些例子已经在Windows环境的J2SE 1.4.2 下测试过. 执行这些应用不需要引入其它API包. 但是本文假设您使用Ant 编译工具.如果您不愿意使用Ant, 那么你仍然可以编译并执行这些例子, 但是要额外多做一点点工作.
详细的描述放在README.txt文件中, 它可以从下载的szip文件中取得. 您只要解开这个压缩包并设置好相关变量(在README.txt中有说明),就可以使用下边的Ant 命令了:
·ant build:删除编译过程产生的文件,重新编译所有代码
·ant clean:删除编译过程产生的文件
·ant example1: 执行例子1
·ant example2: 执行例子2
·ant example2b: 执行例子2b (例子2的一个变种)
·ant example3: 执行例子3
·ant example4: 执行例子4
例子1概要 尽管例子1是一个基本的XSLT转换器, 但是介绍这个例子是有必要的, 因为他它是后续例子被编译的基础. 图 1 展现了例子1的概念视图.
图 1. 例子一个的概念视图.点击看全图. 输入数据(1.1) 为私有XML数据, 它模仿了客户订单报告. 数据大致如下:
<?xml version="1.0"?><ORDER_INFO> <CUSTOMER GROUP="exclusive"> <ID>234</ID> <SERVICE_ORDERS> <ORDER> <PRODUCT_ID>1231</PRODUCT_ID> <PRICE>100</PRICE> <TIMESTAMP>2004-06-05:14:40:05</TIMESTAMP> </ORDER> <ORDER> <PRODUCT_ID>2001</PRODUCT_ID> <PRICE>20</PRICE> <TIMESTAMP>2004-06-12:15:00:44</TIMESTAMP> </ORDER> </SERVICE_ORDERS> </CUSTOMER>...
例子 1的完整输入数据在<EXAMPLE_ROOT>/input/orderInfo_1.1.xml文件中. 从此, <EXAMPLE_ROOT> 指代您解压本文例子的那个目录.
图1的XSLT 模板 (1.2)是一个将输入数据转换为HTML格式的常规XSLT 样式表单. XSLT模板大致如下:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="ORDER_INFO"/>
</xsl:template>
<xsl:template match="ORDER_INFO">
<HTML>
<HEAD>
<TITLE>Customers' Order information</TITLE>
</HEAD>
<BODY>
<H1>Customers' Order information</H1>
<xsl:apply-templates select="CUSTOMER"/>
<xsl:apply-templates select="PRICE_SUMMARY"/>
</BODY>
</HTML>
</xsl:template>...
完整的模板在<EXAMPLE_ROOT>/template/transform_1.2.xml文件中. 当您执行例子1时, 程序将输出文件写到<EXAMPLE_ROOT>/output/result_1.3.html文件中.
为了让读者熟悉XSLT, 例子1应该是一个很简单的好的编程练习.这个XSLT 模板仅仅包含格式化输出的必要数据,而不包含任何复杂计算. 我们继续看更有挑战的例子2.
例子2概要 例子2可能是本文最有趣的一个例子.这个例子实际上由两个不同的转换组成(转换2a和转换2b), 我们分别考虑这两部分,首先我们来看转换2a. 例子2的概念视图视图如图2所示.
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.7.25.xslt2.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
图 2. 例子2的概念视图. 点击看全图. 输入数据 (1.1) 和 XSLT模板(1.2) 与例子1的相应部分对应.在XSLT转换被应用之前,输入数据通过由一组JAVA类组成的预处理器,预处理器通过SAX的时间过滤特性来操作XML数据. 数据源(datasource)是一组实现了一种虚数据源的JAVA类. 在真实的应用程序中, 数据源可能是数据库接口. 引入这个虚数据库是为了以一种极为简单模式来展示从外部数据源取回的动态XML数据. 我希望让这例子尽可能的容易安装和执行, 因此没有实现任何真实的数据库连接――希望虚数据源实现可以给你这个概念.
在执行例子时,程序(如果mode参数设置为debug)回显来自预处理器的XML数据给标准输出流(System.out). 这些数据类似预处理器的输出数据, 现在它是转换器的输入数据.回显预处理器的输出数据是一种方便的调试预处理转换过程的方式. 本文稍后讨论这个特性的实现.
在执行转换2a时, 下面数据被回显到屏幕:
<ORDER_INFO>
<CUSTOMER GROUP="exclusive">
<ID> Jill </ID>
<SERVICE_ORDERS>
<ORDER>
<PRODUCT_ID>
Doohickey
</PRODUCT_ID>
<PRICE> 100 </PRICE>
</ORDER>...
如果您把这个数据和原始输入数据(<EXAMPLE_ROOT>/input/orderInfo_1.1.xml)比较一下, 将会注意到在数据的开始部分有如下不同:
·第一个CUSTOMER/ID元素的值从234变为Jill.
·第一个 CUSTOMER/SERVICE_ORDERS/ORDER/PRODUCT_ID元素的值从1231变为Doohickey.
·TIMESTAMP元素被去除掉了.
预处理器已经用从内部模拟数据库来的数据替换了CUSTOMER/ID和 CUSTOMER/SERVICE_ORDERS/ORDER/PRODUCT_ID元素的值. 同时它过滤掉了TIMESTAMP元素和它的值. 这些修改后的数据现在提供给XSLT转换器作为输入.
转换 2a的输出文件是<EXAMPLE_ROOT>/output/result_2.1.html.
在您执行转化2b时, 咋一看回显的屏幕的数据与转换2a的回显数据很相似. 不同在于预处理器在最后部分插入了PRICE_SUMMARY元素:
<PRICE_SUMMARY>
<PRODUCT>
<NAME>
Doohickey
</NAME>
<SUM> 110 </SUM>
</PRODUCT>
<PRODUCT>
<NAME> Nose Cleaner </NAME>
<SUM> 10 </SUM>
</PRODUCT>
<PRODUCT>
<NAME>
Raccoon </NAME>
<SUM> 40 </SUM>
</PRODUCT>
</PRICE_SUMMARY>
</ORDER_INFO>
这个例子的目的是用来证明预处理器也可以用来引入新的XML元素, 元素的值可以直接来自输入的XML数据的计算, 也可以使用一些来自于外部数据源的补充数据.
例子2b 的输出文件是 <EXAMPLE_ROOT>/output/result_2.1b.html.
这些例子为什么令人感兴趣?在概念层面上,这个数据转换方法似乎并没有什么创新. 令人感兴趣的是使用SAX对输入的XML数据做微小的动态的改善是相对容易实现的, 而这可是使本来使用纯XSLT无法实现的转换成为可能. 另一方面, 仅仅使用SAX来实现整个转换是有可能的,但是有点单调乏味. 在对复杂数据的实施转换时使用SAX和XSLT相结合几乎无所不能.
在"深入研究例子2"部分,我会讨论如何扩展带有预处理器的XSLT转换器的模式. 你将会看到,只要稍微修改一下本文的例子代码,它就可以有很多不同的用途.
例子3和例子4提升了例子2对非XML数据的读写能力. 如果你对这些不感兴趣,可以直接例子1和例子2的实施细节部分.
例子3概要 有时候, 输入数据来自几个不同的数据源;有时候,这些数据并非都时XML格式的数据.例子3说明了对于非XML数据如何使用SAX产生事件, 从而使应用XSLT成为可能. 例子3的概念视图如图3.
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.7.36.xslt3.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
图 3. 例子3的概念视图. 点击看全图. 例子3的输入数据大致如下:
3exclusive:2342Order:1231Price:100Timestamp:2004-06-05:14:40:05Order:2001Price:20Timestamp:2004-06-12:15:00:44...
例子3的完整输入文件是<EXAMPLE_ROOT>/input/orderInfoAsText_3.1.txt.
XML产生器读取这些数据并产生SAX事件,用以匹配前面例子(例子1和例子2)的XML输入文档. 从而预处理器接收到的数据与例子2的预处理器接收到的输入数据是相似的.转换的其余部分与转换2b类似. 结果输出文件(<EXAMPLE_ROOT>/input/result_3.2.html) is also similar to 与转换2b的输出文件 (file <EXAMPLE_ROOT>/output/result_2.1b.html)也是相似的.
例子4概要 XML并非总是我们希望得到的输出格式. 此例类似前例,但是它产生的是一个纯文本文件的输出而不是XML文件T. 见图4,它是个概念视图.
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.7.51.xslt4.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
图 4. 例子4的概念视图4. 点击可见完整尺寸的图片 输入数据跟例子3相同(<EXAMPLE_ROOT>/input/orderInfoAsText_3.1.txt), 而XSLT 模板不同. 既然读者现在已经熟悉XSLT, XSLT转换也可以用来产生非XML数据. XSLT 模板 (4.1) 是个用来将输入数据转换为文本根是的规则的XSLT 样式表单, 模板大致如下:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="ORDER_INFO"/>
</xsl:template>
<xsl:template match="ORDER_INFO">Customers' Order information
<xsl:apply-templates select="CUSTOMER"/>
<xsl:apply-templates select="PRICE_SUMMARY"/>
</xsl:template>
<xsl:template match="CUSTOMER">Customer id:<xsl:value-of select="ID"/>
Customer group is '<xsl:value-of select="@GROUP"/>'
<xsl:apply-templates select="SERVICE_ORDERS"/>
</xsl:template>...
完整的转换模板<EXAMPLE_ROOT>/template/transform_4.1.xml文件中. 当运行例子4的时候,程序将输出写在<EXAMPLE_ROOT>/output/result_4.2.txt中
除了使用XSLT模板以外,例子4的代码和例子3的代码是一样的.
代码查看
让我们看看代码的实现
Example 1
Example 1的Transformer实体由class Example1 ,ExampleTester和Example接口实现。Class ExampleTester解析输入参数,创建Example1的实例,Example1实现了Example接口,并且调用doTransform()方法。图例5是Example的类图。绿色框内的是J2SE的标准库。如StreamSource,StreamResult和javax.xml.transform.stream
Figure 5. Class diagram of Example 1. Click on thumbnail to view full-sized image. Example1 的doTransform() 方法 in Example1 比较有趣. 让我们仔细阅读:
1. public doTransform(String _inputFileName, String _transformerFileName, String _outputFileName) {2. try {3. initTransformer(_transformerFileName);4. } catch (TransformerConfigurationException tce) {5. // omitted for clarity6. } 7. Source myXMLSource = getInputSourceObject(_inputFileName);8. Result myResult = getResultObject(_outputFileName)9. try { 10. myTransformer.transform(myXMLSource, myResult); 11. } catch (TransformerException te) {12. // omitted for clarity 13. }14. }
doTransform() 方法有3个参数:
·_inputFileName: 读入xml数据的源文件名
·_transformerFileName: XSLT 文件名
·_outputFileName: 转换XML数据后输出的文件名
在第3行, 调用initTransformer() 方法来创建XSLT 转换器(transformer). 类 myXMLSource 是需要被转换的数据源. 在第7行, 调用getInputSource() 来创建myXMLSource 实例. 源数据转换后写入myResult 对象,在第8行, getResultObject() 方法根据传入的参数_outputFileName 来创建输出文件. 做好这些准备步骤后,转换工作在第10行完成:
myTransformer.transform(myXMLSource, myResult);
开始进入第2个例子前,考虑一下getInputSourceObject()方法创建的source实例:
Source myXMLSource = getInputSourceObject(_inputFileName);
这是 XSLT 转换器的数据源. 3个类实现了Source接口: StreamSource, SAXSource, and DOMSource. 这篇文章只用了 StreamSource 和 SAXSource (see Figure 6).
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.8.16.xslt6.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
Figure 6. XSLT sources. Click on thumbnail to view full-sized image.
Example 1中, getInputSourceObject() 方法返回一个 StreamSource对象. 数据来自文件 (<EXAMPLE_ROOT>/input/orderInfo_1.1.xml) 。
下个例子的XML数据依然来自文件,不同的是使用了SAX事件流处理。 实际上,Example 1的转换器接受事件流比接受文件流不会有多大麻烦。 然而在下一个例子中,整个思想基于捕获,操控SAX事件流的能力
A deeper look into Example 2
Example 2's 预处理的实现包含 (in Transformation 2a) 类 Example2 和包 myutil.里的类
类 Example2 继承了Example1. 该类重载了 getInputSourceObject()方法,声明的命令工厂的名字. 先仔细研究getInputSourceObject():
1. protected Source getInputSourceObject(String _pathName) throws FileNotFoundException {
2. InputSource inputSource = getInputSourceFromFile(_pathName);
3. XMLReader xmlFilter = getFilteringReader();
4. SAXSource saxSource = new SAXSource(xmlFilter, inputSource);
5. return saxSource;
6. }
第2行, getInputSourceFromFile() 方法从文件中创建InputSource 实例 ,第4行, SAXSource 实例被InputSource 和 XMLReader创建。 XMLReader 实例的创建值得关注,让我们进入getFilteringReader 方法看看:
1. private XMLReader getFilteringReader() {
2. XMLReader myReader = getReader();
3. XMLFilterImpl xmlFilter = new ModifyingXMLSource(myReader, FACTORY_NAME);
4. if ((MODE != null) && (MODE.equals("debug"))) {
5. xmlFilter = new XMLPrinter(xmlFilter);
6. }
7. return xmlFilter;8. }
第2行, XMLReader 被创建. 这是缺省的reader,它解析原始输入数据并发送SAX事件到内容处理器―― 内容处理器在这里就是XSLT转换器。第3行创建了 ModifyingXMLSource实例, 它是一个SAX事件过滤器的实现,是这篇文档的核心.第 5行,第2个事件过滤器,XMLPrinter被创建。 Figure 7 显示结果对象的结构。
Figure 7. Object diagram of Example 2. Click on thumbnail to view full-sized image.
SAXSource现在运行后,SAX事件先被ModifyingXMLSource 对象处理, 而后决定是否下一个事件过滤器XMLPrinter处理. 你会看到, ModifyingXMLSource 也可生成并转发新事件。XMLPrinter 转发所有事件到最终目标XSLT transformer. Figure 8 显示了这个class的结构。
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.8.37.xslt8.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
Figure 8. Class diagram of Example 2. Click on thumbnail to view full-sized image.
为了明白fillter的功能,让我们看看XMLPrinter 的代码, XMLPrinter打印XML 数据到标准输出流并且转发SAX事件到他的父类。他的结构如下:
public class XMLPrinter extends XMLFilterImpl {
private CharArrayWriter contents = new CharArrayWriter();
private String indent = "";
public XMLPrinter(XMLReader _reader)
{ super(_reader); };
public void startElement(String _uri,
String _localName,
String _qName,
Attributes _atts) throws SAXException
{ // Print the indentation // Print start tag // Increase the indent
super.startElement(_uri, _localName, _qName, _atts);
} public void characters(char[] _ch,
int _start,
int _length) throws SAXException {
contents.write(_ch, _start, _length);
super.characters(_ch, _start, _length); };
public void endElement(String _uri,
String _localName,
String _qName) throws SAXException {
XMLPrinter 继承了 XMLFilterImpl, XMLFilterImpl 提供了全部SAX事件回调方法的默认实现。 如果你不重载任何方法,事件流通过这个过滤器不会有任何改变。 XMLPrinter 包含方法 startElement(), characters(), 和 endElement(), 这些是打印XML文档基本内容到标准输出流的必需的方法。 当遇到XML元素的标签时, startElement()总是被调用(例如, <TIMESTAMP>). 遇到XML元素的内容时,characters() 方法被调用 (对如 TIMESTAMP 元素, 内容就是 2004-06-05:14:40:05). 遇到XML元素的结束标签时, endElement() 被调用。
例如,在XMLPrinter, 可打印的XML开始标签被构建和打印在startElement()方法内,注意在每个方法的结束时,父类的相应方法总是被调用。 因此,整个事件流保持不变。
对于ModifyingXMLSource, 象 XMLPrinter一样, 重载 了startElement(), characters(), and endElement()方法. 这个类使用回调方法startElement(), characters(), or endElement()处理接受到的SAX事件。它查找Command 对象来处理事件,大部分的逻辑处理在分开的command类里实现,这样可以保持 ModifyingXMLSource 小而简单。
让我们看看Commands 怎样和XML的元素映射. command-to-element 的映射在工厂类里完成。 创建 ModifyingXMLSource 时,初始化了一个 command 工厂. 在Example 2 (Transformation 2a), 这个工厂时Example2CommandFactory 类。
1. public Example2CommandFactory() {
2. CustomerIdCommand myCustomerIdCommand = new CustomerIdCommand();
3. commands.put("/ORDER_INFO/CUSTOMER/ID", myCustomerIdCommand);
4. FilterCommand myFilterCommand = new FilterCommand();
5. commands.put( "/ORDER_INFO/CUSTOMER/SERVICE_ORDERS/ORDER/TIMESTAMP", myFilterCommand);
6. ProductIdCommand myProductIdCommand = new ProductIdCommand();
7. commands.put( "/ORDER_INFO/CUSTOMER/SERVICE_ORDERS/ORDER/PRODUCT_ID", myProductIdCommand);
8. }
第3行, 元素Customer的子元素ID 映射到 CustomerIdCommand 处理,第5行 TIMESTAMP 元素映射到 FilterCommand, 第7行 PRODUCT_ID元素映射到 ProductIdCommand. 全部 command 类实现了Command 接口:
1. public interface Command {
2. public void reset();
3. public Object getResult();
4. public void startElement(String _uri,
String _localName,
String _qName,
Attributes _atts,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException;
5. public void characters(char[] ch,
int start,
int length,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException;
6. public void endElement(String _uri,
String _localName,
String _qName,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException;
7. }
Commands 可以用来收集和合并数据,所以重置command的状态显然是需要的。 The getResult() 可以 method被其他command调用,用于交换信息 。有了startElement(), characters(), and endElement() (Lines 4, 5, and 6), ModifyingXMLSource 可以很容易委托 SAX 事件给Command对象处理。
ModifyingXMLSource 通过工厂获得commands, 如他的 startElement()方法所示:
1. public void startElement(String _uri,
String _localName,
String _qName,
Attributes _atts) throws SAXException {
2. tagIdentifier += ("/" + _localName);
3. try {
4. currentCommand = factory.getCommand(tagIdentifier);
5. currentCommand.startElement(_uri, _localName, _qName, _atts, this, this);
6. } catch (SAXException sax) {...
characters() 和 endElement() 方法 遵循同一个原理, tagIdentifier() 跟踪XML事件流中元素,用于获取正确的Command。
让我们首先看看 FilterCommand 类, 当 tagIdentifier 值等于/ORDER_INFO/CUSTOMER/SERVICE_ORDERS/ORDER/TIMESTAMP, 工厂类factory 返回 FilterCommand对象. FilterCommand's startElement(), characters(), 和 endElement() 方法什么也没处理,NullCommand一样,<TIMESTAMP>元素随他的内容一起原样虑过
开始查看CustomerIdCommand 和 ProductIdCommand前, 看看Figure 9里的类的结构 (注意图中CustomerIdCommand 省略了).
<a href="http://www.chinaitlab.com/www/imgfiles/2005.11.12.19.8.49.xslt9.gif?http://www.xvna.com" target="_blank"> </a>
点击看大图
Figure 9. Class diagram of Example
2, continued. Click on thumbnail to view full-sized image.
CustomerIdCommand 和 ProductIdCommand commands 从datasource (参看 Figure 2)中接受数据.下列代码演示了怎样把ID映射成名字字符串 :
1. public void characters(char[] ch, int start, int length, XMLFilterImpl _caller, DefaultHandlerInterface _default) throws SAXException {
2. contents.write(ch, start, length);
3. };
4. public void endElement(String _uri,
String _localName,
String _qName,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException {
5. idString = contents.toString();
6. DataAccessorFactory myFactory =
DataAccessorFactory.getFactory();
7. DataAccessor myAccessor = myFactory.getDataAccessor();
8. idString = myAccessor.getProductName(idString);
9. _default.defaultCharactersHandler(idString.toCharArray(), 0, idString.length());
10. _default.defaultEndElementHandler(_uri, _localName, _qName);11. };
窍门在于characters() 方法没有立刻调用 defaultCharactersHandler, 因此原始数据没有被转发(forword)。 在endElement() 方法, defaultCharactersHandler() 和 defaultElementHandler() 被调用called. defaultCharactersHandler()调用时 is 传如了来自DataAccessor的改变的值called with a modified value received from DataAccessor. DataAccessor 接口interface如下:
1. package myutil.dataAccess;
2. public interface DataAccessor {
3. public String getCustomerName(String _customerId);
4. public String getProductName(String _productId);
5. }
getProductName()根据给定的参数返回产品名,在这个例子中时参数是1231。
CustomerIdCommand与 ProductIdCommand类似,不同点在于它调用的是getCustomerName()而不是getProductName()。
Example 2 的第2部分是Transformation 2b, 它添加小结信息到XML数据中 (PRICE_SUMMARY 元素). Example2b 如下:
1. public class Example2b extends Example2 {
2. public Example2b() {
3. super();
4. FACTORY_NAME = "Example2b";
5. }
6. }
正如你看到,他的优美的设计,在于只要继承Example2,赋给command工厂一个新的名字 。
在 CommandFactory's getInstance()方法中,工厂名 Example2b 映射到Example2bCommandFactory 对象。 Example2bCommandFactory 与Example2CommandFactory 类似。不同点在于Example2bCommandFactory'的构造函数包含如下新行:
1. PriceCollectorCommand myPriceCollectorCommand = new PriceCollectorCommand(myProductIdCommand);
2. commands.put("/ORDER_INFO/CUSTOMER/SERVICE_ORDERS/ORDER/PRICE", myPriceCollectorCommand);
3. commands.put("/ORDER_INFO", new PriceSummaryPrintingCommand(myPriceCollectorCommand));
PriceCollectorCommand从输入XML数据中收集全部 PRICE 元素。 PriceSummaryPrintingCommand 要从PriceCollectorCommand收集Price信息并输出成XML格式。所以,PriceSummaryPrintingCommand 引用PriceCollectorCommand对象在他的构造函数中 (第3行). 图 10 为 Example 2b的类图。
Figure 10. Class diagram of Example
2, Transformation 2b. Click on thumbnail to view full-sized image.
注意仅仅新的Command类显示在图10中。 实际上 Example2bCommandFactory与Example2CommandFactory一样, 有相同的Command 依赖。
让我们深入查看一个 Command 类. 下面是PriceCollectorCommand 的部分代码:
1. public void endElement(String _uri,
String _localName,
String _qName,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException {
2. String productIdAsKey = (String)myProductIdCommand.getResult();
3. String priceAsString = contents.toString();
4. Integer price = priceToNumber(priceAsString);
5. if (price != null) {
6. addPriceToHashMap(productIdAsKey, price);
7. }
8. _default.defaultEndElementHandler(_uri, _localName, _qName);
9. };
在endElement() 方法中,addPriceToHashMap() 方法存入了每种产品的总值,详情请看PriceCollectorCommand.java的代码。
同样,PriceSummaryPrintingCommand 的endElement() 方法的代码是令人感兴趣的:
1. public void endElement(String _uri,
String _localName,
String _qName,
XMLFilterImpl _caller,
DefaultHandlerInterface _default) throws SAXException {
2. insertSummary(_default, _uri);
3. _default.defaultEndElementHandler(_uri, _localName, _qName);
4. };
在insertSummary() 方法中, PriceSummaryPrintingCommand 在默认的结束元素处理事件前插入他的XML 内容 。insertSummary() 从PriceCollectorCommand 的 getResult() 方法中接受价格汇总信息,然后根据hashmap的内容激发SAX事件。 Command 类使用了ModifyingXMLSource的默认方法 (defaultstartElementHandler(), defaultCharactersHandler(), 和 defaultEndElementHandler()) 来激发新的SAX事件.详见 PriceSummaryPrintingCommand.java.
深入查看Example3 Example 3可以从非XML数据生成新的XML元素,这不是一个陌生的主意。他的窍门是有了我们自己的XML reader (见 Figure 11).
Figure 11. Class diagram of Example 3
类Example3很简单:
1. import org.xml.sax.XMLReader;
2. import org.xml.sax.SAXException;
3. import org.xml.sax.helpers.XMLReaderFactory;
4. public class Example3 extends Example2 {
5. public Example3() {
6. super();
7. FACTORY_NAME = "Example2b";
8. }
9. protected XMLReader getReader() {
10. XMLReader myReader = new Example3Reader();
11. return myReader;
12. }
13. }
在这个类里,只有重载的 getReader() 方法是新的。 因为输入数据不是XML数据,所以我们需要使用自己定制的reader, 它在第10行创建。
Example3Reader 需要实现XMLReader接口的全部方法。然而,接口中的大部分方法可以虚拟实现(原文为dummy implementation, 理解为方法体内直接返回)。值得关注的是parse(), setContentHandler(), 和 getContentHandler(). Example3Reader包含 一个成员类 ContentHandler ,变量名为myHandler. setContentHandler() 是他的设置方法:
1. public void setContentHandler(ContentHandler _handler) {
2. myHandler = _handler;
3. }
ContentHandler 提供了产生SAX事件回调方法。Transformer 在XSLT转换开始前调用setContentHandler() 方法。在这个例子中,调用发生在 Example1的 doTransform() 方法, 更进一步说, 是 Transformer的 transform() 方法中。
设置 ContentHandler后, Transformer 调用 Example3Reader的 parse() 方法. 这个方法实现了自有的解析器来解析文本格式数据。 解析的原理是XML文档在ContentHandler 方法中构建。在我们的简单的例子中, 只需要下列5个回调方法:
·startDocument():XML 文档的开始
·startElement(String, String, String, Attributes): XML 元素开始
·endElement(String, String, String): XML 元素结束
·characters(char[], int, int): XML 元素的内容
·endDocument():XML 文档结束
如下面的调用与price元素对应:
myHandler.startElement("", "<PRICE>", "<PRICE>", new AttributesImpl());// Converting String "20" to char array char[] myChArray = new char[255];"20".getChars(0, 2, myChArray, 0);// Conversion donemyHandler.characters(myChArray, 0, 2);myHandler.endElement("", "<PRICE>", "<PRICE>");
对应的 XML 元素:
<PRICE>20</PRICE>
总结 这篇文章主要介绍怎样使用SAX和XSLT完成复杂的数据转换,用SAX处理XML数据进行“预处理“,而后XSLT进行转换. Example 1 介绍了基本的XSLT 转换器. Example 2 演示了怎样操控XML数据,并提供给XSLT转换器。 Example 3 演示了怎样从非XML数据生成XML数据,进而应用XSLT转换处理,Example 4 演示了怎样使用XSLT生成非XML数据。
结合 SAX 和 XSLT 是完成复杂数据转换工作的强有力的工具。 它还可以动态的改变转换规则.另一方面,这中技术可能被滥用,因为即使在不应该使用它的情况下的转换引擎中,它很容易隐藏商业逻辑, 例如,很少推荐在整合层(integration layer)包含任何商业逻辑 。
当不再使用XSLT模板包含全部的转换逻辑,维护变得困难。 可以编写command代码,使转换逻辑的数据来自属性文件,因此避免每次转换规则变了后需要重新编辑。尽管如此, 让事情保持可控的最好方法还是有文档说明,并随时保持文档更新。