2008-11-07

学习DOM

  虽然大概已经了解了DOM,不过对我来说org.w3c.dom的奇怪扫描方式让我对其易使用性大有疑惑,所以还是必须要学习一下DOM。
  这篇文章是在学习DOM的过程中整理出来的资料:

  DOM 文档是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档并且构造层次结构,然后才能做其他事情。由于它是基于信息层次的,因而 DOM 被认为是基于树基于对象的。
  XML 文件的基本组成部分包括:

  • XML 声明:基本的声明 将这个文件定义为 XML 文档。在声明中指定一种字符编码的情况并不鲜见,如下所示。通过这种方式,不管该 XML 文件使用的语言或字符编码是什么,只要解析器理解特定的编码,它就能够正确地读取该 XML 文件。
  • DOCTYPE 声明:XML 是人机之间交换信息的便利手段,但是要使它能够顺利地工作,必须要有一个公共的词汇表。可选的 DOCTYPE 声明可用于指定一个应该用来与此文件做比较的文档(在本例中为 orders.dtd),以确保不会产生任何混淆或丢失信息(例如,丢失一个 userid 或错误拼写某个元素名称)。以这种方式处理过的文档称为有效的文档。成功的有效性检查并不是 XML 所必需的,后面的例子实际上从文档中省略了 DOCTYPE 声明。
  • 数据本身:XML 文档中的数据必须包含在单个根元素内,比如下面的 orders 元素。要使 XML 文档得到处理,它必须是格式良好的(well-formed)


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ORDERS SYSTEM "orders.dtd">

<orders>

<order>
<customerid limit="1000">12341</customerid>
<status>pending</status>
<item instock="Y" itemid="SA15">
<name>Silver Show Saddle, 16 inch</name>
<price>825.00</price>
<qty>1</qty>
</item>
<item instock="N" itemid="C49">
<name>Premium Cinch</name>
<price>49.00</price>
<qty>1</qty>
</item>
</order>
<order>
<customerid limit="150">251222</customerid>
<status>pending</status>
<item instock="Y" itemid="WB78">
<name>Winter Blanket (78 inch)</name>
<price>20</price>
<qty>10</qty>
</item>
</order>

</orders>

  DOM 本质上是节点的集合。 由于一个文档中可能包含不同类型的信息,因此要定义不同类型的节点。
  

基本的节点类型:文档、原始、属性和文本

XML 中最常见的节点类型包括:

  • 元素:元素是 XML 的基本构造模块。通常,元素拥有子元素、文本节点,或两者的组合。元素节点也是能够拥有属性的唯一节点类型。
  • 属性:属性节点包含关于元素节点的信息,但是并不实际认为是元素的孩子,比如在下面的例子中: <customerid limit="1000">12341</customerid>
  • 文本:文本节点就是名副其实的文本。它可以由更多信息组成,也可以只包含空白。
  • 文档:文档节点是文档中其他所有节点的父亲。
-----------------------------------------
接下来是处理XML文档的说明了:

将文件解析为document

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

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

  1. 创建 DocumentBuilderFactory。 DocumentBuilderFactory 对象创建 DocumentBuilder。
  2. 创建 DocumentBuilder。 DocumentBuilder 执行实际的解析以创建 Document 对象。
  3. 解析文件以创建 Document 对象。

现在您可以开始构建应用程序了。

一个例子:

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());
}
}
}

  这里应用程序创建了 DocumentBuilderFactory,然后再使用它来创建 DocumentBuilder。 最后,DocumentBuilder 解析文件以创建 Document。

解析器设置:

  使用 DocumentBuilder 创建解析器的优点之一在于能够控制 DocumentBuilderFactory 创建的解析器上的各种设置。例如,可以设置解析器验证文档:


Java 的 DOM Level 2 实现允许通过以下方法控制解析器的参数:

  • setCoalescing():决定解析器是否要将 CDATA 节点转换为文本,以及是否要和周围的文本节点合并(如果适用的话)。其默认值为 false。
  • setExpandEntityReferences(): 确定是否要展开外部实体引用。如果为 true,外部数据将插入文档。其默认值为 true 。(请参阅参考资料以了解关于使用外部实体的技巧。)
  • setIgnoringComments():确定是否要忽略文件中的注释。其默认值为 false。
  • setIgnoringElementContentWhitespace():确定是否要忽略元素内容中的空白(类似于浏览器对待 HTML 的方式)。其默认值为 false。
  • setNamespaceAware():确定解析器是否要注意名称空间信息。其默认值为 false。
  • setValidating():默认情况下,解析器不验证文档。将这个参数设置为 true 可打开验证功能。

