JAVA語言的XPath API
在眾多查詢語言之中,結構化查詢語言(SQL)是一種針對查詢特定型別的關係庫而設計和優化的語言。其他不那麼常見的查詢語言還有物件查詢語言(OQL)和 XQuery。但本文的主題是 XPath,一種為查詢 XML 文件而設計的查詢語言。比如,下面這個簡單的 XPath 查詢可以在文件中找到作者為 Neal Stephenson 的所有圖書的標題:
//book[author="Neal Stephenson"]/title |
雖然有很強的表達能力,XPath 並不是 Java 語言,事實上 XPath 不是一種完整的程式語言。有很多東西用 XPath 表達不出來,甚至有些查詢也無法表達。比方說,XPath 不能查詢國際標準圖書編碼(ISBN)檢驗碼不匹配的所有圖書,或者找出境外帳戶資料庫顯示欠帳的所有作者。幸運的是,可以把 XPath 結合到 Java 程式中,這樣就能發揮兩者的優勢了:Java 做 Java 所擅長的,XPath 做 XPath 所擅長的。
直到最近,Java 程式執行 XPath 查詢所需要的應用程式程式設計介面(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 所示:
<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 語言中執行這個查詢,然後把找到的所有圖書的標題打印出來。首先,需要將文件載入到一個 DOMDocument
物件中。為了簡化起見,假設該文件在當前工作目錄的 books.xml 檔案中。下面的簡單程式碼片段解析文件並建立對應的Document
物件:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this! DocumentBuilder builder = factory.newDocumentBuilder(); ByteArrayInputStream bais = new ByteArrayInputStream(xmlStr.getBytes()); BufferedInputStream bis = new BufferedInputStream(bais);Document doc = builder.parse(bis); |
到目前為止,這僅僅是標準的 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
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 和 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.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
<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。
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 校驗和不匹配的所有圖書。
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
,即便查詢的文件沒有使用任何名稱空間。由於 XPathFunctionResolver
、XPathFunction
和 NamespaceResolver
都是介面,如果方便的話可以將它們放在所有的類中。
連結: