准备 XML 及相关技术认证,第 3 部分: XML 处理

探讨如何解析和验证 XML 文档以及 XQuery 的用法

解析和验证是 XML 的核心。了解如何充分利用这些能力对于能否在项目中成功地引入 XML 至关重要。关于 XML 处理的这篇教程讨论如何解析和验证 XML 文件以及使用 XQuery。本系列教程帮助您准备 IBM 认证考试 142(XML and Related Technologies),包括五篇,这是第三篇。

Mark Lorenz (mlorenz@nc.rr.com), 资深应用程序架构师, Hatteras Software, Inc.

Photo of Mark LorenzMark Lorenz 创建了 Hatteras Software,这是一家面向对象技术咨询公司,他出版了多部关于软件开发的著作。他通过了面向对象分析和设计(OOAD)、XML、RAD 和 Java 认证。他应用 XHTML、Web 服务、Ajax、JSF、Spring、BIRT 及相关基于 Eclipse 的工具开发 Java 企业应用程序。您可以阅读 Mark 的技术博客



2006 年 11 月 20 日

开始之前

这一节介绍本教程的主要内容以及如何充分利用它。

关于本系列

本系列教程 分为五部分,帮助您准备参加 IBM 认证考试 142(XML and Related Technologies),通过 IBM Certified Solution Developer - XML and Related Technologies 认证。通过该认证表明达到了中级开发人员的水平,能够使用 XML 及相关技术设计和实现应用程序,比如 XML Schema、可扩展样式表语言转换(XSLT)和 XPath。这些开发人员对 XML 基础有深刻的理解;清楚 XML 概念和相关技术;了解数据和 XML 的关系,特别适合信息建模、XML 处理、XML 呈现以及与 Web 服务有关的问题;对核心的、与 XML 有关的万维网联盟(W3C)推荐标准了然于心,熟悉常见的最佳实践。

在过去几年中从事软件开发的任何人都知道,XML 为数据 提供了跨平台能力,就像 Java® 编程语言为应用程序逻辑提供跨平台能力一样。本系列教程适合于希望在使用 XML 技术方面超越入门阶段的任何人。

关于本教程

本教程是 “准备 XML 及相关技术认证” 系列的第三部分,该系列教程介绍了在 Java 项目中有效使用 XML 技术的各个重要方面。第三期教程主要讨论 XML 处理,即如何解析和验证 XML 文档。本教程主要讨论转换,包括 XSLT、XPath 和级联样式表(CSS)的使用,为第 4 部分做好铺垫。

本教程是为那些对 XML 有基本的了解、技能和经验接近中级水平的 Java 程序员编写的。读者应该比较熟悉 XML 文档的定义、验证和读取,并有使用 Java 语言的实际经验。

目标

学完本教程后读者应该能够:

  • 使用 Simple API for XML 2(SAX2)和文档对象模型 2(DOM2)解析器解析 XML 文档
  • 用文档类型定义(DTD)和 XML Schemas 验证 XML 文档
  • 使用 XQuery 从数据库中访问 XML 内容

前提条件

本教程是为那些具有编程和脚本背景,了解计算机科学基本模型和数据结构的开发人员编写的。您应该熟悉下列与 XML 有关的计算机科学概念:树遍历、递归和数据重用。应该熟悉 Internet 标准和概念,比如 Web 浏览器、客户机-服务器、文档化、格式化、电子商务和 Web 应用程序。最后,具有设计和实现基于 Java 的计算机应用程序和使用关系数据库的经验。

系统需求

要运行本教程中的例子,需要 Linux® 或 Microsoft® Windows® 操作系统,至少 50MB 空闲空间和安装软件的管理权限。本教程使用了(但不是必需的)下列软件:

  • Java 软件开发工具箱(JDK) 1.4.2 或更高版本
  • Eclipse 3.1 或更高版本
  • XMLBuddy 2.0 或更高版本(注意:本系列教程的部分内容使用了 XMLBuddy Pro 的功能,该版本不是免费的。)

上述软件的下载链接请参阅 参考资料


解析 XML 文档

解析 XML 文档有多种方式(请参阅本系列的第 1 部分,那篇教程讨论了体系结构),不过 SAX 解析器和 DOM 解析器是最基本的。第 1 部分从较高层次上比较了这两种方法(请参阅 参考资料)。

StAX

2006 年后期将出现一种新的 API,称为 Streaming API for XML(StAX)。这是一种拉式 API,和 SAX 的推式 模型不同,是由应用程序而不是解析器控制的。也能使用 StAX 修改解析的文档。更多详情请阅读 “An Introduction to StAX”(请参阅 参考资料)。

XML 实例文档

本教程使用一个 DVD 商店的库存目录作为示例文档。从概念上说,目录是 DVD 以及关于每个 DVD 的信息的集合。实际的文档很短,只有四张 DVD,但是对于学习 XML 处理(包括验证)而言已经足够复杂了。清单 1 显示了该文件。

清单 1. DVD 目录的 XML 实例文档
<?xml version="1.0"?>
<!DOCTYPE catalog SYSTEM "dvd.dtd">
<!-- DVD inventory -->
<catalog>
  <dvd code="_1234567">
      <title>Terminator 2</title>
      <description>
        A shape-shifting cyborg is sent back from the future 
		to kill the leader of the resistance.
      </description>
      <price>19.95</price>
      <year>1991</year>
  </dvd>
  <dvd code="_7654321">
      <title>The Matrix</title>
      <price>12.95</price>
      <year>1999</year>
  </dvd>
  <dvd code="_2255577" genre="Drama">
      <title>Life as a House</title>
      <description>
        When a man is diagnosed with terminal cancer, 
        he takes custody of his misanthropic teenage son.
      </description>
      <price>15.95</price>
      <year>2001</year>
  </dvd>
  <dvd code="_7755522" genre="Action">
      <title>Raiders of the Lost Ark</title>
      <price>14.95</price>
      <year>1981</year>
  </dvd>
</catalog>

使用 SAX 解析器

本系列教程的第 1 部分已经讨论过,SAX 解析器是一种基于事件的解析器。这意味着,解析器在解析文档的时候向回调方法发送事件(如 图 1 所示)。为了简化起见,图 1 中没有显示发生的所有事件。

图 1. SAX 解析器事件
SAX 解析器事件

随着解析器读入文档内容,这些事件被实时地推给应用程序。这种处理模型的好处是可以用相对较少的内存处理很大的文档。缺点是处理所有这些事件要做更多的工作。

org.xml.sax 包提供了一组接口。其中之一提供了解析器的 XMLReader 接口。可以这样建立解析:

try {
    XMLReader parser = XMLReaderFactory.createXMLReader();
    parser.parse( "myDocument.xml" ); //complete path
} catch ( SAXParseException e ) { 
    //document is not well-formed
} catch ( SAXException e ) { 
    //could not find an implementation of XMLReader 
} catch ( IOException e ) { 
    //problem reading document file
}

