Java 语言的 XPath API

Java 程序中查询 XML

Elliotte Harold ([email protected]), 副教授, Polytechnic University

简介: XPath 表达式比繁琐的文档对象模型(DOM)导航代码要容易编写得多。如果需要从 XML 文档中提取信息,最快捷、最简单的办法就是在 Java™ 程序中嵌入 XPath 表达式。Java 5 推出了 javax.xml.xpath 包,这是一个用于 XPath 文档查询的独立于 XML 对象模型的库。

如果要告诉别人买一加仑牛奶,您会怎么说?请去买一加仑牛奶回来还是从前门出去,向左转,走三个街区向右转,再走半个街区向右转进入商店。走向四号通道,沿通道走五米向左,拿一瓶一加仑装的牛奶然后到收银台付款。再沿原路回家。简直太可笑了。只要在请去买一加仑牛奶回来的基础上稍加指示,多数成人都能自己买回牛奶来。

查询语言和计算机搜索与此类似。直接说找一个 Cryptonomicon 的副本要比编写搜索某个数据库的详细逻辑容易得多。由于搜索操作的逻辑非常相似,可以发明一种通用语言让您使用找到 Neal Stephenson 的所有著作这样的命令,然后编写对特定数据存储执行此类查询的引擎。

XPath

在众多查询语言之中,结构化查询语言(SQL)是一种针对查询特定类型的关系库而设计和优化的语言。其他不那么常见的查询语言还有对象查询语言(OQL)和 XQuery。但本文的主题是 XPath,一种为查询 XML 文档而设计的查询语言。比如,下面这个简单的 XPath 查询可以在文档中找到作者为 Neal Stephenson 的所有图书的标题:

//book[author="Neal Stephenson"]/title

 

作为对照,查询同样信息的纯 DOM 搜索代码如 清单 1 所示:


清单 1. 找到 Neal Stephenson 所有著作 title 元素的 DOM 代码

        ArrayList result = new ArrayList();

        NodeList books = doc.getElementsByTagName("book");

        for (int i = 0; i < books.getLength(); i++) {

            Element book = (Element) books.item(i);

            NodeList authors = book.getElementsByTagName("author");

            boolean stephenson = false;

            for (int j = 0; j < authors.getLength(); j++) {

                Element author = (Element) authors.item(j);

                NodeList children = author.getChildNodes();

                StringBuffer sb = new StringBuffer();

                for (int k = 0; k < children.getLength(); k++) {

                    Node child = children.item(k);

                    // really should to do this recursively

                    if (child.getNodeType() == Node.TEXT_NODE) {

                        sb.append(child.getNodeValue());

                    }

                }

                if (sb.toString().equals("Neal Stephenson")) {

                    stephenson = true;

                    break;

                }

 

            }

 

            if (stephenson) {

                NodeList titles = book.getElementsByTagName("title");

                for (int j = 0; j < titles.getLength(); j++) {

                    result.add(titles.item(j));

                }

            }

 

        }

 

不论您是否相信,清单 1 中的 DOM 显然不如简单的 XPath 表达式通用或者健壮。您愿意编写、调试和维护哪一个?我想答案很明显。

但是虽然有很强的表达能力,XPath 并不是 Java 语言,事实上 XPath 不是一种完整的编程语言。有很多东西用 XPath 表达不出来,甚至有些查询也无法表达。比方说,XPath 不能查找国际标准图书编码(ISBN)检验码不匹配的所有图书,或者找出境外帐户数据库显示欠帐的所有作者。幸运的是,可以把 XPath 结合到 Java 程序中,这样就能发挥两者的优势了:Java Java 所擅长的,XPath XPath 所擅长的。

直到最近,Java 程序执行 XPath 查询所需要的应用程序编程接口(API)还因形形色色的 XPath 引擎而各不相同。Xalan 有一种 APISaxon 使用另一种,其他引擎则使用其他的 API。这意味着代码往往把您限制到一种产品上。理想情况下,最好能够试验具有不同性能特点的各种引擎,而不会带来不适当的麻烦或者重新编写代码。

于是,Java 5 推出了 javax.xml.xpath 包,提供一个引擎和对象模型独立的 XPath 库。这个包也可用于 Java 1.3 及以后的版本,但需要单独安装 Java API for XML Processing (JAXP) 1.3Xalan 2.7 Saxon 8 以及其他产品包含了这个库的实现。

回页首

一个简单的例子

我将举例说明如何使用它。然后再讨论一些细节问题。假设要查询一个图书列表,寻找 Neal Stephenson 的著作。具体来说,这个图书列表的形式如 清单 2 所示:


清单 2. 包含图书信息的 XML 文档

