XML From IBM Developer works
简介:该文简要描述了DOM的概念和内部逻辑结构,给出了DOM文档操作和XML文件互相转换的java实现过程。郭洪锋:主要从事UNIX系统下分布式应用的开发和研究。您可以通过电子邮件ghf_email@sohu.com 和他联系
1. DOM简介
目前,W3C已于2000年11月13日推出了规范DOM level 2。文档对象模型(DOM)是HTML和XML文档的编程接口规范,它与平台和语言是无关的,因而可以用各种语言在各种平台上实现。该模型定义了THML和XML文件在内存中的逻辑结构(即为文档),提供了访问、存取THML和XML文件的方法。利用DOM规范,可以实现DOM 文档和XML之间的相互转换,遍历、操作相应DOM文档的内容。可以说,要自由的操纵XML文件,就要用到DOM规范。
2. DOM内部逻辑结构
DOM文档中的逻辑结构可以用节点树的形式进行表述。通过对XML文件的解析处理,XML文件中的元素便转化为DOM文档中的节点对象。DOM的文档节点有Document、Element、Comment、Type等等节点类型,其中每一个DOM文档必须有一个Document节点,并且为节点树的根节点。它可以有子节点,或者叶子节点如Text节点、Comment节点等。任何的格式良好的XML文件中的每一个元素均有DOM文档中的一个节点类型与之对应。利用DOM接口将XML文件转化成DOM文档后,我们就可以自由的处理XML文件了。
3. java中的DOM接口
DOM规范提供的API的规范,目前Sun公司推出的jdk1.4测试版中的java API遵循了 DOM level 2 Core推荐接口的语义说明,提供了相应的java语言的实现。
在org.xml.dom中,jkd1.4提供了Document、DocumentType、Node、NodeList、Element、Text等接口,这些接口均是访问DOM文档所必须的。我们可以利用这些接口创建、遍历、修改DOM文档。
在javax.xml.parsers中,jkd1.4提供的DoumentBuilder和DocumentBuilderFactory组合可以对XML文件进行解析,转换成DOM文档。
在javax.xml.transform.dom和javax.xml.transform.stream中,jdk1.4提供了DOMSource类和StreamSource类,可以用来将更新后的DOM文档写入生成的XML文件中。
4. 例程
4.1 将XML文件转化成DOM文档
这个过程是获得一个XML文件解析器,解析XML文件转化成DOM文档的过程。
Jdk1.4中,Document接口描述了对应于整个XML文件的文档树,提供了对文档数据的访问,是该步骤的目标。Document接口可以从类DocumentBuilder中获取,该类包含了从XML文档获得DOM文档实例的API。XML的解析器可以从类DocumentBuilderFactory中获取。在jdk1.4中,XML文件转化成DOM文档可以有如下代码实现:
//获得一个XML文件的解析器
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//解析XML文件生成DOM文档的接口类,以便访问DOM。
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse( new File(FileName) );
4.2 遍历DOM文档
获得接口类document实例后,可以对DOM的文档树进行访问。要遍历DOM文档,首先要获得Root元素。然后获得Root元素的子节点列表。这里通过递归的方法实现遍历的目的。
//获得Root元素
Element element = document.getDocumentElement();
//获得Root元素的子节点列表
nodelist = element.getChildNodes();
//用递归方法实现DOM文档的遍历
GetElement(nodelist);
其中GetElement方法实现如下:
public void GetElement(NodeList nodelist){
Node cnode;
int i,len;
String str;
if(nodelist.getLength() == 0){
// 该节点没有子节点
return;
}
for(i=0;i 1)
System.out.println(" "+str+" "+len);
}
}
}
注意:上面的代码只是显示Node类型和Text类型的对象。它们的类型标识分别是1和3。
4.3 修改DOM文档
修改DOM文档的API在DOM level 2 Core规范中做了说明,jkd1.4中的org.xml.dom中实现了这些API。修改DOM文档操作主要集中在Document、Element、Node、Text等类中,这里给出的例子中是在解析出的DOM文档中增加一系列对象,对应与在XML文件中增加一条记录。
// 获得Root对象
Element root = document.getDocumentElement();
// 在DOM文档中增加一个Element节点
Element booktype = document.createElement("COMPUTES");
//将该节点转化成root对象的子节点
root.appendChild(cdrom);
//在DOM文档中增加一个Element节点
Element booktitle = document.createElement("Title");
//将该节点转化成booktype对象的子节点
booktype.appendChild(booktitle);
//在DOM文档中增加一个Text节点
Text bookname = document.createTextNode("understand Corba");
//将该节点转化成bookname对象的子节点
booktitle.appendChild(bookname);
4.4 将DOM文档转化成XML文件
// 获得将DOM文档转化为XML文件的转换器,在jdk1.4中,有类TransformerFactory
// 来实现,类Transformer实现转化API。
TransformerFactory tfactory = TransformerFactory.newInstance();
Transformer transformer = tfactory.newTransformer();
// 将DOM对象转化为DOMSource类对象,该对象表现为转化成别的表达形式的信息容器。
DOMSource source = new DOMSource(document);
// 获得一个StreamResult类对象,该对象是DOM文档转化成的其他形式的文档的容器,可以是XML文件,文本文件,HTML文件。这里为一个XML文件。
StreamResult result = new StreamResult(new File(“text.xml”));
// 调用API,将DOM文档转化成XML文件。
transformer.transform(source,result);
这里提供了该例程的完整程序,该例程在windows 2000中jdk1.4环境中运行通过。
以上给出了一个例子,读者可以从中了解到对DOM操作的思路。因为对DOM的操作均遵循了DOM规范,所以也适用于其它语言对DOM的处理。
参考资料:
1. http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-2000-1113
这是篇细探 JAXP,Sun 的 Java API for XML 的文章,帮助解除了有关 JAXP 本质和服务目的的疑惑。本文讲解了 JAXP 的基本概念,演示 XML 语法分析为什么需要 JAXP,并显示如何轻易更改 JAXP 使用的语法分析器。本文还进一步讲述了 SAX 和 DOM 这两个流行的与 JAXP 相关的 Java 和 XML API。
Java 和 XML 在每一个技术领域都制造了新闻,并且对于软件开发人员来说,似乎是 1999 年和 2000 年最重要的发展。结果,Java 和 XML API 的数量激增。其中两个最流行的 DOM 和 SAX 还引起极大兴趣,而 JDOM 和数据绑定 API 也接踵而来。只透彻理解这些技术中的一个或两个就是一项艰巨任务,而正确使用所有这些技术就会使您成为专家。但在去年,另一个 API 给人留下了深刻印象,它就是 Sun 的 Java API for XML,通常称为 JAXP。如果考虑到 Sun 在其平台上还没有任何特定于 XML 的产品,那么这个进展就不足为奇。而令人惊奇的是人们对 JAXP 了解的缺乏。多数使用它的开发人员在他们所用的这个 API 的概念理解上都有错误。
什么是 JAXP?
本文假设您有 SAX 和 DOM 的基本知识。这里实在没有足够篇幅来解释 SAX、DOM 和 JAXP。如果您是 XML 语法分析的新手,那么可能要通过联机资源阅读 SAX 和 DOM,或者浏览我的书。(参考资源一节中有至 API 和我的书的链接。)获得基本知识后再看本文会比较好。
API 还是抽象?
在讲解代码之前,介绍一些基本概念很重要。严格地说,JAXP 是 API,但是将其称为抽象层更准确。它不提供处理 XML 的新方式,不补充 SAX 或 DOM,也不向 Java 和 XML 处理提供新功能。(如果在这点上理解有误,则本文正好适合您!)它只是使通过 DOM 和 SAX 处理一些困难任务更容易。如果在使用 DOM 和 SAX API 时遇到特定于供应商的任务,它还使通过独立于供应商的方式处理这些任务成为可能。
虽然要分别讲述所有这些特性,但是真正需要掌握的是:JAXP 不提供语法分析功能!没有 SAX、DOM 或另一个 XML 语法分析 API,就无法分析 XML 语法。有很多人曾让我将 DOM、SAX 或 JDOM 与 JAXP 进行对比。但进行这些对比是不可能的,因为前三个 API 与 JAXP 的目的完全不同。SAX、DOM 和 JDOM 都分析 XML 语法。而 JAXP 却提供到达这些语法分析器和结果的方式。它自身不提供分析文档语法的新方法。如果要正确使用 JAXP,则一定要弄清这点。这将使您比其它 XML 开发人员领先一大截。
如果仍然怀疑(或认为我故弄玄虚),请从 Sun 的 Web 站点下载 JAXP 分发(请参阅参考资料一节),然后就会知道基本 JAXP 是什么。在包括的 jar (jaxp.jar) 中只有六个类!这个 API 会有多难哪?所有这些类(javax.xml.parsers 包的一部分)都位于现有语法分析器之上。这些类中的两个还用于错误处理。JAXP 比人们想象的要简单得多。那么,为什么还感到困惑哪?
Sun 的 JAXP 和 Sun 的语法分析器
JAXP 下载时包括 Sun 的语法分析器。所有 parser 器类作为 com.sun.xml.parser 包和相关子包的一部分位于 parser.jar 档案中。应该知道,该语法分析器(代码名为 Crimson)不是 JAXP 自身的一部分。它是 JAXP 版本的一部分,但不是 JAXP API 的一部分。令人困惑吗?有一点。换这种方式想想:JDOM 与 Apache Xerces 语法分析器一起提供。该语法分析器不是 JDOM 的一部分,但由 JDOM 使用,所以包括它,以确保 JDOM 可以单独使用。JAXP 也是如此,但不象 JDOM 那样好表达:JAXP 与 Sun 的语法分析器一起提供,以便可以立即使用。但是,很多人将 Sun 的语法分析器中包括的类当成 JAXP API 的一部分。例如,新闻组中一个常见的问题是:“怎样使用 JAXP 中的 XMLDocument 类?其目的是什么?”这个答案可有些复杂。
首先,com.sun.xml.tree.XMLDocument 类不是 JAXP 的一部分。它是 Sun 语法分析器的一部分。所以,这个问题从一开始就给人以误导。其次,JAXP 的整个意义在于在处理语法分析器时提供供应商独立性。使用 JAXP 的同一代码可以与 Sun 的 XML 语法分析器、Apache 的 Xerces XML 语法分析器和 Oracle 的 XML 语法分析器一起使用。而使用特定于 Sun 的类是个坏主意。这与 JAXP 的整个意义相背离。现在看出来这个问题怎样混淆概念了吗?语法分析器和 JAXP 发行版本(至少是 Sun 的版本)中的 API 被混为一谈,开发人员将其中一个的类和特性当成是另一个的了,反之亦然。
旧和新
关于 JAXP,最后需要指出的是:使用 JAXP 有一些缺陷。例如,JAXP 只支持 SAX 1.0 和 DOM 第一层规范。SAX 2.0 从 2000 年 5 月起就完成,DOM 第二层规范支持甚至在大多数语法分析器中存在更长时间。DOM 第二层规范还没有完成,但确实足够稳定以用于生产。这两个 API 的新版本都有重大改进,最明显的是对 XML 名称空间的支持。该支持还允许“XML Schema 确认”,这个与 XML 相关的另一热门技术。公平地说,当 JAXP 发布 1.0 最终发行版时,SAX 2.0 和 DOM 第一层规范都还没有完成。但是,由于没有包括这些新版本,确实为开发人员带来很大不便。
还可以使用 JAXP,但是也可以等待 JAXP 1.1,它支持 SAX 2.0 和 DOM第二层规范 。否则,将发现,JAXP 提供的优点以 SAX 和 DOM 最新版本中的功能为代价,并使应用程序更加难以编码。无论是否等待下一个 JAXP 发行版,都要留意这个问题。如果将 JAXP 与语法分析器一起使用,而语法分析器支持的 DOM 和 SAX 版本比 JAXP 支持的要高,则可能会有类路径问题。所以,事先留意一下,并且,一旦有 JAXP 1.1,马上升级。基本理解 JAXP 之后,让我们看一下 JAXP 依赖的 API:SAX 和 DOM。
从 SAX 开始
SAX (Simple API for XML)是用于处理 XML 的事件驱动方法。它基本由许多回调函数组成。例如,每当 SAX 语法分析器遇到元素的开始标记时就调用 startElement()。对于字符串,将调用 characters() 回调函数,然后在元素结束标记处调用 endElement()。还有很多回调函数用于文档处理、错误和其它词汇结构。现在知道这是怎么回事了。SAX 程序员实现一个定义这些回调函数的 SAX 接口。SAX 还实现一个名为 HandlerBase 的类,该类实现所有这些回调函数,并提供所有这些回调方法的缺省空实现。(提到这一点是因为它在后面讲到的 DOM 中很重要。)SAX 开发人员只需扩展这个类,然后实现需要插入特定逻辑的方法。所以,SAX 的关键在于为这些不同的回调函数提供代码,然后允许语法分析器在适当的时候触发这些回调函数中的每一个。
因此,典型的 SAX 过程如下:
用特定供应商的语法分析器实现创建一个 SAXParser 实例
注册回调实现(例如,通过使用扩展 HandlerBase 的类)
开始进行语法分析,然后在触发回调实现时等待
JAXP 的 SAX 组件提供执行所有这些步骤的简单方式。如果没有 JAXP,SAX 语法分析器要直接从供应商类(如 org.apache.xerces.parsers.SAXParser)进行实例化,或者必须使用名为 ParserFactory 的帮助类。第一个方法的问题很明显:不独立于供应商。第二个方法的问题在于类厂需要一个自变量,即要使用的语法分析器类的字符串名称(还是那个 Apache 类 org.apache.xerces.parsers.SAXParser)。可以通过将不同语法分析器作为 String 传递来更改语法分析器。使用这种方法不必更改任何 import 语句,但是还是要重新编译类。这显然不是最佳解决方案。如果能够不重新编译类而更改语法分析器,可能会简单得多,是不是这样呢?
JAXP 提供了更好的替代方法:它允许将语法分析器作为 Java 系统属性来提供。当然,当从 Sun 下载版本时,将得到使用 Sun 语法分析器的 JAXP 实现。可以从 Apache XML Web 站点下载在 Apache Xerces 上构建其实现的相同 JAXP 接口。因此(无论哪一种情况),更改正在使用的语法分析器需要更改类路径设置,即从一种语法分析器实现更改到另一个,但是 不要求重新编译代码。这就是 JAXP 的魔力,或抽象性。
SAX 语法分析器一瞥
JAXP SAXParserFactory 类是能够轻易更改语法分析器实现的关键所在。必须创建这个类的新实例(等一会将讲到)。创建新实例之后,类厂提供一个方法来获得支持 SAX 的语法分析器。在内部,JAXP 实现处理依赖于供应商的代码,使您的代码不受影响。这个类厂还提供其它一些优秀特性。
除创建 SAX 语法分析器实例的基本工作之外,类厂还允许设置配置选项。这些选项影响所有通过类厂获得的语法分析器实例。JAXP 1.0 中两个可用的功能是设置名称空间敏感性 (setNamespaceAware(boolean awareness)),和打开确认 (setValidating(boolean validating))。请记住,一旦设置了这些选项,在调用该方法之后,它们将影响 所有从 类厂获得的实例。
设置了类厂之后,调用 newSAXParser() 将返回一个随时可用的 JAXP SAXParser 类实例。这个类封装了一个下层的 SAX 语法分析器(SAX 类 org.xml.sax.Parser 的实例)。它还防止向语法分析器类添加任何特定于供应商的附加功能。(还记得以前对 XmlDocument 的讨论吗?)这个类可以开始进行实际的语法分析。以下清单显示如何创建、配置和使用 SAX 类厂。
清单 1. 使用 SAXParserFactory
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
// JAXP
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
// SAX
import org.xml.sax.AttributeList;
import org.xml.sax.HandlerBase;
import org.xml.sax.SAXException;
public class TestSAXParsing {
public static void main(String[] args) {
try {
if (args.length != 1) {
System.err.println ("Usage: java TestSAXParsing [filename]");
System.exit (1);
}
// 获得SAX 语法分析器类厂
SAXParserFactory factory = SAXParserFactory.newInstance();
//设置设置名称空间敏感性选项,关掉确认选项
factory.setValidating(true);
factory.setNamespaceAware(false);
SAXParser parser = factory.newSAXParser();
parser.parse(new File(args[0]), new MyHandler());
} catch (ParserConfigurationException e) {
System.out.println("The underlying parser does not support " +
" the requested features.");
} catch (FactoryConfigurationError e) {
System.out.println("Error occurred obtaining SAX Parser Factory.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyHandler extends HandlerBase {
//通过 DocumentHandler, ErrorHandler等实现的SAX回调函数
}
请注意,在这段代码中,在使用类厂时可能发生两个特定于 JAXP 的问题:无法获得或配置 SAX 类厂,以及无法配置 SAX 语法分析器。当无法获得 JAXP 实现中指定的语法分析器或系统属性时,通常会发生第一个问题 FactoryConfigurationError。当正在使用的语法分析器中的特性不可用时,会发生第二个问题 ParserConfigurationException。这两个问题都容易处理,应该不会对 JAXP 的使用造成任何困难。
在获得类厂、关闭名称空间并打开“确认”之后,将获得 SAXParser,然后开始语法分析。请注意, SAX 语法分析器的 parse() 方法取得前面提到的 SAX HandlerBase 类的一个实例。(可以通过完整的 Java 清单查看该类的实现。)还要传递要进行语法分析的文件。但是, SAXParser 所包含的远不止这一个方法。
使用 SAX 语法分析器
获得 SAXParser 类的实例之后,除了向语法分析器传递 File 进行语法分析之外,还可以用它做更多的事。由于如今大型应用中的应用程序组件之间通信方式,“对象实例创建者就是其使用者”这样的假定并不总是安全的。换句话说,一个组件可能创建 SAXParser 实例,而另一组件(可能由另一开发人员编码)可能需要使用那个实例。由于这个原因,提供了一些方法来确定语法分析器的设置。执行此任务的两个方法是 isValidating(),它通知调用程序:语法分析器将要、或不要执行“确认”,以及 isNamespaceAware(),它返回一个指示,说明语法分析器可以或不可以处理 XML 文档中的名称空间。虽然这些方法能提供有关语法分析器可以执行功能的信息,但是无法更改这些特性。必须在语法分析器类厂级别执行该操作。
另外,有多种方法来请求对文档进行语法分析。除了只接受 File 和 SAX HandlerBase 实例,SAXParser 的 parse() 方法还能以 String 形式接受 SAX InputSource、Java InputStream 或 URL,所有这些都要与 HandlerBase 实例一起提供。所以,不同类型的输入文档可以用不同方式的语法分析来处理。
最后,可以直接通过 SAXParser 的 getParser() 方法获得和使用下层的 SAX 语法分析器(org.xml.sax.Parser 的实例)。获得这个下层实例之后,就可以获得通常的 SAX 方法。下一个清单显示 SAXParser 类(这个 JAXP 中 SAX 语法分析的核心类)的各种使用示例。
清单 2. 使用 JAXP SAXParser
//获得SAXP的一个实例
SAXParser saxParser = saxFactory.newSAXParser();
//查看是否支持 Validate 选项
boolean isValidating = saxParser.isValidating();
//查看是否支持 namespace 选项
boolean isNamespaceAware = saxParser.isNamespaceAware();
// 运用一个File 和一个SAX HandlerBase 的实例进行多种形式的语法分析
saxParser.parse(new File(args[0]), myHandlerBaseInstance);
// 运用一个 SAX InputSource实例 和一个 SAX HandlerBase 实例
saxParser.parse(mySaxInputSource, myHandlerBaseInstance);
//运用一个 InputStream 实例和一个SAX HandlerBase 实例
saxParser.parse(myInputStream, myHandlerBaseInstance);
// 运用一个 URI 和一个SAX HandlerBase 实例
saxParser.parse("http://www.newInstance.com/xml/doc.xml", myHandlerBaseInstance);
//获得底层的(封装)SAX 语法分析器
org.xml.sax.Parser parser = saxParser.getParser();
//利用底层的语法分析器
parser.setContentHandler(myContentHandlerInstance);
parser.setErrorHandler(myErrorHandlerInstance);
parser.parse(new org.xml.sax.InputSource(args[0]));
目前为止,关于 SAX 已经讲了很多,但是还没有揭示任何不寻常或令人惊奇的东西。事实上,JAXP 的功能很少,特别是当 SAX 也牵涉进来时。这很好,因为有最少的功能性意味着代码可移植性更强,并可以由其他开发人员与任何与 SAX 兼容的 XML 语法分析器一起使用,无论是免费(通过开放源码,希望如此)还是通过商业途径。就是这样。在 JAXP 中使用 SAX 没有更多的东西。如果已经知道 SAX,那么现在已经掌握大约 98% 的内容。只需学习两个新类和两个 Java 异常,您就可以开始了。如果从没使用过 SAX,那也很简单,现在就可以开始。
处理 DOM
如果要休息以迎接 DOM 挑战,那么先别休息。在 JAXP 中使用 DOM 的过程与 SAX 几乎相同,所要做的全部只是更改两个类名和一个返回类型,这样就差不多了。如果理解 SAX 的工作原理和 DOM 是什么,则不会有任何问题。
DOM 和 SAX 的主要差异是它们的 API 结构。SAX 包含一个基于事件的回调函数集,而 DOM 有一个内存中的树状结构。换句话说,在 SAX 中,从不需要操作数据结构(除非开发人员手工创建)。因此,SAX 不提供修改 XML 文档的功能。而 DOM 正好提供这种类型的功能。org.w3c.dom.Document 类表示 XML 文档,它由表示元素、属性和其它 XML 结构的 DOM 节点组成。所以,JAXP 无需触发 SAX 回调,它只负责从语法分析返回一个 DOM Document 对象。
DOM 语法分析器类厂一瞥
基本理解 DOM 以及 DOM 和 SAX 的差异之后,就没什么好说的了。以下代码看起来与 SAX 代码类似。首先,获得 DocumentBuilderFactory(与 SAX 中的方式相同)。然后,配置类厂来处理确认和名称空间(与 SAX 中的方式相同)。下一步,从类厂中检索 DocumentBuilder(它与 SAXParser 类似)(与 SAX 中的方式相同. . . 啊,您都知道了)。然后,就可以进行语法分析了,产生的 DOM Document 对象传递给打印 DOM 树的方法。
清单 3. 使用文档构建器类厂
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
// JAXP
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
// DOM
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class TestDOMParsing {
public static void main(String[] args) {
try {
if (args.length != 1) {
System.err.println ("Usage: java TestDOMParsing [filename]");
System.exit (1);
}
// 获得 Document Builder Factory
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//打开确认选项,关掉名称空间敏感性选项。
factory.setValidating(true);
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File(args[0]));
// 从DOM 数中打印文档,并加一初始空格
printNode(doc, "");
// 在这里也可以对 DOM 文档进行修改
} catch (ParserConfigurationException e) {
System.out.println("The underlying parser does not support the requested features.");
} catch (FactoryConfigurationError e) {
System.out.println("Error occurred obtaining Document Builder Factory.");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void printNode(Node node, String indent) {
// 打印 DOM 树
}
此代码中可能会出现两个不同的问题(与 JAXP 中的 SAX 类似):FactoryConfigurationError 和 ParserConfigurationException。每一个的原因与 SAX 中的相同。不是实现类 (FactoryConfigurationError) 中有问题,就是语法分析器不支持请求的特性 (ParserConfigurationException)。DOM 和 SAX 的唯一差异是:在 DOM 中,用 DocumentBuilderFactory 替代 SAXParserFactory,用 DocumentBuilder 替代 SAXParser。就这么简单!(可以查看完整代码清单,该清单包括用于打印 DOM 树的方法。)
使用 DOM 语法分析器
有了 DOM 类厂之后,就可以获得 DocumentBuilder 实例。DocumentBuilder 实例可以使用的方法与 SAX 的非常类似。主要差异是 parse() 的变种不需要 HandlerBase 类的实例。它们返回表示语法分析之后的 XML 文档的 DOM Document 实例。另一唯一不同之处是:为类似于 SAX 的功能提供了两个方法:用 SAX ErrorHandler 实现来处理语法分析时可能出现的问题的 setErrorHandler(),和用 SAX EntityResolver 实现来处理实体解析的 setEntityResolver()。如果不熟悉这些概念,则需要通过联机或在我的书中学习 SAX。以下清单显示使用这些方法的示例。
清单 4. 使用 JAXP DocumentBuilder
//获得一个 DocumentBuilder 实例
DocumentBuilder builder = builderFactory.newDocumentBuilder();
//查看是否支持 Validate 选项
boolean isValidating = builder.isValidating();
//查看是否支持 namespace 选项
boolean isNamespaceAware = builder.isNamespaceAware();
// 设置一个 SAX ErrorHandler
builder.setErrorHandler(myErrorHandlerImpl);
// 设置一个 SAX EntityResolver
builder.setEntityResolver(myEntityResolverImpl);
// 运用多种方法对 file 进行语法分析
Document doc = builder.parse(new File(args[0]));
// 运用 SAX InputSource
Document doc = builder.parse(mySaxInputSource);
// 运用 InputStream
Document doc = builder.parse(myInputStream, myHandlerBaseInstance);
// 运用 URI
Document doc = builder.parse("http://www.newInstance.com/xml/doc.xml");
是不是感到 DOM 这一节有些令人厌烦?有这种想法的不止您一个,写 DOM 代码有些令人厌烦是因为它是直接取得所学的 SAX 知识,然后将其用于 DOM。因此,和朋友、同事打赌吧,说使用 JAXP 只是小菜一碟。
更改语法分析器
最后要探讨的主题是 JAXP 轻易更改类厂类使用的语法分析器的能力。更改 JAXP 使用的语法分析器实际意味着更改 类厂,因为所有 SAXParser 和 DocumentBuilder 实例都来自这些类厂。既然确定装入哪个语法分析器的是类厂,因此,必须更改类厂。可以通过设置 Java 系统属性 javax.xml.parsers.SAXParserFactory 来更改要使用的 SAXParserFactory 接口实现。如果没有定义该属性,则返回缺省实现(供应商指定的任何语法分析器)。相同原理适用于 DocumentBuilderFactory 实现。在这种情况下,将查询 javax.xml.parsers.DocumentBuilderFactory 系统属性。就这么简单,我们已经学完了!这就是 SAXP 的全部:提供到 SAX 的挂钩,提供到 DOM 的挂钩,并允许轻易更改语法分析器。
结束语
如您所见,没多少复杂的东西。更改系统属性,通过类厂、而不是语法分析器或构建器来设置“确认”,以及弄清楚JAXP实际上不是人们通常所认为的那样,这些是使用 JAXP 的最困难部分。除了没有 SAX 2.0 和 DOM第二层规范支持之外,JAXP 在两个流行的 Java 和 XML API 之上提供一个有帮助的可插入层。它使代码独立于供应商,并允许不编译语法分析代码而更改语法分析器。那么,从 Sun、Apache XML 或其它方便之处下载 JAXP,并使用它吧!继续关注 JAXP 1.1,并增加对 SAX 2 和 DOM 2、XSLT 及更多内容的支持。您将在这里获得第一手新闻,所以,请关注 developerWorks。
参考资料
阅读 JAXP 1.0 规范(英),以获得详细信息。
在 Sun 的 Java and XML 总部(英)查看 Sun 的所有关于 XML 的动向。
在 Apache XML 获得 Apache JAXP 实现。
查找更多 API 内幕。在 SAX Web 站点(英)从 SAX 2 for Java 开始。
要查看 SAX 支持的另一个 XML,在 W3C Web 站点看一下 DOM。
要得到 490 多个页面的 XML 专家建议,请查看 O'Reilly 出版的 Java 与 XML(英),和
Brett 关于最热门技术的书籍。
加入 developerWorks 的 Java 语言开发人员 XML 工具和 API 新闻组
需要更基本的 XML 介绍吗?尝试 developerWorks 的 XML 教程介绍(英)和 其它教育文章,它们包括最基本的主题。
继上篇关于 JAXP(Sun 的 Java API for XML Parsing)的文章之后,在本续篇中,作者分析了对 SAX 和 DOM 标准支持进行了更新的最新版本 1.1。添加了 TRaX 之后,JAXP 1.1 为 Java 和 XML 开发人员提供了在编写对 XML 文档进行语法分析和变换的独立于供应商的代码方面不可缺少的工具。
如果您经常阅读 developerWorks 的 XML 专区,就可能对另一篇 JAXP 文章的出现感到有些奇怪。就在一个月前,我写了一篇文章“JAXP 专述”。在那篇文章中,我完整地解释了 JAXP(Java API for XML Parsing),其工作原理,以及它如何帮助您用 Java 程序处理 XML 数据。那篇文章讲的是 JAXP 发行版 1.0。
熟悉的领域
那么,为什么还要写 JAXP 方面的文章呢?我是 JAXP 专家小组的成员之一,我们现在即将完成 1.1 规范。虽然大多数“点发行版”(指的是,例如,版本从 1.0 升到 1.1,或从 2.2 升到 2.3)只对现有的 API 作很小、或者至少是简单的改动,但是 JAXP 的 1.1 发行版却与其前一版有很大不同。事实上,本文只有三分之一讲述现有类和功能性中的新方法,而其余部分将集中讲述 JAXP 1.1 版完全新的类和功能。换句话说,JAXP 1.1 中的新东西(也是好东西)实在太多,我迫不及待地要让您感受一下它们。
如果是 JAXP 新手,或者现在正在使用它,或者要等它再成熟一些时使用,那么,本文正适合您。我将讲述对 API 1.0 版所作的修改,然后再花一些时间讲一下 TRaX(XML 变换)。TRaX 是合并到 JAXP 中来允许用独立于供应商的方式进行 XSL 变换的 API,它补充了 JAXP 在进行 XML 语法分析时允许供应商独立性的现有能力。建议您阅读我的第一篇 JAXP 文章,休息片刻,然后再读这篇 JAXP 1.1 的讨论。
增强语法分析 API
很多对 JAXP API 的改动都围绕语法分析进行,考虑到 JAXP 中的 "P" 代表 "parsing"(语法分析),这是有意义的。但是,JAXP 1.1 中的重大改动是围绕 XML 变换进行的,以后将在本文中介绍。在现有的 JAXP 功能性方面,改动非常少。最大的增加是对 SAX 2.0(在 2000 年 5 月完成)的支持,和对 DOM 级别 2(还在完成工作中)级别 2 的支持。前一版的 JAXP 只支持 SAX 1.0 和 DOM 级别 1。这种更新标准的缺乏曾一度是 JAXP 1.0 中最受批评之处。
除了使 JAXP 支持 SAX 和 DOM 的最新版之外,API 中还有几处小的改动(如我上一篇文章所述)。几乎所有这些改动都是通过不同公司和个人对 JAXP 专家小组提供的反馈而产生的重要改动。所有这些改动还处理由 JAXP 的两个 factory(SAXParserFactory 和 DocumentBuilderFactory)返回的语法分析器的配置问题。现在,我将讲述这些,以及 SAX 和 DOM 标准支持中的更新。
更新标准
从 JAXP 1.0 到 1.1 的升级中,最令人期待的改变是对流行的 SAX 和 DOM 标准支持的更新。SAX (Simple API for XML) 在 2000 年 5 月发行了 2.0 版,与 XML 其它组件相比,该版本对 XML 名称空间的支持提供了极大增强。这种名称空间的支持允许使用众多其它 XML 词汇,如 XML 模式、XLink 和 XPointer。虽然在 SAX 1.0 中也可以使用这些词汇,但是开发人员需要将元素的本地(或限定)名称与其名称空间分开,并在整个文档中跟踪这些名称空间。SAX 2.0 为开发人员提供了这种信息,从而极大简化了执行这些编程任务的过程。DOM 级别 2 也是一样:有名称空间支持和许多 DOM 类中的其它方法。虽然 DOM 级别 2 还没有完成,但是,JAXP 1.1 支持其目前的规范。如果 DOM 标准的最终版本引入了小的改动,JAXP 当然要包括这些修改。
好的消息是:这些改动对使用 JAXP 的开发人员来说通常都是透明的。换句话说,这些标准更新可以说是“自动”发生的,无需用户介入。只要对 SAXParserFactory 指定与 SAX 2.0 兼容的语法分析器,并对 DocumentBuilderFactory 类指定与 DOM 级别 2 兼容的语法分析器,就可以利用这种更新。
通往 SAX 2.0 之路
有几个与这些更新相关的重要改动。在 SAX 1.0 中,由供应商和 XML 语法分析器项目实现的语法分析器接口是 org.xml.sax.Parser。然后,JAXP 类 SAXParser 通过 getParser() 方法提供一个方法来获得这个底层实现类。该方法的特征如下:
清单 1. getParser() 方法
public interface SAXParser {
public org.xml.sax.Parser getParser();
// Other methods
}
然而,在从 SAX 1.0 到 2.0 的改动中,反对使用 Parser 接口,并将其用新接口 org.xml.sax.XMLReader 替代。这在本质上使 getParser() 方法在获得 SAX 2.0 XMLReader 类的实例方面已无用处。为支持这种做法并支持 SAX 2.0,将一个新方法添加到 JAXP SAXParser 类。自然地,将该方法命名为 getXMLReader(),它看起来如下:
清单 2. getXMLReader() 方法
public interface SAXParser {
public org.xml.sax.XMLReader getXMLReader();
public org.xml.sax.Parser getParser();
// Other methods
}
同样,在 SAX 1.0 中用来实现回调的类是 org.xml.sax.HandlerBase,并将该类的实例提供给所有 JAXP 1.0 parse() 方法。但是,由于某些其它的 SAX 2.0 不支持和改动,在 SAX 2.0 中已不再使用这个类。取代它的是 org.xml.sax.ext.DefaultHandler。为适应这种改动,为 SAXParser 类中的所有 parse() 方法补充了接受 DefaultHandler 类实例以支持 SAX 2.0 的同一方法的不同版本。为帮助您看到这一不同之处,清单 3 中显示了所讨论的方法:
清单 3. parse() 方法
public interface SAXParser {
// The SAX 1.0 parse methods
public void parse(File file, HandlerBase handlerBase);
public void parse(InputSource inputSource, HandlerBase handlerBase);
public void parse(InputStream inputStream, HandlerBase handlerBase);
public void parse(InputStream inputStream, HandlerBase handlerBase,
String systemID);
public void parse(String uri, HandlerBase handlerBase);
// The SAX 2.0 parse methods
public void parse(File file, DefaultHandler defaultHandler);
public void parse(InputSource inputSource, DefaultHandler defaultHandler);
public void parse(InputStream inputStream, DefaultHandler defaultHandler);
public void parse(InputStream inputStream, DefaultHandler defaultHandler,
String systemID);
public void parse(String uri, DefaultHandler defaultHandler);
// Other methods
}
所有这些方法都用来进行语法分析可能有点令人困惑,但是,只有在使用 SAX 的两个版本时才会令人感到棘手。如果正在使用 SAX 1.0,那么将使用 Parser 接口和 HandlerBase 类,应该使用哪些方法也很明显。同样,在使用 SAX 2.0 时,很明显,应该使用那些接收 DefaultHandler 实例并返回 XMLReader 的方法。因此,请把所有这些都看成是参考,不要对此太担心。另外,还对 API 的 SAX 部分作了某些其它改动。
现有 SAX 类中的改动
要完成对现有 JAXP 功能性改动的讨论,需要复习几个 JAXP SAX 用户可以使用的新方法。首先,SAXParserFactory 类有一个新方法:setFeature()。如您可从 JAXP 1.0 中所回忆的一样,SAXParserFactory 类允许对从 factory 返回的 SAXParser 实例进行配置。除了已有的方法(setValidating() 和 setNamespaceAware())之外,这个新方法允许请求新语法分析器实例的 SAX 2.0 功能。SAX 2.0 提供允许供应商为其语法分析器创建特定功能性的功能,然后,用户可以与通过 SAX 与这些功能交互。例如,用户可以请求 http://apache.org/xml/features/validation/schema 功能,该功能允许将 XML 模式验证打开或关闭。现在,这可以在 SAXParserFactory 上执行,如清单 4 所示:
清单 4. 使用 setFeature() 方法
SAXParserFactory myFactory = SAXParserFactory.newInstance();
// Turn on XML Schema validation
myFactory.setFeature("http://apache.org/xml/features/validation/schema", true);
// Now get an instance of the parser with schema validation enabled
SAXParser parser = myFactory.newSAXParser();
当然,还提供了 getFeature() 方法来补充 setFeature() 方法,并允许查询特定的功能。该方法返回一个简单的 boolean 值。
SAX 除了允许设置功能(设置成 true 或 false 值)之外,还允许设置特性。在 SAX 中,特性是与实际的 Java 对象相关的名称。例如,使用 SAX 语法分析器实例,您可以设置特性 http://xml.org/sax/properties/lexical-handler,为该特性分配一个 SAX LexicalHandler 接口的实现。然后,语法分析器将使用该实现来做词汇处理。因为象词汇这样的特性是特定于语法分析器、而不是特定于 factory 的(象特性一样),所以在 JAXP SAXParser 类中、而不是在 SAXParserFactory 类中提供 setProperty() 方法。与功能一样,也在 SAXParser 中提供 getProperty() 的补充方法,以返回与特定特性相关的值。
DOM 中的更新
JAXP 的 DOM 部分有一些新方法。已经将这些方法添加到现有 JAXP 类中,以同时支持 DOM 级别 2 选项,以及去年出现的一些常见配置情况。我不想在这里讲述所有这些选项及相应方法,因为它们中的很多都不易被察觉(它们只在极罕见的情况下使用),在很多应用中都不需要。当然鼓励您在最近的 JAXP 规范中联机查看这些(请参阅参考资料一节)。讲完了标准更新、SAX 改动和其它 DOM 方法之后,您可以阅读 JAXP 1.1 中最重要的改动 -- TRaX API 了。
TRaX API
目前为止,已经讲了对用 JAXP 进行 XML 语法分析的改动。现在,可以讲 JAXP 1.1 中的 XML 变换了。Sun 的 最新版 API 中最令人兴奋的进展恐怕就是它允许进行独立于供应商的 XML 文档变换。如果对 XML 变换和 XSLT(XML 变换)不熟悉,请查看 dW 教程(请参阅参考资料)。虽然这种供应商独立性可以扩展目前 JAXP 只作为语法分析 API 的想法,但是它更是个迫切需要的设施,因为目前 XSL 处理器使用不同的方法和方式来允许用户和开发人员的交互。实际上,不同供应商的 XSL 处理器比 XML 语法分析器有更大的不同。
最初,JAXP 专家小组试图提供一个简单的 Transform 类和几个方法,以使样式表和后面的文档变换规范化。这个最初的尝试后来被证明很不可靠,但是,我很高兴地说:我们(JAXP 专家小组)在这之后的尝试中正取得重大进展。Scott Boag 和 Michael Kay 这两位当今的 XSL 处理器专家(分别致力于 Apache Xalan 和 SAXON)已经与他人一起开发出 TRaX。它支持更广范围的选项和功能,并为几乎所有的 XML 变换提供完整的支持 -- 所有这些都在 JAXP 支持下工作。
与 JAXP 的语法分析部分一样,执行 XML 变换需要三个基本步骤:
获得 Transformer factory
检索 Transformer
执行操作(变换)
使用 factory
在 JAXP 的变换部分中,使用名为 javax.xml.transform.TransformerFactory 的 factory。这个类与我在第一篇 JAXP 文章和本文前面提到的 SAXParserFactory 和 DocumentBuilderFactory 类相似。当然,只获得要使用的 factory 实例实在使太简单了:
清单 5. 获得 TransformerFactory 实例
TransformerFactory factory = TransformerFactory.newInstance();
一旦获得 factory 之后,可以在该 factory 上设置各种选项。那些选项将影响由该 factory 创建的 Transformer(稍后就要讲)的所有实例。(顺便提一句,可以通过 TransformerFactory 获得 javax.xml.transform.Templates 实例。模板是高级 JAXP 概念,本文不再详述。)
首先可以使用的选项是属性。这些不是 XML 属性,而是与在 XML 语法分析器中讲到的特性类似。属性允许将选项传递到底层的 XML 处理器(可能是 Apache Xalan、SAXON 或 Oracle 的 XSL 处理器)。它们高度依赖于供应商。与 JAXP 的语法分析方面类似,还提供 setAttribute() 方法和其搭档 getAttribute()。与 setProperty() 类似,前者接受属性名和 Object 值。与 getProperty() 类似,后者接受属性名并返回相关的 Object 值。
设置 ErrorListener 是可用的第二个选项。 ErrorListener 在 javax.xml.transform.ErrorListener 接口中定义,它允许捕获变换中出现的问题,并在程序中处理。如果熟悉 SAX,您将发现,该接口与 org.xml.sax.ErrorHandler 接口非常类似:
清单 6. ErrorListener 接口
package javax.xml.transform;
public interface ErrorListener {
public void warning(TransformerException exception)
throws TransformerException;
public void error(TransformerException exception)
throws TransformerException;
public void fatalError(TransformerException exception)
throws TransformerException;
}
通过创建该接口的一个实现,填充三个回调方法,并在正在使用的 TransformerFactory 实例上使用 setErrorListener() 方法,您将可以处理任何错误。
最后,提供一个方法来设置和检索由 factory 生成的实例的 URI(uniform resource indicator,统一资源标识,通常称为 URL)解析器。在 javax.xml.transform.URIResolver 中定义的接口也与其 SAX 对应接口 org.xml.sax.EntityResolver 类似。该接口有一个方法:
清单 7. URIResolver 接口
package javax.xml.transform;
public interface URIResolver {
public Source resolve(String href, String base)
throws TransformerException;
}
该接口在实现之后允许处理在 XML 构造(如 xsl:import 和 xsl:include)中发现的 URI。返回 Source(即将讲到)后,可以在遇到特定 URI 时指导变换器在不同位置搜索指定文档。例如,当遇到包括的 URI http://www.oreilly.com/oreilly.xsl 时,可以返回本地文档 oreilly.xsl,而无需进行网络访问。可以使用 TransformerFactory 的 setURIResolver() 方法设置 URIResolver 的实现,并使用 getURIResolver() 方法进行检索。
最后,一旦设置了想要的选项,就可以通过 factory 的 newTransformer 方法获得 Transformer 的一个或多个实例。
清单 8. 获得 Transformer
// Get the factory
TransformerFactory factory = TransformerFactory.newInstance();
// Configure the factory
factory.setErrorResolver(myErrorResolver);
factory.setURIResolver(myURIResolver);
// Get a Transformer to work with, with the options specified
Transformer transformer = factory.newTransformer(new StreamSource("sheet.xsl"));
如您所见,该方法将样式表作为输入,以在那个 Transformer 实例的所有变换中使用。换句话说,如果要使用样式表 A 和样式表 B 变换文档,那么,将需要两个 Transformer 实例,每个实例用于一个样式表。然而,如果要用同一个样式表变换多个文档,(我们称其为样式表 C),那么将只需要一个与样式表 C 关联的 Transformer 实例。
变换 XML
有了 Transformer 实例之后,就可以进行实际的 XML 变换了。这包含两个基本步骤:
设置要使用的 XSL 样式表
执行变换,指定 XML 文档和结果目标
如上所述,第一步实际上是最简单的。在从 factory 获得 Transformer 实例时必须提供样式表。必须通过提供 javax.xml.transform.Source 来指定该样式表的位置。目前为止,您已经在几个代码样本中见过 Source 接口,该接口是查找输入的方式 -- 无论是样式表、文档还是其它信息集。TRaX 不仅提供 Source 接口,还提供三个具体的实现:
javax.xml.transform.stream.StreamSource
javax.xml.transform.dom.DOMSource
javax.xml.transform.sax.SAXSource
这三个中的第一个 StreamSource 从某些 I/O 设备类型读取输入。提供一些构造器来接受 InputStream、Reader 或 String 系统标识作为输入。创建之后,可以将 StreamSource 传递到 Transformer 来使用。这可能是最常用的 Source 实现。对于从网络、输入流或其它某些静态表示法读取文档来说,这种方法是非常好的。
下一个 Source,DOMSource,允许从现有的 DOM 树读取信息。它提供一个构造器来接收 DOM org.w3c.dom.Node,然后在使用时从该 Node 读取。如果已经开始进行语法分析,并且 XML 文档已经在内存中以 DOM 结构存在,那么,这可能是为变换提供现有 DOM 树的理想方法。
SAXSource 允许从 SAX 生产者读取读取输入。这种 Source 实现接受 SAX org.xml.sax.InputSource 或 org.xml.sax.XMLReader 作为输入,并使用来自这些来源的事件作为输入。对于已经开始使用 SAX,并设置了回调,而且需要在变换之前触发回调的情况,这是理想的方法。
一旦获得了 Transformer 的实例(通过提供要在适当的 Source 中使用的样式表),就可以执行变换了。要完成变换,需如下使用 transform() 方法(没什么好奇怪的):
清单 9. 执行变换
// Get the factory
TransformerFactory factory = TransformerFactory.newInstance();
// Configure the factory
factory.setErrorResolver(myErrorResolver);
factory.setURIResolver(myURIResolver);
// Get a Transformer to work with, with the options specified
Transformer transformer = factory.newTransformer(new StreamSource("sheet.xsl"));
// Perform transformation on document A, and print out result
transfomer.transform(new StreamSource("documentA.xml"),
new StreamResult(System.out));
transform() 方法接受两个自变量:Source 实现和 javax.xml.transform.Result 实现。您应该已经看到其工作原理的对称性,并了解 Result 接口的功能性。Source 应该提供要变换的 XML 文档,而 Result 则应该提供变换的输出目标。与 Source 类似,Result 接口的 TRaX 和 JAXP 提供了三个具体的实现:
javax.xml.transform.stream.StreamResult
javax.xml.transform.dom.DOMResult
javax.xml.transform.sax.SAXResult
StreamResult 将 OutputStream(与上例中的 System.out 类似)或 Writer 作为构造机制。 DOMResult 将变换输出到 DOM Node(假设是 DOM org.w3c.dom.Document),而 SAXResult 将回调触发到由已变换的 XML 产生的 SAX ContentHandler。所有这些都与其对应的 Source 实现类似,您可以通过后者很容易了解其用法。
虽然上例显示了从流到流的变换,但是,源和结果的任何组合都是可能的。以下是几个示例:
清单 10. 各种 TRaX/JAXP 变换
// Perform transformation on document A, and print out result
transformer.transform(new StreamSource("documentA.xml"),
new StreamResult(System.out));
// Transform from SAX and output results to a DOM Node
transformer.transform(new SAXSource
(new InputSource("http://www.oreilly.com/catalog.xml")),
new DOMResult(DocumentBuilder.newDocument()));
// Transform from DOM and output to a File
transformer.transform(new DOMSource(myDomTree),
new StreamResult(new FileOutputStream("results.xml")));
// Use a custom source and result (JDOM)
transformer.transform(new org.jdom.trax.JDOMSource(myJdomDocument),
new
org.jdom.trax.JDOMResult(new org.jdom.Document()));
如您所见,TRaX 在从各种输入类型到各种输出类型的变换中,以及在使用各种格式(文件、内存中的 DOM 树、SAX 读取器等等)的 XSL 样式表中,TRaX 可以提供极大的灵活性。
浅尝即止
TRaX 中还有一些其它有用的功能,但是它们不象此处所示的功能那样常用,而且,本文也没有足够篇幅将它们全部列出。建议您在 JAXP 规范包括了 TRaX API 的时候(马上就要包括)查看它,它是用于 XML 变换的丰富而强有力的 API。您可以尝试输出特性,设置错误处理(不仅在 XSL 变换时,而且还在查找输入源时),并发现 API 中的各种好东西。开始享受吧,并告诉我们(专家小组)您的想法!
警告
在结束之前,还要给出一个警告。如果在三个月以后阅读本文,下载 JAXP 1.1 ,并得到编译器和运行时错误,请记住,本文是在 JAXP 1.1 将要完成的情况下写的。与任何早期的发行版一样,事情总是会发生变化的 -- 甚至在从我的便携式电脑到 developerWorks 生产过程中就会改变。换句话说,这里所讲的方法和功能在我写本文时是最新的,但是 JAXP 规范可以说仍处于变化中。请记住,重要的是本文的概念,而本文所述的方法可能会发生名称改动,甚至是轻微的行为改变。本文所概述的核心概念仍将以某种形式出现在 JAXP 1.1 中。因此,如果在 JAXP 1.1 的规范和参考实现都完成之时发现细节部分不完全正确,请认为本文所述的细节部分在概念上是正确的。
结束语
现在,您知道下一版的 JAXP 将有什么内容了。最后的规范公开草案应该在 2000 年底完成。实际的参考实现将在不久之后发布,并在 2001 年第一季度之前完成全部收尾工作。在查找 JAXP 参考资料时要小心,因为目前的规范草案(到 2000 年 11 月初为止)不包括本文所讨论的 TRaX API。在我写这篇文章时,规范正在修改中,因此,更新的规范将在不久后面世。
对那些一直在等待使用 JAXP(考虑到 1.0 版的限制,这是个相当明智的转变)的人来说,现在是开始使用它的时机了。在我的文章和书 Java and XML 中,由于 JAXP 1.0 在 SAX 2.0 和 DOM 级别 2 方面的不足,我对 JAXP 1.1 给予了含混的支持。现在,我高兴地承认 JAXP 1.1 是一个重大的进步。Java 和 XML 开发人员将发现,在编写对 XML 文档进行语法分析和变换的独立于供应商的代码方面,它是不可缺少的工具。那么,仔细查看 JAXP 1.0,并使您的应用程序作好准备。
这篇对 Benoit Marchal 所著的 XML by Example 第二版的预览给出了对 SAX 的翔实介绍,SAX 是用于处理 XML 的基于事件的 API,它已经成为事实上的标准。本篇预览讲述了何时使用 SAX 替换 DOM,概述了常用的 SAX 接口,并在基于 Java 的应用程序中提供了带有许多代码样本的详细示例。
经 Pearson Technology Group 的一个分部 Que Publishing 许可使用。
本文由即将出版的 XML by Example 第二版中的一章改编,介绍了 SAX,它是用于处理 XML 的基于事件的 API,SAX 是对“文档对象模型”或者 DOM 的补充,DOM 是用于由 W3C 发布的 XML 语法分析器的基于对象的 API。
您将了解到,SAX:
是基于事件的 API。
在一个比 DOM 低的级别上操作。
为您提供比 DOM 更多的控制。
几乎总是比 DOM 更有效率。
但不幸的是,需要比 DOM 更多的工作。
为什么出现另一个 API?
不要被名称欺骗。SAX 可能是 Simple API for XML,但它需要比 DOM 更多的工作。其回报- 更紧凑的代码 - 是值得努力的。
图 1 显示了典型 XML 程序的两个组件:
语法分析器,代表应用程序解码 XML 文件的软件组件。语法分析器有效地使开发者避开复杂的 XML 语法。
应用程序,它使用文件内容。
图 1. XML 程序的体系结构
显然,应用程序可以很简单(例如,在欧元和美元之间转换价格的应用程序)也可以非常复杂,例如,通过因特网订购货物的分布式电子贸易应用程序。
本章集中讨论图 1 中的虚线 - 语法分析器和应用程序之间的接口或 API(应用程序编程接口)。
基于对象和基于事件的接口
您可能已经知道语法分析器有两类接口 - 基于对象的和基于事件的接口。
在拙作的另一章中详细讨论了由 W3C 开发并发布的 DOM,它是基于对象的语法分析器的标准 API。这个关于 DOM 的简要概述只为您提供背景知识,以便您更好地全面理解 SAX。
作为基于对象的接口,DOM 通过在内存中显示地构建对象树来与应用程序通信。对象树是 XML 文件中元素树的精确映射。
DOM 易于学习和使用,因为它与基本 XML 文档紧密匹配。它对于我称为以 XML 为中心的应用程序(例如,浏览器和编辑器)也是很理想的。以 XML 为中心的应用程序为了操纵 XML 文档而操纵 XML 文档。
然而,对于大多数应用程序,处理 XML 文档只是其众多任务中的一种。例如,记帐软件包可能导入 XML 发票,但这不是其主要活动。计算帐户余额、跟踪支出以及使付款与发票匹配才是主要活动。记帐软件包可能已经具有一个数据结构(最有可能是数据库)。DOM 模型不太适合记帐应用程序,因为在那种情况下,应用程序必须在内存中维护数据的两份副本(一个是 DOM 树,另一个是应用程序自己的结构)。至少,在内存维护两次数据会使效率下降。对于桌面应用程序来说,这可能不是主要问题,但是它可能导致服务器瘫痪。
对于不以 XML 为中心的应用程序,SAX 是明智的选择。实际上,SAX 并不在内存中显式地构建文档树。它使应用程序能用最有效率的方法存储数据。
图 2 说明了应用程序如何在 XML 树及其自身数据结构之间进行映射。
图 2. 将 XML 结构映射成应用程序结构
基于事件的接口
正如其名称所暗示的,基于事件的语法分析器将事件发送给应用程序。这些事件类似于用户界面事件,例如,浏览器中的 ONCLICK 事件或者 Java 中的 AWT/Swing 事件。
事件通知应用程序发生了某件事并需要应用程序作出反应。在浏览器中,通常为响应用户操作而生成事件:当用户单击按钮时,按钮产生一个 ONCLICK 事件。
在 XML 语法分析器中,事件与用户操作无关,而与正在读取的 XML 文档中的元素有关。有对于以下方面的事件:
元素开始和结束标记
元素内容
实体
语法分析错误
图 3 显示语法分析器在读取文档时如何生成事件。
图 3. 语法分析器生成事件
清单 1 显示了 XML 格式的清单。它详细列出了不同公司对 XML 培训的收费。图 4 显示了价目表文档的结构。
清单 1. pricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.1">
<xbe:product>XML Training</xbe:product>
<xbe:price-quote price="999.00" vendor="Playfield Training"/>
<xbe:price-quote price="699.00" vendor="XMLi"/>
<xbe:price-quote price="799.00" vendor="WriteIT"/>
<xbe:price-quote price="1999.00" vendor="Emailaholic"/>
</xbe:price-list>
图 4. 价目表的结构
XML 语法分析器读取并解释该文档。每当它识别出文档中的某些内容,就会生成一个事件。
读取清单 1 时,语法分析器首先读取 XML 声明并生成文档开始事件。当它遇到第一个开始标记 <xbe:price-list> 时,语法分析器生成它的第二个事件来通知应用程序已经遇到了 price-list 元素。
接下来,语法分析器看到 product 元素的开始标记(为简单起见,在本文其余部分,我将忽略名称空格和缩进空格)并生成它的第三个事件。
在开始标记后,语法分析器看到 product 元素的内容:XML Training,它产生另一个事件。
下一个事件指出 product 元素的结束标记。语法分析器已经完成了对 product 元素的语法分析。到目前为止,它已经激发了 5 个事件:product 元素的 3 个事件,一个文档开始事件和一个 price-list 开始标记事件。
语法分析器现在移动到第一个 price-quote 元素。它为每个 price-quote 元素生成两个事件:一个开始标记事件和一个结束标记事件。
是的,即使将结束标记简化为开始标记中的 / 字符,语法分析器仍然生成一个结束事件。
有 4 个 price-quote 元素,所以语法分析器在分析它们时生成 8 个事件。最后,语法分析器遇到 price-list 的结束标记并生成它的最后两个事件:结束 price-list 和文档结束。
如图 5 所示,这些事件共同向应用程序描述了文档树。开始标记事件意味着“转到树的下一层”,而结束标记元素意味着“转到树的上一层”。
图 5. 语法分析器如何隐含地构建树
请注意,语法分析器传递了足够信息以构建 XML 文档的文档树,但是与 DOM 语法分析器不同,它并不显式地构建该树。
为什么使用基于事件的接口?
现在,我敢肯定你已经糊涂了。应该使用哪一种类型的 API,应该何时使用它 - SAX 还是 DOM?不幸的是,这个问题没有明确的答案。这两种 API 中没有一种在本质上更好;他们适用于不同的需求。
经验法则是在需要更多控制时使用 SAX;要增加方便性时,则使用 DOM。例如,DOM 在脚本语言中很流行。
注:自然的接口
对于语法分析器来说,基于事件的接口是最理想的选择:它只需报告它看见了什么。
采用 SAX 的主要原因是效率。SAX 比 DOM 做的事要少,但提供了对语法分析器的更多控制。当然,如果语法分析器的工作减少,则意味着您(开发者)有更多的工作要做。
而且,正如我们已讨论的,SAX 比 DOM 消耗的资源要少,这只是因为它不需要构建文档树。
在 XML 早期,DOM 得益于 W3C 批准的官方 API 这一身份。逐渐地,开发者选择了功能性而放弃了方便性,并转向了 SAX。
SAX 的主要限制是它无法向后浏览文档。实际上,激发一个事件后,语法分析器就将其忘记。如您将看到的,应用程序必须显式地缓冲其感兴趣的事件。
注:SAX 构建的树
如果需要,应用程序可以用它从语法分析器接收的事件构建 DOM 树。事实上,几个 DOM 语法分析器是在 SAX 语法分析器的基础上构建的。
当然,无论它实现 SAX 还是 DOM API,语法分析器都做许多工作:它读取文档,强制实施 XML 语法并解析实体 - 先只列举这几个。验证语法分析器还强制实施文档模式。
使用语法分析器有很多原因,并且您应该掌握 API、SAX 和 DOM。它使您能灵活地根据手上的任务来选择最好的 API。幸好,现代语法分析器同时支持两种 API。
SAX,功能强大的 API
SAX 是由 XML-DEV 邮件列表的成员开发的一种用于基于事件的语法分析器的标准和简单的 API。SAX 是“Simple API for XML”的缩写。
SAX 最初是为 Java 而定义,但是它也可以用于 Python、Perl、C++ 和 COM(Windows 对象)。以后一定还有更多的语言绑定。而且,通过 COM,SAX 语法分析器还可以用于所有 Windows 编程语言,包括 Visual Basic 和 Delphi。
与 DOM 不同,SAX 没有经过官方标准机构的认可,但是它被广泛使用并被视为事实上的标准。(现在,SAX 由 David Megginson 编辑,但是他已经宣布将要退休。)
如您所见,在浏览器中,DOM 是首选的 API。因此,本章中的示例是用 Java 编写的。(如果您觉得需要一个 Java 速成课程,请转至拙作的附录 A 或者 developerWorks Java 区的“教学”部分。)
一些支持 SAX 的语法分析器包括 Xerces,Apache parser(以前的 IBM 语法分析器)、MSXML(Microsoft 语法分析器)和 XDK(Oracle 语法分析器)。这些语法分析器是最灵活的,因为它们还支持 DOM。
有几个语法分析器仅提供 SAX,例如 James Clark 的 XP、苐fred 和 Vivid Creations 的 ActiveSAX(请参阅参考资料)。
SAX 入门
清单 2 是查找清单 1 中最便宜价格的 Java 应用程序。该应用程序打印出最优的价格和供应商名称。
编译示例
要编这个应用程序,需要适用于您平台的“Java 开发工具箱(JDK)”(请参阅参考资料)。对于该示例,Java Runtime(Java 运行时环境)是不够的。
注意
Java 难以处理包含空格的路径。如果“最便宜”的公司抱怨它无法找到文件,请检查目录中的错误空格。
从作者网站的 XBE2 页面下载本摘录的清单。下载内容包括 Xerces。如果清单有问题,请访问作者网站以获取更新。
在名为 Cheapest.java 的文件中保存清单 2。转至 DOS 提示符,更改到保存 Cheapest.java 的目录,然后在 DOS 提示符处发出下列命令来编译:
mkdir classes
set classpath=classes;lib\xerces.jar
javac -d classes src\Cheapest.java
编译将在 classes 目录中安装 Java 程序。这些命令假设您已经在 lib 目录中安装了 Xerces,并且在 src 目录中安装了清单 2。如果在另一个目录下安装语法分析器,则可能必须修改 classpath(第二条命令)。
要对价目表运行应用程序,请发出下面的命令:
java com.psol.xbe2.Cheapest data\pricelist.xml
结果应该是:
The cheapest offer is from XMLi ($699.00)
这条命令假设清单 1在一个名为 data\pricelist.xml 的文件中。同样,您可能需要修改系统路径。
技巧:关于事件处理器
事件处理器不调用语法分析器。实际上正好相反:语法分析器调用事件处理器。困惑了?想想 AWT 事件。连接到按钮的事件处理器不调用按钮。它等待按钮被单击。
事件处理器的逐步讨论
将 SAX 中的事件定义为连接到特定 Java 接口的方法。本节将逐步复查清单 2。下面一节为您提供关于主要 SAX 接口的更多信息。
声明事件处理器的最简单方案是继承 SAX 提供的 DefaultHandler:
public class Cheapest
extends DefaultHandler
该应用程序仅实现一个事件处理器 startElement(),语法分析器在遇到开始标记时调用它。语法分析器将对文档 <xbe:price-list>、<xbe:product> 和 <xbe:price-quote> 中的每个开始标记调用 startElement()。
在清单 2 中,事件处理器仅对 price-quote 感兴趣,所以仅对它测试。该处理器对其它元素的事件不作任何处理。
if(uri.equals(NAMESPACE_URI) && name.equals("price-quote"))
{
// ...
}
当事件处理器发现 price-quote 元素时,它从属性列表中抽取供应商名称和价格。有了这些信息,查找最便宜的产品就是一个简单的比较处理了。
String attribute =
attributes.getValue("","price");
if(null != attribute)
{
double price = toDouble(attribute);
if(min > price)
{
min = price;
vendor = attributes.getValue("","vendor");
}
}
请注意,事件处理器接收元素名称、名称空间和属性列表作为来自语法分析器的参数。
现在,让我们将注意力转向 main() 方法。它创建一个事件处理器对象和一个语法分析器对象:
Cheapest cheapest = new Cheapest();
XMLReader parser =
XMLReaderFactory.createXMLReader(PARSER_NAME);
XMLReader 和 XMLReaderFactory 由 SAX 定义。XMLReader 是一种 SAX 语法分析器。factory 是用于创建 XMLReaders 的帮助器类。
main() 设置一个语法分析器功能以请求名称空间处理,并且使用语法分析器注册事件处理器。最后,main() 使用至 XML 文件的 URI 调用 parse() 方法:
parser.setFeature("http://xml.org/sax/features/namespaces",true);
parser.setContentHandler(cheapest);
parser.parse(args[0]);
技巧:名称空间
缺省情况下,将 http://xml.org/sax/features/namespaces 设置为真,但是显式地将它设置为真将使代码更具可读性。
看似无关的 parse() 方法触发对 XML 文档的语法分析,这导致了调用事件处理器。我们的 startElement() 方法正是在执行这个方法期间被调用的。在调用 parse() 背后发生了很多事情。
最后但很重要的一点,main() 打印出结果:
Object[] objects = new Object[]
{
cheapest.vendor,
new Double(cheapest.min)
};
System.out.println(MessageFormat.format(MESSAGE,objects));
等一下!Cheapest.vendor 和 Cheapest.min 何时获取它们的值?我们不在 main() 中显式地设置它们!确实如此;这是事件处理器的工作。最后由 parse() 调用事件处理器。这就是事件处理的美妙之处。
注意
请记住,除非已经安装了“Java 开发工具箱”,否则不能编译这些示例。
最后,可能有一个错误类似于:
src\Cheapest.java:7: Package org.xml.sax
not found in import.
import org.xml.sax.*;
或
Can't find class com/psol/xbe2/Cheapest
or something it requires
这极有可能出自以下原因:
类路径(第二个命令,classes;lib\xerces.jar))不正确。
在最后一个命令(com.psol.xbe2.Cheapest)中输入了不正确的类名称。
常用的 SAX 接口和类
到目前为止,我们仅讨论了一个事件(startElement())。在继续之前,让我们研究一下 SAX 定义的主接口。
注:SAX 版本
到目前为止,有两个 SAX 版本:SAX1 和 SAX2。本章仅介绍 SAX2 API。SAX1 与 SAX2 很相似,但是它缺少名称空间处理。
SAX 将其事件分为几个接口:
ContentHandler 定义与文档本身关联的事件(例如,开始和结束标记)。大多数应用程序都注册这些事件。
DTDHandler 定义与 DTD 关联的事件。然而,它不定义足够的事件来完整地报告 DTD。如果需要对 DTD 进行语法分析,请使用可选的 DeclHandler。DeclHandler 是 SAX 的扩展,并且不是所有的语法分析器都支持它。
EntityResolver 定义与装入实体关联的事件。只有少数几个应用程序注册这些事件。
ErrorHandler 定义错误事件。许多应用程序注册这些事件以便用它们自己的方式报错。
注:SAX 的成功之处
本节不是 SAX 的全面参考。相反,它集中讨论最常用的类。
为简化工作,SAX 在 DefaultHandler 类中提供了这些接口的缺省实现。在大多数情况下,为应用程序扩展 DefaultHandler 并覆盖相关的方法要比直接实现一个接口更容易。
XMLReader
为注册事件处理器并启动语法分析器,应用程序使用 XMLReader 接口。如我们所见,parse(),这种 XMLReader 方法,启动语法分析:
parser.parse(args[0]);
XMLReader 的主要方法是:
parse() 对 XML 文档进行语法分析。parse() 有两个版本;一个接受文件名或 URL,另一个接受 InputSource 对象(请参阅“InputSource”一节)。
setContentHandler()、setDTDHandler()、 setEntityResolver() 和 setErrorHandler() 让应用程序注册事件处理器。
setFeature() 和 setProperty() 控制语法分析器如何工作。它们采用一个特性或功能标识(一个类似于名称空间的 URI 和值)。功能采用 Boolean 值,而特性采用“对象”。
最常用的 XMLReaderFactory 功能是:
http:// xml.org/sax/features/namespaces ,所有 SAX 语法分析器都能识别它。如果将它设置为 true(缺省值),则在调用 ContentHandler 的方法时,语法分析器将识别出名称空间并解析前缀。
http://xml.org/sax/features/validation ,它是可选的。如果将它设置为 true,则验证语法分析器将验证该文档。非验证语法分析器忽略该功能。
XMLReaderFactory
XMLReaderFactory 创建语法分析器对象。它定义 createXMLReader() 的两个版本:一个采用语法分析器的类名作为参数,另一个从 org.xml.sax.driver 系统特性中获得类名称。
对于 Xerces,类是 org.apache.xerces.parsers.SAXParser。应该使用 XMLReaderFactory,因为它易于切换至另一种 SAX 语法分析器。实际上,只需要更改一行然后重新编译。
XMLReader parser = XMLReaderFactory.createXMLReader(
"org.apache.xerces.parsers.SAXParser");
为获得更大的灵活性,应用程序可以从命令行读取类名或使用不带参数的 createXMLReader()。因此,甚至可以不重新编译就更改语法分析器。
InputSource
InputSource 控制语法分析器如何读取文件,包括 XML 文档和实体。
在大多数情况下,文档是从 URL 装入的。但是,有特殊需求的应用程序可以覆盖 InputSource。例如,这可以用来从数据库中装入文档。
ContentHandler
ContentHandler 是最常用的 SAX 接口,因为它定义 XML 文档的事件。
如您所见,清单 2 实现在 ContentHandler 中定义的事件 startElement()。它用语法分析器注册 ContentHandler:
Cheapest cheapest = new Cheapest();
// ...
parser.setContentHandler(cheapest);
ContentHandler 声明下列事件:
startDocument()/endDocument() 通知应用程序文档的开始或结束。
startElement()/endElement() 通知应用程序标记的开始或结束。属性作为 Attributes 参数传递(请参阅下面一节“属性”)。即使只有一个标记,“空”元素(例如, <img href="logo.gif"/>)也生成 startElement() 和 endElement()。
startPrefixMapping()/endPrefixMapping() 通知应用程序名称空间作用域。您几乎不需要该信息,因为当 http://xml.org/sax/features/namespaces 为 true 时,语法分析器已经解析了名称空间。
当语法分析器在元素中发现文本(已经过语法分析的字符数据)时,characters()/ ignorableWhitespace() 会通知应用程序。要知道,语法分析器负责将文本分配到几个事件(更好地管理其缓冲区)。 ignorableWhitespace 事件用于由 XML 标准定义的可忽略空格。
processingInstruction() 将处理指令通知应用程序。
skippedEntity() 通知应用程序已经跳过了一个实体(即,当语法分析器未在 DTD/schema 中发现实体声明时)。
setDocumentLocator() 将 Locator 对象传递到应用程序;请参阅后面的 Locator 一节。请注意,不需要 SAX 语法分析器提供 Locator,但是如果它提供了,则必须在任何其它事件之前激活该事件。
属性
在 startElement() 事件中,应用程序在 Attributes 参数中接收属性列表。
String attribute = attributes.getValue("","price");
Attributes 定义下列方法:
getValue(i)/getValue(qName) /getValue(uri,localName) 返回第 i 个属性值或给定名称的属性值。
getLength() 返回属性数目。
getQName(i)/getLocalName(i)/getURI(i) 返回限定名(带前缀)、本地名(不带前缀)和第 i 个属性的名称空间 URI。
getType(i)/getType(qName)/getType(uri,localName) 返回第 i 个属性的类型或者给定名称的属性类型。类型为字符串,即在 DTD 所使用的:“CDATA”、“ID”、“IDREF”、 “IDREFS”、“NMTOKEN”、“NMTOKENS”、 “ENTITY”、“ENTITIES” 或 “NOTATION”
.
注意
Attributes 参数仅在 startElement() 事件期间可用。如果在事件之间需要它,则用 AttributesImpl 复制一个。
定位器
Locator 为应用程序提供行和列的位置。不需要语法分析器来提供 Locator 对象。
Locator 定义下列方法:
getColumnNumber() 返回当前事件结束时所在的那一列。在 endElement() 事件中,它将返回结束标记所在的最后一列。
getLineNumber() 返回当前事件结束时所在的行。在 endElement() 事件中,它将返回结束标记所在的行。
getPublicId() 返回当前文档事件的公共标识。
getSystemId() 返回当前文档事件的系统标识。
DTDHandler
DTDHandler 声明两个与 DTD 语法分析器相关的事件。
notationDecl() 通知应用程序已经声明了一个标记。
nparsedEntityDecl() 通知应用程序已经发现了一个未经过语法分析的实体声明。
EntityResolver
EntityResolver 接口仅定义一个事件 resolveEntity(),它返回 InputSource(在另一章讨论)。
因为 SAX 语法分析器已经可以解析大多数 URL,所以很少应用程序实现 EntityResolver。例外情况是目录文件(在另一章中讨论),它将公共标识解析成系统标识。如果在应用程序中需要目录文件,请下载 Norman Walsh 的目录软件包(请参阅参考资料)。
ErrorHandler
ErrorHandler 接口定义错误事件。处理这些事件的应用程序可以提供定制错误处理。
安装了定制错误处理器后,语法分析器不再抛出异常。抛出异常是事件处理器的责任。
接口定义了与错误的三个级别或严重性对应的三个方法:
warning() 警示那些不是由 XML 规范定义的错误。例如,当没有 XML 声明时,某些语法分析器发出警告。它不是错误(因为声明是可选的),但是它可能值得注意。
error() 警示那些由 XML 规范定义的错误。
fatalError() 警示那些由 XML 规范定义的致命错误。
SAXException
SAX 定义的大多数方法都可以抛出 SAXException。当对 XML 文档进行语法分析时,SAXException 通知一个错误。
错误可以是语法分析错误也可以是事件处理器中的错误。要报告来自事件处理器的其它异常,可以将异常封装在 SAXException 中。
示例:假设在处理 startElement 事件时,事件处理器捕获了一个 IndexOutOfBoundsException。事件处理器可以将 IndexOutOfBoundsException 封装在 SAXException 中:
public void startElement(String uri,
String name,
String qualifiedName,
Attributes attributes)
{
try
{
// the code may throw an IndexOutOfBoundsException
}
catch(IndexOutOfBounds e)
{
throw new SAXException(e);
}
}
SAXException 一直向上传递到 parse() 方法,它在那里被捕获并进行解释。
try
{
parser.parse(uri);
}
catch(SAXException e)
{
Exception x = e.getException();
if(null != x)
if(x instanceof IndexOutOfBoundsException)
// process the IndexOutOfBoundsException
}
维护状态
清单 1 对于 SAX 语法分析器是很方便的,因为它将信息存储为价格元素的属性。应用程序只需要注册 startElement()。
示例清单 3 更复杂,因为信息分散到了几个元素中。特别是,根据不同的交付延迟,供应商有不同的价格。如果用户愿意等待,他(或她)可能得到更好的价格。图 6 演示了文档结构。
清单 3. xtpricelist.xml
<?xml version="1.0"?>
<xbe:price-list xmlns:xbe="http://www.psol.com/xbe2/listing8.3">
<xbe:name>XML Training</xbe:name>
<xbe:vendor>
<xbe:name>Playfield Training</xbe:name>
<xbe:price-quote delivery="5">999.00</xbe:price-quote>
<xbe:price-quote delivery="15">899.00</xbe:price-quote>
</xbe:vendor>
<xbe:vendor>
<xbe:name>XMLi</xbe:name>
<xbe:price-quote delivery="3">2999.00</xbe:price-quote>
<xbe:price-quote delivery="30">1499.00</xbe:price-quote>
<xbe:price-quote delivery="45">699.00</xbe:price-quote>
</xbe:vendor>
<xbe:vendor>
<xbe:name>WriteIT</xbe:name>
<xbe:price-quote delivery="5">799.00</xbe:price-quote>
<xbe:price-quote delivery="15">899.00</xbe:price-quote>
</xbe:vendor>
<xbe:vendor>
<xbe:name>Emailaholic</xbe:name>
<xbe:price-quote delivery="1">1999.00</xbe:price-quote>
</xbe:vendor>
</xbe:price-list>
图 6. 价目表结构
要找到最好的生意,应用程序必须从几个元素搜集信息。但是,语法分析器可以最多为每个元素生成三个事件 - startElement()、characters() 和 endElement()。应用程序必须以某种方法将事件和元素相关联。
清单 4 是一个新建的 Java 应用程序,它查找价目表中的最优价格。当查找最优价格时,它考虑到了客户对交付日期的需求。实际上,清单 3 中最便宜的供应商(XMLi)也是最慢的。另一方面,Emailaholic 很贵,但是它可以在两天内交付。
您可以如前面介绍的 Cheapest 应用程序那样编译并运行该应用程序。结果取决于对交付日期的需求。您将注意到这个程序采用两个参数:文件名和客户愿意等待的最长延迟。
java com.psol.xbe2.BestDeal data/xtpricelist.xml 60
返回:
The best deal is proposed by XMLi. A(n) XML Training delivered[ccc]
in 45 days for $699.00
而:
java com.psol.xbe2.BestDeal data/xtpricelist.xml 3
返回:
The best deal is proposed by Emailaholic. A(n) XML Training[ccc]
delivered in 1 days for $1,999.00
分层体系结构
清单 4 是目前为止您所见到的最复杂的应用程序。这没什么不寻常的:SAX 语法分析器的级别很低,所以应用程序必须接管本来由 DOM 才能完成的大量工作。
应用程序是围绕两个类组织的:SAX2BestDeal 和 BestDeal。SAX2BestDeal 管理 SAX 语法分析器之间的接口。它用一致的方法来管理状态并将事件分组。
BestDeal 具有执行价格比较的逻辑。它还以结构形式保持为应用程序而不是为 XML 优化的信息。图 7 演示了该应用程序的体系结构。图 8 显示了 UML 类图。
图 7. 应用程序的体系结构
图 8. 应用程序的类图
SAX2BestDeal 处理几个事件:startElement()、endElement() 和 characters()。SAX2BestDeal 一直跟踪其在文档树中的位置。
例如,在 characters() 事件中,SAX2BestDeal 需要知道文本是名称、价格还是可以忽略的空格。而且,有两个 name 元素:price-list 的 name 和 vendor 的 name。
状态
与 DOM 语法分析器不同,SAX 语法分析器不提供状态信息。应用程序负责跟踪它自己的状态。这有几个可选实体。清单 4 标识有意义的状态以及它们之间的转换。从图 6 中的文档结构中获得该信息并不困难。
很明显,应用程序将首先遇到 price-list 标记。因此,第一个状态应该是位于 price-list 内。从那里开始,应用程序到达一个 name。因此,第二个状态是位于 price-list 的 name 内。
下一个元素必须是 vendor,因此第三个状态是位于 price-list 的 vendor 内。第四个状态是位于 price-list 的 vendor 的 name 内,因为 name 跟在 vendor 后。
name 后面是一个 price-quote 元素,相应的状态是位于 price-list 的 vendor 的 price 内。随后,语法分析器遇到已经有状态存在的 price-quote 或 vendor。
在带有状态和转换的图上(例如图 9 所示)会更容易使这个概念可视化。请注意根据您在处理 price-list/name 还是 price-list/vendor/name,有两个不同的状态与两个不同的名称元素相关联。
图 9. 状态转换图
在清单 4 中状态变量存储当前状态:
final protected int START = 0,
PRICE_LIST = 1,
PRICE_LIST_NAME = 2,
VENDOR = 3,
VENDOR_NAME = 4,
VENDOR_PRICE_QUOTE = 5;
protected int state = START;
转换
转换 1 状态变量的值根据事件而相应更改。在本示例中,elementStart() 更新状态:
ifswitch(state)
{
case START:
if(name.equals("price-list"))
state = PRICE_LIST;
break;
case PRICE_LIST:
if(name.equals("name"))
state = PRICE_LIST_NAME;
// ...
if(name.equals("vendor"))
state = VENDOR;
break;
case VENDOR:
if(name.equals("name"))
state = VENDOR_NAME;
// ...
if(name.equals("price-quote"))
state = VENDOR_PRICE_QUOTE;
// ...
break;
}
SAX2BestDeal 有几个实例变量来存储当前 name 和 price-quote 的内容。实际上,它维护树的一个小子集。请注意,与 DOM 不同,它从不拥有整个树,因为当应用程序使用过 name 和 price-quote 之后,它会废弃它们。
这是很有效的内存策略。事实上,您可以处理几十亿字节的文件,因为在任何时候,内存中只有一个小子集。
转换 2 语法分析器对文档中的每个字符(包括缩进)调用 characters()。只有记入 name 和 price-quote 中的文本才有意义,因此事件处理器使用状态。
switch(state)
{
case PRICE_LIST_NAME:
case VENDOR_NAME:
case VENDOR_PRICE_QUOTE:
buffer.append(chars,start,length);
break;
}
转换 3 endElement() 的事件处理器更新状态,并调用 BestDeal 来处理当前元素:switch(state)
{
case PRICE_LIST_NAME:
if(name.equals("name"))
{
state = PRICE_LIST;
setProductName(buffer.toString());
// ...
}
break;
case VENDOR_NAME:
if(name.equals("name"))
state = VENDOR;
// ...
break;
case VENDOR_PRICE_QUOTE:
if(name.equals("price-quote"))
{
state = VENDOR;
// ...
compare(vendorName,price,delivery);
// ...
}
break;
case VENDOR:
if(name.equals("vendor"))
state = PRICE_LIST;
// ...
break;
case PRICE_LIST:
if(name.equals("price-list"))
state = START;
break;
}
学过的课程
清单 4 是典型的 SAX 应用程序。有一个 SAX 事件处理器(SAX2BestDeal),它用最适合应用程序的格式将事件打包。
注:状态还是堆栈?
使用状态变量的替换方法是使用“堆栈”。将元素名(或另一个标识)推入 startElement();然后在 endElement() 中弹出它。
应用程序逻辑(在 BestDeal 中)与事件处理器保持分离。事实上,在很多情况下,都独立于 XML 来编写应用程序逻辑。
分层方法在应用程序逻辑和语法分析之间建立一个明显的分界。
示例也清晰地说明了 SAX 比 DOM 更高效,但是它需要程序员完成更多工作。特别是,程序员必须显式地管理状态和状态之间的转换。(在 DOM 中,状态在树的递归遍历过程中是隐含的。)
灵活性
XML 是非常灵活的标准。但实际上,XML 应用程序的灵活性取决于您,程序员,如何创建它们。本节提供一些技巧,以确保您的应用程序利用 XML 的灵活性。
为灵活性而构建
BestDeal 应用程序对 XML 文档结构的约束很少。如果在 XML 文档中添加元素,它们就会被忽略。例如,BestDeal 将接受下列 vendor 元素:
<xbe:vendor>
<xbe:name>Playfield Training</xbe:name>
<xbe:contact>John Doe</xbe:contact>
<xbe:price-quote delivery="5">999.00</xbe:price-quote>
<xbe:price-quote delivery="15">899.00</xbe:price-quote>
</xbe:vendor>
但是将忽略联系信息。通常,简单地忽略未知元素是个好主意 - HTML 浏览器就总这样做。
强制实施结构
但是,从事件处理器验证它们的结构并不困难。下列代码片断(摘自 startElement())检查结构,并且如果 vendor 元素包含除名称或价格以外的任何元素,则抛出 SAXException。
case VENDOR:
if(name.equals("name"))
{
state = VENDOR_NAME;
buffer = new StringBuffer();
}
else if(name.equals("price-quote"))
{
state = VENDOR_PRICE_QUOTE;
String st = attributes.getValue("","delivery");
delivery = Integer.parseInt(st);
buffer = new StringBuffer();
}
else
throw new SAXException("Expecting <xbe:name> or <xbe:price-quote>");
break;
如果清单带有 contact 元素,它将报告:
org.xml.sax.SAXException: Expecting <xbe:name> or <xbe:price-quote>
但是,如果实际上应用程序真正依赖于文档的结构,那么最好编写一个模式并使用验证语法分析器。
下一步做什么?
在这个 XML by Example 第二版的摘录中,您学习了如何读取 XML 文档。该书的其余部分将指导您学习如何编写文档的整个过程,并就此结束本课程。或者,您可以参考在线教程和文章,以完成学习。(当然,我希望您选择我的书。)
参考资料
从作者网站的 XBE2 页面下载本摘录的清单。
在本书的发布者页面上查找有关 XML by Example 第二版的更多信息,该页面包括到书店网站的链接,您可以在那里订购这本书(将于 9 月出版)。
通过在官方的 SAX 主页上浏览 FAQ、历史和软件支持来更深入了解 SAX,它是由 XML-dev 列表上的团体开发的。从 SourceForge 上的 SAX 项目页面下载 SAX。
支持 SAX 的语法分析器:
·
Xerces,Apache 语法分析器,以前的 IBM 语法分析器,是几种支持 DOM 和 SAX 的语法分析器之一。目前有 Java 和 C++ 版本,并且还可以与 Perl 和 COM 结合使用。
请在 SAX 页面上的“SAX2 的驱动程序和应用程序”中参阅更多语法分析器、XSLT 引擎、驱动程序、实用程序和其它支持 SAX2 的软件。
如果在应用程序中需要目录文件,请下载 Norman Walsh 的目录软件包。
在 developerWorks XML 专区中查看最近发布的有关 SAX 的技巧,包括 Turning on validation in SAX-based parsers 和 Using SAX EntityResolver。
请仔细查看 developerWorks XML 专区作者专栏的 Working XML 中构建的更多 XML 应用程序示例。
请在 WAS 高级版 3.5 在线帮助的 technical background info on XML 中查看合并了 XML 语法分析器和 XSLT 引擎的 IBM WebSphere Application Server 如何支持 XML 。
关于 SAX 和 DOM 的幻灯片介绍,请从 Kelvin Lawrence(IBM XML 技术方面的 CTO)的发言人页面演讲稿“A Detailed Introduction to Parsing and Processing XML documents using Java(TM) technology”中打开幻灯片的 PDF。
我们在使用SAX对XML文件做解析时,往往会被大量的if或者switch语句所困绕。如果使用适当的设计模式,结合适当的算法,则可以避免在解析程序中到处散布着大量的判断语句。
关于SAX
了解XML Parser的朋友都应该知道,XML有2种基本的解析方式:DOM(Document Object Model)、SAX(Simple Api of Xml)。其中,SAX对XML文档的解析不会象DOM那样需要形成树,从而减少了内存的占用,而且SAX是事件触发,用户只需要实现自己关心的接口即可,简化了编程量。
在实际的应用中,如果只是对XML的内容扫描一次,不需要对节点重复操作,则SAX解析方式应该是用户的首选。但是,它也有自己的弱点:
SAX模式是事件触发,用户实现相应的接口方法,对不同节点的判断,主要是根据节点名称。按照常规的处理方式,为了判断当前处理的节点名称,需要使用大量的判断语句。这样,大量的判断语句就散布到解析程序中去了。对于节点的变更,需要修改解析程序中所有接口方法的判断语句,使得整个程序框架难以维护。
组合的设计模式(Composite)
关于设计模式
谈到程序设计,就不能不考虑设计模式。设计模式可以成为我们做设计时相互交流的语言,就象UML是我们建模时统一使用的语言一样。
每一个模式描述了一个在我们做设计时不断重复遇到的问题,以及该问题的解决方案的核心。这样,我们就能一次又一次地使用该方案而不必做重复劳动。其核心在于提供了相关问题的解决方案。
设计模式不能够随意使用。通常我们通过引入额外的间接层次获得灵活性和可变性的同时,也使设计变得更复杂并牺牲了一定的性能。一个设计模式只有当它提供的灵活性是真正需要的时候,才有必要使用。
结构图:
Composite模式的关键是抽象类Component,它既可以代表叶节点,也可以代表组合节点;它定义了所有节点的公共操作。
意图:
将对象组合成树形结构以表示"部分-整体"的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
适用性:
在下列情况下可以考虑使用"组合"模式:
你想表示对象的部分-整体层次结构。
你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
我们的使用背景
我们小组计划使用XML作为配置文件,来描述页面的显示,从而能够自动生成相应的JSP代码。下面,我会说明我们的设计思想及其变迁过程,与大家共勉。
在我们的系统里,我们把页面简化后,将页面元素限制在下面若干种。最初,我们的DTD定义如下:
<?xml version="1.0" encoding="gb2312-1"?>
<!-- edited with XML Spy v3.0 NT (http://www.xmlspy.com) by liwei (lulusoft) -->
<!ELEMENT UserInterface (option+, datashow, control)>
<!ELEMENT datashow (maintable?, detailtable, maintable?)>
<!--maintable定义主表(表单)的内容-->
<!ELEMENT maintable (lable, input+)+>
<!--detailtable定义细表(列表)的表头、实际内容;header在生成JSP样式时使用,column在JSP动态生成HTML页面时使用。-->
<!ELEMENT detailtable (header, column)+>
<!--control包括所有的按钮-->
<!ELEMENT control (button+, pagebutton?)>
<!--lable的内容就是它的value-->
<!ELEMENT lable (#PCDATA)>
<!ELEMENT input EMPTY>
<!ATTLIST input
hidden (true | false) "false"
onkeypress CDATA #IMPLIED>
<!--checkbox没有属性、没有值-->
<!ELEMENT checkbox EMPTY>
<!--button的内容就是它的显示值,action属性指定该按钮的连接,onclick属性指定onclick事件的响应-->
<!ATTLIST button
action CDATA #IMPLIED
onclick CDATA #IMPLIED
>
<!--每个header代表列表表头的一列,只有复选框才允许表头为button(全选/全不选)-->
<!ELEMENT header (lable | button)>
<!--column定义JSP动态生成HTML页面时该列的内容,每个column可能会包含若干个隐含域,只有第1列可能出现checkbox,并且,checkbox不能有隐含域-->
<!ELEMENT button (#PCDATA)>
<!ELEMENT column (input+ | checkbox)>
<!--pagebutton只是一个标签,定义了一组翻页按钮,包括:上一页、下一页、第几页-->
<!ELEMENT pagebutton EMPTY>
<!--option定义在当前情况(type)下,每个按钮是否需要输出;每种情况都需要定义一套option-->
<!ELEMENT option (type, (enable | disable)+)>
<!ELEMENT type (#PCDATA)>
<!ELEMENT enable EMPTY>
<!ELEMENT disable EMPTY>
在最初的XML解析器中,没有规划对象接口,每个节点的操作针对其特点,各不相同,属性、子元素的存取也没有统一。而且,某些节点没有设计对应的JAVA类,某些JAVA类对应了若干个节点。所以,解析程序中充斥着大量的判断语句。
后来,我们总结了系统需求,发现:有几个基本节点:lable、input、botton,其它的是容器节点,可以包含若干种基本节点。如果想去掉程序中所有的判断语句,就需要对任意节点的操作完全一致。这样,就需要修改我们的设计:
1. 扩充DTD,对每个节点增加了classname 属性,使得每个节点对象的创建完全一致,通过classname来动态创建;新的DTD见参考资料。
2. 扩充IxmlElement接口,而且,每个节点都有对应的JAVA类,同时,采用了组合的设计模式(Composite),使得对叶子节点(基本节点)和非叶子节点(容器节点)的处理能够统一。
新的类系图如下:
其次,需要采用新的算法。
1. 考虑到SAX是事件触发,我们使用了出入堆栈的方式,基本描述如下:
2. 在每个节点元素的开始(startElement接口方法),创建节点对象,并且,保存所有的属性,然后入栈;
3. 在每个节点元素的结束(endElement接口方法),从堆栈中弹出2个对象,第1个对象增加到第2个对象中,再把第2个对象入栈;
解析程序的详细代码如下:
package lulusoft.jspbuilderv10.parser;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.io.*;
import org.apache.xerces.parsers.SAXParser;
import java.util.Stack;
import lulusoft.jspbuilderv10.common.*;
public class CXmlParser extends org.xml.sax.helpers.DefaultHandler{
/**私有属性*/
CXmlElement root = null;
private CharArrayWriter contents = new CharArrayWriter();
private Stack stack = null;
/**
* 功能描述:构造函数
*/
public CXmlParser(){
super();
stack = new Stack();
}
/**
* 功能描述:标签起始
*/
public void startElement( String namespaceURI,
String localName,
String qName,
Attributes attr ) throws SAXException {
contents.reset();
String cn = attr.getValue("classname");
if(cn==null || cn.trim().equals("")){
System.out.println("标签未有对应的类!请检查!");
}
CXmlElement element = CXmlElement.createElement(cn);
int size = attr.getLength() ;
for(int i=0;i<size;i++){
String name = attr.getQName(i);
String value = attr.getValue(i);
if(name.equals("classname")){
continue;
}
element.addAttribute(name,value);
}
stack.push(element);
}
/**
* 功能描述:标签结束
*/
public void endElement(String namespaceURI, String localName,
String qName ) throws SAXException {
if(stack.size() > 1){
CXmlElement element1 = (CXmlElement)stack.pop();
CXmlElement element2 = (CXmlElement)stack.pop();
if(contents.toString().trim()!= null){
element1.setElementValue(contents.toString().trim());
}
element2.addContainElement(element1);
stack.push(element2);
}
else{
root = (CXmlElement)stack.peek();
}
}
/**
* 功能描述:获得root数据包
*/
public CXmlElement getRoot(){
return root;
}
public void characters( char[] ch, int start, int length )
throws SAXException {
contents.write(ch,start,length);
}
/**
* 功能描述:解析xml并返回数据
* @param fileName String xml文件名称
* @return CXmlElement
*/
public static CXmlElement parseDocument(String fileName){
CXmlParser m_oXmlParser = new CXmlParser();
try {
XMLReader xr = new SAXParser();
xr.setContentHandler(m_oXmlParser);
xr.parse( new InputSource(new FileReader( fileName )) );
}
catch ( SAXParseException saxParseException ) {
System.out.println("Parsing Error:");
System.out.println("行:"+saxParseException.getLineNumber());
System.out.println("列:"+saxParseException.getColumnNumber());
System.out.println(" "+saxParseException.getMessage());
}
catch(SAXException saxException){
saxException.printStackTrace();
}
catch(Exception e){
e.printStackTrace();
}
return m_oXmlParser.getRoot();
}
}
XML 是现在非常流行的数据表达格式,其特点是可移植、与平台无关以及具有直接可读的形式。Document Object Model (DOM) 是应用程序存取 XML 数据的接口。不幸的是,DOM 是一种相当复杂的 API,因而较难以迅速掌握。但是,如能知道所存取数据的 DTD,这时就容易得多了。本文将通过若干简单步骤,对如何利用 Java 版的 DOM 来存取 XML 数据进行介绍。
可扩展标记语言 (XML) 已经相当普及,它是一种可移植的、与平台无关的且直接可读的数据格式。许多软件厂商均已宣称“支持 XML”,这通常是指他们的软件产品将生成或用到 XML 格式的数据。
XML 也同样被看作是企业间交流数据的通用格式。它允许企业在 XML 文档类型定义(即 DTD)的基础上对所交流的数据取得一致。这些 DTD 文件独立于企业中所使用的数据类型。
许多标准化组织正在致力于规范交流数据的 DTD。其中一个例子就是国际出版通信委员会(请参见资源)已经定义了一个 XML 的 DTD,这个 DTD 可以使“所传输的带有标记的新闻信息能够轻松地转换为电子出版格式”。这些市场标准将使不同应用程序之间能够在未事先确定方式的情况下进行数据的交换。
由 W3C 定义的 XML 规范(请参见资源)中规定了 XML 的语法和语义。一个 XML 文档必须经过语法分析才能被处理。如果每个程序都必须先对 XML 进行语法分析再去处理,那将是非常困难的,因为给出这种语言的语法和语义是很复杂的。W3C 已经定义了文档对象模型(DOM)(请参见资源)来解决这一问题。DOM 是一个针对XML数据的应用编程接口。大部分 XML 语法分析器都为所分析的 XML 生成一个 DOM 描述。
DOM 标准
DOM API 被定义为一系列 CORBA IDL 接口(请参见资源)。它用一个抽象树来描述一个经过语法分析的 XML 文档。之所以说它是抽象的,这是因为只有这些接口反映出树形的结构。而用来实现抽象树的实际数据结构和算法不必是树形结构。
由于 DOM API 是以 CORBA IDL 形式规定的,所以它被许多编程语言所支持,包括 Java 语言。我们假定本文中使用标准的 Java 语言。DOM 规范给出了详细的基于Java 接口。
DOM 第一层规范是在 1998 年被采用的。它留下一些保留部分,以根据后来的实践经验来进一步扩充。DOM 第二层规范在第一层的基础上增加了对 XML 命名空间、文档创建、视图和式样单等内容的支持。第二层规范尚有待公众评价。虽然从技术上而言还没有最终完成,但是也已经相当稳定。
对于一个 XML 文档,许多 XML 语法分析器均可供 Java 程序使用,以生成 DOM 的第一层描述。因此,这里的代码只假定为基于DOM 的第一层子集。
通用或特定 DTD 代码
在Java中使用DOM API编写的代码要么是通用的,要么是基于特定的DTD。通用代码在所有 XML 文档中都能正常工作。通用代码一般更难编写,因为它通常必须遍历整个 DOM 树,考虑各种可能。代码不能依赖任何特定的元素、属性和文档结构。通用代码用于处理一般性事务,例如检查文档中的拼写错误、计算文字数目、通过网络发送文件,等等。
另一方面,特定 DTD 代码是在根据特定的 DTD 写出的。它不能用来操作由另一种 DTD 定义的 XML 文档。特定的 DTD 比较容易编写,因为它假设 XML 文档具有该特定 DTD 所指定的格式。例如,假设一个 DTD 声明一个名为“NAME”的元素要求具有一个名为“GIVEN”的属性,Java 代码可以假设这个属性存在并通过简单地 DOM getAttribute() 调用来访问它
这篇文章将帮助你编写特定 DTD Java 代码。在此之后的任务则是学习如何编写通用 DTD 代码。
一个示例
为了解释怎样在 Java 程序中使用 DOM API,我们将用一个定购程序作为例子,因为它是一个典型的基于 XML 的 B2B 应用,并且具有很丰富的 XML 结构。
下面是我们所用定购单的 DTD。
<?xml encoding="US-ASCII"?>
<!ELEMENT order (header,item+,price)>
<!ELEMENT header (billing,shipping)>
<!ELEMENT billing (name,address,creditCard)>
<!ELEMENT shipping (name,address)>
<!ELEMENT name EMPTY>
<!ATTLIST name
given CDATA #REQUIRED
family CDATA #REQUIRED
>
<!ELEMENT address (street,city,state,zipcode,country,phone)>
<!ELEMENT item (prodId,prodName,quantity,price)>
<!ELEMENT creditCard (#PCDATA)>
<!ELEMENT street (#PCDATA)>
<!ELEMENT city (#PCDATA)>
<!ELEMENT state (#PCDATA)>
<!ELEMENT zipcode (#PCDATA)>
<!ELEMENT country (#PCDATA)>
<!ELEMENT phone (#PCDATA)>
<!ELEMENT prodId (#PCDATA)>
<!ELEMENT prodName (#PCDATA)>
<!ELEMENT quantity (#PCDATA)>
<!ELEMENT price (#PCDATA)>
下面是 包含100 个装饰品的定购单的 XML 文档:
<?xml version="1.0"?>
<!DOCTYPE order SYSTEM "order.dtd">
<order>
<header>
<billing>
<name given="Jane" family="Doe"/>
<address>
<street>555 Main Street</street>
<city>Mill Valley</city>
<state>California</state>
<zipcode>94520</zipcode>
<country>USA</country>
<phone>707 555-1000</phone>
</address>
<creditCard>4555 5555 5555 5555</creditCard>
</billing>
<shipping>
<name given="John" family="Doe"/>
<address>
<street>100 Main Street</street>
<city>
Brisbane</city>
<state>California</state>
<zipcode>94005</zipcode>
<country>USA</country>
<phone>415 555-9999</phone>
</address>
</shipping>
</header>
<item>
<prodId>5555555</prodId>
<prodName>Widget</prodName>
<quantity>100</quantity>
<price>.25</price>
</item>
<price>25.00</price>
</order>
XML 语法分析器将上面的 XML 文档抽象地描述成一个树。这个树形结构可以用图形描述如下:
椭圆代表 XML 元素。方形代表数据,从name元素出发的直线代表 XML 属性。图中没有详细标出 address 元素。
在 Java 程序中存取 XML 数据的一些简单步骤
现在,我们用特定的 DTD JAVA 代码来说明 DOM API 的一个重要部分。特别的,我们给出以下DOM方法的用法。
getDocType
getName
getElementsByTagName
item
getFirstChild
getNodeValue
getAttribute
getChildNodes
getLength
getTagName
在本文的范例代码中,所有带下划线的方法调用都是 DOM API 的一部分。另有完整的源代码可供下载。
定义存取 DOM 文档的一个简单接口
第一步是先定义一个抽象接口,该接口能够大大简化使用 XML 文档的代码。对于我们所使用的范例 DTD,定义以下接口:
interface order {
String creditCard();
String billingName();
double totalPrice();
boolean authorizeCredit();
}
存取 XML 数据的代码简单地调用由接口定义的操作集。
应当注意的是,这个接口的实现可有多种不同方式,其中的一些与 XML 无关(例如,某个实现可向数据库发出请求)。当然,这里我们只关心用DOM API 来实现处理 XML 数据的操作,这些数据同前面基于DTD的订单一致。
实现接口
现在编写一个实现该接口并封装 DOM 文档的类。例如,我们定义:
class orderImpl implements order {
Document theDocument;
在构建器中将 DOM 文档捆绑至封装类
将 DOM 文档传递给封装类的构建器。构建器检查文档类型,以确定该文档确实符合订单 DTD。记住这些代码只是针对这个 DTD 的,并对数据的结构和内容做出假设。
public orderImpl(Document document) throws Exception {
theDocument = document;
DocumentType docType = theDocument.getDoctype();
if (docType==null) throw new Exception("Cannot determine document type.");
if (!docType.getName().equals("order")) throw new ?
Exception("Document is not an order.");
}
注意,代码利用 getDoctype 操作来得到文档类型,该操作是在 Document 接口中由DOM定义的。getDoctype 操作返回一个支持 DocumentType 接口的对象。它的名字代表了文档的类型。
另请注意,一些 DOM 实现对 getDoctype 操作返回 null,这样就不能用于这个构建器。
给出用于将该文档捆绑至包装类的代码后,实现这个接口。在接口范例中给出的每项操作,说明了如何使用 DOM 完成特定任务。
返回特定元素的值
creditCard 方法说明了如何返回一个特定元素的值。它使用了在 Document 接口中定义的 getElementsByTagName 操作。
public String creditCard() {
NodeList nl = theDocument.getElementsByTagName("creditCard");
return nl.item(0).getFirstChild().getNodeValue();
}
通常,getElementsByTagName 返回一个元素列表。因为在我们的样例 DTD 中只有一个名为“creditCard”的元素,所以这个列表中只包含一个元素,即 item(0)。这样,nl.item(0) 可用下图表示:
creditCard 元素的字符串值可通过在信用卡节点上调用 getFirstChild().getNodeValue() 方法得到。
注意,getElementsByTagName(elementName) 操作返回文档中利用 elementName 来命名的所有元素。根据定义,它将前序遍历文档树来返回元素。
由于元素名称 creditCard 在我们的样例中是唯一的,所以可以直接找到该元素。然而,其他的元素(例如 name)不是唯一的。我们不能直接使用 getElementsByTagName 返回的第一个元素。实际上,billing 的一个子元素的名称为 name,另外 shipping 的一个子元素也叫做 name。
从唯一的子树中返回一个元素的属性值
在诸如 name 这样的名称不唯一的情况下,billingName 方法是获得元素值的一种方法。注意 name 在这个文档中不唯一,但是在 billing 子树中却是唯一的。另外还要注意,billing 元素在整个文档中是唯一的。这样,我们可以在文档中简单地调用 getElementsByTagName("billing"),然后在返回的 billing 元素中调用 getElementsByTagName。由于 getElementsByTagName 也是在 DOM API 中的 Element 接口中定义的,所以可以这样做。
public String billingName() {
NodeList bl = theDocument.getElementsByTagName("billing");
NodeList nl = ((Element)bl.item(0)).getElementsByTagName("name");
Element name = (Element)nl.item(0);
return name.getAttribute("given")+" "+name.getAttribute("family");
}
利用 billingName 方法,还可以说明另一个技术,即获得一个元素的属性值。请注意,在我们的 DTD 中,name 元素被定义了两个属性:given 和 family。getAttribute 操作由 Element 接口定义,它返回属性的文本值。
从多子树中返回元素的值
现在考虑 price 元素。我们不能再使用刚才的方法,因为 price 是文档的一个子元素,同时也是每个 item 元素的子元素。totalPrice 方法说明了另外一种查找非唯一元素值的方法。依据文档结构可知,我们需要的是顶层的 price 元素。
public double totalPrice() {
NodeList nl=theDocument.getDocumentElement().getChildNodes();
Element candidateElement=null;
for (int i=0; i<nl.getLength(); i++) {
if (nl.item(i) instanceof Element) {
candidateElement = (Element)nl.item(i);
if (candidateElement.getTagName().equals("price")) break;
}
}
return Double.parseDouble(candidateElement.getFirstChild().getNodeValue());
}
getDocumentElement 操作返回一个描述文档的元素。从这里通过 getChildNodes 得到它的子节点。通过分析 DTD,可以看出子元素只有一个 header 元素,至少一个 item 元素和一个 price 元素。所以我们只要循环查找子节点,直到我们找到一个名为 price 的子元素。
同样,我们一旦得到这个元素,则调用 getFirstChild().getNodeValue()来获得它的值。
抽象
creditCard、billingName 及 totalPrice 在我们的接口中是基本的操作。它们简单地查找并返回相应的 XML 元素。另一方面,我们的接口也包含抽象的 authorizeCredit 方法。在 XML 文档中没有与它相应的元素。
下面给出 authorizeCredit 实现。它简单地使用我们已经在包装类中实现的 billingName、creditCard 及 totalPrice 方法。
public boolean authorizeCredit() {
// illustrates abstraction
return authorize(
this.billingName(),
this.creditCard(),
this.totalPrice());
}
类的客户端使用
我们定义了一个抽象接口来访问我们的 XML 文档并通过使用 10 个重要的 DOM 操作来在包装类中实现了它们。接下来,我们介绍一下怎样通过Java代码来利用这些已经定义好的接口
下面的代码简单地调用语法分析器并将其返回的 DOM 文档传递给我们的包装类的构造器,然后调用我们实现的每一个方法。这些代码的编写使用了 IBM XML Parser for Java (参见资源)。其他语法分析器的使用大致相同。
import com.ibm.xml.xpk4j.xml4j2.*;
import java.io.*;
public class test {
public static void main(String[] args) {
XML4J2DOMSource parser = new XML4J2DOMSource();
try {
parser.parse("order.xml");
order theOrder = new orderImpl(parser.getDocument());
System.out.println("The credit card is "+theOrder.creditCard());
System.out.println("The total price is "+theOrder.totalPrice());
System.out.println("The billing name is "+theOrder.billingName());
theOrder.authorizeCredit()
}
catch(Exception e) {
e.printStackTrace();
}
}
}
我们通过这个非常简单的例子向大家展示了 DOM API 的 10 个重要的操作。通过这些操作,我们说明了如何在已知 DTD 时进行查找、浏览、遍历元素以及获得元素及其属性值。这将为学习其它 DOM API 打下坚实的基础。
“文档对象模型(DOM)”提供了有用的模块来以高级方式扩展其核心功能。本文深入研究了 DOM Traversal 模块,演示了如何查明您的语法分析器是否支持该模块以及如何使用它来遍历选中的节点集或整个 DOM 树。读完本文之后,您将彻底理解 DOM Traversal,并会在您的 Java 和 XML 编程工具箱中拥有一个强大的新工具。八个样本代码清单演示了这些技术。
如果您在过去三年中作过很多 XML 处理,那么您几乎一定遇到过“文档对象模型”(简称 DOM)。这种对象模型表示应用程序中的 XML 文档,并提供一种简单的方式来读取 XML 并写入或更改现有文档中的数据(如果您是 DOM 新手,请参阅参考资料来获得更多背景知识。)如果您正在努力成为一名 XML 高手,那您可能已经彻底地学过 DOM,并且知道如何使用它所提供的几乎每一种方法。然而,还有许多 DOM 功能没有被大多数开发人员认识到。
大多数开发人员实际都已接触过 DOM 核心。该核心指的是 DOM 规范,它概括 DOM 的含义、它应该如何操作以及提供哪些方法等等。甚至有经验的开发人员都不太知道或了解许多不太常用的 DOM 模块。这些模块允许开发人员更高效而轻松地使用树、同时处理不同的范围的节点、对 HTML 或 CSS 页面进行操作以及其它任务,所有这些都不是仅使用核心 DOM 规范可以做到的。在以后几个月中,我计划写几篇文章,详细介绍几个模块,包括 HTML 模块 — Range 模块 — 在本文中,将介绍 Traversal 模块。
通过学习如何使用 DOM Traversal,您将看到遍历整个 DOM 树、构建定制对象过滤器来轻易查找所需数据以及以前所未有的轻松方式遍历 DOM 树是多么快捷。我还将向您介绍一个实用程序,该程序允许您检查您选择的语法分析器是否支持特定的 DOM 模块,同时,我还将为您演示许多其它样本代码。那么,请启动您喜爱的源码编辑器,然后让我们开始。
获得信息
首先,确保您有所需工具来遍历一些示例代码。对于本文,您手头要有一个 XML 语法分析器。该语法分析器需要提供 DOM 实现。事实上,那很简单;几乎每一种您可以得到的 XML 语法分析器都支持 SAX(Simple API for XML)和 DOM。您要确保您所用的语法分析器具有 DOM 级别 2 支持,这很简单,只需阅读该语法分析器的发行说明或简单地从供应商处获得最新版本即可。
获得语法分析器之后,您需要确保它支持我们正在讨论的 DOM Traversal 模块。虽然这应该也可以在语法分析器文档中找到关于这方面的说明,但我想为您演示一个简单的编程方法来检查这一点。事实上,“清单 1”中演示的程序可以让您询问任何语法分析器:看它是否有任何模块。我在其中包括了大多数常见 DOM 模块的特定检查,当然包括 DOM Traversal。这个程序使用 DOM 类 org.w3c.dom.DOMImplementation 及其 hasFeature() 方法:通过传入每个模块的名称来检查是否支持这些模块,找出实际支持哪些模块很容易。代码相当简单,我把阅读程序流程的任务留给您。
清单 1. DOMModuleChecker 类
import org.w3c.dom.DOMImplementation;
public class DOMModuleChecker {
/** Vendor DOMImplementation impl class */
private String vendorImplementationClass =
"org.apache.xerces.dom.DOMImplementationImpl";
/** Modules to check */
private String[] moduleNames =
{"XML", "Views", "Events", "CSS", "Traversal", "Range", "HTML"};
public DOMModuleChecker() {
}
public DOMModuleChecker(String vendorImplementationClass) {
this.vendorImplementationClass = vendorImplementationClass;
}
public void check() throws Exception {
DOMImplementation impl =
(DOMImplementation)Class.forName(vendorImplementationClass)
.newInstance();
for (int i=0; i
请确保您的 CLASSPATH 环境变量和工作目录中有您的语法分析器(应该包括 DOM 实现)。您可以编译这个源文件,然后运行它。另外,请注意“清单 1”中的粗体行;如果您正在使用 Apache Xerces 以外的语法分析器,需要为 DOMImplementation 接口提供那个语法分析器的 DOM 实现类。如果您正在使用 Xerces,则可以保持“清单 1”中的程序不变,然后编译它即可。
我就是用最新版本 Apache Xerces 1.4.1(在我的类路径中)这样做的。得到以下输出:
清单 2. 看看 Xerces 支持哪些模块
Brett McLaughlin@GALADRIEL ~
$ java DOMModuleChecker
Support for XML is included in this DOM implementation.
Support for Views is not included in this DOM implementation.
Support for Events is included in this DOM implementation.
Support for CSS is not included in this DOM implementation.
Support for Traversal is included in this DOM implementation.
Support for Range is not included in this DOM implementation.
Support for HTML is not included in this DOM implementation.
这让我知道:对 Traversal 模块的支持(也是本文的主题)确实存在,那么我就准备继续前进了。如果您的语法分析器不同,并且不提供 DOM Traversal 支持,那么我建议您使用 Apache Xerces,至少对本文中的示例使用它(有关链接,请查看参考资料一节)。一旦得到了支持 DOM Traversal 的语法分析器,请继续阅读下一节“入门”。
入门
构成 DOM Traversal 的类都在 org.w3c.dom.traversal 包中,我将在本节中探讨它们。首先,您可能只想看一看包中的类;只有四个类。第一个类是 DocumentTraversal,它是所有处理遍历的工作开始的地方。它用来创建模块所提供的两种类型的遍历类: NodeIterator 和 TreeWalker。我稍后就讲这两个类。很容易想出如何创建这些类:使用 createNodeIterator() 创建 NodeIterator,使用 createTreeWalker() 创建 TreeWalker。很简单,哈?还有一个类是 NodeFilter,它用来定制在迭代和树遍历中返回的节点。
下一步,需要找出语法分析器中的哪个(或哪些)类实现 org.w3c.dom.DocumentTraversal 接口,以便可以创建一个树遍历器或节点迭代器。通过参考语法分析器的 Java 文档来这样做是最简单的。然而,一般来说,实现 DOM org.w3c.dom.Document 接口的类也实现 DocumentTraversal。因此,需要完成如“清单 3”中所示的工作:
清单 3. 获得一个 DocumentTraversal 实例
// Get access to your parser's org.w3c.dom.Document implementation
Document doc = new org.apache.xerces.dom.DocumentImpl();
// Get a traversal instance by type-casting
DocumentTraversal traversal = (DocumentTraversal)doc;
// Create node iterators or tree walkers
注:我在“清单 3”中留下了一个注释,在那里放置您自己的 DOM 代码。我将在后面详述这点。
首先,对象的类型被强制转换成 DocumentTraversal。然后,准备创建 NodeIterator 和 TreeWalker 实例。
节点知道
既然知道如何获得 DocumentTraversal 实例,您就可以让 DOM Traversal 开始工作。但是,在开始迭代节点之前,需要为您演示一个具体的示例。清单 4 演示了一个 XML 文档,该文档存储了有关一个在线书店中书籍(显然是一部分书籍)的信息。
可以看到每个 book 项都有一个标题、作者和简短描述。这些描述使用 keyword 标记包围了一些关键字。如果将属性 search 设置成“true”,则将在关键字搜索中使用这个字;如果设置成“false”,则只在内部索引中使用这些关键字(请原谅,这只是个示例!)。在对在线书籍目录的处理中,一个常见任务是允许用户通过这些关键字搜索书籍。例如,客户可能想要有关“middle earth”或“galaxy”或其它任何关键字的书籍。这些字出现在这些书籍的描述中,但使用标准 DOM 方式找到它们却不容易。而这正是 DOM Traversal 的用武之地。通常,必须找到根元素,再找到根元素下面的 book 元素,找到每本书籍的 description 元素,然后在其中搜索 search 元素为“true”的 keyword 元素。即使对于这项相当普通的任务,也需要许多代码来实现。然而,DOM Traversal 使这变得容易。
首先,需要通过实现 org.w3c.dom.traversal.NodeFilter 接口中的唯一方法 acceptNode() 来创建该接口的一个实现。此方法接受一个 DOM org.w3c.dom.Node 作为唯一自变量,然后,它将被传递给正在处理的 DOM 结构中的每一个节点。然后,它开始处理该节点并返回一个常数(以 Java short 形式),指出应该将该节点返回给当前 NodeIterator 还是应该忽略它。 这意味着,开发人员无需编写许多非常恼人的节点迭代代码(这个名称开始有意义了,不是吗?)。此过滤器只需检查所提供 节点的特殊类型、属性及其值以及其它标准,从而判断是接受还是拒绝该节点。既然一副图画顶一千个字的说明,一段代码顶一百万字的描述,那么我将为您演示 NodeFilter 的一个实现,该实现只接受位于 keyword 元素内且 search 属性为“true”的节点。请查看“清单 5”。
清单 5. 获得可搜索的关键字
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.NodeFilter;
public class KeywordsNodeFilter implements NodeFilter {
public short acceptNode(Node n) {
if (n.getNodeType() == Node.TEXT_NODE) {
Node parent = n.getParentNode();
if (parent.getNodeName().equalsIgnoreCase("keyword")) {
if (((Element)parent).getAttributeNode("search")
.getNodeValue()
.equalsIgnoreCase("true")) {
return FILTER_ACCEPT;
}
}
}
// If we got here, not interested
return FILTER_SKIP;
}
}
如果您非常熟悉 DOM 核心,则“清单 5”对您来说有许多意义。首先,您只需要实际关键字本身(而不是 keyword 元素),因此这个过滤器检查节点,看它是否是一个 org.w3c.dom.Text 节点。然后,如果其父节点名为 keyword,则它检查该元素的属性值,如果该属性值是“true”,那么它接受这个节点(使用在 NodeFilter 接口中定义的 FILTER_ACCEPT 常数)。否则,通过返回 FILTER_SKIP 来跳过该节点。很简单,是吗?
编写完这个过滤器之后,现在只需用这个过滤器创建一个新的 NodeIterator。有几段信息要提供给我在前一节讨论过的 createNodeIterator() 方法。首先,提供一个元素,从该元素这个地方开始搜索;除非只想搜索 DOM 树的特定部分,否则,我通常从根元素开始搜索。第二步,可以通过指定只搜索元素或属性或其它结构来限定搜索范围。因为有 NodeFilter,我实际上要迭代所有节点(让过滤器做这件事),因此我提供了常数 NodeFilter.SHOW_ALL。 下一个自变量是 NodeFilter 的示例,这个示例当然是清单 5 中 KeywordsNodeFilter 类的一个示例。最后的自变量是一个 Boolean 值,它指出是否应该扩展实体引用(如果不知道什么是实体引用,请查看 XML 教程,该教程可在参考资料一节中找到)。我几乎总是希望扩展它们,因此我通常提供一个 true 值。然后,迭代器象普通 Java 迭代器那样工作。要看到所有这些是如何工作的,请查看清单 6,该清单将所有这些细节组织在一起,创建了一个搜索 XML 文档并打印出所有可搜索关键字的程序。
到目前为止,根据对其它清单的解释,您应该理解“清单 6”了。请编译此程序和清单 5 中的代码。然后,将清单 4 的内容保存为一个 XML 文档。我将我的这个文档命名为 keywords.xml。将 xerces.jar 添加到类路径和工作目录中,然后运行 KeywordSearcher 类。我知道我省略了最后一步的描述,但是如果您也是一名 Java 用户,就不应该有任何编译和设置方面的问题。在对您自己的 XML 目录副本运行这个类之后,应该得到类似于“清单 7”的结果。
清单 7. 运行关键字搜索程序
C:\javaxml2\ibm>java KeywordSearcher keywords.xml
Processing file: keywords.xml
Search phrase found: 'galaxy'
Search phrase found: 'Hyperion'
Search phrase found: 'dwarves'
Search phrase found: 'hobbit'
Search phrase found: 'Foundation'
Search phrase found: 'Wheel of Time'
Search phrase found: 'The Path of Daggers'
显然,随着文档越来越复杂,NodeFilter 实现也会相应地更加复杂。此处的要点在于,DOM Traversal 与这个小过滤器一样有用,它在更复杂的情况下会变得极其强大。例如,可以根据元素/属性名,在文档中查找表示成属性 或元素的数据。对于核心 DOM 代码来说,这确实是个棘手的任务,它同样需要进行许多树的遍历,而 NodeIterator 则可以为您处理这些。因此,让您的想象力自由驰骋,并构筑那些过滤器吧!
在森林中查找树
在结束关于 DOM Traversal 的讲座之前,我要简短介绍一下 TreeWalker。由于篇幅所限(我希望这是一篇文章,而不只是一个章节),不想过于深入,但是因为您已经了解了 NodeIterator,这应该很简单。通过清单 8 中的方法创建 TreeWalker:
如果认识到 TreeWalker 方法采用的参数与 createNodeFilter() 方法相同,那并不会引起什么问题。事实上,剩下的唯一问题是“迭代节点与遍历树有什么区别?”答案是:使用 TreeWalker 时,可以维护一个树结构。在使用 NodeIterator 时,返回的节点实际上已从其树中的初始位置分离。迭代节点使操作很快捷,因为一旦返回节点就废弃其树位置。但是,使用 TreeWalker 时,当节点从定制节点过滤器返回时,节点仍保留在它们的树上下文中。这就允许您可以实际通过过滤器来查看整个 XML 文档。
做一个练习,尝试编写一个程序来显示清单 4 中不带处理说明、注释或属性的 XML 文档。 在开始前,有一个提示:首先,要使用 TreeWalker 来确保保留树格式。其次,编写一个定制的 NodeFilter 实现,以便只接受元素或文本类型的节点。最后,使用清单 6 中的程序作为模板,并更改几行代码。然后,就像那样,您自己就得到了一个定制的 DOM 树视图。如果您理解了节点部分并且可以编写出这个样本程序,那么您正在成为 DOM Traversal 高手的路上顺利前进。
希望您一直都在看 Traversal 模块所展示的所有可能性。以过滤方式遍历 DOM 树使得寻找元素、属性、文本和其它 DOM 结构变得容易。您还应该能够使用 DOM Traversal 模块编写更有效、结构更好的代码。因此,采用一个现有的搜索 DOM 树的程序,然后转换它,使其使用 traversal 方法;我知道您将对该结果感到满意。同以往一样,请让我知道本文是否对您有所帮助(使用本文所附的论坛),咱们网上见。
简要探讨 Java 中不同 XML 文档模型的工作原理
Dennis M. Sosnoski(dms@sosnoski.com)
总裁,Sosnoski Software Solutions, Inc.
2002 年 2 月
本文中,XML 工具观察家 Dennis Sosnoski 对比了几种 Java 文档模型的可用性。当选取一种模型时并不总是很清楚有哪些折衷,而且如果您稍后改了主意,那么可能需要进行大量重新编码工作才能转换。作者将样本代码与模型 API 的分析相结合,对哪些模型可能真正使您的工作方便给出了建议。本文包含显示五种不同文档模型的方法的代码样本。
在本系列的第一篇文章中,我研究了一些用 Java 编写的主要的 XML 文档模型的性能。但是,在开始选择这种类型的技术时,性能只是问题的一部分。使用方便至少是同样重要的,并且它已是一个主要理由,来支持使用 Java 特定的模型,而不是与语言无关的 DOM 。
为切实了解哪个模型真正的作用,您需要知道它们在可用性程度上是如何排名的。本文中,我将尝试进行这个工作,从样本代码开始,来演示如何在每个模型中编码公共类型的操作。并对结果进行总结来结束本文,而且提出了促使一种表示比另一种更容易使用的一些其它因素。
请参阅以前的文章(请参阅参考资料或本文“内容”下的便捷链接)来获取这个对比中使用的各个模型的背景资料,包含实际的版本号。还可以参阅“参考资料”一节中关于源代码下载、到模型主页的链接以及其它相关信息。
代码对比
在对不同文档表示中用法技术的这些对比中,我将显示如何在每种模型中实现三种基本操作:
根据输入流构建文档
遍历元素和内容,并做一些更改:
从文本内容中除去前导和尾随的空白。
如果结果文本内容为空,就删除它。
否则,将它包装到父元素的名称空间中一个名为“text”的新元素中。
将已修改的文档写入输出流
这些示例的代码是以我在上篇文章中使用的基准程序为基础的,并进行了一些简化。基准程序的焦点是为了显示每个模型的最佳性能;对于本文,我将尝试显示在每种模型中实现操作的最简便方法。
我已经将每个模型的示例结构化为两个独立的代码段。第一段是读取文档、调用修改代码和编写已修改文档的代码。第二段是真正遍历文档表示和执行修改的递归方法。为避免分散注意力,我已在代码中忽略了异常处理。
您可以从本页底部参考资料一节链接到下载页,以获取所有样本的完整代码。样本的下载版本包括一个测试驱动程序,还有一些添加的代码用于通过计算元素、删除和添加的个数来检查不同模型的操作。
即使您不想使用 DOM 实现,但还是值得浏览下面对 DOM 用法的描述。因为 DOM 示例是第一个示例,所以与后面的模型相比,我用它来探究有关该示例的一些问题和结构的更详细信息。浏览这些内容可以补充您想知道的一些细节,如果直接阅读其它模型之一,那么将错过这些细节。
DOM
DOM 规范涵盖了文档表示的所有类型的操作,但是它没有涉及例如对文档的语法分析和生成文本输出这样的问题。包括在性能测试中的两种 DOM 实现,Xerces 和 Crimson,对这些操作使用不同的技术。清单 1 显示了 Xerces 的顶级代码的一种形式。
清单 1. Xerces DOM 顶级代码
1 // parse the document from input stream ("in")
2 DOMParser parser = new DOMParser();
3 parser.setFeature("http://xml.org/sax/features/namespaces", true);
4 parser.parse(new InputSource(in));
5 Document doc = parser.getDocument();
6 // recursively walk and modify document
7 modifyElement(doc.getDocumentElement());
8 // write the document to output stream ("out")
9 OutputFormat format = new OutputFormat(doc);
10 XMLSerializer serializer = new XMLSerializer(out, format);
11 serializer.serialize(doc.getDocumentElement());
正如我在注释中指出的,清单 1 中的第一块代码(第 1-5 行)处理对输入流的语法分析,以构建文档表示。Xerces 定义了 DOMParser 类,以便从 Xerces 语法分析器的输出构建文档。InputSource 类是 SAX 规范的一部分,它能适应供 SAX 分析器使用的几种输入形式的任何之一。通过单一调用进行实际的语法分析和文档构造,如果成功完成了这一操作,那么应用程序就可以检索并使用已构造的 Document。
第二个代码块(第 6-7 行)只是将文档的根元素传递给我马上要谈到的递归修改方法。这些代码与本文中所有文档模型的代码在本质上是相同的,所以在剩余的示例中我将跳过它,不再做任何讨论。
第三个代码块(第 8-11 行)处理将文档作为文本写入输出流。这里,OutputFormat 类包装文档,并为格式化生成的文本提供了多种选项。XMLSerializer 类处理输出文本的实际生成。
Xerces 的 modify 方法只使用标准 DOM 接口,所以它还与任何其它 DOM 实现兼容。清单 2 显示了代码。
清单 2. DOM Modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 Node child;
4 Node next = (Node)element.getFirstChild();
5 while ((child = next) != null) {
6 // set next before we change anything
7 next = child.getNextSibling();
8 // handle child by node type
9 if (child.getNodeType() == Node.TEXT_NODE) {
10 // trim whitespace from content text
11 String trimmed = child.getNodeValue().trim();
12 if (trimmed.length() == 0) {
13 // delete child if nothing but whitespace
14 element.removeChild(child);
15 } else {
16 // create a "text" element matching parent namespace
17 Document doc = element.getOwnerDocument();
18 String prefix = element.getPrefix();
19 String name = (prefix == null) ? "text" : (prefix + ":text");
20 Element text =
21 doc.createElementNS(element.getNamespaceURI(), name);
22 // wrap the trimmed content with new element
23 text.appendChild(doc.createTextNode(trimmed));
24 element.replaceChild(text, child);
25 }
26 } else if (child.getNodeType() == Node.ELEMENT_NODE) {
27 // handle child elements with recursive call
28 modifyElement((Element)child);
29 }
30 }
31 }
清单 2 中显示的方法所使用的基本方法与所有文档表示的方法相同。通过一个元素调用它,它就依次遍历那个元素的子元素。如果找到文本内容子元素,要么删除文本(如果它只是由空格组成的),要么通过与包含元素相同的名称空间中名为“text”的新元素来包装文本(如果有非空格的字符)。如果找到一个子元素,那么这个方法就使用这个子元素,递归地调用它本身。
对于 DOM 实现,我使用一对引用:child 和 next 来跟踪子元素排序列表中我所处的位置。在对当前子节点进行任何其它处理之前,先装入下个子节点的引用(第 7 行)。这样做使得我能够删除或替代当前的子节点,而不丢失我在列表中的踪迹。
当我创建一个新元素来包装非空白的文本内容(第 16-24 行)时,DOM 接口开始有点杂乱。用来创建元素的方法与文档关联并成为一个整体,所以我需要在所有者文档中检索当前我正在处理的元素(第 17 行)。我想将这个新元素放置在与现有的父元素相同的名称空间中,并且在 DOM 中,这意味着我需要构造元素的限定名称。根据是否有名称空间的前缀,这个操作会有所不同(第 18-19 行)。利用新元素的限定名称,以及现有元素中的名称空间 URI,我就能创建新元素(第 20-21 行)。
一旦创建了新元素,我只要创建和添加文本节点来包装内容 String,然后用新创建的元素来替代原始文本节点(第 22-24 行)。
清单 3. Crimson DOM 顶级代码
1 // parse the document from input stream
2 System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
3 "org.apache.crimson.jaxp.DocumentBuilderFactoryImpl");
4 DocumentBuilderFactory dbf = DocumentBuilderFactoryImpl.newInstance();
5 dbf.setNamespaceAware(true);
6 DocumentBuilder builder = dbf.newDocumentBuilder();
7 Document doc = builder.parse(in);
8 // recursively walk and modify document
9 modifyElement(doc.getDocumentElement());
10 // write the document to output stream
11 ((XmlDocument)doc).write(out);
清单 3 中的 Crimson DOM 示例代码使用了用于语法分析的 JAXP 接口。JAXP 为语法分析和转换 XML 文档提供了一个标准化的接口。本示例中的语法分析代码还可以用于 Xerces(对文档构建器类名称的特性设置有适当的更改)来替代较早给定的 Xerces 特定的示例代码。
在本示例中,我首先在第 2 行到第 3 行中设置系统特性来选择要构造的 DOM 表示的构建器工厂类(JAXP 仅直接支持构建 DOM 表示,不支持构建本文中讨论的任何其它表示)。仅当想选择一个要由 JAXP 使用的特定 DOM 时,才需要这一步;否则,它使用缺省实现。出于完整性起见,我在代码中包含了设置这个特性,但是更普遍的是将它设置成一个 JVM 命令行参数。
接着我在第 4 行到第 6 行中创建构建器工厂的实例,对使用那个工厂实例构造的构建器启用名称空间支持,并从构建器工厂创建文档构建器。最后(第 7 行),我使用文档构建器来对输入流进行语法分析并构造文档表示。
为了写出文档,我使用 Crimson 中内部定义的基本方法。不保证在 Crimson 未来版本中支持这个方法,但是使用 JAXP 转换代码来将文档作为文本输出的替代方法需要诸如 Xalan 那样的 XSL 处理器的。那超出了本文的范围,但是要获取详细信息,可以查阅 Sun 中的 JAXP 教程。
JDOM
使用 JDOM 的顶级代码比使用 DOM 实现的代码稍微简单一点。为构建文档表示(第 1-3 行),我使用带有由参数值禁止验证的 SAXBuilder。通过使用提供的 XMLOutputter 类,将已修改的文档写入输出流同样简单(第 6-8 行)。
清单 4. JDOM 顶级代码
1 // parse the document from input stream
2 SAXBuilder builder = new SAXBuilder(false);
3 Document doc = builder.build(in);
4 // recursively walk and modify document
5 modifyElement(doc.getRootElement());
6 // write the document to output stream
7 XMLOutputter outer = new XMLOutputter();
8 outer.output(doc, out);
清单 5 中 JDOM 的 modify 方法也比 DOM 的同一方法简单。我获取包含元素所有内容的列表并扫描了这张列表,检查文本(象 String 对象那样的内容)和元素。这张列表是“活的”,所以我能直接对它进行更改,而不必调用父元素上的方法。
清单 5. JDOM modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 List children = element.getContent();
4 for (int i = 0; i < children.size(); i++) {
5 // handle child by node type
6 Object child = children.get(i);
7 if (child instanceof String) {
8 // trim whitespace from content text
9 String trimmed = child.toString().trim();
10 if (trimmed.length() == 0) {
11 // delete child if only whitespace (adjusting index)
12 children.remove(i--);
13 } else {
14 // wrap the trimmed content with new element
15 Element text = new Element("text", element.getNamespace());
16 text.setText(trimmed);
17 children.set(i, text);
18 }
19 } else if (child instanceof Element) {
20 // handle child elements with recursive call
21 modifyElement((Element)child);
22 }
23 }
24 }
创建新元素的技术(第 14-17 行)非常简单,而且与 DOM 版本不同,它不需要访问父文档。
dom4j
dom4j 的顶级代码比 JDOM 的稍微复杂些,但是它们的代码行非常类似。这里的主要区别是我保存了用来构建 dom4j 文档表示的 DocumentFactory(第 5 行),并在输出已修改的文档文本之后刷新了 writer(第 10 行)。
清单 6. dom4j 的顶级代码
1 // parse the document from input stream
2 SAXReader reader = new SAXReader(false);
3 Document doc = reader.read(in);
4 // recursively walk and modify document
5 m_factory = reader.getDocumentFactory();
6 modifyElement(doc.getRootElement());
7 // write the document to output stream
8 XMLWriter writer = new XMLWriter(out);
9 writer.write(doc);
10 writer.flush();
正如您在清单 6 中看到的,dom4j 使用一个工厂方法来构造文档表示(从语法分析构建)中包含的对象。根据接口来定义每个组件对象,所以实现其中一个接口的任何类型的对象都能包含在表示中(与 JDOM 相反,它使用具体类:这些类在某些情况中可以划分子类和被继承,但是在文档表示中使用的任何类都需要以原始 JDOM 类为基础)。通过使用不同工厂进行 dom4j 文档构建,您能获取不同系列的组件中构造的文档。
在样本代码(第 5 行)中,我检索了用于构建文档的(缺省)文档工厂,并将它存储在一个实例变量(m_factory)中以供 modify 方法使用。并不严格需要这一步 — 可以在一个文档中同时使用来自不同工厂的组件,或者可以绕过工厂而直接创建组件的实例 — 但在该例中,我只想创建与在文档其余部分中使用的同一类型的组件,并且使用相同的工厂来确保完成这个步骤。
清单 7. dom4j modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 List children = element.content();
4 for (int i = 0; i < children.size(); i++) {
5 // handle child by node type
6 Node child = (Node)children.get(i);
7 if (child.getNodeType() == Node.TEXT_NODE) {
8 // trim whitespace from content text
9 String trimmed = child.getText().trim();
10 if (trimmed.length() == 0) {
11 // delete child if only whitespace (adjusting index)
12 children.remove(i--);
13 } else {
14 // wrap the trimmed content with new element
15 Element text = m_factory.createElement
16 (QName.get("text", element.getNamespace()));
17 text.addText(trimmed);
18 children.set(i, text);
19 }
20 } else if (child.getNodeType() == Node.ELEMENT_NODE) {
21 // handle child elements with recursive call
22 modifyElement((Element)child);
23 }
24 }
25 }
清单 7 中 dom4j modify 方法与 JDOM 中使用的方法非常类似。不通过使用 instanceof 运算符来检查内容项的类型,我可以通过 Node 接口方法 getNodeType 来获取类型代码(也可以使用 instanceof,但类型代码方法看起来更清晰)。通过使用 QName 对象来表示元素名称和通过调用已保存的工厂的方法来构建元素可以区别新元素的创建技术(第 15-16 行)。
Electric XML
清单 8 中 Electric XML(EXML)的顶级代码是任何这些示例中最简单的一个,通过单一方法调用就可以读取和编写文档。
清单 8. EXML 顶级代码
1 // parse the document from input stream
2 Document doc = new Document(in);
3 // recursively walk and modify document
4 modifyElement(doc.getRoot());
5 // write the document to output stream
6 doc.write(out);
清单 9 中 EXML modify 方法尽管与 JDOM 一样,需要使用 instanceof 检查,但它与 DOM 方法最相似。在 EXML 中,无法创建一个带名称空间限定的名称的元素,所以取而代之,我创建新元素,然后设置其名称来达到相同的效果。
清单 9. EXML modify 方法
1 protected void modifyElement(Element element) {
2 // loop through child nodes
3 Child child;
4 Child next = element.getChildren().first();
5 while ((child = next) != null) {
6 // set next before we change anything
7 next = child.getNextSibling();
8 // handle child by node type
9 if (child instanceof Text) {
10 // trim whitespace from content text
11 String trimmed = ((Text)child).getString().trim();
12 if (trimmed.length() == 0) {
13 // delete child if only whitespace
14 child.remove();
15 } else {
16 // wrap the trimmed content with new element
17 Element text = new Element();
18 text.addText(trimmed);
19 child.replaceWith(text);
20 text.setName(element.getPrefix(), "text");
21 }
22 } else if (child instanceof Element) {
23 // handle child elements with recursive call
24 modifyElement((Element)child);
25 }
26 }
27 }
XPP
XPP 的顶级代码(在清单 10 中)是所有示例中最长的一个,与其它模型相比,它需要相当多的设置。
清单 10. XPP 顶级代码
1 // parse the document from input stream
2 m_parserFactory = XmlPullParserFactory.newInstance();
3 m_parserFactory.setNamespaceAware(true);
4 XmlPullParser parser = m_parserFactory.newPullParser();
5 parser.setInput(new BufferedReader(new InputStreamReader(in)));
6 parser.next();
7 XmlNode doc = m_parserFactory.newNode();
8 parser.readNode(doc);
9 // recursively walk and modify document
10 modifyElement(doc);
11 // write the document to output stream
12 XmlRecorder recorder = m_parserFactory.newRecorder();
13 Writer writer = new OutputStreamWriter(out);
14 recorder.setOutput(writer);
15 recorder.writeNode(doc);
16 writer.close();
因为使用 JAXP 接口,所以我必须首先创建分析器工厂的实例并在创建分析器实例之前启用名称空间处理(第 2-4 行)。一旦获取了分析器实例,我就能将输入设置到分析器中,并真正构建文档表示(第 5-8 行),但是这涉及比其它模型更多的步骤。
输出处理(第 11-16 行)也涉及比其它模型更多的步骤,主要因为 XPP 需要 Writer 而不是直接将 Stream 作为输出目标接受。
清单 11 中 XPP modify 方法尽管需要更多代码来创建新元素(第 13-21 行),但它与 JDOM 方法最类似。名称空间处理在这里有点麻烦。我首先必须创建元素的限定名称(第 15-16 行),然后创建元素,最后在稍后设置名称和名称空间 URI(第 18-21 行)。
清单 11. XPP modify 方法
1 protected void modifyElement(XmlNode element) throws Exception {
2 // loop through child nodes
3 for (int i = 0; i < element.getChildrenCount(); i++) {
4 // handle child by node type
5 Object child = element.getChildAt(i);
6 if (child instanceof String) {
7 // trim whitespace from content text
8 String trimmed = child.toString().trim();
9 if (trimmed.length() == 0) {
10 // delete child if only whitespace (adjusting index)
11 element.removeChildAt(i--);
12 } else {
13 // construct qualified name for wrapper element
15 String prefix = element.getPrefix();
16 String name = (prefix == null) ? "text" : (prefix + ":text");
17 // wrap the trimmed content with new element
18 XmlNode text = m_parserFactory.newNode();
19 text.appendChild(trimmed);
20 element.replaceChildAt(i, text);
21 text.modifyTag(element.getNamespaceUri(), "text", name);
22 }
23 } else if (child instanceof XmlNode) {
24 // handle child elements with recursive call
25 modifyElement((XmlNode)child);
26 }
27 }
28 }
结束语
DOM、dom4j 和 Electric XML 都得到这些几乎同样易于使用的代码样本,其中 EXML 可能最简单,而 dom4j 受一些小条件限制而较困难。DOM 提供了与语言无关的非常实在的好处,但是如果你只使用 Java 代码,那么通过与 Java 特定的模型相比较,它看上去有点麻烦。我认为这表明 Java 特定的模型通常成功地实现简化 Java 代码中的 XML 文档处理这个目标。
超越基础:真实世界可用性
代码样本显示 JDOM 和 EXML 为基本文档操作(使用元素、属性和文本)提供了简单和清晰的接口。根据我的经验,它们的方法并不能很好地完成处理整个文档表示的编程任务。要完成这些类型的任务,DOM 和 dom4j 使用的组件方法 — 其中从属性到名称空间的所有文档组件实现一些公共接口 — 工作得更好。
相关的例子是最近我为 JDOM 和 dom4j 实现的 XML 流型(XML Streaming (XMLS) )编码。这个代码遍历整个文档并编码每个组件。JDOM 实现比 dom4j 实现复杂得多,主要是因为 JDOM 使用一些没有公共接口的独特类来表示每个组件。
因为 JDOM 缺少公共接口,所以即使处理 Document 对象的代码与处理 Element 对象的代码都有一些诸如子组件那样相同类型的组件,但是它们必须有所不同。还需要特殊方法来检索与其它类型的子组件相对的 Namespace 组件。甚至当处理被认为是内容的子组件类型时,您需要在组件类型上使用多个带 instanceof 检查的 if 语句,而不是使用一条更清晰更快速的 switch 语句。
具有讽刺意味的可能是 JDOM 的最初目标之一是利用 Java Collection 类,这些类本身在很大程度上以接口为基础。库中接口的使用增加了许多灵活性,而这是以增加了一些复杂性为代价的,并且这对于为重用而设计的代码来说,通常是一个很好的折衷。这可能还主要归功于 dom4j,它达到一个成熟并且稳定的状态,比 JDOM 要快得多。
尽管如此,对于使用多种语言的开发人员来说,DOM 仍是一个非常好的选择。DOM 实现广泛应用于多种编程语言。它还是许多其它与 XML 相关的标准的基础,所以即使您使用 Java 特定的模型,也还有一个您逐步熟悉 DOM 所需要的好机会。因为它正式获得 W3C 推荐(与基于非标准的 Java 模型相对),所以在某些类型的项目中可能也需要它。
就使用方便这一范畴而言,在 JDOM、dom4j 和 Electric XML 这三个主要竞争者中,dom4j 与其它两个的区别在于它使用带有多个继承层的基于接口的方法。这会使得遵循 API JavaDocs 更为困难些。例如,您正在寻找的一个方法(例如 content(),在我们 dom4j 的 modify 方法示例的第 3 行中使用的)可能是 Element 扩展的
Branch 接口的一部分,而不是 Element 接口本身的一部分。尽管如此,这种基于接口的设计添加了许多灵活性(请参阅侧栏超越基础:真实世界可用性)。考虑到 dom4j 的性能、稳定性和特性设置的优点,您应把它当作多数项目中的一个有力的候选者。
在任一 Java 特定的文档模型之中,JDOM 可能拥有最广泛的用户基础,并且它的确是使用起来最简单的模型之一。尽管如此,作为项目开发的一个选择,它还是必须容忍 API 的不固定性和从一个版本到下一个版本的更新,在性能对比中它也表现得很糟糕。基于当前实现,我愿为着手新项目的人们推荐 dom4j,而不是 JDOM。
除了 XPP 以外,EXML 比其它任何模型占用的资源都要少得多,并且考虑到 EXML 易于使用的优点,您应肯定会认为它适用于 jar 文件大小很重要的应用程序。但是,EXML 的 XML 支持的局限性和受限的许可证,以及在较大文件上所表现出的相对拙劣的性能,不得不在许多应用程序中放弃使用它。
XPP 在语法分析和编写文本文档时需要更多步骤,并且在处理名称空间时也需要更多步骤。如果 XPP 打算添加一些便利的方法来处理其中一些常见情况,那么在对比中它可能会更胜一筹。正如它现在所表现的,上篇文章中性能方面的领先者却成了本文中的可用性方面的失败者。尽管如此,因为 XPP 性能方面的优势,所以对于需要较小的 jar 文件大小的应用程序还是值得将它作为 EXML 的替代方法。
下一次...
到目前为止在我写的两篇文章中,涉及到用 Java 编写的 XML 文档模型的性能和可用性。在本系列的后两篇文章中,我将讨论用 Java 技术进行 XML 数据绑定的方法。这些方法与文档模型的方法有许多相似处,但是它们更进一步将 XML 文档映射到实际应用程序数据结构中。我们将看到这一操作在使用的简便性和提高性能方面是如何做得如此好的。
回到 developerWorks,检查 Java 代码的 XML 数据绑定的实质。同时,您可以通过下面链接的论坛,给出您对本文的评论和问题。