Java XML 和 JSON 教程(一)
一、XML 简介
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1916-4_1) contains supplementary material, which is available to authorized users.
应用程序通常使用 XML 文档来存储和交换数据。XML 定义了以格式编码文档的规则,这种格式既可读又机器可读。本章介绍 XML,浏览 XML 语言特性,并讨论格式良好的有效文档。
什么是 XML?
XML(可扩展标记语言)是一种用于定义词汇(自定义标记语言)的元语言(一种用于描述其他语言的语言),这是 XML 的重要性和普及性的关键。基于 XML 的词汇表(如 XHTML)让您能够以有意义的方式描述文档。
XML 词汇表文档类似于 HTML(参见 http://en.wikipedia.org/wiki/HTML )文档,因为它们是基于文本的,由标记(文档逻辑结构的编码描述)和内容(不被解释为标记的文档文本)组成。标记通过标签(尖括号分隔的语法结构)来证明,每个标签都有一个名称。此外,一些标签具有属性(名称-值对)。
Note
XML 和 HTML 是标准通用标记语言(SGML)的后代,SGML 是创建词汇表的原始元语言。XML 本质上是 SGML 的限制形式,而 HTML 是 SGML 的应用。XML 和 HTML 之间的关键区别在于,XML 让您使用自己的标记和规则创建自己的词汇表,而 HTML 为您提供一个预先创建的词汇表,它有自己固定的标记和规则集。XHTML 和其他基于 XML 的词汇表都是 XML 应用程序。创建 XHTML 是为了更清晰地实现 HTML。
如果您以前没有接触过 XML,您可能会对它的简单性和它的词汇与 HTML 的相似程度感到惊讶。学习如何创建 XML 文档并不需要成为火箭科学家。为了证明这一点,请查看清单 1-1 。
<recipe>
<title>
Grilled Cheese Sandwich
</title>
<ingredients>
<ingredient qty="2">
bread slice
</ingredient>
<ingredient>
cheese slice
</ingredient>
<ingredient qty="2">
margarine pat
</ingredient>
</ingredients>
<instructions>
Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
</instructions>
</recipe>
Listing 1-1.XML-Based Recipe for a Grilled Cheese Sandwich
清单 1-1 展示了一个 XML 文档,描述了制作烤奶酪三明治的食谱。这个文档类似于 HTML 文档,因为它由标签、属性和内容组成。然而,相似之处也就到此为止了。这种非正式的菜谱语言呈现了自己的<recipe>、<ingredients>和其他标签,而不是 HTML 标签,比如<html>、<head>、<img>和<p>。
Note
虽然清单 1-1 的<title>和</title>标签也可以在 HTML 中找到,但是它们与它们的 HTML 对应物不同。Web 浏览器通常会在标题栏中显示这些标签之间的内容。相比之下,清单 1-1 的<title>和</title>标签之间的内容可能会显示为菜谱标题、大声朗读或以其他方式呈现,这取决于解析该文档的应用程序。
语言特色之旅
XML 提供了几种用于定义自定义标记语言的语言功能:XML 声明、元素和属性、字符引用和 CDATA 节、名称空间以及注释和处理指令。在本节中,您将了解这些语言特性。
XML 声明
XML 文档通常以 XML 声明开始,XML 声明是一种特殊的标记,告诉 XML 解析器该文档是 XML。清单 1-1 中缺少 XML 声明表明这种特殊的标记不是强制性的。当 XML 声明存在时,它前面不能出现任何内容。
XML 声明看起来至少类似于<?xml version="1.0"?>,其中非可选的version属性标识文档符合的 XML 规范的版本。该规范的初始版本(1.0)于 1998 年推出,并得到了广泛的实现。
Note
维护 XML 的万维网联盟(W3C)在 2004 年发布了 1.1 版。该版本主要支持使用 EBCDIC 平台上使用的行尾字符(见 http://en.wikipedia.org/wiki/EBCDIC )和使用 Unicode 3.2 中没有的脚本和字符(见 http://en.wikipedia.org/wiki/Unicode )。与 XML 1.0 不同,XML 1.1 没有被广泛实现,应该只由那些需要其独特特性的人使用。
XML 支持 Unicode,这意味着 XML 文档完全由 Unicode 字符集中的字符组成。文档的字符被编码成字节以便存储或传输,编码是通过 XML 声明的可选属性encoding指定的。一种常见的编码是 UTF-8(见 http://en.wikipedia.org/wiki/UTF-8 ),它是 Unicode 字符集的可变长度编码。UTF-8 是 ASCII 的严格超集(见 http://en.wikipedia.org/wiki/ASCII ),这意味着纯 ASCII 文本文件也是 UTF-8 文档。
Note
如果没有 XML 声明或者 XML 声明的encoding属性不存在,XML 解析器通常会在文档的开头寻找一个特殊的字符序列来确定文档的编码。该字符序列被称为字节顺序标记(BOM ),由编辑程序(如 Microsoft Windows 记事本)根据 UTF-8 或其他编码保存文档时创建。例如,十六进制序列EF BB BF表示编码为 UTF-8。同样,FE FF表示 UTF-16 大端(参见 https://en.wikipedia.org/wiki/UTF-16 ),FF FE表示 UTF-16 小端,00 00 FE FF表示 UTF-32 大端(参见 https://en.wikipedia.org/wiki/UTF-32 ),而FF FE 00 00表示 UTF-32 小端。当没有物料清单时,假定为 UTF-8。
如果除了 ASCII 字符集之外,您从来不使用字符,那么您可能会忘记encoding属性。但是,当您的母语不是英语,或者当您被要求创建包含非 ASCII 字符的 XML 文档时,您需要正确地指定encoding。例如,当您的文档包含来自非英语西欧语言(如法语、葡萄牙语和其他语言中使用的 cedilla)的 ASCII plus 字符时,您可能希望选择ISO-8859-1作为encoding属性的值——以这种方式编码的文档可能比使用 UTF-8 编码的文档更小。清单 1-2 向您展示了生成的 XML 声明。
<?xml version="1.0" encoding="ISO-8859-1"?>
<movie>
<name>Le Fabuleux Destin d’Amélie Poulain</name>
<language>français</language>
</movie>
Listing 1-2.An Encoded Document Containing Non-ASCII Characters
可以出现在 XML 声明中的最后一个属性是standalone。这个可选属性只与 dtd 相关(稍后讨论),它决定了是否有外部标记声明影响从 XML 处理器(一个解析器)传递到应用程序的信息。它的值默认为no,暗示有,或者可能有这样的声明。一个yes值表示没有这样的声明。有关更多信息,请查看( www.xmlplease.com/xml/xmlquotations/standalone )上的文章“独立伪属性仅在使用 DTD 时相关”。
元素和属性
XML 声明之后是元素的层次(树)结构,其中元素是由开始标签(如<name>)和结束标签(如</name>)分隔的文档的一部分,或者是空元素标签(名称以正斜杠(/)结尾的独立标签,如<break/>)。开始标签和结束标签包围内容和可能的其他标记,而空元素标签不包围任何东西。图 1-1 展示了清单 1-1 的 XML 文档树结构。
图 1-1。
Listing 1-1’s tree structure is rooted in the recipe element
与 HTML 文档结构一样,XML 文档的结构锚定在根元素(最顶层的元素)中。在 HTML 中,根元素是html(<html>和</html>标签对)。与 HTML 不同,您可以为 XML 文档选择根元素。图 1-1 显示根元素为recipe。
与其他有父元素的元素不同,recipe没有父元素。还有,recipe和ingredients有子元素:recipe的子元素是title、ingredients和instructions;而ingredients子代是ingredient的三个实例。title、instructions和ingredient元素没有子元素。
元素可以包含子元素、内容或混合内容(子元素和内容的组合)。清单 1-2 揭示了movie元素包含name和language子元素,还揭示了这些子元素中的每一个都包含内容(例如,language包含français)。清单 1-3 展示了另一个例子,展示了混合内容以及子元素和内容。
<?xml version="1.0"?>
<article title="The Rebirth of JavaFX" lang="en">
<abstract>
JavaFX 2 marks a significant milestone in the history of JavaFX. Now that Sun Microsystems has passed the torch to Oracle, we have seen the demise of JavaFX Script and the emergence of Java APIs (such as <code-inline>javafx.application.Application</code-inline>) for interacting with this technology. This article introduces you to this new flavor of JavaFX, where you learn about JavaFX 2 architecture and key APIs.
</abstract>
<body>
</body>
</article>
Listing 1-3.An abstract Element Containing Mixed Content
这个文档的根元素是article,它包含了abstract和body子元素。abstract元素将内容与包含内容的code-inline元素混合在一起。相反,body元素是空的。
Note
与清单 1-1 和 1-2 一样,清单 1-3 也包含空格(不可见的字符,比如空格、制表符、回车符和换行符)。XML 规范允许在文档中添加空白。内容中出现的空白(如单词之间的空格)被视为内容的一部分。相反,解析器通常会忽略结束标记和下一个开始标记之间出现的空白。这样的空白不被认为是内容的一部分。
XML 元素的开始标记可以包含一个或多个属性。例如,清单 1-1 的<ingredient>标签有一个qty(数量)属性,清单 1-3 的<article>标签有title和lang属性。属性提供了关于元素的附加细节。例如,qty表示可以添加的成分的量,title表示文章的标题,lang表示文章使用的语言(en表示英语)。属性可以是可选的。例如,当未指定qty时,假定默认值为1。
Note
元素和属性名称可以包含英语或其他语言的任何字母数字字符,还可以包含下划线(_)、连字符(-)、句点(。),以及冒号(:)标点字符。冒号应该只用于名称空间(将在本章后面讨论),并且名称不能包含空格。
字符引用和 CDATA 节
某些字符不能出现在开始标记和结束标记之间的内容中,也不能出现在属性值中。例如,不能在开始标记和结束标记之间放置文字字符<,因为这样做会使 XML 解析器误以为遇到了另一个标记。
这个问题的一个解决方案是用字符引用替换原义字符,字符引用是代表字符的代码。字符引用分为数字字符引用或字符实体引用:
- 数字字符引用通过其 Unicode 码位来引用字符,并遵循格式
&#nnnn;(不限于四位)或&#xhhhh;(不限于四位),其中 nnnn 提供码位的十进制表示,hhhh 提供十六进制表示。例如,Σ和Σ代表希腊文大写字母 sigma。虽然 XML 要求&#xhhhh;中的x是小写的,但是它很灵活,前导零在两种格式中都是可选的,并且允许您为每个 h 指定大写或小写字母。因此,Σ、Σ和Σ也是希腊大写字母 sigma 的有效表示。 - 字符实体引用通过实体名称(别名数据)引用字符,该实体将所需字符指定为其替换文本。字符实体引用由 XML 预定义,格式为
&name;,其中 name 是实体的名称。XML 预定义了五个字符实体引用:<(<)>(>)&(&)'(’)和"(")。
考虑一下<expression>6 < 4</expression>。你可以用数字参考<代替<,产生<expression>6 < 4</expression>,或者更好的是用<,产生<expression>6 < 4</expression>。第二种选择更清晰,更容易记忆。
假设您想在一个元素中嵌入一个 HTML 或 XML 文档。为了使嵌入的文档能够被 XML 解析器接受,您需要用它的<和&预定义的字符实体引用来替换每个文字<(标签的开始)和&(实体的开始)字符,这是一项繁琐且可能容易出错的工作——您可能会忘记替换其中的一个字符。为了避免繁琐和潜在的错误,XML 以 CDATA(字符数据)部分的形式提供了一种替代方法。
CDATA 部分是由前缀<![CDATA[和后缀]]>包围的文字 HTML 或 XML 标记和内容的一部分。您不需要在 CDATA 部分中指定预定义的字符实体引用,如清单 1-4 所示。
<?xml version="1.0"?>
<svg-examples>
<example>
The following Scalable Vector Graphics document describes a blue-filled and black-stroked rectangle.
<![CDATA[<svg width="100%" height="100%" version="1.1"
>
<rect width="300" height="100"
style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
</svg>]]>
</example>
</svg-examples>
Listing 1-4.Embedding an XML Document in Another Document’s CDATA Section
清单 1-4 嵌入了一个可缩放的矢量图形(SVG[参见 SVG 示例文档的example元素中的 https://en.wikipedia.org/wiki/Scalable_Vector_Graphics ) XML 文档。SVG 文档放在一个CDATA部分,避免了用<预定义的字符实体引用替换所有<字符的需要。
命名空间
创建结合不同 XML 语言特性的 XML 文档是很常见的。当元素和其他 XML 语言特性出现时,命名空间用于防止名称冲突。如果没有名称空间,XML 解析器就无法区分同名元素或其他具有不同含义的语言特性,比如来自两种不同语言的两个同名title元素。
Note
名称空间不是 XML 1.0 的一部分。它们是在这个规范发布一年后出现的。为了确保向后兼容 XML 1.0,名称空间利用了冒号字符,这是 XML 名称中的合法字符。不识别名称空间的解析器返回包含冒号的名称。
名称空间是基于统一资源标识符(URI)的容器,它通过为包含的标识符提供唯一的上下文来帮助区分 XML 词汇表。命名空间 URI 通过指定(通常在 XML 文档的根元素中)单独的xmlns属性(表示默认命名空间)或xmlns:前缀属性(表示被标识为前缀的命名空间),并将 URI 分配给该属性,与命名空间前缀(URI 的别名)相关联。
Note
一个命名空间的作用域从声明它的元素开始,应用于该元素的所有内容,除非被另一个具有相同前缀名称的命名空间声明覆盖。
当 prefix 被指定时,前缀和冒号字符被添加到属于该名称空间的每个元素标签的名称之前(参见清单 1-5 )。
<?xml version="1.0"?>
<h:html xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:r="http://www.javajeff.ca/">
<h:head>
<h:title>
Recipe
</h:title>
</h:head>
<h:body>
<r:recipe>
<r:title>
Grilled Cheese Sandwich
</r:title>
<r:ingredients>
<h:ul>
<h:li>
<r:ingredient qty="2">
bread slice
</r:ingredient>
</h:li>
<h:li>
<r:ingredient>
cheese slice
</r:ingredient>
</h:li>
<h:li>
<r:ingredient qty="2">
margarine pat
</r:ingredient>
</h:li>
</h:ul>
</r:ingredients>
<h:p>
<r:instructions>
Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
</r:instructions>
</h:p>
</r:recipe>
</h:body>
</h:html>
Listing 1-5.Introducing a Pair of Namespaces
清单 1-5 描述了一个结合了 XHTML(参见 http://en.wikipedia.org/wiki/XHTML )元素和菜谱语言元素的文档。所有与 XHTML 相关的元素标签都以h:为前缀,所有与菜谱语言相关的元素标签都以r:为前缀。
h:前缀与 www.w3.org/1999/xhtml URI 相关联,r:前缀与 www.javajeff.ca URI 相关联。XML 不要求 URIs 指向文档文件。它只要求它们是惟一的,以保证惟一的名称空间。
该文档将菜谱数据与 XHTML 元素分离开来,这使得保留该数据的结构成为可能,同时还允许符合 XHTML 的 web 浏览器(如 Mozilla Firefox)通过网页呈现菜谱(见图 1-2 )。
图 1-2。
Mozilla Firefox presents the recipe data via XHTML tags
当标签的属性属于元素时,这些属性不需要加上前缀。例如,<r:ingredient qty="2">中没有前缀qty。但是,属于其他名称空间的属性需要前缀。例如,假设您想要向文档的<r:title>标签添加一个 XHTML style属性,以便在通过应用程序显示时为菜谱标题提供样式。您可以通过在title标签中插入一个 XHTML 属性来完成这项任务,如下所示:
<r:title h:style="font-family: sans-serif;">
XHTML style属性带有前缀h:,因为该属性属于 XHTML 语言名称空间,而不属于 recipe 语言名称空间。
当涉及多个名称空间时,将其中一个名称空间指定为默认名称空间会很方便,这样可以减少输入名称空间前缀的繁琐。考虑列出 1-6 。
<?xml version="1.0"?>
<html
xmlns:r="http://www.javajeff.ca/">
<head>
<title>
Recipe
</title>
</head>
<body>
<r:recipe>
<r:title>
Grilled Cheese Sandwich
</r:title>
<r:ingredients>
<ul>
<li>
<r:ingredient qty="2">
bread slice
</r:ingredient>
</li>
<li>
<r:ingredient>
cheese slice
</r:ingredient>
</li>
<li>
<r:ingredient qty="2">
margarine pat
</r:ingredient>
</li>
</ul>
</r:ingredients>
<p>
<r:instructions>
Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
</r:instructions>
</p>
</r:recipe>
</body>
</html>
Listing 1-6.Specifying a Default Namespace
清单 1-6 指定了 XHTML 语言的默认名称空间。没有 XHTML 元素标签需要以h:为前缀。然而,配方语言元素标签仍然必须以前缀r:为前缀。
注释和处理说明
XML 文档可以包含注释,注释是以<!--开始,以-->结束的字符序列。例如,您可以将<!-- Todo -->放在清单 1-3 的body元素中,以提醒自己需要完成该元素的编码。
注释用于阐明文档的各个部分。它们可以出现在 XML 声明之后的任何地方,除了在标记内,不能嵌套,不能包含双连字符(--),因为这样做可能会使 XML 解析器混淆,认为注释已经结束,出于同样的原因,不应该包含连字符(-),并且通常在处理过程中被忽略。评论不内容。
XML 还允许存在处理指令。处理指令是对解析文档的应用程序可用的指令。指令以<?开始,以?>结束。<?前缀后面是一个名为目标的名字。该名称通常标识处理指令所针对的应用程序。处理指令的其余部分包含适合应用程序格式的文本。以下是处理指令的两个示例:
<?xml-stylesheet href="modern.xsl" type="text/xml"?>将可扩展样式表语言(XSL)样式表与 XML 文档相关联(参见http://en.wikipedia.org/wiki/XSL)。<?php /* PHP code */ ?>将一段 PHP 代码片段传递给应用程序(参见http://en.wikipedia.org/wiki/PHP)。尽管 XML 声明看起来像一个处理指令,但事实并非如此。
Note
XML 声明不是处理指令。
格式良好的文档
HTML 是一种松散的语言,在这种语言中,可以无序地指定元素,可以省略结束标记,等等。web 浏览器页面布局代码的复杂性部分是由于需要处理这些特殊情况。相比之下,XML 是一种更严格的语言。为了使 XML 文档更容易解析,XML 要求 XML 文档遵循某些规则:
- 所有元素必须有开始和结束标记,或者由空元素标记组成。例如,不像 HTML
<p>标签那样经常没有对应的</p>,</p>也必须从 XML 文档的角度出现。 - 标记必须正确嵌套。例如,虽然您可能在 HTML 中指定了
<b><i>XML</b></i>,但是 XML 解析器会报告一个错误。相比之下,<b><i>XML</i></b>不会导致错误,因为嵌套的标签对相互镜像。 - 所有属性值都必须用引号括起来。单引号(
’)或双引号(")都是允许的(尽管双引号是更常见的指定引号)。省略这些引号是错误的。 - 空元素必须正确格式化。例如,HTML 的
<br>标签在 XML 中必须被指定为<br/>。您可以在标签名称和/字符之间指定一个空格,尽管空格是可选的。 - 小心大小写。XML 是一种区分大小写的语言,其中大小写不同的标签(如
<author>和<Author>)被认为是不同的。将不同大小写的开始和结束标签混在一起是错误的,例如,<author>和</Author>。
意识到名称空间的 XML 解析器执行两个额外的规则:
- 每个元素和属性名称不得包含一个以上的冒号字符。
- 实体名称、处理指令目标或符号名称(稍后讨论)都不能包含冒号。
符合这些规则的 XML 文档是格式良好的。该文档具有逻辑清晰的外观,并且更易于处理。XML 解析器只会解析格式良好的 XML 文档。
有效文件
对于一个 XML 文档来说,格式良好并不总是足够的;在许多情况下,文件也必须是有效的。有效的文档遵守约束。例如,可以在列出 1-1 的食谱文档时设置一个约束,以确保ingredients元素总是在instructions元素之前;也许一个申请必须首先处理ingredients。
Note
XML 文档验证类似于编译器分析源代码,以确保代码在机器上下文中有意义。例如,int、count、=、1和;都是有效的 Java 字符序列,但是1 count ; int =不是有效的 Java 结构(而int count = 1;是有效的 Java 结构)。
一些 XML 解析器执行验证,而其他解析器不执行验证,因为验证解析器更难编写。执行验证的解析器将 XML 文档与语法文档进行比较。对语法文档的任何偏离都会被报告为应用程序的错误 XML 文档是无效的。应用程序可以选择修复错误或拒绝 XML 文档。与格式良好性错误不同,有效性错误不一定是致命的,解析器可以继续解析 XML 文档。
Note
默认情况下,验证 XML 解析器通常不进行验证,因为验证非常耗时。必须指导他们执行验证。
语法文件是用一种特殊的语言写的。两种常用的语法语言是文档类型定义和 XML 模式。
文档类型定义
文档类型定义(DTD)是规定 XML 文档语法的最古老的语法语言。DTD 语法文档(称为 DTD)是根据一种严格的语法编写的,该语法规定了什么元素可以出现在文档的什么部分,元素中包含什么(子元素、内容或混合内容)以及可以指定什么属性。例如,DTD 可能会指定一个recipe元素必须有一个ingredients元素,后跟一个instructions元素。
清单 1-7 给出了用于构建清单 1-1 文档的配方语言的 DTD。
<!ELEMENT recipe (title, ingredients, instructions)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT ingredients (ingredient+)>
<!ELEMENT ingredient (#PCDATA)>
<!ELEMENT instructions (#PCDATA)>
<!ATTLIST ingredient qty CDATA "1">
Listing 1-7.The Recipe Language’s DTD
这个 DTD 首先声明配方语言的元素。元素声明采用的形式是<!ELEMENT name content-specifier >,其中 name 是任何合法的 XML 名称(例如,它不能包含空格),content-specifier 标识元素中可以出现的内容。
第一个元素声明声明 XML 文档中只能出现一个recipe元素——这个声明并不意味着recipe是根元素。此外,这个元素必须包含title、ingredients和instructions子元素中的一个,并且按照这个顺序。子元素必须指定为逗号分隔的列表。此外,列表总是用括号括起来。
第二个元素声明声明title元素包含解析的字符数据(非标记文本)。第三个元素声明声明至少一个ingredient元素必须出现在ingredients中。+字符是表示一个或多个的正则表达式的一个例子。其他可能使用的表达式有*(零或更多)和?(一次或根本不使用)。第四个和第五个元素声明与第二个类似,声明ingredient和instructions元素包含解析的字符数据。
Note
元素声明支持其他三种内容说明符。您可以指定<!ELEMENT名称ANY>来允许任何类型的元素内容,或者指定<!ELEMENT名称EMPTY>来禁止任何元素内容。要声明一个元素包含混合内容,您可以指定#PCDATA和一个元素名称列表,用竖线(|)分隔。例如,<!ELEMENT ingredient (#PCDATA | measure | note)*>表示ingredient元素可以包含已解析的字符数据、零个或多个measure元素以及零个或多个note元素。它没有指定被解析的字符数据和这些元素出现的顺序。但是,#PCDATA必须是列表中指定的第一项。在此上下文中使用正则表达式时,它必须出现在右括号的右侧。
清单 1-7 的 DTD 最后声明了菜谱语言的属性,其中只有一个:qty。属性声明的形式是<!ATTLISTename aname type default-value>,其中 ename 是属性所属元素的名称,aname 是属性的名称,type 是属性的类型,default-value 是属性的默认值。
属性声明将qty标识为ingredient的属性。它还说明了qty的类型是CDATA(任何不包括&符号、小于或大于符号或双引号的字符串都可能出现;这些字符可以分别通过&、<、>或"来表示,并且qty是可选的,当不存在时采用默认值1。
More About Attributes
DTD 允许您指定附加的属性类型:ID(为标识元素的属性创建唯一的标识符)、IDREF(属性值是位于文档中其他位置的元素)、IDREFS(值由多个IDREF组成)、ENTITY(可以使用外部二进制数据或未解析的实体)、ENTITIES(值由多个实体组成)、NMTOKEN(值限于任何有效的 XML 名称)、NMTOKENS(值由多个 XML 名称组成)、NOTATION(值已经通过 DTD 表示法声明指定)值用竖线分隔)。
您可以不逐字指定缺省值,而是指定#REQUIRED来表示属性必须始终具有某个值(<!ATTLIST名称名称类型#REQUIRED>),#IMPLIED来表示属性是可选的并且不提供缺省值(<!ATTLIST名称名称类型#IMPLIED>),或者#FIXED来表示属性是可选的并且在使用时必须始终采用 DTD 分配的缺省值(<!ATTLIST名称名称类型#FIXED "value">)。
您可以在一个ATTLIST声明中指定属性列表。例如,<!ATTLISTename aname1 type 1 default-value 1 aname2 type 2 default-value 2>声明了标识为 aname 1 和 aname 2 的两个属性。
基于 DTD 的验证 XML 解析器在验证文档之前,要求文档包含一个标识 DTD 的文档类型声明,该 DTD 指定了文档的语法。
Note
文档类型定义和文档类型声明是两回事。DTD 首字母缩略词标识文档类型定义,从不标识文档类型声明。
文档类型声明紧跟在 XML 声明之后,并以下列方式之一指定:
<!DOCTYPEroot-element-nameSYSTEMuri>通过 uri 引用一个外部但私有的 DTD。引用的 DTD 不可用于公共审查。例如,我可能将我的配方语言的 DTD 文件(recipe.dtd)存储在我的www.javajeff.ca网站上的私有dtds目录中,并使用<!DOCTYPE recipe SYSTEM "http://www.javajeff.ca/dtds/recipe.dtd">通过系统标识符http://www.javajeff.ca/dtds/recipe.dtd来标识该 DTD 的位置。<!DOCTYPEroot-element-namePUBLICfpi uri>通过 FPI、正式的公共标识符(参见http://en.wikipedia.org/wiki/Formal_Public_Identifier)和 uri 引用外部但公共的 DTD。如果验证 XML 解析器不能通过公共标识符 fpi 定位 DTD,它可以使用系统标识符 uri 来定位 DTD。比如<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">首先通过公共标识符-//W3C//DTD XHTML 1.0 Transitional//EN引用 XHTML 1.0 DTD,其次通过系统标识符http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd引用。<!DOCTYPE根元素[dtd]>引用了一个内部 dtd,一个嵌入在 XML 文档中的 DTD。内部 DTD 必须出现在方括号中。
清单 1-8 显示了带有内部 DTD 的清单 1-1 (减去<recipe>和</recipe>标签之间的子元素)。
<?xml version="1.0"?>
<!DOCTYPE recipe [
<!ELEMENT recipe (title, ingredients, instructions)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT ingredients (ingredient+)>
<!ELEMENT ingredient (#PCDATA)>
<!ELEMENT instructions (#PCDATA)>
<!ATTLIST ingredient qty CDATA "1">
]>
<recipe>
<!-- Child elements removed for brevity. -->
</recipe>
Listing 1-8.The Recipe Document with an Internal DTD
Note
文档可以有内部和外部 DTDs 比如<!DOCTYPE recipe SYSTEM " http://www.javajeff.ca/dtds/recipe.dtd " [ <!ELEMENT ...>]>。内部 DTD 称为内部 DTD 子集,外部 DTD 称为外部 DTD 子集。任何一个子集都不能覆盖另一个子集的元素声明。
您还可以在 dtd 中声明符号、一般实体和参数实体。符号是一段任意的数据,通常描述未解析的二进制数据的格式,通常具有形式<!NOTATION name SYSTEM uri >,其中 name 标识符号,uri 标识某种插件,该插件可以代表解析 XML 文档的应用程序处理数据。例如,<!NOTATION image SYSTEM "psp.exe">声明了一个名为image的符号,并将 Windows 可执行文件psp.exe标识为处理图像的插件。
使用符号通过媒体类型指定二进制数据类型也很常见(参见 https://en.wikipedia.org/wiki/Media_type )。例如,<!NOTATION image SYSTEM "image/jpeg">声明了一个图像符号,它标识了联合图像专家组图像的image/jpeg媒体类型。
通用实体是通过通用实体引用从 XML 文档内部引用的实体,格式为&name;。例如预定义的lt、gt、amp、apos和quot角色实体,其<、>、&、'和"角色实体引用分别是角色<、>、&、’和"的别名。
一般实体分为内部实体和外部实体。内部一般实体是其值存储在 DTD 中的一般实体,其形式为<!ENTITY name value >,其中 name 标识实体,value 指定其值。例如,<!ENTITY copyright "Copyright © 2016 Jeff Friesen. All rights reserved.">声明了一个名为copyright的内部通用实体。这个实体的值可能包括另一个声明的实体,比如©(版权符号的 HTML 实体),并且可以通过指定©right;从 XML 文档中的任何地方引用。
外部一般实体是其值存储在 DTD 外部的一般实体。该值可能是文本数据(如 XML 文档),也可能是二进制数据(如 JPEG 图像)。外部通用实体分为外部已解析通用实体和外部未解析通用实体。
外部解析的通用实体引用存储该实体的文本数据的外部文件,当在文档中指定了通用实体引用时,该文件将被插入到文档中并由验证解析器进行解析,并且该文件具有形式<!ENTITY name SYSTEM uri >,其中 name 标识实体,uri 标识外部文件。例如,<!ENTITY chapter-header SYSTEM " http://www.javajeff.ca/entities/chapheader.xml ">将chapheader.xml标识为存储要插入到 XML 文档中&chapter-header;出现的任何地方的 XML 内容。可以指定替代的<!ENTITY名称PUBLIC fpi uri >形式。
Caution
因为外部文件的内容可能被解析,所以该内容必须是格式良好的。
一个外部未解析的通用实体引用一个存储实体二进制数据的外部文件,其格式为<!ENTITY name SYSTEM uri NDATA nname >,其中 name 标识实体,uri 定位外部文件,NDATA标识名为 nname 的符号声明。该符号通常标识用于处理二进制数据或该数据的互联网媒体类型的插件。例如,<!ENTITY photo SYSTEM "photo.jpg" NDATA image>将名称photo与外部二进制文件photo.png和符号image相关联。可以指定替代的<!ENTITY名称PUBLIC fpi uri NDATA名称>形式。
Note
XML 不允许对外部通用实体的引用出现在属性值中。例如,您不能在属性值中指定&chapter-header;。
参数实体是通过参数实体引用从 DTD 内部引用的实体,格式为%name;。它们有助于消除元素声明中的重复内容。例如,您正在为一家大公司创建一个 DTD,这个 DTD 包含三个元素声明:<!ELEMENT salesperson (firstname, lastname)>、<!ELEMENT lawyer (firstname, lastname)>和<!ELEMENT accountant (firstname, lastname)>。每个元素都包含重复的子元素内容。如果你需要添加另一个子元素(比如middleinitial,你需要确保所有的元素都被更新;否则,您将面临 DTD 格式错误的风险。参数实体可以帮你解决这个问题。
参数实体分为内部实体和外部实体。内部参数实体是其值存储在 DTD 中的参数实体,其形式为<!ENTITY % name value >,其中 name 标识实体,value 指定其值。例如,<!ENTITY % person-name "firstname, lastname">声明了一个名为person-name的参数实体,其值为firstname, lastname。一旦声明,这个实体可以在前面的三个元素声明中被引用,如下:<!ELEMENT salesperson (%person-name;)>、<!ELEMENT lawyer (%person-name;)>和<!ELEMENT accountant (%person-name;)>。不是像以前那样将middleinitial添加到salesperson、lawyer和accountant中,而是像在<!ENTITY % person-name "firstname, middleinitial, lastname">中那样将这个子元素添加到person-name中,并且这个更改将被应用到这些元素声明中。
外部参数实体是其值存储在 DTD 外部的参数实体。它的形式是<!ENTITY % name SYSTEM uri>,其中name标识实体,uri定位外部文件。例如,<!ENTITY % person-name SYSTEM " http://www.javajeff.ca/entities/names.dtd ">将names.dtd标识为存储要插入到 DTD 中%person-name;出现的任何地方的firstname, lastname文本。可以指定替代的<!ENTITY %名称PUBLIC fpi uri >形式。
Note
这个讨论总结了 DTD 的基础。另外一个没有涉及的主题(为了简洁)是条件包含,它允许您指定 DTD 中可供解析器使用的部分,通常与参数实体引用一起使用。
XML 模式
XML Schema 是一种语法语言,用于声明 XML 文档的结构、内容和语义(含义)。这种语言的语法文档被称为模式,模式本身就是 XML 文档。模式必须符合 XML 模式 DTD(参见 www.w3.org/2001/XMLSchema.dtd )。
W3C 引入了 XML Schema 来克服 DTD 的局限性,比如 DTD 缺乏对名称空间的支持。此外,XML Schema 提供了一种面向对象的方法来声明 XML 文档的语法。这种语法语言提供了比 DTD 的 CDATA 和 PCDATA 类型更多的基本类型。例如,整数、浮点、各种日期和时间以及字符串类型都是 XML 模式的一部分。
Note
XML Schema 预定义了 19 种原语类型,通过以下标识符来表示:anyURI、base64Binary、boolean、date、dateTime、decimal、double、duration、float、hexBinary、gDay、gMonth、gMonthDay、gYear、gYearMonth、NOTATION、QName、string和time。
XML Schema 提供了限制(通过约束减少允许值的集合)、列表(允许值的序列)和联合(允许从几种类型中选择值)派生方法,用于从这些原始类型创建新的简单类型。比如 XML Schema 通过限制从decimal派生出 13 个整数类型;这些类型通过以下标识符表示:byte、int、integer、long、negativeInteger、nonNegativeInteger、nonPositiveInteger、positiveInteger、short、unsignedByte、unsignedInt、unsignedLong和unsignedShort。它还支持从简单类型创建复杂类型。
熟悉 XML 模式的一个好方法是通过一个例子,比如为清单 1-1 的菜谱语言文档创建一个模式。创建这个配方语言模式的第一步是识别它的所有元素和属性。要素有recipe、title、ingredients、instructions、ingredient;qty是孤属性。
下一步是根据 XML Schema 的内容模型对元素进行分类,该模型指定了元素中可以包含的子元素和文本节点的类型(参见 [http://en.wikipedia.org/wiki/Node_(computer_science](en.wikipedia.org/wiki/Node_(… ))。当元素没有子元素或文本节点时,该元素被认为是空的;当只接受文本节点时,该元素被认为是简单的;当只接受子元素时,该元素被认为是复杂的;当接受子元素和文本节点时,该元素被认为是混合的。清单 1-1 的元素都没有空的或混合的内容模型。然而,title、ingredient和instructions元素具有简单的内容模型;并且recipe和ingredients元素具有复杂的内容模型。
对于具有简单内容模型的元素,我们可以区分有属性的元素和没有属性的元素。XML Schema 将具有简单内容模型并且没有属性的元素分类为简单类型。此外,它将具有简单内容模型和属性的元素或者来自其他内容模型的元素分类为复杂类型。此外,XML Schema 将属性分类为简单类型,因为它们只包含文本值——属性没有子元素。清单 1-1 的title和instructions元素及其qty属性是简单类型。它的recipe、ingredients和ingredient元素是复杂类型。
此时,您可以开始声明模式了。以下代码片段展示了介绍性的schema元素:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
元素介绍了语法。它还将常用的xs名称空间前缀分配给标准 XML 模式名称空间;xs:随后被添加到 XML 模式元素名称的前面。
接下来,使用element元素声明title和instructions简单类型元素,如下所示:
<xs:element name="title" type="xs:string"/>
<xs:element name="instructions" type="xs:string"/>
XML Schema 要求每个元素都有一个名称,并且(与 DTD 不同)与一个类型相关联,该类型标识元素中存储的数据类型。例如,第一个element声明通过其name属性将title标识为名称,通过其type属性将string标识为类型(字符串或字符数据出现在<title>和</title>标记之间)。xs:string中的xs:前缀是必需的,因为string是预定义的 W3C 类型。
继续,现在使用attribute元素声明qty简单类型属性,如下所示:
<xs:attribute name="qty" type="xs:unsignedInt" default="1"/>
这个attribute元素声明了一个名为qty的属性。我选择unsignedInt作为这个属性的type,因为数量是非负值。此外,我指定了1作为没有指定qty时的default值— attribute元素默认声明可选属性。
Note
元素和属性声明的顺序在模式中并不重要。
既然已经声明了简单类型,就可以开始声明复杂类型了。首先,声明recipe如下:
<xs:element name="recipe">
<xs:complexType>
<xs:sequence>
<xs:element ref="title"/>
<xs:element ref="ingredients"/>
<xs:element ref="instructions"/>
</xs:sequence>
</xs:complexType>
</xs:element>
该声明声明recipe是一个复杂类型(通过complexType元素),由一个title元素、一个ingredients元素和一个instructions元素组成(通过sequence元素)。这些元素中的每一个都由一个不同的element声明,这个不同的element由它的element的ref属性引用。
下一个要声明的复杂类型是ingredients。下面的代码片段提供了它的声明:
<xs:element name="ingredients">
<xs:complexType>
<xs:sequence>
<xs:element ref="ingredient" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
这个声明声明ingredients是一个复杂类型,由一个或多个ingredient元素组成。“或更多”是通过包含element的maxOccurs属性并将该属性的值设置为unbounded来指定的。
Note
maxOccurs属性标识一个元素可以出现的最大次数。一个类似的minOccurs属性标识了一个元素出现的最小次数。每个属性可以被赋予0或一个正整数。此外,您可以为maxOccurs指定unbounded,这意味着元素的出现次数没有上限。每个属性的默认值为1,这意味着当两个属性都不存在时,一个元素只能出现一次。
最后要声明的复杂类型是ingredient。虽然ingredient只能包含文本节点,这意味着它应该是一个简单的类型,但正是qty属性的存在使它变得复杂。查看以下声明:
<xs:element name="ingredient">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute ref="qty"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
名为ingredient的元素是一个复杂类型(因为它有可选的qty属性)。simpleContent元素表示ingredient只能包含简单的内容(文本节点),extension元素表示ingredient是一个新类型,扩展了预定义的string类型(通过base属性指定),意味着ingredient继承了string的所有属性和结构。此外,ingredient被赋予了一个附加的qty属性。
清单 1-9 将前面的例子组合成一个完整的模式。
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="title" type="xs:string"/>
<xs:element name="instructions" type="xs:string"/>
<xs:attribute name="qty" type="xs:unsignedInt" default="1"/>
<xs:element name="recipe">
<xs:complexType>
<xs:sequence>
<xs:element ref="title"/>
<xs:element ref="ingredients"/>
<xs:element ref="instructions"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="ingredients">
<xs:complexType>
<xs:sequence>
<xs:element ref="ingredient" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="ingredient">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute ref="qty"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
Listing 1-9.The Recipe Document’s Schema
创建模式后,您可以从配方文档中引用它。通过在文档的根元素开始标记(<recipe>)上指定xmlns:xsi和xsi:schemaLocation属性来完成这个任务,如下所示:
<recipe
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.javajeff.ca/schemas recipe.xsd">
xmlns属性将 http://www.javajeff.ca/ 标识为文档的默认名称空间。无前缀的元素及其无前缀的属性属于此命名空间。
xmlns:xsi属性将传统的xsi (XML 模式实例)前缀与标准的 http://www.w3.org/2001/XMLSchema-instance 名称空间相关联。文档中唯一以xsi:为前缀的项目是schemaLocation。
schemaLocation属性用于定位模式。该属性的值可以是多对空格分隔的值,但在本例中被指定为一对这样的值。第一个值( http://www.javajeff.ca/schemas )标识模式的目标名称空间,第二个值(recipe.xsd)标识模式在该名称空间中的位置。
Note
符合 XML 模式语法的模式文件通常被指定为文件扩展名.xsd。
如果一个 XML 文档声明了一个名称空间(xmlns default 或xmlns:prefix),那么该名称空间必须对模式可用,以便验证解析器可以解析对该名称空间的元素和其他模式组件的所有引用。您还需要提到模式描述了哪个名称空间,通过在schema元素中包含targetNamespace属性可以做到这一点。例如,假设您的配方文档声明了一个默认的 XML 名称空间,如下所示:
<?xml version="1.0"?>
<recipe >
至少,您需要修改清单 1-9 的schema元素,以包含targetNameSpace和配方文档的默认名称空间作为targetNameSpace的值,如下所示:
<xs:schema targetNamespace="http://www.javajeff.ca/"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
Exercises
以下练习旨在测试您对第一章内容的理解。
Define XML. True or false: XML and HTML are descendents of SGML. What language features does XML provide for use in defining custom markup languages? What is the XML declaration? Identify the XML declaration’s three attributes. Which attribute is nonoptional? True or false: An element always consists of a start tag followed by content followed by an end tag. Following the XML declaration, an XML document is anchored in what kind of element? What is mixed content? What is a character reference? Identify the two kinds of character references. What is a CDATA section? Why would you use it? Define namespace. What is a namespace prefix? True or false: A tag’s attributes don’t need to be prefixed when those attributes belong to the element. What is a comment? Where can it appear in an XML document? Define processing instruction. Identify the rules that an XML document must follow to be considered well formed. What does it mean for an XML document to be valid? A parser that performs validation compares an XML document to a grammar document. Identify the two common grammar languages. What is the general syntax for declaring an element in a DTD? Which grammar language lets you create complex types from simple types? Create a books.xml document file with a books root element. The books element must contain one or more book elements, where a book element must contain one title element, one or more author elements, and one publisher element (and in that order). Also, the book element’s <book> tag must contain isbn and pubyear attributes. Record Advanced C++/James Coplien/Addison Wesley/0201548550/1992 in the first book element, Beginning Groovy and Grails/Christopher M. Judd/Joseph Faisal Nusairat/James Shingler/Apress/9781430210450/2008 in the second book element, and Effective Java/Joshua Bloch/Addison Wesley/0201310058/2001 in the third book element. Modify books.xml to include an internal DTD that satisfies the previous exercise’s requirements.
摘要
应用程序经常使用 XML 文档来存储和交换数据。XML 定义了以格式编码文档的规则,这种格式既可读又机器可读。它是一种定义词汇表的元语言,这是 XML 的重要性和受欢迎程度的关键。
XML 提供了几种用于定义自定义标记语言的语言功能。这些特性包括 XML 声明、元素和属性、字符引用和 CDATA 节、名称空间以及注释和处理指令。
HTML 是一种松散的语言,其中元素可以无序指定,结束标记可以省略,等等。相比之下,XML 文档格式良好,因为它们符合特定的规则,这使得它们更容易处理。XML 解析器只解析格式良好的 XML 文档。
在许多情况下,XML 文档也必须是有效的。有效的文档遵循语法文档所描述的约束。语法文档是用语法语言编写的,比如常用的文档类型定义和 XML Schema。
第二章介绍了 Java 解析 XML 文档的 SAX API。
二、使用 SAX 解析 XML 文档
Java 为解析 XML 文档提供了几个 API。这些 API 中最基本的是 SAX,这是本章的重点。
什么是萨克斯?
Simple API for XML (SAX)是一个基于事件的 Java API,用于从头到尾顺序解析 XML 文档。当面向 SAX 的解析器遇到文档信息集(描述 XML 文档信息的抽象数据模型;参见 http://en.wikipedia.org/wiki/XML_Information_Set ),它通过调用应用程序的一个处理程序(解析器调用其方法以使事件信息可用的对象)中的一个方法,使该项目作为事件对应用程序可用,应用程序先前已经向解析器注册了该处理程序。然后,应用程序可以通过以某种方式处理 infoset 项来使用该事件。
SAX 解析器比 DOM 解析器更节省内存(参见第三章),因为它不需要将整个文档放入内存。这个好处变成了使用 XPath(见第五章)和 XSLT(见第六章)的一个缺点,它们需要将整个文档存储在内存中。
Note
根据其官方网站( www.saxproject.org ),SAX 起源于 Java 的 XML 解析 API。然而,SAX 并不是 Java 的专利。微软也支持 SAX。NET 框架(参见 http://saxdotnet.sourceforge.net )。
探索 SAX API
SAX 有两个主要版本。Java 通过javax.xml.parsers包的抽象SAXParser和SAXParserFactory类实现 SAX 1,通过org.xml.sax包的XMLReader接口和org.xml.sax.helpers包的XMLReaderFactory类实现 SAX 2。org.xml.sax、org.xml.sax.ext和org.xml.sax.helpers包提供了各种类型来增强这两种 Java 实现。
Note
我只研究 SAX 2 实现,因为 SAX 2 提供了关于 XML 文档的附加信息集项目(比如注释和 CDATA 节通知)。
获得 SAX 2 解析器
实现XMLReader接口的类描述了基于 SAX 2 的解析器。这些类的实例是通过调用XMLReaderFactory类的createXMLReader()类方法获得的。例如,下面的代码片段调用该类的XMLReader createXMLReader()类方法来创建并返回一个XMLReader对象:
XMLReader xmlr = XMLReaderFactory.createXMLReader();
方法调用返回一个XMLReader实现类的实例,并将其引用分配给xmlr。
Note
在幕后,createXMLReader()试图根据一个查找过程从系统默认值创建一个XMLReader对象,该过程首先检查org.xml.sax.driver系统属性以查看它是否有值。如果是这样,这个属性的值被用作实现XMLReader的类的名称。此外,还尝试实例化该类并返回实例。当createXMLReader()不能获得一个合适的类或者实例化该类时,抛出org.xml.sax.SAXException类的一个实例。
巡视 XMLReader 方法
返回的XMLReader对象提供了几种配置解析器和解析文档内容的方法。这些方法如下:
ContentHandlergetContentHandler()返回当前内容处理程序,它是一个实现org.xml.sax.ContentHandler接口的类的实例,或者当没有注册时返回null。DTDHandlergetDTDHandler()返回当前的 DTD 处理程序,它是一个实现了org.xml.sax.DTDHandler接口的类的实例,或者当没有注册时返回null。EntityResolvergetEntityResolver()返回当前实体解析器,它是一个实现org.xml.sax.EntityResolver接口的类的实例,或者当没有注册时返回null。ErrorHandlergetErrorHandler()返回当前的错误处理程序,它是一个实现org.xml.sax.ErrorHandler接口的类的实例,或者当没有注册时返回null。booleangetFeature(String name)返回对应于由name标识的特征的布尔值,它必须是全限定的 URI。当名字没有被识别为特性时,这个方法抛出org.xml.sax.SAXNotRecognizedException,当名字被识别但在调用getFeature()时不能确定相关值时,抛出org.xml.sax.SAXNotSupportedException。SAXNotRecognizedException和SAXNotSupportedException是SAXException的子类。ObjectgetProperty(String name)返回name标识的属性对应的java.lang.Object实例,必须是全限定的 URI。当名字没有被识别为属性时,这个方法抛出SAXNotRecognizedException,当名字被识别但在调用getProperty()时不能确定相关值时,抛出SAXNotSupportedException。void parse(InputSource input)解析 XML 文档,直到文档被解析后才返回。input参数存储了对一个org.xml.sax.InputSource对象的引用,该对象描述了文档的来源(比如一个java.io.InputStream对象,或者甚至是一个基于java.lang.String的系统标识符 URI)。当无法读取源代码时,该方法抛出java.io.IOException,当解析失败时抛出SAXException,这可能是由于违反了格式良好性。void parse(String systemId)通过执行parse(new InputSource(systemId));来解析 XML 文档。voidsetContentHandler(ContentHandler handler)向解析器注册由handler标识的内容处理器。ContentHandler接口提供了 11 个回调方法,调用这些方法来报告各种解析事件(比如元素的开始和结束)。void setDTDHandler(DTDHandler handler)向解析器注册handler标识的 DTD 处理程序。DTDHandler接口提供了一对回调方法,用于报告符号和外部未解析的实体。void setEntityResolver(EntityResolver resolver)向解析器注册resolver标识的实体解析器。EntityResolver接口为解析实体提供了单一的回调方法。void setErrorHandler(ErrorHandler handler)向解析器注册handler标识的错误处理程序。ErrorHandler接口提供了三个回调方法,用于报告致命错误(阻止进一步解析的问题,比如违反了格式良好性)、可恢复错误(不阻止进一步解析的问题,比如验证失败)和警告(不需要解决的错误,比如在元素名前面加上 W3C 保留的前缀xml)。void setFeature(String name, boolean value)将value分配给name标识的特征,该特征必须是完全合格的 URI。当名字没有被识别为特性时,这个方法抛出SAXNotRecognizedException,当名字被识别但是在调用setFeature()时不能设置相关值时,抛出SAXNotSupportedException。void setProperty(String name, Object value)将value分配给name标识的房产,该房产必须是完全合格的 URI。当名字没有被识别为属性时,这个方法抛出SAXNotRecognizedException,当名字被识别但在调用setProperty()时不能设置相关值时,抛出SAXNotSupportedException。
如果没有安装处理程序,所有与该处理程序相关的事件都会被忽略。不安装错误处理程序可能会有问题,因为正常的处理可能无法继续,应用程序也不会意识到有什么地方出错了。如果没有安装实体解析器,解析器会执行自己的默认解析。在这一章的后面我会有更多关于实体解析的内容。
Note
通常在解析文档之前安装新的内容处理程序、DTD 处理程序、实体解析程序或错误处理程序,但也可以在解析文档时安装。当下一个事件发生时,解析器开始使用处理程序。
设置功能和属性
获得一个XMLReader对象后,您可以通过设置其特性和属性来配置该对象。特性是描述解析器模式的名称-值对,比如验证。相反,属性是一个名称-值对,它描述了解析器接口的一些其他方面,例如词法处理程序,它通过提供用于报告注释、CDATA 分隔符和一些其他语法结构的回调方法来增强内容处理程序。
特性和属性有名称,名称必须是以http://前缀开头的绝对 URIs。一个特性的值总是一个布尔值true / false。相反,属性值是一个任意的对象。下面的代码片段演示了如何设置功能和属性:
xmlr.setFeature("http://xml.org/sax/features/validation", true);
xmlr.setProperty("http://xml.org/sax/properties/lexical-handler",
new LexicalHandler() { /* ... */ });
setFeature()调用启用了validation特性,以便解析器执行验证。特征名称以 http://xml.org/sax/features/ 为前缀。
Note
解析器必须支持namespaces和namespace-prefixes特性。namespaces决定是否将 URIs 和本地名字传递给ContentHandler的startElement()和endElement()方法。默认为true—这些名字都是经过的。当false. namespace-prefixes决定名称空间声明的xmlns和xmlns:prefix属性是否包含在传递给startElement()的org.xml.sax.Attributes列表中时,解析器可以传递空字符串,并且还决定限定名是否作为方法的第三个参数传递——限定名是一个前缀加一个本地名。它默认为false,意味着不包括xmlns和xmlns:prefix,也意味着解析器不必传递限定名。没有强制的属性。JDK 文档的org.xml.sax包页面列出了标准的 SAX 2 特性和属性。
setProperty()调用将实现org.xml.sax.ext.LexicalHandler接口的类的实例分配给lexical-handler属性,这样就可以调用接口方法来报告注释、CDATA 部分等等。物业名称以 http://xml.org/sax/properties/ 为前缀。
Note
与ContentHandler、DTDHandler、EntityResolver和ErrorHandler不同,LexicalHandler是一个扩展(它不是核心 SAX API 的一部分),这就是为什么XMLReader没有声明void setLexicalHandler(LexicalHandler handler)方法。如果你想安装一个词法处理程序,你必须使用XMLReader的setProperty()方法来安装这个处理程序作为 http://xml.org/sax/properties/lexical-handler 属性的值。
要素和属性可以是只读的,也可以是读写的。(在极少数情况下,功能或属性可能是只写的。)设置或读取特性或属性时,可能会抛出SAXNotSupportedException或SAXNotRecognizedException。例如,如果你试图修改一个只读的特征/属性,就会抛出一个SAXNotSupportedException类的实例。此外,如果在解析过程中调用setFeature()或setProperty(),可能会抛出这个异常。试图为不执行验证的解析器设置验证特性是一种抛出SAXNotRecognizedException类实例的情况。
浏览处理程序和解析器接口
由setContentHandler()、setDTDHandler()、setErrorHandler()安装的基于接口的处理程序;setEntityResolver()安装的实体解析器;并且lexical-handler属性描述的处理程序提供了各种回调方法。您需要理解这些方法,然后才能对它们进行编码,以便有效地响应解析事件。
旅游内容处理程序
ContentHandler声明了以下面向内容的信息回调方法:
void characters(char[] ch, int start, int length)通过ch数组报告一个元素的字符数据。传递给start和length的参数标识数组中与该方法调用相关的部分。字符通过一个char[]数组传递,而不是通过一个String对象传递,以优化性能。解析器通常将大量文档存储在一个数组中,并反复将对该数组的引用以及更新后的start和length值传递给characters()。void endDocument()报告已到达文档的结尾。应用程序可能会使用此方法来关闭输出文件或执行一些其他清理。voidendElement(String uri, String localName, String qName)报告已经到达一个元素的末尾。uri标识元素的名称空间 URI,或者当没有名称空间 URI 或名称空间处理尚未启用时为空。localName标识元素的本地名称,即没有前缀的名称(例如html或h:html中的html)。qName引用限定名,例如,当没有前缀时,h:html或html。当检测到结束标签时,调用endElement(),或者当检测到空元素标签时,紧接着startElement()调用。void endPrefixMapping(String prefix)报告已经到达名称空间前缀映射的结尾(例如xmlns:h),而prefix报告这个前缀(例如h)。void ignorableWhitespace(char[] ch, int start, int length)报告可忽略的空白(位于 DTD 不允许混合内容的标签之间的空白)。这个空白通常用于缩进标签。这些参数与characters()方法的目的相同。voidprocessingInstruction(String target, String data)报告一条处理指令,其中target标识指令指向的应用,data提供指令的数据(无数据时为空引用)。voidsetDocumentLocator(Locator locator)报告一个org.xml.sax.Locator对象(实现Locator接口的类的一个实例),可以调用它的int getColumnNumber()、int getLineNumber()、String getPublicId()和String getSystemId()方法来获得任何文档相关事件结束位置的位置信息,即使解析器没有报告错误。这个方法在startDocument()之前被调用,是保存Locator对象的好地方,这样就可以从其他回调方法中访问它。void skippedEntity(String name)报告所有跳过的实体。验证解析器解析所有一般的实体引用,但是非验证解析器可以选择跳过它们,因为非验证解析器不读取声明这些实体的 dtd。如果非验证解析器不读取 DTD,它就不知道实体是否被正确声明。非验证解析器不是试图读取 DTD 并报告实体的替换文本,而是用实体的名称调用skippedEntity()。void startDocument()报告已到达文档的开头。应用程序可能使用此方法来创建输出文件或执行一些其他初始化。void startElement(String uri, String localName, String qName, Attributes attributes)报告已经到达一个元素的开始。uri标识元素的名称空间 URI,或者当没有名称空间 URI 或者名称空间处理还没有启用时为空。localName标识元素的本地名,qName引用其限定名,attributes引用元素的属性列表——当没有属性时,该列表为空。当检测到开始标签或空元素标签时,调用startElement()。void startPrefixMapping(String prefix, String uri)报告已经到达名称空间前缀映射的开始处(例如xmlns:h="http://www.w3.org/1999/xhtml"),其中prefix报告该前缀(例如h),而uri报告该前缀映射到的 URI(例如http://www.w3.org/1999/xhtml)。
除了setDocumentLocator()之外,每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。
巡回演出
DTDHandler声明了以下面向 DTD 的信息回调方法:
void notationDecl(String name, String publicId, String systemId)报告一个批注声明,其中name提供该声明的name属性值,publicId提供该声明的public属性值(该值不可用时为空引用),systemId提供该声明的system属性值。void unparsedEntityDecl(String name, String publicId, String systemId, String notationName)报告外部未解析的实体声明,其中name提供该声明的name属性的值,publicId提供public属性的值(该值不可用时为空引用),systemId提供system属性的值,notationName提供NDATA名称。
每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。
旋转误差处理程序
ErrorHandler声明了以下面向错误的信息回调方法:
void error(SAXParseException exception)报告发生了可恢复的解析器错误(通常是文档无效);细节通过传递给exception的参数指定。此方法通常被重写以通过命令窗口报告错误,或者将其记录到文件或数据库中。void fatalError(SAXParseException exception)报告出现不可恢复的解析器错误(文档格式不正确);细节是通过传递给exception的参数指定的。此方法通常被重写,以便应用程序可以在停止处理文档之前记录错误(因为文档不再可靠)。void warning(SAXParseException e)报告发生了非严重错误(如以保留的xml字符序列开头的元素名);细节通过传递给exception的参数指定。此方法通常被重写以通过控制台报告警告,或者将其记录到文件或数据库中。
每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。
旅游实体解决方案
EntityResolver声明了下面的回调方法:
- 调用
InputSource resolveEntity(String publicId, String systemId)让应用程序通过返回基于不同 URI 的自定义InputSource对象来解析外部实体(如外部 DTD 子集)。该方法被声明为在检测到面向 SAX 的问题时抛出SAXException,并且还被声明为在遇到 I/O 错误时抛出IOException,这可能是为了响应为正在创建的InputSource创建一个InputStream对象或java.io.Reader对象。
旋转字典处理程式
LexicalHandler声明了以下附加的面向内容的信息回调方法:
void comment(char[] ch, int start, int length)通过ch数组报告注释。传递给start和length的参数标识了数组中与该方法调用相关的部分。void endCDATA()报告 CDATA 段的结尾。void endDTD()报告 DTD 的结束。void endEntity(String name)报告由name标识的实体的结束。void startCDATA()报告 CDATA 段的开始。void startDTD(String name, String publicId, String systemId)报告由name. publicId标识的 DTD 的开始指定外部 DTD 子集声明的公共标识符,或者当没有声明时为空引用。类似地,systemId为外部 DTD 子集指定声明的系统标识符,或者当没有声明时为空引用。void startEntity(String name)报告由name标识的实体的开始。
每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。
因为在每个接口中实现所有方法可能很繁琐,所以 SAX API 方便地提供了org.xml.sax.helpers.DefaultHandler适配器类来减轻您的负担。DefaultHandler实现ContentHandler、DTDHandler、EntityResolver、ErrorHandler。SAX 也提供了org.xml.sax.ext.DefaultHandler2,它继承了DefaultHandler,也实现了LexicalHandler。
演示 SAX API
清单 2-1 向SAXDemo展示了源代码,这是一个演示 SAX API 的应用程序。该应用程序由一个SAXDemo入口点类和一个DefaultHandler2的Handler子类组成。
import java.io.FileReader;
import java.io.IOException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
public class SAXDemo
{
public static void main(String[] args)
{
if (args.length < 1 || args.length > 2)
{
System.err.println("usage: java SAXDemo xmlfile [v]");
return;
}
try
{
XMLReader xmlr = XMLReaderFactory.createXMLReader();
if (args.length == 2 && args[1].equals("v"))
xmlr.setFeature("http://xml.org/sax/features/validation", true);
xmlr.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
Handler handler = new Handler();
xmlr.setContentHandler(handler);
xmlr.setDTDHandler(handler);
xmlr.setEntityResolver(handler);
xmlr.setErrorHandler(handler);
xmlr.setProperty("http://xml.org/sax/properties/lexical-handler",
handler);
xmlr.parse(new InputSource(new FileReader(args[0])));
}
catch (IOException ioe)
{
System.err.println("IOE: " + ioe);
}
catch (SAXException saxe)
{
System.err.println("SAXE: " + saxe);
}
}
}
Listing 2-1.
SAXDemo
SAXDemo的main()方法首先验证是否指定了一个或两个命令行参数(XML 文档的名称,后面可选地跟着小写字母v,它告诉SAXDemo创建一个验证解析器)。然后它创建一个XMLReader对象;有条件地使能validation特性并使能namespace-prefixes特性;实例化伴生的Handler类;安装这个Handler对象作为解析器的内容处理程序、DTD 处理程序、实体解析程序和错误处理程序。安装这个Handler对象作为lexical-handler属性的值;创建输入源以从文件中读取文档;并解析文档。
清单 2-2 中展示了Handler类的源代码。
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DefaultHandler2;
public class Handler extends DefaultHandler2
{
private Locator locator;
@Override
public void characters(char[] ch, int start, int length)
{
System.out.print("characters() [");
for (int i = start; i < start + length; i++)
System.out.print(ch[i]);
System.out.println("]");
}
@Override
public void comment(char[] ch, int start, int length)
{
System.out.print("characters() [");
for (int i = start; i < start + length; i++)
System.out.print(ch[i]);
System.out.println("]");
}
@Override
public void endCDATA()
{
System.out.println("endCDATA()");
}
@Override
public void endDocument()
{
System.out.println("endDocument()");
}
@Override
public void endDTD()
{
System.out.println("endDTD()");
}
@Override
public void endElement(String uri, String localName, String qName)
{
System.out.print("endElement() ");
System.out.print("uri=[" + uri + "], ");
System.out.print("localName=[" + localName + "], ");
System.out.println("qName=[" + qName + "]");
}
@Override
public void endEntity(String name)
{
System.out.print("endEntity() ");
System.out.println("name=[" + name + "]");
}
@Override
public void endPrefixMapping(String prefix)
{
System.out.print("endPrefixMapping() ");
System.out.println("prefix=[" + prefix + "]");
}
@Override
public void error(SAXParseException saxpe)
{
System.out.println("error() " + saxpe);
}
@Override
public void fatalError(SAXParseException saxpe)
{
System.out.println("fatalError() " + saxpe);
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length)
{
System.out.print("ignorableWhitespace() [");
for (int i = start; i < start + length; i++)
System.out.print(ch[i]);
System.out.println("]");
}
@Override
public void notationDecl(String name, String publicId, String systemId)
{
System.out.print("notationDecl() ");
System.out.print("name=[" + name + "]");
System.out.print("publicId=[" + publicId + "]");
System.out.println("systemId=[" + systemId + "]");
}
@Override
public void processingInstruction(String target, String data)
{
System.out.print("processingInstruction() [");
System.out.println("target=[" + target + "]");
System.out.println("data=[" + data + "]");
}
@Override
public InputSource resolveEntity(String publicId, String systemId)
{
System.out.print("resolveEntity() ");
System.out.print("publicId=[" + publicId + "]");
System.out.println("systemId=[" + systemId + "]");
// Do not perform a remapping.
InputSource is = new InputSource();
is.setPublicId(publicId);
is.setSystemId(systemId);
return is;
}
@Override
public void setDocumentLocator(Locator locator)
{
System.out.print("setDocumentLocator() ");
System.out.println("locator=[" + locator + "]");
this.locator = locator;
}
@Override
public void skippedEntity(String name)
{
System.out.print("skippedEntity() ");
System.out.println("name=[" + name + "]");
}
@Override
public void startCDATA()
{
System.out.println("startCDATA()");
}
@Override
public void startDocument()
{
System.out.println("startDocument()");
}
@Override
public void startDTD(String name, String publicId, String systemId)
{
System.out.print("startDTD() ");
System.out.print("name=[" + name + "]");
System.out.print("publicId=[" + publicId + "]");
System.out.println("systemId=[" + systemId + "]");
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes)
{
System.out.print("startElement() ");
System.out.print("uri=[" + uri + "], ");
System.out.print("localName=[" + localName + "], ");
System.out.println("qName=[" + qName + "]");
for (int i = 0; i < attributes.getLength(); i++)
System.out.println(" Attribute: " + attributes.getLocalName(i) +
", " + attributes.getValue(i));
System.out.println("Column number=[" + locator.getColumnNumber() +
"]");
System.out.println("Line number=[" + locator.getLineNumber() + "]");
}
@Override
public void startEntity(String name)
{
System.out.print("startEntity() ");
System.out.println("name=[" + name + "]");
}
@Override
public void startPrefixMapping(String prefix, String uri)
{
System.out.print("startPrefixMapping() ");
System.out.print("prefix=[" + prefix + "]");
System.out.println("uri=[" + uri + "]");
}
@Override
public void unparsedEntityDecl(String name, String publicId,
String systemId, String notationName)
{
System.out.print("unparsedEntityDecl() ");
System.out.print("name=[" + name + "]");
System.out.print("publicId=[" + publicId + "]");
System.out.print("systemId=[" + systemId + "]");
System.out.println("notationName=[" + notationName + "]");
}
@Override
public void warning(SAXParseException saxpe)
{
System.out.println("warning() " + saxpe);
}
}
Listing 2-2.The Handler Class
Handler子类非常简单;它根据特性和属性设置输出关于 XML 文档的每一条可能的信息。您会发现这个类非常便于探索事件发生的顺序以及各种特性和属性。
假设基于清单 2-1 和 2-2 的文件位于同一个目录中,编译如下:
javac SAXDemo.java
执行以下命令来解析清单 1-4 的svg-examples.xml文档:
java SAXDemo svg-examples.xml
SAXDemo通过显示以下输出做出响应(哈希码可能不同):
setDocumentLocator() locator=[com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy@6d06d69c]
startDocument()
startElement() uri=[], localName=[svg-examples], qName=[svg-examples]
Column number=[15]
Line number=[2]
characters() [
]
startElement() uri=[], localName=[example], qName=[example]
Column number=[13]
Line number=[3]
characters() [
The following Scalable Vector Graphics document describes a ]
characters() [
blue-filled and black-stroked rectangle.
]
startCDATA()
characters() [<svg width="100%" height="100%" version="1.1"
>
<rect width="300" height="100"
style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
</svg>]
endCDATA()
characters() [
]
endElement() uri=[], localName=[example], qName=[example]
characters() [
]
endElement() uri=[], localName=[svg-examples], qName=[svg-examples]
endDocument()
第一个输出行证明先调用setDocumentLocator()。它还标识了Locator对象,当调用startElement()时,调用该对象的getColumnNumber()和getLineNumber()方法来输出解析器位置——这些方法返回从 1 开始的列号和行号。
也许您对以下输出的三个实例感到好奇:
characters() [
]
跟在endCDATA()输出后面的这个输出的实例报告了一个回车/换行符组合,这个组合没有包含在前面的characters()方法调用中,传递给它的是 CDATA 部分的内容减去这些行结束符。相比之下,在对svg-examples的startElement()调用之后和对example的endElement()调用之后的输出实例就有些奇怪了。<svg-examples>和<example>之间没有内容,</example>和</svg-examples>之间也没有内容,还是有?
您可以通过修改svg-examples.xml来包含一个内部 DTD 来满足这种好奇心。在 XML 声明和<svg-examples>开始标记之间放置下面的 DTD(表示一个svg-examples元素包含一个或多个example元素,一个example元素包含解析的字符数据):
<!DOCTYPE svg-examples [
<!ELEMENT svg-examples (example+)>
<!ELEMENT example (#PCDATA)>
]>
继续,执行以下命令:
java SAXDemo svg-examples.xml
这一次,您应该会看到以下输出(尽管 hashcode 可能会有所不同):
setDocumentLocator() locator=[com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy@6d06d69c]
startDocument()
startDTD() name=[svg-examples]publicId=[null]systemId=[null]
endDTD()
startElement() uri=[], localName=[svg-examples], qName=[svg-examples]
Column number=[15]
Line number=[6]
ignorableWhitespace() [
]
startElement() uri=[], localName=[example], qName=[example]
Column number=[13]
Line number=[7]
characters() [
The following Scalable Vector Graphics document describes a
blue-filled and black-stroked rectangle.]
characters() [
]
startCDATA()
characters() [<svg width="100%" height="100%" version="1.1"
>
<rect width="300" height="100"
style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
</svg>]
endCDATA()
characters() [
]
endElement() uri=[], localName=[example], qName=[example]
ignorableWhitespace() [
]
endElement() uri=[], localName=[svg-examples], qName=[svg-examples]
endDocument()
这个输出揭示了在svg-examples的startElement()之后和example的endElement()之后调用了ignorableWhitespace()方法。产生奇怪输出的前两个对characters()的调用报告了可忽略的空白。
回想一下,我以前将可忽略的空白定义为位于 DTD 不允许混合内容的标签之间的空白。例如,DTD 指出svg-examples应该只包含example元素,不包含example元素和解析的字符数据。然而,<svg-examples>标签后面的行结束符和<example>前面的前导空格是解析的字符数据。解析器现在通过调用ignorableWhitespace()来报告这些字符。
这一次,以下输出只出现了两次:
characters() [
]
第一次出现时,将行结束符与example元素的文本分开报告(在 CDATA 部分之前);它以前没有这样做,这证明了用元素的全部或部分内容调用了characters()。再次,第二次出现报告 CDATA 部分后面的行结束符。
让我们在没有之前给出的内部 DTD 的情况下验证svg-examples.xml。您可以通过执行下面的命令来做到这一点——不要忘记包含v命令行参数,否则文档将无法验证:
java SAXDemo svg-examples.xml v
在它的输出中有几行以error()为前缀的代码,如下所示:
error() org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 14; Document is invalid: no grammar found.
error() org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 14; Document root element "svg-examples", must match DOCTYPE root "null".
这几行表明还没有找到 DTD 语法。此外,解析器报告了svg-examples(它认为第一个遇到的元素是根元素)和null(它认为在没有 DTD 的情况下null是根元素的名称)之间的不匹配。这两种违规都不被认为是致命的,这就是为什么叫error()而不是fatalError()的原因。
将内部 DTD 添加到svg-examples.xml并重新执行java SAXDemo svg-examples.xml v。这一次,您应该在输出中看不到带有error()前缀的行。
Tip
SAX 2 验证默认为根据 DTD 进行验证。相反,要根据基于 XML 模式的模式进行验证,请将带有 http://www.w3.org/2001/XMLSchema 值的schemaLanguage属性添加到XMLReader对象中。通过在xmlr.parse(new InputSource(new FileReader(args[0])));前指定xmlr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");为SAXDemo完成此任务。
创建自定义实体解析程序
在第一章探索 XML 时,我向您介绍了实体的概念,它是别名数据。然后,我讨论了一般实体和参数实体的内部和外部变体。
不同于其值在 DTD 中指定的内部实体,外部实体的值在 DTD 之外指定,并通过公共和/或系统标识符来标识。系统标识符是 URI,而公共标识符是正式的公共标识符。
XML 解析器通过连接到适当系统标识符的InputSource对象读取外部实体(包括外部 DTD 子集)。在许多情况下,您向解析器传递一个系统标识符或InputSource对象,让解析器发现在哪里可以找到从当前文档实体引用的其他实体。
但是,出于性能或其他原因,您可能希望解析器从不同的系统标识符中读取外部实体的值,例如本地 DTD 副本的系统标识符。您可以通过创建一个实体解析器来完成这项任务,该实体解析器使用公共标识符来选择不同的系统标识符。当遇到外部实体时,解析器调用自定义实体解析器来获取这个标识符。
考虑清单 2-3 对清单 1-1 的烤奶酪三明治配方的正式说明。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE recipeml PUBLIC "-//FormatData//DTD RecipeML 0.5//EN"
"http://www.formatdata.com/recipeml/recipeml.dtd">
<recipeml version="0.5">
<recipe>
<head>
<title>Grilled Cheese Sandwich</title>
</head>
<ingredients>
<ing>
<amt><qty>2</qty><unit>slice</unit></amt>
<item>bread</item>
</ing>
<ing>
<amt><qty>1</qty><unit>slice</unit></amt>
<item>cheese</item>
</ing>
<ing>
<amt><qty>2</qty><unit>pat</unit></amt>
<item>margarine</item>
</ing>
</ingredients>
<directions>
<step>Place frying pan on element and select medium heat.</step>
<step>For each bread slice, smear one pat of margarine on one side
of bread slice.</step>
<step>Place cheese slice between bread slices with margarine-smeared sides away from the cheese.</step>
<step>Place sandwich in frying pan with one margarine-smeared size in contact with pan.</step>
<step>Fry for a couple of minutes and flip.</step>
<step>Fry other side for a minute and serve.</step>
</directions>
</recipe>
</recipeml>
Listing 2-3.XML-Based Recipe for a Grilled Cheese Sandwich Specified in Recipe Markup Language
清单 2-3 用食谱标记语言(RecipeML)指定了烤奶酪三明治食谱,Recipe ml 是一种基于 XML 的标记食谱的语言。(一家名为 FormatData 的公司在 2000 年发布了这种格式; www.formatdata.com见。)
文档类型声明将-//FormatData//DTD RecipeML 0.5//EN报告为正式的公共标识符,将 http://www.formatdata.com/recipeml/recipeml.dtd 报告为系统标识符。让我们将这个正式的公共标识符映射到recipeml.dtd,一个 DTD 文件本地副本的系统标识符,而不是保留默认的映射。
要创建一个定制的实体解析器来执行这种映射,您需要声明一个类,该类根据它的InputSource resolveEntity(String publicId, String systemId)方法来实现EntityResolver接口。然后使用传递的publicId值作为指向所需systemId值的映射的键,然后使用这个值创建并返回一个定制的InputSource。清单 2-4 展示了结果类。
import java.util.HashMap;
import java.util.Map;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class LocalRecipeML implements EntityResolver
{
private Map<String, String> mappings = new HashMap<>();
LocalRecipeML()
{
mappings.put("-//FormatData//DTD RecipeML 0.5//EN", "recipeml.dtd");
}
@Override
public InputSource resolveEntity(String publicId, String systemId)
{
if (mappings.containsKey(publicId))
{
System.out.println("obtaining cached recipeml.dtd");
systemId = mappings.get(publicId);
InputSource localSource = new InputSource(systemId);
return localSource;
}
return null;
}
}
Listing 2-4.
LocalRecipeML
列表 2-4 声明LocalRecipeML。该类的构造函数在映射中存储 RecipeML DTD 的正式公共标识符和该 DTD 文档的本地副本的系统标识符。
Note
尽管在这个例子中没有必要使用映射(一个if (publicId.equals("-//FormatData//DTD RecipeML 0.5//EN")) return new InputSource("recipeml.dtd") else return null;语句就足够了),但我还是选择了使用映射,以防将来我想要扩展映射的数量。在另一种情况下,您可能会发现地图非常方便。例如,使用映射比在自定义实体解析器中使用一系列if语句更容易,它映射 XHTML 的严格、过渡和框架集正式公共标识符,并且还将其各种实体集映射到这些文档文件的本地副本。
覆盖的resolveEntity()方法使用publicId的参数在映射中定位相应的系统标识符——忽略systemId参数值,因为它从不引用recipeml.dtd的本地副本。找到映射后,会创建并返回一个InputSource对象。如果找不到映射,将返回null。
要在SAXDemo中安装这个自定义实体解析器,请在parse()方法调用之前指定xmlr.setEntityResolver(new LocalRecipeML());。重新编译源代码后,执行以下命令:
java SAXDemo gcs.xml
在这里,gcs.xml店铺列表 2-3 的正文。在结果输出中,您应该在调用startEntity()之前看到消息“obtaining cached recipeml.dtd”。
Tip
SAX API 包括一个org.xml.sax.ext.EntityResolver2接口,为解析实体提供了改进的支持。如果你更喜欢实现EntityResolver2而不是EntityResolver,那么用一个特性名为use-entity-resolver2的setFeature()调用代替setEntityResolver()调用来安装实体解析器(不要忘记 http://xml.org/sax/features/ 前缀)。
Exercises
以下练习旨在测试您对第二章内容的理解。
Define SAX. How do you obtain a SAX 2-based parser? What is the purpose of the XMLReader interface? How do you tell a SAX parser to perform validation? Identify the four kinds of SAX-oriented exceptions that can be thrown when working with SAX. What interface does a handler class implement to respond to content-oriented events? Identify the three other core interfaces that a handler class is likely to implement. Define ignorable whitespace. True or false: void error(SAXParseException exception) is called for all kinds of errors. What is the purpose of the DefaultHandler class? What is an entity? What is an entity resolver? Apache Tomcat is an open-source web server developed by the Apache Software Foundation. Tomcat stores usernames, passwords, and roles (for authentication purposes) in its tomcat-users.xml configuration file. Create a DumpUserInfo application that uses SAX to parse the user elements in the following tomcat-users.xml file and, for each user element, dump its username, password, and roles attribute values to standard output in a key = value format:
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="dbadmin"/>
<role rolename="manager"/>
<user username="JohnD" password="password1" roles="dbadmin,manager"/>
<user username="JillD" password="password2" roles="manager"/>
</tomcat-users>
Create a SAXSearch application that searches Exercise 1-21’s books.xml file for those book elements whose publisher child elements contain text that equals the application’s single command-line publisher name argument. Once there is a match, output the title element’s text followed by the book element’s isbn attribute value. For example, java SAXSearch Apress should output title = Beginning Groovy and Grails, isbn = 9781430210450, whereas java SAXSearch "Addison Wesley" should output title = Advanced C++, isbn = 0201548550 followed by title = Effective Java, isbn = 0201310058 on separate lines. Nothing should output when the command-line publisher name argument doesn’t match a publisher element’s text. Use Listing 2-1’s SAXDemo application to validate Exercise 1-22’s books.xml content against its DTD. Execute java SAXDemo books.xml -v to perform the validation.
摘要
SAX 是一个基于事件的 Java API,用于从头到尾顺序解析 XML 文档。当面向 SAX 的解析器遇到文档信息集中的一个项目时,它通过调用应用程序的一个处理程序中的一个方法,将这个项目作为一个事件提供给应用程序,应用程序之前已经向解析器注册了这个处理程序。然后,应用程序可以通过以某种方式处理 infoset 项来使用该事件。
SAX 有两个主要版本。Java 通过javax.xml.parsers包的抽象SAXParser和SAXParserFactory类实现 SAX 1,通过org.xml.sax包的XMLReader接口和org.xml.sax.helpers包的XMLReaderFactory类实现 SAX 2。org.xml.sax、org.xml.sax.ext和org.xml.sax.helpers包提供了各种类型来增强这两种 Java 实现。
XMLReader提供了几种配置解析器和解析文档内容的方法。其中一些方法获取并设置内容处理程序、DTD 处理程序、实体解析器和错误处理程序,这些由ContentHandler、DTDHandler、EntityResolver和ErrorHandler接口描述。在了解了XMLReader的方法和这些接口之后,您了解了非标准的LexicalHandler接口以及如何创建一个定制的实体解析器。
第三章介绍了 Java 解析/创建 XML 文档的 DOM API。
三、使用 DOM 解析和创建 XML 文档
SAX 可以解析 XML 文档,但不能创建它们。相比之下,DOM 可以解析和创建 XML 文档。本章向您介绍 DOM。
什么是 DOM?
文档对象模型(DOM)是一个 Java API,用于将 XML 文档解析成内存中的节点树,并从节点树创建 XML 文档。在 DOM 解析器创建一棵树后,应用程序使用 DOM API 导航并从树的节点中提取信息集项目。
DOM 相对于 SAX 有两大优势:
- DOM 允许随机访问文档的信息集项,而 SAX 只允许串行访问。
- DOM 还允许您创建 XML 文档,而您只能用 SAX 解析文档。
但是,SAX 优于 DOM,因为它可以解析任意大小的文档,而由 DOM 解析或创建的文档的大小受到用于存储文档的基于节点的树结构的可用内存量的限制。
Note
DOM 起源于 Netscape Navigator 3 和 Microsoft Internet Explorer 3 web 浏览器的对象模型。这些实现统称为 DOM Level 0。因为每个供应商的 DOM 实现彼此之间只有轻微的兼容性,W3C 随后负责 DOM 的开发以促进标准化,并且到目前为止已经发布了 DOM 级别 1、2 和 3(级别 4 正在开发中)。Java 8 通过其 DOM API 支持所有三个 DOM 级别。
节点树
DOM 将 XML 文档视为由几种节点组成的树。该树只有一个根节点,除了根节点之外,所有节点都有一个父节点。此外,每个节点都有一个子节点列表。当该列表为空时,子节点称为叶节点。
Note
DOM 允许不属于树结构的节点存在。例如,元素节点的属性节点不被视为元素节点的子节点。此外,可以创建节点,但不能将其插入树中;它们也可以从树中删除。
每个节点都有一个节点名,对于有名称(如元素或属性的前缀名)的节点来说是完整的名称,对于未命名的节点来说是#节点类型,其中节点类型是cdata-section、comment、document、document-fragment或text中的一个。节点也有本地名称(没有前缀的名称)、前缀和名称空间 URIs(尽管这些属性对于某些类型的节点可能是空的,比如 comments)。最后,节点有字符串值,恰好是文本节点、评论节点,以及类似的面向文本的节点的内容;属性的规范化值;其他的都是空的。
DOM 将节点分为 12 种类型,其中 7 种类型可以视为 DOM 树的一部分。所有这些类型描述如下:
- 属性节点:元素的属性之一。它有一个名称、一个本地名称、一个前缀、一个命名空间 URI 和一个规范化的字符串值。该值通过解析任何实体引用以及将空白序列转换为单个空白字符来规范化。属性节点有子节点,这些子节点是构成其值的文本和任何实体引用节点。属性节点不被视为其关联元素节点的子节点。
- CDATA 节节点:CDATA 节的内容。它的名字是
#cdata-section,它的值是 CDATA 部分的文本。 - 注释节点:文档注释。它的名字是
#comment,它的值是注释文本。注释节点有一个父节点,它是包含注释的节点。 - 文档节点:DOM 树的根。它的名字是
#document,它总是有一个单元素子节点,当文档有文档类型声明时,它也有一个文档类型子节点。此外,它还可以有额外的子节点,这些子节点描述出现在根元素的开始标记之前或之后的注释或处理指令。树中只能有一个文档节点。 - 文档片段节点:一个可选的根节点。它的名字是
#document-fragment,包含一个元素节点可以包含的任何东西(比如其他元素节点,甚至注释节点)。解析器从不创建这种类型的节点。但是,当应用程序提取 DOM 树的一部分并将其移动到其他地方时,它可以创建文档片段节点。文档片段节点允许您使用子树。 - 文档类型节点:文档类型声明。它的名称是由根元素的文档类型声明指定的名称。此外,它还有一个(可能为空的)公共标识符、一个必需的系统标识符、一个内部 DTD 子集(可能为空)、一个父节点(包含文档类型节点的文档节点)以及 DTD 声明的符号和一般实体的列表。它的值总是设置为 null。
- 元素节点:文档的元素。它有一个名称、一个本地名称、一个前缀(可能为空)和一个名称空间 URI,当元素不属于任何名称空间时,该名称空间为空。元素节点包含子节点,包括文本节点,甚至注释和处理指令节点。
- 实体节点:在文档的 DTD 中声明的已解析和未解析的实体。当一个解析器读取一个 DTD 时,它会将一个实体节点映射(由实体名索引)附加到文档类型节点上。实体节点有一个名称和一个系统标识符,如果在 DTD 中出现了一个公共标识符,它也可以有一个公共标识符。最后,当解析器读取实体时,实体节点会得到一个包含实体替换文本的只读子节点列表。
- 实体引用节点:对 DTD 声明的实体的引用。每个实体引用节点都有一个名称,当解析器没有用它们的值替换实体引用时,它就会包含在树中。解析器从不包含字符引用的实体引用节点(如
&或Σ),因为它们被各自的字符替换并包含在文本节点中。 - 符号节点:DTD 声明的符号。读取 DTD 的解析器将符号节点的映射(由符号名索引)附加到文档类型节点。每个符号节点都有一个名称和一个公共标识符或系统标识符,无论哪个标识符用于在 DTD 中声明符号。符号节点没有子节点。
- 处理指令节点:出现在单据中的处理指令。它有一个名称(指令的目标)、一个字符串值(指令的数据)和一个父节点(它的包含节点)。
- 文本节点:文档内容。它的名字是
#text,当必须创建一个中间节点(比如注释)时,它代表元素内容的一部分。通过字符引用在文档中表示的字符,如<和&,将被它们所表示的文字字符替换。当这些节点被写入文档时,这些字符必须被转义。
尽管这些节点类型存储了关于 XML 文档的大量信息,但是也有一些限制,比如不能在根元素之外暴露空白。此外,大多数 DTD 或模式信息,比如元素类型(<!ELEMENT...>)和属性类型(<xs:attribute...>,都不能通过 DOM 访问。
DOM Level 3 解决了 DOM 的各种限制。例如,尽管 DOM 没有为 XML 声明提供节点类型,但是 DOM Level 3 使得通过文档节点的属性访问 XML 声明的version、encoding和standalone属性值成为可能。
Note
非根节点从不孤立存在。例如,元素节点永远不会不属于文档或文档片段。即使当这些节点与主树断开连接时,它们仍然知道它们所属的文档或文档片段。
探索 DOM API
Java 通过javax.xml.parsers包的抽象DocumentBuilder和DocumentBuilderFactory类以及非抽象FactoryConfigurationError和ParserConfigurationException类实现 DOM。org.w3c.dom、org.w3c.dom.bootstrap、org.w3c.dom.events、org.w3c.dom.ls和org.w3c.dom.views包提供了各种类型来增强这种实现。
获取 DOM 解析器/文档构建器
DOM 解析器也称为文档构建器,因为它在解析和创建 XML 文档方面有双重作用。通过首先实例化DocumentBuilderFactory,调用它的一个newInstance()类方法,可以获得一个 DOM 解析器/文档构建器。例如,下面的代码片段调用了DocumentBuilderFactory的DocumentBuilderFactory newInstance()类方法:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
在幕后,newInstance()遵循一个有序的查找过程来识别要加载的DocumentBuilderFactory实现类。这个过程首先检查javax.xml.parsers.DocumentBuilderFactory系统属性,最后在找不到其他类时选择 Java 平台的默认DocumentBuilderFactory实现类。如果一个实现类不可用(也许由javax.xml.parsers.DocumentBuilderFactory系统属性标识的类不存在)或者不能被实例化,newInstance()抛出一个FactoryConfigurationError类的实例。否则,它实例化该类并返回其实例。
获得一个DocumentBuilderFactory实例后,可以调用各种配置方法来配置工厂。例如,您可以用一个true参数调用DocumentBuilderFactory的void setNamespaceAware(boolean awareness)方法,告诉工厂任何返回的文档构建器必须提供对 XML 名称空间的支持。您还可以使用true作为参数调用void setValidating(boolean validating)来根据文档的 dtd 验证文档,或者调用void setSchema(Schema schema)来根据由schema标识的javax.xml.validation.Schema实例验证文档。
Validation API
Schema是 Java 的验证 API 的一员,它将文档解析与验证解耦,使应用程序更容易利用支持其他模式语言的专用验证库(如 Relax NG——参见 http://en.wikipedia.org/wiki/RELAX_NG ),也更容易指定模式的位置。
验证 API 与javax.xml.validation包相关联,该包还包括SchemaFactory、SchemaFactoryLoader、TypeInfoProvider、Validator,而ValidatorHandler. Schema是中心类,代表一个语法的不可变内存表示。
DOM 通过DocumentBuilderFactory的void setSchema(Schema schema)和Schema getSchema()方法支持验证 API。类似地,SAX 1.0 支持通过 j avax.xml.parsers.SAXParserFactory的void setSchema(Schema schema)和Schema getSchema()方法进行验证。SAX 2.0 和 StAX(参见第四章)不支持验证 API。
以下代码片段演示了 DOM 上下文中的验证 API:
// Parse an XML document into a DOM tree.
DocumentBuilder parser =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = parser.parse(new File("instance.xml"));
// Create a SchemaFactory capable of understanding W3C XML Schema (WXS).
SchemaFactory factory =
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// Load a WXS schema, represented by a Schema instance.
Source schemaFile = new StreamSource(new File("mySchema.xsd"));
Schema schema = factory.newSchema(schemaFile);
// Create a Validator instance, which is used to validate an XML document.
Validator validator = schema.newValidator();
// Validate the DOM tree.
try
{
validator.validate(new DOMSource(document));
}
catch (SAXException saxe)
{
// XML document is invalid!
}
这个例子引用了 XSLT 类型,比如Source。我在第六章探索 XSLT。
配置完工厂后,调用它的DocumentBuilder newDocumentBuilder()方法返回一个支持配置的文档生成器,如下所示:
DocumentBuilder db = dbf.newDocumentBuilder();
如果不能返回一个文档构建器(也许工厂不能创建一个支持 XML 名称空间的文档构建器),这个方法抛出一个ParserConfigurationException实例。
解析和创建 XML 文档
假设您已经成功地获得了一个文档生成器,接下来会发生什么取决于您是想要解析还是创建一个 XML 文档。
DocumentBuilder提供了几个重载的parse()方法,用于将 XML 文档解析成节点树。这些方法在获取文档的方式上有所不同。例如,Document parse(String uri)解析由基于字符串的 URI 参数标识的文档。
Note
当null作为方法的第一个参数传递时,每个parse()方法抛出java.lang.IllegalArgumentException,当出现输入/输出错误时抛出java.io.IOException,当文档无法解析时抛出org.xml.sax.SAXException。最后一种异常类型表明DocumentBuilder的parse()方法依赖 SAX 来处理实际的解析工作。因为 DOM 解析器更多地参与构建节点树,所以通常被称为文档构建器。
DocumentBuilder还声明了创建文档树的抽象Document newDocument()方法。
返回的org.w3c.dom.Document对象通过像DocumentType getDoctype()这样的方法提供了对已解析文档的访问,这使得文档类型声明通过org.w3c.dom.DocumentType接口可用。从概念上讲,Document是文档节点树的根。它还声明了各种“create”和其他创建节点树的方法。例如,Element createElement(String tagName)创建一个名为tagName的元素,返回一个具有指定名称的新的org.w3c.dom.Element对象,但是其本地名称、前缀和名称空间 URI 设置为null。
Note
除了DocumentBuilder、DocumentBuilderFactory和其他几个类,DOM 是基于接口的,其中Document和DocumentType就是例子。在幕后,DOM 方法(如parse()方法)返回其类实现这些接口的对象。
Document和所有其他描述不同类型节点的org.w3c.dom接口都是org.w3c.dom.Node接口的子接口。因此,它们继承了Node的常量和方法。
Node声明 12 个常数,表示各种节点;ATTRIBUTE_NODE和ELEMENT_NODE就是例子。要识别给定的Node对象所代表的节点类型,调用Node的short getNodeType()方法,并将返回值与这些常量之一进行比较。
Note
使用getNodeType()和这些常量而不是使用instanceof和一个类名的基本原理是,DOM(对象模型,而不是 Java DOM API)被设计成语言独立的,像 AppleScript 这样的语言没有等同的instanceof。
Node声明了几个获取和设置公共节点属性的方法。这些方法包括String getNodeName()、String getLocalName()、String getNamespaceURI()、String getPrefix()、void setPrefix(String prefix)、String getNodeValue()和void setNodeValue(String nodeValue),它们允许您获取和(对于某些属性)设置节点的名称(如#text)、本地名称、名称空间 URI、前缀和规范化的字符串值。
Note
各种Node方法(比如setPrefix()和getNodeValue())在出错时抛出org.w3c.dom.DOMException类的一个实例。例如,当prefix参数包含非法字符、节点是只读的或者参数格式不正确时,setPrefix()会抛出这个异常。类似地,当getNodeValue()返回的字符多于实现平台上的DOMString(W3C 类型)变量所能容纳的字符时,getNodeValue()抛出DOMException。DOMException声明了一系列常量(如DOMSTRING_SIZE_ERR)对异常原因进行分类。
Node声明了几种导航节点树的方法。这里描述了它的三种导航方法:
- 当一个节点有子节点时,
boolean hasChildNodes()返回true。 Node getFirstChild()返回节点的第一个子节点。Node getLastChild()返回节点的最后一个子节点。
对于有多个子节点的节点,您会发现NodeList getChildNodes()方法非常方便。该方法返回一个org.w3c.dom.NodeList实例,其int getLength()方法返回列表中的节点数,其Node item(int index)方法返回列表中第index个位置的节点(或者当index的值无效时返回null——小于零或者大于等于getLength()的值)。
Node声明了通过插入、移除、替换和追加子节点来修改树的四种方法:
Node insertBefore (Node newChild, Node refChild)在refChild指定的现有节点前插入newChild并返回newChild。Node removeChild (Node oldChild)从树中删除由oldChild标识的子节点,并返回oldChild。Node replaceChild (Node newChild, Node oldChild)用newChild替换oldChild并返回oldChild。Node appendChild (Node newChild)将newChild添加到当前节点子节点的末尾,并返回newChild。
最后,Node声明了几个实用方法,包括Node cloneNode(boolean deep)(创建并返回当前节点的副本,当true传递给deep时递归克隆其子树),以及void normalize()(从给定节点开始向下遍历树,合并所有相邻的文本节点,删除那些空的文本节点)。
Tip
要获得元素节点的属性,首先调用Node的NamedNodeMap getAttributes()方法。当节点表示一个元素时,该方法返回一个org.w3c.dom.NamedNodeMap实现;否则,它返回null。除了通过名字声明访问这些节点的方法(比如Node getNamedItem (String name))之外,NamedNodeMap还通过index声明返回所有属性节点的int getLength()和Node item(int index)方法。然后,通过调用诸如getNodeName()这样的方法来获得Node的名称。
除了继承Node的常量和方法,Document还声明了自己的方法。例如,您可以调用Document的String getXmlEncoding()、boolean getXmlStandalone()和String getXmlVersion()方法来分别返回 XML 声明的encoding、standalone和version属性值。
Document声明了三种定位一个或多个元素的方法:
Element getElementById(String elementId)返回具有与elementId指定的值相匹配的id属性(如<img id=...>)的元素。NodeList getElementsByTagName(String tagname)返回与指定的tagName匹配的文档元素的节点列表(按文档顺序)。- 除了只将那些匹配
localName和namespaceURI值的元素添加到节点列表之外,NodeList getElementsByTagNameNS(String namespaceURI,String localName)等同于第二种方法。传递"*"到namespaceURI匹配所有名称空间;传递"*"到localName来匹配所有本地名称。
返回的元素节点和列表中的每个元素节点都实现了Element接口。该接口声明了返回树中派生元素的节点列表、与元素相关的属性等的方法。例如,String getAttribute(String name)返回由name标识的属性的值,而Attr getAttributeNode(String name)通过名称返回属性节点。返回的节点是org.w3c.dom.Attr接口的一个实现。
演示 DOM API
现在,您已经有了足够的信息来探索解析和创建 XML 文档的应用程序。清单 3-1 将源代码呈现给基于 DOM 的解析应用程序。
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class DOMDemo
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java DOMDemo xmlfile");
return;
}
try
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(args[0]);
System.out.printf("Version = %s%n", doc.getXmlVersion());
System.out.printf("Encoding = %s%n", doc.getXmlEncoding());
System.out.printf("Standalone = %b%n%n", doc.getXmlStandalone());
if (doc.hasChildNodes())
{
NodeList nl = doc.getChildNodes();
for (int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
dump((Element) node);
}
}
}
catch (IOException ioe)
{
System.err.println("IOE: " + ioe);
}
catch (SAXException saxe)
{
System.err.println("SAXE: " + saxe);
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (ParserConfigurationException pce)
{
System.err.println("PCE: " + pce);
}
}
static void dump(Element e)
{
System.out.printf("Element: %s, %s, %s, %s%n", e.getNodeName(),
e.getLocalName(), e.getPrefix(),
e.getNamespaceURI());
NamedNodeMap nnm = e.getAttributes();
if (nnm != null)
for (int i = 0; i < nnm.getLength(); i++)
{
Node node = nnm.item(i);
Attr attr = e.getAttributeNode(node.getNodeName());
System.out.printf(" Attribute %s = %s%n", attr.getName(), attr.getValue());
}
NodeList nl = e.getChildNodes();
for (int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node instanceof Element)
dump((Element) node);
}
}
}
Listing 3-1.
DOMDemo
(Version 1)
DOMDemo的main()方法首先验证已经指定了一个命令行参数(XML 文档的名称)。然后,它创建一个文档构建器工厂,通知工厂它需要一个知道名称空间的文档构建器,并让工厂返回这个文档构建器。
继续,main()将文档解析成节点树;输出 XML 声明的版本号、编码和独立属性值。并递归地转储所有元素节点(从根节点开始)及其属性值。
注意清单的一部分使用了getNodeType(),另一部分使用了instanceof。getNodeType()方法调用是不必要的(它只是为了演示才出现的),因为可以用instanceof来代替。然而,在dump()方法调用中从Node类型到Element类型的转换是必要的。
编译清单 3-1 如下:
javac DOMDemo.java
运行生成的应用程序来转储列表 1-3 的文章 XML 内容,如下所示:
java DOMDemo article.xml
您应该观察到以下输出:
Version = 1.0
Encoding = null
Standalone = false
Element: article, article, null, null
Attribute lang = en
Attribute title = The Rebirth of JavaFX
Element: abstract, abstract, null, null
Element: code-inline, code-inline, null, null
Element: body, body, null, null
每一个带Element前缀的行表示节点名,后面是本地名,后面是名称空间前缀,再后面是名称空间 URI。节点和本地名称是相同的,因为没有使用名称空间。出于同样的原因,名称空间前缀和名称空间 URI 都是null。
继续,执行以下命令转储列表 1-5 的配方内容:
java DOMDemo recipe.xml
这一次,您会看到以下输出,其中包括名称空间信息:
Version = 1.0
Encoding = null
Standalone = false
Element: h:html, html, h, http://www.w3.org/1999/xhtml
Attribute xmlns:h = http://www.w3.org/1999/xhtml
Attribute xmlns:r = http://www.javajeff.ca/
Element: h:head, head, h, http://www.w3.org/1999/xhtml
Element: h:title, title, h, http://www.w3.org/1999/xhtml
Element: h:body, body, h, http://www.w3.org/1999/xhtml
Element: r:recipe, recipe, r, http://www.javajeff.ca/
Element: r:title, title, r, http://www.javajeff.ca/
Element: r:ingredients, ingredients, r, http://www.javajeff.ca/
Element: h:ul, ul, h, http://www.w3.org/1999/xhtml
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
Attribute qty = 2
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
Attribute qty = 2
Element: h:p, p, h, http://www.w3.org/1999/xhtml
Element: r:instructions, instructions, r, http://www.javajeff.ca/
清单 3-2 展示了另一个版本的DOMDemo应用程序,它简要演示了文档树的创建。
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
public class DOMDemo
{
public static void main(String[] args)
{
try
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
// Create the root element.
Element root = doc.createElement("movie");
doc.appendChild(root);
// Create name child element and add it to the root.
Element name = doc.createElement("name");
root.appendChild(name);
// Add a text element to the name element.
Text text = doc.createTextNode("Le Fabuleux Destin d'Amélie " + "Poulain"); name.appendChild(text);
// Create language child element and add it to the root.
Element language = doc.createElement("language");
root.appendChild(language);
// Add a text element to the language element.
text = doc.createTextNode("français");
language.appendChild(text);
System.out.printf("Version = %s%n", doc.getXmlVersion());
System.out.printf("Encoding = %s%n", doc.getXmlEncoding());
System.out.printf("Standalone = %b%n%n", doc.getXmlStandalone());
NodeList nl = doc.getChildNodes();
for (int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
dump((Element) node);
}
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (ParserConfigurationException pce)
{
System.err.println("PCE: " + pce);
}
}
static void dump(Element e)
{
System.out.printf("Element: %s, %s, %s, %s%n", e.getNodeName(), e.getLocalName(), e.getPrefix(), e.getNamespaceURI());
NodeList nl = e.getChildNodes();
for (int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node instanceof Element)
dump((Element) node);
else
if (node instanceof Text)
System.out.printf("Text: %s%n", ((Text) node).getWholeText());
}
}
}
Listing 3-2.
DOMDemo (Version 2)
DOMDemo创建列表 1-2 的电影文档。它使用Document的createElement()方法创建根movie元素和movie的name和language子元素。它还使用Document的Text createTextNode(String data)方法创建附加到name和language节点的文本节点。注意对Node的appendChild()方法的调用,将子节点(如name)追加到父节点(如movie)。
在创建这个树之后,DOMDemo输出树的元素节点和其他信息。该输出如下所示:
Version = 1.0
Encoding = null
Standalone = false
Element: movie, null, null, null
Element: name, null, null, null
Text: Le Fabuleux Destin d'Amélie Poulain
Element: language, null, null, null
Text: français
输出有一个问题:XML 声明的encoding属性没有被设置为ISO-8859-1。您不能通过 DOM API 完成这项任务。相反,您需要使用 XSLT API。在第六章的中探索 XSLT 时,您将学习如何设置encoding属性,还将学习如何将这个树输出到 XML 文档文件中。
Exercises
以下练习旨在测试您对第三章内容的理解。
Define DOM. True or false: Java 8 supports DOM Levels 1 and 2 only. Identify the 12 types of DOM nodes. How do you obtain a document builder? How do you use a document builder to parse an XML document? True or false: Document and all other org.w3c.dom interfaces that describe different kinds of nodes are subinterfaces of the Node interface. How do you use a document builder to create a new XML document? How would you determine if a node has children? True or false: When creating a new XML document, you can use the DOM API to specify the XML declaration’s encoding attribute. Exercise 2-12 asked you to create a DumpUserInfo application that uses SAX to parse the user elements in an example tomcat-users.xml file and, for each user element, dump its username, password, and roles attribute values to standard output in a key = value format. Recreate this application to use DOM. Create a DOMSearch application that’s the equivalent of Exercise 2-13’s SAXSearch application. Create a DOMValidate application based on Listing 3-1’s DOMDemo source code (plus one new line that enables validation) to validate Exercise 1-22’s books.xml content against its DTD. Execute java DOMValidate books.xml to perform the validation. You should observe no errors. However, if you attempt to validate books.xml without the DTD, you should observe errors.
摘要
文档对象模型(DOM)是一个 Java API,用于将 XML 文档解析成内存中的节点树,并从节点树创建 XML 文档。在 DOM 解析器创建一棵树后,应用程序使用 DOM API 导航并从树的节点中提取信息集项目。
DOM 将 XML 文档视为由几种节点组成的树:属性、CDATA 节、注释、文档、文档片段、文档类型、元素、实体、实体引用、符号、处理指令和文本。
DOM 解析器也称为文档构建器,因为它在解析和创建 XML 文档方面有双重作用。通过首先实例化DocumentBuilderFactory,您获得了一个文档构建器。然后调用工厂的newDocumentBuilder()方法返回文档构建器。
调用文档构建器的parse()方法之一将 XML 文档解析成节点树。调用以“create”为前缀的各种 document builder 方法(以及一些额外的方法)来创建一个 XML 文档。
第四章介绍了用于解析/创建 XML 文档的 StAX API。
四、用 StAX 解析和创建 XML 文档
Java 还包括用于解析和创建 XML 文档的 StAX API。本章向您介绍 StAX。
StAX 是什么?
Streaming API for XML (StAX)是一个 Java API,用于从开始到结束顺序解析 XML 文档,也用于创建 XML 文档。StAX 是由 Java 6 引入的,作为 SAX 和 DOM 的替代,位于这两个“对立面”的中间。
StAX vs. SAX and DOM
因为 Java 已经支持用于文档解析的 SAX 和 DOM 以及用于文档创建的 DOM,所以您可能想知道为什么还需要另一个 XML API。以下几点证明了 StAX 在 core Java 中的存在:
- StAX(像 SAX 一样)可以用来解析任意大小的文档。相比之下,DOM 解析的文档的最大大小受到可用内存的限制,这使得 DOM 不适合内存有限的移动设备。
- StAX(像 DOM 一样)可以用来创建文档。DOM 可以创建最大大小受可用内存限制的文档,与之相反,StAX 可以创建任意大小的文档。SAX 不能用来创建文档。
- StAX(像 SAX 一样)使得应用程序几乎可以立即使用 infoset 项。相比之下,DOM 在构建完节点树之后才能使用这些项目。
- StAX(像 DOM 一样)采用 pull 模型,在这种模型中,应用程序告诉解析器何时准备好接收下一个 infoset 项。这个模型基于迭代器设计模式(参见
http://sourcemaking.com/design_patterns/iterator),这使得应用程序更容易编写和调试。相比之下,SAX 采用推送模型,在这种模型中,解析器通过事件将信息集项目传递给应用程序,而不管应用程序是否准备好接收它们。这个模型基于观察者设计模式(参见http://sourcemaking.com/design_patterns/observer),这导致应用程序通常更难编写和调试。
总之,StAX 可以解析或创建任意大小的文档,使应用程序几乎可以立即使用 infoset 项,并使用 pull 模型来管理应用程序。SAX 和 DOM 都没有提供所有这些优势。
探索 StAX
Java 通过存储在javax.xml.stream、javax.xml.stream.events和javax.xml.stream.util包中的类型实现 StAX。本节将向您介绍前两个包中的各种类型,同时展示如何使用 StAX 解析和创建 XML 文档。
Stream-Based vs. Event-Based Readers and Writers
StAX 解析器被称为文档阅读器,StAX 文档创建者被称为文档编写器。StAX 将文档阅读器和文档编写器分为基于流的和基于事件的。
基于流的读取器通过游标(信息集项指针)从输入流中提取下一个信息集项。类似地,基于流的编写器将下一个 infoset 项写入光标位置处的输出流。光标一次只能指向一个项目,并且总是向前移动,通常移动一个信息集项目。
在为 Java ME 等内存受限的环境编写代码时,基于流的读取器和写入器是合适的,因为您可以使用它们来创建更小、更高效的代码。它们还为低级别的库提供了更好的性能,在低级别的库中,性能是很重要的。
基于事件的读取器通过获取事件从输入流中提取下一个信息集项。类似地,基于事件的编写器通过向输出流添加事件,将下一个 infoset 项写入流中。与基于流的读取器和编写器相比,基于事件的读取器和编写器没有游标的概念。
基于事件的读取器和编写器适用于创建 XML 处理管道(转换前一个组件的输入并将转换后的输出传递给序列中的下一个组件的组件序列)、修改事件序列等。
解析 XML 文档
文档阅读器是通过调用在javax.xml.stream.XMLInputFactory类中声明的各种create方法获得的。这些创建方法分为两类:创建基于流的读取器的方法和创建基于事件的读取器的方法。
在获得基于流或基于事件的读取器之前,您需要通过调用一个newFactory()类方法来获得工厂的实例,比如XMLInputFactory newFactory():
XMLInputFactory xmlif = XMLInputFactory.newFactory();
Note
您也可以调用XMLInputFactory newInstance()类方法,但是您可能不希望这样做,因为为了保持 API 的一致性,它的同名但参数化的伴随方法已经被弃用,而且newInstance()也可能被弃用。
newFactory()方法遵循一个有序的查找过程来定位XMLInputFactory实现类。这个过程首先检查javax.xml.stream.XMLInputFactory系统属性,最后选择 Java 平台的默认XMLInputFactory实现类的名称。如果这个过程找不到类名,或者如果这个类不能被加载(或实例化),这个方法抛出一个javax.xml.stream.FactoryConfigurationError类的实例。
创建工厂后,调用XMLInputFactory的void setProperty(String name, Object value)方法,根据需要设置各种特性和属性。例如,您可以执行xmlif.setProperty(XMLInputFactory.IS_VALIDATING, true); ( true通过自动装箱作为java.lang.Boolean对象传递——参见 http://docs.oracle.com/javase/tutorial/java/data/autoboxing.html )来请求一个验证 DTD 的基于流的阅读器。然而,默认的 StAX 工厂实现抛出了java.lang.IllegalArgumentException,因为它不支持 DTD 验证。类似地,您可以执行xmlif.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);来请求一个支持名称空间感知的基于事件的读取器。
用基于流的阅读器解析文档
基于流的阅读器是通过调用XMLInputFactory的createXMLStreamReader()方法之一创建的,比如XMLStreamReader createXMLStreamReader(Reader reader)。当不能创建基于流的读取器时,这些方法抛出javax.xml.stream.XMLStreamException。
下面的代码片段创建了一个基于流的阅读器,它的源代码是一个名为recipe.xml的文件:
Reader reader = new FileReader("recipe.xml");
XMLStreamReader xmlsr = xmlif.createXMLStreamReader(reader);
底层的javax.xml.stream.XMLStreamReader接口提供了用 StAX 读取 XML 数据的最有效的方法。当有下一个信息集项目要获取时,该接口的boolean hasNext()方法返回true;否则返回false。int next()方法将光标向前移动一个信息集项目,并返回一个标识该项目的类型的整数代码。
不是将next()的返回值与一个整数值进行比较,而是将这个值与一个javax.xml.stream.XMLStreamConstants信息集常量进行比较,比如START_ELEMENT或DTD——XMLStreamReader扩展了XMLStreamConstants接口。
Note
您还可以通过调用XMLStreamReader的int getEventType()方法来获取光标所指向的信息集项的类型。在这个方法的名称中指定“Event”是很不幸的,因为它混淆了基于流的读取器和基于事件的读取器。
下面的代码片段使用hasNext()和next()方法来编写一个解析循环,该循环检测每个元素的开始和结束:
while (xmlsr.hasNext())
{
switch (xmlsr.next())
{
case XMLStreamReader.START_ELEMENT: // Do something at element start. break;
case XMLStreamReader.END_ELEMENT : // Do something at element end.
}
}
XMLStreamReader还声明了提取信息集信息的各种方法。例如,next()返回XMLStreamReader.START_ELEMENT或XMLStreamReader.END_ELEMENT时,QName getName()返回光标位置元素的限定名(作为javax.xml.namespace.QName实例)。
Note
将限定名描述为名称空间 URI、本地部分和前缀部分的组合。在实例化这个不可变的类(通过一个像QName(String namespaceURI, String localPart, String prefix)这样的构造函数)之后,您可以通过调用QName的String getNamespaceURI()、String getLocalPart()和String getPrefix()方法来返回这些组件。
清单 4-1 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于流的阅读器报告 XML 文档的开始和结束元素。
import java.io.FileNotFoundException;
import java.io.FileReader;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
class StAXDemo
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java StAXDemo xmlfile");
return;
}
try
{
XMLInputFactory xmlif = XMLInputFactory.newFactory();
XMLStreamReader xmlsr;
xmlsr = xmlif.createXMLStreamReader(new FileReader(args[0]));
while (xmlsr.hasNext())
{
switch (xmlsr.next())
{
case XMLStreamReader.START_ELEMENT:
System.out.println("START_ELEMENT");
System.out.println(" Qname = " + xmlsr.getName());
break;
case XMLStreamReader.END_ELEMENT:
System.out.println("END_ELEMENT");
System.out.println(" Qname = " + xmlsr.getName());
}
}
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (FileNotFoundException fnfe)
{
System.err.println("FNFE: " + fnfe);
}
catch (XMLStreamException xmlse)
{
System.err.println("XMLSE: " + xmlse);
}
}
}
Listing 4-1.StAXDemo (version 1)
在验证了命令行参数的数量之后,清单 4-1 的main()方法创建一个工厂,使用该工厂创建一个基于流的读取器,该读取器从由单独的命令行参数标识的文件中获取 XML 数据,然后进入一个解析循环。每当next()返回XMLStreamReader.START_ELEMENT或XMLStreamReader.END_ELEMENT时,就会调用XMLStreamReader的getName()方法来返回元素的限定名。
编译清单 4-1 如下:
javac StAXDemo.java
运行生成的应用程序来转储列表 1-2 的电影 XML 内容,如下所示:
java StAXDemo movie.xml
您应该观察到以下输出:
START_ELEMENT
Qname = movie
START_ELEMENT
Qname = name
END_ELEMENT
Qname = name
START_ELEMENT
Qname = language
END_ELEMENT
Qname = language
END_ELEMENT
Qname = movie
Note
XMLStreamReader声明了一个void close()方法,如果您的应用程序被设计为长时间运行,您将希望调用该方法来释放与这个基于流的读取器相关联的任何资源。调用此方法不会关闭基础输入源。
用基于事件的阅读器解析文档
基于事件的阅读器是通过调用XMLInputFactory的createXMLEventReader()方法之一创建的,比如XMLEventReader createXMLEventReader(Reader reader)。当无法创建基于事件的读取器时,这些方法抛出XMLStreamException。
下面的代码片段创建了一个基于事件的阅读器,它的源代码是一个名为recipe.xml的文件:
Reader reader = new FileReader("recipe.xml");
XMLEventReader xmler = xmlif.createXMLEventReader(reader);
高级的javax.xml.stream.XMLEventReader接口提供了一种效率稍低但更面向对象的方式来用 StAX 读取 XML 数据。当有事件要获取时,该接口的boolean hasNext()方法返回true;否则返回false。XMLEvent nextEvent()方法将下一个事件作为一个对象返回,该对象的类实现了javax.xml.stream.events.XMLEvent接口的子接口。
Note
XMLEvent是处理标记事件的基本接口。它声明适用于所有子接口的方法;例如,Location getLocation()(返回一个javax.xml.stream.Location对象,其int getCharacterOffset()和其他方法返回事件的位置信息)和int getEventType()(将事件类型作为XMLStreamConstants infoset 常量返回,如START_ELEMENT和PROCESSING_INSTRUCTION — XMLEvent扩展XMLStreamConstants)。XMLEvent由其他javax.xml.stream.events接口子类型化,这些接口根据返回 infoset 项目特定信息的方法(例如Attribute的QName getName()和String getValue()方法)描述不同种类的事件(例如Attribute)。
下面的代码片段使用hasNext()和nextEvent()方法编写一个解析循环,该循环检测元素的开始和结束:
while (xmler.hasNext())
{
switch (xmler.nextEvent().getEventType())
{
case XMLEvent.START_ELEMENT: // Do something at element start.
break;
case XMLEvent.END_ELEMENT : // Do something at element end.
}
}
清单 4-2 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于事件的阅读器报告 XML 文档的开始和结束元素。
import java.io.FileNotFoundException;
import java.io.FileReader;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
class StAXDemo
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java StAXDemo xmlfile");
return;
}
try
{
XMLInputFactory xmlif = XMLInputFactory.newFactory();
XMLEventReader xmler;
xmler = xmlif.createXMLEventReader(new FileReader(args[0]));
while (xmler.hasNext())
{
XMLEvent xmle = xmler.nextEvent();
switch (xmle.getEventType())
{
case XMLEvent.START_ELEMENT:
System.out.println("START_ELEMENT");
System.out.println(" Qname = " +
((StartElement) xmle).getName());
break;
case XMLEvent.END_ELEMENT:
System.out.println("END_ELEMENT");
System.out.println(" Qname = " +
((EndElement) xmle).getName());
}
}
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (FileNotFoundException fnfe)
{
System.err.println("FNFE: " + fnfe);
}
catch (XMLStreamException xmlse)
{
System.err.println("XMLSE: " + xmlse);
}
}
}
Listing 4-2.StAXDemo (version 2)
在验证了命令行参数的数量之后,清单 4-2 的main()方法创建一个工厂,使用该工厂创建一个基于事件的读取器,该读取器从由单独的命令行参数标识的文件中获取 XML 数据,然后进入一个解析循环。每当nextEvent()返回XMLEvent.START_ELEMENT或XMLEvent.END_ELEMENT时,就会调用StartElement或EndElement的getName()方法来返回元素的限定名。
编译清单 4-2 后,运行生成的应用程序来转储清单 1-3 的文章 XML 内容,如下所示:
java StAXDemo article.xml
您应该观察到以下输出:
START_ELEMENT
Qname = article
START_ELEMENT
Qname = abstract
START_ELEMENT
Qname = code-inline
END_ELEMENT
Qname = code-inline
END_ELEMENT
Qname = abstract
START_ELEMENT
Qname = body
END_ELEMENT
Qname = body
END_ELEMENT
Qname = article
Note
您还可以通过调用XMLInputFactory的createFilteredReader()方法之一,如XMLEventReader createFilteredReader(XMLEventReader reader, EventFilter filter),创建一个基于过滤事件的阅读器来接受或拒绝各种事件。javax.xml.stream.EventFilter接口声明了一个boolean accept(XMLEvent event)方法,当指定的事件是事件序列的一部分时,该方法返回true;否则返回false。
创建 XML 文档
文档编写器是通过调用在javax.xml.stream.XMLOutputFactory类中声明的各种create方法获得的。这些创建方法分为两类:创建基于流的编写器的方法和创建基于事件的编写器的方法。
在获得基于流或基于事件的编写器之前,您需要通过调用一个newFactory()类方法来获得工厂的实例,比如XMLOutputFactory newFactory():
XMLOutputFactory xmlof = XMLOutputFactory.newFactory();
Note
您也可以调用XMLOutputFactory newInstance()类方法,但是您可能不希望这样做,因为为了保持 API 的一致性,它的同名但参数化的伴随方法已经被弃用,而且newInstance()也可能被弃用。
newFactory()方法遵循一个有序的查找过程来定位XMLOutputFactory实现类。这个过程首先检查javax.xml.stream.XMLOutputFactory系统属性,最后选择 Java 平台的默认XMLOutputFactory实现类的名称。如果这个过程找不到类名,或者如果这个类不能被加载(或实例化),这个方法抛出一个FactoryConfigurationError类的实例。
创建工厂后,调用XMLOutputFactory的void setProperty(String name, Object value)方法,根据需要设置各种特性和属性。目前所有作家支持的唯一财产是XMLOutputFactory.IS_REPAIRING_NAMESPACES。启用时(通过将true或Boolean对象,如Boolean.TRUE传递给value),文档编写器负责所有名称空间绑定和声明,只需应用程序提供最少的帮助。就名称空间而言,输出总是格式良好的。但是,启用该属性会给编写 XML 的工作增加一些开销。
使用基于流的编写器创建文档
基于流的编写器是通过调用XMLOutputFactory的createXMLStreamWriter()方法之一创建的,比如XMLStreamWriter createXMLStreamWriter(Writer writer)。当无法创建基于流的编写器时,这些方法抛出XMLStreamException。
下面的代码片段创建了一个基于流的编写器,它的目标是一个名为recipe.xml的文件:
Writer writer = new FileWriter("recipe.xml");
XMLStreamWriter xmlsw = xmlof.createXMLStreamWriter(writer);
底层的XMLStreamWriter接口声明了几个将 infoset 项写到目的地的方法。下面的列表描述了其中的一些方法:
- 关闭这个基于流的编写器并释放所有相关的资源。基础编写器未关闭。
- 将任何缓存的数据写入底层编写器。
void setPrefix(String prefix, String uri)标识了uri值绑定到的名称空间prefix。这个prefix由writeStartElement()、writeAttribute()和writeEmptyElement()方法的变体使用,这些方法接受名称空间参数而不接受前缀。同样,它保持有效,直到对应于最后一次writeStartElement()调用的writeEndElement()调用。这个方法不产生任何输出。void writeAttribute(String localName, String value)将由localName标识并具有指定的value的属性写入底层编写器。不包括名称空间前缀。该方法对&、<、>和"字符进行转义。void writeCharacters(String text)将text的字符写入底层编写器。这个方法对&、<和>字符进行转义。- 关闭所有开始标签,并将相应的结束标签写入底层编写器。
void endElement()将结束标记写入底层编写器,依靠基于流的编写器的内部状态来确定标记的前缀和本地名称。- 将命名空间写入底层编写器。必须调用此方法以确保写入由
setPrefix()指定并在此方法调用中复制的名称空间;否则,从名称空间的角度来看,生成的文档将不是格式良好的。 - 将 XML 声明写入底层编写器。
void writeStartElement(String namespaceURI, String localName)用传递给namespaceURI和localName的参数写一个开始标签给底层编写器。
清单 4-3 将源代码呈现给一个StAXDemo应用程序,该应用程序通过一个基于流的编写器创建一个包含清单 1-5 的信息集项目的recipe.xml文件。
import java.io.FileWriter;
import java.io.IOException;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
class StAXDemo
{
public static void main(String[] args)
{
try
{
XMLOutputFactory xmlof = XMLOutputFactory.newFactory();
XMLStreamWriter xmlsw;
xmlsw = xmlof.createXMLStreamWriter(new FileWriter("recipe.xml"));
xmlsw.writeStartDocument();
xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "html");
xmlsw.writeNamespace("h", "http://www.w3.org/1999/xhtml");
xmlsw.writeNamespace("r", "http://www.javajeff.ca/");
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "head");
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "title");
xmlsw.writeCharacters("Recipe");
xmlsw.writeEndElement();
xmlsw.writeEndElement();
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "body");
xmlsw.setPrefix("r", "http://www.javajeff.ca/");
xmlsw.writeStartElement("http://www.javajeff.ca/", "recipe");
xmlsw.writeStartElement("http://www.javajeff.ca/", "title");
xmlsw.writeCharacters("Grilled Cheese Sandwich");
xmlsw.writeEndElement();
xmlsw.writeStartElement("http://www.javajeff.ca/",
"ingredients");
xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "ul");
xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "li");
xmlsw.setPrefix("r", "http://www.javajeff.ca/");
xmlsw.writeStartElement("http://www.javajeff.ca/", "ingredient");
xmlsw.writeAttribute("qty", "2");
xmlsw.writeCharacters("bread slice");
xmlsw.writeEndElement();
xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
xmlsw.writeEndElement();
xmlsw.writeEndElement();
xmlsw.setPrefix("r", "http://www.javajeff.ca/");
xmlsw.writeEndElement();
xmlsw.writeEndDocument();
xmlsw.flush();
xmlsw.close();
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (IOException ioe)
{
System.err.println("IOE: " + ioe);
}
catch (XMLStreamException xmlse)
{
System.err.println("XMLSE: " + xmlse);
}
}
}
Listing 4-3.
StAXDemo (version 3)
尽管清单 4-3 很容易理解,但是您可能会对setPrefix()和writeStartElement()方法调用中名称空间 URIs 的重复感到困惑。例如,您可能想知道xmlsw.setPrefix("h", " http://www.w3.org/1999/xhtml ");中的重复 URIs 及其xmlsw.writeStartElement(" http://www.w3.org/1999/xhtml ", "html");继任者。
setPrefix()方法调用创建名称空间前缀(值)和 URI(键)之间的映射,而不生成任何输出。writeStartElement()方法调用指定了 URI 键,该方法使用该键来访问前缀值,然后在将该标签写入底层编写器之前,将该前缀值(用冒号字符)添加到html开始标签的名称之前。
编译清单 4-3 并运行生成的应用程序。您应该在当前目录中发现一个recipe.xml文件。
使用基于事件的编写器创建文档
基于事件的编写器是通过调用XMLOutputFactory的createXMLEventWriter()方法之一创建的,比如XMLEventWriter createXMLEventWriter(Writer writer)。当无法创建基于事件的编写器时,这些方法抛出XMLStreamException。
以下代码片段创建了一个基于事件的编写器,其目标是一个名为recipe.xml的文件:
Writer writer = new FileWriter("recipe.xml");
XMLEventWriter xmlew = xmlof.createXMLEventWriter(writer);
高级的XMLEventWriter接口声明了void add(XMLEvent event)方法,用于将描述信息集项目的事件添加到底层编写器实现的输出流中。传递给event的每个参数都是一个类的实例,该类实现了XMLEvent的子接口(比如Attribute和StartElement)。
Tip
XMLEventWriter还声明了一个void add(XMLEventReader reader)方法,您可以用它将一个XMLEventReader实例链接到一个XMLEventWriter实例。
为了省去你实现这些接口的麻烦,StAX 提供了javax.xml.stream.EventFactory。这个实用程序类声明了创建XMLEvent子接口实现的各种工厂方法。例如,Comment createComment(String text)返回一个对象,该对象的类实现了XMLEvent的javax.xml.stream.events.Comment子接口。
因为这些工厂方法被声明为abstract,所以您必须首先获得一个EventFactory类的实例。您可以通过调用EventFactory的XMLEventFactory newFactory()类方法轻松完成这项任务,如下所示:
XMLEventFactory xmlef = XMLEventFactory.newFactory();
然后,您可以获得一个XMLEvent子接口实现,如下所示:
XMLEvent comment = xmlef.createComment("ToDo");
清单 4-4 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于事件的编写器创建一个recipe.xml文件,其中包含清单 1-5 的许多信息集项目。
import java.io.FileWriter;
import java.io.IOException;
import java.util.Iterator;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.XMLEvent;
class StAXDemo
{
public static void main(String[] args)
{
try
{
XMLOutputFactory xmlof = XMLOutputFactory.newFactory();
XMLEventWriter xmlew;
xmlew = xmlof.createXMLEventWriter(new FileWriter("recipe.xml"));
final XMLEventFactory xmlef = XMLEventFactory.newFactory();
XMLEvent event = xmlef.createStartDocument();
xmlew.add(event);
Iterator<Namespace> nsIter;
nsIter = new Iterator<Namespace>()
{
int index = 0;
Namespace[] ns;
{
ns = new Namespace[2];
ns[0] = xmlef.
createNamespace("h",
"http://www.w3.org/1999/xhtml");
ns[1] = xmlef.
createNamespace("r",
"http://www.javajeff.ca/");
}
@Override
public boolean hasNext()
{
return index != 2;
}
@Override
public Namespace next()
{
return ns[index++];
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"html", null, nsIter);
xmlew.add(event);
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"head");
xmlew.add(event);
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"title");
xmlew.add(event);
event = xmlef.createCharacters("Recipe");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml",
"title");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml",
"head");
xmlew.add(event);
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"body");
xmlew.add(event);
event = xmlef.createStartElement("r",
"http://www.javajeff.ca/",
"recipe");
xmlew.add(event);
event = xmlef.createStartElement("r",
"http://www.javajeff.ca/",
"title");
xmlew.add(event);
event = xmlef.createCharacters("Grilled Cheese Sandwich");
xmlew.add(event);
event = xmlef.createEndElement("r",
"http://www.javajeff.ca/",
"title");
xmlew.add(event);
event = xmlef.createStartElement("r",
"http://www.javajeff.ca/",
"ingredients");
xmlew.add(event);
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"ul");
xmlew.add(event);
event = xmlef.createStartElement("h",
"http://www.w3.org/1999/xhtml",
"li");
xmlew.add(event);
Iterator<Attribute> attrIter;
attrIter = new Iterator<Attribute>()
{
int index = 0;
Attribute[] attrs;
{
attrs = new Attribute[1];
attrs[0] = xmlef.createAttribute("qty", "2");
}
@Override
public boolean hasNext()
{
return index != 1;
}
@Override
public Attribute next()
{
return attrs[index++];
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
};
event = xmlef.createStartElement("r",
"http://www.javajeff.ca/",
"ingredient", attrIter, null);
xmlew.add(event);
event = xmlef.createCharacters("bread slice");
xmlew.add(event);
event = xmlef.createEndElement("r",
"http://www.javajeff.ca/",
"ingredient");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml
",
"li");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml",
"ul");
xmlew.add(event);
event = xmlef.createEndElement("r",
"http://www.javajeff.ca/",
"ingredients");
xmlew.add(event);
event = xmlef.createEndElement("r",
"http://www.javajeff.ca/",
"recipe");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml",
"body");
xmlew.add(event);
event = xmlef.createEndElement("h",
"http://www.w3.org/1999/xhtml",
"html");
xmlew.add(event);
xmlew.flush();
xmlew.close();
}
catch (FactoryConfigurationError fce)
{
System.err.println("FCE: " + fce);
}
catch (IOException ioe)
{
System.err.println("IOE: " + ioe);
}
catch (XMLStreamException xmlse)
{
System.err.println("XMLSE: " + xmlse);
}
}
}
Listing 4-4.
StAXDemo (version 4)
清单 4-4 应该很容易理解;这是基于事件的清单 4-3 的等价物。注意,这个清单包括从实现这个接口的匿名类创建的java.util.Iterator实例。创建这些迭代器是为了将名称空间或属性传递给XMLEventFactory的StartElement createStartElement(String prefix, String namespaceUri, String localName, Iterator attributes, Iterator namespaces)方法。(当迭代器不适用时,可以将null传递给这个参数;例如,当开始标签没有属性时。)
编译清单 4-4 并运行生成的应用程序。您应该在当前目录中发现一个recipe.xml文件。
Exercises
以下练习旨在测试您对第四章内容的理解。
Define StAX. What packages make up the StAX API? True or false: A stream-based reader extracts the next infoset item from an input stream by obtaining an event. How do you obtain a document reader? How do you obtain a document writer? What does a document writer do when you call XMLOutputFactory’s void setProperty(String name, Object value) method with XMLOutputFactory.IS_REPAIRING_NAMESPACES as the property name and true as the value? Create a ParseXMLDoc application that uses a StAX stream-based reader to parse its single command-line argument, an XML document. After creating this reader, the application should verify that a START_DOCUMENT infoset item has been detected, and then enter a loop that reads the next item and uses a switch statement to output a message corresponding to the item that has been read: ATTRIBUTE, CDATA, CHARACTERS, COMMENT, DTD, END_ELEMENT, ENTITY_DECLARATION, ENTITY_REFERENCE, NAMESPACE, NOTATION_DECLARATION, PROCESSING_INSTRUCTION, SPACE, or START_ELEMENT. When START_ELEMENT is detected, output this element’s name and local name, and output the local names and values of all attributes. The loop ends when the END_DOCUMENT infoset item has been detected. Explicitly close the stream reader followed by the file reader upon which it’s based. Test this application with Exercise 1-21’s books.xml file.
摘要
StAX 是一个 Java API,用于从开始到结束顺序解析 XML 文档,也用于创建 XML 文档。Java 通过存储在javax.xml.stream、javax.xml.stream.events和javax.xml.stream.util包中的类型实现 StAX。
StAX 解析器被称为文档阅读器,StAX 文档创建者被称为文档编写器。StAX 将文档阅读器和文档编写器分为基于流的和基于事件的。
文档阅读器是通过调用在XMLInputFactory类中声明的各种create方法获得的。文档编写者是通过调用在XMLOutputFactory类中声明的各种create方法获得的。
第五章介绍了 Java 的 XPath API 来简化 DOM 节点访问。