いままで日本語のAPIドキュメントがあるという理由でjavax.xml.xpath.XPathを使っていたのだけれども、隣の芝生であるところのorg.apache.xpath.XPathAPIが異様に青く見えて仕方ないので違いを調べてみた。
一番の違いは、前者はファクトリクラスで生成したオブジェクトを使ってXPath式を評価するのに対し、後者はスタティックメソッドを使ってXPath式を評価する点だと思った。あとはメソッドに渡す引数の型が java.lang.Object
か org.w3c.dom.Node
かの違いとか、返ってくるのが java.lang.Object
か org.w3c.dom.NodeList
かの違いとかそんなところ。ちょっと細かく見ていってみよう。
こういうXMLを処理させたい。
<?xml version='1.0' encoding='UTF-8' ?>
<diary>
<title>へんな日記</title>
<entry date="2007-05-04T20:46:02+09:00">
<title>朝起きて夜寝た</title>
<p>朝起きたけど、ただの人間には興味がないので夜に寝た。</p>
</entry>
<entry date="2007-05-04T20:47:39+09:00">
<title>朝食べて夜食べた</title>
<p>朝食べたけど、まだその域に達していないので夜にも食べた。</p>
</entry>
</diary>
どちらのライブラリを使うにせよ、まずXMLをパースしてDOMにする。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(xml);
Element root = doc.getDocumentElement();
javax.xml.xpath.XPath
の場合はファクトリクラスを使ってXPathオブジェクトを作る。
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
org.apache.xpath.XPathAPI
は静的メソッドを呼ぶので事前準備はない。
単一ノードの値の取得
diary
要素直下のtitle
要素の値を取得してみる。
javax.xml.xpath.XPath
の場合はevaluate
メソッドを使う。
System.out.println(xpath.evaluate("/diary/title", root));
org.apache.xpath.XPathAPI
の場合はeval
メソッドを使う。
System.out.println(XPathAPI.eval(root, "/diary/title"));
複数ノードの繰り返し処理
各entry
要素について、その子要素のp
要素の値を取得してみる。
javax.xml.xpath.XPath
の場合はevaluate
メソッドを使う。
NodeList entries = (NodeList) xpath.evaluate("/diary/entry", root, XPathConstants.NODESET);
for(int i = 0; i < entries.getLength(); i++) System.out.println(xpath.evaluate("p", entries.item(i)));
キャストをしないといけないのが面倒。あと、evaluate
の第3引数で返却値の値を指定するくらいなら、最初から違う名前のメソッドにしておけば良いのではないか。それと、Xalanが吐き出すorg.w3c.dom.NodeList
オブジェクトはIterable
の実装ではないので、拡張Forループを使えない。Iterable
とNodeList
の両方を実装したオブジェクトを吐き出してくれるXMLパーサがあればとってもTiger。
各entry
要素ごとに他にも処理をするならともかく、単にp
要素だけを抜き出したいなら、以下のようにすればXPathの評価を1回だけにできる。
NodeList entries = (NodeList) xpath.evaluate("/diary/entry/p", root, XPathConstants.NODESET);
for(int i = 0; i < entries.getLength(); i++) System.out.println(entries.item(i).getFirstChild().getNodeValue());
org.apache.xpath.XPathAPI
の場合はselectNodeIterator
メソッドを使う。
NodeIterator it = XPathAPI.selectNodeIterator(root, "/diary/entry");
Node node;
while((node = it.nextNode())!=null) System.out.println(XPathAPI.eval(node, "p"));
selectNodeListメソッドを使っても良い。その場合はjavax.xml.xpath.XPathと大体同じ。
名前空間つきのXMLの場合
と、ここまでだと、org.apache.xpath.XPathAPI
の方が使いやすい感じがする。XPathのために新たにオブジェクトを生成する必要がないし、ほしいデータ型によって使うメソッドが違っている方がコードを読みやすい。しかし、例のXMLに名前空間を宣言すると、どうもうまくいかなくなるのだ。
以下のような名前空間付きのXMLを処理させることを考える。
<?xml version='1.0' encoding='UTF-8' ?>
<diary xmlns="http://txqz.net/ns/diary#" xmlns:xh="http://www.w3.org/1999/xhtml">
<title>へんな日記</title>
<entry date="2007-05-04T20:46:02+09:00">
<title>朝起きて夜寝た</title>
<xh:p>朝起きたけど、ただの人間には興味がないので夜に寝た。</xh:p>
</entry>
<entry date="2007-05-04T20:47:39+09:00">
<title>朝食べて夜食べた</title>
<xh:p>朝食べたけど、まだその域に達していないので夜にも食べた。</xh:p>
</entry>
</diary>
javax.xml.xpath.XPath
を使う場合は、setNamespaceContext
メソッドで名前空間を指定する。IBMの解説を参考にして、javax.xml.namespace.NamespaceContext
を実装する。
import java.util.Iterator;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
class NamespaceContextImpl implements NamespaceContext{
private Map<String,String> nsMap;
public NamespaceContextImpl(Map<String,String> nsMap){
this.nsMap = nsMap;
}
public String getNamespaceURI(String prefix) {
if(prefix == null) throw new NullPointerException("Null prefix");
if(nsMap.containsKey(prefix)) return nsMap.get(prefix);
return XMLConstants.NULL_NS_URI;
}
public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}
public Iterator getPrefixes(String namespaceURI) {
throw new UnsupportedOperationException();
}
}
で、XMLを読むときに;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
Element root = doc.getDocumentElement();
XPathFactory xFactory = XPathFactory.newInstance();
XPath xpath = xFactory.newXPath();
xpath.setNamespaceContext(getNamespaceContext(doc, "txqz"));
getNamespaceContext
メソッドの中身はこんな感じ。
private NamespaceContext getNamespaceContext(Document doc, String defaultPrefix){
Map<String,String> nsMap = new HashMap<String,String>();
Element root = doc.getDocumentElement();
NamedNodeMap attrs = root.getAttributes();
String xmlns = "xmlns";
for(int i = 0; i < attrs.getLength(); i++){
Node attr = attrs.item(i);
String[] name = attr.getNodeName().split(":");
if(xmlns.equals(name[0]))
nsMap.put(name.length == 1 ? defaultPrefix : name[1], attr.getNodeValue());
}
return new NamespaceContextImpl(nsMap);
}
さっきと同じことをするには下のように名前空間接頭辞をつければよい。
NodeList entries = (NodeList) xpath.evaluate("/txqz:diary/txqz:entry", root, XPathConstants.NODESET);
for(int i = 0; i < entries.getLength(); i++) System.out.println(xpath.evaluate("xh:p", entries.item(i)));
org.apache.xpath.XPathAPI
の名前空間付きXMLの処理には問題がある。ドキュメントを見ると、namespaceNodeをあらわすorg.w3c.dom.Node
オブジェクトを第3引数に取るメソッドは確かに用意されている。そこで以下のように指定してみたがうまくいかない。
Node namespace = XPathAPI.selectSingleNode(root, "//namespace::xh");
System.out.println(XPathAPI.eval(root, "//xh:p[1]", namespace));
xh接頭辞が名前空間に解決されていないよんと言われてしまう。
Exception in thread "main" javax.xml.transform.TransformerException: Prefix must resolve to a namespace: xh
at org.apache.xpath.compiler.XPathParser.error(XPathParser.java:602)
at org.apache.xpath.compiler.Lexer.mapNSTokens(Lexer.java:674)
at org.apache.xpath.compiler.Lexer.tokenize(Lexer.java:397)
at org.apache.xpath.compiler.Lexer.tokenize(Lexer.java:139)
at org.apache.xpath.compiler.XPathParser.initXPath(XPathParser.java:143)
at org.apache.xpath.XPath.<init>(XPath.java:217)
at org.apache.xpath.XPathAPI.eval(XPathAPI.java:279)
at net.txqz.xpathapi.XPathAPITest.main(XPathAPITest.java:56)
一応、述語を使えばやりたいことはできる。
System.out.println(XPathAPI.eval(root, "//*[local-name()='p' and namespace-uri()='http://www.w3.org/1999/xhtml'][1]"));
でも、いかにも長ったらしくてあんまり使いたくない。定数にしてしまう手もあるが、どうもねぇ。
困って検索してみたらXML & SOA にそれっぽい書き込みがあった。
namespaceとそのprefixを設定するには、
PrefixResolver
インターフェイスを継承して、getNamespaceForPrefix
メソッドを実装します。そして、それをXPathAPIに渡します。
ということでorg.xml.utils.PrefixResolver
を実装して以下のようなクラスを作った。
import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import org.apache.xml.utils.PrefixResolver;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
public class PrefixResolverImpl implements PrefixResolver {
private Map<String,String> nsMap;
public PrefixResolverImpl(Element root, String defaultPrefix){
nsMap = new HashMap<String,String>();
NamedNodeMap attrs = root.getAttributes();
String xmlns = "xmlns";
for(int i = 0; i < attrs.getLength(); i++){
Node attr = attrs.item(i);
String[] name = attr.getNodeName().split(":");
if(xmlns.equals(name[0]))
nsMap.put(name.length == 1 ? defaultPrefix : name[1], attr.getNodeValue());
}
}
public String getNamespaceForPrefix(String prefix) {
if(prefix == null) throw new NullPointerException("Null prefix");
if(nsMap.containsKey(prefix)) return nsMap.get(prefix);
return XMLConstants.NULL_NS_URI;
}
public String getNamespaceForPrefix(String arg0, Node arg1) {
throw new UnsupportedOperationException();
}
public String getBaseIdentifier() {
throw new UnsupportedOperationException();
}
}
元ソースはこんな感じ。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));
Element root = doc.getDocumentElement();
PrefixResolver resolver = new PrefixResolverImpl(root, "txqz");
System.out.println(XPathAPI.eval(root, "/txqz:diary/txqz:entry/xh:p[1]",resolver));
一応これで良いみたいだけど、面倒くさすぎだろう常識的に考えて……。ていうか何でPrefixResolverをインプリメントしたオブジェクトをevalとかの第3引数に入れれるの? ここに入るのはNodeじゃないの? 割り切れないものを感じつつ、とりあえずこれくらいで終わり。