Java XML教程(第5章)

80酷酷网    80kuku.com

  xml|教程来源:http://d23xapp2.cn.ibm.com/developerWorks/education/xml/xmljava/tutorial/xmljava-1-1.html

第五章 解析器高级功能


概览

我们已经讨论了使用一个 XML 解析器来处理 XML 文档的基础。在本章节,我们将探讨一些高级概念。

首先,我们将从头构建一棵 DOM 树。换而言之,我们将不需要一个 XML 源文件来创建一个 Document 对象。

然后,我们将向您显示如何使用解析器来处理包含在一个字符串的 XML 文档。

接着,我们将向您显示如何操作一棵 DOM 树。我们将对我们示例的 XML 文档操作并对其诗句排序。

最后,我们将展示如何利用如 DOM 和 SAX 标准的接口使得解析器的更改十分容易。我们将向您展示两个使用不同 XML 解析器的示例应用。而其中 DOM 和 SAX 代码没有改变。

从头构建一棵 DOM 树

有时您想要从头构建一棵 DOM 树。要完成这个任务,您创建一个 Document 对象,然后对其添加不同的 Node 对象。

您可运行 java domBuilder 来看一个从头构建一棵 DOM 树的示例应用。该应用重新创建了通过对 sonnet.xml 最初解析而构建出的 DOM 树(但不再创建空格符)。

我们首先创建一个 DocumentImpl 类的实例。此类实现 DOM 定义的 Document 接口。
Document doc = (Document)Class.
forName("com.ibm.xml.dom.DocumentImpl").
newInstance();


domBuilder.java (代码请参考附录2)

这段代码不使用一个 XML 文档来构建一个 DOM 树。当树被构建完后,该代码将树的内容输出到标准输出。

...
<address>
<name>
<title>Mrs.</title>
<first-name>Mary</first-name>
<last-name>McGoon</last-name>
</name>
<street>1401 Main Street</street>
<city>Anytown</city>
<state>NC</state>
<zip>34829</zip>
</address>
<address>
<name>
...

添加 Node 到我们的 Document

现在我们有自己的 Document 对象了,我们开始创建 Node。我们第一个要创建的 Node 是一个 <sonnet> 元素。我们将创建所有的 Node,然后将每个节点添加到其对应的父母。

注意我们使用 setAttribute 方法来对<sonnet>元素设置其 type 属性的值。
Element root = doc.
createElement("sonnet");
root.setAttribute("type",
h"Shakespearean");
构建您的文档结构

当我们开始构建我们的 DOM 树时,我们将需要构建我们文档的结构。要完成它,我们将需要恰当地使用 appendChild 方法。我们将创建 <author> 元素,然后创建在其下的其它元素,接着使用 appendChild 方法将所有这些元素添加到正确的父母。

注意 createElement 是属于 Document 类的一个方法。我们的 Document 对象拥有我们在此创建的所有元素。

最终,注意到我们为所有的元素内容创建 Text 节点。Text 节点是元素的子女,而 Text 节点的父母则被添加到对应的父母下。

Element author =
doc.createElement("author");

Element lastName = doc.
createElement("last-name");
lastName.appendChild(doc.
createTextNode("Shakespeare"));
author.appendChild(lastName);

完成我们的 DOM 树

一旦我们对 <sonnet> 元素添加了所有内容,我们需要将它添加到 Document 对象。我们最后一次调用 appendChild 方法,这次是将子元素添加到 Document 对象上。

记住一个 XML 文档只能有一个根(root)元素;如果您要向Document添加多个根元素appendChild 将抛出一个异常。

当我们构建好 DOM 树后,我们构建一个 domBuilder 对象,然后调用它的 printDOMTree 方法来打印 DOM 树。
Element line14 = doc.
createElement("line");
line14.appendChild(doc.
createTextNode("As any ..."));
text.appendChild(line14);
root.appendChild(text);

doc.appendChild(root);

domBuilder db = new domBuilder();
db.printDOMTree(doc);

使用 DOM 对象来避免解析

您可以想象一个 DOM Document 对象作为一个 XML 文档的编译形式。如果您正使用 XML 来在不同方之间传递数据,您将可以通过接受和发送 DOM 对象而不是 XML 源数据来节约时间。

这是最常见的原因您为何需要从头来构建一个 DOM 树。

最坏情况下,您需要在您发送数据前从一棵 DOM 树创建出 XML 源数据,然后在您接受 XML 数据时创建出一棵 DOM 树。直接使用 DOM 对象将节约大量时间。

一个警告:要注意一个 DOM 对象可能比 XML 源数据要大很多。如果您要在一个十分缓慢的连接线路上传递数据,发送较小的 XML 源数据而重新解析数据要比传递大数据更有效。

