じゃじゃ馬ならし

 

ストリーミングで XML - StAX

第 3 の刺客?

今まで、Java で XML を扱うのには 3 種類の方法がありました。

JAXB はちょっと毛色が違うので、単に XML をパースするならば DOM か SAX です。

ところが、Java SE 6 では新たにもう 1 つパーサが追加されました。

それが今回紹介する Streaming API for XML、通称 StAX です。StAX は JCP の JSR-173 で策定されており、BEA がスペックリードになっています。

さて、この 3 つのパーサの違いというのは何なのでしょう。よく説明されるのが、

というものです。

DOM がオブジェクトモデルというのはすぐに分かりますね。DOM ツリーとしてヒープに持つわけですから。

分からないのがプッシュとプルです。

push/pull

どうやら、プッシュというのは一方的にパーサがパースをしているアプリケーションを呼び出すようなイメージです。ようするにイベントですね。

SAX パーサは XML ドキュメントを頭から読んでいき、タグの開始や終了などにぶつかると、決められたアプリケーション側のコールバックルーチンをコールするわけです。

アプリケーション側は XML ドキュメントの読み込みの制御をおこなうことはできず、コールバックルーチンで口を開けて待っているだけです。

StAX は逆に XML ドキュメントの読み込みの制御をアプリケーション側がおこないます。そして、必要に応じて StAX パーサから情報を取得するような構成になります。

そのため、SAX がプッシュ、StAX がプルと呼ばれるのでしょう。

それにしても StAX のネーミングですが、明らかに SAX を意識していますね ^^;;

前置きが長くなってしまいました。プルモデルの StAX をどういうふうに使えばいいのか、見ていきましょう。

 

とりあえずパース - Cursor API 編

StAX には 2 種類の方法でパースをすることができます。一方が Cursor API で、もう一方が Event Iterator API です。

使い方の基本的な思想は同じなのですが、メソッドなどの抽象度が違っています。Event Iterator API の方が抽象度が高くなっています。

とはいっても、両方使いこなせた方がいいので、まずは Cursor API から。

サンプルのソースコード StAXSample1.java

StAX はその名の通りストリームを使ってパースをします。そのストリームを表すクラスが javax.xml.stream.XMLStreamReader クラスです。

XMLStreamReader オブジェクトを生成するには、javax.xml.script.XMLInputFactory クラスの createXMLStreamReader メソッドを使用します。

createXMLStreamReader メソッドは引数の異なるものが何種類かありますが、ここでは InputStream クラスを使用するものを使っています。

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamException;
 
public class StAXSample1 {
    public StAXSample1(String xmlfile) throws FileNotFoundException, XMLStreamException {
        XMLInputFactory factory = XMLInputFactory.newInstance();
 
        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(xmlfile));
        XMLStreamReader reader = factory.createXMLStreamReader(stream);
 
        for (; reader.hasNext(); reader.next()) {
            int eventType = reader.getEventType();
 
            if (eventType == XMLStreamConstants.START_ELEMENT) {
                System.out.println("Name: " + reader.getName());
            }
        }
 
        reader.close();
    }
 
    public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
        new StAXSample1(args[0]);
    }
}

XMLStreamReader オブジェクトが生成できたら、後は Iterator インタフェースと同じように使用します。

まず、hasNext メソッドでイベントがあるかどうかを調べます。

そして、イベントの種類を調べて、それに応じて必要な情報を取得します。

そして、next メソッドを使用して次のイベントに移動します。

このイテレーションループと XMLStreamReader オブジェクトから情報を取得するところが、プルの特徴ですね。

この例では hasNext メソッドをコールした後、イベントの種類が "タグの開始" であればタグの名前を出力するという処理をおこなっています。

このサンプルでこのドキュメント (つまり stax.html ですね) をパースしてみます。このドキュメントは XHTML で記述されているので、パースできるはずです。

