ちょっとXMLを扱うのに、XPathを使った方がよさそうな場面に出くわしておりまして。
で、JavaでXPathを扱う場合に、どれを使って実現しようかなと思うわけですが、今回はJDOM 2を利用することにしました。
XPath自体については、検索して出てくるような情報を参考に。
http://qiita.com/merrill/items/aa612e6e865c1701f43b
では、JDOM 2を使ってXPathを扱ってみたいと思います。
お題
以下のXMLから、適当にXPathで要素やテキスト要素を抽出するものとします。
src/test/resources/data1.xml
<?xml version="1.0" encoding="UTF-8"?> <root> <data> <array> <value>foo</value> <value>bar</value> </array> <string> <value>Hello World</value> </string> <array> <value>fuga</value> </array> <integer> <value>100</value> </integer> <array> <value>data1</value> <value>data2</value> <value>data3</value> </array> <integer> <value>200</value> </integer> </data> </root>
ちょっとお題がある種、作為的なのですが…。
準備
<dependency> <groupId>org.jdom</groupId> <artifactId>jdom</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.1.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.4.1</version> <scope>test</scope> </dependency>
JDOM 2でXPathを扱うためには、jaxenが必要です。
また、JUnitとAssertJはテスト用です。
テストコード
では、テストコードで確認してみます。全体の雛形としては、こんな感じで。
src/test/java/org/littlewings/jdom/JdomXpathTest.java
package org.littlewings.jdom; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.util.List; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.Text; import org.jdom2.filter.Filters; import org.jdom2.input.SAXBuilder; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class JdomXpathTest { protected Document loadXml(String path) { try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) { SAXBuilder saxBuilder = new SAXBuilder(); return saxBuilder.build(is); } catch (JDOMException e) { throw new RuntimeException(e); } catch (IOException e) { throw new UncheckedIOException(e); } } // ここに、テストコードを書く! }
先ほど用意したXMLファイルを、クラスパス上からロードしてJDOMのDocumentにするメソッド付き。
@Test public void gettingStarted() { Document document = loadXml("data1.xml"); XPathFactory xPathFactory = XPathFactory.instance(); XPathExpression<Object> stringElementExpression = xPathFactory.compile("/root/data/string"); List<Object> stringElements = stringElementExpression.evaluate(document); assertThat(stringElements).hasSize(1); assertThat(((Element) stringElements.get(0)).getChild("value").getText()) .isEqualTo("Hello World"); XPathExpression<Object> arrayElementExpression = xPathFactory.compile("/root/data/array"); List<Object> arrayElements = arrayElementExpression.evaluate(document); assertThat(arrayElements).hasSize(3); assertThat(((Element) arrayElements.get(0)).getChildren().get(0).getText()) .isEqualTo("foo"); XPathExpression<Object> textExpression = xPathFactory.compile("/root/data/integer/value/text()"); List<Object> textElements = textExpression.evaluate(document); assertThat(textElements).hasSize(2); assertThat(((Text) textElements.get(0)).getText()).isEqualTo("100"); XPathExpression<Object> equalsExpression = xPathFactory.compile("/root/data/integer/value[text() = '200']"); List<Object> integerElements = equalsExpression.evaluate(document); assertThat(integerElements).hasSize(1); assertThat(((Element) integerElements.get(0)).getText()).isEqualTo("200"); XPathExpression<Object> missingExpression = xPathFactory.compile("/root/data/long"); List<Object> missingElements = missingExpression.evaluate(document); assertThat(missingElements).isEmpty(); }
XPathの利用は、XPathFactoryのインスタンスを取得するところから始まります。
XPathFactory xPathFactory = XPathFactory.instance();
このXPathFactoryに対して、compileメソッドにXPathを与えてコンパイルすることで、XPathを表すXPathExpressionを取得することができます。
XPathExpression<Object> stringElementExpression = xPathFactory.compile("/root/data/string");
あとは、このXPathExpressionに対して、対象のDOMノードを渡すとXPathが評価されます。
List<Object> stringElements = stringElementExpression.evaluate(document); assertThat(stringElements).hasSize(1); assertThat(((Element) stringElements.get(0)).getChild("value").getText()) .isEqualTo("Hello World");
戻り値はListとなるので、適宜要素を取り出していく感じになります。
テキストノードを抜き出したり
XPathExpression<Object> textExpression = xPathFactory.compile("/root/data/integer/value/text()");
述語を付けるのもOKです。
XPathExpression<Object> equalsExpression = xPathFactory.compile("/root/data/integer/value[text() = '200']");
存在しない対象に対するXPath式を指定した場合は、空のListが返ります。
XPathExpression<Object> missingExpression = xPathFactory.compile("/root/data/long");
List<Object> missingElements = missingExpression.evaluate(document);
assertThat(missingElements).isEmpty();
Filterを使う
先ほどの例では、XPathExpression#evaluateの戻り値のListの中身がObjectのため、キャストが必要だったりして少し面倒でした。
ここで、Filterを使うとこの部分を軽減することができます。
XpathFactory#compileの第2引数に、Filterのインスタンスを与えます。汎用的なものは、Filtersで利用できるメソッドで定義されているようなので、こちらを利用するとよいでしょう。
@Test public void usingFilter() { Document document = loadXml("data1.xml"); XPathFactory xPathFactory = XPathFactory.instance(); XPathExpression<Element> stringElementExpression = xPathFactory.compile("/root/data/string", Filters.element()); List<Element> stringElements = stringElementExpression.evaluate(document); assertThat(stringElements.get(0).getChild("value").getText()) .isEqualTo("Hello World"); XPathExpression<Element> arrayElementExpression = xPathFactory.compile("/root/data/array", Filters.element()); List<Element> arrayElements = arrayElementExpression.evaluate(document); assertThat(arrayElements).hasSize(3); assertThat(arrayElements.get(0).getChildren().get(0).getText()) .isEqualTo("foo"); XPathExpression<Text> textExpression = xPathFactory.compile("/root/data/integer/value/text()", Filters.text()); List<Text> textElements = textExpression.evaluate(document); assertThat(textElements).hasSize(2); assertThat(textElements.get(0).getText()).isEqualTo("100"); XPathExpression<Element> equalsExpression = xPathFactory.compile("/root/data/integer/value[text() = '200']", Filters.element()); List<Element> integerElements = equalsExpression.evaluate(document); assertThat(integerElements).hasSize(1); assertThat(integerElements.get(0).getText()).isEqualTo("200"); XPathExpression<Element> missingExpression = xPathFactory.compile("/root/data/long", Filters.element()); List<Element> missingElements = missingExpression.evaluate(document); assertThat(missingElements).isEmpty(); }
例えば、Filters#elementで要素、Filters#textでテキストノード用のFilterが取得できます。
XPathExpressionや、XPathExpression#evaluateで戻ってくるListの型パラメーターがElementなどになるところがポイントです。
XPathExpression<Element> stringElementExpression =
xPathFactory.compile("/root/data/string", Filters.element());
List<Element> stringElements = stringElementExpression.evaluate(document);
相対パスで
先ほどから「/」始まりの絶対パスでXPathを書いていましたが、特定のノードからの相対パスも、もちろん可能です。
ここでは、「data」要素からたどってみます。
@Test public void relativeAndPredicate() { Document document = loadXml("data1.xml"); XPathFactory xPathFactory = XPathFactory.instance(); Element dataElement = document.getRootElement().getChild("data"); XPathExpression<Element> integerElementExpression = xPathFactory.compile("integer/value[text() = '200']", Filters.element()); List<Element> integerElements = integerElementExpression.evaluate(dataElement); assertThat(integerElements).hasSize(1); assertThat(integerElements.get(0).getText()) .isEqualTo("200"); XPathExpression<Element> arrayElementExpression = xPathFactory.compile("array[value/text() = 'data1']", Filters.element()); List<Element> arrayElements = arrayElementExpression.evaluate(dataElement); assertThat(arrayElements).hasSize(1); assertThat(arrayElements.get(0).getName()).isEqualTo("array"); assertThat(arrayElements.get(0).getChildren()).hasSize(3); }
少し、述語も変わったものをいれてみました。
簡単にですが、JDOM 2でXPathを扱ってみました。とりあえずは、こんなところでしょう。