解析一个 XML 字符串

很有可能您需要解析一个 XML 字符串。IBM 的 XML4J 解析器支持这个功能,尽管您需要将您的字符串转换成一个 InputSource 对象。

第一步是从您的字符串中创建一个 StringReader 对象。一旦完成此步,您可以从 StringReader 创建一个 InputSource 对象。

您可运行 java parseString 来查看代码的运行结果。在示例应用中,XML 字符串是写死的(hardcoded);有许多种方法让您从一个用户或其它机器获得 XML 输入。有了这个技术,您就不再需要将 XML 文档输出到一个文件系统来解析它了。

parseString ps = new parseString();
StringReader sr =
new StringReader("<?xml version=\"1.0\"?>
<a>AlphaBravo...");
InputSource iSrc = new InputSource(sr);
ps.parseAndPrint(iSrc);

parseString.java (参附录2)

这段代码展示了如何解析一个包含 XML 文档的字符串。



排列在一棵 DOM 树中的 Node

为了介绍您如何改变一棵 DOM 树的结构,我们将修改我们的 DOM 示例来排列十四行诗的 <line> 元素。有多种 DOM 方法可以用来在 DOM 树中搬移节点。

要查看代码的结果,运行 java domSorter sonnet.xml。它并不会改进诗的韵律,但它真确地排列了 <line> 元素。

要开始排列工作,我们将使用 getElementsByTagName 方法来提取在文档中的所有 <line> 元素。该方法节约了我们编写代码来遍历整个树的开销。

if (doc != null)
{
sortLines(doc);
printDOMTree(doc);
}
...
public void sortLines(Document doc)
{
NodeList theLines =
doc.getDocumentElement().
getElementsByTagName("line");
...

domSorter.java (参附录2)

这段代码在 XML 文档中查找所有的 <line> 元素,然后排序。它展示了如何操作一个 DOM 树。



提取我们的 <line> 文本

为了简化代码,我们创建一个 helper 功能,getTextFromLine,用来提取包含在一对 <line> 元素中的文本。它简单地找到 <line> 元素的第一个子女,如果其是一个 Text 节点就返回其文本。

该方法返回一个 Java String 因此我们的排序过程可以使用 String.compareTo 方法来决定排序的次序。

该段代码实际上应该检查 <line> 所有的子女,因为它们可能包含实体(entity)引用 (例如实体 &miss; 可能替代了文本 "mistress")。我们将把这个改进作为读者的一个练习。

public String getTextFromLine(Node
lineElement)
{
StringBuffer returnString =
new StringBuffer();
if (lineElement.getNodeName().
equals("line"))
{
NodeList kids = lineElement.
getChildNodes();
if (kids != null)
if (kids.item(0).getNodeType() ==
Node.TEXT_NODE)
returnString.append(kids.item(0).
getNodeValue());
}
else
returnString.setLength(0);

return new String(returnString);
}

文本排序

现在我们有能力从一个给定的 <line> 元素获取文本,我们可以开始排列数据了。由于我们只有 14 个元素,我们将使用冒泡排序。

冒泡排序算法是比较两个相邻数据值,然后如果它们次序不对就交换它们。要完成交换,我们使用 getParentNode 和 insertBefore 方法。

getParentNode 返回任意 Node 的父母;我们使用这个方法来获得当前 <line> 的父母 (文档的一个 <lines> 元素使用 sonnet DTD)。

insertBefore(nodeA, nodeB) 在 nodeB 前插入 nodeA 到 DOM 树中。insertBefore 最重要的特性是如果 nodeA 已经存在于 DOM 树中了,它在插入 nodeB 前将删除该节点。
public void sortLines(Document doc)
{
NodeList theLines =
doc.getDocumentElement().
getElementsByTagName("line");
if (theLines != null)
{
int len = theLines.getLength();
for (int i=0; i < len; i++)
for (int j=0; j < (len-1-i); j++)
if (getTextFromLine(
theLines.item(j)).
compareTo(getTextFromLine(
theLines.item(j+1))) > 0)
theLines.item(j).
getParentNode().insertBefore(
theLines.item(j+1),
theLines.item(j));
}
}

用来操作树的有用的 DOM 方法

除insertBefore 之外,还有其它的 DOM 方法可用来操作树。

parentNode.appendChild(newChild)
将一个节点作为给定父母节点的最后子女添加。调用 parentNode.insertBefore(newChild, null) 完成同样功能。
parentNode.replaceChild(newChild, oldChild)
将 oldChild 用 newChild 来取代。节点 oldChild 必须是 parentNode 的子女。
parentNode.removeChild(oldChild)
将 oldChild 从 parentNode 下删除。

parentNode.appendChild(newChild);
...
parentNode.insertBefore(newChild,
oldChild);
...
parentNode.replaceChild(newChild,
oldChild);
...
parentNode.removeChild(oldChild);
...

关于树操作还需注意的事项

如果您需要删除一个给定节点的所有子女,这要比看上去难多了。这两段在左边的示例代码看起来可以完成任务。但是,在第二个才能完成。第一个示例代码由于 kid 的实例数据在 removeChild(kid) 被调用后就改变了而无法完成任务。

换而言之,for 循环删除了 kid,第一个子女,然后检查 kid.getNextSibling 是否为 null 。由于 kid 刚被删除,它不再有任何同胞了,因此 kid.getNextSibling 是 null。 for 循环只会运行一次。不论 node 有一个或几千个子女,第一段示例代码只是删除第一个子女。要使用第二段示例代码来删除所有的子节点。
/** Doesn't work **/
for (Node kid = node.getFirstChild();
kid != null;
kid = kid.getNextSibling())
node.removeChild(kid);

/** Does work **/
while (node.hasChildNodes())
node.removeChild(node.getFirstChild());


使用另一个DOM 解析器

尽管我们想象不出一个您为何要改换解析器的理由,您可以使用非 XML4J 的解析器来解析您的 XML 文档。如果您查看t domTwo.java 的代码,您将看到更换到 Sun 的 XML 解析器只需要两个修改。

首先,我们必须载入(import) Sun 公司的类。这很简单。我们要修改的只是创建 Parser 对象的代码。如您所见,Sun 的解析器构建过程比较复杂,但余下的代码不用修改了。所有的 DOM 代码不需要任何的修改。

最终,在 domTwo 的不同之处是命令行格式。出于某些原因,Sun 的解析器不能用常用的方法来解析文件名。如果您运行 java domTwo file:///d:/sonnet.xml (当然根据您的系统修改 file URI),您将获得 domOne 的相同结果。

import com.sun.xml.parser.Parser;
import
com.sun.xml.tree.XmlDocumentBuilder;

...

XmlDocumentBuilder builder =
new XmlDocumentBuilder();
Parser parser =
new com.sun.xml.parser.Parser();
parser.setDocumentHandler(builder);
builder.setParser(parser);
parser.parse(uri);
doc = builder.getDocument();


domTwo.java (参见附录2)

这段代码等同于 domOne.java,但它使用 Sun 公司的 XML 解析器而不是 IBM 的。它展示了 DOM 接口的可移植性。



使用一个不同的 SAX 解析器

我们也编写了 saxTwo.java 来展示如何使用 Sun 公司的 SAX 解析器。与 domTwo 类似,我们有两个修改之处。第一个是载入(import) Sun 的 Resolver 类而不是 IBM 的 SAXParser 类。

我们需要修改创建 Parser 对象的代码,然后我们需要根据我们输入的 URI 创建一个 InputSource 对象。我们要修改的只是创建 parser 的代码需要被包含在 try 代码段中以捕获当我们创建 Parser 对象时可能产生的异常。

import com.sun.xml.parser.Resolver;
...

try
{
Parser parser =
ParserFactory.makeParser();
parser.setDocumentHandler(this);
parser.setErrorHandler(this);
parser.parse(Resolver.
createInputSource(new File(uri)));
}

saxTwo.java (见附录2)

这段代码等同于 saxOne.java,但它使用 Sun 公司的 XML 解析器而不是 IBM 的。它展示了 SAX 接口的可移植性。


总结

在本章节,我们介绍了一些使用 XML 解析器的高级编程技巧。我们展现了如何直接生成 DOM 树,如何解析字符串而不是文件,如何在一棵 XML 树中移动元素以及如何改变解析器而不用影响 DOM 和 SAX 的代码。

希望您喜欢本教程!

这就是本教程的所有内容了。 我们讨论了 XML 应用的基本架构,而且我们也介绍了您如何处理 XML 文档。以后的教程将介绍构建 XML 应用更多的细节,包括:

使用可视工具来构建 XML 应用
将一个 XML 文档从一种形式转换到另一种
为最终用户或其他进程创建接口,及对后端存储数据的接口
要获得更多信息

如果您想了解更多的 XML 知识,可访问 developerWorks 的 XML 专区。这个站点有示例代码、其它的教程、关于 XML 标准的信息以及其它内容。

最后,我们很愿意听取您的意见!我们设计 developerWorks 是成为开发人员的资源。如果您有任何评价、建议或抱怨,请让我们知道。

谢谢,---Doug Tidwell 或 developerWorks 中国站点!



分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: