XML dom

时间:2023-03-09 03:20:39
XML dom
将文件解析为文档

三步过程

为了使用 XML 文件中的信息,必须解析文件以创建一个 Document 对象。

Document 对象是一个接口,因而不能直接将它实例化;一般情况下,应用程序会相应使用一个工厂。准确的过程因实现而异,但是基本思想是相同的。(同样,Level 3 标准化了这个任务)。在这个例子 Java 环境中,解析文件是一个三步过程:

创建 DocumentBuilderFactory。DocumentBuilderFactory 对象创建 DocumentBuilder。
创建 DocumentBuilder。DocumentBuilder 执行实际的解析以创建 Document 对象。
解析文件以创建 Document 对象。
现在您可以开始构建应用程序了。 回页首 基本的应用程序 首先创建一个基本的应用程序,即一个名为 OrderProcessor 的类。 import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import org.w3c.dom.Document; public class OrderProcessor {
public static void main (String args[]) {
File docFile = new File("orders.xml");
Document doc = null;
try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile); } catch (Exception e) {
System.out.print("Problem parsing the file: "+e.getMessage());
}
}
} 首先,Java 代码导入必要的类,然后它创建 OrderProcessor 应用程序。本教程中的例子仅处理一个文件,因此为简洁起见,应用程序包含了对该文件的直接引用。 因此 Document 对象可以在以后使用,应用程序把它定义在 try-catch 块之外。 在 try-catch 块中,应用程序创建了 DocumentBuilderFactory,然后再使用它来创建 DocumentBuilder。 最后,DocumentBuilder 解析文件以创建 Document。 回页首 解析器设置 使用 DocumentBuilder 创建解析器的优点之一在于能够控制 DocumentBuilderFactory 创建的解析器上的各种设置。例如,可以设置解析器验证文档: ... try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setValidating(true); DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile);
} catch (Exception e) {
... Java 的 DOM Level 2 实现允许通过以下方法控制解析器的参数: setCoalescing():决定解析器是否要将 CDATA 节点转换为文本,以及是否要和周围的文本节点合并(如果适用的话)。其默认值为 false。
setExpandEntityReferences():确定是否要展开外部实体引用。如果为 true,外部数据将插入文档。其默认值为 true。(请参阅 参考资料 以了解关于使用外部实体的技巧)。
setIgnoringComments():确定是否要忽略文件中的注释。其默认值为 false。
setIgnoringElementContentWhitespace():确定是否要忽略元素内容中的空白(类似于浏览器对待 HTML 的方式)。其默认值为 false。
setNamespaceAware():确定解析器是否要注意名称空间信息。其默认值为 false。
setValidating():默认情况下,解析器不验证文档。将这个参数设置为 true 可打开验证功能。
回页首 解析器异常 由于在创建解析器时存在所有这些可能性,其中许多地方都可能会出错。正如这里的例子所表明的,应用程序把所有这些内容转储到一个单一的通用 Exception 中,就调试而言,这样可能不是很有帮助。 为更好地查明问题,您可以捕捉与创建和使用解析器的各方面相关的特定异常: ... try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile);
} catch (javax.xml.parsers.ParserConfigurationException pce) {
System.out.println("The parser was not configured correctly.");
System.exit(1);
} catch (java.io.IOException ie) {
System.out.println("Cannot read input file.");
System.exit(1);
} catch (org.xml.sax.SAXException se) {
System.out.println("Problem parsing the file.");
System.exit(1);
} catch (java.lang.IllegalArgumentException ae) {
System.out.println("Please specify an XML source.");
System.exit(1); }
... 一旦解析器已创建了一个 Document,应用程序就能单步调试它以检查数据。 单步调试文档 获取根元素 一旦解析了文档并创建了一个 Document,应用程序就能单步调试该结构以审核、查找或显示信息。这种导航功能是将要在 Document 上执行的许多操作的基础。 对文档的单步调试首先从根元素开始。格式良好的文档仅有一个根元素,也称为 DocumentElement。应用程序首先检索这个元素。 import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import org.w3c.dom.Document; import org.w3c.dom.Element; public class OrderProcessor {
...
System.exit(1);
} //STEP 1: Get the root element Element root = doc.getDocumentElement();
System.out.println("The root element is " + root.getNodeName()); }
} 编译和运行这个应用程序将输出根元素的名称 orders。 回页首 获取节点的孩子 一旦应用程序确定了根元素,它就把根元素的孩子的列表作为一个 NodeList 来检索。NodeList 类是一系列的项,应用程序将逐个迭代这些项。 在本例中,为简洁起见,应用程序通过仅显示有多少元素出现在生成的 NodeList 中,从而获取孩子节点和验证检索结果。 注意这里的文档仅有两个元素,但是 NodeList 包含五个孩子,包括包含换行的三个文本节点 ―― 还要注意节点和元素在 DOM 中不是等价的。其他三个节点是包含换行的文本节点。 ... import org.w3c.dom.NodeList; ...
//STEP 1: Get the root element
Element root = doc.getDocumentElement();
System.out.println("The root element is "+root.getNodeName()); //STEP 2: Get the children
NodeList children = root.getChildNodes();
System.out.println("There are "+children.getLength()
+" nodes in this document."); }
} 回页首 使用 getFirstChild() 和 getNextSibling() 父子和兄弟关系提供了迭代某个节点的所有孩子的替代方法,它在某些场合下可能更为适宜,比如在这些关系和孩子的出现顺序对理解数据至关重要的时候。 在 Step 3 中,for 循环首先从根元素的第一个孩子开始。应用程序迭代第一个孩子的所有兄弟,直至已全部对它们求值。 每次应用程序执行该循环,它都要检索一个 Node 对象,输出其名称和值。注意 orders 的五个孩子包括 orders 元素和三个文本节点。还要注意元素具有一个 null 值,而不是预期的文本。包含实际内容作为其值的,是作为元素的孩子的文本节点。 ... import org.w3c.dom.Node; ... //STEP 3: Step through the children
for (Node child = root.getFirstChild();
child != null;
child = child.getNextSibling())
{
System.out.println(start.getNodeName()+" = "
+start.getNodeValue());
} }
}
... 回页首 在多层孩子中递归 使用 getFirstChild() 和 getNextSibling() 中的代码显示了第一层孩子,但那远远不是整个文档。为了看到所有元素,必须将前一个例子中的功能转换为一个方法并递归地调用。 应用程序首先从根元素开始,向屏幕输出名称和值。然后应用程序就像以前一样遍历根元素的每个孩子。但是对于每个孩子,应用程序还会遍历该孩子的每个孩子,即检查根元素的所有孩子和孙子。 ... public class OrderProcessor { private static void stepThrough (Node start)
{ System.out.println(start.getNodeName()+" = "+start.getNodeValue()); for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
stepThrough(child); }
} public static void main (String args[]) {
File docFile = new File("orders.xml"); ...
System.out.println("There are "+children.getLength()
+" nodes in this document."); //STEP 4: Recurse this functionality
stepThrough(root); }
} 回页首 包含属性 到目前为止,所编写的 stepThrough() 方法能够遍历大多数类型的节点,但是它完全地遗漏了属性,因为属性不是任何节点的孩子。为了显示属性,可修改 stepThrough() 来检查元素节点的属性。 下面修改过的代码检查每个节点的输出,通过将节点的 nodeType 与常量 ELEMENT_NODE 作比较,从而确定它是否为一个元素。Node 对象带有成员常量,它们表示每种类型的节点,比如 ELEMENT_NODE 或 ATTRIBUTE_NODE。如果 nodeType 与 ELEMENT_NODE 匹配,它就是一个元素。 对于找到的每个元素,应用程序都会创建一个包含该元素的所有属性的 NamedNodeMap。应用程序能够迭代 NamedNodeMap,输出每个属性的名称和值,就像它迭代 NodeList 一样。 ... import org.w3c.dom.NamedNodeMap; ...
private static void stepThroughAll (Node start)
{
System.out.println(start.getNodeName()+" = "+start.getNodeValue()); if (start.getNodeType() == start.ELEMENT_NODE)
{
NamedNodeMap startAttr = start.getAttributes();
for (int i = 0;
i < startAttr.getLength();
i++) {
Node attr = startAttr.item(i);
System.out.println(" Attribute: "+ attr.getNodeName()
+" = "+attr.getNodeValue());
}
} for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
stepThroughAll(child);
}
} 编辑文档 更改节点的值 检查一下 XML 文档内容的常量是有用的,但是在处理全功能的应用程序时,您可能需要更改数据以添加、编辑、移动或删除信息。编辑数据的能力对创建新 XML 文档的过程也是至关重要的。这类更改中最简单的情况就是更改元素的文本内容。 这里的目标是更改某个元素的文本节点的值,在此例中是将每个 order 的 status 设置为 “processed”,然后向屏幕输出新的值。 使用起始节点(root)以及要更改的元素名称和要更改到的目标值作为参数,调用 changeOrder() 方法。 changeOrder() 首先检查节点的名称,以确定它是否为要编辑的元素之一。如果是,应用程序需要更改的不是这个节点的值,而是这个节点的第一个孩子的值,因为这第一个孩子才是实际包含内容的文本节点。 在任一种情况下,应用程序都要检查每个孩子,就像它在第一次单步调试文档时一样。 当完成更改时,更改后的值将使用 getElementsByTagName() 来检查。这个方法返回具有指定名称(比如 status)的所有孩子的列表。然后应用程序就能够检查该列表中的值,以检验 changeOrder() 方法调用的有效性。 ... public class OrderProcessor { private static void changeOrder (Node start,
String elemName,
String elemValue)
{
if (start.getNodeName().equals(elemName)) {
start.getFirstChild().setNodeValue(elemValue);
} for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
changeOrder(child, elemName, elemValue);
}
} ...
public static void main (String args[]) {
... // Change text content changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName("status");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{
System.out.println(orders.item(orderNum)
.getFirstChild().getNodeValue());
} }
} 注意应用程序挑选了 status 节点,虽然这些节点是根元素的孙子,而不是直接的孩子。getElementsByTagName() 单步调试文档,并查找具有特定名称的所有元素。 回页首 添加节点:准备数据 与更改现有节点不同,有时添加一个节点是必要的,而您可以通过多种方式来做到这点。在本例中,应用程序汇总每个 order 的价格,然后添加一个 total 元素。它通过接受每个订单,循环迭代它的每件商品以获取商品价格,然后汇总这些价格,从而获得总价格。然后应用程序向订单添加一个新的元素(参见下面的代码清单)。 首先,应用程序检索 order 元素,就像它检索 status 元素一样。然后它循环迭代这其中的每个元素。 对于这其中的每个 order,应用程序都需要其 item 元素的一个 NodeList,因此应用程序必须首先将 order Node 强制转换为 Element,才能使用 getElementsByTagName()。 然后应用程序可以循环迭代 item 元素,以寻找选定的 order。应用程序将每个 item 强制转换为 Element,以便能够根据名称检索 price 和 qty。它是通过使用 getElementsByTagName() 来实现的。因为每件商品只有一个名称,它可以直接转到 item(0),即生成的 NodeList 中的第一个条目。这第一个条目表示 price(或 qty)元素。它就是从那里获得文本节点的值的。 文本节点的值是 String 值,应用程序然后会将它转换为一个 double 值,以允许进行必要的数学运算。 当应用程序检查完每个订单的每件商品时,total 就是一个代表总价格的 double 值。然后 total 被转换为 String 值,以便能将它用作新元素的内容,<total> 最终加入了 order。 ... changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName(" order ");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{ Element thisOrder = (Element)orders.item(orderNum);
NodeList orderItems = thisOrder.getElementsByTagName("item");
double total = 0;
for (int itemNum = 0;
itemNum < orderItems.getLength();
itemNum++) { // Total up cost for each item and
// add to the order total //Get this item as an Element
Element thisOrderItem = (Element)orderItems.item(itemNum); //Get pricing information for this Item
String thisPrice =
thisOrderItem.getElementsByTagName("price").item(0)
.getFirstChild().getNodeValue();
double thisPriceDbl = new Double(thisPrice).doubleValue(); //Get quantity information for this Item
String thisQty =
thisOrderItem.getElementsByTagName("qty").item(0)
.getFirstChild().getNodeValue();
double thisQtyDbl = new Double(thisQty).doubleValue(); double thisItemTotal = thisPriceDbl*thisQtyDbl;
total = total + thisItemTotal;
}
String totalString = new Double(total).toString(); }
... 回页首 添加节点:向文档添加节点 您可以用许多方法创建新 Node,而本例将使用其中的几种方法。首先,Document 对象能够创建新的值为 totalString 的文本节点。新的 Node 现在已经存在了,但是还没在任何地方实际连接到 Document。新的 total 元素是以类似的方式创建的,起初也没有实质性的内容。 添加节点的另一种方法是使用 appendChild(),就像这里将节点添加到新的 total 元素一样。 最后,应用程序可以使用 insertBefore() 来向 Document 添加新的元素,同时指定新的 Node,然后指定新 Node 之后的那个 Node。 单步调试文档将检验这些更改。 ... changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName("order");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{
...
String totalString = new Double(total).toString(); Node totalNode = doc.createTextNode(totalString); Element totalElement = doc.createElement("total");
totalElement.appendChild(totalNode); thisOrder.insertBefore(totalElement, thisOrder.getFirstChild()); } stepThrough(root); ... 回页首 删除节点 应用程序不是替换元素的文本,而是将它完全删除。在这个例子中,应用程序检查商品是否有现货。如果没有,它将从订单中删除该商品,而不是将其添加到汇总中。 在将商品成本添加到价款汇总中之前,应用程序会检查 instock 属性的值。如果该值为 N,则不是将它添加到价款汇总中,而是将它完全删除。为此,它使用了 removeChild() 方法,不过首先要使用 getParentNode() 来确定 orderItem 的父节点。实际的 Node 已从文档中删除,但是该方法还是将它作为一个对象返回,以便能根据需要移动它。 ... //Get this item as an Element
Element thisOrderItem = (Element)orderItems.item(itemNum); if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Node deadNode =
thisOrderItem.getParentNode().removeChild(thisOrderItem); } else { //Get pricing information for this Item
String thisPrice =
thisOrderItem.getElementsByTagName("price").item(0)
.getFirstChild().getNodeValue();
...
total = total + thisItemTotal; } }
String totalString = new Double(total).toString(); ... 回页首 替换节点 当然,因为某件商品订货不足(backordered)而删除该商品是没有多大意义的。相反,应用程序会使用一个 backordered 项来替换它。 应用程序并不使用 removeChild(),而是简单地使用了replaceChild()。注意在此示例中,该方法还会返回旧的节点,以便能根据需要将它移往别处,或许可以移动到一个新 Document,它列出了所有订单不足的商品。 注意由于没有内容被添加到该元素,因此它是一个空元素。空元素没有内容,并且可以用一种特殊的简写来表示: <backordered /> 有了斜线(/),就不再需要结束标签(</backordered>)。 ... if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Element backElement = doc.createElement("backordered"); Node deadNode = thisOrderItem.getParentNode()
. replaceChild ( backElement, thisOrderItem); } else {
... 回页首 创建和设置属性 那么,如果没有标志表明一个 backordered 元素是什么商品,那该怎么正确处理它呢?纠正信息缺乏的方法之一是向元素添加属性。 应用程序首先创建一个 itemid 属性。接下来,它根据原先的 item 元素确定 itemid 的值,然后再自己设置该属性的值。最后,它把该元素添加到文档,就像以前一样。 ... if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Element backElement = doc.createElement("backordered"); backElement.setAttributeNode(doc.createAttribute("itemid")); String itemIdString =
thisOrderItem.getAttributeNode("itemid").getNodeValue();
backElement.setAttribute("itemid", itemIdString); Node deadNode = thisOrderItem.getParentNode().replaceChild(backElement,
thisOrderItem); } else {
... 需要重点注意的是,如果具有该名称的节点不存在,setAttribute() 就会创建一个属性节点,因此在本例中应用程序可以完全略过 createAttribute()。 回页首 删除属性 应用程序还可以删除属性。例如,在输出中显示客户信用限额信息也许是不可取的,因此应用程序可以临时地将该属性从文档中删除。 删除信息是相当简单的,只需使用 removeAttribute() 来删除数据。 ... Element thisOrder = (Element)orders.item(orderNum); Element customer =
(Element)thisOrder.getElementsByTagName("customerid")
.item(0);
customer.removeAttribute("limit"); NodeList orderItems = thisOrder.getElementsByTagName("item");
... 然而接下来的步骤要用到限额信息,因此要在继续之前删除这个最新的更改。 输出文档 准备数据 到目前为止,本教程已考察了如何接受、使用和操作 XML 数据。要完成这个周期,您还必须能够输出 XML。 对于本教程中的情况,目标输出是一个文件,该文件简单地列出每个订单、依据客户信用限额来确定的订单处理情况,以及 customerid。 <?xml version="1.0" encoding="UTF-8"?>
<processedOrders>
<order>
<status>PROCESSED</status>
<customerid>2341</customerid>
<amount>874.00</amount>
</order>
<order>
<status>REJECTED</status>
<customerid>251222</customerid>
<amount>200.00</amount>
</order>
</processedOrders> 应用程序首先创建要输出的 Document 对象。为方便起见,可以使用创建原先的 Document 的 DocumentBuilder 来创建新的 Document 对象。 ... public static void main (String args[]) {
File docFile = new File("orders.xml");
Document doc = null;
Document newdoc = null;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile); newdoc = db.newDocument(); } catch (Exception e) {
System.out.print("Problem parsing the file: "+e.getMessage());
}
...
thisOrder.insertBefore(totalElement, thisOrder.getFirstChild());
} Element newRoot = newdoc.createElement("processedOrders"); NodeList processOrders = doc.getElementsByTagName("order");
for (int orderNum = 0;
orderNum < processOrders.getLength();
orderNum++) { Element thisOrder = (Element)processOrders.item(orderNum); Element customerid =
(Element)thisOrder.getElementsByTagName("customerid")
.item(0);
String limit = customerid.getAttributeNode("limit").getNodeValue(); String total = thisOrder.getElementsByTagName("total").item(0)
.getFirstChild().getNodeValue(); double limitDbl = new Double(limit).doubleValue();
double totalDbl = new Double(total).doubleValue(); Element newOrder = newdoc.createElement("order"); Element newStatus = newdoc.createElement("status");
if (totalDbl > limitDbl) {
newStatus.appendChild(newdoc.createTextNode("REJECTED"));
} else {
newStatus.appendChild(newdoc.createTextNode("PROCESSED"));
} Element newCustomer = newdoc.createElement("customerid");
String oldCustomer = customerid.getFirstChild().getNodeValue();
newCustomer.appendChild(newdoc.createTextNode(oldCustomer)); Element newTotal = newdoc.createElement("total");
newTotal.appendChild(newdoc.createTextNode(total)); newOrder.appendChild(newStatus);
newOrder.appendChild(newCustomer);
newOrder.appendChild(newTotal); newRoot.appendChild(newOrder);
} newdoc.appendChild(newRoot); System.out.print(newRoot.toString()); ... 在处理 orders.xml 之后,应用程序创建了一个新的元素 processedOrders,这个新的元素最终将成为新文档的根元素。然后它遍历每个订单。对于每个订单,它都提取其 total 和 limit 信息。 接下来,应用程序为订单创建新元素:order、status、customerid 和 amount。它根据汇总款项是否超过客户的信用限额来填充 status,然后相应地填充其他元素。 一旦应用程序为订单创建了元素,它就必须将这些元素整合起来。它首先向新的 order 元素添加状态、客户信息和汇总款项。然后它把新的 order 添加到 newRoot 元素。 尽管如此,newRoot 元素并没有实际连接到某个父节点。当应用程序完成所有订单的处理时,newRoot 就被追加到新的文档。 最后,应用程序将 newRoot 转换为一个 String,并简单地将它发送到 System.out,从而输出数据。 注意:在节点上,一些版本的 Java 代码不支持这个版本的 toString()。如果出现这种情况,使用 恒等转换 中展示的技巧就可以查看节点的内容。 回页首 创建 XML 文件 现在应用程序已经创建了新的信息,将这个信息输出到某个文件是很简单的。 对数据也使用了相同的逻辑,只不过应用程序不是将它输出到屏幕,而是将它输出到一个文件。 要注意的一件重要事情是,由于 XML 数据是文本,因此可以通过任何方式来对它进行格式化。例如,您可以创建 stepThroughAll() 的变体,它将创建缩进的或完美输出的版本。记住,这将创建额外的空白(文本)节点。 ... import java.io.FileWriter;
... try
{
File newFile = new File("processedOrders.xml");
FileWriter newFileStream = new FileWriter(newFile); newFileStream.write ("<?xml version=\"1.0\"?>"); newFileStream.write ("<!DOCTYPE
"+doc.getDoctype().getName()+" "); if (doc.getDoctype().getSystemId() != null) { newFileStream.write (" SYSTEM "); newFileStream.write (doc.getDoctype().getSystemId());
}
if (doc.getDoctype().getPublicId() != null) { newFileStream.write (" PUBLIC "); newFileStream.write (doc.getDoctype().getPublicId());
} newFileStream.write (">"); newFileStream.write (newRoot.toString()); newFileStream.close(); } catch (IOException e) {
System.out.println("Can't write new file.");
}
... 回页首 恒等转换 对于像本教程中这样简单 Document,很容易设想输出 XML 也是简单的,但是需要考虑许多可能导致问题复杂化的因素 ―― 比如像下面这样很少出现的情况:文件的内容要受到某个 DTD 或模式的影响。通常,最好使用考虑了所有这些可能性的应用程序。 开发人员经常选择用于序列化 Document 的一种方法就是创建一个恒等转换。这是一个不包括样式表的 XSL 转换。例如: ... import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileOutputStream;
...
newdoc.appendChild(newRoot); try {
DOMSource source = new DOMSource(newdoc);
StreamResult result =
new StreamResult(new FileOutputStream("processed.xml")); TransformerFactory transFactory = TransformerFactory.newInstance();
Transformer transformer = transFactory.newTransformer(); transformer.transform(source, result);
} catch (Exception e){
e.printStackTrace();
}
}
} 这里创建了一个源和一个结果,但是由于是在使用恒等转换,因此没有创建一个对象来代表样式表。如果这是实际的转换,则可以在 Transformer 的创建过程中使用样式表。否则,Transformer 将简单地接受源(Document),然后将它发送到结果
将文件解析为文档

