はじめての“XStream”

今さらながら、XStreamを使ってみた。JavaにおけるXMLの操作は、何年も前にJAXBを使って以来だ。いつの間にやら、JAXBはJava SEにバンドルされ、StAXも加わっていた。ちょっと目を離すと、すぐにおいていかれる。ちなみにStAXについては、櫻庭氏の次の解説がわかりやすい。
http://www.javainthebox.net/laboratory/JavaSE6/stax/stax.html


さて、今回の目的は、次のようなXMLファイル*1を読み込んで、必要な情報を取得して解析したり、加工して別ファイルに出力したり、そういう処理をすることだ。

<box>
  <pack>
    <magazines>
      <magazine mcode="001" name="雑誌001" />
      <magazine mcode="002" name="雑誌002" />
    </magazines>
    <books>
      <book isbn="00001" name="書籍00001" />
      <book isbn="00002" name="書籍00002" />
      <book isbn="00003" name="書籍00003" />
      <book isbn="00004" name="書籍00004" />
      <book isbn="00005" name="書籍00005" />
      <!-- 途中略 -->
      <book isbn="99999" name="書籍99999" />
    </books>
  </pack>
</box>

book要素の数が多いので、DOMは使いたくない。どれだけメモリを使ってしまうかわかったものではない。SAXでもなんとかなるかと思ったが、パース時の制御が不十分になりそうなので、やっぱりストリームっぽく処理したい。で、著名(だと思っている)でお手軽っぽいXStreamを使おうと思ったわけだ。


以下では、まずXStreamを使う下準備をして、次に最もシンプルな使い方をためして、最後にストリームっぽく処理をしてみようと思う。

下準備(1) XStreamを知る

著名なわりには、あまりまとまった日本語の情報がない。。もしかして、著名ではない?それとも常識すぎ?とりあえず本家の情報とソースをざっくり見て、わかったことをメモ。
XStreamには、XMLを簡単に処理できるように作られた XStreamクラスがある。XStreamでは、これを「ファサード」と呼んでいる。このファサードでは、XMLのリーダとライタとして、HierarchicalStreamReader/HierarchicalStreamWriterインターフェースのインスタンスを用いている。このリーダとライタを、XStreamでは「ドライバ」と呼んでいる。デフォルトでは、ドライバの実装としてXPP3を用いる(DOMを用いることもできる)ため、xstream-version.jar と xpp3-version.jar をクラスパスに通す必要がある(ダウンロードページから“Binary distribution”をダウンロードすれば、XPP3のjarも同梱されている)。

ちなみに、XMLのライタ(またはXMLのアウトプットストリーム)から、Javaのオブジェクトをデシリアライズすることを、「アンマーシャル(アンマーシャリング)」と呼ぶ。XMLのリーダ(またはXMLのインプットストリーム)へ、Javaのオブジェクトをシリアライズすることを、「マーシャル(マーシャリング)」と呼ぶ。まぁつまり、次のように呼ぶってこと。


上記の他にも、クラスごとにマーシャル、アンマーシャルをフックして処理を追加できる「コンバータ」という仕組みも用意されており、きめ細やかな操作が可能であるが、今回は使っていない。

下準備(2) スキーマに対応するクラスを作成する

「JAXBのように、XMLスキーマからJavaのクラスを自動生成してくれるとよいな」と思っていたのだが、次にある通り、XMLスキーマからクラスを作成してくれるようなツールはないようだ*2

Can XStream generate classes from XSD?
No. For this kind of work a data binding tool such as XMLBeans is appropriate.

http://xstream.codehaus.org/faq.html#Uses

なので、クラスは自分の手で作ることにした。XMLの要素名や属性名とのマッピングは、いくつか方法があるのだが、アノテーションで指定することに。明示的なマッピングの指定なしでも動作するっぽいが、きちんと指定しておきたい人。ちなみに、アノテーションの使い方は、以下に書いてある(ソースを見ても、アノテーションの数は多くないので、書いてある以上のことはほとんどできないっぽい)。
http://xstream.codehaus.org/annotations-tutorial.html


今回は、XMLの要素に対応する、Box、Pack、Magazines、Magazine、Books、Bookクラスを作成した*3。で、アノテーション“@XStreamAlias”を用いて、XMLの要素名と、クラス名またはフィールド名とを紐づけた。ルートであるBoxクラスは、次のようにPackクラスを持つ。

@XStreamAlias("box")
public class Box {

    @XStreamAlias("pack")
    private Pack pack = null;

    /* 以下略 */
}

そのPackクラスは、MagazinesクラスとBooksクラスを持つ。

@XStreamAlias("pack")
public class Pack {

    @XStreamAlias("magazines")
    private Magazines magazines = null;

    @XStreamAlias("books")
    private Books books = null;

    /* 以下略 */
}

で、Booksクラスは、Bookクラスのリストを持つ。

@XStreamAlias("books")
public class Books {

    /* 子要素の名称は、次のように記述する。 */

    @XStreamImplicit(itemFieldName="book")
    private List<Book> bookList = null;

    /* 以下略 */
}

最後にBookクラスは、2つの属性情報と、XMLとは紐づかない情報を持つ。

@XStreamAlias("book")
public class Book {

