https://www.ibm.com/developerworks/cn/xml/x-saxapi/
SAX,功能强大的 API
在摘自 XML by Example 的本篇预览中比较了 DOM 和 SAX,然后开始使用 SAX
这篇对 Benoit Marchal 所著的 XML by Example第二版的预览给出了对 SAX 的翔实介绍,SAX 是用于处理 XML 的基于事件的 API,它已经成为事实上的标准。本篇预览讲述了何时使用 SAX 替换 DOM,概述了常用的 SAX 接口,并在基于 Java 的应用程序中提供了带有许多代码样本的详细示例。
本文由即将出版的 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,功能强大的 API
当然,无论它实现 SAX 还是 DOM API,语法分析器都做许多工作:它读取文档,强制实施 XML 语法并解析实体 - 先只列举这几个。验证语法分析器还强制实施文档模式。
使用语法分析器有很多原因,并且您应该掌握 API、SAX 和 DOM。它使您能灵活地根据手上的任务来选择最好的 API。幸好,现代语法分析器同时支持两种 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和 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; }
转换 3endElement() 的事件处理器更新状态,并调用 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>
但是,如果实际上应用程序真正依赖于文档的结构,那么最好编写一个模式并使用验证语法分析器。