C:\temp>java StAXSample1 stax.html
Name: {http://www.w3.org/1999/xhtml}html
Name: {http://www.w3.org/1999/xhtml}head
Name: {http://www.w3.org/1999/xhtml}title
Name: {http://www.w3.org/1999/xhtml}meta
Name: {http://www.w3.org/1999/xhtml}meta
Name: {http://www.w3.org/1999/xhtml}link
Name: {http://www.w3.org/1999/xhtml}body
Name: {http://www.w3.org/1999/xhtml}div
Name: {http://www.w3.org/1999/xhtml}p
Name: {http://www.w3.org/1999/xhtml}h1
Name: {http://www.w3.org/1999/xhtml}h2
Name: {http://www.w3.org/1999/xhtml}p
Name: {http://www.w3.org/1999/xhtml}ul
Name: {http://www.w3.org/1999/xhtml}li
          ......

長くなるので以下省略。

{ } で囲まれているのが名前空間で、その後にタグ名が表示されています。というのも getName メソッドの戻り値は QName オブジェクトだからですね。

もちろん、タグ名以外の情報も取得できます。

サンプルのソースコード StAXSample2.java

 

        for (; reader.hasNext(); reader.next()) {
            int eventType = reader.getEventType();
  
            if (eventType == XMLStreamConstants.START_ELEMENT) {
                System.out.println("Name: " + reader.getName());
                System.out.println("Local Name: " + reader.getLocalName());
                System.out.println("Prefix: " + reader.getPrefix());
  
                System.out.println("Attribute:");
                int count = reader.getAttributeCount();
                System.out.println("  Count: " + count);
                for (int i = 0; i < count; i++) {
                    System.out.println("  Name: " + reader.getAttributeName(i));
                    System.out.println("  Local Name: " + reader.getAttributeLocalName(i));
                    System.out.println("  Namespace: " + reader.getAttributeNamespace(i));
                    System.out.println("  Type: " + reader.getAttributeType(i));
                    System.out.println("  Value: " + reader.getAttributeValue(i));
                }
  
                System.out.println("Namespace:");
                System.out.println("  URI: " + reader.getNamespaceURI());
                count = reader.getNamespaceCount();
                System.out.println("  Count: " + count);
                for (int i = 0; i < count; i++) {
                    System.out.println("  Prefix: " + reader.getNamespacePrefix(i));
                    System.out.println("  URI: " + reader.getNamespaceURI(i));
                }
  
                System.out.println();
            }
        }

長いので、for 文の中だけ示します。

これだけとれれば、たいていのことはとれます。

実行してみると、次のようになりました。

C:\temp>java StAXSample2 stax.html
Name: {http://www.w3.org/1999/xhtml}html
Local Name: html
Prefix:
Attribute:
  Count: 0
Namespace:
  URI: http://www.w3.org/1999/xhtml
  Count: 1
  Prefix: null
  URI: http://www.w3.org/1999/xhtml

Name: {http://www.w3.org/1999/xhtml}head
Local Name: head
Prefix:
Attribute:
  Count: 0
Namespace:
  URI: http://www.w3.org/1999/xhtml
  Count: 0

Name: {http://www.w3.org/1999/xhtml}title
Local Name: title
Prefix:
Attribute:
  Count: 0
Namespace:
  URI: http://www.w3.org/1999/xhtml
  Count: 0

Name: {http://www.w3.org/1999/xhtml}meta
Local Name: meta
Prefix:
Attribute:
  Count: 2
  Name: http-equiv
  Local Name: http-equiv
  Namespace: null
  Type: CDATA
  Value: Content-Type
  Name: content
  Local Name: content
  Namespace: null
  Type: CDATA
  Value: text/html; charset=shift_jis
Namespace:
  URI: http://www.w3.org/1999/xhtml
  Count: 0

Name: {http://www.w3.org/1999/xhtml}meta
Local Name: meta
Prefix:
Attribute:
  Count: 2
  Name: name
  Local Name: name
  Namespace: null
  Type: CDATA
  Value: keywords
  Name: content
  Local Name: content
  Namespace: null
  Type: CDATA
  Value: Java SE 6, Java SE 6, StAX, Streaming API for XML
Namespace:
  URI: http://www.w3.org/1999/xhtml
  Count: 0
          ......

長くなるので以下省略。

さて、ここではイベントとして START_ELEMENT を使用しましたが、その他にも次のようなイベントがあります。

いずれも XMLStreamConstants クラスで定義されています。

そして、ちょっとやっかいなのが、それぞれのイベントごとに XMLStreamReader クラスのメソッドが決まっているということです。詳しくは XMLStreamReader クラスの Javadoc をご覧ください。

for 文を自分で制御できるということは、すなわちパースをコントロールできるということです。つまり、必要な情報がそろったら途中でパースをやめることも簡単だし、特定のタグをスキップするのも簡単に書けます。

でも、このようなスタイルだと、ちゃんとパースをしようとするとどうしても巨大な if/swintch 文が登場してしまいがちです。これはちょっとやですね。

これがイヤな人は次の EventIterator API を使いましょう。

 

とりあえずパース - Event Iterator API 編

さて、もう一方の Event Iterator API です。

Event Iterator API も Cursor API も基本は同じです。何が違うかというと、イベントがオブジェクト化される点です。

まずは StAXSample1 クラスと同じことを Event Iterator API を使って書いてみましょう。

サンプルのソースコード StAXSample3.java

 

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
 
public class StAXSample3 {
    public StAXSample3(String xmlfile) throws FileNotFoundException, XMLStreamException {
        XMLInputFactory factory = XMLInputFactory.newInstance();
 
        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(xmlfile));
        XMLEventReader reader = factory.createXMLEventReader(stream);
 
        while (reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
 
            if (event.isStartElement()) {
                StartElement element = (StartElement)event;
                System.out.println("Name: " + element.getName());
             }
        }
 
        reader.close();
    }
 
    public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
        new StAXSample3(args[0]);
    }
}

Cursor API では XMLStreamReader クラスを使用していましたが、Event Iterator API では XMLEventReader クラスを使用します。

生成方法は XMLStreamReader クラスとほぼ同じですね。

XMLEventReader オブジェクトが生成できたら、ループです。先ほどと違うのは、イベントが XMLEvent クラスとして現されている点です。

XMLEvent クラスには isStartElement メソッドなどのイベントの種類を調べるためのメソッドがあるので、それを使ってどのイベントか調べることができます。

実際にはイベントはイベント種類によって XMLEvent インタフェースの派生インタフェースである StartElement インタフェースなどを使用して表されます。

しかし、上記のコードだと if 文が残ったままで、あまり Cursor API と違いはありません。そこで、これをつぎのようにしてみました。

サンプルのソースコード StAXSample4.java

 

public class StAXSample4 {
    private Map<Integer, EventHandler> handlers;
 
    public StAXSample4(String xmlfile) throws FileNotFoundException, XMLStreamException {
        handlers = initHandlers();
 
        XMLInputFactory factory = XMLInputFactory.newInstance();
 
        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(xmlfile));
        XMLEventReader reader = factory.createXMLEventReader(stream);
 
        while (reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
 
            EventHandler handler = handlers.get(event.getEventType());
            handler.handleEvent(event);
        }
 
        reader.close();
    }
 
    private Map<Integer, EventHandler> initHandlers() {
        Map<Integer, EventHandler> handlers
            = new HashMap<Integer, EventHandler>();
 
        handlers.put(XMLEvent.START_ELEMENT, new StartElementHandler());
        handlers.put(XMLEvent.END_ELEMENT, new EndElementHandler());
        handlers.put(XMLEvent.ATTRIBUTE, new GeneralElementHandler());
        handlers.put(XMLEvent.CDATA, new GeneralElementHandler());
        handlers.put(XMLEvent.CHARACTERS, new GeneralElementHandler());
        handlers.put(XMLEvent.COMMENT, new GeneralElementHandler());
        handlers.put(XMLEvent.DTD, new GeneralElementHandler());
        handlers.put(XMLEvent.END_DOCUMENT, new GeneralElementHandler());
        handlers.put(XMLEvent.ENTITY_DECLARATION, new GeneralElementHandler());
        handlers.put(XMLEvent.ENTITY_REFERENCE, new GeneralElementHandler());
        handlers.put(XMLEvent.NAMESPACE, new GeneralElementHandler());
        handlers.put(XMLEvent.NOTATION_DECLARATION, new GeneralElementHandler());
        handlers.put(XMLEvent.PROCESSING_INSTRUCTION, new GeneralElementHandler());
        handlers.put(XMLEvent.SPACE, new GeneralElementHandler());
        handlers.put(XMLEvent.START_DOCUMENT, new GeneralElementHandler());
 
        return handlers;
    }
 
    public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
        new StAXSample4(args[0]);
    }
}
 
interface EventHandler {
    public void handleEvent(XMLEvent element);
}
 
class StartElementHandler implements EventHandler {
    public void handleEvent(XMLEvent event) {
        StartElement element = (StartElement)event;
        System.out.println("StartElement: " + element.getName().getLocalPart());
    }
}
 
class EndElementHandler implements EventHandler {
    public void handleEvent(XMLEvent event) {
        EndElement element = (EndElement)event;
        System.out.println("EndElement: " + element.getName().getLocalPart());
    }
}
 
class GeneralElementHandler implements EventHandler {
    public void handleEvent(XMLEvent event) {}
}

このクラスの肝はイベントを扱うための EventHandler インタフェースを定義して、イベント種類ごとに処理をおこなうクラスを分けたところです。それぞれのオブジェクトをマップにし、イベント種類によってどのクラスの handleEvent メソッドをコールするか決めることができます。

イベントハンドラでのキャストがちょっと気になるのですが、if 文は消すことができました。

こういうことができるのも、イベントをオブジェクトとして扱うことができるからです。

しかし、問題もあります。

ハンドラを格納しておく Map オブジェクトには、処理をするイベントだけでなく他のイベントに関してもハンドラを対応付けておかなくてはいけないということです。何もしないのに、コードだけは長くなって、保守性が悪くなってしまいます。

そういう用途のためにイベントのフィルターが StAX には用意されています。。

サンプルのソースコード StAXSample5.java

 

public class StAXSample5 {
    private Map<Integer, EventHandler> handlers;
 
    public StAXSample5(String xmlfile) throws FileNotFoundException, XMLStreamException {
        handlers = initHandlers();
 
        XMLInputFactory factory = XMLInputFactory.newInstance();
 
        BufferedInputStream stream = new BufferedInputStream(new FileInputStream(xmlfile));
        XMLEventReader reader = factory.createXMLEventReader(stream);
 
        EventFilter filter = new EventFilter() {
                public boolean accept(XMLEvent event) {
                    return event.isStartElement() || event.isEndElement();
                }
            };
        reader = factory.createFilteredReader(reader, filter);
 
        while (reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
 
            EventHandler handler = handlers.get(event.getEventType());
            handler.handleEvent(event);
        }
 
        reader.close();
    }
 
    private Map<Integer, EventHandler> initHandlers() {
        Map<Integer, EventHandler> handlers
            = new HashMap<Integer, EventHandler>();
 
        handlers.put(XMLEvent.START_ELEMENT, new StartElementHandler());
        handlers.put(XMLEvent.END_ELEMENT, new EndElementHandler());

        return handlers;
    }
 
    public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
        new StAXSample5(args[0]);
    }
}
 
interface EventHandler {
    public void handleEvent(XMLEvent element);
}
 
class StartElementHandler implements EventHandler {
    public void handleEvent(XMLEvent event) {
        StartElement element = (StartElement)event;
        System.out.println("StartElement:"" + element.getName().getLocalPart());
    }
}
 
class EndElementHandler implements EventHandler {
    public void handleEvent(XMLEvent event) {
        EndElement element = (EndElement)event;
        System.out.println("EndElement: " + element.getName().getLocalPart());
    }
}

赤字のところがフィルターです。

フィルターには EventFilter インタフェースを使用します。EventFilter インタフェースは accept メソッドを定義しています。この accept メソッドで、パーすするイベントは true を返し、それ以外は false を返すようにします。

フィルターを作成したら、XMLInputFactory クラスの createFilteredReader メソッドを使用して、もう 1 度 XMLEventReader オブジェクトを作り直します。

これだけで、先ほどまであった長い Map オブジェクトの初期化コードがなくなりました。ただ、フィルターに if 文が入ってしまっているので、ちょっと気になるのですが。これはちょっと工夫すれば if 文を使わないで書けるはずなので、興味がある方は挑戦してみてください。

さて、StAX の 2 種類のパースを見てきたわけですが、SAX に比べて簡単だと思いませんか。

SAX を使っていて何がいやかというと、状態を保持しておかないといけないことです。StAX では State パターンや Strategy パターンを使うことで状態を保持せずにパースを進めていくことが可能です。

これだけでも、私は使いがいがあると感じているのですが、いかがですか?

でも、StAX はそれだけではないのです。

 

XML の作成 - Cursor API 編

なんと StAX では XML の作成もできてしまうのです。

これを使い出すと DOM なんか使っていられないですよ。

XML の作成も Cursor API と Event Iterator API に分かれています。まずは、Cursor API で XML を作ってみましょう。

サンプルのソースコード StAXSample6.java

 

import java.io.StringWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.XMLStreamException;
 
public class StAXSample6 {
    public StAXSample6() throws XMLStreamException {
        XMLOutputFactory factory = XMLOutputFactory.newInstance();
 
        StringWriter stringWriter = new StringWriter();
        XMLStreamWriter writer = factory.createXMLStreamWriter(stringWriter);
        System.out.println("XML: " + stringWriter);
 
        writer.writeStartDocument();
        System.out.println("XML: " + stringWriter);
 
        writer.writeDTD("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
                        + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
        System.out.println("XML: " + stringWriter);
 
        writer.writeStartElement("html");
        System.out.println("XML: " + stringWriter);
 
        writer.writeDefaultNamespace("http://www.w3.org/1999/xhtml");
        System.out.println("XML: " + stringWriter);
 
        writer.writeEndElement();
        System.out.println("XML: " + stringWriter);
 
        writer.writeEndDocument();
        System.out.println("XML: " + stringWriter);
 
        writer.close();
    }
 
    public static void main(String[] args) throws XMLStreamException {
        new StAXSample6();
    }
}

パースは XMLInputFactory クラスでしたが、XML の作成では XMLOutputFactory クラスを使用します。

そして、XMLOutputFactory#createXMLStreamWriter メソッドを使用して XMLStreamWriter オブジェクトを生成します。

この XMLStreamWriter クラスが XML の生成をおこないます。

XMLStreamWriter クラスには writeStartDocument メソッドや writeStartElement メソッドといったメソッドが定義されているので、これらを順にコールすることで XML を作成します。

実行したらどうなるでしょうか。

C:\temp>java StAXSample6 stax.html
XML:
XML: <?xml version=":1.0": ?>
XML: <?xml version=":1.0": ?><!DOCTYPE html PUBLIC ":-//W3C//DTD XHTML 1.0 Transiti
onal//EN": ":http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd":>
XML: <?xml version=":1.0": ?><!DOCTYPE html PUBLIC ":-//W3C//DTD XHTML 1.0 Transiti
onal//EN": ":http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd":><html
XML: <?xml version=":1.0": ?><!DOCTYPE html PUBLIC ":-//W3C//DTD XHTML 1.0 Transiti
onal//EN": ":http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd":><html xmlns=
":http://www.w3.org/1999/xhtml":
XML: <?xml version=":1.0": ?><!DOCTYPE html PUBLIC ":-//W3C//DTD XHTML 1.0 Transiti
onal//EN": ":http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd":><html xmlns=
":http://www.w3.org/1999/xhtml":></html>
XML: <?xml version=":1.0": ?><!DOCTYPE html PUBLIC ":-//W3C//DTD XHTML 1.0 Transiti
onal//EN": ":http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd":><html xmlns=
":http://www.w3.org/1999/xhtml":></html>

XML ドキュメントが徐々にできあがっていく様子がお分かりでしょうか。複数行に分かれているのはコマンドプロンプトの表示のためで、実際には 1 行に書かれているようです。

StAX では XMLStreamWriter クラスを使用して、どんどん XML を書き出していきます。つまり、DOM のようにメモリに抱えこまないわけです。ということは、使用メモリも低く抑えられるという利点がありますね。

 

XML の作成 - Event Iterator API 編

次は Event Iterator API を使用して、XML を作成してみます。

パースの時と同じように Event Iterator API では、イベントをオブジェクトとして扱います。

StAXSample6 クラスと同じ XML を生成するサンプルを作って、両者を比較してみましょう。

サンプルのソースコード StAXSample7.java

 

    public StAXSample7() throws XMLStreamException {
        XMLOutputFactory factory = XMLOutputFactory.newInstance();
 
        StringWriter stringWriter = new StringWriter();
        XMLEventWriter writer = factory.createXMLEventWriter(stringWriter);
        XMLEventFactory eventFactory = XMLEventFactory.newInstance();
 
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createStartDocument());
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createDTD("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
                                          + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"));
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createStartElement("", "http://www.w3.org/1999/xhtml", "html"));
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createNamespace("http://www.w3.org/1999/xhtml"));
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createEndElement("", "http://www.w3.org/1999/xhtml", "html"));
        System.out.println("XML: " + stringWriter);
 
        writer.add(eventFactory.createEndDocument());
        System.out.println("XML: " + stringWriter);
 
        writer.close();
    }

だいたいお分かりだと思いますが、出力をおこなうのは XMLEventWriter クラスです。

XMLEventWriter クラスには add メソッドが定義されており、XMLEvent オブジェクトを引数にとります。

ここで、イベントオブジェクトを渡すわけですが、イベントを毎回作るのも面倒です。そんなときのために、XMLEventFactory クラスを使うことができます。このクラスには createStartDocument メソッドや createStartElement メソッドのように、作成するイベントに対応したメソッドが定義してあります。

これらのメソッドを使用してイベントを作成し、XMLEventWriter#add の引数にするわけです。

 

おまけ

実をいうと StAX はすでに JWSDP に含まれています。

ですから、JWSDP のドキュメントなどでその名前は見たことがある人が結構いるのではないでしょうか。

でも、使われていたかどうかとなるとかなり疑問符がつきます。メディアで取り上げられることも少ないし、Web で検索してもあまり引っかかりません。

でも、それはあまりにももったいない。

巨大な XML ドキュメントを扱うときとか、XML ドキュメントの一部しか使わないとか、XML ドキュメント作成したいとか、いろいろな用途で StAX は有効です。

ぜひ、この機会を逃さずに StAX を使われてみたらいかがでしょうか。

 

参考

JSR-173 Streaming API for XML

Paul R. Brown, "StAX のベールを剥ぐ", JavaPress 2004.5

 

(Nov. 2005)