Sử dụng đối tượng NamespaceContext của ngôn ngữ Java với XPath
Các phương thức để phân tích các không gian tên với API của Java
Holger Kraus, Chuyên gia IT, IBM
Tóm tắt: Nếu bạn muốn dùng các không gian tên trong các biểu thức XPath, bạn
phải cung cấp đường dẫn của tiền tố được sử dụng cho URI của không gian tên.
Bài viết này mô tả ba biến thể của việc cung cấp tiền tố cho ánh xạ không gian tên.
Bài này có các đoạn mã mẫu giúp bạn dễ dàng viết mã cho NamespaceContext.
Điều kiện tiên quyết và ví dụ
Trong bài này, tôi giả sử rằng bạn đã quen với các chi tiết kỹ thuật được mô tả
trong "Đánh giá XPath từ nền tảng Java™" do Brett McLaughlin viết. Nếu bạn
chưa biết cách thực thi các chương trình Java sử dụng XPath, xin vui lòng tham
khảo bài viết của Brett (tìm thấy ở Tài nguyên.) Và bạn cũng đã biết các hàm API
cần thiết để đọc tệp XML và để đánh giá biểu thức XPath.
Bạn sẽ dùng tệp XML sau cho tất cả các ví dụ trong bài:
Ví dụ 1. Tệp XML mẫu
<?xml version="1.0" encoding="UTF-8"?>
<books:booklist
xmlns:books="http://univNaSpResolver/booklist"
xmlns="http://univNaSpResolver/book"
xmlns:fiction="http://univNaSpResolver/fictionbook">
<science:book xmlns:science="http://univNaSpResolver/sciencebook">
<title>Learning XPath</title>
<author>Michael Schmidt</author>
</science:book>
<fiction:book>
<title>Faust I</title>
<author>Johann Wolfgang von Goethe</author>
</fiction:book>
<fiction:book>
<title>Faust II</title>
<author>Johann Wolfgang von Goethe</author>
</fiction:book>
</books:booklist>
Mẫu XML này có ba không gian tên (namespace) được khai báo trong thành phần
gốc và một cái được khái báo trong thành phần con. Bạn sẽ thấy sự khác biệt từ ví
dụ này.
Các từ viết tắt hay dùng
API: Giao diện lập trình ứng dụng
DOM: Mô hình đối tượng tài liệu
URI: Định danh tài nguyên tổng quát
XHTML: Ngôn ngữ đánh dấu siêu văn bản mở rộng
XML: Ngôn ngữ đánh dấu mở rộng
XSD: Định nghĩa lược đồ XML
XSLT: Chuyển đổi ngôn ngữ định kiểu mở rộng
Điều thú vị thứ hai về XML mẫu này là thành phần booklist có ba thành phần con,
tất cả có tên là book. Nhưng thành phần con đầu tiên có không gian tên là science,
trong khi thành phần con khác có không gian tên là fiction. Điều này có nghĩa là,
những thành phần này là khác nhau đối với XPath. Bạn sẽ thấy các hệ quả trong
các ví dụ tiếp theo.
Cũng cần phải nói trước, đoạn mã không được tối ưu hóa cho việc bảo trì nhưng
nó dễ đọc. Có nghĩa là nó có một số chỗ dư thừa. Kết quả được đưa ra bằng cách
đơn giản nhất thông qua System.out.println(). Tất cả các dòng của đoạn mã liên
quan đến kết quả được viết tắt với ' ' trong bài viết. Tôi cũng không đề cập đến
phương thức trợ giúp (helper) trong bài này, nhưng chúng đi kèm trong tệp tải về
(Xem Tải về).
Nền tảng lý thuyết
Ý nghĩa của các không gian tên là gì và tại sao phải quan tâm tới chúng? Một
không gian tên là một phần của định danh cho một thành phần hay thuộc tính. Bạn
có thể có các thành phần hoặc thuộc tính với cùng tên cục bộ, nhưng khác không
gian tên. Chúng khác nhau hoàn toàn. Xem ví dụ ở trên (science:book và
fiction:book). Bạn cần các không gian tên để phân tích xung đột tên nếu bạn bạn
kết hợp các tệp XML từ các nguồn khác nhau. Lấy một tệp XSLT làm ví dụ. Nó
bao gồm các thành phần của không gian tên XSLT, các thành phần từ không gian
tên của chính bạn, và (thường thì) các thành phần của không gian tên XHTML.
Bằng cách sử dụng các không gian tên bạn có thể tránh sự nhập nhằng khó hiểu
liên quan đến các thành phần có cùng tên địa phương.
Không gian tên được xác định bởi URI (trong ví dụ này,
http://univNaSpResolver/booklist). Để tránh việc sử dụng chuỗi dài như thế, bạn
định nghĩa một tiền tố (prefix) mà gắn với URI này (trong ví dụ này, books). Xin
nhớ rằng tiền tố cũng giống như một biến: tên của nó không thành vấn đề. Nếu hai
tiền tố cùng tham chiếu tới một URI, không gian tên của các thành phần gắn với
tiền tố sẽ giống nhau (xem ví dụ 1 trong Ví dụ 5).
Một biểu thức XPath sử dụng các tiền tố (ví dụ, books:booklist/science:book) và,
bạn phải cung cấp URI gắn với mỗi tiền tố. Đây là nơi mà NamespaceContext xuất
hiện. Nó làm những việc đó.
Bài viết này giải thích các cách khác nhau để cung cấp ánh xạ giữa tiền tố và URI.
Trong tệp XML, ánh xạ được cung cấp bởi thuộc tính xmlns như:
xmlns:books="http://univNaSpResolver/booklist" hoặc
xmlns="http://univNaSpResolver/book" (không gian tên mặc định).
Sự cần thiết của việc cung cấp sự phân giải không gian tên
Nếu bạn có XML mà sử dụng các không gian tên, một biểu thức XPath sẽ thất bại
nếu bạn không cung cấp một NamespaceContext. Ví dụ 0 trong Ví dụ 2 minh họa
trường hợp này. Đối tượng XPath được xây dựng và ước lượng trên tài liệu XML.
Đầu tiên, thử viết biểu thức mà không có các tiền tố không gian tên (result1).
Trong phần hai, viết biểu thức với các tiền tố không gian tên result2).
Ví dụ 2. Ví dụ 0 mà không có sự phân giải không gian tên
private static void example0(Document example)
throws XPathExpressionException, TransformerException {
sysout("\n*** Zero example - no namespaces provided ***");
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList result1 = (NodeList) xPath.evaluate("booklist/book", example,
XPathConstants.NODESET);
NodeList result2 = (NodeList) xPath.evaluate(
"books:booklist/science:book", example, XPathConstants.NODESET);
}
Kết quả như sau.
Ví dụ 3. Kết quả từ ví dụ 0
*** Zero example - no namespaces provided ***
First try asking without namespace prefix:
> booklist/book
Result is of length 0
Then try asking with namespace prefix:
> books:booklist/science:book
Result is of length 0
The expression does not work in both cases.
Trong cả hai trường hợp, sự ước lượng XPath không trả lại bất cứ nốt nào và
không có ngoại lệ. XPath không thể tìm thấy nút nào bởi vì ánh xạ của các tiền tố
tới các URI bị thiếu.
Sự phân giải không gian tên được viết sẵn trong mã
Hoàn toàn có thể cung cấp các không gian tên như là các giá trị có sẵn trong mã
cái mà trông giống như lớp trong Ví dụ 4:
Ví dụ 4. Sự phân giải không gian tên được viết sẵn
public class HardcodedNamespaceResolver implements NamespaceContext {
/**
* This method returns the uri for all prefixes needed. Wherever possible
* it uses XMLConstants.
*
* @param prefix
* @return uri
*/
public String getNamespaceURI(String prefix) {
if (prefix == null) {
throw new IllegalArgumentException("No prefix provided!");
} else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
return "http://univNaSpResolver/book";
} else if (prefix.equals("books")) {
return "http://univNaSpResolver/booklist";
} else if (prefix.equals("fiction")) {
return "http://univNaSpResolver/fictionbook";
} else if (prefix.equals("technical")) {
return "http://univNaSpResolver/sciencebook";
} else {
return XMLConstants.NULL_NS_URI;
}
}
public String getPrefix(String namespaceURI) {
// Not needed in this context.
return null;
}
public Iterator getPrefixes(String namespaceURI) {
// Not needed in this context.
return null;
}
}
Xin lưu ý rằng không gian tên http://univNaSpResolver/sciencebook được gắn với
tiền tố technical (không phải science như trước). Bạn sẽ thấy hệ quả trong ví dụ
sau (xem Ví dụ 6). Trong Ví dụ 5, đoạn mã sử dụng trình phân giải này sử dụng
tiền tố mới.
Ví dụ 5. Ví dụ 1 với sự phân giải không gian tên được viết sẵn trong mã
private static void example1(Document example)
throws XPathExpressionException, TransformerException {
sysout("\n*** First example - namespacelookup hardcoded ***");
XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new HardcodedNamespaceResolver());
NodeList result1 = (NodeList) xPath.evaluate(
"books:booklist/technical:book", example,
XPathConstants.NODESET);
NodeList result2 = (NodeList) xPath.evaluate(
"books:booklist/fiction:book", example, XPathConstants.NODESET);
String result = xPath.evaluate("books:booklist/technical:book/:author",
example);
}
Kết quả như sau.
Ví dụ 6. Kết quả từ ví dụ 1
*** First example - namespacelookup hardcoded ***
Using any namespaces results in a NodeList:
> books:booklist/technical:book
Number of Nodes: 1
<?xml version="1.0" encoding="UTF-8"?>
<science:book xmlns:science="http://univNaSpResolver/sciencebook">
<title xmlns="http://univNaSpResolver/book">Learning XPath</title>
<author xmlns="http://univNaSpResolver/book">Michael Schmidt</author>
</science:book>
> books:booklist/fiction:book
Number of Nodes: 2
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust I</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust II</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
The default namespace works also:
> books:booklist/technical:book/:author
Michael Schmidt
Như bạn thấy, XPath đã tìm thấy các nút. Điều thuận lợi là bạn có thể thay đổi tên
các tiền tố nếu bạn muốn, cái mà tôi đã thực hiện với tiền tố science. Tệp XML
chứa tiền tố science, trong khi XPath sử dụng tiền tố khác, technical. Bởi vì các
URI là giống nhau, các nốt được tìm thấy bởi XPath. Điều bất tiện là bạn phải duy
trì các không gian tên ở nhiều nơi hơn: XML, có lẽ XSD, biểu thức XPath, và ngữ
cảnh không gian tên.
Đọc các không gian tên từ tài liệu
Các không gian tên và các tiền tố của chúng được ghi chép trong các tệp XML, do
đó bạn có thể sử dụng chúng từ đó. Cách dễ nhất là ủy nhiệm việc tìm kiếm cho tài
liệu.
Ví dụ 7. Sự phân giải không gian tên trực tiếp từ tài liệu
public class UniversalNamespaceResolver implements NamespaceContext {
// the delegate
private Document sourceDocument;
/**
* This constructor stores the source document to search the namespaces in
* it.
*
* @param document
* source document
*/
public UniversalNamespaceResolver(Document document) {
sourceDocument = document;
}
/**
* The lookup for the namespace uris is delegated to the stored document.
*
* @param prefix
* to search for
* @return uri
*/
public String getNamespaceURI(String prefix) {
if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
return sourceDocument.lookupNamespaceURI(null);
} else {
return sourceDocument.lookupNamespaceURI(prefix);
}
}
/**
* This method is not needed in this context, but can be implemented in a
* similar way.
*/
public String getPrefix(String namespaceURI) {
return sourceDocument.lookupPrefix(namespaceURI);
}
public Iterator getPrefixes(String namespaceURI) {
// not implemented yet
return null;
}
}
Những điều đáng chú ý:
Nếu tài liệu bị thay đổi trước khi XPath được dùng, sự thay đổi sẽ được thể
hiện trong việc tìm kiếm của không gian tên, bởi vì sự ủy nhiệm được hoàn
tất khi cần sử dụng phiên bản hiện tại của tài liệu.
Sự tìm kiếm các không gian tên hoặc các tiền tố được hoàn tất trong các nút
tiền bối, trong trường hợp của chúng ta nút sourceDocument. Điều này có
nghĩa là, với đoạn mã được cung cấp, bạn chỉ lấy các không gian tên được
mô tả trong nút gốc. Không gian tên science sẽ được tìm thấy trong ví dụ
này.
Việc tìm kiếm được gọi khi XPath được ước lượng, vì vậy nó sẽ tiêu tốn
thêm một chút thời gian.
Đây là đoạn mã ví dụ:
Ví dụ 8. Ví dụ 2 với sự phân giải không gian tên trực tiếp từ tài liệu
private static void example2(Document example)
throws XPathExpressionException, TransformerException {
sysout("\n*** Second example - namespacelookup delegated to document
***");
XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new UniversalNamespaceResolver(example));
try {
NodeList result1 = (NodeList) xPath.evaluate(
"books:booklist/science:book", example,
XPathConstants.NODESET);
} catch (XPathExpressionException e) {
}
NodeList result2 = (NodeList) xPath.evaluate(
"books:booklist/fiction:book", example, XPathConstants.NODESET);
String result = xPath.evaluate(
"books:booklist/fiction:book[1]/:author", example);
}
Kết quả là:
Ví dụ 9. Kết quả từ ví dụ 2
*** Second example - namespacelookup delegated to document ***
Try to use the science prefix: no result
> books:booklist/science:book
The resolver only knows namespaces of the first level!
To be precise: Only namespaces above the node, passed in the constructor.
The fiction namespace is such a namespace:
> books:booklist/fiction:book
Number of Nodes: 2
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust I</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust II</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
The default namespace works also:
> books:booklist/fiction:book[1]/:author
Johann Wolfgang von Goethe
Như bạn thấy trong kết quả, không gian tên được mô tả trong thành phần book với
tiền tố science không được phân giải. Phương thức ước lượng gửi trả lại một
XPathExpressionException. Để tránh vấn đề này, bạn có lẽ tách nốt science:book
khỏi tài liệu và sử dụng nốt này như là sự ủy nhiệm. Nhưng điều này có nghĩa bạn
phải phân tích thêm tài liệu và không hiệu quả cho lắm.
Đọc các không gian tên từ tài liệu và lưu trữ trong vùng đệm
Phiên bản tiếp theo này của NamespaceContext thì tốt hơn. Nó đọc các không gian
tên chỉ một lần trong phương thức khởi tạo. Mọi lời gọi cho một không gian tên
đều được trả lời từ một vùng đệm. Và kết quả, một sự thay đổi trong tài liệu không
ảnh hưởng khi mà danh sách các không gian tên được lưu trữ tạm thời tại thời
điểm tạo đối tượng Java.
Ví dụ 10. Lưu trữ sự phân giải không gian tên trong vùng đệm từ tài liệu
public class UniversalNamespaceCache implements NamespaceContext {
private static final String DEFAULT_NS = "DEFAULT";
private Map<String, String> prefix2Uri = new HashMap<String, String>();
private Map<String, String> uri2Prefix = new HashMap<String, String>();
/**
* This constructor parses the document and stores all namespaces it can
* find. If toplevelOnly is true, only namespaces in the root are used.
*
* @param document
* source document
* @param toplevelOnly
* restriction of the search to enhance performance
*/
public UniversalNamespaceCache(Document document, boolean toplevelOnly)
{
examineNode(document.getFirstChild(), toplevelOnly);
System.out.println("The list of the cached namespaces:");
for (String key : prefix2Uri.keySet()) {
System.out
.println("prefix " + key + ": uri " + prefix2Uri.get(key));
}
}
/**
* A single node is read, the namespace attributes are extracted and stored.
*
* @param node
* to examine
* @param attributesOnly,
* if true no recursion happens
*/
private void examineNode(Node node, boolean attributesOnly) {
NamedNodeMap attributes = node.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attribute = attributes.item(i);
storeAttribute((Attr) attribute);
}
if (!attributesOnly) {
NodeList chields = node.getChildNodes();
for (int i = 0; i < chields.getLength(); i++) {
Node chield = chields.item(i);
if (chield.getNodeType() == Node.ELEMENT_NODE)
examineNode(chield, false);
}
}
}
/**
* This method looks at an attribute and stores it, if it is a namespace
* attribute.
*
* @param attribute
* to examine
*/
private void storeAttribute(Attr attribute) {
// examine the attributes in namespace xmlns
if (attribute.getNamespaceURI() != null
&& attribute.getNamespaceURI().equals(
XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) {
// Default namespace xmlns="uri goes here"
if
(attribute.getNodeName().equals(XMLConstants.XMLNS_ATTRIBUTE)) {
putInCache(DEFAULT_NS, attribute.getNodeValue());
} else {
// The defined prefixes are stored here
putInCache(attribute.getLocalName(), attribute.getNodeValue());
}
}
}
private void putInCache(String prefix, String uri) {
prefix2Uri.put(prefix, uri);
uri2Prefix.put(uri, prefix);
}
/**
* This method is called by XPath. It returns the default namespace, if the
* prefix is null or "".
*
* @param prefix
* to search for
* @return uri
*/
public String getNamespaceURI(String prefix) {
if (prefix == null || prefix.equals(XMLConstants.DEFAULT_NS_PREFIX))
{
return prefix2Uri.get(DEFAULT_NS);
} else {
return prefix2Uri.get(prefix);
}
}
/**
* This method is not needed in this context, but can be implemented in a
* similar way.
*/
public String getPrefix(String namespaceURI) {
return uri2Prefix.get(namespaceURI);
}
public Iterator getPrefixes(String namespaceURI) {
// Not implemented
return null;
}
}
Xin lưu ý rằng có kết quả bắt lỗi trong đoạn mã. Các thuộc tính của mỗi nút được
kiểm tra và lưu trữ. Các nút con không được kiểm tra, bởi vì biến logic
toplevelOnly trong phương thức khởi tạo được đặt là true. Nếu biến logic được đặt
là false, sự xem xét các nút con sẽ được bắt đầu sau khi các thuộc tính được lưu
trữ. Một điều về đoạn mã cần cân nhắc: Trong DOM, nút đầu tiên sẽ biểu diễn tài
liệu như là toàn bộ, vì vậy, để lấy các thành phần book để đọc các không gian tên,
bạn phải đi đến nút con một cách chính xác.
Trong trường hợp này, sử dụng NamespaceContext thì đơn giản hơn:
Ví dụ 11. Ví dụ 3 với sự phân giải không gian tên được lưu trữ tạm thời (chỉ
cấp độ cao nhất)
private static void example3(Document example)
throws XPathExpressionException, TransformerException {
sysout("\n*** Third example - namespaces of toplevel node cached ***");
XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new UniversalNamespaceCache(example,
true));
try {
NodeList result1 = (NodeList) xPath.evaluate(
"books:booklist/science:book", example,
XPathConstants.NODESET);
} catch (XPathExpressionException e) {
}
NodeList result2 = (NodeList) xPath.evaluate(
"books:booklist/fiction:book", example, XPathConstants.NODESET);
String result = xPath.evaluate(
"books:booklist/fiction:book[1]/:author", example);
}
Kết quả như sau:
Ví dụ 12. Kết quả từ ví dụ 3
*** Third example - namespaces of toplevel node cached ***
The list of the cached namespaces:
prefix DEFAULT: uri http://univNaSpResolver/book
prefix fiction: uri http://univNaSpResolver/fictionbook
prefix books: uri http://univNaSpResolver/booklist
Try to use the science prefix:
> books:booklist/science:book
The cache only knows namespaces of the first level!
The fiction namespace is such a namespace:
> books:booklist/fiction:book
Number of Nodes: 2
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust I</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
<?xml version="1.0" encoding="UTF-8"?>
<fiction:book xmlns:fiction="http://univNaSpResolver/fictionbook">
<title xmlns="http://univNaSpResolver/book">Faust II</title>
<author xmlns="http://univNaSpResolver/book">Johann Wolfgang von
Goethe</author>
</fiction:book>
The default namespace works also:
> books:booklist/fiction:book[1]/:author
Johann Wolfgang von Goethe
Đoạn mã này chỉ tìm thấy các không gian tên của thành phần gốc. Để cho chính
xác: các không gian tên của nút được truyền vào phương thức examineNode bởi
phương thức khởi tạo. Điều này làm tăng tốc phương thức khởi tạo bởi vì nó
không phải lặp qua toàn bộ tài liệu. Tuy nhiên, như bạn có thể thấy từ kết quả, tiền
tố science không thể được phân giải. Biểu thức XPath trả lại một ngoại lệ
(XPathExpressionException).
Đọc các không gian tên từ tài liệu và tất cả các thành phần của nó và lưu trong
vùng nhớ đệm
Phiên bản này đọc toàn bộ các mô tả không gian tên từ tệp XML. Bây giờ thậm
chí XPath trên tiền tố science cũng hoạt động. Một tình huống làm cho phiên bản
này trở lên phức tạp: Nếu một tiền tố bị quá tải (được mô tả trong các thành phần
lồng nhau trong các URI khác nhau), tiền tố nào tìm thấy cuối cùng sẽ chiến thắng.
Trong thực tế, điều này ít khi xảy ra.
Sử dụng NamespaceContext trong ví dụ này cũng giống như trong các ví dụ trước.
Biến logic toplevelOnly trong phương thức khởi tạo phải được đặt là false.
Ví dụ 13. Ví dụ 4 với sự phân giải không gian tên được lưu trữ trong vùng
đêm (tất cả các cấp độ)
private static void example4(Document example)
throws XPathExpressionException, TransformerException {
sysout("\n*** Fourth example - namespaces all levels cached ***");
XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new UniversalNamespaceCache(example,
false));
NodeList result1 = (NodeList) xPath.evaluate(
"books:booklist/science:book", example, XPathConstants.NODESET);
NodeList result2 = (NodeList) xPath.evaluate(
"books:booklist/fiction:book", example, XPathConstants.NODESET);
String result = xPath.evaluate(
"books:booklist/fiction:book[1]/:author", example);