简介
| 关于本教程 |
在上一篇教程(“ 利用 Java 技术进行 XML 编程,第 1 部分”)中,我介绍了在 Java 语言中解析 XML 的基本知识,讨论了主要的 API(DOM、SAX 和 JDOM),然后通过一些示例说明了多数 XML 应用程序中常见的基本任务。本教程将考察以前没有涉及到的更加困难的工作,比如:
-
- 读取和设置解析器特性
- 使用名称空间
- 验证 XML 文档
在那篇入门教程中谈到的 API 有:
-
- 文档对象模型(DOM),Levels 1、Levels 2 和 Levels 3
- Simple API for XML (SAX),Version 2.0
- JDOM,由 Jason Hunter 和 Brett McLaughli 创建的一种简单 Java APIn
- Java API for XML Processing (JAXP)
我还将讨论几种验证方法,其中包括 W3C XML Schema、RELAX NG 和 Schematron。
| 关于本文中的示例 |
这里的多数示例都使用上一篇教程中出现的莎士比亚十四行诗。该十四行诗的结构如下:
<sonnet>
<author>
<lastName>
<firstName>
<nationality>
<yearOfBirth>
<yearOfDeath>
</author>
<lines>
[14 <line> elements]
</lines>
</sonnet>
在文中的不同示例中,该文档的一些版本使用名称空间,而另一些则使用 DTD、W3C XML Schemas 或者其他的验证模式语言。完整示例请参阅以下文件:
-
- sonnet.xml
- sonnet.dtd(下载后在文本编辑器中查看)
- sonnetNamespaces.xml
- sonnet.xsd
- sonnetSchema.xml
- sonnet.rng
- sonnetRules.xsl
- sonnetSchematron.xml
您还可以下载这些文件 ,在文本编辑器中查看它们。
| 设置机器 |
运行这些例子之前需要对您的机器做一些设置(假设您知道如何编译和运行 Java 程序,也知道如何设置 CLASSPATH 变量)。
-
- 首先请访问 Apache XML Project(http://xml.apache.org/xerces2-j)上的 Xerces XML 解析器主页,您也可以直接进入下载页面(http://xml.apache.org/xerces2-j/download.cgi)。
- 对从 Apache 下载的文件进行解压缩,这将创建一个名为
xerces-2_5_0或者类似名称的(取决于解析器的版本)目录。您所需要的 JAR 文件(xercesImpl.jar和xml-apis.jar)应该在 Xerces 根目录中。 - 请访问 JDOM 项目网站 并下载最新版本的 JDOM(http://jdom.org/)。
- 对从 JDOM 下载的文件进行解压缩,创建名为
jdom-b9或者类似名称的目录。您所需要的 JAR 文件(jdom.jar)应该在build目录中。 - 最后请下载本教程所用的样例文件压缩包 ,并对其进行解压缩。
- 将当前目录(
.)、xercesImpl.jar、xml-apis.jar和jdom.jar添加到CLASSPATH中。
| 关于作者 |
最早的 13 个殖民地之一,“花园之州”新泽西是七百多万人的家园。熙熙攘攘,充满活力,它具有非凡的多样性,从特伦顿的田园风味的山丘到与纽约以及费城毗邻的市区。新泽西州以其自然遗产而自豪,近年来因为其丰富的野生动植物资源而备受尊崇,比如贝壳娥螺(新州的州贝)和东方金翅雀(新州的州鸟)。
新泽西州也以其对现代交通的长期支持而闻名。境内的特拉华河和哈德逊河是这个国家最初的两条高速公路,时至今日,它的 Turnpike 是这个国家最繁忙的公路。它对现代技术的热衷,从最先允许城市使用停车付费卡和在线购买捕鱼许可证可见一斑。您可以通过 dtidwell@us.ibm.com 与作者联系,或者访问他的网站 www.state.nj.us。
读取和设置解析器特性
| 解析器特性 |
随着 XML 变得越来越复杂,解析器也变得越来越复杂。DOM、SAX 和 JDOM 都定义了一些解析器特性。其中有些特性是必须的,有些则是可选的。这三种 API 都为解析器特性的读取和设置提供了类似的方法和异常。
拿 SAX 来说吧,SAX API 本身在 XMLReader 接口中定义了读取和设置解析器特性的方法。JAXP 也在 SAXParserFactory 和 SAXParser 类中提供了相同的方法。下面的代码就使用了这些方法:
public static void checkFeatures() { try { SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setFeature ("http://xml.org/sax/features/namespace-prefixes", true); if (!spf.getFeature ("http://xml.org/sax/features/validation")) spf.setFeature ("http://xml.org/sax/features/validation", true); . . . }
除了 getFeature() 和 setFeature() 方法之外, JAXP 还定义了处理常用的名称空间和验证特性的方法。SAXParserFactory 类定义了 setNamespaceAware() 和 setValidating() 方法,在创建的任何 SAXParser 上设置这些特性。此外,SAXParserFactory 和 SAXParser 都提供了 isNamespaceAware() 和 isValidating() 方法。
| 设置 SAX 解析器特性 |
在设置 SAX 特性的 getFeature() 和 setFeature() 方法中我留下了一个细节:处理异常。在设置 SAX 解析器特性时,一定要捕获两种异常: SAXNotSupportedException 和 SAXNotRecognizedException。
catch (SAXNotSupportedException snse) { System.out.println ("The feature you requested is not supported."); } catch (SAXNotRecognizedException snre) { System.out.println ("The feature you requested is not recognized."); }
SAXNotSupportedException 表示解析器可以识别出请求的特性,但是不支持它,而 SAXNotRecognizedException 表示解析器根本没有听说过所要求的特性。
| SAX 解析器特性 |
SAX 2.0 标准要求解析器能够识别以下两种特性:
-
解析器识别名称空间:如果该属性为
true,所有的元素和属性都可以使用名称空间 URI 和未限定的本地名。任何 SAX 2.0 兼容的解析器必须支持将该属性的默认值设为true。 -
解析器对名称空间前缀的解析提供支持。当该属性设为
true时,元素和属性可以使用名称空间前缀,包括xmlns:属性。任何 SAX 2.0 兼容的解析器都必须支持将该属性的默认值设为false。
http://xml.org/sax/features/namespaces
http://xml.org/sax/features/namespace-prefixes
关于这两必备特性的另外一点是:解析器必须提供它们的 get 方法,但不一定提供 set 方法。
为了帮助您避免造成 SAXNotRecognizedException 异常,下面列出了常见的支持特性。在 SAX 2.0 标准中没有定义这些特性(标准只定义了两种必备特性)。SAX 解析器不一定要支持或者识别下述任何一种特性,任何给定的解析器都可以增加其他专有的特性。
-
决定解析器是否处理外部通用实体。该特性没有默认值,虽然在启用验证时该特性被设为
true。 -
决定解析器是否处理外部参数实体。该特性没有默认值,虽然在启用验证时该属性被设为
true。 -
该属性定义 XML 声明是否包含
standalone="yes"。不能修改、只能查询该属性,而且只能在startDocument事件之后查询。 -
SAX 解析器的
LexicalHandler将报告参数实体的开始和结束。 -
若值为
true,表明定义声明的SYSTEMID 将以相对于文档基 URI 的形式给出;false则表明 ID 是按照文档中出现的形式给出的。程序中可以使用Locator.getSystemId()方法获得文档的基准 URI。 -
如果将该属性设为
true,那么就会使用java.lang.String.intern()将所有的 XML 名字和名称空间 URI 都“拘禁”(intern)起来。与调用java.lang.String.equals()相比,使用intern()能使字符串的比较更快。 -
若值为
true,即表明解析器使用更新后的接口,它们分别是org.xml.sax.ext.Attributes2、org.xml.sax.ext.Locator2和org.xml.sax.ext.EntityResolver2。 -
规定解析器是否验证文档。若为
true,则external-general-entities和external-parameter-entities也自动被设为true。 -
如果设置了
namespace-prefixes特性,可以使用该特性来控制名称空间声明是否在名称空间之中。 Namespaces in XML 规范规定名称空间声明不在任何名称空间之中,而 DOM Level 1 规范将名称空间声明放在名称空间http://www.w3.org/2000/xmlns/中。如果该特性为true,则名称空间声明在名称空间中,如果设为false则不在名称空间中。
http://xml.org/sax/features/external-general-entities
http://xml.org/sax/features/external-parameter-entities
http://xml.org/sax/features/is-standalone
http://xml.org/sax/features/lexical-handler/parameter-entities
http://xml.org/sax/features/resolve-dtd-uris
http://xml.org/sax/features/string-interning
http://xml.org/sax/features/use-attributes2
http://xml.org/sax/features/use-locator2 http://xml.org/sax/features/use-entity-resolver2
http://xml.org/sax/features/validation
http://xml.org/sax/features/xmlns-uris
您可以在 saxproject.org/apidoc/org/xml/sax/package-summary.html 找到所有已经定义的特性 URI 的列表。
| 关于实体的简要说明 |
有很多解析器特性都与实体及其处理有关。我在这里对实体作一简要的说明,因为有些读者可能不太熟悉。总的来说,实体定义了在处理 XML 文档的过程中将被其他事物代替的字符串。下面分别介绍不同类型的实体。
一般实体和参数实体: 一般实体定义了在 XML 文档内部使用的置换,而参数实体只出现在 DTD 中,定义了仅能在 DTD 内部使用的置换。(DTD 后面还要介绍。)一般实体如下所示:
<!ENTITY corp "International Business Machines Corporation">
通过这个实体定义,字符串 &corp; 出现的地方都被替换为字符串 International Business Machines Corporation。
参数实体如下所示:
<!DOCTYPE article [ <!ENTITY % basic "a|b|code|dl|i|ol|ul|#PCDATA"> <!ELEMENT body (%basic;)*>
在该示例中,参数实体 basic 和一个特定的字符串联系在一起。假设这些是 HTML 元素,如果使用参数实体 %basic;,则意味着一个元素可以包含文本(#PCDATA 代表“以解析的字符数据”)或者元素 <a>、 <b>、<code>、<dl>、<i>、<ol> 和 <ul>。在 DTD 中使用这个参数实体可以省去很多输入工作。
内部实体与外部实体:内部实体在 XML 文件内部定义,而外部实体则在单独的(外部)文件中定义。外部文件很可能是另一个 XML 文件,但也可能是其他的东西(很快就会看到)。下面代码的第一行定义了一个内部实体,而第二行使用 SYSTEM 关键字和对文部文件的引用定义了一个外部实体:
<!ENTITY auth "Doug Tidwell"> <!ENTITY BoD SYSTEM "http://www.ibm.com/board_of_directors.html">
通过这些代码,字符串 &auth; 将被替换为文本 Doug Tidwell,而字符串 &BoD; 将使用文件 board_of_directors.html 中的内容代替。第一个实体是一个内部一般实体,而第二个实体则是一个外部一般实体。
预定义实体: XML 标准定义了 5 个随时可以使用的实体,即小于号(<)、大于号(>)、“与”号(&)、单引号(')和双引号(")。
| 设置 DOM 解析器特性 |
与 SAXParserFactory 相比,JAXP DocumentBuilderFactory 能够设置的特性要少一些。其中最重要的方法包括:
- 设置工厂的验证属性。
-
如果工厂创建了验证解析器,则返回
true,否则返回false。 - 设置工厂的名称空间感知属性。
-
如果工厂创建的是名称空间感知解析器,则返回
true,否则返回false。 - 设置工厂的空白字符属性。如果该属性为 true,则工厂创建的解析器不会再为文档中可忽略的空白字符创建节点。
-
如果工厂创建了忽略空白字符的解析器,则返回
true,否则返回false。
setValidating(boolean)
isValidating()
setNamespaceAware(boolean)
isNamespaceAware()
setIgnoringElementContentWhitespace(boolean)
isIgnoringElementContentWhitespace()
除了上述特性外,下面这些特性很少使用:
-
工厂创建的解析器将
CDATA节转化成文本节点。(CDATA代表“字符数据”,这里指的是未解析的文本。) - 工厂创建的解析器展开实体引用节点。
- 工厂创建的解析器忽略注释。
setCoalescing(boolean)
和 isCoalescing()
setExpandEntityReferences(boolean)
和 isExpandEntityReferences()
setIgnoringComments(boolean)
和 isIgnoringComments()
名称空间概述
| 名称空间简介 |
当 XML 宣告诞生时,失落的 HTML 开发人员对创建自己的标签的前景感到希望渺茫。但最终他们都能够创建标签描述自己的数据,而不再将数据硬塞到 HTML 狭小的结构中。
一旦最初的兴奋冷静下来,就会发现事情显然并非如此简单,早晚会有两个或更多的组织定义同样的标签。比如,我维护了一个在线书店,因此使用 <title> 标签作为书名。而您也可能将生意上的所有客户的地址保存在 XML 中,从而使用 <title> 标签表示客户的敬称。
创建 XML 图书订单的时候,对我而言,使用我的标签描述所订购的书籍是完全合理的;而对于您,使用您的标签描述发货地址也同样合情合理。问题很清楚,如何区分这两种不同的 <title> 标签呢?答案就是名称空间。
总体上来说,名称空间就像是 Java 中的包声明。两个类可以有相同的名称,只要它们属于不同的包就可以。比如,JDOM 和 DOM 都定义了 Document 类。为了明确使用的是哪一个类,需要同时使用包名和类名,如 org.jdom.Document 和 org.w3c.dom.Document。
名称空间由两部分组成:前缀和惟一字符串。下面是使用名称空间的文档片段:
<bookOrder xmlns:lit="http://www.literarysociety.org/books" xmlns:addr="http://www.usps.com/addresses"> . . . <lit:title>My Life in the Bush of Ghosts</lit:title> . . . <shipTo> <addr:title>Ms.</addr:title> <addr:firstName>Linda</addr:firstName> <addr:lastName>Lovely</addr:lastName> . . .
该文档定义了两个名称空间。lit 前缀与字符串 http://www.literarysociety.org/books 关联,而 addr 前缀与字符串 http://www.usps.com/addresses 关联。当使用 <lit:title> 或 <addr:title> 元素时,使用的是哪一个 <title> 元素很清楚。
| 更多的名称空间细节 |
当在给定的元素上定义名称空间时,该元素及该元素中的每个元素都可以使用这个名称空间。在上面的例子中,我在文档的根元素上定义了使用的所有名称空间。我也可以在 <shipTo> 元素上定义 addr 名称空间:
<shipTo xmlns:addr="http://www.usps.com/addresses"> <addr:title>Ms.</addr:title> <addr:firstName>Linda</addr:firstName> <addr:lastName>Lovely</addr:lastName> . . .
如果愿意多输入一些字符,也可以在每个元素上重定义名称空间:
<shipTo> <addr:title xmlns:addr="http://www.usps.com/addresses"> Ms. </addr:title> <addr:firstName xmlns:addr="http://www.usps.com/addresses"> Linda </addr:firstName> <addr:lastName xmlns:addr="http://www.usps.com/addresses"> Lovely </addr:lastName> . . .
只要使用名称空间前缀,就必须在该元素或者它的某个祖先上定义与该前缀关联的名称空间。在根元素上定义所有的名称空间可以简化和缩短文档。
最后一项技术是使用 xmlns 属性而根本不去定义前缀。这样就为当前元素和没有名称空间前缀的所有后代定义了默认名称空间。因此可以将 <title> 元素写成:
<title xmlns="http://www.literarysociety.org/books"> My Life in the Bush of Ghosts </title>
后面我将给出 XML 十四行诗的带有名称空间限定的版本。如果需要,可以对 <author> 元素使用默认名称空间:
<author xmlns="http://www.literarysociety.org/authors"> <lastName>Shakespeare</lastName> <firstName>William</firstName> <nationality>British</nationality> <yearOfBirth>1564</yearOfBirth> <yearOfDeath>1616</yearOfDeath> </author>
因为这些元素都没有名称空间前缀,名称空间感知的解析器会认为这些元素都属于 http://www.literarysociety.org/authors 名称空间。而在<author> 元素外部,没有定义该默认名称空间。
| 比较两个名称空间 |
有时候可能会需要检查名称空间的值。比如,在 XSLT 样式表中,所有的样式表元素都必须来自名称空间 http://www.w3.org/1999/XSL/Transform。通常该名称空间字符串与前缀 xsl 关联,但是并非必须如此。如果要保证给定元素的名称空间是正确的,则必须检查名称空间字符串,而不是检查名称空间前缀。换句话说,下面的 XSLT 元素是正确的:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
而这个 XSLT 元素是不正确的:
<xsl:stylesheet xmlns:xsl="http://i-love-stylesheets.com">
在第二个示例中,名称空间前缀是我们所期望的前缀,但是名称空间字符串是错误的。在下面的示例中, XSLT 元素是正确的:
<xqdkera:stylesheet xmlns:xqdkera="http://www.w3.org/1999/XSL/Transform">
前缀不一定要是传统的 xsl,这并不重要。比较两个名称空间时,问题不在于前缀。
| 关于名称空间的一般误解 |
关于名称空间最容易混淆的一点是并没有将那个惟一的字符串作为 URL 使用(虽然看起来好像是,但实际上并不是)。组织的通常做法是用它们的域名来保证字符串的惟一性。您可能认为(我最初也是这样想的)连接到 Internet 的 XML 解析器应该能够从那个 URL 下载 DTD 或者模式,但事实上并非如此。
虽然 XML 标准中的名称空间将那个惟一的字符串定义为 URI(统一资源标识符,请参阅 参考资料),但实际上它只是一个字符串。只要 cranberries 在文档所有的名称空间定义是惟一的,xmlns:addr="cranberries" 就是合法的。关于有效 URI 的所有细节,请参阅 参考资料 中的 URI 标准(RFC2396)。
| (可能混淆的)最后一点 |
如果您很清楚,该惟一字符串不是一个 URL,而且 XML 解析器永远不会使用该字符串作为 URL,那么请您继续读下去。如果您对此怀疑,或者仍然有点混淆,请直接看下一部分 从解析器获取名称空间信息 的内容。
尽管名称空间字符串不是一个 URL,当您在浏览器上指向这个字符串时,有时候会看到关于标签集或者文档类型的有用信息。这种做法完全是可选的,主要是为了帮助人类阅读,而非供机器使用。比如,http://www.w3.org/2003/05/soap-envelope/role/next 在 SOAP 1.2 规范中定义名称空间。如果在浏览器中转向 http://www.w3.org/2003/05/soap-envelope/role/next ,您就会看到一个 HTML 页面,它提供更多关于 SOAP 1.2 规范的信息。
如果其他人希望使用您定义的元素,那么只在有人试图加载名称空间 URI 时提供该信息将是一个好主意。
解析名称空间
| 从解析器获取名称空间信息 |
现在我们已经讨论了名称空间的基本知识,下面将考察 DOM、SAX 和 JDOM,看一看它们如何报告名称空间信息。所有这些示例都使用了下面的十四行诗版本:
<sonnet pt:type="Shakespearean" xmlns:pt="http://www.literarysociety.org/poemtypes"> <auth:author xmlns:auth="http://www.literarysociety.org/authors"> <auth:lastName>Shakespeare</auth:lastName> <auth:firstName>William</auth:firstName> <auth:nationality>British</auth:nationality> <auth:yearOfBirth>1564</auth:yearOfBirth> <auth:yearOfDeath>1616</auth:yearOfDeath> </auth:author> <title>Sonnet 130</title> <lines> . . . </lines> </sonnet>
在使用名称空间时,需要了解关于名称空间限定元素的一些信息。比如上面的 <auth:lastName> 元素:
-
元素的本地名称(未限定名)
-
lastName——不带名称空间前缀的元素名
名称空间前缀
-
auth
元素的限定名
-
auth:lastName——元素的名称,包含任何名称空间前缀
名称空间 URI
-
http://www.literarysociety.org/authors
我将向您展示每种 API 是如何暴露该信息的。
| DOM 和名称空间 |
DOM Level 2 的文档对象模型中增加了对名称空间的支持。下面列出了需要的信息项和访问该信息的 DOM 方法:
-
元素的本地名(未限定名)
-
Node.getLocalName()
名称空间前缀
-
Node.getPrefix()
元素的限定名
-
Node.getNodeName()
名称空间 URI
-
Node.getNamespaceURI()
注意所有这些方法都是 Node 接口的一部分,这些方法的使用有点麻烦:
-
- 如果对
Element或Attribute之外的其他东西调用getLocalName()、getPrefix()或getNamespaceURI(),结果都是null。 - 如果对用 DOM Level 1 方法,如
Document.createElement()创建的元素,调用getLocalName()、getPrefix()或getNamespaceURI(),得到的结果是null。(要创建可用于这些方法的节点请使用 DOM Level 2 方法Document.createElementNS()。) - 对于
Element和Attribute,getNodeName()总是返回元素或属性的名称。如果元素或属性是名称空间限定的,则节点名就是auth:lastName或者类似的东西,否则节点名就是lastName。为了确定给定的节点是否是名称空间限定的,必须查看getPrefix()是否为空,或者在节点名中查找冒号。
- 如果对
| 创建名称空间感知的 DOM 解析器 |
现在看一看 DomNS,这个 Java 程序取自利用 Java 进行 XML 编程的入门教程(http://www.ibm.com/developerworks/edu/x-dw-xml-i.html),在结构上类似于 DomOne。这里有两个基本的不同点:您需要创建一个名称空间感知的 DOM 解析器,并增加额外的代码来处理 DOM 树中的名称空间信息。不仅要在控制台中回显解析的文档,还要打印 DOM 树中关于名称空间限定元素的信息。
第一步是创建名称空间感知的解析器。对于多数解析器,名称空间感知在默认情况下是关闭的。因为要使用 JAXP,所以需要在创建 DOM 解析器之前设置 DocumentBuilderFactory 的该属性:
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(uri);
if (doc != null)
printNamespaceInfo(doc.getDocumentElement());
使用 JAXP setNamespaceAware() 方法打开工厂类的数据感知特性,此后该工厂创建的所有的 DOM 解析器都将具有名称空间感知性。一旦建立的解析器,则递归调用 printNamespaceInfo() 方法就可以找到文档中所有名称空间限定的元素和属性。
| 寻找 DOM 树中的名称空间 |
printNamespaceInfo() 方法查看 DOM 树中所有的元素和属性,只要 getPrefix() 方法返回非空字符串,就立刻打印这些节点的所有细节。下面是该方法代码的主要部分:
case Node.ELEMENT_NODE: { if (node.getPrefix() != null) { System.out.println("/nElement " + node.getNodeName()); System.out.println("/tLocal name = " + node.getLocalName()); System.out.println("/tNamespace prefix = " + node.getPrefix()); System.out.println("/tNamespace URI = " + node.getNamespaceURI()); } if (node.hasAttributes()) { NamedNodeMap attrs = node.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) if ((attrs.item(i).getPrefix()) != null) printNamespaceInfo(attrs.item(i)); } if (node.hasChildNodes()) { NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) printNamespaceInfo(children.item(i)); } break; } case Node.ATTRIBUTE_NODE: { System.out.println("/nAttribute " + node.getNodeName() + "=" + node.getNodeValue()); System.out.println("/tLocal name = " + node.getLocalName()); System.out.println("/tNamespace prefix = " + node.getPrefix()); System.out.println("/tNamespace URI = " + node.getNamespaceURI()); break; }
按照以下三个步骤处理元素节点:
-
- 如果元素有名称空间前缀,则打印该节点的细节。
- 查看该元素的所有属性,如果有名称空间限定的属性,则对其调用
printNamespaceInfo()。 - 对该元素的所有子调用
printNamespaceInfo()。
处理属性节点时只需要打印其细节。注意,这里假定只对名称空间限定的属性节点调用 printNamespaceInfo()。
如果对文件 sonnetNamespaces.xml 运行 DomNS,将得到如下的结果:
C:/adv-xml-prog>java DomNS sonnetnamespaces.xml Attribute pt:type=Shakespearean Local name = type Namespace prefix = pt Namespace URI = http://www.literarysociety.org/poemtypes Attribute xmlns:pt=http://www.literarysociety.org/poemtypes Local name = pt Namespace prefix = xmlns Namespace URI = http://www.w3.org/2000/xmlns/ Element auth:author Local name = author Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors Attribute xmlns:auth=http://www.literarysociety.org/authors Local name = auth Namespace prefix = xmlns Namespace URI = http://www.w3.org/2000/xmlns/ Element auth:lastName Local name = lastName Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors . . .
输出结果列出了源文档中所有名称空间限定元素或属性的细节。上面这一部分列出了 pt:type 属性、<auth:author> 元素和 <auth:lastName> 元素。
注意 xmlns:pt 和 xmlns:auth ,这两个名称空间定义都被映射到名称空间 http://www.w3.org/2000/xmlns/。该空间是 DOM 解析器用于名称空间定义本身的默认名称空间。 原版 XML 名称空间规范的第 4 节规定“前缀 xmlns 仅用于名称空间绑定,本身不绑定到任何名称空间名。”但是在 DOM Level 2 发布时, DOM Level 2 规范的第 1.1.8 节修正了这一点:
注意:在 DOM 中,所有的名称空间声明属性都是根据定义被绑定到名称空间 URI:http://www.w3.org/2000/xmlns/。这就是名称空间前缀或者限定为“xmlns”的属性。虽然在撰写本文时该属性还不是 XML 名称空间规范的一部分,但计划将其结合到将来的版本中。
换句话说,定义名称空间(xmlns:abc="...")或者默认名称空间(xmlns="...")的任何属性自身都映射到名称空间 http://www.w3.org/2000/xmlns/中。当使用与 SAX 解析相同的文档时,名称空间定义没被作为属性而报告,因此没有为这些定义本身定义名称空间。
| 名称空间感知的 DOM 方法 |
在结束关于 DOM 和名称空间的讨论之前,我们在下面列出了所有的名称空间感知 DOM 方法。每个方法名后面都有简要的描述、关于各种方法的细节和使用请参阅解析器所带的 DOM 文档。
- 使用给定的名称空间和限定名创建属性。
- 使用给定的名称空间和限定名创建元素。
-
返回
NodeList,它包含将给定名称空间与本地名称进行匹配的所有后代节点 。 -
给定属性的名称空间和本地名称,返回一个
Attribute节点。 - 给定属性的名称空间和本地名称返回属性值。
-
返回
NodeList,它包含将给定名称空间与本地名称进行匹配的所有后代节点 。 -
如果该元素有一个包含具有给定名称空间和本地名称的属性,则返回
true,否则返回false。 - 给定名称空间和本地名称,从元素中删除包含该名称空间和本地名称的属性。
-
向该元素中增加给定名称空间限定的
Attr对象。 - 给定属性的名称空间、本地名称和一个值,向该元素中增加一个包含规定值的名称空间限定属性。
- 返回给定节点的本地名称(非限定名)。
-
返回和给定节点关联的名称空间字符串,如果该元素或属性没有关联的名称空间则返回
null。 - 返回节点名。如果节点是名称空间限定的,则返回前缀和元素名,否则只返回元素名。
-
返回节点的名称空间前缀,如果 XML 源中的元素或属性没有前缀,则返回
null。 - 设置该节点的名称空间前缀。
Document.createAttributeNS(...)
Document.createElementNS(...)
Document.getElementsByTagNameNS(...)
Element.getAttributeNodeNS(...)
Element.getAttributeNS(...)
Element.getElementsByTagNameNS(...)
Element.hasAttributeNS(...)
Element.removeAttributeNS(...)
Element.setAttributeNodeNS(...)
Element.setAttributeNS(...)
Node.getLocalName()
Node.getNamespaceURI()
Node.getNodeName()
Node.getPrefix()
Node.setPrefix(...)
提示: 所有这些方法都定义在 Node 接口中,只对 Element 和 Attribute 返回有意义的信息。对其他节点类型调用这些方法将返回 null。
| SAX 和名称空间 |
与 DOM 相同,SAX 的第一版也不支持名称空间。 SAX 2 通过多种不同的方法增加了名称空间感知,这里作一简要的回顾。首先看一看如何获得这 4 个信息项:
-
元素的本地名称(非限定名)
-
startElement和endElement事件的localName参数(第二个参数)。
名称空间前缀
-
没有直接的方法。名称空间前缀是
startElement和endElement事件的qName参数(第三个参数)中冒号之前的所有内容。
元素的限定名
-
startElement和endElement事件的qName参数(第三个参数)。
名称空间 URI
-
startElement和endElement事件的uri参数(第一个参数)。
如您所料,名称空间信息是通过不同的事件提供的,尤其是 startElement 和 endElement。
| 创建名称空间感知的 SAX 解析器 |
现在来看一看 SAXNS,它类似于上述 DomNS 的 Java 程序。和 DomNS 一样,第一步是要创建一个名称空间感知的解析器。设置 SAXParserFactory 对象的一个属性,创建 SAXParser ,然后就可以准备进行解析了。下面是创建 SAXParser 需要的代码:
SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(true); SAXParser sp = spf.newSAXParser(); sp.parse(uri, this);
使用 JAXP setNamespaceAware() 方法打开工厂类的名称空间感知特性,此后,该工厂创建的所有的 SAXParser 都包含名称空间感知特性。一旦创建了解析器,事件处理程序就可以从那里获得名称空间信息。
| 寻找 SAX 事件中的名称空间 |
对于 SaxNS,您可能希望在控制台中回显所有名称空间限定元素和属性的细节。您可以在 startElement 事件中找到所有需要的信息。下面是 startElement 事件处理程序的代码:
public void startElement(String namespaceURI, String localName, String qName, Attributes attrs) { if (namespaceURI.length() > 0) { System.out.println("/nElement " + qName); System.out.println("/tLocal name = " + localName); if (qName.indexOf(':') > 0) System.out.println("/tNamespace prefix = " + qName.substring(0, qName.indexOf(':'))); else System.out.println("/tNamespace prefix ="); System.out.println("/tNamespace URI = " + namespaceURI); } if (attrs != null) { int len = attrs.getLength(); for (int i = 0; i < len; i++) if (attrs.getURI(i).length() > 0) { System.out.println("/nAttribute " + attrs.getQName(i) + "=" + attrs.getValue(i)); System.out.println("/tLocal name = " + attrs.getLocalName(i)); if (qName.indexOf(':') > 0) System.out.println("/tNamespace prefix = " + attrs.getQName(i). substring(0, attrs.getQName(i). indexOf(':'))); else System.out.println("/tNamespace prefix = "); System.out.println("/tNamespace URI = " + attrs.getURI(i)); } } }
如您所料,这些代码与 DomNS 非常类似,但也存在一些不同:
-
- 在处理元素的属性时,这些属性是事件处理程序的参数。在 DOM 版本中,这些属性是通过方法来访问的。
- 如前所述,您不能直接获得元素或属性的名称空间前缀。上面的示例使用
java.lang.String.indexOf()和java.lang.String.substring()方法从元素或属性的限定名中抽取前缀。(注意,必须保证限定名中包含冒号,如果元素或属性使用默认名称空间,限定名中就没有冒号。) - SAX 中的属性与 DOM 世界中的属性更加相似。元素的属性是用一组对象表示的,通过
getLocalName()和getValue()方法可以处理给定属性的性质。
如果对文件 sonnetNamespaces.xml 运行 SaxNS,将得到下面的结果:
C:/adv-xml-prog>java SaxNS sonnetnamespaces.xml Attribute pt:type=Shakespearean Local name = type Namespace prefix = pt Namespace URI = http://www.literarysociety.org/poemtypes Element auth:author Local name = author Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors Element auth:lastName Local name = lastName Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors Element auth:firstName Local name = firstName Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors Element auth:nationality Local name = nationality Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors
SaxNS 输出的大部分内容和 DomNS 相同。惟一的区别是 SAXParser 没有将名称空间定义(如 xmlns:pt="http://www.lit...")作为属性报告。后面我将说明处理名称空间定义的 SAX 事件。
在讨论使用 DOM 解析器发现名称空间(寻找 DOM 树中的名称空间)时,我提到所有的名称空间定义都是作为属于 名称空间定义没有出现,是因为 SAX 通过单独的事件来报告它们。 处理 这里的逻辑非常简单,当遇到 在控制台中回显名称空间信息时,就可以使用
|
| 处理名称空间的最后一种方法 |
SaxNSTwo 使用的方法非常直接:遇到 startPrefixMapping 事件时,在 HashMap 中加入一条记录,然后在需要的时候检索该值。如果给定 URI 映射到两个前缀,而且这些定义互相嵌套,那么SaxNSTwo 就无法工作了。尽管这种情况不常见,但它却是合法的。 SaxNS Two 不能正确处理给定前缀映射到两个 URI 的情况,不过在您的代码作出错误的处理之前,解析器的错误检查会给出报告。
要解决这个问题(假设您认为这是一个问题),就需要为每个 URI 都实现一个栈,在遇到 startPrefixMapping 事件时,将值压入这个 URI 的栈,而在遇到 endPrefixMapping 事件时从该 URI 的栈中弹出值。
如果您需要处理这种情况,org.xml.sax.helpers 包提供了一个特殊的类 NamespaceSupport,您可以在进入和离开作用域时用这个类管理名称空间。下面我们来看最后一个类 SaxNSThree,它使用 NamespaceSupport 对象处理名称空间。
使用 NamespaceSupport 对象处理名称空间的过程如下:
-
- 在遇到
startPrefixMapping事件时,调用pushContext()保存定义的名称空间的当前状态。此后可以调用declarePrefix向当前上下文中增加新定义的名称空间。 - 遇到
endPrefixMapping事件时,调用popContext返回以前的名称空间定义设置。 - 如果需要访问和给定 URI 关联的前缀,则调用
getPrefix()。
- 在遇到
现在看一看 SaxNSThree。首先是 NamespaceSupport 对象的声明和两个事件处理程序:
private NamespaceSupport ns = new NamespaceSupport(); . . . public void startPrefixMapping(String prefix, String uri) { ns.pushContext(); ns.declarePrefix(prefix, uri); } public void endPrefixMapping(String prefix) { ns.popContext(); }
为了说明名称空间的处理过程,该程序将像前面那样处理所有的元素和属性,并在处理每个元素时列出所有已经定义的名称空间。十四行诗的处理结果如下:
C:/adv-xml-prog>java SaxNSThree sonnetnamespaces.xml <sonnet> : 2 prefixes defined - (xml, pt) Attribute pt:type=Shakespearean Local name = type Namespace prefix = pt Namespace URI = http://www.literarysociety.org/poemtypes <auth:author> : 3 prefixes defined - (xml, auth, pt) Local name = author Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors <auth:lastName> : 3 prefixes defined - (xml, auth, pt) Local name = lastName Namespace prefix = auth Namespace URI = http://www.literarysociety.org/authors . . . <title> : 2 prefixes defined - (xml, pt) <lines> : 2 prefixes defined - (xml, pt) . . .
注意 XML 前缀始终是定义的,它映射到字符串 http://www.w3.org/XML/1998/namespace。NamespaceSupport 对象,跟踪目前在作用域之内的名称空间定义。
NamespaceSupport 实现的一个不足之处是它的 getPrefixes() 方法返回 Enumeration。(NamespaceSupport 还在其他地方使用 Enumeration。)要获得作用域中的名称空间数量,就必须编写看起来有点笨拙的代码:
private int getPrefixCount() { Enumeration e = ns.getPrefixes(); int count = 0; while (e.hasMoreElements()) { count++; e.nextElement(); } return count; }
如果 getPrefixes() 方法返回某种 Java 集合对象,则可以使用 size() 方法获得当前定义的前缀的数量。
| 名称空间感知的 SAX 对象 |
在开始讨论使用 JDOM 处理名称空间之前,我们在下面列出了所有名称空间感知的 SAX 类和接口,以及一些简要的说明。和往常一样,关于这些方法及其含义的最终定义,请参阅 XML 解析器附带的 SAX 文档。
-
除了返回属性个数的
getLength()方法之外,该类的所有方法都是名称空间感知的。 -
startElement、endElement、startPrefixMapping和endPrefixMapping事件都是名称空间感知的。 -
下列方法是名称空间感知的:
addAttribute()、getIndex()、getLocalName()、getQName()、getType()、getURI()、getValue()、setAttribute()、setLocalName()、setQName()和setURI()。 -
startElement、endElement、startPrefixMapping和endPrefixMapping事件都是名称空间感知的。(这些事件在ContentHandler接口中定义,该接口由DefaultHandler实现。) - 可以用这个类在进入和离开作用域时管理名称空间。
-
该类实现了
ContentHandler接口,因此它包括名称空间感知事件startElement、endElement、startPrefixMapping和endPrefixMapping。
org.xml.sax.Attributes
org.xml.sax.ContentHandler
org.xml.sax.helpers.AttributesImpl
org.xml.sax.helpers.DefaultHandler
org.xml.sax.helpers.NamespaceSupport
org.xml.sax.helpers.XMLFilterImpl
| JDOM 和名称空间 |
在开始讨论之前,先看一看获得元素 4 种基本信息的 JDOM API:
- 元素的本地名称(非限定名)
- 名称空间前缀
- 元素的限定名
- 名称空间 URI
Element.getName()
Element.getNamespacePrefix()
Element.getQualifiedName()
Element.getNamespaceURI()
这些方法的名称简单明确。为了说明 JDOM 如何处理名称空间,我给出了一个应用程序 JdomNS,该程序解析了 XML 文件,并将名称空间信息回显到控制台上。
在谈到如何使用 DOM 和 SAX 处理名称空间时,我曾经提到过,您必须规定您需要一个名称空间感知的解析器。对于 JDOM,在默认情况下,名称空间感知特性在底层 SAX 解析器中是打开的,可以用 org.jdom.input.SAXBuilder 对象封装它。如果需要控制 SAXBuilder 的性质,则可以使用 setFeature() 或 setProperty() 方法。要知道,JDOM 需要用特定的方式配置 SAX 解析器,因此 JDOM 文档建议您使用这些方法时小心点。
| 使用 JDOM 处理名称空间信息 |
要创建 JdomNS ,则需要解析 XML 文件并获得 JDOM Document 结构。和 DomNS 一样,您必须遍历这个结构并发现所有的名称空间信息,这里将使用递归的方法。下面是开始部分的代码:
SAXBuilder sb = new SAXBuilder(); Document doc = sb.build(new File(argv[0])); if (doc != null) printNamespaceInfo(doc.getRootElement());
注意,printNamespaceInfo() 的参数是一个 JDOM Element。在 DOM 中只有一个惟一数据类型(Node),所有的节点类型都是它的子类型。对于 JDOM 则只能使用 Element。
谈到 printNamespaceInfo(),后面我将给出这个方法,它有 4 个功能:
-
- 如果该元素是名称空间限定的,则在控制台中回显名称空间信息。
- 如果该元素有名称空间声明,在控制台中回显这些属性。
- 如果该元素有名称空间限定的属性,在控制台中回显关于这些属性的名称空间信息。
- 如果该元素有任何子元素,则对每个子元素调用
printNamespaceInfo()。
我将在介绍源代码的过程中分别说明这些任务。首先,如果当前元素是名称空间限定的,则需要检查当前元素的名称空间 URI:
if (el.getNamespaceURI().length() > 0) { System.out.println("/nElement " + el.getQualifiedName()); System.out.println("/tLocal name = " + el.getName()); System.out.println("/tNamespace prefix = " + el.getNamespacePrefix()); System.out.println("/tNamespace URI = " + el.getNamespaceURI()); }
然后寻找该元素中定义的新名称空间。注意, JDOM 处理名称空间声明的方式与 DOM 的不同。在 DOM 规范中,名称空间声明(如 xmlns:tp="http://...")被报告为另一个属性。对于 JDOM,名称空间声明不被看作是属性,因此必须使用 getAdditionalNamespaces() 方法。
最后要注意的是,如果当前元素包含自身的名称空间定义(如示例文档中的 <auth:author xmlns:auth="..."> 元素),那么只能通过当前元素访问该名称空间定义,不能通过 getAdditionalNamespaces() 来访问它们。下面是该代码的下一段:
Iterator nsIter = el.getAdditionalNamespaces().listIterator(); while (nsIter.hasNext()) { Namespace ns = (Namespace)nsIter.next(); System.out.println("/nNamespace declaration:"); System.out.println("/tNamespace prefix = " + ns.getPrefix()); System.out.println("/tNamespace URI = " + ns.getURI()); }
所有返回一组事物的 JDOM 方法(getAttributes()、 getAdditionalNamespaces()、 getChildren()等)——成组事物是作为 List 返回的——都属于 Java Collections API。您应该使用 ListIterator 遍历 List。关于使用集合最后需要注意的一点是,由于 ListIterator 返回 Object,所以您必须将各个项强制转化成 Namespace、 Attribute 等。
下一个任务是看一看当前元素的所有属性,以及这些属性是否是名称空间限定的。如您所料,这些代码使用 ListIterator:
Iterator attrIter = el.getAttributes().listIterator(); while (attrIter.hasNext()) { Attribute attr = (Attribute)attrIter.next(); if (attr.getNamespaceURI().length() > 0) { System.out.println("/nAttribute " + attr.getQualifiedName() + "=" + attr.getValue()); . . .
最后一项任务是获得该 Element 的子任务,并调用 printNamespaceInfo 处理它们。代码如下所示:
Iterator childIter = el.getChildren().listIterator(); while (childIter.hasNext()) printNamespaceInfo((Element)childIter.next());
(待续)
XML名称空间解析
本文深入探讨了XML名称空间的概念和用途,以及如何使用DOM、SAX和JDOM等API进行名称空间感知的XML解析。

被折叠的 条评论
为什么被折叠?