    /* 属性は、次のように @XStreamAsAttribute を用いて記述する。 */

    @XStreamAlias("isbn")
    @XStreamAsAttribute
    private String isbn = null;

    @XStreamAlias("name")
    @XStreamAsAttribute
    private String name = null;

    /* XMLとマッピングしないフィールドは、次のように記述する。 */

    @XStreamOmitField
    private String description = null;

    /* 以下略 */
}

ちなみに、次のような属性と値を持つ要素の場合には、アノテーションではマッピングできないっぽい(コンバータを作成しないといけないっぽい)。これはなんか不便だ。“@XStreamAlias”アノテーションが、どうもしっくりこない。

    <book isbn="00001" name="書籍00001">あああ</book>

シンプルな使い方

XStreamを最も簡単には、XStream#fromXMLメソッド、XStream#toXMLメソッドを使えばよい。これだけだ。さすがファサードと豪語するだけある(このファサードのソースが、XStreamをどのように用いるかの指針となる)。
例えば、文字エンコーディングを指定して、XMLファイルを読み込む場合は、次のようなソースになる。

    static Box fromXml(File file, String charsetName) {
        // ファサードである XStream のインスタンスを生成する。
        XStream stream = new XStream();
        // アノテーションを有効にするためには、次のメソッドを実行する。
        stream.processAnnotations(Box.class);

        InputStreamReader reader = null;
        try {
            reader = new InputStreamReader(
                     new FileInputStream(file), charsetName);

            // XStream#fromXML メソッドを実行することで、XMLファイルから
            // Javaのオブジェクトを生成する(アンマーシャリング)。
            return (Box) stream.fromXML(reader);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (reader != null) reader.close();
            } catch (IOException e) {
                ;  // 何もしない。
            }
        }
    }

同様に、書き出す場合には、次のようになる。

    static void toXml(Box box, File file, String charsetName) {
        XStream stream = new XStream();
        stream.processAnnotations(Box.class);

        OutputStreamWriter writer = null;
        try {
            writer = new OutputStreamWriter(
                     new FileOutputStream(file), charsetName);
            stream.toXML(box, writer);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (writer != null) writer.close();
            } catch (IOException e) {
                ;  // 何もしない。
            }
        }
    }

確かに簡単だ。

オブジェクトストリームの使い方

で、いよいよストリームっぽく処理する実装に。簡単な例は、次にちょこっとだけ記載されている。
http://xstream.codehaus.org/objectstream.html


でもこの例だと、ルート直下にある要素を、順次読んでいる。冒頭のXMLファイルだと、pack要素がいっぺんに読まれてお終いだ。これでは意味がない。ソースを見ていると、ノードの移動ができるリーダ(HierarchicalStreamReader)を発見。コンバータの例でも、これをうまく使っている。なるほど、books要素まで降りていってあげればよいのね。ちなみに、インプットストリームの最後は、EOFExceptionによって判別するらしい。あまりうれしくはないが、まぁ仕方がなさそうだ。

Detecting the end of the ObjectInputStream
When there are no more objects left to read in the stream, ObjectInputStream.readObject() (or primitive equivalent) will throw java.io.EOFException.

http://xstream.codehaus.org/objectstream.html

で、最終的には、次のような実装になった。

    /**
     * テストXMLファイルを読み込み、順次、books 要素の内容を標準出力に出力する。
     * @param bookPackXml 雑誌と書籍の情報を記載したXMLファイル
     */
    static void printBooksToCsv(File bookPackXml) {
        // XStreamを用意する。
        // ノードの移動ができるリーダ(HierarchicalStreamReader)の参照を得るために、
        // あえて、ドライバ(XppDriver)の参照を取得しておく。
        XppDriver driver = new XppDriver();
        XStream stream = new XStream(driver);
        stream.processAnnotations(Box.class);

        HierarchicalStreamReader reader = null;
        ObjectInputStream in = null;
        try {
            reader = driver.createReader(new FileReader(bookPackXml));
            in = stream.createObjectInputStream(reader);

            reader.moveDown();  // 現在のノードを pack に移動
            in.readObject();    // magazines 要素を読みだす

            reader.moveDown();  // 現在のノードを books に移動
            try {
                for (;;) {
                    Book book = (Book) in.readObject();
                    System.out.println(String.format(
                            "%s: %s", book.getIsbn(), book.getName()));
                }
            } catch (EOFException e) {
                ;  // ファイルを読み終わった。
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }finally {
            // IOExceptionをスローしない。
            if (reader != null) reader.close();
            try {
                if (in != null) in.close();
            } catch (IOException e) {
                ;  // 何もしない。
            }
        }
    }

本当にこれでよいのか(もっとうまいやり方があるのか)、メモリをどの程度使うのかなど、まだまだ十分にわかったとは言えないが、、とりあえず動く。

*1:大きな段ボール箱に、本がたくさん入っていて、動かないように全体をビニールでラップしているイメージ。もちろん、実際に扱いたいXMLファイルは、こんな意味不明な内容ではないw

*2:むろん、逆もないようだ。JAXB2.0からは、両方向サポートされている。

*3:Magazines、MagazineはBooks、Bookと同様なので、ここへの記載を省略する。