Java 中文官方教程 2022 版(三十八)(2)https://developer.aliyun.com/article/1488148
处理验证错误
根据 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
节点或一个属性节点。在构建过程中,请记住你在本教程中看到的结构。记住:层次结构中的每个节点都非常简单,只包含一个数据元素。
Java 中文官方教程 2022 版(三十八)(4)https://developer.aliyun.com/article/1488150