该教程是针对JDK 8编写的。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及后续版本中更新的语言功能的摘要。
请参阅JDK发行说明,了解所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
在这个部分中,您将通过读取现有的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;
最后,导入DOM、DOM异常、实体和节点的W3C定义:
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("警告:" + getParseExceptionInfo(spe)); } public void error(SAXParseException spe) throws SAXException { String message = "错误:" + getParseExceptionInfo(spe); throw new SAXException(message); } public void fatalError(SAXParseException spe) throws SAXException { String message = "致命错误:" + 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模式的验证,并且工厂被配置为能够识别命名空间,并执行用户指定的验证类型。
注意 - 符合JAXP规范的解析器并不要求支持所有这些选项的所有组合,尽管参考解析器是这样。如果您指定了一个无效的选项组合,工厂在尝试获取解析器实例时会生成一个ParserConfigurationException。
关于如何使用命名空间和验证的更多信息,请参阅使用XML模式进行验证,其中将描述上述摘录中缺失的代码。
根据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的内部结构,以便你可以看到它包含了什么。 DOMEcho示例通过回显DOM节点,然后将它们打印到屏幕上,使用适当的缩进使节点层次结构明显。这些节点类型的规范可以在DOM Level 2 Core Specification中找到,在Node的规范下。下面的表 3-1是根据该规范进行调整的。
节点
节点名称
节点值
属性
属性
属性名称
属性值
null
CDATASection
#cdata-section
CDATA节的内容
null
注释
#comment
注释内容
null
文档
#document
null
null
文档片段
#documentFragment
null
null
文档类型
文档类型名称
null
null
元素
标签名称
null
null
实体
实体名称
null
null
EntityReference
引用的实体的名称
null
null
Notation
符号名称
null
null
ProcessingInstruction
目标
除目标外的全部内容
null
Text
#text
文本节点的内容
null
这个表中的信息非常有用;在处理DOM时,你将需要它,因为所有这些类型在DOM树中是混合在一起的。
DOM节点元素类型信息通过调用org.w3c.dom.Node类的各种方法来获取。以下代码显示了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构造函数类中。
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中看到的词法信息。
将CDATA节点转换为Text节点,并追加到相邻的Text节点(如果有的话)。
展开实体引用节点。
忽略注释。
忽略不是元素内容的空格。
所有这些属性的默认值都是false,这样可以保留重建原始文档所需的所有词法信息。将它们设置为true可以构建最简单的DOM,使应用程序能够专注于数据的语义内容,而无需担心词法语法细节。 表3-2总结了这些设置的效果。
API |
保留词法信息 |
关注内容 |
---|---|---|
setCoalescing() |
否 |
是 |
setExpandEntityReferences() |
否 |
是 |
setIgnoringComments() |
否 |
是 |
setIgnoringElementContentWhitespace() |
否 |
是 |
下面显示了DomEcho示例中在main方法中实现这些方法的代码。
// ... dbf.setIgnoringComments(ignoreComments); dbf.setIgnoringElementContentWhitespace(ignoreWhitespace); dbf.setCoalescing(putCDATAIntoText); dbf.setExpandEntityReferences(!createEntityRefs); // ...
布尔变量ignoreComments、ignoreWhitespace、putCDATAIntoText和createEntityRefs在main方法代码的开头声明,并在运行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 (...) { // 在这里验证参数 // ... } 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]; // 必须是最后一个参数 if (i != args.length - 1) { usage(); } } } // ... }
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("不支持的节点类型:" + 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层次结构中搜索节点时需要编写的一种例程示例。它在此完整呈现(包括注释),以便您可以在应用程序中使用它作为模板。
/** * 在节点的子列表中查找指定名称的子节点。 * <ul> * <li>忽略注释和处理指令。 * <li>忽略TEXT节点(如果不进行验证,则可能存在并包含可忽略的空白)。 * <li>忽略CDATA节点和EntityRef节点。 * <li>检查元素节点以找到具有指定名称的节点。 * </ul> * @param name 要查找的元素的标签名 * @param node 开始搜索的元素节点 * @return 找到的节点 */ public Node findSubNode(String name, Node node) { if (node.getNodeType() != Node.ELEMENT_NODE) { System.err.println("错误:搜索节点不是元素类型"); 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节点中找到的文本。以下是您可以使用的此过程的示例。
/** * 返回节点包含的文本。此过程: * <ul> * <li>忽略注释和处理指令。 * <li>连接TEXT节点、CDATA节点和递归处理EntityRef节点的结果。 * <li>忽略子列表中的任何元素节点。 * (其他可能的选项是递归进入元素子列表或抛出异常。) * </ul> * @param node 一个DOM节点 * @return 代表其内容的字符串 */ 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) { // 递归进入子树获取文本 // (并忽略注释) result.append(getText(subnode)); } } return result.toString(); }
要深入解释此代码,请参阅增加复杂性在何时使用DOM中。同样,您可以使用词法控制中描述的API简化此代码,以修改解析器构造的DOM的类型。但是这段代码的好处是几乎适用于任何DOM。
org.w3c.dom.Element接口继承自Node,定义了setAttribute操作,用于向该节点添加属性(从Java平台的角度来看,更好的名称应该是addAttribute。属性不是该类的属性,而是创建了一个新对象)。您还可以使用Document的createAttribute操作创建Attribute的实例,然后使用setAttributeNode方法添加。
要删除一个节点,你需要使用它的父节点的removeChild方法。要更改一个节点,你可以使用父节点的replaceChild操作或者节点的setNodeValue操作。
在创建新节点时,要记住的重要事情是,当你创建一个元素节点时,你只需要指定一个名称。实际上,该节点为你提供了一个挂载物的钩子。你可以通过将其子节点添加到其子节点列表中来挂载物。例如,你可以添加一个文本节点,一个CDATA节点或者一个属性节点。在构建过程中,请记住本教程中所见到的结构。记住:层次结构中的每个节点都非常简单,只包含一个数据元素。
要运行DOMEcho示例,请按照以下步骤操作。
% cd install-dir/jaxp-1_4_2-release-date/samples
% javac dom/*
选择data目录中的一个XML文件,并在其上运行DOMEcho程序。这里,我们选择在文件personal-schema.xml上运行该程序。
% java dom/DOMEcho data/personal-schema.xml
XML文件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打印出了文档中不同元素的所有节点,并以正确的缩进显示节点层次结构。