<inventory>

    <book year="2000">

        <title>Snow Crash</title>

        <author>Neal Stephenson</author>

        <publisher>Spectra</publisher>

        <isbn>0553380958</isbn>

        <price>14.95</price>

    </book>

 

    <book year="2005">

        <title>Burning Tower</title>

        <author>Larry Niven</author>

        <author>Jerry Pournelle</author>

        <publisher>Pocket</publisher>

        <isbn>0743416910</isbn>

        <price>5.99</price>

    <book>

 

    <book year="1995">

        <title>Zodiac</title>

        <author>Neal Stephenson<author>

        <publisher>Spectra</publisher>

        <isbn>0553573862</isbn>

        <price>7.50</price>

    <book>

 

    <!-- more books... -->

 

</inventory>

 

抽象工厂

XPathFactory 是一个抽象工厂。抽象工厂设计模式使得这一种 API 能够支持不同的对象模型,如 DOMJDOM XOM。为了选择不同的模型,需要向XPathFactory.newInstance() 方法传递标识对象模型的统一资源标识符(URI)。比如 http://xom.nu/ 可以选择 XOM。但实际上,到目前为止 DOM 是该 API 支持的惟一对象模型。

查找所有图书的 XPath 查询非常简单://book[author="Neal Stephenson"]。为了找出这些图书的标题,只要增加一步,表达式就变成了 //book[author="Neal Stephenson"]/title。最后,真正需要的是title 元素的文本节点孩子。这就要求再增加一步,完整的表达式就是//book[author="Neal Stephenson"]/title/text()

现在我提供一个简单的程序,它从 Java 语言中执行这个查询,然后把找到的所有图书的标题打印出来。首先,需要将文档加载到一个 DOMDocument 对象中。为了简化起见,假设该文档在当前工作目录的 books.xml 文件中。下面的简单代码片段解析文档并建立对应的Document 对象:


清单 3. JAXP 解析文档

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        factory.setNamespaceAware(true); // never forget this!

        DocumentBuilder builder = factory.newDocumentBuilder();

        Document doc = builder.parse("books.xml");

 

到目前为止,这仅仅是标准的 JAXP DOM,没有什么新鲜的。

接下来创建 XPathFactory

XPathFactory factory = XPathFactory.newInstance();

 

然后使用这个工厂创建 XPath 对象:

XPath xpath = factory.newXPath();

 

XPath 对象编译 XPath 表达式:

PathExpression expr = xpath.compile("//book[author='Neal Stephenson']/title/text()");

 

直接求值

如果 XPath 表达式只使用一次,可以跳过编译步骤直接对XPath 对象调用 evaluate() 方法。但是,如果同一个表达式要重复使用多次,编译可能更快一些。

最后,计算 XPath 表达式得到结果。表达式是针对特定的上下文节点计算的,在这个例子中是整个文档。还必须指定返回类型。这里要求返回一个节点集:

Object result = expr.evaluate(doc, XPathConstants.NODESET);

 

可以将结果强制转化成 DOM NodeList,然后遍历列表得到所有的标题:

        NodeList nodes = (NodeList) result;

        for (int i = 0; i < nodes.getLength(); i++) {

            System.out.println(nodes.item(i).getNodeValue());

        }

 

清单 4 把上述片段组合到了一个程序中。还要注意,这些方法可能抛出一些检查异常,这些异常必须在 throws 子句中声明,但是我在上面把它们掩盖起来了:


清单 4. 用固定的 XPath 表达式查询 XML 文档的完整程序

import java.io.IOException;

import org.w3c.dom.*;

import org.xml.sax.SAXException;

import javax.xml.parsers.*;

import javax.xml.xpath.*;

 

public class XPathExample {

 

  public static void main(String[] args)

   throws ParserConfigurationException, SAXException,

          IOException, XPathExpressionException {

 

    DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();

    domFactory.setNamespaceAware(true); // never forget this!

    DocumentBuilder builder = domFactory.newDocumentBuilder();

    Document doc = builder.parse("books.xml");

 

    XPathFactory factory = XPathFactory.newInstance();

    XPath xpath = factory.newXPath();

    XPathExpression expr

     = xpath.compile("//book[author='Neal Stephenson']/title/text()");

 

    Object result = expr.evaluate(doc, XPathConstants.NODESET);

    NodeList nodes = (NodeList) result;

    for (int i = 0; i < nodes.getLength(); i++) {

        System.out.println(nodes.item(i).getNodeValue());

    }

 

  }

 

}

 

XPath 数据模型

每当混合使用诸如 XPath Java 这样两种不同的语言时,必定会有某些将两者粘合在一起的明显接缝。并非一切都很合拍。XPath Java 语言没有同样的类型系统。XPath 1.0 只有四种基本数据类型:

  • node-set
  • number
  • boolean
  • string

