作为XML文件写出DOM

不管是通过解析XML文件还是按部就班地构建出一个DOM,你可能都希望将其保存为XML。本节将告诉你如何利用Xalan转换包来实现这一目标。

利用这个包,可以创建一个转换器对象,并为StreamResult写一个DomSource。然后就可以调用该转换器的transform()方法来写出作为XML数据的DOM

读取XML

第一步是通过解析XML文件创建一个DOM。到目前为止,你应该很熟悉该过程。


注意:本节讨论的代码可以TransformationApp01.java中找到。


下面的代码提供了一个基本的模板。(大家应该对这些代码很熟悉。它们和在DOM教程的开始部分中编写的代码基本一样。如果你还没忘记的话,就会发现它们与下面的代码几乎相同。)

import javax.xml.parsers.DocumentBuilder; 
import javax.xml.parsers.DocumentBuilderFactory; 
import javax.xml.parsers.FactoryConfigurationError; 
import javax.xml.parsers.ParserConfigurationException; 
import org.xml.sax.SAXException; 
import org.xml.sax.SAXParseException; 
 
import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
 
import java.io.*;
 
public class TransformationApp 
{
  static Document document; 
 
  public static void main(String argv[])
  {
    if (argv.length != 1) {
      System.err.println (
        "Usage: java TransformationApp filename");
      System.exit (1);
    }
 
    DocumentBuilderFactory factory =
      DocumentBuilderFactory.newInstance();
    //factory.setNamespaceAware(true);
    //factory.setValidating(true); 
 
    try {
      File f = new File(argv[0]);
      DocumentBuilder builder =
        factory.newDocumentBuilder();
      document = builder.parse(f);
  
    } catch (SAXParseException spe) {
      // Error generated by the parser
      System.out.println("\n** Parsing error"
        + ", line " + spe.getLineNumber()
        + ", uri " + spe.getSystemId());
      System.out.println("  " + spe.getMessage() );
  
      // Use the contained exception, if any
      Exception x = spe;
      if (spe.getException() != null)
        x = spe.getException();
      x.printStackTrace();
 
    } catch (SAXException sxe) {
      // Error generated by this application
      // (or a parser-initialization error)
      Exception x = sxe;
      if (sxe.getException() != null)
        x = sxe.getException();
      x.printStackTrace();
 
    } catch (ParserConfigurationException pce) {
      // Parser with specified options can't be built
      pce.printStackTrace();
 
    } catch (IOException ioe) {
      // I/O error
      ioe.printStackTrace();
    }
  } // main 
} 

创建转换器

下一步是创建一个可用来将XML传送到System.out的转换器。



让我们从添加下面的导入语句开始吧:

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;
 
import javax.xml.transform.dom.DOMSource; 
 
import javax.xml.transform.stream.StreamResult; 
 
import java.io.*; 

这里已添加的一系列类应该能够组成一个标准模式:一个实体(Transformer)、创建它的工厂(TransformerFactory)以及每个类可以生成的异常。由于转换需要有一个源(source)和一个结果(result),所以必须将使用DOM所需的类导出为源(DomSource),并且将输出流作为结果(StreamResult)。

下一步,添加实现转换的代码:

