将 XML 数据读入 DOM
在本节中,您将通过读取现有的 XML 文件来构造文档对象模型。
注意- 在可扩展样式表语言转换中,您将看到如何将 DOM 作为 XML 文件写出。 (您还将看到如何相对轻松地将现有数据文件转换为 XML.)
创建程序
文档对象模型提供了 API,可用于创建,修改,删除和重新排列节点。在try创建 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;
Handle Errors
接下来,添加错误处理逻辑。最重要的一点是,当解析 XML 文档时遇到麻烦时,需要 JAXP 兼容的文档构建器报告 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 兼容解析器支持这些选项的所有组合。如果指定的选项无效组合,则工厂在try获取解析器实例时会生成ParserConfigurationException
。
使用 XMLPattern 进行验证提供了有关如何使用名称空间和验证的更多信息,其中将描述上述摘录中缺少的代码。
处理验证错误
根据 SAX 标准的规定,对验证错误的默认响应是不执行任何操作。 JAXP 标准要求抛出 SAX 异常,因此您使用与用于 SAX 应用程序完全相同的错误处理机制。特别是,您可以使用DocumentBuilder
类的setErrorHandler
方法向其提供实现 SAX ErrorHandler
interface的对象。
注意- DocumentBuilder
也可以使用setEntityResolver
方法。
以下代码将文档构建器配置为使用Handle Errors中定义的错误处理程序。
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 节点,然后使用适当的缩进在屏幕上将它们打印出来,以使节点层次结构清晰可见,来实现此 Object。这些节点类型的规范可以在Node
的规范下的DOM 2 级核心规范中找到。以下Table 3-1是根据该规范改编的。
Node | nodeName | nodeValue | Attributes |
---|---|---|---|
Attr | 属性名称 | 属性值 | null |
CDATASection | \#cdata\-section | CDATA 部分的内容 | null |
Comment | \#comment | Comment 内容 | null |
Document | \#document | null | null |
DocumentFragment | \#documentFragment | null | null |
DocumentType | 文件类型名称 | null | null |
Element | Tag name | null | null |
Entity | Entity name | null | null |
EntityReference | 所引用实体的名称 | null | null |
Notation | Notation name | null | null |
ProcessingInstruction | Target | 整个内容(不包括目标) | 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
interface的getNamespaceURI\(\)
,getPrefix\(\)
,getLocalName\(\)
和getNodeValue\(\)
方法返回并打印回显节点的名称空间 URI,名称空间前缀,本地限定名称和值。请注意,对getNodeValue\(\)
返回的值调用trim\(\)
方法,以确定该节点的值是否为空白,并相应地打印一条消息。
有关Node
方法的完整列表以及它们返回的不同信息,请参阅Node的 API 文档。
接下来,定义一种方法来设置节点在打印时的缩进量,以便可以轻松看到节点层次结构。
private void outputIndentation() {
for (int i = 0; i < indent; i++) {
out.print(basicIndent);
}
}
通过将以下突出显示的行添加到DOMEcho
构造函数类中来定义basicIndent
常量,该常量定义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;
}
}
与Handle Errors中定义的错误处理程序一样,DOMEcho
程序会将其输出创建为PrintWriter
实例。
Lexical Controls
词汇信息是重构 XML 文档原始语法所需的信息。保留词法信息对于编辑应用程序很重要,在该应用程序中,您想保存一个文档,该文档准确地反映了原始完整内容,包括 注解,实体引用以及它可能一开始就包含的所有 CDATA 部分。
但是,大多数应用程序仅关注 XML 结构的内容。他们有能力忽略 注解,也不必关心数据是在 CDATA 节中编码还是以纯文本格式编写,或者是否包含实体引用。对于此类应用程序,需要最少的词汇信息,因为它简化了应用程序必须准备检查的 DOM 节点的数量和种类。
以下DocumentBuilderFactory
方法使您可以控制在 DOM 中看到的词汇信息。
-
setCoalescing\(\)
- 要将
CDATA
个节点转换为Text
个节点并追加到相邻的Text
个节点(如果有)。
- 要将
-
setExpandEntityReferences\(\)
- 扩展实体参考节点。
-
setIgnoring注解\(\)
- 忽略 Comment。
-
setIgnoringElementContentWhitespace\(\)
- 忽略不是元素内容重要部分的空格。
所有这些属性的默认值为 false,这保留了以原始形式重建传入文档所需的所有词汇信息。将它们设置为 true 可以构造最简单的 DOM,以便应用程序可以专注于数据的语义内容,而不必担心词法语法细节。 Table 3-2总结了设置的效果。
API | 保留词法信息 | 专注于内容 |
---|---|---|
setCoalescing\(\) | False | True |
setExpandEntityReferences\(\) | False | True |
setIgnoring注解\(\) | False | True |
setIgnoringElementContent Whitespace\(\) | False | True |
DomEcho
示例的 main 方法中的这些方法的实现如下所示。
// ...
dbf.setIgnoring注解(ignore注解);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);
// ...
布尔变量ignore注解
,ignoreWhitespace
,putCDATAIntoText
和createEntityRefs
在主方法代码的开头声明,并且在运行DomEcho
时由命令行参数设置。
public static void main(String[] args) throws Exception {
// ...
boolean ignoreWhitespace = false;
boolean ignore注解 = 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")) {
ignore注解 = 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
interface的getAttributes
方法获得的。
DocType
interface是w3c\.org\.dom\.Node
的扩展。它定义了getEntities
方法,可用于获取Entity
节点-定义实体的节点。像Attribute
节点一样,Entity
节点不会显示为 DOM 节点的子节点。
Node Operations
本节将快速介绍您可能要应用于 DOM 的一些操作。
-
Creating nodes
-
Traversing nodes
-
搜索节点
-
获取节点内容
-
Creating attributes
-
删除和更改节点
-
Inserting nodes
Creating Nodes
您可以使用Document
interface的方法创建不同类型的节点。例如createElement
,createComment
,createCDATAsection
,createTextNode
等。 org.w3c.dom.Document的 API 文档中提供了用于创建不同节点的方法的完整列表。
Traversing Nodes
org\.w3c\.dom\.Node
interface定义了可用于遍历节点的多种方法,包括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中的增加复杂性。还要注意,您可以使用Lexical Controls中描述的 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中的增加复杂性。同样,您可以通过使用Lexical Controls中描述的 API 修改解析器构造的 DOM 的类型来简化此代码。但是,此代码的优点是,它将几乎可用于任何 DOM。
Creating Attributes
org\.w3c\.dom\.Element
interface扩展了 Node,它定义了setAttribute
操作,该操作向该节点添加了一个属性。 (从 Java 平台的角度来看,更好的名字是addAttribute
.该属性不是类的属性,而是创建了一个新对象.)您还可以使用Document
的createAttribute
操作创建Attribute
的实例,然后使用setAttributeNode
方法添加它。
删除和更改节点
要删除节点,请使用其父节点的removeChild
方法。要对其进行更改,可以使用父节点的replaceChild
操作或节点的setNodeValue
操作。
Inserting Nodes
创建新节点时要记住的重要一点是,当您创建元素节点时,指定的唯一数据就是名称。实际上,该节点为您提供了一个挂东西的钩子。通过将项目添加到子节点列表中,可以将其挂在钩子上。例如,您可以添加文本节点,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.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="[email protected]"
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="[email protected]"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="link"
local="link"
ATTR: nodeName="manager"
local="manager"
nodeValue="Big.Boss"
TEXT: nodeName="#text"
nodeValue=[WS]
[...]
如您所见,DOMEcho
打印出文档中不同元素的所有节点,并带有正确的缩进以显示节点层次结构。