当然,Java 语言有更多的数据类型,包括用户定义的对象类型。

多数 XPath 表达式,特别是位置路径,都返回节点集。但是还有其他可能。比如,XPath 表达式 count(//book) 返回文档中的图书数量。XPath 表达式 count(//book[@author="Neal Stephenson"]) > 10 返回一个布尔值:如果文档中 Neal Stephenson 的著作超过 10 本则返回 true,否则返回 false

evaluate() 方法被声明为返回 Object。实际返回什么依赖于 XPath 表达式的结果以及要求的类型。一般来说,XPath

  • number 映射为 java.lang.Double
  • string 映射为 java.lang.String
  • boolean 映射为 java.lang.Boolean
  • node-set 映射为 org.w3c.dom.NodeList

XPath 2

前面一直假设您使用的是 XPath 1.0XPath 2 大大扩展和修改了类型系统。Java XPath API 支持 XPath 2 所需的主要修改是为返回 XPath 2 新数据类型增加常量。

Java 中计算 XPath 表达式时,第二个参数指定需要的返回类型。有五种可能,都在 javax.xml.xpath.XPathConstants 类中命名了常量:

  • XPathConstants.NODESET
  • XPathConstants.BOOLEAN
  • XPathConstants.NUMBER
  • XPathConstants.STRING
  • XPathConstants.NODE

最后一个 XPathConstants.NODE 实际上没有匹配的 XPath 类型。只有知道 XPath 表达式只返回一个节点或者只需要一个节点时才使用它。如果 XPath 表达式返回了多个节点并且指定了 XPathConstants.NODE,则 evaluate() 按照文档顺序返回第一个节点。如果 XPath 表达式选择了一个空集并指定了 XPathConstants.NODE,则 evaluate() 返回 null

如果不能完成要求的转换,evaluate() 将抛出 XPathException

回页首

名称空间上下文

XML 文档中的元素在名称空间中,查询该文档的 XPath 表达式必须使用相同的名称空间。XPath 表达式不一定要使用相同的前缀,只需要名称空间 URI 相同即可。事实上,如果 XML 文档使用默认名称空间,那么尽管目标文档没有使用前缀,XPath 表达式也必须使用前缀。

但是,Java 程序不是 XML 文档,因此不能用一般的名称空间解析。必须提供一个对象将前缀映射到名称空间 URI。该对象是javax.xml.namespace.NamespaceContext 接口的实例。比如,假设图书文档放在 http://www.example.com/books 名称空间中,如 清单 5 所示:


清单 5. 使用默认名称空间的 XML 文档

<inventory xmlns="http://www.example.com/books">

    <book year="2000">

        <title>Snow Crash</title>

        <author>Neal Stephenson</author>

        <publisher>Spectra</publisher>

        <isbn>0553380958</isbn>

        <price>14.95<price>

    </book>

 

    <!-- more books... -->

 

<inventory>

 

查找 Neal Stephenson 全部著作标题的 XPath 表达式就要改为 //pre:book[pre:author="Neal Stephenson"]/pre:title/text()。但是,必须将前缀 pre 映射到 URI http://www.example.com/booksNamespaceContext 接口在 Java 软件开发工具箱(JDK)或 JAXP 中没有默认实现似乎有点笨,但确实如此。不过,自己实现也不难。清单 6 对一个名称空间给出了简单的实现。还需要映射xml 前缀。


清单 6. 绑定一个名称空间和默认名称空间的简单上下文

import java.util.Iterator;

import javax.xml.*;

import javax.xml.namespace.NamespaceContext;

 

public class PersonalNamespaceContext implements NamespaceContext {

 

    public String getNamespaceURI(String prefix) {

        if (prefix == null) throw new NullPointerException("Null prefix");

        else if ("pre".equals(prefix)) return "http://www.example.org/books";

        else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;

        return XMLConstants.NULL_NS_URI;

    }

 

    // This method isn't necessary for XPath processing.

    public String getPrefix(String uri) {

        throw new UnsupportedOperationException();

    }

 

    // This method isn't necessary for XPath processing either.

    public Iterator getPrefixes(String uri) {

        throw new UnsupportedOperationException();

    }

 

}

 

使用映射存储绑定和增加 setter 方法实现名称空间上下文的重用也不难。

创建 NamespaceContext 对象后,在编译表达式之前将其安装到 XPath 对象上。以后就可以像以前一样是用这些前缀查询了。比如:


清单 7. 使用名称空间的 XPath 查询

  XPathFactory factory = XPathFactory.newInstance();

  XPath xpath = factory.newXPath();

  xpath.setNamespaceContext(new PersonalNamespaceContext());

  XPathExpression expr

    = xpath.compile("//pre:book[pre:author='Neal Stephenson']/pre:title/text()");

 

  Object result = expr.evaluate(doc, XPathConstants.NODESET);

  NodeList nodes = (NodeList) result;

  for (int i = 0; i < nodes.getLength(); i++) {

      System.out.println(nodes.item(i).getNodeValue());

  }

 

回页首

函数求解器

有时候,在 Java 语言中定义用于 XPath 表达式的扩展函数很有用。这些函数可以执行用纯 XPath 很难或者无法执行的任务。不过必须是真正的函数,而不是随意的方法。就是说不能有副作用。(XPath 函数可以按照任意的顺序求值任意多次。)

通过 Java XPath API 访问的扩展函数必须实现 javax.xml.xpath.XPathFunction 接口。这个接口只声明了一个方法 evaluate

public Object evaluate(List args) throws XPathFunctionException

 

该方法必须返回 Java 语言能够转换到 XPath 的五种类型之一:

  • String
  • Double
  • Boolean
  • Nodelist
  • Node

比如,清单 8 显示了一个扩展函数,它检查 ISBN 的校验和并返回 Boolean。这个校验和的基本规则是前九位数的每一位乘上它的位置(即第一位数乘上 1,第二位数乘上 2,依次类推)。将这些数加起来然后取除以 11 的余数。如果余数是 10,那么最后一位数就是 X


清单 8. 检查 ISBN XPath 扩展函数

import java.util.List;

import javax.xml.xpath.*;

import org.w3c.dom.*;

 

public class ISBNValidator implements XPathFunction {

 

  // This class could easily be implemented as a Singleton.

   

  public Object evaluate(List args) throws XPathFunctionException {

 

    if (args.size() != 1) {

      throw new XPathFunctionException("Wrong number of arguments to valid-isbn()");

    }

 

    String isbn;

    Object o = args.get(0);

 

    // perform conversions

    if (o instanceof String) isbn = (String) args.get(0);

    else if (o instanceof Boolean) isbn = o.toString();

    else if (o instanceof Double) isbn = o.toString();

    else if (o instanceof NodeList) {

        NodeList list = (NodeList) o;

        Node node = list.item(0);

        // getTextContent is available in Java 5 and DOM 3.

        // In Java 1.4 and DOM 2, you'd need to recursively

        // accumulate the content.

        isbn= node.getTextContent();

    }

    else {

        throw new XPathFunctionException("Could not convert argument type");

    }

 

    char[] data = isbn.toCharArray();

    if (data.length != 10) return Boolean.FALSE;

    int checksum = 0;

    for (int i = 0; i < 9; i++) {

        checksum += (i+1) * (data[i]-'0');

    }

    int checkdigit = checksum % 11;

 

    if (checkdigit + '0' == data[9] || (data[9] == 'X' && checkdigit == 10)) {

        return Boolean.TRUE;

    }

    return Boolean.FALSE;

 

  }

 

}

 

下一步让这个扩展函数能够在 Java 程序中使用。为此,需要在编译表达式之前向 XPath 对象安装javax.xml.xpath.XPathFunctionResolver。函数求解器将函数的 XPath 名称和名称空间 URI 映射到实现该函数的 Java 类。清单 9是一个简单的函数求解器,将扩展函数 valid-isbn 和名称空间 http://www.example.org/books 映射到 清单 8 中的类。比如,XPath 表达式 //book[not(pre:valid-isbn(isbn))] 可以找到 ISBN 校验和不匹配的所有图书。


清单 9. 识别 valid-isbn 扩展函数的上下文

iimport javax.xml.namespace.QName;

import javax.xml.xpath.*;

 

public class ISBNFunctionContext implements XPathFunctionResolver {

 

  private static final QName name

   = new QName("http://www.example.org/books", "valid-isbn");

 

  public XPathFunction resolveFunction(QName name, int arity) {

      if (name.equals(ISBNFunctionContext.name) && arity == 1) {

          return new ISBNValidator();

      }

      return null;

  }

 

}

 

由于扩展函数必须有名称空间,所以计算包含扩展函数的表达式时必须使用 NamespaceResolver,即便查询的文档没有使用任何名称空间。由于 XPathFunctionResolverXPathFunction  NamespaceResolver 都是接口,如果方便的话可以将它们放在所有的类中。

回页首

结束语

SQL XPath 这样的声明性语言编写查询,要比使用 Java C 这样的命令式语言容易得多。但是,用 Java C 这样的图灵完整语言编写复杂的逻辑,又比 SQL XPath 这样的声明性语言容易得多。所幸的是,通过使用 Java Database Connectivity (JDBC) javax.xml.xpath 之类的 API 可以将两者结合起来。随着世界上越来越多的数据转向 XMLjavax.xml.xpath 将与 java.sql 一样变得越来越重要。

 

你可能感兴趣的:(xpath)