try {
  File f = new File(argv[0]);
  DocumentBuilder builder = factory.newDocumentBuilder();
  document = builder.parse(f);
 
   // Use a Transformer for output
  TransformerFactory tFactory =
    TransformerFactory.newInstance();
  Transformer transformer = tFactory.newTransformer();
 
  DOMSource source = new DOMSource(document);
  StreamResult result = new StreamResult(System.out);
  transformer.transform(source, result); 

这里创建了一个转换器对象,用DOM构建了一个源对象,并且用System.out构建了一个结果对象。然后告诉转换器操作源对象并输出到结果对象。


注意:在这种情况下,“转换器”实际上并没有作任何变化。在XSLT术语中,使用身份转换(identity transform来表示“转换”生成源的一个未变化的副本。


最后,添加下面的代码来捕获可能生成的新错误。

} catch (TransformerConfigurationException tce) {
  // Error generated by the parser
  System.out.println ("* Transformer Factory error");
  System.out.println("  " + tce.getMessage() );
 
   // Use the contained exception, if any
  Throwable x = tce;
  if (tce.getException() != null)
    x = tce.getException();
  x.printStackTrace(); 
} catch (TransformerException te) {
  // Error generated by the parser
  System.out.println ("* Transformation error");
  System.out.println("  " + te.getMessage() );
 
  // Use the contained exception, if any
  Throwable x = te;
  if (te.getException() != null)
    x = te.getException();
  x.printStackTrace();
 
} catch (SAXParseException spe) {
  ... 

:

注意;

·   TransformerExceptions被转换器对象拋出。

·   TransformerConfigurationException被工厂拋出。

·   为了保存XML文档的DOCTYPE设置,有必要添加如下代码:

  import javax.xml.transform.OutputKeys;
  ...
  if (document.getDoctype() != null){
    String systemValue = (new
      File(document.getDoctype().getSystemId())).getName();
    transformer.setOutputProperty(
      OutputKeys.DOCTYPE_SYSTEM, systemValue
      );
  } 

编写XML

关于如何编译和运行该程序的说明,可以参考SAX教程中的编译和运行程序。(如果你正在使用该教程,请把程序名Echo替换为TransformationApp。如果你正在编译该示例代码,请使用TransformationApp02。)如果运行了slideSample01. xml上的代码,将看到如下输出:

<?xml version="1.0" encoding="UTF-8"?>
<!-- A SAMPLE set of slides -->
<slideshow author="Yours Truly" date="Date of publication"
title="Sample Slide Show">
 
  <!-- TITLE SLIDE -->
  <slide type="all">
    <title>Wake up to WonderWidgets!</title>
  </slide>
 
  <!-- OVERVIEW -->
  <slide type="all">
    <title>Overview</title>
    <item>Why <em>WonderWidgets</em> are great</item>
    <item/>
    <item>Who <em>buys</em> WonderWidgets</item>
  </slide>
 
</slideshow> 

注意:属性的顺序可能根据所用解析器的不同而不同。


关于配置工厂和处理确认错误的更多信息,请参考XML数据读入DOM

编写DOM的子树

操作DOM的子树也是有可能的。让我们在本教程的这一节中试试这一选项。


注意:本节中讨论的代码可以在TransformationApp03.java中找到。其中输出在TransformationLog03.txt中。(可浏览的版本为TransformationLog03.html)。


该过程中的唯一区别在于创建DOMSource时,用的是DOM中的一个节点,而不是整个DOM。第一步,导入所需的类,以获得想要的节点。代码如下:

import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;  

下一步,找出一个合适的节点。通过添加如下代码来选中第一个<slide>元素。

try {
  File f = new File(argv[0]);
  DocumentBuilder builder = factory.newDocumentBuilder();
  document = builder.parse(f);
 
  // Get the first <slide> element in the DOM
  NodeList list = document.getElementsByTagName("slide");
  Node node = list.item(0); 

最后,构建一个由发自该节点的子树组成的源对象:

DOMSource source = new DOMSource(document);
DOMSource source = new DOMSource(node);
StreamResult result = new StreamResult(System.out);
transformer.transform(source, result); 

现在运行该应用,输出如下:

<?xml version="1.0" encoding="UTF-8"?>
<slide type="all">
    <title>Wake up to WonderWidgets!</title>
  </slide> 

清除

现在通过如下操作,来取消在本节中添加的代码(TransformationApp04.java中包含了下面的代码)。

Import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList; 
...
  try {
    ...
    // Get the first <slide> element in the DOM
    NodeList list = document.getElementsByTagName("slide");
    Node node = list.item(0);
    ...
    DOMSource source = new DOMSource(node);
    StreamResult result = new StreamResult(System.out);
    transformer.transform(source, result); 

小结

到此时为止,你已经知道了如何利用转换器编写DOM,并且知道如何将DOM的子树用作转换中的源对象。在下一节中,你将会看到如何利用转换器,从任何可以解析的数据结构中创建XML

从任意数据结构创建XML

本节将告诉你如何利用XSLT将一个任意的数据结构转换成XML

本节概要如下:

1. 修改读取数据的现有程序,以便让它生成SAX事件。(该程序仅是一个某种类型的数据过滤器,还是一个真正的解析器,在这里无关紧要)

2. 然后利用SAX“解析器”为该转换构建一个SAXSource

3. 由于使用的StreamResult对象和在上一个练习中创建的相同,所以可以看到结果。(但要注意可以像创建DOMResult对象一样轻松地在内存中创建一个DOM

4. 利用转换器对象来进行这个转换,从而将源捆绑到结果中。

首先,需要一个希望转换的数据集以及一个能够读取这些数据的程序。在下面的两节中,将创建一个简单的数据文件和一个读取这个文件的程序。

创建简单文件

首先为一个地址簿创建数据集。如果愿意,可以复制这个过程,或者只是利用PersonalAddressBook.ldif中存储的数据。

下面所示的文件是这样生成的:在Netscape Messenger中创建一个新地址簿,给它一些虚拟数据(一个地址卡片),然后将其导出为LDIF格式。


注意:LDIF表示LDAP数据交换格式(LDAP Data Interchange Format),而LDAP表示轻便目录访问协议(Lightweight Directory Access Protocol)。不过我倾向于认为它是行定界交换格式(Line Delimited Interchange Format),因为这样更能说明问题。


8-1展示了创建的地址簿条目。

Address Book Entry

8-1  地址簿条目

导出地址簿会生成如下所示的文件。文件中我们关心的部分用黑体表示。

dn: cn=Fred Flintstone,mail=fred@barneys.house
modifytimestamp: 20010409210816Z
cn: Fred Flintstone
xmozillanickname: Fred
mail: Fred@barneys.house
xmozillausehtmlmail: TRUE
givenname: Fred
sn: Flintstone
telephonenumber: 999-Quarry
homephone: 999-BedrockLane
facsimiletelephonenumber: 888-Squawk
pagerphone: 777-pager
cellphone: 555-cell
xmozillaanyphone: 999-Quarry
objectclass: top
objectclass: person 

注意该文件的每一行都包含了一个变量名和冒号,并且在这些变量值的前面还有一个空格。变量sn包含人们的姓,变量cn包含地址簿条目中的DisplayName域。

创建简单解析器

下一步是创建能解析数据的程序。


注意:本节中讨论的代码可以在AddressBookReader01.java中找到。输出在AddressBookReaderLog01.txt中。


该程序的文本如下所示。它是一个极其简单的程序,甚至连多个条目的循环都没有,因为毕竟这只是一个演示程序。

import java.io.*; 
public class AddressBookReader 
{ 
 
  public static void main(String argv[])
  {
    // Check the arguments
    if (argv.length != 1) {
      System.err.println (
        "Usage: java AddressBookReader filename");
      System.exit (1);
    }
    String filename = argv[0];
    File f = new File(filename);
    AddressBookReader01 reader = new AddressBookReader01();
    reader.parse(f);
  }
 
  /** Parse the input */
  public void parse(File f) 
  {
    try {
      // Get an efficient reader for the file
      FileReader r = new FileReader(f);
      BufferedReader br = new BufferedReader(r);
 
       // Read the file and display it's contents.
      String line = br.readLine();
      while (null != (line = br.readLine())) {
        if (line.startsWith("xmozillanickname: "))
          break;
      }
      output("nickname", "xmozillanickname", line);
      line = br.readLine();
      output("email",    "mail",             line);
      line = br.readLine();
      output("html",     "xmozillausehtmlmail", line);
      line = br.readLine();
      output("firstname","givenname",        line);
      line = br.readLine();
      output("lastname", "sn",               line);
      line = br.readLine();
      output("work",     "telephonenumber",  line);
      line = br.readLine();
      output("home",     "homephone",        line);
      line = br.readLine();
      output("fax",      "facsimiletelephonenumber",
        line);
      line = br.readLine();
      output("pager",    "pagerphone",       line);
      line = br.readLine();
      output("cell",     "cellphone",        line);
    
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  } 
 
  void output(String name, String prefix, String line) 
  {
    int startIndex = prefix.length() + 2;  
      // 2=length of ": "
    String text = line.substring(startIndex);
    System.out.println(name + ": " + text); 
  } 
} 

该程序包含三个方法:

main

main方法从命令行中获得文件名,创建解析器的实例并对其进行设置以便能解析文件。该方法在我们将程序转换到SAX解析器时不起作用。(这就是我们将解析代码放到一个单独的方法中的原因。)

parse

该方法操作main子程序发送给它的那些File对象。可以看到这是多么的简单。在效率方面唯一有用的是BufferedReader的使用,这在操作大文件时很重要。

output

output方法包含某个行结构的逻辑。从右边开始,它采用了三个参数。第一个参数给该方法一个要显示的名称,所以我们可以将html作为一个变量名输出,而不是xmozillausehtmlmail。第二个参数给出存储在文件中的变量名(xmozillausehtmlmail)。第三个参数给出包含数据的行。该子程序然后从行的起始部分去掉该变量名,并输出所要的名称及数据。

运行PersonalAddressBook.ldif上的这个程序生成的输出如下所示:

nickname: Fred
email: Fred@barneys.house
html: TRUE
firstname: Fred
lastname: Flintstone
work: 999-Quarry
home: 999-BedrockLane
fax: 888-Squawk
pager: 777-pager
cell: 555-cell 

我想大家会一致同意上面的输出更具可读性。

修改解析器以生成SAX事件

下一步,修改解析器以生成SAX事件,这样就可以把它用作XSLT transformSAXSource对象的基础了。


注意:本节中讨论的代码可以在AddressBookReader02.java中找到。


让我们首先导入所需的的附加类:

import java.io.*; 
import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl; 

接着修改应用,以扩展XmlReader。这个改动将应用转换成一个能生成合适SAX事件的解析器。

public class AddressBookReader 
  implements XMLReader
{  

这时,可以删除main方法,因为已经不再需要它了。

public static void main(String argv[])
{
  // Check the arguments
  if (argv.length != 1) {
    System.err.println ("Usage: Java AddressBookReader 
filename");
    System.exit (1);
  }
  String filename = argv[0];
  File f = new File(filename);
  AddressBookReader02 reader = new AddressBookReader02();
  reader.parse(f);
} 

再添加一些稍后将会用到的全局变量:

public class AddressBookReader 
  implements XMLReader
{
  ContentHandler handler;
 
  // We're not doing namespaces, and we have no
  // attributes on our elements. 
  String nsu = "";  // NamespaceURI
  Attributes atts = new AttributesImpl();
  String rootElement = "addressbook";
 
  String indent = "\n      "; // for readability! 

SAX ContentHandler是将用来获得解析器生成的SAX事件的对象。为了将应用装入XmlReader,需要定义一个setContentHandler方法。Handler变量会包含一个该对象的引用,其中该对象在setContentHandler被调用时被发送出去。

并且,当解析器生成SAX元素事件时,需要提供命名空间和属性信息。但由于这是一个简单的应用,所以可以将它们定义为空值。

还需要为数据结构定义一个根元素(addressbook),并创建一个缩进字符串,以增加可读性。

下一步,修改parse方法,以便它能够将InputSource看作一个参数,而不是将File当作参数。并且还要考虑它可能生成的异常。

public void parse(File f)InputSource input) 
throws IOException, SAXException  

现在把读取器封装到InputSource对象中,代码如下:

try {
  // Get an efficient reader for the file
  FileReader r = new FileReader(f);
  java.io.Reader r = input.getCharacterStream();
  BufferedReader Br = new BufferedReader(r); 

注意:下一节中我们将创建输入源对象,其中输入的内容实际上一个缓冲读取器。但AddressBookReader可以被其他人用在行下面的某处。这一步骤保证不管给出的读取器如何,处理都是有效的。


下一步要做的是修改parse方法,以便为文档和根元素的开始生成SAX事件。为此,需要添加下面的代码:

/** Parse the input */
public void parse(InputSource input) 
...
{
  try {
    ...
    // Read the file and display its contents.
    String line = br.readLine();
    while (null != (line = br.readLine())) {
      if (line.startsWith("xmozillanickname: ")) break;
    }
 
    if (handler==null) {
      throw new SAXException("No content handler");
    }
 
    handler.startDocument(); 
    handler.startElement(nsu, rootElement, 
      rootElement, atts);