倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

SAX API で org.dom4j.Document を構築するサンプル

前回までで紹介した SAX API を使用するサンプルとして、SAX 解析によって org.dom4j.Document のインスタンスを構築するクラスを作成してみます*1一覧)。

クラス定義


作成するクラスを SimpleSAXContentHandler とします。 SAXParser を用いて解析を行うようにするので、org.xml.sax.helpers.DefaultHandler クラスを継承する必要があります。 また、コメントや CDATA セクションも扱うため、org.xml.sax.ext.LexicalHandler インターフェースも実装します*2

public class SimpleSAXContentHandler extends DefaultHandler implements LexicalHandler {
...
}

フィールドやコンストラクタなど


次に、フィールドやコンストラクタなどを作成します:

public class SimpleSAXContentHandler extends DefaultHandler implements LexicalHandler {

    private Document doc;          // SAX 解析後に作成される org.dom4j.Document オブジェクト
    private Branch current;          // SAX イベントから作成した要素やテキストを付加するノード
    private DocumentFactory factory;     // dom4j の各種ノードを生成するファクトリ・クラス
    private List<Namespace> nsList;     // 名前空間宣言をストックしておくリスト
    private boolean inCDATA;          // (テキストの)解析位置が CDATA セクション内かどうかを示すフラグ
    
    /** デフォルトの DocumentFactory オブジェクトを用いるコンストラクタ */
    public SimpleSAXContentHandler(){
        this(new DocumentFactory());
    }
    
    /** DocumentFactory オブジェクトを指定したコンストラクタ */
    public SimpleSAXContentHandler(final DocumentFactory factory){
        this.factory = factory;
        this.nsList = new LinkedList<Namespace>();
    }
    
    /** SAX 解析後にこのメソッドを呼び出して org.dom4j.Document オブジェクトを取得する */
    public Document getDocument(){
        return this.doc;
    }
   ...
}

各種のノードを付加する current フィールドは org.dom4j.Element もしくは org.dom4j.Document オブジェクトなので、それらが実装している共通のインターフェース org.dom4j.Branch として宣言します。 また、フィールド nsList, inCDATA はそれぞれ要素とテキストの箇所でまた出てきます。

ドキュメント : startDocument()


startDocument() で各フィールドの初期化を行います。 コンストラクタで初期化を行った場合、1つの SimpleSAXContentHandler オブジェクトで1度しか解析が行えなくなるので注意してください。

    @Override
    public void startDocument(){
        this.doc = this.factory.createDocument();
        this.current = this.doc;
        this.nsList.clear();
        this.inCDATA = false;
    }

コメント・処理命令 : comment(), processingInstruction(..)


まず、実装が簡単なコメントと処理命令の処理をしておきましょう:

    @Override
    public void comment(char[] ch, int start, int length){
        final String text = String.valueOf(ch, start, length);
        this.current.add(this.factory.createComment(text));
    }

    @Override
    public void processingInstruction(String target, String data){
        this.current.add(this.factory.createProcessingInstruction(target, data));
    }

テキスト・CDATA セクション : characters(..), startCDATA(), endCDATA()


次はテキストと CDATA セクションです。 LexicalHandler の説明の記事でも書きましたが、テキストも CDATA セクションも内容の文字列は characters() メソッドに渡されます。 したがって、characters() メソッド内でどちらかを判断するためにフィールド inCDATA を定義し、startCDATA(), endCDATA() 内でこのフラグを切り替えるようにします:

    @Override
    public void startCDATA(){
        this.inCDATA = true;
    }

    @Override
    public void endCDATA(){
        this.inCDATA = false;
    }

    @Override
    public void characters(char[] ch, int start, int length){
        
        final String text = String.valueOf(ch, start, length);
        
        if(this.inCDATA)
            this.current.add(this.factory.createCDATA(text));
        else
            this.current.add(this.factory.createText(text));
    }

名前空間宣言 : startPrefixMapping(..)


名前空間宣言があった場合に呼び出される startPrefixMapping() メソッドは startElement() よりも前に呼び出されるため、直接名前空間ノードを作成して要素に付加することができません。 したがって、名前空間ノードをストックしておくフィールド nsList を作成しておきます。 そして、次に要素が現れたときにそれらの名前空間ノードを付加します:

    @Override
    public void startPrefixMapping(String prefix, String uri){
        this.nsList.add(this.factory.createNamespace(prefix, uri));
    }

    /** startElement() 内から呼び出す */
    private void addNamespaces(Element e){
        for(Namespace ns: nsList)
            e.add(ns);
        
        this.nsList.clear();
    }

要素 : startElement(..), endElement(..)


最後は要素が現れたときに呼び出される startElement(), endElement() メソッドです。 current フィールドに対する処理はネスト構造をしているデータではよく行う処理かと思います。 属性をひとまとめにしたクラス Attributes は、Java のコレクション・フレームワークをサポートしていませんが、特に分かり難いメソッドなどはないので説明は不要でしょう:

    @Override
    public void startElement(String uri, String localName, String name, Attributes attributes) {
        
        Element newE = this.factory.createElement(name, uri);
        
        addNamespaces(newE);
        addAttributes(newE, attributes);
        
        this.current.add(newE);
        this.current = newE;
    }

    private void addAttributes(Element e, Attributes atts){
        for(int i = 0, n = atts.getLength(); i < n; i++)
            e.addAttribute(atts.getQName(i), atts.getValue(i));
    }

    @Override
    public void endElement(String uri, String localName, String name){
        this.current = this.current.getParent();
    }

以上で SimpleSAXContentHandler の実装は終わり。

SimpleSAXContentHandler の使用方法


SimpleSAXContentHandler を実際に使うには、以下のようにします("test.xml" というファイルを読み込んでいます。):

public class SAXContentHandlerTest extends TestCase {

    public void test()throws Exception{
        
        SimpleSAXContentHandler sch = new SimpleSAXContentHandler();
        SAXParser parser = getSAXParser();
        parser.setProperty("http://xml.org/sax/properties/lexical-handler", sch);
        parser.parse("test.xml", sch);
        
        Document doc = sch.getDocument();
        System.out.println(doc.asXML());    // org.dom4j.Document オブジェクトをテキストとして出力
    }
    
    private SAXParser getSAXParser()throws Exception{

        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        SAXParser parser = factory.newSAXParser();
        return parser;
    }
}

*1:この機能を持ったクラスは org.dom4j.io.SAXContentHandler として既に作成されています。

*2:代わりに org.xml.sax.ext.DefaultHandler2 クラスを継承しても構いません。