将 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 ErrorHandlerinterface的对象。


注意- 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是根据该规范改编的。

表 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 节点至少都有一个类型,一个名称和一个值,该值可能为空也可能不是空。在上面的示例中,Nodeinterface的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 中看到的词汇信息。

所有这些属性的默认值为 false,这保留了以原始形式重建传入文档所需的所有词汇信息。将它们设置为 true 可以构造最简单的 DOM,以便应用程序可以专注于数据的语义内容,而不必担心词法语法细节。 Table 3-2总结了设置的效果。

表 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注解ignoreWhitespaceputCDATAIntoTextcreateEntityRefs在主方法代码的开头声明,并且在运行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 树中的绝大多数节点将是ElementText个节点。


注意- 文本节点位于 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 层次结构中。相反,它们是通过Nodeinterface的getAttributes方法获得的。

DocTypeinterface是w3c\.org\.dom\.Node的扩展。它定义了getEntities方法,可用于获取Entity节点-定义实体的节点。像Attribute节点一样,Entity节点不会显示为 DOM 节点的子节点。

Node Operations

本节将快速介绍您可能要应用于 DOM 的一些操作。

Creating Nodes

您可以使用Documentinterface的方法创建不同类型的节点。例如createElementcreateCommentcreateCDATAsectioncreateTextNode等。 org.w3c.dom.Document的 API 文档中提供了用于创建不同节点的方法的完整列表。

Traversing Nodes

org\.w3c\.dom\.Nodeinterface定义了可用于遍历节点的多种方法,包括getFirstChildgetLastChildgetNextSiblinggetPreviousSiblinggetParentNode。这些操作足以从树中的任何位置到达树中的任何其他位置。

搜索节点

当您搜索具有特定名称的节点时,还有一点需要考虑。尽管很想获得第一个孩子并检查它是否合适,但是搜索必须考虑到以下事实:子列表中的第一个孩子可能是 注解 或处理指令。如果 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\.Elementinterface扩展了 Node,它定义了setAttribute操作,该操作向该节点添加了一个属性。 (从 Java 平台的角度来看,更好的名字是addAttribute.该属性不是类的属性,而是创建了一个新对象.)您还可以使用DocumentcreateAttribute操作创建Attribute的实例,然后使用setAttributeNode方法添加它。

删除和更改节点

要删除节点,请使用其父节点的removeChild方法。要对其进行更改,可以使用父节点的replaceChild操作或节点的setNodeValue操作。

Inserting Nodes

创建新节点时要记住的重要一点是,当您创建元素节点时,指定的唯一数据就是名称。实际上,该节点为您提供了一个挂东西的钩子。通过将项目添加到子节点列表中,可以将其挂在钩子上。例如,您可以添加文本节点,CDATA节点或属性节点。在构建时,请记住本教程中看到的结构。切记:层次结构中的每个节点都非常简单,仅包含一个数据元素。

运行 DOMEcho 示例

要运行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="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打印出文档中不同元素的所有节点,并带有正确的缩进以显示节点层次结构。

首页