单步调试文档:

  获取根元素:

  一旦解析了文档并创建了一个 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());

 //STEP 2: Get the children
 NodeList children = root.getChildNodes();
 System.out.println("There are "+children.getLength()
+" nodes in this document.");


//STEP 3: Step through the children
for (Node child = root.getFirstChild();
child != null;
child = child.getNextSibling())
{
System.out.println(start.getNodeName()+" = "
+start.getNodeValue());
}


}
}

获取节点的子节点:

  一旦应用程序确定了根元素,它就把根元素的子节点的列表作为一个 NodeList 来检索。NodeList 类是一系列的项,应用程序将逐个迭代这些项。 在本例中,为简洁起见,应用程序通过仅显示有多少元素出现在结果 NodeList中,从而获取子节点和验证检索结果。

  注意这里的文档仅有两个元素,但是 NodeList 包含五个子节点,包括包含换行的三个文本节点 ―― 还要注意节点和元素在 DOM 中不是等价的。


使用 getFirstChild() 和 getNextSibling():

  父子和兄弟关系提供了迭代某个节点的所有孩子的替代方法,它在某些场合下可能更为适宜,比如在这些关系和孩子的出现顺序对理解数据至关重要的时候。

  在 Step 3 中,for 循环首先从根元素的第一个子节点开始。 应用程序迭代第一个孩子的所有兄弟,直至已全部对它们求值。

  每次应用程序执行该循环,它都要检索一个 Node 对象,输出其名称和值。注意 orders 的五个孩子包括 order 元素和三个文本节点。还要注意元素具有一个 null 值,而不是预期的文本。包含实际内容作为其值的,是作为元素的孩子的文本节点。

  还可以通过使用递归来获取所有节点.此外,需要注意的是,在dom中,属性不是任何节点的子节点。用w3c school提供的图示可以理解:

  当你实际使用dom扫描器去获取document的时候,你会发现它会将element之间的空白也作为一个节点包存在文档树中,本来,按照ibm这篇文章的说法是只要设置了setIgnoringElementContentWhitespace(true)就可以让扫描器无视掉这些空白,但是却没有提到,要让这个功能正常工作还必须要有定义过的dtd文件,同理,Node.normalized()方法也是一样的。关于文档标准化的内容,可以读读这一段:

-----------------------------------------

文档标准化

在 DOM Level 3 中定义的一个新方法是 Document 接口的 normalizeDocument 方法。从方法名可以看出,您可以使用这个方法来标准化文档。在默认情况下,这个方法完成以下工作:

  • 标准化 Text 节点,将相邻的 Text 节点整合为一个 Text 节点。
  • 根据 EntityReference 节点所引用的实体更新它们的内容。
  • 验证和修复文档中的命名空间信息,使命名空间格式是格式良好的。

一定要注意,在这个方法中使用的命名空间标准化算法(在 Appendix B 中定义)只对 namespace-aware 节点起作用,这些节点是使用具有“NS”后缀的方法(例如 createElementNS )创建的。 Namespace unaware 节点,即用 DOM Level 1 方法(如 createElement )创建的节点,与所有依赖于 XML 命名空间的处理不完全兼容。如果在文档中有 DOM Level 1 节点,那么在尝试进行命名空间标准化时, normalizeDocument 会失败并报告一个错误。一般来说,如果希望使用 XML 命名空间,并且要对文档执行任何需要 XML 命名空间支持的操作,那么就不应该用 DOM Level 1 方法创建节点。对于其他的操作,例如针对 XML Schema 重新验证内存中的文档,也是如此。

还可以通过 DOMConfiguration 配置 normalizeDocument ,以便对文档执行其他操作。例如,可以使用这个方法去掉注释,将 CDATASection 节点转换为 Text 节点,或者放弃树中所有命名空间声明属性。还可以通过它一次完成上述所有工作,从而轻松地使文档具有可以自然地映射到 XML Infoset 的形式。下面的程序片段展示了如何用 Document.config 控制 normalizeDocument 。

