今さらながら、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?
http://xstream.codehaus.org/faq.html#Uses
No. For this kind of work a data binding tool such as XMLBeans is appropriate.
なので、クラスは自分の手で作ることにした。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
http://xstream.codehaus.org/objectstream.html
When there are no more objects left to read in the stream, ObjectInputStream.readObject() (or primitive equivalent) will throw java.io.EOFException.
で、最終的には、次のような実装になった。
/** * テスト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) { ; // 何もしない。 } } }
本当にこれでよいのか(もっとうまいやり方があるのか)、メモリをどの程度使うのかなど、まだまだ十分にわかったとは言えないが、、とりあえず動く。