Apache Xerces2 解析器

如果需要解析器的话,可以从 Apache Software Foundation 网站(请参阅 参考资料)下载开放源码的 Apache Xerces2 解析器。

技巧:重用解析器实例是可能的。创建解析器需要大量资源。如果运行多个线程,可以从资源池中重用解析器实例。

这样当然好,但是应用程序如何从解析器得到事件呢?很高兴您能问这个问题。

处理 SAX 事件

为了从解析器接收事件,需要实现 ContentHandler 接口。可以实现该接口的很多方法以便处理文档。或者,如果只需要处理一两个回调,也可以实现 DefaultHandler 的子类,该类实现了所有 ContentHandler 方法(什么也不做),然后覆盖需要的方法。

无论哪种方式,都要编写逻辑,从而在收到 startElementcharactersendDocument 和 SAX 解析器激活的其他回调方法时进行所需要的处理。XML in a Nutshell, Third Edition(请参阅 参考资料)的第 351-355 页列出了文档中可能出现的所有方法调用。

这些回调事件是文档解析过程中的正常 事件。还可以实现 ErrorHandler 来处理有效性 回调。介绍有效性之后我再讨论这个话题,暂时先放一放。

要进一步了解 SAX 解析,请阅读 XML in a Nutshell, Third Edition 的第 20 章或者 “Serial Access with the Simple API for XML (SAX)”(请参阅 参考资料)。

SAX 解析器异常处理

在默认情况下,SAX 解析器忽略错误。为了应付无效的或者非结构良好的文档,必须实现 ErrorHandler(要注意,DefaultHandler 同时实现了该接口和 ContentHandler 接口)并定义 error() 方法:

public class SAXEcho extends DefaultHandler {
...
//Handle validity errors
public void error( SAXParseException e ) { 
    echo( e.getMessage() );
    echo( "Line " + e.getLineNumber() + 
         " Column " + e.getColumnNumber();
}

然后必须打开验证特性:

parser.setFeature( "http://xml.org/sax/features/validation", true );

最后调用下列代码:

parser.setErrorHandler( saxEcho );

要记住,parserXMLReader 的一个实例。如果文档违反了模式(DTD 或 XML Schema)规则,解析器就会调用 error() 方法。

其他 ErrorHandler 方法

ErrorHandler 还有 warningfatalError 方法,分别用于没有破坏和破坏了结构良好性的错误。通常不用管这些方法。

回显 SAX 事件

作为上面所学 SAX 解析器应用技巧的一个练习,使用 清单 2 中的 SAXEcho.java 代码输出 catalog.xml 文件的解析器事件。

清单 2. 回显 SAX 事件
package com.xml.tutorial;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * A handler for SAX parser events that outputs certain event 
 * information to standard output.
 * 
 * @author mlorenz
 */
public class SAXEcho extends DefaultHandler {
  public static final String XML_DOCUMENT_DTD = "catalogDTD.xml";
                                        //validates via catalog.dtd
  public static final String XML_DOCUMENT_XSD = "catalogXSD.xml";
                                        //validates via catalog.xsd
  public static final String NEW_LINE = System.getProperty("line.separator");
  protected static Writer writer;
  /**
   * Constructor
   */
  public SAXEcho() {
    super();
  }
  /**
   * @param args
   */
  public static void main(String[] args) {
    //-- Set up my instance to handle SAX events
    DefaultHandler eventHandler = new SAXEcho();  
    //-- Echo to standard output
    writer = new OutputStreamWriter( System.out );
    try {
      //-- Create a SAX parser
      XMLReader parser = XMLReaderFactory.createXMLReader();
      parser.setContentHandler( eventHandler );
      parser.setErrorHandler( eventHandler );
      parser.setFeature( 
                            "http://xml.org/sax/features/validation", true );
      //-- Validation via DTD --
      echo( "=== Parsing " + XML_DOCUMENT_DTD + " ===" + NEW_LINE );
      //-- Parse my XML document, reporting DTD-related errors
      parser.parse( XML_DOCUMENT_DTD );
      //-- Validation via XSD --
      parser.setFeature(  
                            "http://apache.org/xml/features/validation/schema", 
                             true );
      echo( NEW_LINE + NEW_LINE + "=== Parsing " + 
                            XML_DOCUMENT_XSD + " ===" + NEW_LINE );
      //-- Parse my XML document, reporting XSD-related errors
      parser.parse( XML_DOCUMENT_XSD );
    } catch (SAXException e) {
      System.out.println( "Parsing Exception occurred" );
      e.printStackTrace();
    } catch (IOException e) {
      System.out.println( "Could not read the file" );
      e.printStackTrace();
    }
    System.exit(0);    
  }
  //--Implement SAX callback events of interest (default is do nothing) --
  /* (non-Javadoc)
   * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
     * java.lang.String, java.lang.String, org.xml.sax.Attributes)
   * @see org.xml.sax.ContentHandler interface
   * Element and its attributes
   */
  @Override
  public void startElement(  String uri, 
          String localName,
          String qName, 
          Attributes attributes)
  throws SAXException {
      if( localName.length() == 0 )
        echo( "<" + qName );
      else
        echo( "<" + localName );
      if( attributes != null ) {
          for( int i=0; i < attributes.getLength(); i++ ) {
              if( attributes.getLocalName(i).length() == 0 ) {
                  echo( " " + attributes.getQName(i) + 
                             "=\"" + attributes.getValue(i) + "\"" );
              }
          }
      }
      echo( ">" );
  }
  /* (non-Javadoc)
   * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, 
   * java.lang.String, java.lang.String)
   * End tag
   */
  @Override
  public void endElement(String uri, String localName, String qName) 
       throws SAXException {
      echo( "</" + qName + ">" );
  }
  /* (non-Javadoc)
   * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
   * Character data inside an element
   */
  @Override
  public void characters(char[] ch, int start, int length) 
       throws SAXException {
      String s = new String(ch, start, length);
      echo(s);
  }
  //-- Add additional event echoing at your discretion --
  /**
   * Output aString to standard output
   * @param aString
   */
  protected static void echo( String aString ) {
      try {
          writer.write( aString );
          writer.flush();
      } catch (IOException e) {
          System.out.println( "I/O error during echo()" );
          e.printStackTrace();
      }
  }
  /* (non-Javadoc)
   * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
   * @see org.xml.sax.ErrorHandler interface
   */
  @Override
  public void error(SAXParseException e) throws SAXException {
    echo( NEW_LINE + "*** Failed validation ***" + NEW_LINE );
    super.error(e);
    echo( "* " + e.getMessage() + NEW_LINE + 
          "* Line " + e.getLineNumber() + 
               " Column " + e.getColumnNumber() + NEW_LINE +
       "*************************" + NEW_LINE );
    try {
      Thread.sleep( 10 );
    } catch (InterruptedException e1) {
      e1.printStackTrace();
    }
  }
}

可以利用 SAXEcho.java 中的代码观察 SAX 解析是如何进行的。要注意,这段代码没有处理所有的事件,因此并非原始文档中的所有事件都回显出来(请参阅 清单 3)。观察 ContentHandler 接口了解可能遇到的其他消息。

清单 3. 执行 SAXEcho 的输出
=== Parsing catalogDTD.xml ===
<catalog><dvd><title>Terminator 2</title><description>
      A shape-shifting cyborg is sent back from the future
       to kill the leader of the resistance.
    </description><price>19.95</price><year>1991</year>
    </dvd><dvd><title>The Matrix</title><price>10.95</price>
<year>1999</year></dvd><dvd><title>
Life as a House</title><description>
 When a man is diagnosed with terminal cancer,
 he takes custody of his misanthropic teenage son.
    </description><price>15.95</price><year>2001</year>
  </dvd><dvd><title>Raiders of the Lost Ark</title><price>
14.95</price><year>1981</year></dvd></catalog>
    
=== Parsing catalogXSD.xml ===
<catalog>
  <dvd>
    <title>Terminator 2</title>
    <description>
      A shape-shifting cyborg is sent back from the future 
      to kill the leader of the resistance.
    </description>
    <price>19.95</price>
    <year>1991</year>
  </dvd>
  <dvd>
    <title>The Matrix</title>
    <price>10.95</price>
    <year>1999</year>
  </dvd>
  <dvd>
    <title>Life as a House</title>
    <description>
      When a man is diagnosed with terminal cancer,
      he takes custody of his misanthropic teenage son.
    </description>
    <price>15.95</price> 
    <year>2001</year>
  </dvd>
  <dvd>
    <title>Raiders of the Lost Ark</title>
    <price>14.95</price>
    <year>1981</year>
  </dvd>
</catalog>

使用 DOM 解析器

和 SAX 解析器不同,DOM 解析器根据 XML 文档内容创建树结构(如 图 2 所示)。为了简化起见,部分解析动作没有显示出来。

图 2. DOM 解析树
DOM 解析树

DOM 没有为 XML 解析器指定接口,因此不同的厂商有不同的解析器类。我将继续使用 Xerces 解析器,它包含一个 DOMParser 类。

可以像下面这样建立 DOM 解析器:

DOMParser parser = new DOMParser();
try {
    parser.parse( "myDocument.xml" );
    Document document = parser.getDocument();
} catch (DOMException e) {
    // take validity action here
} catch (SAXException e) {
    // well-formedness action here 
} catch (IOException e) {
    // take I/O action here
}

遍历 DOM 树

由于要构造整个文档树,DOM 需要更多的时间和内存。这些开销的好处是可以利用树结构通过多种方式遍历和操纵文档的内容。图 3 显示了 DVD catalog 文档的一部分。

图 3. 遍历 DOM 树
遍历 DOM 树

树有一个根,可以通过 Document.getDocumentElement() 方法来访问它。从任何 Node 出发,都可使用 Node.getChildNodes() 获得当前 Node 的所有孩子的 NodeList。要注意,属性 被看作包含它的 Node 的孩子。可以创建新的 Node、追加、插入、按名查找和删除节点。这些还仅仅是所有功能的一小部分。

一个更强大的方法是 Document.getElementsByTagName(),它返回一个 NodeList,包含后代元素中与 Node 匹配的所有孩子。DOM 既可在客户机上使用,也可在服务器上使用。

客户机遍历

可以在客户机上遍历 DOM 树,而且可以在浏览器中通过 JavaScript 验证 XHTML 页面上的动作。比如,客户机可能需要确定是否存在特定名称的 Node

//-- make sure a new DVD's title is unique
var titles = document.getElementsByTagName("title");
var newTitleValue = newTitle.getNodeValue();
var nextTitle;
for( i=0; i < titles.getLength(); i++ ) {
    nextTitle = titles.item(i); //NodeList access by index
    if( nextTitle.getNodeValue().equals( newTitleValue ) {
        //take some action
    }
}

服务器遍历

在服务器上肯定会需要操纵树,比如在一个 Node 中增加新的孩子:

//-- add a new DVD with aName and description
public void createNewDvd( String aName, String description ) {
    Element catalog =  document.getDocumentElement(); //root
    Element newDvd = document.createElement( aName );
    Element dvdDescription = 
                         document.createTextNode( description );
    newDvd.appendChild( dvdDescription );
    catalog.appendChild( newDvd ); //as last element
}

也可以换为 XHTML

本教程使用了数据文档,不过文档也可以是 XHTML 页面,这样的话,就会看到 headbodyptdli 之类的节点。

忠告: 一定要使用 DOM 接口,如 NodeListNamedNodeMap 来操纵树。DOM 树是动态的,就是说随着所做的修改立刻更新,因此如果使用本地变量,缓冲的数据有可能是错误的。比如,在调用 removeChild() 之后, Node.getLength() 将返回不同的值。

DOM 解析器异常处理

DOM3

DOM3 增加了 DOMErrorHandler,它提供了一种回调机制来代替 DOMException。比如下面的例子:

DOMParser parser = new DOMParser(); DOMConfiguration domConfig = document.domConfig; domConfig.setParameter( DOMErrorHandler handler );

该类实现的 DOMErrorHandler 接口有一个 handleError(DOMError error) 方法,它返回 true 则继续处理,返回 false 则中止处理(对于致命错误,通常中止处理)。

如果在解析过程中遇到问题,DOM 解析器将抛出 DOMException。这是一种 RuntimeException,虽然有些语言不支持检查异常,但在 Java 代码中应该始终捕获并抛出异常。

为了确定操作中出现的问题,应该使用 DOMExceptioncode。这些 code 说明什么地方出现了问题,比如尝试的修改使文档变得无效(DOMException.INVALID_MODIFICATION_ERR)或者找不到目标 NodeDOMException.NOT_FOUND_ERR)。Processing XML with Java: A Guide to SAX, DOM, JDOM, JAXP, and TrAX 第 9 章的 DOMException 一节提供了完整的 DOMException code 列表,包括有关说明(请参阅 参考资料)。

回显 DOM 树

作为上面所学 DOM 解析器使用技巧的一个练习,可以使用 清单 4 中的 DOMEcho.java 代码输出 catalog.xml 文件 DOM 树的内容。这段代码回显树的信息,然后修改并回显更新后的树。

清单 4. 回显 DOM 树
package com.xml.tutorial;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;
import org.xml.sax.SAXException;

import com.sun.org.apache.xerces.internal.parsers.DOMParser;

/**
 * A handler to output certain information about a DOM tree 
 * to standard output.
 * 
 * @author lorenzm
 */
public class DOMEcho {
  public static final String XML_DOCUMENT_DTD = 
                              "catalogDTD.xml";   //validates via catalog.dtd
  public static final String NEW_LINE = System.getProperty("line.separator");
  protected static Writer writer;
    // Types of DOM nodes, indexed by nodeType value (e.g. Attr = 2)
    protected static final String[] nodeTypeNames = {
        "none",         //0
        "Element",      //1
        "Attr",         //2
        "Text",         //3
        "CDATA",        //4
        "EntityRef",    //5
        "Entity",       //6
        "ProcInstr",    //7
        "Comment",      //8
        "Document",     //9
        "DocType",      //10
        "DocFragment",  //11
        "Notation",     //12
    };
    //-- DOMImplementation features (we only need one for now)
    protected static final String TRAVERSAL_FEATURE  = "Traversal";
    //-- DOM versions (we're using DOM2)
    protected static final String DOM_2        = "2.0";

  /**
   * Constructor
   */
  public DOMEcho() {
    super();
  }

  /**
   * @param args
   */
  public static void main(String[] args) {
        //Echo to standard output
      writer = new OutputStreamWriter( System.out );
        //use the Xerces parser
      try {
          DOMParser parser = new DOMParser();  
          parser.setFeature( "http://xml.org/sax/features/validation", true );
          parser.parse( XML_DOCUMENT_DTD ); //use DTD grammar for validation
          Document document = parser.getDocument();
          echoAll( document );
          //-- add description for Indiana Jones movie
          //---- find parent Node
          Element indianaJones = document.getElementById("_7755522");
          //---- insert a description before the price 
          //    (anywhere else would be invalid)
          NodeList prices = indianaJones.getElementsByTagName("price");
          Node desc = document.createElement("description");
          desc.setTextContent(
                      "Indiana Jones is hired to find the Ark of the Covenant");
          indianaJones.insertBefore( desc, prices.item(0)  );
          //-- now, echo the document again to see the change
          echoAll( document );
      } catch (DOMException e) {  //handle invalid manipulations
          short code = e.code;
          if( code == DOMException.INVALID_MODIFICATION_ERR ) {
              //take action when invalid manipulation attempted
          } else if( code == DOMException.NOT_FOUND_ERR ) {
              //take action when element or attribute not found
          }   //add more checks here as desired
      } catch (SAXException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      }
  }
  
  /**
   * Echo all the Nodes, in preorder traversal order, for aDocument
   * @param aDocument
   */
  protected static void echoAll(Document aDocument) {
      if( aDocument.getImplementation().hasFeature(
                                        TRAVERSAL_FEATURE,DOM_2) ) {
          echo( "=== Echoing " + XML_DOCUMENT_DTD + " ===" + NEW_LINE );
          Node root = (Node) aDocument.getDocumentElement();
          int whatToShow = NodeFilter.SHOW_ALL;
          NodeFilter filter = null;
          boolean expandRefs = false;
          //-- depth first, preorder traversal 
          DocumentTraversal traversal = (DocumentTraversal)aDocument;
          TreeWalker walker = traversal.createTreeWalker(
                          (org.w3c.dom.Node) root, //where to start 
                                         //(cannot go "above" the root)
                          whatToShow,  //what to include
                          filter,       //what to exclude
                          expandRefs); //include referenced entities or not
          for( Node nextNode = (Node) walker.nextNode(); nextNode != null; 
                                        nextNode = (Node) walker.nextNode() ) {
              echoNode( nextNode );
          }
      } else {
          echo( NEW_LINE + "*** " + TRAVERSAL_FEATURE + 
                           " feature is not supported" + NEW_LINE );
      }
  }

  /**
   * Output aNode's name, type, and value to standard output.
   * @param aNode
   */
  protected static void echoNode( Node aNode ) {
    String type = nodeTypeNames[aNode.getNodeType()];
    String name = aNode.getNodeName();
    StringBuffer echoBuf = new StringBuffer();
    echoBuf.append(type);
    if( !name.startsWith("#") ) {  //do not output duplicate names
        echoBuf.append(": ");
        echoBuf.append(name);
    }
    if( aNode.getNodeValue() != null ) {
      if( echoBuf.indexOf("ProcInst") == 0 )
          echoBuf.append( ", " );
      else
          echoBuf.append( ": " );  //output only to first newline
      String trimmedValue = aNode.getNodeValue().trim();
      int nlIndex = trimmedValue.indexOf("\n");
      if( nlIndex >= 0 )  //found newline
          trimmedValue = trimmedValue.substring(0,nlIndex);
      echoBuf.append(trimmedValue);
    }
    echo( echoBuf.toString() + NEW_LINE );
    echoAttributes( aNode );
  }
  
  /**
   * Output aNode's attributes to standard output.
   * @param aNode
   */
  protected static void echoAttributes(Node aNode) {
      NamedNodeMap attr = aNode.getAttributes();
      if( attr != null ) {
          StringBuffer attrBuf = new StringBuffer();
          for( int i = 0; i < attr.getLength(); i++ ) {
              String type = nodeTypeNames[attr.item(i).getNodeType()];
              attrBuf.append(type);
              attrBuf.append( ": " + attr.item(i).getNodeName() + "=" );
              attrBuf.append( "\"" + attr.item(i).getNodeValue() + "\"" + 
                                   NEW_LINE );
          }
          echo( attrBuf.toString() );
      }
  }

  /**
   * Output aString to standard output
   * @param aString
   */
  protected static void echo( String aString ) {
    try {
        writer.write( aString );
        writer.flush();
    } catch (IOException e) {
        System.out.println( "I/O error during echo()" );
        e.printStackTrace();
    }
  }
}

看看其中的部分逻辑:

protected static final String[] nodeTypeNames = {
    ...
};

该数组将 Node.getNodeType() int 值映射到可能遇到的每种 Node 类型:

if( aDocument.getImplementation().hasFeature(
                            TRAVERSAL_FEATURE,DOM_2) ) {

DOM1 与 DOM2

在 DOM1 中,遍历 document 树是按照 “线性” 的方式进行的,使用 NodeIteratorNodeFilter 来访问上一个和下一个 Node。在 DOM2 中,TreeWalker 接口增加了当前 Node 的概念,可以在父、子、兄弟节点间移动。

Processing XML with Java: A Guide to SAX, DOM, JDOM, JAXP, and TrAX(请参阅 参考资料)的第 12 章介绍了 DOM 的 NodeIteratorNodeFilter 以及 DOM2 的 TreeWalker

Bruno R. Preiss 介绍了各种树的遍历方法(请参阅 参考资料)。

DOMEcho 利用了 DOM2(参见 DOM 1 与 DOM 2)的 TreeWalker 接口。为了安全起见,一定要检查您的解析器是否支持该特性。阅读 Processing XML with Java: A Guide to SAX, DOM, JDOM, JAXP, and TrAX(请参阅 参考资料)的第 9 章可以了解所有这些特性。

简而言之,DOMEcho 有一个 echoAll(Document aDoc) 方法,该方法使用不带筛选的 TreeWalker 按照先序遍历顺序访问 Node(请参阅 DOM 1 与 DOM 2)。然后对每个节点调用 echoNode(Node aNode)。接着 echoNode 对它的 Node 调用 echoAttributes(Node aNode)

//---- find parent Node
Element indianaJones = document.getElementById("_7755522");
//---- insert a description before the price 
//    (anywhere else would be invalid)
NodeList prices = indianaJones.getElementsByTagName("price");
Node desc = document.createElement("description");
desc.setTextContent(
      "Indiana Jones is hired to find the Ark of the Covenant");
indianaJones.insertBefore( desc, prices.item(0)  );

就是这部分代码修改了 DOM 树。它在适当的位置添加了描述来保证树按照文档模式仍然是有效的。

清单 5 显示了 DOMEcho 的输出结果。

清单 5. DOMEcho 的输出
=== Echoing catalogDTD.xml ===
Text: 
Comment: DVD inventory
Text: 
Element: dvd
Attr: code="_1234567"
Text: 
Element: title
Text: Terminator 2
Text: 
Element: description
Text: A shape-shifting cyborg is sent back from the future 
to kill the leader of the resistance.
Text: 
Element: price
Text: 19.95
Text: 
Element: year
Text: 1991
Text: 
Text: 
Element: dvd
Attr: code="_7654321"
Text: 
Element: title
Text: The Matrix
Text: 
Element: price
Text: 10.95
Text: 
Element: year
Text: 1999
Text: 
Text: 
Element: dvd
Attr: code="_2255577"
Attr: genre="Drama"
Text: 
Element: title
Text: Life as a House
Text: 
Element: description
Text: When a man is diagnosed with terminal cancer, 
he takes custody of his misanthropic teenage son.
Text: 
Element: price
Text: 15.95
Text: 
Element: year
Text: 2001
Text: 
Text: 
Element: dvd
Attr: code="_7755522"
Attr: genre="Action"
Text: 
Element: title
Text: Raiders of the Lost Ark
Text: 
Element: price
Text: 14.95
Text: 
Element: year
Text: 1981
Text: 
Text: 
=== Echoing catalogDTD.xml ===
Text: 
Comment: DVD inventory
Text: 
Element: dvd
Attr: code="_1234567"
Text: 
Element: title
Text: Terminator 2
Text: 
Element: description
Text: A shape-shifting cyborg is sent back from the future 
to kill the leader of the resistance.
Text: 
Element: price
Text: 19.95
Text: 
Element: year
Text: 1991
Text: 
Text: 
Element: dvd
Attr: code="_7654321"
Text: 
Element: title
Text: The Matrix
Text: 
Element: price
Text: 10.95
Text: 
Element: year
Text: 1999
Text: 
Text: 
Element: dvd
Attr: code="_2255577"
Attr: genre="Drama"
Text: 
Element: title
Text: Life as a House
Text: 
Element: description
Text: When a man is diagnosed with terminal cancer, 
he takes custody of his misanthropic teenage son.
Text: 
Element: price
Text: 15.95
Text: 
Element: year
Text: 2001
Text: 
Text: 
Element: dvd
Attr: code="_7755522"
Attr: genre="Action"
Text: 
Element: title
Text: Raiders of the Lost Ark
Text: 
Element: description
Text: Indiana Jones is hired to find the Ark of the Covenant
Element: price
Text: 14.95
Text: 
Element: year
Text: 1981
Text: 
Text:

空白

您将注意到 DOMEcho 输出(清单 6)中有大量 Text Node,其中很多看起来没有内容。为什么会这样呢?

解析器报告文档元素内容中出现的空白(多余的空白、制表符和回车换行)。

需要注意的是没有 报告的东西:元素中的空白,比如属性周围的空格。这里没有显示,但是也不会报告的是序言中的空白。要注意,description 一个 Text Element,但是空白被规范化 从而去掉了非空白内容之前和之后的多余字符。

Element 内容中的空白造成的 Text 元素称为不可忽略的 空白。不可忽略的空白是验证的一部分,如 图 4 所示。

图 4. 空白处理
空白处理

验证 XML 文档

验证包括使用文法保证 XML 文档具有正确的结构和内容。可以使用 XML 模式来指定文法,形式包括 DTD 或 XML Schema 文件(请参阅 模式)。教程的这一节讨论 DTD 和 XML Schema 文件。

模式

从技术上说,DTD、XML Schemas(S 大写)和 RELAX NG 都属于 XML 模式(schema 中的 s 小写)。XML Schemas(S 大写)严格地说应该是 W3C XML Schemas。在本教程中,只要看到 XML Schema 就应将其看作是这种 W3C 语言而不是一般意义上的模式文档。

使用 DTD 验证

DTD 定义了应用于 XML 实例文档上的约束。这些约束与结构良好性无关。事实上,非结构良好的文档根本不被看作是 XML 文档。约束与关于内容的业务规则有关,为了保证应用程序能够使用文档,文档必须满足这些规则。

DTD 规定了为了保证有效 XML 实例文档必须包含的元素和属性。可以通过在文档开始部分包含 DOCTYPE 语句把文档和 DTD 联系起来:

<!DOCTYPE catalog SYSTEM "catalog.dtd">

现在来看看 catalog.dtd 文件。为了验证该文档,需要启用验证并使用验证解析器。下面的代码启用 SAX 解析器的验证特性:

saxParser.setFeature(     
    "http://xml.org/sax/features/validation", true );

下面的代码启用 DOM 解析器的验证特性:

domParser.setFeature( 
    "http://xml.org/dom/features/validation", true );

图 5 显示了 catalog.dtd 文件。

图 5. Catalog DTD
Catalog DTD

我们逐行地来分析这个 DTD,看看规定了什么:

<!ELEMENT catalog    (dvd+)>

dvd+ 指定 <catalog> 元素包含一个或多个 <dvd>。这样做是合理的,否则就不可能卖掉多少 DVD!

<!ELEMENT dvd       (title, description?, price, year)>

title, ..., year 称为序列。就是说这些命名的元素必须按照这种 顺序作为 <dvd> 元素的孩子出现。description 后面的问号表明 <dvd> 可以有零个或一个描述元素 —— 换句话说,它是可选的,但如果指定的话也只能有一个(星号表示零个或多个,加号表示 1 个或多个)。

<!ATTLIST dvd  code    ID   #REQUIRED>

ID 类型的属性在文档中的名称必须是惟一的。您将看到 catalog.xml 文件中的 ID 都是以下划线开始的。XML 名称不能以数字开始,但是下划线(以及字母或者其他非数字字符)可以。一个元素只能有一个 ID 类型的属性。您可能已经猜到了,REQUIRED 意味着 <dvd>必须 有一个 code

<!ATTLIST dvd genre     ( Drama | Comedy | SciFi | Action | Romance ) #IMPLIED>

这是一个枚举。由于指定为 IMPLIED,因而是可选的。但是,如果在文档中出现 了,则必须是枚举值中的一个(读作 “Drama 或 Comedy 或……”)。

<!ELEMENT title    (#PCDATA)>
<!ELEMENT description  (#PCDATA)>
<!ELEMENT price    (#PCDATA)>
<!ELEMENT year      (#PCDATA)>

剩下的行都用于指定可解析字符数据。这些元素都不能有孩子。

现在尝试修改实例文档以便保证这些规则能够正常工作。首先增加一个 <description>,但是将其放在 <dvd> 的最后。如您所料,结果得到了一条错误消息(如图 6 所示)。

图 6. Description 错误
Description 错误

现在增加一个 genre(如 图 7 所示)。

图 7. Genre 错误
Genre 错误

为什么不行呢?列表中有科幻小说啊!不过,您知道,XML 是大小写敏感的,因此 "scifi" 是无效的,而必须是 "SciFi"

现在看看 ID 是否必须是惟一的。将 code 复制到另一个 <dvd> 中(如 图 8 所示)。

图 8. ID 错误
ID 错误

毫无疑问都看到了适当的错误。您应该清楚了。可使用这里的 DTD 和 XML 文件尝试其他修改(源文件请参阅 下载 部分)。

DTD 异常处理

为了处理 DTD 操作错误必须启用验证。对于 Xerces,只要将模式验证特性设置为 true

parser.setFeature( 
    "http://apache.org/xml/features/validation/schema",     
    true );

通过 Apache Software Foundation 网站(请参阅 参考资料)可以了解 Xerces 解析器的各种特性。关于 DTD 验证的更多信息,请参阅 XML in a Nutshell, Third Edition(参见 参考资料)的第 3 章。

用 SAXEcho 验证

现在来看看验证。注释掉 XML 文档中 Life as a Housedvdprice,再看看结果如何,分别使用 DTD 和 XSD 文件进行验证。清单 6 显示了输出结果。

清单那 6. 执行 SAXEcho 的输出
=== Parsing catalogDTD.xml ===
<catalog><dvd><title>Terminator 2</title><description>
      A shape-shifting cyborg is sent back from the future 
      to kill the leader of the resistance.
    </description><price>19.95</price><year>1991</year>
    </dvd><dvd><title>The Matrix</title><price>10.95
    </price><year>1999</year></dvd><dvd><title>
    Life as a House</title><description>
      When a man is diagnosed with terminal cancer, 
      he takes custody of his misanthropic teenage son.
    </description><year>2001</year>
*** Failed validation ***
* The content of element type "dvd" must match "(title,description?,price,year)".
*************************
</dvd><dvd><title>Raiders of the Lost Ark</title>
<price>14.95
</price><year>1981</year></dvd></catalog>
=== Parsing catalogXSD.xml ===
<catalog>

    <dvd>
        <title>Terminator 2</title>
    <description>
      A shape-shifting cyborg is sent back from the future 
      to kill the leader of the resistance.
    </description>
        <price>19.95</price>
        <year>1991</year>
    </dvd>
    <dvd>
        <title>The Matrix</title>
        <price>10.95</price>
        <year>1999</year>
    </dvd>
    <dvd>
        <title>Life as a House</title>
    <description>
      When a man is diagnosed with terminal cancer, 
      he takes custody of his misanthropic teenage son.
    </description>
    
        
*** Failed validation ***
* cvc-complex-type.2.4.a: Invalid content was found starting with 
element 'year'. One of '{"":price}' is expected.
*************************
<year>2001</year>
    </dvd>
    <dvd>
        <title>Raiders of the Lost Ark</title>
        <price>14.95</price>
        <year>1981</year>
    </dvd>
</catalog>

使用 XML 模式验证

您也许会奇怪,既然可以用 DTD 保证文档结构和内容的有效性,为何需要验证文档的其他 方式呢?下面给出几个理由:

  • 对元素和属性值的控制粒度:XML Schema 允许指定格式、长度和数据类型。
  • 复杂数据类型:XML Schema 支持从已有类型创建新的数据类型和规范。
  • 元素频次:使用 XML Schema,可以在更细的粒度上控制元素。
  • 名称空间:XML Schema 使用名称空间,名称空间对于和其他组织打交道的组织来说越来越重要。

XML Schema 语言比 DTD 语言更强大,因此也更复杂。好的方面是 XML Schemas 用 XML 编写,而 DTD 不是。

XSD

XML Schema 也称为 XML Schema Definition,因此文件扩展名为 .xsd。

我们来验证一下 DTD 验证所用的同一个 XML 实例文档(如 清单 1 所示)。清单 7 显示了 XML Schema:

清单 7. Catalog XML Schema
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema elementFormDefault="qualified" xml:lang="EN"
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
    
    <!-- Our DVD catalog contains four or more DVDs -->
  <xs:element name="catalog">
        <xs:complexType>
            <xs:sequence minOccurs="4" maxOccurs="unbounded">
                <xs:element ref="dvd"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    
    <!-- DVDs have a title, an optional description, a price, and a release year -->
    <xs:element name="dvd">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="title"       type="xs:string"/>
                <xs:element name="description" type="descriptionString" 
minOccurs="0"/>
<xs:element name="price" type="priceValue"/> <xs:element name="year" type="yearString"/> </xs:sequence> <xs:attribute name="code" type="xs:ID"/> <!-- requires a unique ID --> <xs:attribute name="genre"> <!-- default = optional --> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Drama"/> <xs:enumeration value="Comedy"/> <xs:enumeration value="SciFi"/> <xs:enumeration value="Action"/> <xs:enumeration value="Romance"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> </xs:element> <!-- Descriptions must be between 10 and 120 characters long --> <xs:simpleType name="descriptionString"> <xs:restriction base="xs:string"> <xs:minLength value="10"/> <xs:maxLength value="120"/> </xs:restriction> </xs:simpleType> <!-- Price must be < 100.00 --> <xs:simpleType name="priceValue"> <xs:restriction base="xs:decimal"> <xs:totalDigits value="4"/> <xs:fractionDigits value="2"/> <xs:maxExclusive value="100.00"/> </xs:restriction> </xs:simpleType> <!-- Year must be 4 digits, between 1900 and 2099 --> <xs:simpleType name="yearString"> <xs:restriction base="xs:string"> <xs:pattern value="(19|20)\d\d"/> </xs:restriction> </xs:simpleType> </xs:schema>

要注意,XML Schema 与对应的 DTD 相比要长得多。事实上,即便去掉注释和空白,这个模式仍然超过了 50 行,而 DTD 模式只有九行。(当然,这个模式检查的细节要比 DTD 多。)因此,控制的粒度越精细,代码也越复杂,而且复杂得。这意味着,如果验证不需要 XML Schema,则使用 DTD。

除了和前面 DTD 中所用的可比较的约束以外,看看 XML Schemas 增加了什么以及为 DVD catalog 文档带来了哪些好处:

  • 对元素和属性值控制的粒度大小:和允许任何字符值的 DTD 不同,XSD 约束了 descriptions(20 到 120 个字符)、prices(0.00 到 100.00)和 years(1900 到 2999)的值。
  • 复杂数据类型: 创建了将来可重用、可扩展的新数据类型:dvddescriptionStringpriceValueyearString
  • 元素出现频次:因为本教程使用的例子文档很小,我把 DVD 的数量设置为四个或更多以便使该文档有效。实际上,最小值可能是一个很大的数,不过通过这个例子可以看到这种约束是可能的。
  • 名称空间:仅对 XML Schema 类型使用了名称空间,但是由于 XML Schemas 是名称空间感知的,因而可以增加更多名称空间来控制名称冲突。

我们更详细地讨论一下 XML Schema 以便理解其内容:

  • xs:complexTypexs:simpleTypecomplexType 元素可以包含其他元素或属性:

    <xs:element name="dvd">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="title" type="xs:string"/>
      ...

    simpleType 元素只能包含文本和它自己的属性值:

    <xs:simpleType name="yearString">
        <xs:restriction base="xs:string">
            <xs:pattern value="(19|20)\d\d"/>
        </xs:restriction>
    </xs:simpleType>

    这个具体的例子中定义了一种新类型 yearString,它必须包含四位数字,并且最高两位是 “19” 或 “20”。使用 xs:restriction 元素从已有的(基)类型派生新的、受限制的类型。使用 xs:pattern 刻面元素来比较值,检查是否和指定的表达式匹配(请参阅 刻面)。

  • xs:sequence. 孩子元素必须按照所列顺序出现(虽然 minOccurs 可以把元素变成可选的,如前所述):

    <xs:sequence>
        <xs:element name="title"       type="xs:string"/>
        <xs:element name="description" type="descriptionString" minOccurs="0"/>
        <xs:element name="price"       type="priceValue"/>
        <xs:element name="year"        type="yearString"/>
    </xs:sequence>

    sequence 声明,有效文档中的 dvd 必须有一个 title,后面可以跟 10 到 120 个字符长的 description,后跟小于 US$100 形如 “nn.nn” 的 price,最后是 year

刻面

Schemas 对值支持一组可能的方面。这些方面称为刻面,在 restriction 中用于约束有效值。支持下列刻面类型:

  • pattern
  • enumeration
  • minLengthmaxLength
  • minInclusivemaxInclusiveminExclusivemaxExclusive
  • totalDigitsfractionDigits
  • whiteSpace

注意:XML Schemas 验证需要 XMLBuddy Pro

现在做一些修改看看约束是否生效了。 为 Adventure 增加 genre,输入长度超过 120 字符的 description、设置重复的 dvd code(如 图 9 所示)。

图 9. XSD 错误
XSD 错误

可以发现 genre、惟一的 IDdescription 长度都是强制性的。

XML Schema 还能做更多。下面强调几点:

  • xs:choice:必须出现其中的一个孩子。
  • xs:all:列出的每个孩子都必须出现一次,但是对顺序没有要求。
  • xs:group:可以定义和引用一组元素的组名(通过 ref=groupName)。
  • xs:attributeGroup:和用于元素的 xs:group 一样,这个相对应的指示符用于属性。
  • xs:date:这是 ISO 8601 定义的格里高利历法日期,格式为 YYYY-MM-DD。
  • xs:time:hh:mm:ss 形式的时间,用 "Z" 表示 UTC 相对时间。
  • xs:duration:一定数量的年、月、日、时、分。

可以看到,编写 XML Schema 时可以利用很多内建的强大功能。如果找不到需要的类型,可以创建新的类型。

数据类型

XML Schema 的一个强大特性是能够创建新的数据类型。在 catalog.xsd 文件中可以看到使用了大量新建数据类型,包括 yearStringpriceValue 类型。在该例中,这些类型只在 dvd 类型中使用,但是可以文档中出现 yearprice 的任何地方使用。

这些类型扩展原有的小数和字符串类型:

    <!-- Price must be < 100.00 -->
    <xs:simpleType name="priceValue">
        <xs:restriction base="xs:decimal">
            <xs:totalDigits    value="4"/>
            <xs:fractionDigits value="2"/>
            <xs:maxExclusive   value="100.00"/>
        </xs:restriction>
    </xs:simpleType>

    <!-- Year must be 4 digits, between 1900 and 2099 -->
    <xs:simpleType name="yearString">
        <xs:restriction base="xs:string">
            <xs:pattern value="(19|20)\d\d"/>
        </xs:restriction>
    </xs:simpleType>

如前所述,可以结合使用 restriction 元素和一种或更多刻面来实现已有类型的特化。如果有多个刻面,可结合起来确定哪些值有效,哪些值无效。

范式匹配

pattern 刻面元素支持一种类似 Perl 的丰富的表达式语法。前面在 yearString 中用到了它,范式 “ (19|20)\d\d” 应读作 “字符串必须以 19 或 20 开始并且后跟两位数字”。表 1 列出了几个范式。

表 1. XML Schema 范式匹配表达式
范式匹配
(A|B)匹配 A 或 B 的字符串
A?和 A 匹配的字符串出现零次或一次
A*和 A 匹配的字符串出现零次或多次
A+和 A 匹配的字符串出现一次或多次
[abcd]和指定字符之一匹配的单个字符
[^abc]和指定字符之外的字符匹配的单个字符
\t制表符
\\反斜杠
\cXML 名称字符
\s空格、制表符、回车换行或者新行字符
.回车换行或新行字符外的任何字符

更多表达式请参阅 XML in a Nutshell, Third Edition 的第 427-429 页或者阅读 XML Bible, Second Edition 在线版(请参阅 参考资料)第 24 章的表 24-5。

XSD 异常处理

为了处理 XML Schema 操作异常,必须启用验证。对于 Xerces 只要将模式验证特性设置为 true

parser.setFeature( 
    "http://apache.org/xml/features/validation/schema",     
    true );

可以通过 Apache Software Foundation 网站(请参阅 参考资料)了解 Xcerse 解析器的各种特性。

前面讨论过由于操纵问题可能造成的 DOMExceptionDOMExceptioncode 表明发生了什么类型的问题。

回顾 DOMEcho

改变 DOMEcho.java 逻辑引发一个 DOMException。新的逻辑如下:

//---- find parent Node
Element indianaJones = document.getElementById("_7755522");
//---- insert a description before the price 
//    (anywhere else would be invalid)
NodeList years = indianaJones.getElementsByTagName("price");
Node desc = document.createTextNode(
        "Indiana Jones is hired to find the Ark of the Covenant");
        // This change will now fail validation.
indianaJones.insertBefore( desc, indianaJones  );

从而执行下列代码:

short code = e.code;
...
} else if( code == DOMException.NOT_FOUND_ERR ) {
  //take action when element or attribute not found
  echo( "*** Element not found" );
  System.exit(code);
}

关于 XML Schemas 验证的更多信息,请阅读 XML in a Nutshell, Third Edition 的第 17 章、W3Schools 或者 “Interactive XML tutorials”(请参阅 参考资料)。


使用 XQuery

XML Query(XQuery)是一种用于编写表达式的语言,表达式从 XML 数据(通常是数据库)中返回匹配的结果。其功能类似于操作非 XML 内容的 SQL:

“与 SQL 相似,XQuery 包括从多个数据集中提取、汇总、聚合和连接数据的功能。”
—— “Java theory and practice: Screen-scraping with XQuery”,Brian Goetz(请参阅 参考资料

XQuery 扩展了 XPath 表达式,后者将在本系列教程的第四部分 XML 转换 中详细讨论。XPath 表达式也是有效的 XQuery 表达式。那么为什么要用 XQuery 呢?XQuery 的价值在于它在表达式中增加的子句,从而能够实现和 SQL 中的 SELECT 语句功能类似的更复杂的表达式。

XQuery 子句

XQuery 包含多个子句,用缩写词 FLWOR 表示:for、let、where、order by、return。表 2 说明了各个部分。

表 2. FLWOR 子句
子句说明
for使用这种循环结构将值赋给其他子句中使用的变量。变量使用美元符号声明,比如 $name,然后从搜索结果中获得赋给它们的值。
let使用 let 将值赋给 for 以外的变量。
where和 SQL 相似,使用 where 子句根据某种条件对结果进行筛选。
order by使用该子句确定如何对结果集进行排序(ascendingdescending)。
return使用 return 子句确定查询要输出的内容。内容可以包括文字、XML 文档内容、HTML 标记或者其他任何东西。

XQuery 包含由结果为 truefalse 的条件组成 FLWOR 子句中的检索条件。下面看一些例子。可用 清单 8 中所示的 dvd.xml 作为 XML 实例文档。

清单 8. dvd.xml
<?xml version="1.0"?>
<!-- DVD inventory -->
<catalog>
    <dvd code="1234567">
        <title>Terminator 2</title>
        <price>19.95</price>
        <year>1991</year>
    </dvd>
    <dvd code="7654321">
        <title>The Matrix</title>
        <price>12.95</price>
        <year>1999</year>
    </dvd>
    <dvd code="2255577">
        <title>Life as a House</title>
        <price>15.95</price>
        <year>2001</year>
    </dvd>
    <dvd code="7755522">
        <title>Raiders of the Lost Ark</title>
        <price>14.95</price>
        <year>1981</year>
    </dvd>
</catalog>

Saxon

如果希望自己尝试 XQuery,可以从 Saxonica 免费下载 Saxon 工具(请参阅 参考资料)。

为了试验,我使用了 Saxon XQuery 工具。所有文件都放到解压 Saxon 的目录中。为了使用 XQuery 创建一个 HTML 页面按升序列出所有 DVD 的标题,我使用 清单 9 中所示的 dvdTitles.xq 文件,其中也显示了输出结果。执行该查询使用了下面的命令:

java -cp saxon8.jar net.sf.saxon.Query -t dvdTitles.xq > dvdTitles.html
清单 9. 按升序列出 DVD 标题的 XQuery
dvdTitles.xq:

<html>
<body>
  Available DVDs:
  <br/>
  <ol>
  {
  for $title in doc("dvd.xml")/catalog/dvd/title
    order by $title
    return <li>{data($title)}</li>
  }
  </ol>
</body>
</html>

                    dvdTitles.html:

<?xml version="1.0" encoding="UTF-8"?>
<html>
   <body>
  Available DVDs:
  <br/>
      <ol>
         <li>Life as a House</li>
         <li>Raiders of the Lost Ark</li>
         <li>Terminator 2</li>
         <li>The Matrix</li>
      </ol>
   </body>
</html>

仔细观察 清单 9 中的 XQuery 逻辑。首先,查询必须用花括号(“{}”)包围起来。可以看到,该例中使用了三个子句(fororder byreturn)。使用 doc() 函数打开一个 XML 文档。$title 是一个变量,在循环中设置为每个搜索结果。具体到该例中,它表示 /catalog/dvd/title 表达式的结果,即 DVD 的标题。返回子句中的 data() 函数输出 XML 中的值而不包含标记。如果仅仅使用 $title,就会得到 “<title>value</title>”,这不是希望出现在 HTML 输出中的结果。要注意 XQuery 包围在完成该网页所需要的全部 HTML 代码中。

现在,假设需要按降序输出超过 15 美元的 DVD 的价格。清单 10 显示了 XQuery 及输出。

清单 10. 按降序输出超过 15 美元的 DVD 价格
dvdPriceThreshold.hq

<html>
<body>
  DVDs prices below $15.00:
  <br/>
  <ol>
  {
  for $price in doc("dvd.xml")/catalog/dvd/price
    where $price < 15.00
    order by $price descendingreturn <li>{data($price)}</li>
  }
  </ol>
</body>
</html>

                    dvdPrices.html

<?xml version="1.0" encoding="UTF-8"?>
<html>
   <body>
   DVDs prices below $15.00:
  <br/>
      <ol>
         <li>14.95</li>
         <li>12.95</li>
      </ol>
   </body>
</html>

该查询中主要的区别是指定了 where 子句。仅仅为了好玩一点儿而改变了排列顺序。

显然,还可以做很多实验来学习 XQuery 的强大功能,不过我已经介绍得够多了。如果想进一步了解,请参阅 “XQuery” 和 “Five Practical XQuery Applications”(请参阅 参考资料)。


结束语

XML 的核心是解析和验证。了解如何充分利用这些功能对于在项目中能否成功地引入 XML 至关重要。

结束语

本教程介绍了 XML 处理,主要涉及:

  • 使用 SAX2 和 DOM2 解析器解析 XML 文档
  • 用 DTD 和 XML Schemas 验证 XML 文档
  • 使用 XQuery 从数据库中访问 XML 内容

下载

描述名字大小
Sample DTD and XML filesx-cert1423-code-samples.zip16KB

参考资料

学习

  • 准备 XML 及相关技术认证(developerWorks,2006 年 8 到 10 月):本系列教程分为五部分,帮助您准备参加 IBM 认证考试 142(XML and Related Technologies),通过 IBM Certified Solution Developer - XML and Related Technologies 认证。
  • XML: A Manager's Guide, Second Edition(Kevin Dick,Addison-Wesley Professional,2002):阅读该书了解 XML 技术在企业应用程序中的应用。
  • XML in a Nutshell, 3rd Edition(Elliotte Rusty Harold 和 W. Scott Means,O'Reilly Media,2004,ISBN: 0596007647):这是本全面的 XML 参考,涵盖了基本语法规则、DTD 和 XML Schema 创建、XSLT 转换、处理 API、XML 1.1,以及 SAX2 和 DOM Level 3。
  • XML:developerWorks 中国网站 XML 专区提供了各种技术文章和技巧、教程、标准和 IBM 红皮书。
  • developerWorks 技术活动网络广播:通过这些活动跟踪技术的发展。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=175415
ArticleTitle=准备 XML 及相关技术认证,第 3 部分: XML 处理
publish-date=11202006