使用 Document.config 控制 normalizeDocument

// retrieve document configuration
DOMConfiguration config = document.getConfig();
// remove comments from
config.setParameter("comments", false);
// remove namespace declarations
config.setParameter("namespace-declarations", false);
// transform document
core.normalizeDocument();
// put document into a form closest to the XML Infoset
config.setParameter("infoset", true);
// transform document
core.normalizeDocument();

normalizeDocument 方法还允许您用 XML Schema 或者 DTD 对内存中的文档进行重新验证。过去,要在文档修改后对它进行重新验证,必须将它保存为一个文件,再用验证解析器读回去。有了这种新方法,现在就可以通过让 DOM 实现重新验证内存中的文档从而更有效地完成这项工作。为此,首先需要将 DOMConfiguration 的 validate 参数设置为 true 。然后需要实现一个 DOMErrorHandler 对象,验证错误将报告给这个对象,再用 error-handler 参数将这个对象注册到 Document 上。这与对 SAX 解析器所做的工作很类似。最后,可以通过调用 normalizeDocument 检查文档是否有效。

目前,还没有访问 XML Schema Post-Schema Validation Infoset(PSVI)的标准 API。不过,DOM Level 3 允许您获取一些 PSVI 信息。例如,如果希望获得 PSVI 标准化的模式值属性,那么就将 DOMConfiguration 上的 datatype-normalization 和 validate 参数设为“true”,并用经过 XML Schema 标准化的值调用 normalizeDocument 以更新树 ——这意味着文档中属性值和元素内容现在表示的是 PSVI 标准化的模式值属性。

-----------------------------------------

添加节点:

  一般要先构造一个element 并且要把它的值作为子节点append到这个element下。

删除节点:

  直接把要删除的节点remove就可以,子节点会跟着消除


最后就是写入XML文档了。

  如果用递归的方法去遍历文档树是个很不实用的方法。所以看这篇文章:http://www.ibm.com/developerworks/cn/java/l-javaxml/


  在JAXP中所提供的标准的更新原始XML文档的方法就是调用XSLT引擎,亦即使用TransformerFactory和Transformer类。请看下面的Java代码片断:

 //首先创建一个DOMSource对象,该构造函数的参数可以是一个Document对象doc代表更改后的DOM Tree。
DOMSource doms = new DOMSource (doc);

//创建一个File对象,代表DOM Tree所包含的数据的输出介质,这是一个XML文件。
File f = new File ("XMLOutput.xml");
//创建一个StreamResult对象,该构造函数的参数可以取为File对象。
StreamResult sr = new StreamResult (f);
//下面调用JAXP中的XSLT引擎来实现输出DOM Tree中的数据到XML文件中的功能。
//XSLT引擎的输入为DOMSource对象,输出为StreamResut对象。
try
{
//首先创建一个TransformerFactory对象,再由此创建Transformer对象。Transformer类相当于一个XSLT引擎。通常我们使用它来处理XSL文件,但是在这里我们使用它来输出XML文档。
TransformerFactory tf=TransformerFactory.newInstance();
Transformer t=tf.newTransformer ();
//关键的一步, 调用Transformer对象 (XSLT引擎)的transform()方法,该方法的第一个参数是DOMSource对象,第二个参数是StreamResult对象。
t.transform(doms,sr);
}
catch (TransformerConfigurationException tce)
{
System.out.println("Transformer Configuration Exception\n-----");
tce.printStackTrace();
}
catch (TransformerException te)
{
System.out.println ("Transformer Exception\n---------");
te.printStackTrace ();
}

  在实际的应用中,我们可以应用传统的DOM API从XML文档中获取DOM Tree,然后根据实际的需求对DOM Tree执行各种操作,得到最终的Document对象,接下来可以由此Document对象创建DOMSource对象,剩下的事情就是照搬上面的代码 了,程序运行完毕后, XMLOutput.xml就是你所需要的结果(当然了,你可以随意更改StreamResult类构造函数的参数,指定不同的输出介质,而不必是千篇一 律的XML文档)。

  这个方法最大的好处在于可以随心所欲的控制DOM Tree中的内容输出到输出介质中的格式,但是光靠TransformerFactory类和Transformer类并不能实现这个功能,还需要依赖OutputKeys类的帮助。 完整的例子请参考下列文件: AddRecord2.javauser.xml。该例子的运行环境为:Windows XP Professional、JDK 1.3.1。为了能够正常编译运行AddRecord2.java这个程序,你需要到网址 http://java.sun.com去下载安装JAXP 1.1或者Java XML Pack(Java XML Pack已经内含JAXP了)。