三步过程

为了使用 XML 文件中的信息,必须解析文件以创建一个 Document 对象。

Document 对象是一个接口,因而不能直接将它实例化;一般情况下,应用程序会相应使用一个工厂。准确的过程因实现而异,但是基本思想是相同的。(同样,Level 3 标准化了这个任务)。在这个例子 Java 环境中,解析文件是一个三步过程:

创建 DocumentBuilderFactory。DocumentBuilderFactory 对象创建 DocumentBuilder。
创建 DocumentBuilder。DocumentBuilder 执行实际的解析以创建 Document 对象。
解析文件以创建 Document 对象。
现在您可以开始构建应用程序了。 回页首 基本的应用程序 首先创建一个基本的应用程序,即一个名为 OrderProcessor 的类。 import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import org.w3c.dom.Document; public class OrderProcessor {
public static void main (String args[]) {
File docFile = new File("orders.xml");
Document doc = null;
try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile); } catch (Exception e) {
System.out.print("Problem parsing the file: "+e.getMessage());
}
}
} 首先,Java 代码导入必要的类,然后它创建 OrderProcessor 应用程序。本教程中的例子仅处理一个文件,因此为简洁起见,应用程序包含了对该文件的直接引用。 因此 Document 对象可以在以后使用,应用程序把它定义在 try-catch 块之外。 在 try-catch 块中,应用程序创建了 DocumentBuilderFactory,然后再使用它来创建 DocumentBuilder。 最后,DocumentBuilder 解析文件以创建 Document。 回页首 解析器设置 使用 DocumentBuilder 创建解析器的优点之一在于能够控制 DocumentBuilderFactory 创建的解析器上的各种设置。例如,可以设置解析器验证文档: ... try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setValidating(true); DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile);
} catch (Exception e) {
... Java 的 DOM Level 2 实现允许通过以下方法控制解析器的参数: setCoalescing():决定解析器是否要将 CDATA 节点转换为文本,以及是否要和周围的文本节点合并(如果适用的话)。其默认值为 false。
setExpandEntityReferences():确定是否要展开外部实体引用。如果为 true,外部数据将插入文档。其默认值为 true。(请参阅 参考资料 以了解关于使用外部实体的技巧)。
setIgnoringComments():确定是否要忽略文件中的注释。其默认值为 false。
setIgnoringElementContentWhitespace():确定是否要忽略元素内容中的空白(类似于浏览器对待 HTML 的方式)。其默认值为 false。
setNamespaceAware():确定解析器是否要注意名称空间信息。其默认值为 false。
setValidating():默认情况下,解析器不验证文档。将这个参数设置为 true 可打开验证功能。
回页首 解析器异常 由于在创建解析器时存在所有这些可能性,其中许多地方都可能会出错。正如这里的例子所表明的,应用程序把所有这些内容转储到一个单一的通用 Exception 中,就调试而言,这样可能不是很有帮助。 为更好地查明问题,您可以捕捉与创建和使用解析器的各方面相关的特定异常: ... try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile);
} catch (javax.xml.parsers.ParserConfigurationException pce) {
System.out.println("The parser was not configured correctly.");
System.exit(1);
} catch (java.io.IOException ie) {
System.out.println("Cannot read input file.");
System.exit(1);
} catch (org.xml.sax.SAXException se) {
System.out.println("Problem parsing the file.");
System.exit(1);
} catch (java.lang.IllegalArgumentException ae) {
System.out.println("Please specify an XML source.");
System.exit(1); }
... 一旦解析器已创建了一个 Document,应用程序就能单步调试它以检查数据。 单步调试文档 获取根元素 一旦解析了文档并创建了一个 Document,应用程序就能单步调试该结构以审核、查找或显示信息。这种导航功能是将要在 Document 上执行的许多操作的基础。 对文档的单步调试首先从根元素开始。格式良好的文档仅有一个根元素,也称为 DocumentElement。应用程序首先检索这个元素。 import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import org.w3c.dom.Document; import org.w3c.dom.Element; public class OrderProcessor {
...
System.exit(1);
} //STEP 1: Get the root element Element root = doc.getDocumentElement();
System.out.println("The root element is " + root.getNodeName()); }
} 编译和运行这个应用程序将输出根元素的名称 orders。 回页首 获取节点的孩子 一旦应用程序确定了根元素,它就把根元素的孩子的列表作为一个 NodeList 来检索。NodeList 类是一系列的项,应用程序将逐个迭代这些项。 在本例中,为简洁起见,应用程序通过仅显示有多少元素出现在生成的 NodeList 中,从而获取孩子节点和验证检索结果。 注意这里的文档仅有两个元素,但是 NodeList 包含五个孩子,包括包含换行的三个文本节点 ―― 还要注意节点和元素在 DOM 中不是等价的。其他三个节点是包含换行的文本节点。 ... import org.w3c.dom.NodeList; ...
//STEP 1: Get the root element
Element root = doc.getDocumentElement();
System.out.println("The root element is "+root.getNodeName()); //STEP 2: Get the children
NodeList children = root.getChildNodes();
System.out.println("There are "+children.getLength()
+" nodes in this document."); }
} 回页首 使用 getFirstChild() 和 getNextSibling() 父子和兄弟关系提供了迭代某个节点的所有孩子的替代方法,它在某些场合下可能更为适宜,比如在这些关系和孩子的出现顺序对理解数据至关重要的时候。 在 Step 3 中,for 循环首先从根元素的第一个孩子开始。应用程序迭代第一个孩子的所有兄弟,直至已全部对它们求值。 每次应用程序执行该循环,它都要检索一个 Node 对象,输出其名称和值。注意 orders 的五个孩子包括 orders 元素和三个文本节点。还要注意元素具有一个 null 值,而不是预期的文本。包含实际内容作为其值的,是作为元素的孩子的文本节点。 ... import org.w3c.dom.Node; ... //STEP 3: Step through the children
for (Node child = root.getFirstChild();
child != null;
child = child.getNextSibling())
{
System.out.println(start.getNodeName()+" = "
+start.getNodeValue());
} }
}
... 回页首 在多层孩子中递归 使用 getFirstChild() 和 getNextSibling() 中的代码显示了第一层孩子,但那远远不是整个文档。为了看到所有元素,必须将前一个例子中的功能转换为一个方法并递归地调用。 应用程序首先从根元素开始,向屏幕输出名称和值。然后应用程序就像以前一样遍历根元素的每个孩子。但是对于每个孩子,应用程序还会遍历该孩子的每个孩子,即检查根元素的所有孩子和孙子。 ... public class OrderProcessor { private static void stepThrough (Node start)
{ System.out.println(start.getNodeName()+" = "+start.getNodeValue()); for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
stepThrough(child); }
} public static void main (String args[]) {
File docFile = new File("orders.xml"); ...
System.out.println("There are "+children.getLength()
+" nodes in this document."); //STEP 4: Recurse this functionality
stepThrough(root); }
} 回页首 包含属性 到目前为止,所编写的 stepThrough() 方法能够遍历大多数类型的节点,但是它完全地遗漏了属性,因为属性不是任何节点的孩子。为了显示属性,可修改 stepThrough() 来检查元素节点的属性。 下面修改过的代码检查每个节点的输出,通过将节点的 nodeType 与常量 ELEMENT_NODE 作比较,从而确定它是否为一个元素。Node 对象带有成员常量,它们表示每种类型的节点,比如 ELEMENT_NODE 或 ATTRIBUTE_NODE。如果 nodeType 与 ELEMENT_NODE 匹配,它就是一个元素。 对于找到的每个元素,应用程序都会创建一个包含该元素的所有属性的 NamedNodeMap。应用程序能够迭代 NamedNodeMap,输出每个属性的名称和值,就像它迭代 NodeList 一样。 ... import org.w3c.dom.NamedNodeMap; ...
private static void stepThroughAll (Node start)
{
System.out.println(start.getNodeName()+" = "+start.getNodeValue()); if (start.getNodeType() == start.ELEMENT_NODE)
{
NamedNodeMap startAttr = start.getAttributes();
for (int i = 0;
i < startAttr.getLength();
i++) {
Node attr = startAttr.item(i);
System.out.println(" Attribute: "+ attr.getNodeName()
+" = "+attr.getNodeValue());
}
} for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
stepThroughAll(child);
}
} 编辑文档 更改节点的值 检查一下 XML 文档内容的常量是有用的,但是在处理全功能的应用程序时,您可能需要更改数据以添加、编辑、移动或删除信息。编辑数据的能力对创建新 XML 文档的过程也是至关重要的。这类更改中最简单的情况就是更改元素的文本内容。 这里的目标是更改某个元素的文本节点的值,在此例中是将每个 order 的 status 设置为 “processed”,然后向屏幕输出新的值。 使用起始节点(root)以及要更改的元素名称和要更改到的目标值作为参数,调用 changeOrder() 方法。 changeOrder() 首先检查节点的名称,以确定它是否为要编辑的元素之一。如果是,应用程序需要更改的不是这个节点的值,而是这个节点的第一个孩子的值,因为这第一个孩子才是实际包含内容的文本节点。 在任一种情况下,应用程序都要检查每个孩子,就像它在第一次单步调试文档时一样。 当完成更改时,更改后的值将使用 getElementsByTagName() 来检查。这个方法返回具有指定名称(比如 status)的所有孩子的列表。然后应用程序就能够检查该列表中的值,以检验 changeOrder() 方法调用的有效性。 ... public class OrderProcessor { private static void changeOrder (Node start,
String elemName,
String elemValue)
{
if (start.getNodeName().equals(elemName)) {
start.getFirstChild().setNodeValue(elemValue);
} for (Node child = start.getFirstChild();
child != null;
child = child.getNextSibling())
{
changeOrder(child, elemName, elemValue);
}
} ...
public static void main (String args[]) {
... // Change text content changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName("status");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{
System.out.println(orders.item(orderNum)
.getFirstChild().getNodeValue());
} }
} 注意应用程序挑选了 status 节点,虽然这些节点是根元素的孙子,而不是直接的孩子。getElementsByTagName() 单步调试文档,并查找具有特定名称的所有元素。 回页首 添加节点:准备数据 与更改现有节点不同,有时添加一个节点是必要的,而您可以通过多种方式来做到这点。在本例中,应用程序汇总每个 order 的价格,然后添加一个 total 元素。它通过接受每个订单,循环迭代它的每件商品以获取商品价格,然后汇总这些价格,从而获得总价格。然后应用程序向订单添加一个新的元素(参见下面的代码清单)。 首先,应用程序检索 order 元素,就像它检索 status 元素一样。然后它循环迭代这其中的每个元素。 对于这其中的每个 order,应用程序都需要其 item 元素的一个 NodeList,因此应用程序必须首先将 order Node 强制转换为 Element,才能使用 getElementsByTagName()。 然后应用程序可以循环迭代 item 元素,以寻找选定的 order。应用程序将每个 item 强制转换为 Element,以便能够根据名称检索 price 和 qty。它是通过使用 getElementsByTagName() 来实现的。因为每件商品只有一个名称,它可以直接转到 item(0),即生成的 NodeList 中的第一个条目。这第一个条目表示 price(或 qty)元素。它就是从那里获得文本节点的值的。 文本节点的值是 String 值,应用程序然后会将它转换为一个 double 值,以允许进行必要的数学运算。 当应用程序检查完每个订单的每件商品时,total 就是一个代表总价格的 double 值。然后 total 被转换为 String 值,以便能将它用作新元素的内容,<total> 最终加入了 order。 ... changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName(" order ");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{ Element thisOrder = (Element)orders.item(orderNum);
NodeList orderItems = thisOrder.getElementsByTagName("item");
double total = 0;
for (int itemNum = 0;
itemNum < orderItems.getLength();
itemNum++) { // Total up cost for each item and
// add to the order total //Get this item as an Element
Element thisOrderItem = (Element)orderItems.item(itemNum); //Get pricing information for this Item
String thisPrice =
thisOrderItem.getElementsByTagName("price").item(0)
.getFirstChild().getNodeValue();
double thisPriceDbl = new Double(thisPrice).doubleValue(); //Get quantity information for this Item
String thisQty =
thisOrderItem.getElementsByTagName("qty").item(0)
.getFirstChild().getNodeValue();
double thisQtyDbl = new Double(thisQty).doubleValue(); double thisItemTotal = thisPriceDbl*thisQtyDbl;
total = total + thisItemTotal;
}
String totalString = new Double(total).toString(); }
... 回页首 添加节点:向文档添加节点 您可以用许多方法创建新 Node,而本例将使用其中的几种方法。首先,Document 对象能够创建新的值为 totalString 的文本节点。新的 Node 现在已经存在了,但是还没在任何地方实际连接到 Document。新的 total 元素是以类似的方式创建的,起初也没有实质性的内容。 添加节点的另一种方法是使用 appendChild(),就像这里将节点添加到新的 total 元素一样。 最后,应用程序可以使用 insertBefore() 来向 Document 添加新的元素,同时指定新的 Node,然后指定新 Node 之后的那个 Node。 单步调试文档将检验这些更改。 ... changeOrder(root, "status", "processing");
NodeList orders = root.getElementsByTagName("order");
for (int orderNum = 0;
orderNum < orders.getLength();
orderNum++)
{
...
String totalString = new Double(total).toString(); Node totalNode = doc.createTextNode(totalString); Element totalElement = doc.createElement("total");
totalElement.appendChild(totalNode); thisOrder.insertBefore(totalElement, thisOrder.getFirstChild()); } stepThrough(root); ... 回页首 删除节点 应用程序不是替换元素的文本,而是将它完全删除。在这个例子中,应用程序检查商品是否有现货。如果没有,它将从订单中删除该商品,而不是将其添加到汇总中。 在将商品成本添加到价款汇总中之前,应用程序会检查 instock 属性的值。如果该值为 N,则不是将它添加到价款汇总中,而是将它完全删除。为此,它使用了 removeChild() 方法,不过首先要使用 getParentNode() 来确定 orderItem 的父节点。实际的 Node 已从文档中删除,但是该方法还是将它作为一个对象返回,以便能根据需要移动它。 ... //Get this item as an Element
Element thisOrderItem = (Element)orderItems.item(itemNum); if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Node deadNode =
thisOrderItem.getParentNode().removeChild(thisOrderItem); } else { //Get pricing information for this Item
String thisPrice =
thisOrderItem.getElementsByTagName("price").item(0)
.getFirstChild().getNodeValue();
...
total = total + thisItemTotal; } }
String totalString = new Double(total).toString(); ... 回页首 替换节点 当然,因为某件商品订货不足(backordered)而删除该商品是没有多大意义的。相反,应用程序会使用一个 backordered 项来替换它。 应用程序并不使用 removeChild(),而是简单地使用了replaceChild()。注意在此示例中,该方法还会返回旧的节点,以便能根据需要将它移往别处,或许可以移动到一个新 Document,它列出了所有订单不足的商品。 注意由于没有内容被添加到该元素,因此它是一个空元素。空元素没有内容,并且可以用一种特殊的简写来表示: <backordered /> 有了斜线(/),就不再需要结束标签(</backordered>)。 ... if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Element backElement = doc.createElement("backordered"); Node deadNode = thisOrderItem.getParentNode()
. replaceChild ( backElement, thisOrderItem); } else {
... 回页首 创建和设置属性 那么,如果没有标志表明一个 backordered 元素是什么商品,那该怎么正确处理它呢?纠正信息缺乏的方法之一是向元素添加属性。 应用程序首先创建一个 itemid 属性。接下来,它根据原先的 item 元素确定 itemid 的值,然后再自己设置该属性的值。最后,它把该元素添加到文档,就像以前一样。 ... if (thisOrderItem.getAttributeNode("instock")
.getNodeValue().equals("N")) { Element backElement = doc.createElement("backordered"); backElement.setAttributeNode(doc.createAttribute("itemid")); String itemIdString =
thisOrderItem.getAttributeNode("itemid").getNodeValue();
backElement.setAttribute("itemid", itemIdString); Node deadNode = thisOrderItem.getParentNode().replaceChild(backElement,
thisOrderItem); } else {
... 需要重点注意的是,如果具有该名称的节点不存在,setAttribute() 就会创建一个属性节点,因此在本例中应用程序可以完全略过 createAttribute()。 回页首 删除属性 应用程序还可以删除属性。例如,在输出中显示客户信用限额信息也许是不可取的,因此应用程序可以临时地将该属性从文档中删除。 删除信息是相当简单的,只需使用 removeAttribute() 来删除数据。 ... Element thisOrder = (Element)orders.item(orderNum); Element customer =
(Element)thisOrder.getElementsByTagName("customerid")
.item(0);
customer.removeAttribute("limit"); NodeList orderItems = thisOrder.getElementsByTagName("item");
... 然而接下来的步骤要用到限额信息,因此要在继续之前删除这个最新的更改。 输出文档 准备数据 到目前为止,本教程已考察了如何接受、使用和操作 XML 数据。要完成这个周期,您还必须能够输出 XML。 对于本教程中的情况,目标输出是一个文件,该文件简单地列出每个订单、依据客户信用限额来确定的订单处理情况,以及 customerid。 <?xml version="1.0" encoding="UTF-8"?>
<processedOrders>
<order>
<status>PROCESSED</status>
<customerid>2341</customerid>
<amount>874.00</amount>
</order>
<order>
<status>REJECTED</status>
<customerid>251222</customerid>
<amount>200.00</amount>
</order>
</processedOrders> 应用程序首先创建要输出的 Document 对象。为方便起见,可以使用创建原先的 Document 的 DocumentBuilder 来创建新的 Document 对象。 ... public static void main (String args[]) {
File docFile = new File("orders.xml");
Document doc = null;
Document newdoc = null;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(docFile); newdoc = db.newDocument(); } catch (Exception e) {
System.out.print("Problem parsing the file: "+e.getMessage());
}
...
thisOrder.insertBefore(totalElement, thisOrder.getFirstChild());
} Element newRoot = newdoc.createElement("processedOrders"); NodeList processOrders = doc.getElementsByTagName("order");
for (int orderNum = 0;
orderNum < processOrders.getLength();
orderNum++) { Element thisOrder = (Element)processOrders.item(orderNum); Element customerid =
(Element)thisOrder.getElementsByTagName("customerid")
.item(0);
String limit = customerid.getAttributeNode("limit").getNodeValue(); String total = thisOrder.getElementsByTagName("total").item(0)
.getFirstChild().getNodeValue(); double limitDbl = new Double(limit).doubleValue();
double totalDbl = new Double(total).doubleValue(); Element newOrder = newdoc.createElement("order"); Element newStatus = newdoc.createElement("status");
if (totalDbl > limitDbl) {
newStatus.appendChild(newdoc.createTextNode("REJECTED"));
} else {
newStatus.appendChild(newdoc.createTextNode("PROCESSED"));
} Element newCustomer = newdoc.createElement("customerid");
String oldCustomer = customerid.getFirstChild().getNodeValue();
newCustomer.appendChild(newdoc.createTextNode(oldCustomer)); Element newTotal = newdoc.createElement("total");
newTotal.appendChild(newdoc.createTextNode(total)); newOrder.appendChild(newStatus);
newOrder.appendChild(newCustomer);
newOrder.appendChild(newTotal); newRoot.appendChild(newOrder);
} newdoc.appendChild(newRoot); System.out.print(newRoot.toString()); ... 在处理 orders.xml 之后,应用程序创建了一个新的元素 processedOrders,这个新的元素最终将成为新文档的根元素。然后它遍历每个订单。对于每个订单,它都提取其 total 和 limit 信息。 接下来,应用程序为订单创建新元素:order、status、customerid 和 amount。它根据汇总款项是否超过客户的信用限额来填充 status,然后相应地填充其他元素。 一旦应用程序为订单创建了元素,它就必须将这些元素整合起来。它首先向新的 order 元素添加状态、客户信息和汇总款项。然后它把新的 order 添加到 newRoot 元素。 尽管如此,newRoot 元素并没有实际连接到某个父节点。当应用程序完成所有订单的处理时,newRoot 就被追加到新的文档。 最后,应用程序将 newRoot 转换为一个 String,并简单地将它发送到 System.out,从而输出数据。 注意:在节点上,一些版本的 Java 代码不支持这个版本的 toString()。如果出现这种情况,使用 恒等转换 中展示的技巧就可以查看节点的内容。 回页首 创建 XML 文件 现在应用程序已经创建了新的信息,将这个信息输出到某个文件是很简单的。 对数据也使用了相同的逻辑,只不过应用程序不是将它输出到屏幕,而是将它输出到一个文件。 要注意的一件重要事情是,由于 XML 数据是文本,因此可以通过任何方式来对它进行格式化。例如,您可以创建 stepThroughAll() 的变体,它将创建缩进的或完美输出的版本。记住,这将创建额外的空白(文本)节点。 ... import java.io.FileWriter;
... try
{
File newFile = new File("processedOrders.xml");
FileWriter newFileStream = new FileWriter(newFile); newFileStream.write ("<?xml version=\"1.0\"?>"); newFileStream.write ("<!DOCTYPE
"+doc.getDoctype().getName()+" "); if (doc.getDoctype().getSystemId() != null) { newFileStream.write (" SYSTEM "); newFileStream.write (doc.getDoctype().getSystemId());
}
if (doc.getDoctype().getPublicId() != null) { newFileStream.write (" PUBLIC "); newFileStream.write (doc.getDoctype().getPublicId());
} newFileStream.write (">"); newFileStream.write (newRoot.toString()); newFileStream.close(); } catch (IOException e) {
System.out.println("Can't write new file.");
}
... 回页首 恒等转换 对于像本教程中这样简单 Document,很容易设想输出 XML 也是简单的,但是需要考虑许多可能导致问题复杂化的因素 ―― 比如像下面这样很少出现的情况:文件的内容要受到某个 DTD 或模式的影响。通常,最好使用考虑了所有这些可能性的应用程序。 开发人员经常选择用于序列化 Document 的一种方法就是创建一个恒等转换。这是一个不包括样式表的 XSL 转换。例如: ... import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileOutputStream;
...
newdoc.appendChild(newRoot); try {
DOMSource source = new DOMSource(newdoc);
StreamResult result =
new StreamResult(new FileOutputStream("processed.xml")); TransformerFactory transFactory = TransformerFactory.newInstance();
Transformer transformer = transFactory.newTransformer(); transformer.transform(source, result);
} catch (Exception e){
e.printStackTrace();
}
}
} 这里创建了一个源和一个结果,但是由于是在使用恒等转换,因此没有创建一个对象来代表样式表。如果这是实际的转换,则可以在 Transformer 的创建过程中使用样式表。否则,Transformer 将简单地接受源(Document),然后将它发送到结果