CLOVER🍀

That was when it all began.

JDOM 2でXPathを使う

ちょっとXMLを扱うのに、XPathを使った方がよさそうな場面に出くわしておりまして。

で、JavaでXPathを扱う場合に、どれを使って実現しようかなと思うわけですが、今回はJDOM 2を利用することにしました。

JDOM

XPath

XPath自体については、検索して出てくるような情報を参考に。

XML Path Language - Wikipedia

XPath | TECHSCORE(テックスコア)

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>

ちょっとお題がある種、作為的なのですが…。

準備

JDOM 2+XPathを使うための、Maven依存関係。

        <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が必要です。

http://jaxen.org/

また、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を扱ってみました。とりあえずは、こんなところでしょう。