(ibm)Java 語言的 XPath API

來源:互聯網
上載者:User
 

XPath 運算式比繁瑣的文件物件模型(DOM)導航代碼要容易編寫得多。如果需要從 XML 文檔中提取資訊,最快捷、最簡單的辦法就是在 Java 程式中嵌入 XPath 運算式。Java 5 推出了 javax.xml.xpath 包,這是一個用於 XPath 文檔查詢的獨立於 XML 物件模型的庫。

如果要告訴別人買一加侖牛奶,您會怎麼說?“請去買一加侖牛奶回來” 還是 “從前門出去,向左轉,走三個街區向右轉,再走半個街區向右轉進入商店。走向四號通道,沿通道走五米向左,拿一瓶一加侖裝的牛奶然後到收銀台付款。再沿原路回家。” 簡直太可笑了。只要在 “請去買一加侖牛奶回來” 的基礎上稍加指示,多數成人都能自己買回牛奶來。

查詢語言和電腦搜尋與此類似。直接說 “找一個 Cryptonomicon 的副本” 要比編寫搜尋某個資料庫的詳細邏輯容易得多。由於搜尋操作的邏輯非常相似,可以發明一種通用語言讓您使用 “找到 Neal Stephenson 的所有著作” 這樣的命令,然後編寫對特定資料存放區執行此類查詢的引擎。

XPath

在眾多查詢語言之中,結構化查詢語言 (SQL)(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(API)還因形形色色的 XPath 引擎而各不相同。Xalan 有一種 API,Saxon 使用另一種,其他引擎則使用其他的 API。這意味著代碼往往把您限制到一種產品上。理想情況下,最好能夠實驗具有不同效能特點的各種引擎,而不會帶來不適當的麻煩或者重新編寫代碼。

於是,Java 5 推出了 javax.xml.xpath 包,提供一個引擎和物件模型獨立的 XPath 庫。這個包也可用於 Java 1.3 及以後的版本,但需要單獨安裝 Java API for XML Processing (JAXP) 1.3。Xalan 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 能夠支援不同的物件模型,如 DOM、JDOM 和 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 語言中執行這個查詢,然後把找到的所有圖書的標題列印出來。首先,需要將文檔載入到一個 DOM Document 對象中。為了簡化起見,假設該文檔在當前工作目錄的 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 只有四種基礎資料型別 (Elementary Data Type):

  • 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.0。XPath 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/books。NamespaceContext 介面在 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,即便查詢的文檔沒有使用任何名稱空間。由於 XPathFunctionResolverXPathFunctionNamespaceResolver 都是介面,如果方便的話可以將它們放在所有的類中。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.