在上一篇技巧中,“使用 StAX 编写 XML 文档”,我说明了如何使用底层的基于指针的 StAX API 通过编程方式创建 XML 文档。在这篇技巧中,我使用高层的基于事件的 API 演示如何建立一个程序,把两个输入的 XML 文档合并成一个。
同时处理多个 XML 文档可能是一个很大的挑战。比如,SAX 解析器通过回调客户应用程序提交解析事件。因为 SAX 解析器控制了这个过程,客户应用程序实际上没有机会同步不同的输入源。因此,在需要处理多个文档时程序员常常求助于 DOM 解析器。但是,代价是额外的资源占用——所有输入文档的节点树必须都驻留在内存中。
StAX 不存在这种缺陷。如它的名字所表明的那样,其目标就是像合并两个文档这样的流式应用程序。下面的例子说明了如何实现这种功能。假设您需要合并包含产品列表的两个文档。每个文档都有一个 <products> 元素,其中包含一个或多个 <product> 元素,根据 pid 属性按照字母顺序排序。清单 1 是一个这种文档的例子:
清单 1. 产品列表
<products>
<product pid="01"/>
<product pid="05"/>
<product pid="09"/>
</products>
在清单 2 中,我使用传统的合并算法合并来自两个文档中的列表。通过比较来自两个文档的合并条件,决定从文档 1 还是文档 2 复制一个事件到输出文档。这项工作由 readToNextElement() 方法完成。该方法还有一些其他的逻辑,用于检查产品列表的结束。文档开始和文档结束都需要专门处理。
清单 2. 合并文档
import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.XMLEvent;
public class Merger {
private static final QName prodName = new QName("product");
private static final QName pidName = new QName("pid");
public static void main(String[] args)
throws FileNotFoundException, XMLStreamException {
// Use the reference implementation for the XML input factory
System.setProperty(
"javax.xml.stream.XMLInputFactory",
"com.bea.xml.stream.MXParserFactory");
// Create the XML input factory
XMLInputFactory factory = XMLInputFactory.newInstance();
// Create XML event reader 1
XMLEventReader r1 =
factory.createXMLEventReader(new FileReader("prodList1.xml"));
// Create XML event reader 2
XMLEventReader r2 =
factory.createXMLEventReader(new FileReader("prodList2.xml"));
// Create the output factory
XMLOutputFactory xmlof = XMLOutputFactory.newInstance();
// Create XML event writer
XMLEventWriter xmlw = xmlof.createXMLEventWriter(System.out);
// Read to first <product> element in document 1
// and output to result document
String pid1 = readToNextElement(r1, xmlw, false);
// Read to first <product> element in document 1
// without writing to result document
String pid2 = readToNextElement(r2, null, false);
// Loop over both XML input streams
while (pid1 != null || pid2 != null) {
// Compare merge criteria
if (pid2 == null || (pid1 != null && pid1.compareTo(pid2) <= 0))
// Continue in document 1
pid1 = readToNextElement(r1, xmlw, pid2 == null);
else
// Continue in document 2
pid2 = readToNextElement(r2, xmlw, pid1 == null);
}
xmlw.close();
}
/**
* @param reader - the document reader
* @param writer - the document writer
* @param processEnd - forces the document end to be written
* @return - the next merge criterion value
* @throws XMLStreamException
*/
private static String readToNextElement(XMLEventReader reader,
XMLEventWriter writer, boolean processEnd) throws XMLStreamException {
// Nesting level
int level = 0;
while (true) {
// Read event to be written to result document
XMLEvent event = reader.next();
// Avoid double processing of document end
if (!processEnd)
switch (event.getEventType()) {
case XMLEvent.START_ELEMENT :
++level;
break;
case XMLEvent.END_ELEMENT :
if (--level < 0)
return null;
break;
}
// Output event
if (writer != null)
writer.add(event);
// Look at next event
event = reader.peek();
switch (event.getEventType()) {
case XMLEvent.START_ELEMENT :
// Start element - stop at <product> element
QName name = event.asStartElement().getName();
if (name.equals(prodName)) {
return event
.asStartElement()
.getAttributeByName(pidName)
.getvalue();
}
break;
case XMLEvent.END_DOCUMENT :
// Stop at end of document
return null;
}
}
}
}
如您所见,基于事件的 API 非常适于从其他文档派生文档。如果使用底层的基于指针的 API,您还需要对不同的事件类型调用不同的方法,而使用基于事件的 API,只需要向事件编写器的 add() 方法 传递普通事件就可以了。
结束语
这篇技巧示范了在管道式 XML 应用程序中使用 StAX 的基于事件的 API,比如文档的合并。2003 年 11 月 3 日,StAX 通过了 Final JSR-0173 Approval Ballot。它将为每个 Java 程序员的工具箱中增加一些有用的东西。