OutputKeys类

javax.xml.transform.OutputKeys类和java.util.Properties类配合使用,可以控制JAXP的XSLT引擎(Transformer类)输出XML文档的格式。请看下面的代码片断:

 //首先创建一个TransformerFactory对象,再由此创建Transformer对象。
tf=TransformerFactory.newInstance();
Transformer t=tf.newTransformer ();

//获取Transformser对象的输出属性,亦即XSLT引擎的缺省输出属性,这是一个java.util.Properties对象。
Properties properties = t.getOutputProperties();
//设置新的输出属性:输出字符编码为GB2312,这样可以支持中文字符,XSLT引擎所输出
//的XML文档如果包含了中文字符,可以正常显示,不会出现所谓的"汉字问题"。
//请留意OutputKeys类的字符串常数OutputKeys.ENCODING。
properties.setProperty(OutputKeys.ENCODING,"GB2312");
/更新XSLT引擎的输出属性。
t.setOutputProperties(properties);
//调用XSLT引擎,按照输出属性中的设置,输出DOM Tree中的内容到输出介质中。
t.transform(DOMSource_Object,StreamResult_Object);


从 上面的程序代码,我们不难看出,通过设置XSLT引擎(Transformer类)的输出属性,可以控制DOM Tree中的内容的输出格式,这对于我们定制输出内容是很有帮助的。那么JAXP的XSLT引擎(Transformer类)有那些输出属性可以设置呢? javax.xml.transform.OutputKeys类定义了很多字符串常数,它们都是可以自由设置的输出属性,常用的输出属性如下所示:

  1. public static final java.lang.String METHOD
    可以设为"xml"、"html"、"text"等值。
  2. public static final java.lang.String VERSION
    所遵循规范的版本号,如果METHOD设为"xml",那么它的值应该设为"1.0",如果METHOD设为"html",那么它的值应该设为"4.0",如果METHOD设为"text",那么这个输出属性会被忽略。
  3. public static final java.lang.String ENCODING
    设置输出时所采用的编码方式,比如"GB2312"、"UTF-8"等等,如果将其设置为"GB2312",可以解决所谓的"汉字问题"。
  4. public static final java.lang.String OMIT_XML_DECLARATION
    设置输出到XML文档中时是否忽略XML声明,亦即类似于:
    <?xml version="1.0" standalone="yes" encoding="utf-8" ?>
    这样的代码。它可选的值有"yes"、"no"。
  5. public static final java.lang.String INDENT
    IDENT设定XSLT引擎在输出XML文档时,是否自动添加额外的空格,它可选的值为"yes"、"no"。
  6. public static final java.lang.String MEDIA_TYPE
    MEDIA_TYPE设定输出文档的MIME类型。

如果设定XSLT引擎的输出属性呢?下面我们来总结一下:

首先是获取XSLT引擎(Transformer类)的缺省输出属性的集合,这需要使用Transformer类的getOutputProperties()方法,返回值是一个java.util.Properties对象。

Properties properties = transformer.getOutputProperties();
然后是设定新的输出属性,比如:
properties.setProperty(OutputKeys.ENCODING,"GB2312");
properties.setProperty(OutputKeys.METHOD,"html");
properties.setProperty(OutputKeys.VERSION,"4.0");
………………………………………………………

最后是更新XSLT引擎(Transformer类)的缺省输出属性的集合,这需要使用Transformer类的setOutputProperties()方法,参数是一个java.util.Properties对象。

我们编写了一个新的程序,其中应用了OutputKeys类,用以控制XSLT引擎的输出属性,该程序的架构和前一个程序(AddRecord3.java)大致相同,不过输出结果略有不同。完整的代码请参考下列文件: AddRecord3.java(见附件)、 user.xml(见附件)。该例子的运行环境为:Windows XP Professional、JDK 1.3.1。为了能够正常编译运行 AddRecord3.java这个程序,你需要到网址 http://java.sun.com去下载安装JAXP 1.1或者Java XML Pack(Java XML Pack内含JAXP了)。


没有评论: