前回までで紹介した 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; } }