Java 中文官方教程 2022 版(三十八)
实现 SAX 验证
示例程序SAXLocalNameCount默认使用非验证解析器,但也可以激活验证。激活验证允许应用程序判断 XML 文档是否包含正确的标记,或这些标记是否按正确顺序出现。换句话说,它可以告诉您文档是否有效。然而,如果未激活验证,它只能告诉文档是否格式良好,就像在上一节中删除 XML 元素的闭合标记时所示。要进行验证,XML 文档需要关联到一个 DTD 或 XML 模式。SAXLocalNameCount程序可以选择这两个选项。
选择解析器实现
如果没有指定其他工厂类,则将使用默认的SAXParserFactory类。要使用来自不同制造商的解析器,可以更改指向其的环境变量的值。您可以从命令行执行:
java -Djavax.xml.parsers.SAXParserFactory=*yourFactoryHere* [...]
您指定的工厂名称必须是完全限定的类名(包括所有包前缀)。有关更多信息,请参阅SAXParserFactory类的newInstance()方法中的文档。
使用验证解析器
直到这一点,本课程一直集中在非验证解析器上。本节将检查验证解析器,以了解在使用它解析示例程序时会发生什么。
关于验证解析器必须理解的两件事:
-
需要模式或 DTD。
-
因为存在模式或 DTD,只要可能,
ContentHandler.``ignorableWhitespace()方法就会被调用。
可忽略的空格
当存在 DTD 时,解析器将不再调用characters()方法处理它知道是无关紧要的空格。从只对 XML 数据感兴趣的应用程序的角度来看,这是一件好事,因为应用程序永远不会受到纯粹为了使 XML 文件可读而存在的空格的干扰。
另一方面,如果您正在编写一个过滤 XML 数据文件的应用程序,并且希望输出一个同样可读的文件版本,那么这些空格将不再是无关紧要的:它们将变得至关重要。要获取这些字符,您需要在应用程序中添加ignorableWhitespace方法。为了处理解析器看到的任何(通常是)可忽略的空格,您需要添加类似以下代码以实现ignorableWhitespace事件处理程序。
public void ignorableWhitespace (char buf[], int start, int length) throws SAXException {
emit("IGNORABLE");
}
这段代码只是生成一条消息,让您知道看到了可忽略的空格。然而,并非所有解析器都是平等的。SAX 规范不要求调用此方法。Java XML 实现在 DTD 可能时会这样做。
配置工厂
SAXParserFactory 需要设置为使用验证解析器而不是默认的非验证解析器。 下面是 SAXLocalNameCount 示例的 main() 方法中的代码,显示了如何配置工厂以实现验证解析器。
static public void main(String[] args) throws Exception {
String filename = null;
boolean dtdValidate = false;
boolean xsdValidate = false;
String schemaSource = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-dtd")) {
dtdValidate = true;
}
else if (args[i].equals("-xsd")) {
xsdValidate = true;
}
else if (args[i].equals("-xsdss")) {
if (i == args.length - 1) {
usage();
}
xsdValidate = true;
schemaSource = args[++i];
}
else if (args[i].equals("-usage")) {
usage();
}
else if (args[i].equals("-help")) {
usage();
}
else {
filename = args[i];
if (i != args.length - 1) {
usage();
}
}
}
if (filename == null) {
usage();
}
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setValidating(dtdValidate || xsdValidate);
SAXParser saxParser = spf.newSAXParser();
// ...
}
在这里,SAXLocalNameCount 程序被配置为在启动时接受额外的参数,告诉它实现无验证、DTD 验证、XML Schema 定义(XSD)验证或针对特定模式源文件的 XSD 验证。 (这些选项的描述 -dtd、-xsd 和 -xsdss 也被添加到 usage() 方法中,但这里没有显示。)然后,工厂被配置为在调用 newSAXParser 时生成适当的验证解析器。 如 设置解析器 中所示,您还可以使用 setNamespaceAware(true) 来配置工厂返回一个支持命名空间的解析器。 Oracle 的实现支持任何配置选项的组合。 (如果某个实现不支持特定组合,则需要生成工厂配置错误)。
使用 XML Schema 进行验证
尽管本教程不涵盖 XML Schema 的完整内容,但本节向您展示了使用 XML Schema 语言编写的现有模式验证 XML 文档的步骤。 要了解有关 XML Schema 的更多信息,您可以查看在线教程 XML Schema Part 0: Primer,网址为 www.w3.org/TR/xmlschema-0/。
注意 - 存在多种模式定义语言,包括 RELAX NG、Schematron 和 W3C 的 "XML Schema" 标准。 (即使 DTD 也算是一种 "模式",尽管它是唯一不使用 XML 语法描述模式约束的模式。)然而,"XML Schema" 给我们带来了一个术语上的挑战。 虽然短语 "XML Schema schema" 可以很精确,但我们将使用短语 "XML Schema definition" 来避免冗余的外观。
要在 XML 文档中收到验证错误的通知,解析器工厂必须配置为创建验证解析器,如前一节所示。 此外,以下条件必须成立:
-
在 SAX 解析器上必须设置适当的属性。
-
必须设置适当的错误处理程序。
-
文档必须与模式关联。
设置 SAX 解析器属性
从定义将用于设置属性的常量开始是有帮助的。 SAXLocalNameCount 示例设置了以下常量。
public class SAXLocalNameCount extends DefaultHandler {
static final String JAXP_SCHEMA_LANGUAGE =
"http://java.sun.com/xml/jaxp/properties/schemaLanguage";
static final String W3C_XML_SCHEMA =
"http://www.w3.org/2001/XMLSchema";
static final String JAXP_SCHEMA_SOURCE =
"http://java.sun.com/xml/jaxp/properties/schemaSource";
}
注意 - 解析器工厂必须配置为生成一个既具有命名空间感知能力又进行验证的解析器。这在配置工厂中已经展示过。有关命名空间的更多信息在文档对象模型中提供,但现在,请理解模式验证是一个面向命名空间的过程。因为符合 JAXP 规范的解析器默认情况下不具有命名空间感知能力,所以必须设置用于模式验证的属性才能正常工作。
然后,您必须配置解析器以告诉它要使用哪种模式语言。在SAXLocalNameCount中,验证可以针对 DTD 或 XML Schema 执行。以下代码使用上面定义的常量来指定 W3C 的 XML Schema 语言作为程序启动时指定-xsd选项时要使用的语言。
// ...
if (xsdValidate) {
saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
// ...
}
除了设置错误处理中描述的错误处理外,在配置解析器进行基于模式的验证时可能会发生一个错误。如果解析器不符合 JAXP 规范,因此不支持 XML Schema,它可能会抛出SAXNotRecognizedException。为了处理这种情况,setProperty()语句被包裹在 try/catch 块中,如下面的代码所示。
// ...
if (xsdValidate) {
try {
saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
}
catch (SAXNotRecognizedException x){
System.err.println("Error: JAXP SAXParser property not recognized: "
+ JAXP_SCHEMA_LANGUAGE);
System.err.println( "Check to see if parser conforms to the JAXP spec.");
System.exit(1);
}
}
// ...
将文档与模式关联
要使用 XML Schema 定义验证数据,必须确保 XML 文档与之关联。有两种方法可以做到这一点。
-
通过在 XML 文档中包含模式声明。
-
通过在应用程序中指定要使用的模式。
注意 - 当应用程序指定要使用的模式时,它会覆盖文档中的任何模式声明。
要在文档中指定模式定义,您可以创建如下的 XML:
<*documentRoot*
xsi:noNamespaceSchemaLocation='*YourSchemaDefinition*.xsd'>
第一个属性定义了 XML 命名空间(xmlns)前缀xsi,代表 XML Schema 实例。第二行指定了要用于文档中没有命名空间前缀的元素的模式,即通常在任何简单、不复杂的 XML 文档中定义的元素。
注意 - 有关命名空间的更多信息包含在文档对象模型中的使用 XML Schema 进行验证中。现在,将这些属性视为您用来验证不使用它们的简单 XML 文件的"魔法咒语"。在学习更多关于命名空间的知识后,您将了解如何使用 XML Schema 来验证使用它们的复杂文档。这些想法在文档对象模型的使用多个命名空间进行验证中讨论。
您还可以在应用程序中指定模式文件,就像在SAXLocalNameCount中一样。
// ...
if (schemaSource != null) {
saxParser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaSource));
}
// ...
在上面的代码中,变量schemaSource与一个模式源文件相关联,您可以通过使用-xsdss选项启动SAXLocalNameCount应用程序并提供要使用的模式源文件的名称来指向它。
验证解析器中的错误处理
重要的是要认识到,当文件未通过验证时抛出异常的唯一原因是由于设置错误处理中显示的错误处理代码。该代码在此作为提醒再次显示:
// ...
public void warning(SAXParseException spe) throws SAXException {
out.println("Warning: " + getParseExceptionInfo(spe));
}
public void error(SAXParseException spe) throws SAXException {
String message = "Error: " + getParseExceptionInfo(spe);
throw new SAXException(message);
}
public void fatalError(SAXParseException spe) throws SAXException {
String message = "Fatal Error: " + getParseExceptionInfo(spe);
throw new SAXException(message);
}
// ...
如果这些异常没有被抛出,则验证错误将被简单地忽略。一般来说,SAX 解析错误是验证错误,尽管如果文件指定了解析器无法处理的 XML 版本,也会生成它。请记住,除非提供类似这里的错误处理程序,否则您的应用程序不会生成验证异常。
DTD 警告
如前所述,警告仅在 SAX 解析器处理 DTD 时生成。某些警告仅由验证解析器生成。非验证解析器的主要目标是尽可能快地运行,但它也会生成一些警告。
XML 规范建议由于以下原因生成警告:
-
为实体、属性或符号提供额外的声明。(这些声明将被忽略。仅使用第一个。此外,请注意,当验证时,元素的重复定义总是会产生致命错误,就像您之前看到的那样。)
-
引用未声明的元素类型。(仅当未声明的类型实际在 XML 文档中使用时才会发生有效性错误。当在 DTD 中引用未声明的元素时会产生警告。)
-
为未声明的元素类型声明属性。
Java XML SAX 解析器还在其他情况下发出警告:
-
在验证时不需要
<!DOCTYPE ...>。 -
在不进行验证时引用未定义的参数实体。(在验证时会产生错误。尽管非验证解析器不需要读取参数实体,但 Java XML 解析器会这样做。因为这不是一个要求,所以 Java XML 解析器生成警告,而不是错误。)
-
某些情况下,字符编码声明看起来不正确。
运行带验证的 SAX 解析器示例
在本节中,之前使用的SAXLocalNameCount示例程序将再次被使用,但这次将针对 XML Schema 或 DTD 进行验证。展示不同类型的验证的最佳方式是修改被解析的 XML 文件的代码,以及相关的模式和 DTD,以破坏处理并使应用程序生成异常。
尝试使用 DTD 验证错误进行实验
如上所述,这些示例重用了SAXLocalNameCount程序。您将在不进行验证运行 SAX 解析器示例中找到示例及其相关文件的位置。
-
如果尚未这样做,请将
SAXLocalNameCount.java文件保存在名为sax的目录中。在文本编辑器中打开文件并进行上述更改。 -
如果尚未这样做,请按以下方式编译文件:
javac sax/SAXLocalNameCount.java -
如果尚未这样做,请将示例 XML 文件
rich_iii.xml和two_gent.xml保存在data目录中。 -
运行带有 DTD 验证的
SAXLocalNameCount程序。要做到这一点,运行程序时必须指定
-dtd选项。java sax/SAXLocalNameCount -dtd data/rich_iii.xml您看到的结果将类似于这样:
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/rich_iii.xml Line=4: Document is invalid: no grammar found.此消息表示没有语法可以针对文档
rich_iii.xml进行验证,因此它自动无效。换句话说,该消息表示您正在尝试验证文档,但没有声明 DTD,因为没有DOCTYPE声明。因此,现在您知道 DTD 是有效文档的要求。这是有道理的。 -
将示例 DTD 文件
play.dtd保存在data目录中。 -
在文本编辑器中打开文件
data/rich_iii.xml。在data/rich_iii.xml的开头插入以下DOCTYPE声明。(该声明将验证解析器指向名为play.dtd的 DTD 文件。如果激活了 DTD 验证,则将检查正在解析的 XML 文件的结构是否符合play.dtd中提供的结构。)<!DOCTYPE PLAY SYSTEM "play.dtd">不要忘记保存修改,但保持文件打开,因为稍后会再次需要它。
-
返回
data/rich_iii.xml并修改第 18 行中角色"KING EDWARD The Fourth"的标记。将开始和结束标记从
<PERSONA>和</PERSONA>更改为<PERSON>和</PERSON>。现在第 18 行应该是这样的:18:<PERSON>KING EDWARD The Fourth</PERSON>再次,不要忘记保存修改,并保持文件打开。
-
运行带有 DTD 验证的
SAXLocalNameCount程序。这次,运行程序时将看到不同的错误:
java sax/SAXLocalNameCount -dtd data/rich_iii.xml Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/rich_iii.xml Line=26: Element type "PERSON" must be declared.在这里,您可以看到解析器反对的一个未包含在 DTD
data/play.dtd中的元素。 -
在
data/rich_iii.xml中更正"KING EDWARD The Fourth"的标记。将开始和结束标记恢复为原始版本,
<PERSONA>和</PERSONA>。 -
在
data/rich_iii.xml中,从第 16 行删除<TITLE>Dramatis Personae</TITLE>。再次,不要忘记保存修改。
-
运行带有 DTD 验证的
SAXLocalNameCount程序。与以前一样,您将看到另一个验证错误:
java sax/SAXLocalNameCount -dtd data/rich_iii.xml Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/rich_iii.xml Line=77: The content of element type "PERSONAE" must match "(TITLE,(PERSONA|PGROUP)+)".通过从第 16 行删除
<TITLE>元素,<PERSONAE>元素变得无效,因为它不包含 DTD 期望的<PERSONAE>元素的子元素。请注意,错误消息指出错误在data/rich_iii.xml的第 77 行,即使您从第 16 行删除了<TITLE>元素。这是因为<PERSONAE>元素的结束标记位于第 77 行,解析器只有在解析到元素结尾时才会抛出异常。 -
在文本编辑器中打开 DTD 文件
data/play.dtd。在 DTD 文件中,你可以看到
<PERSONAE>元素的声明,以及可以在符合剧本 DTD 的 XML 文档中使用的所有其他元素。<PERSONAE>的声明如下所示。<!ELEMENT PERSONAE (TITLE, (PERSONA | PGROUP)+)>正如你所看到的,
<PERSONAE>元素需要一个<TITLE>子元素。 管道 (|) 键表示<PERSONA>或<PGROUP>子元素可以包含在<PERSONAE>元素中,而在(PERSONA | PGROUP)分组后的加号 (+) 键表示必须包含至少一个或多个这些子元素中的任意一个。 -
在
<PERSONAE>的声明中,在TITLE后面添加一个问号 (?) 键。在 DTD 中给子元素的声明添加一个问号,使得该子元素的一个实例的存在是可选的。
<!ELEMENT PERSONAE (TITLE?, (PERSONA | PGROUP)+)>如果在元素后面添加星号 (
*),则可以包含零个或多个该子元素的实例。 但是,在这种情况下,在文档的某个部分中有多个标题是没有意义的。不要忘记保存对
data/play.dtd所做的修改。 -
运行
SAXLocalNameCount程序,并激活 DTD 验证。java sax/SAXLocalNameCount -dtd data/rich_iii.xml这次,您应该看到
SAXLocalNameCount的正确输出,没有错误。
实验模式验证错误
前面的练习演示了使用 SAXLocalNameCount 验证 XML 文件与 DTD 的过程。 在这个练习中,您将使用 SAXLocalNameCount 验证不同的 XML 文件,同时还要针对标准 XML Schema 定义和自定义模式源文件进行验证。 同样,通过修改 XML 文件和模式,使解析器抛出错误来演示这种类型的验证。
如上所述,这些示例重用了 SAXLocalNameCount 程序。 可以在 不进行验证运行 SAX 解析器示例 中找到示例及其相关文件的位置。
-
如果还没有这样做,请将
SAXLocalNameCount.java文件保存在名为sax的目录中。 在文本编辑器中打开文件并进行上述更改。 -
如果还没有这样做,请按照以下方式编译文件:
javac sax/SAXLocalNameCount.java -
将示例 XML 文件
personal-schema.xml保存在data目录中,然后在文本编辑器中打开它。这是一个简单的 XML 文件,提供了一个小公司员工的姓名和联系方式。 在这个 XML 文件中,你会看到它已经与一个模式定义文件
personal.xsd相关联。<personnel xsi:noNamespaceSchemaLocation='personal.xsd'> -
将示例 XSD Schema 文件
personal.xsd保存在data目录中,然后在文本编辑器中打开它。这个模式定义了关于每个员工所需的信息种类,以便将与模式关联的 XML 文档视为有效。例如,通过检查模式定义,你可以看到每个
person元素需要一个name,每个人的名字必须包括一个family名和一个given名。员工还可以选择性地拥有电子邮件地址和 URL。 -
在
data/personal.xsd中,将person元素所需的电子邮件地址最小数量从0更改为1。email元素的声明现在如下。<xs:element ref="email" minOccurs='1' maxOccurs='unbounded'/> -
在
data/personal-schema.xml中,从person元素one.worker中删除email元素。Worker One 现在看起来像这样:
<person id="one.worker"> <name><family>Worker</family> <given>One</given></name> <link manager="Big.Boss"/> </person> -
对
personal-schema.xml运行SAXLocalNameCount,不进行模式验证。java sax/SAXLocalNameCount data/personal-schema.xmlSAXLocalNameCount通知你每个元素在personal-schema.xml中出现的次数。Local Name "email" occurs 5 times Local Name "name" occurs 6 times Local Name "person" occurs 6 times Local Name "family" occurs 6 times Local Name "link" occurs 6 times Local Name "personnel" occurs 1 times Local Name "given" occurs 6 times你会发现
email只出现了五次,而personal-schema.xml中有六个person元素。因此,因为我们将email元素的最小出现次数设置为每个person元素为 1,我们知道这个文档是无效的。然而,因为SAXLocalNameCount没有被告知要根据模式验证,所以没有报告错误。 -
再次运行
SAXLocalNameCount,这次指定personal-schema.xml文档应该根据personal.xsd模式定义进行验证。正如你在上面看到的使用 XML 模式验证,
SAXLocalNameCount有一个选项可以启用模式验证。使用以下命令运行SAXLocalNameCount。java sax/SAXLocalNameCount -xsd data/personal-schema.xml这次,你将看到以下错误消息。
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/personal-schema.xml Line=14: cvc-complex-type.2.4.a: Invalid content was found starting with element 'link'. One of '{email}' is expected. -
将
email元素恢复到person元素one.worker。 -
第三次运行
SAXLocalNameCount,再次指定personal-schema.xml文档应该根据personal.xsd模式定义进行验证。java sax/SAXLocalNameCount -xsd data/personal-schema.xml这次你将看到正确的输出,没有错误。
-
再次在文本编辑器中打开
personal-schema.xml。 -
从
personnel元素中删除模式定义personal.xsd的声明。从
personnel元素中删除斜体代码。<personnel *xsi:noNamespaceSchemaLocation='personal.xsd'/*> -
再次运行
SAXLocalNameCount,再次指定模式验证。java sax/SAXLocalNameCount -xsd data/personal-schema.xml很明显,这不会起作用,因为尚未声明要验证 XML 文件的模式定义。你将看到以下错误。
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/personal-schema.xml Line=2: cvc-elt.1: Cannot find the declaration of element 'personnel'. -
再次运行
SAXLocalNameCount,这次在命令行传递模式定义文件。java sax/SAXLocalNameCount -xsdss data/personal.xsd data/personal-schema.xml这次你使用了允许指定不在应用程序中硬编码的模式定义的
SAXLocalNameCount选项。你应该看到正确的输出。
处理词法事件
到目前为止,您已经消化了许多 XML 概念,包括 DTD 和外部实体。您还学会了如何使用 SAX 解析器。本课程的其余部分涵盖了您只有在编写基于 SAX 的应用程序时才需要理解的高级主题。如果您的主要目标是编写基于 DOM 的应用程序,您可以直接跳转到文档对象模型。
您之前看到,如果您将文本写出为 XML,您需要知道是否处于 CDATA 部分中。如果是,则尖括号(<)和和号(&)应保持不变输出。但如果不在 CDATA 部分中,则应将它们替换为预定义的实体<和&。但是您如何知道自己是否在处理 CDATA 部分?
另一方面,如果您以某种方式过滤 XML,您希望传递注释。通常解析器会忽略注释。您如何获取注释以便可以回显它们?
本节回答了这些问题。它向您展示了如何使用org.xml.sax.ext.LexicalHandler来识别注释、CDATA 部分和对解析实体的引用。
注释、CDATA 标记和对解析实体的引用构成词法信息-即,涉及 XML 文本本身而不是 XML 信息内容的信息。当然,大多数应用程序只关注 XML 文档的内容。这些应用程序将不使用LexicalEventListener API。但是输出 XML 文本的应用程序会发现它非常有价值。
注意 - 词法事件处理是一个可选的解析器功能。解析器实现不需要支持它。(参考实现是这样的。)本讨论假定您的解析器支持它。
LexicalHandler的工作原理
要在 SAX 解析器看到词法信息时得到通知,您需要使用LexicalHandler配置解析器底层的XmlReader。LexicalHandler接口定义了以下事件处理方法。
comment(String comment)
将注释传递给应用程序。
startCDATA(), endCDATA()
告诉您 CDATA 部分何时开始和结束,这告诉您的应用程序下次调用characters()时可以期望什么样的字符。
startEntity(String name), endEntity(String name)
给出解析实体的名称。
startDTD(String name, String publicId, String systemId), endDTD()
告诉您正在处理 DTD,并标识它。
要激活词法处理程序,您的应用程序必须扩展DefaultHandler并实现LexicalHandler接口。然后,您必须配置您的XMLReader实例,使解析器委托给它,并配置它将词法事件发送到您的词法处理程序,如下所示。
// ...
SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler",
handler);
// ...
在这里,您可以使用XMLReader类中定义的setProperty()方法来配置XMLReader。作为 SAX 标准的一部分定义的属性名称是 URN,http://xml.org/sax/properties/lexical-handler。
最后,添加类似以下代码来定义将实现接口的适当方法。
// ...
public void warning(SAXParseException err) {
// ...
}
public void comment(char[] ch, int start, int length) throws SAXException {
// ...
}
public void startCDATA() throws SAXException {
// ...
}
public void endCDATA() throws SAXException {
// ...
}
public void startEntity(String name) throws SAXException {
// ...
}
public void endEntity(String name) throws SAXException {
// ...
}
public void startDTD(String name, String publicId, String systemId)
throws SAXException {
// ...
}
public void endDTD() throws SAXException {
// ...
}
private void echoText() {
// ...
}
// ...
这段代码将把您的解析应用程序转换为一个词法处理程序。剩下的就是为这些新方法中的每一个指定一个要执行的操作。
使用 DTDHandler 和 EntityResolver
本节介绍了另外两个 SAX 事件处理程序:DTDHandler和EntityResolver。当 DTD 遇到未解析的实体或符号声明时,将调用DTDHandler。当需要将 URN(公共 ID)解析为 URL(系统 ID)时,将使用EntityResolver。
DTDHandler API
选择解析器实现展示了引用包含二进制数据(如图像文件)的文件的方法,使用 MIME 数据类型。这是最简单、最可扩展的机制。但是,为了与旧的 SGML 样式数据兼容,也可以定义未解析的实体。
NDATA关键字定义了一个未解析的实体:
<!ENTITY myEntity SYSTEM "..URL.." NDATA gif>
NDATA关键字表示此实体中的数据不是可解析的 XML 数据,而是使用其他符号的数据。在本例中,符号被命名为gif。然后 DTD 必须包含该符号的声明,类似于以下内容。
<!NOTATION gif SYSTEM "..URL..">
当解析器看到未解析的实体或符号声明时,除了将其传递给应用程序使用DTDHandler接口外,它不会对信息做任何处理。该接口定义了两个方法。
-
notationDecl(String name, String publicId, String systemId) -
unparsedEntityDecl(String name, String publicId, String systemId, String notationName
notationDecl方法传递符号的名称和公共或系统标识符,或两者,取决于 DTD 中声明了哪个。unparsedEntityDecl方法传递实体的名称、适当的标识符和它使用的符号的名称。
注意 - DTDHandler接口由DefaultHandler类实现。
符号也可以用于属性声明。例如,以下声明需要 GIF 和 PNG 图像文件格式的符号。
<!ENTITY image EMPTY>
<!ATTLIST image ... type NOTATION (gif | png) "gif">
在这里,类型声明为 gif 或 png。如果没有指定,则默认为 gif。
无论符号引用用于描述未解析的实体还是属性,都由应用程序进行适当处理。解析器对符号的语义一无所知。它只传递声明。
EntityResolver API
EntityResolver API 允许您将公共 ID(URN)转换为系统 ID(URL)。例如,您的应用程序可能需要将类似href="urn:/someName"的内容转换为"http://someURL"。
EntityResolver接口定义了一个方法:
resolveEntity(String publicId, String systemId)
这种方法返回一个InputSource对象,可以用来访问实体的内容。将 URL 转换为InputSource很容易。但作为系统 ID 传递的 URL 很可能是原始文档的位置,而这个位置很可能在网络上的某个地方。要访问本地副本(如果有的话),必须在系统的某处维护一个目录,将名称(公共 ID)映射到本地 URL。
更多信息
以下链接提供了关于本课程中介绍的技术的进一步有用信息。
-
有关 SAX 标准的更多信息,请参见SAX 标准页面:
-
有关 StAX 拉解析器的更多信息,请参见:
Java 社区流程页面:
Elliot Rusty Harold 的介绍:
-
有关基于模式的验证机制的更多信息,请参见
W3C 标准验证机制,XML Schema:
RELAX NG 的基于正则表达式的验证机制:
Schematron 基于断言的验证机制:
课程:文档对象模型
这节课介绍了文档对象模型(DOM)。DOM 是一种标准的树结构,其中每个节点包含 XML 结构中的一个组件。最常见的节点类型是元素节点和文本节点。使用 DOM 函数可以创建节点,删除节点,更改它们的内容,并遍历节点层次结构。
这节课的示例演示了如何解析现有的 XML 文件以构建 DOM,显示和检查 DOM 层次结构,并探索命名空间的语法。它还展示了如何从头开始创建 DOM,并了解如何使用 Sun 的 JAXP 实现中的一些特定于实现的功能将现有数据集转换为 XML。
何时使用 DOM
文档对象模型标准首先是为文档(例如文章和书籍)设计的。此外,JAXP 1.4.2 实现支持 XML Schema,这对于任何特定应用程序都可能是一个重要考虑因素。
另一方面,如果您处理简单的数据结构,且 XML Schema 不是您计划的重要部分,那么您可能会发现更适合您目的的是 JDOM 或 dom4j 等更面向对象的标准之一。
从一开始,DOM 旨在是与语言无关的。由于它是为诸如 C 和 Perl 之类的语言设计的,DOM 并没有利用 Java 的面向对象特性。这一事实,加上文档和数据之间的区别,也有助于解释处理 DOM 与处理 JDOM 或 dom4j 结构之间的差异。
在本节中,我们将研究这些标准背后的模型之间的差异,以帮助您选择最适合您应用程序的标准。
文档与数据
DOM 中使用的文档模型与 JDOM 或 dom4j 中使用的数据模型之间的主要差异在于:
-
存在于层次结构中的节点类型
-
混合内容的能力
主要是数据层次结构中的“节点”构成的差异主要导致了使用这两种模型进行编程的差异。然而,与其他任何因素相比,混合内容的能力最能解释标准如何定义节点的差异。因此,我们首先来看一下 DOM 的混合内容模型。
混合内容模型
在 DOM 层次结构中,文本和元素可以自由混合。这种结构称为 DOM 模型中的混合内容。
文档中经常出现混合内容。例如,假设您想要表示这种结构:
<sentence>这是一个<bold>重要</bold>的想法。</sentence>
DOM 节点的层次结构可能如下所示,其中每行代表一个节点:
ELEMENT: sentence
+ TEXT: This is an
+ ELEMENT: bold
+ TEXT: important
+ TEXT: idea.
请注意,sentence 元素包含文本,然后是一个子元素,然后是额外的文本。文本和元素的混合定义了混合内容模型。
节点类型
为了提供混合内容的能力,DOM 节点本质上非常简单。在上述示例中,第一个元素的“内容”(其值)只是标识它是什么类型的节点。
第一次使用 DOM 的用户通常会被这个事实搞糊涂。在导航到<sentence>节点后,他们要求节点的“内容”,并期望得到一些有用的东西。相反,他们只能找到元素的名称,sentence。
注意 - DOM 节点 API 定义了nodeValue()、nodeType()和nodeName()方法。对于第一个元素节点,nodeName()返回sentence,而nodeValue()返回 null。对于第一个文本节点,nodeName()返回#text,而nodeValue()返回“This is an”。重要的一点是,元素的值与其内容不同。
在上面的例子中,询问“句子”的“文本”是什么意思?根据您的应用程序,以下任何一种都可能是合理的:
-
这是一个
-
这是一个想法。
-
这是一个重要的想法。
-
这是一个
重要 的想法。
一个更简单的模型
使用 DOM,您可以自由创建所需的语义。但是,您还需要进行必要的处理以实现这些语义。另一方面,像 JDOM 和 dom4j 这样的标准使得执行简单任务变得更容易,因为层次结构中的每个节点都是一个对象。
尽管 JDOM 和 dom4j 允许元素具有混合内容,但它们并非主要设计用于这种情况。相反,它们针对的是 XML 结构包含数据的应用程序。
数据结构中的元素通常只包含文本或其他元素,而不是两者兼有。例如,这里是代表简单地址簿的一些 XML:
<addressbook>
<entry>
<name>Fred</name>
<email>fred@home</email>
</entry>
...
</addressbook>
注意 - 对于像这样非常简单的 XML 数据结构,您还可以使用内置在 Java 平台 1.4 版本中的正则表达式包(java.util.regex)。
在 JDOM 和 dom4j 中,当您导航到包含文本的元素后,您可以调用诸如text()之类的方法来获取其内容。但是,在处理 DOM 时,您必须检查子元素列表以“组合”节点的文本,就像您之前看到的那样 - 即使该列表只包含一个项目(TEXT 节点)。
因此,对于简单的数据结构,比如地址簿,您可以通过使用 JDOM 或 dom4j 来节省一些工作量。即使数据在技术上是“混合的”,但在给定节点中始终只有一个(且仅有一个)文本段落时,使用其中一个模型可能是有意义的。
这是一个这种结构的示例,也可以很容易地在 JDOM 或 dom4j 中处理:
<addressbook>
<entry>Fred
<email>fred@home</email>
</entry>
...
</addressbook>
在这里,每个条目都有一些标识性文本,后面跟着其他元素。有了这种结构,程序可以导航到一个条目,调用text()来找出它属于谁,并在正确的节点处处理<email>子元素。
增加复杂性
但是,为了全面了解在搜索或操作 DOM 时需要执行的处理类型,了解 DOM 可能包含的节点类型是很重要的。
这里有一个说明这一点的示例。这是这些数据的表示:
<sentence>
The &projectName; <![CDATA[<i>project</i>]]> is
<?editor: red><bold>important</bold><?editor: normal>.
</sentence>
这个句子包含一个实体引用 - 指向在其他地方定义的实体的指针。在这种情况下,实体包含项目的名称。示例还包含一个 CDATA 部分(未解释的数据,类似于 HTML 中的 <pre> 数据)以及处理指令(<?...?>),在这种情况下告诉编辑器在呈现文本时使用的颜色。
这是该数据的 DOM 结构。它代表了一个健壮应用程序应该准备处理的结构类型:
+ ELEMENT: sentence
+ TEXT: The
+ ENTITY REF: projectName
+ COMMENT:
The latest name we are using
+ TEXT: Eagle
+ CDATA: <i>project</i>
+ TEXT: is
+ PI: editor: red
+ ELEMENT: bold
+ TEXT: important
+ PI: editor: normal
这个例子描述了 DOM 中可能出现的节点类型。尽管你的应用程序可能大部分时间都能忽略它们,但一个真正健壮的实现需要识别和处理每一个节点。
类似地,导航到一个节点的过程涉及处理子元素,忽略你不感兴趣的元素并检查你感兴趣的元素,直到找到你感兴趣的节点。
一个处理固定、内部生成数据的程序可以承担简化假设:处理指令、注释、CDATA 节点和实体引用在数据结构中不存在。但是真正健壮的应用程序,尤其是处理来自外部世界的各种数据的应用程序,必须准备处理所有可能的 XML 实体。
(一个“简单”的应用程序只能在输入数据包含它所期望的简化 XML 结构时工作。但是没有验证机制来确保更复杂的结构不存在。毕竟,XML 的设计目的就是允许它们存在。)
为了更加健壮,DOM 应用程序必须做到以下几点:
-
在搜索元素时:
-
忽略注释、属性和处理指令。
-
允许子元素不按预期顺序出现的可能性。
-
如果不进行验证,则跳过包含可忽略空格的 TEXT 节点。
-
-
在提取节点的文本时:
-
从 CDATA 节点以及文本节点提取文本。
-
在收集文本时忽略注释、属性和处理指令。
-
如果遇到实体引用节点或另一个元素节点,则递归(即对所有子节点应用文本提取过程)。
-
当然,许多应用程序不必担心这些事情,因为它们看到的数据类型将受到严格控制。但如果数据可能来自各种外部来源,那么应用程序可能需要考虑这些可能性。
执行这些功能所需的代码在本课程的末尾的 搜索节点 和 获取节点内容 中给出。现在,目标只是确定 DOM 是否适合你的应用程序。
选择你的模型
正如您所见,当您使用 DOM 时,即使是从节点获取文本这样的简单操作也需要一些编程。因此,如果您的程序处理简单的数据结构,那么 JDOM、dom4j,甚至 1.4 版本的正则表达式包(java.util.regex)可能更适合您的需求。
另一方面,对于完整的文档和复杂的应用程序,DOM 为您提供了很大的灵活性。如果需要使用 XML Schema,那么再次选择 DOM 是明智之举 - 至少目前是这样。
如果您在开发的应用程序中处理文档和数据,那么 DOM 可能仍然是您最佳选择。毕竟,一旦编写了用于检查和处理 DOM 结构的代码,就很容易为特定目的定制它。因此,选择在 DOM 中执行所有操作意味着您只需处理一组 API,而不是两组。
此外,DOM 标准是内存中文档模型的规范标准。它功能强大且稳健,并且有许多实现。这对许多大型安装来说是一个重要的决策因素,特别是对于需要尽量减少由 API 更改造成的成本的大型应用程序。
最后,即使通讯录中的文本今天可能不允许粗体、斜体、颜色和字体大小,但将来您可能会希望处理这些内容。因为 DOM 能处理几乎任何您提出的要求,选择 DOM 可以更轻松地使您的应用程序具备未来的可扩展性。
将 XML 数据读入 DOM
在本节中,您将通过读取现有的 XML 文件构造一个文档对象模型。
注意 - 在可扩展样式表语言转换中,您将看到如何将 DOM 写出为 XML 文件。(您还将看到如何相对容易地将现有数据文件转换为 XML。)
创建程序
文档对象模型提供了让您创建、修改、删除和重新排列节点的 API。在尝试创建 DOM 之前,了解 DOM 的结构是很有帮助的。这一系列示例将通过一个名为DOMEcho的示例程序展示 DOM 的内部结构,您可以在安装了 JAXP API 后在目录*INSTALL_DIR*/jaxp-*version*/samples/dom中找到它。
创建骨架
首先,构建一个简单的程序,将 XML 文档读入 DOM,然后再将其写回。
从应用程序的正常基本逻辑开始,并检查确保命令行上已提供了参数:
public class DOMEcho {
static final String outputEncoding = "UTF-8";
private static void usage() {
// ...
}
public static void main(String[] args) throws Exception {
String filename = null;
for (int i = 0; i < args.length; i++) {
if (...) {
// ...
}
else {
filename = args[i];
if (i != args.length - 1) {
usage();
}
}
}
if (filename == null) {
usage();
}
}
}
此代码执行所有基本的设置操作。DOMEcho的所有输出都使用 UTF-8 编码。如果未指定参数,则调用usage()方法会简单地告诉您DOMEcho期望的参数,因此此处不显示代码。还声明了一个filename字符串,它将是要由DOMEcho解析为 DOM 的 XML 文件的名称。
导入所需的类
在本节中,所有类都以单独命名,以便您可以看到每个类来自何处,以便在需要引用 API 文档时参考。在示例文件中,导入语句使用较短的形式,如javax.xml.parsers.*。
这些是DOMEcho使用的 JAXP API:
package dom;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
这些类用于在解析 XML 文档时可能抛出的异常:
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.*
这些类读取示例 XML 文件并管理输出:
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
最后,导入 W3C 定义的 DOM、DOM 异常、实体和节点:
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
处理错误
接下来,添加错误处理逻辑。最重要的一点是,当 JAXP 符合标准的文档构建器在解析 XML 文档时遇到问题时,需要报告 SAX 异常。DOM 解析器实际上不必在内部使用 SAX 解析器,但由于 SAX 标准已经存在,因此使用它来报告错误是有意义的。因此,DOM 应用程序的错误处理代码与 SAX 应用程序的错误处理代码非常相似:
private static class MyErrorHandler implements ErrorHandler {
private PrintWriter out;
MyErrorHandler(PrintWriter out) {
this.out = out;
}
private String getParseExceptionInfo(SAXParseException spe) {
String systemId = spe.getSystemId();
if (systemId == null) {
systemId = "null";
}
String info = "URI=" + systemId + " Line=" + spe.getLineNumber() +
": " + spe.getMessage();
return info;
}
public void warning(SAXParseException spe) throws SAXException {
out.println("Warning: " + getParseExceptionInfo(spe));
}
public void error(SAXParseException spe) throws SAXException {
String message = "Error: " + getParseExceptionInfo(spe);
throw new SAXException(message);
}
public void fatalError(SAXParseException spe) throws SAXException {
String message = "Fatal Error: " + getParseExceptionInfo(spe);
throw new SAXException(message);
}
}
正如您所看到的,DomEcho类的错误处理程序使用PrintWriter实例生成其输出。
实例化工厂
接下来,在main()方法中添加以下代码,以获取一个可以提供文档构建器的工厂实例。
public static void main(String[] args) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// ...
}
获取解析器并解析文件
现在,在main()中添加以下代码以获取一个构建器实例,并使用它来解析指定的文件。
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new File(filename));
被解析的文件由在 main() 方法开头声明的 filename 变量提供,当程序运行时,它作为参数传递给 DOMEcho。
配置工厂
默认情况下,工厂返回一个不进行验证的解析器,不了解命名空间。要获得一个验证解析器,或者一个了解命名空间的解析器(或两者兼有),您可以配置工厂来设置这两个选项中的一个或两个,使用以下代码。
public static void main(String[] args) throws Exception {
String filename = null;
boolean dtdValidate = false;
boolean xsdValidate = false;
String schemaSource = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-dtd")) {
dtdValidate = true;
}
else if (args[i].equals("-xsd")) {
xsdValidate = true;
}
else if (args[i].equals("-xsdss")) {
if (i == args.length - 1) {
usage();
}
xsdValidate = true;
schemaSource = args[++i];
}
else {
filename = args[i];
if (i != args.length - 1) {
usage();
}
}
}
if (filename == null) {
usage();
}
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setValidating(dtdValidate || xsdValidate);
// ...
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new File(filename));
}
如您所见,命令行参数已设置好,以便您可以通知 DOMEcho 对 DTD 或 XML Schema 执行验证,并且工厂已配置为了解命名空间并执行用户指定的验证类型。
注意 - 符合 JAXP 标准的解析器并不需要支持所有这些选项的所有组合,即使参考解析器支持。如果您指定了无效的选项组合,在尝试获取解析器实例时,工厂会生成一个 ParserConfigurationException。
有关如何使用命名空间和验证的更多信息,请参阅使用 XML Schema 进行验证,其中将描述上述摘录中缺失的代码。
处理验证错误
根据 SAX 标准规定,对验证错误的默认响应是不执行任何操作。JAXP 标准要求抛出 SAX 异常,因此您使用与 SAX 应用程序相同的错误处理机制。特别是,您使用 DocumentBuilder 类的 setErrorHandler 方法来提供一个实现 SAX ErrorHandler 接口的对象。
注意 - DocumentBuilder 还有一个 setEntityResolver 方法可供使用。
以下代码配置文档构建器使用在处理错误中定义的错误处理程序。
DocumentBuilder db = dbf.newDocumentBuilder();
OutputStreamWriter errorWriter = new OutputStreamWriter(System.err,
outputEncoding);
db.setErrorHandler(new MyErrorHandler (new PrintWriter(errorWriter, true)));
Document doc = db.parse(new File(filename));
到目前为止,您看到的代码已经设置了文档构建器,并配置它在请求时执行验证。错误处理也已就位。然而,DOMEcho 还没有做任何事情。在下一节中,您将看到如何显示 DOM 结构并开始探索它。例如,您将看到在 DOM 中实体引用和 CDATA 部分的样子。也许最重要的是,您将看到文本节点(包含实际数据)如何存在于 DOM 中的元素节点下。
显示 DOM 节点
要创建或操作 DOM,有一个清晰的关于 DOM 中节点结构的概念是很有帮助的。本教程的这一部分揭示了 DOM 的内部结构,这样你就可以看到它包含的内容。DOMEcho 示例通过回显 DOM 节点,然后在屏幕上打印出来,适当缩进以使节点层次结构明显可见。这些节点类型的规范可以在DOM Level 2 Core Specification中找到,在Node规范下。下面的表 3-1 是从该规范中调整过来的。
表 3-1 节点类型
| Node | 节点名称 | 节点值 | 属性 |
|---|---|---|---|
Attr |
属性名称 | 属性值 | null |
CDATASection |
#cdata-section |
CDATA 部分的内容 | null |
Comment |
#comment |
注释的内容 | null |
Document |
#document |
null | null |
DocumentFragment |
#documentFragment |
null | null |
DocumentType |
文档类型名称 | null | null |
Element |
标签名称 | null | null |
Entity |
实体名称 | null | null |
EntityReference |
引用的实体名称 | null | null |
Notation |
符号名称 | null | null |
ProcessingInstruction |
目标 | 不包括目标的整个内容 | null |
Text |
#text |
文本节点的内容 | null |
此表中的信息非常有用;在处理 DOM 时,你将需要它,因为所有这些类型都混合在 DOM 树中。
获取节点类型信息
通过调用org.w3c.dom.Node类的各种方法来获取 DOM 节点元素类型信息。DOMEcho暴露的节点属性由以下代码回显。
private void printlnCommon(Node n) {
out.print(" nodeName=\"" + n.getNodeName() + "\"");
String val = n.getNamespaceURI();
if (val != null) {
out.print(" uri=\"" + val + "\"");
}
val = n.getPrefix();
if (val != null) {
out.print(" pre=\"" + val + "\"");
}
val = n.getLocalName();
if (val != null) {
out.print(" local=\"" + val + "\"");
}
val = n.getNodeValue();
if (val != null) {
out.print(" nodeValue=");
if (val.trim().equals("")) {
// Whitespace
out.print("[WS]");
}
else {
out.print("\"" + n.getNodeValue() + "\"");
}
}
out.println();
}
每个 DOM 节点至少有一个类型、一个名称和一个值,这个值可能为空也可能不为空。在上面的示例中,Node接口的getNamespaceURI()、getPrefix()、getLocalName()和getNodeValue()方法返回并打印回显节点的命名空间 URI、命名空间前缀、本地限定名称和值。请注意,对getNodeValue()返回的值调用trim()方法,以确定节点的值是否为空白字符,并相应地打印消息。
要查看Node方法的完整列表以及它们返回的不同信息,请参阅Node的 API 文档。
接下来,定义一个方法来设置节点打印时的缩进,以便节点层次结构能够清晰可见。
private void outputIndentation() {
for (int i = 0; i < indent; i++) {
out.print(basicIndent);
}
}
当DOMEcho显示节点树层次结构时,使用的基本缩进单位由DOMEcho构造函数类中添加以下突出显示的行来定义basicIndent常量。
public class DOMEcho {
static final String outputEncoding = "UTF-8";
private PrintWriter out;
private int indent = 0;
private final String basicIndent = " ";
DOMEcho(PrintWriter out) {
this.out = out;
}
}
就像在处理错误中定义的错误处理程序一样,DOMEcho 程序将创建其输出作为 PrintWriter 实例。
词法控制
词法信息是您需要重建 XML 文档原始语法的信息。在编辑应用程序中保留词法信息非常重要,因为您希望保存的文档是对原始文档的准确反映-包括注释、实体引用以及一开始可能包含的任何 CDATA 部分。
然而,大多数应用程序只关注 XML 结构的内容。它们可以忽略注释,并且不在乎数据是在 CDATA 部分中编码还是作为纯文本,或者是否包含实体引用。对于这类应用程序,最好保留最少的词法信息,因为这简化了应用程序必须准备检查的 DOM 节点的数量和类型。
以下DocumentBuilderFactory方法让您控制在 DOM 中看到的词法信息。
setCoalescing()
将CDATA节点转换为Text节点并附加到相邻的Text节点(如果有)。
setExpandEntityReferences()
为了扩展实体引用节点。
setIgnoringComments()
忽略注释。
setIgnoringElementContentWhitespace()
忽略不是元素内容的空白。
所有这些属性的默认值都是 false,这保留了重建传入文档所需的所有词法信息,以其原始形式。将它们设置为 true 可以构建最简单的 DOM,以便应用程序可以专注于数据的语义内容,而不必担心词法语法细节。表 3-2 总结了设置的效果。
表 3-2 词法控制设置
| API | 保留词法信息 | 关注内容 |
|---|---|---|
setCoalescing() |
False | True |
setExpandEntityReferences() |
False | True |
setIgnoringComments() |
False | True |
setIgnoringElementContent``Whitespace() |
False | True |
这些方法在DomEcho示例的主方法中的实现如下所示。
// ...
dbf.setIgnoringComments(ignoreComments);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);
// ...
布尔变量ignoreComments、ignoreWhitespace、putCDATAIntoText和createEntityRefs在主方法代码的开头声明,并且当运行DomEcho时通过命令行参数设置。
public static void main(String[] args) throws Exception {
// ...
boolean ignoreWhitespace = false;
boolean ignoreComments = false;
boolean putCDATAIntoText = false;
boolean createEntityRefs = false;
for (int i = 0; i < args.length; i++) {
if (...) { // Validation arguments here
// ...
}
else if (args[i].equals("-ws")) {
ignoreWhitespace = true;
}
else if (args[i].startsWith("-co")) {
ignoreComments = true;
}
else if (args[i].startsWith("-cd")) {
putCDATAIntoText = true;
}
else if (args[i].startsWith("-e")) {
createEntityRefs = true;
// ...
}
else {
filename = args[i];
// Must be last arg
if (i != args.length - 1) {
usage();
}
}
}
// ...
}
打印 DOM 树节点
DomEcho应用程序允许您查看 DOM 的结构,并演示了 DOM 由哪些节点组成以及它们是如何排列的。一般来说,DOM 树中绝大多数节点将是Element和Text节点。
注意 - 文本节点存在于 DOM 中的元素节点下方,数据始终存储在文本节点中。在 DOM 处理中最常见的错误可能是导航到元素节点并期望它包含存储在该元素中的数据。事实并非如此!即使是最简单的元素节点下面也有一个包含数据的文本节点。
打印 DOM 树节点的代码以适当的缩进显示如下。
private void echo(Node n) {
outputIndentation();
int type = n.getNodeType();
switch (type) {
case Node.ATTRIBUTE_NODE:
out.print("ATTR:");
printlnCommon(n);
break;
case Node.CDATA_SECTION_NODE:
out.print("CDATA:");
printlnCommon(n);
break;
case Node.COMMENT_NODE:
out.print("COMM:");
printlnCommon(n);
break;
case Node.DOCUMENT_FRAGMENT_NODE:
out.print("DOC_FRAG:");
printlnCommon(n);
break;
case Node.DOCUMENT_NODE:
out.print("DOC:");
printlnCommon(n);
break;
case Node.DOCUMENT_TYPE_NODE:
out.print("DOC_TYPE:");
printlnCommon(n);
NamedNodeMap nodeMap = ((DocumentType)n).getEntities();
indent += 2;
for (int i = 0; i < nodeMap.getLength(); i++) {
Entity entity = (Entity)nodeMap.item(i);
echo(entity);
}
indent -= 2;
break;
case Node.ELEMENT_NODE:
out.print("ELEM:");
printlnCommon(n);
NamedNodeMap atts = n.getAttributes();
indent += 2;
for (int i = 0; i < atts.getLength(); i++) {
Node att = atts.item(i);
echo(att);
}
indent -= 2;
break;
case Node.ENTITY_NODE:
out.print("ENT:");
printlnCommon(n);
break;
case Node.ENTITY_REFERENCE_NODE:
out.print("ENT_REF:");
printlnCommon(n);
break;
case Node.NOTATION_NODE:
out.print("NOTATION:");
printlnCommon(n);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
out.print("PROC_INST:");
printlnCommon(n);
break;
case Node.TEXT_NODE:
out.print("TEXT:");
printlnCommon(n);
break;
default:
out.print("UNSUPPORTED NODE: " + type);
printlnCommon(n);
break;
}
indent++;
for (Node child = n.getFirstChild(); child != null;
child = child.getNextSibling()) {
echo(child);
}
indent--;
}
该代码首先使用 switch 语句打印出不同的节点类型和任何可能的子节点,并进行适当的缩进。
节点属性不包括在 DOM 层次结构的子节点中。而是通过Node接口的getAttributes方法获取。
DocType接口是w3c.org.dom.Node的扩展。它定义了getEntities方法,您可以使用该方法获取Entity节点 - 定义实体的节点。与Attribute节点一样,Entity节点不会出现为 DOM 节点的子节点。
节点操作
本节简要介绍了您可能想要应用于 DOM 的一些操作。
-
创建节点
-
遍历节点
-
搜索节点
-
获取节点内容
-
创建属性
-
删除和更改节点
-
插入节点
创建节点
您可以使用Document接口的方法创建不同类型的节点。例如,createElement、createComment、createCDATAsection、createTextNode等。有关创建不同节点的方法的完整列表,请参阅org.w3c.dom.Document的 API 文档。
遍历节点
org.w3c.dom.Node接口定义了一些方法,您可以使用这些方法遍历节点,包括getFirstChild、getLastChild、getNextSibling、getPreviousSibling和getParentNode。这些操作足以从树中的任何位置到达树中的任何其他位置。
搜索节点
当您搜索具有特定名称的节点时,需要考虑更多因素。虽然诱人的做法是获取第一个子节点并检查它是否正确,但搜索必须考虑到子列表中的第一个子节点可能是注释或处理指令。如果 XML 数据尚未经过验证,甚至可能是包含可忽略空格的文本节点。
本质上,您需要查看子节点列表,忽略那些不相关的节点,并检查您关心的节点。以下是在 DOM 层次结构中搜索节点时需要编写的一种例程。它在这里完整呈现(包括注释),以便您可以将其用作应用程序中的模板。
/**
* Find the named subnode in a node's sublist.
* <ul>
* <li>Ignores comments and processing instructions.
* <li>Ignores TEXT nodes (likely to exist and contain
* ignorable whitespace, if not validating.
* <li>Ignores CDATA nodes and EntityRef nodes.
* <li>Examines element nodes to find one with
* the specified name.
* </ul>
* @param name the tag name for the element to find
* @param node the element node to start searching from
* @return the Node found
*/
public Node findSubNode(String name, Node node) {
if (node.getNodeType() != Node.ELEMENT_NODE) {
System.err.println("Error: Search node not of element type");
System.exit(22);
}
if (! node.hasChildNodes()) return null;
NodeList list = node.getChildNodes();
for (int i=0; i < list.getLength(); i++) {
Node subnode = list.item(i);
if (subnode.getNodeType() == Node.ELEMENT_NODE) {
if (subnode.getNodeName().equals(name))
return subnode;
}
}
return null;
}
要深入了解此代码,请参阅增加复杂性中的何时使用 DOM。此外,您还可以使用词法控制中描述的 API 来修改解析器构造的 DOM 类型。不过,这段代码的好处是几乎适用于任何 DOM。
获取节点内容
当您想要获取节点包含的文本时,您需要再次查看子节点列表,忽略不相关的条目,并在TEXT节点、CDATA节点和EntityRef节点中找到的文本累积起来。以下是您可以用于该过程的一种例程。
/**
* Return the text that a node contains. This routine:
* <ul>
* <li>Ignores comments and processing instructions.
* <li>Concatenates TEXT nodes, CDATA nodes, and the results of
* recursively processing EntityRef nodes.
* <li>Ignores any element nodes in the sublist.
* (Other possible options are to recurse into element
* sublists or throw an exception.)
* </ul>
* @param node a DOM node
* @return a String representing its contents
*/
public String getText(Node node) {
StringBuffer result = new StringBuffer();
if (! node.hasChildNodes()) return "";
NodeList list = node.getChildNodes();
for (int i=0; i < list.getLength(); i++) {
Node subnode = list.item(i);
if (subnode.getNodeType() == Node.TEXT_NODE) {
result.append(subnode.getNodeValue());
}
else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) {
result.append(subnode.getNodeValue());
}
else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
// Recurse into the subtree for text
// (and ignore comments)
result.append(getText(subnode));
}
}
return result.toString();
}
关于这段代码的更深入解释,请参见增加复杂性中的何时使用 DOM。同样,你可以通过使用词法控制中描述的 API 来简化这段代码,以修改解析器构造的 DOM 类型。但这段代码的好处是几乎适用于任何 DOM。
创建属性
扩展了 Node 接口的org.w3c.dom.Element接口定义了一个setAttribute操作,用于向该节点添加属性。(从 Java 平台的角度来看,更好的名称应该是addAttribute。该属性不是类的属性,而是创建了一个新对象。)你还可以使用Document的createAttribute操作来创建Attribute的实例,然后使用setAttributeNode方法来添加它。
删除和更改节点
要删除一个节点,你可以使用其父节点的removeChild方法。要更改它,你可以使用父节点的replaceChild操作或节点的setNodeValue操作。
插入节点
在创建新节点时要记住的重要事情是,当你创建一个元素节点时,你只需指定一个名称。实际上,该节点给你提供了一个挂载物件的钩子。你可以通过向其子节点列表添加内容来将物件挂在钩子上。例如,你可以添加一个文本节点、一个CDATA节点或一个属性节点。在构建过程中,请记住你在本教程中看到的结构。记住:层次结构中的每个节点都非常简单,只包含一个数据元素。
运行DOMEcho示例
要运行DOMEcho示例,请按照以下步骤操作。
-
导航至
samples目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples. -
编译示例类。
% javac dom/* -
在 XML 文件上运行
DOMEcho程序。选择
data目录中的一个 XML 文件,并在其上运行DOMEcho程序。在这里,我们选择在文件personal-schema.xml上运行该程序。% java dom/DOMEcho data/personal-schema.xmlXML 文件
personal-schema.xml包含了一个小公司的人员档案。当你在其上运行DOMEcho程序时,你应该看到以下输出。DOC: nodeName="#document" ELEM: nodeName="personnel" local="personnel" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="person" local="person" ATTR: nodeName="id" local="id" nodeValue="Big.Boss" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="name" local="name" ELEM: nodeName="family" local="family" TEXT: nodeName="#text" nodeValue="Boss" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="given" local="given" TEXT: nodeName="#text" nodeValue="Big" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="email" local="email" TEXT: nodeName="#text" nodeValue="chief@foo.example.com" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="link" local="link" ATTR: nodeName="subordinates" local="subordinates" nodeValue="one.worker two.worker three.worker four.worker five.worker" TEXT: nodeName="#text" nodeValue=[WS] TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="person" local="person" ATTR: nodeName="id" local="id" nodeValue="one.worker" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="name" local="name" ELEM: nodeName="family" local="family" TEXT: nodeName="#text" nodeValue="Worker" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="given" local="given" TEXT: nodeName="#text" nodeValue="One" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="email" local="email" TEXT: nodeName="#text" nodeValue="one@foo.example.com" TEXT: nodeName="#text" nodeValue=[WS] ELEM: nodeName="link" local="link" ATTR: nodeName="manager" local="manager" nodeValue="Big.Boss" TEXT: nodeName="#text" nodeValue=[WS] [...]正如你所看到的,
DOMEcho打印出文档中不同元素的所有节点,并正确缩进以显示节点层次结构。
使用 XML Schema 进行验证
本节介绍了 XML Schema 验证的过程。虽然完整讨论 XML Schema 超出了本教程的范围,但本节向您展示了使用 XML Schema 定义验证 XML 文档的步骤。(要了解更多关于 XML Schema 的信息,您可以查看在线教程,XML Schema Part 0: Primer。在本节结束时,您还将学习如何使用 XML Schema 定义来验证包含来自多个命名空间的元素的文档。
验证过程概述
要在 XML 文档中通知验证错误,必须满足以下条件:
-
必须配置工厂,并设置适当的错误处理程序。
-
文档必须与至少一个模式相关联,可能还有更多。
配置DocumentBuilder工厂
在配置工厂时,首先定义将在其中使用的常量是很有帮助的。这些常量与在使用 XML Schema 进行 SAX 解析时定义的常量相同,并且它们在DOMEcho示例程序的开头声明。
static final String JAXP_SCHEMA_LANGUAGE =
"http://java.sun.com/xml/jaxp/properties/schemaLanguage";
static final String W3C_XML_SCHEMA =
"http://www.w3.org/2001/XMLSchema";
接下来,您配置DocumentBuilderFactory以生成一个支持命名空间的、使用 XML Schema 的验证解析器。这是通过在已创建的DocumentBuilderFactory实例dbf上调用setValidating方法来完成的,该实例是在实例化工厂中创建的。
// ...
dbf.setNamespaceAware(true);
dbf.setValidating(dtdValidate || xsdValidate);
if (xsdValidate) {
try {
dbf.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
}
catch (IllegalArgumentException x) {
System.err.println("Error: JAXP DocumentBuilderFactory attribute "
+ "not recognized: " + JAXP_SCHEMA_LANGUAGE);
System.err.println("Check to see if parser conforms to JAXP spec.");
System.exit(1);
}
}
// ...
因为符合 JAXP 的解析器默认情况下不支持命名空间,所以必须设置模式验证属性才能正常工作。您还设置了一个工厂属性来指定要使用的解析器语言。(另一方面,对于 SAX 解析,您在工厂生成的解析器上设置一个属性)。
将文档与模式关联
现在程序已准备好使用 XML Schema 定义进行验证,只需确保 XML 文档与(至少)一个相关联即可。有两种方法可以实现这一点:
-
在 XML 文档中有模式声明时
-
通过指定应用程序中要使用的模式
注意 - 当应用程序指定要使用的模式时,它会覆盖文档中的任何模式声明。
要在文档中指定模式定义,您可以创建如下 XML:
<*documentRoot* xsi:noNamespaceSchemaLocation='*YourSchemaDefinition.xsd*'> [...]
第一个属性定义了 XML 命名空间(xmlns)前缀xsi,代表“XML Schema 实例”。第二行指定了要用于文档中没有命名空间前缀的元素的模式-也就是,通常在任何简单、不复杂的 XML 文档中定义的元素。(您将看到如何处理多个命名空间在下一节中。)
您还可以在应用程序中指定模式文件,这是DOMEcho的情况。
static final String JAXP_SCHEMA_SOURCE =
"http://java.sun.com/xml/jaxp/properties/schemaSource";
// ...
dbf.setValidating(dtdValidate || xsdValidate);
if (xsdValidate) {
// ...
}
if (schemaSource != null) {
dbf.setAttribute(JAXP_SCHEMA_SOURCE, new File(schemaSource));
}
在这里,您也有机制可以指定多个模式。我们将在下面看一下。
使用多个命名空间进行验证
命名空间允许您在同一文档中组合服务不同目的的元素,而无需担心重叠的名称。
注意 - 本节讨论的材料也适用于使用 SAX 解析器进行验证。您在这里看到它,是因为您已经学到足够多关于命名空间的知识,才能理解讨论的内容。
举个例子,考虑一个 XML 数据集,用于跟踪人员数据。数据集可能包括来自税务申报表和雇员入职表的信息,两个元素在各自的模式中都命名为form。
如果为税务命名空间定义了一个前缀,并为雇佣命名空间定义了另一个前缀,则人员数据可能包括以下部分。
<employee id="...">
<name>....</name>
<tax:form>
...w2 tax form data...
</tax:form>
<hiring:form>
...employment history, etc....
</hiring:form>
</employee>
tax:form元素的内容显然与hiring:form元素的内容不同,并且必须进行不同的验证。
还要注意,在此示例中存在一个默认命名空间,未限定元素名称employee和name属于该命名空间。为了使文档得到正确验证,必须声明该命名空间的模式,以及tax和hiring命名空间的模式。
注意 - 默认命名空间实际上是一个特定的命名空间。它被定义为“没有名称的命名空间”。因此,您不能简单地将一个命名空间用作本周的默认命名空间,然后将另一个命名空间用作以后的默认命名空间。这个“无名命名空间”(或“空命名空间”)就像数字零一样。它没有任何值(没有名称),但它仍然被精确定义。因此,具有名称的命名空间永远不能用作默认命名空间。
解析时,只要已声明这些模式,数据集中的每个元素都将根据相应的模式进行验证。同样,这些模式可以作为 XML 数据集的一部分或在程序中声明。(也可以混合声明。总的来说,最好将所有声明放在一起。)
在 XML 数据集中声明模式
要声明用于上述示例中数据集的模式,XML 代码将类似于以下内容。
<documentRoot
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation=
"employeeDatabase.xsd"
xsi:schemaLocation=
"http://www.irs.gov.example.com/
fullpath/w2TaxForm.xsd
http://www.ourcompany.example.com/
relpath/hiringForm.xsd"
xmlns:tax=
"http://www.irs.gov.example.com/"
xmlns:hiring=
"http://www.ourcompany.example.com/"
>
noNamespaceSchemaLocation声明是您之前见过的内容,最后两个条目也是如此,它们定义了命名空间前缀tax和hiring。新的是中间的条目,它定义了文档中引用的每个命名空间要使用的模式的位置。
xsi:schemaLocation声明由条目对组成,其中每对中的第一个条目是指定命名空间的完全限定 URI,第二个条目包含模式定义的完整路径或相对路径。一般来说,建议使用完全限定路径。这样,模式只会存在一份副本。
请注意,在定义模式位置时不能使用命名空间前缀。xsi:schemaLocation声明只能理解命名空间名称,而不能理解前缀。
在应用程序中声明模式
要在应用程序中声明等效的模式,代码看起来类似于以下内容。
static final String employeeSchema = "employeeDatabase.xsd";
static final String taxSchema = "w2TaxForm.xsd";
static final String hiringSchema = "hiringForm.xsd";
static final String[] schemas = {
employeeSchema,
taxSchema,
hiringSchema,
};
static final String JAXP_SCHEMA_SOURCE =
"http://java.sun.com/xml/jaxp/properties/schemaSource";
// ...
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance()
// ...
factory.setAttribute(JAXP_SCHEMA_SOURCE, schemas);
在这里,指向模式定义(.xsd文件)的字符串数组作为参数传递给factory.setAttribute方法。请注意与声明要用作 XML 数据集一部分时的区别。
-
默认(未命名)模式没有特殊声明。
-
你不需要指定命名空间名称。相反,你只需要给出指向
.xsd文件的指针。
为了进行命名空间分配,解析器读取.xsd文件,并在其中找到它们适用于的目标命名空间的名称。因为文件是用 URI 指定的,所以解析器可以使用EntityResolver(如果已定义)来找到模式的本地副本。
如果模式定义没有定义目标命名空间,则适用于默认(未命名或 null)命名空间。因此,在我们的示例中,你会期望在模式中看到这些目标命名空间声明:
-
指向模式的 URI 的字符串
-
具有模式内容的
InputStream -
一个 SAX
InputSource -
一个文件
-
一组对象,每个对象都是这里定义的类型之一
只有当模式语言具有在运行时组装模式的能力时,才能使用对象数组。此外,当传递对象数组时,具有相同命名空间的两个模式是不允许的。
运行带有模式验证的DOMEcho示例
要运行带有模式验证的DOMEcho示例,请按照以下步骤进行。
-
导航到
samples目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples. -
编译示例类,使用刚刚设置的类路径。
% javac dom/* -
在 XML 文件上运行
DOMEcho程序,指定模式验证。在
data目录中选择一个 XML 文件,并使用指定的-xsd选项在其上运行DOMEcho程序。在这里,我们选择在文件personal-schema.xml上运行程序。% java dom/DOMEcho -xsd data/personal-schema.xml正如你在配置工厂中看到的,
-xsd选项告诉DOMEcho对personal-schema.xml文件中定义的 XML 模式进行验证。在这种情况下,模式是文件personal.xsd,也位于sample/data目录中。 -
在文本编辑器中打开
personal-schema.xml并删除模式声明。从开头的
<personnel>标签中删除以下内容。xsi:noNamespaceSchemaLocation='personal.xsd'不要忘记保存文件。
-
再次运行
DOMEcho,这次指定-xsd选项。% java dom/DOMEcho -xsd data/personal-schema.xml这次,你将看到一连串的错误。
-
再次运行
DOMEcho,这次指定-xsdss选项并指定模式定义文件。正如你在配置工厂中看到的,
-xsdss选项告诉DOMEcho在程序运行时执行针对指定的 XML 模式定义的验证。再次使用文件personal.xsd。% java dom/DOMEcho -xsdss data/personal.xsd data/personal-schema.xml你将看到与之前相同的输出,这意味着 XML 文件已成功根据模式进行验证。
更多信息
关于 W3C 文档对象模型(DOM)的更多信息,请参见DOM 标准页面。
关于基于模式的验证机制的更多信息,请参见以下内容。
教程:可扩展样式表语言转换
可扩展样式表语言转换(XSLT)标准定义了用于处理 XML 数据(XPath)并指定对数据进行转换以将其转换为其他形式的机制。JAXP 包括 XSLT 的解释实现。
在这节课中,您将编写一个文档对象模型作为 XML 文件,并了解如何从任意数据文件生成一个 DOM,以便将其转换为 XML。最后,您将把 XML 数据转换为不同的形式,沿途学习 XPath 寻址机制。
介绍 XSL、XSLT 和 XPath
可扩展样式表语言(XSL)有三个主要子组件:
XSL-FO
格式化对象标准。迄今为止最大的子组件,此标准提供了描述字体大小、页面布局和对象呈现的机制。此子组件不包含在 JAXP 中,也不包含在本教程中。
XSLT
这是一种转换语言,允许您定义从 XML 到其他格式的转换。例如,您可以使用 XSLT 生成 HTML 或不同的 XML 结构。您甚至可以使用它生成纯文本或将信息放入其他文档格式中。(正如您将在从任意数据结构生成 XML 中看到的,一个聪明的应用程序可以利用它来操作非 XML 数据)。
XPath
归根结底,XSLT 是一种语言,让您指定在遇到特定元素时要执行的操作。但是,要为 XML 数据结构的不同部分编写程序,您需要在任何给定时间指定您正在讨论的结构部分。XPath 就是这种规范语言。它是一种寻址机制,允许您指定到元素的路径,以便例如,可以区分<article><title>和<person><title>。通过这种方式,您可以为不同的<title>元素描述不同类型的翻译。
本节的其余部分描述了组成 JAXP 转换 API 的包。
JAXP 转换包
这里是组成 JAXP 转换 API 的包的描述:
javax.xml.transform
该包定义了您用于获取Transformer对象的工厂类。然后,您使用来自其他三个包中的类创建源(source)和结果(result)对象,并调用其transform()方法使转换发生。源和结果对象是使用其他三个包中的类创建的。
javax.xml.transform.dom
定义了DOMSource和DOMResult类,让您可以将 DOM 用作转换的输入或输出。
javax.xml.transform.sax
定义了SAXSource和SAXResult类,让您可以将 SAX 事件生成器用作转换的输入,或将 SAX 事件作为输出传递给 SAX 事件处理器。
javax.xml.transform.stream
定义了StreamSource和StreamResult类,让您可以将 I/O 流用作转换的输入或输出。
XSLT 示例程序
与本教程中的其他课程不同,本课程中使用的示例程序未包含在 JAXP 1.4.2 参考实现提供的install-dir/jaxp-1_4_2-release-date/samples目录中。但是您可以在此处下载 XSLT 示例的 ZIP 文件。


浙公网安备 33010602011771号