XMLEventReader の時と同様、StAX 解析のサンプルとして XMLStreamReader を用いて SAX 解析を行うクラス StaxSAXParser を実装してみましょう。 ここでは、XML イベントとして扱うのは org.xml.sax.ContentHandler が扱うもの*1とします(org.xml.sax.ext.LexicalHandler が扱うイベントは無視します)。
StaxSAXParser の使用方法は XMLEventReader の時と大体同じです。
フィールド・コンストラクタなど
StaxSAXParser のフィールドは以下のようになります。
フィールド | 型 | 説明 |
---|---|---|
factory | XMLInputFactory | XMLStreamReader オブジェクトを生成するファクトリ・クラス |
contentHandler | org.xml.sax.ContentHandler | 処理を委譲するハンドラクラス |
nsQueue | java.util.Deque | 名前空間宣言を保持しておく LIFO スタック |
コンストラクタでは、これらのフィールドに(必要なら)初期化を行っています。
public class StaxSAXParser implements XMLStreamConstants{ private final XMLInputFactory factory; private ContentHandler contentHandler; private final Deque<List<Namespace>> nsQueue ; public StaxSAXParser(){ this.factory = XMLInputFactory.newInstance(); this.nsQueue = new LinkedList<List<Namespace>>(); } /** 接頭辞のマッピングを行う際に、名前空間宣言の情報を保持しておくためのクラス */ private static class Namespace{ final String prefix; final String uri; Namespace(String prefix, String uri){ this.prefix = prefix; this.uri = uri; } } }
parse() メソッド
parse() メソッドでは、各解析ごとに行う初期化と XMLStreamReader の生成を行った後、XML イベントの列挙を行っています。 後処理を finally 節で行っていることも注意。
public void parse(Source source, ContentHandler handler)throws SAXException, XMLStreamException{ this.contentHandler = handler; XMLStreamReader reader = null; try{ reader = this.factory.createXMLEventReader(source); // XML イベントの列挙 while(reader.hasNext()){ reader.next(); switch(event.getEventType()){ // XML イベントの種類によって処理を振り分ける } } }finally{ if(reader != null)reader.close(); this.contentHandler = null; this.nsQueue.clear(); } }
以下で、XML イベントの型によって分岐させている switch 文の下に、各イベント型に対する処理を case 節として付加していきます。
文書の開始・文書の終了
まずは、XML 文書の開始と終了時に ContentHandler の startDocument() と endDocument() メソッドをそれぞれ呼び出す必要があります。 付加する case 節は以下の通り:
case START_DOCUMENT: // this.contentHandler.setDocumentLocator(...); this.contentHandler.startDocument(); break; case END_DOCUMENT: this.contentHandler.endDocument(); break;
処理命令
次に、単純な処理の処理命令に対する処理を書きましょう。 case 節は次の通り:
case PROCESSING_INSTRUCTION: this.contentHandler.processingInstruction(reader.getPITarget(), reader.getPIData()); break;
テキスト・無視できる空白
次は Characters 関連の XML イベント。 通常のテキスト、CDATA セクション、無視できる空白の3種類があります。 テキスト、CDATA セクションは(LexicalHandler を扱わないので)同じ処理。 無視できる空白は XMLStreamConstant.SPACE によって判定。 付加する case 節は以下のようになります:
case CHARACTERS: case CDATA: char[] charArray = reader.getTextCharacters(); this.contentHandler.characters(charArray, 0, charArray.length); break; case SPACE: char[] charArray = reader.getTextCharacters(); this.contentHandler.ignorableWhitespace(charArray, 0, charArray.length); break;
要素の開始
要素の開始を処理する case 節は次の通り:
case START_ELEMENT: handleStartElement(XMLStreamReader reader); break;
要素の開始時に行う処理は幾つかあります。 行う処理は
- その要素に付加されている名前空間宣言を処理する
- javax.xml.stream.events.Attribute の Iterator から org.xml.sax.Attributes オブジェクトを生成する
- ContentHandler#startElement(..) を呼び出す
です。
private void handleStartElement(XMLStreamReader reader)throws SAXException{ // ContentHandler#startPrefixMapping() を呼び出すメソッド startPrefixMappings(reader); this.contentHandler.startElement( reader.getName().getNamespaceURI(), reader.getName().getLocalPart(), QNameUtils.toPrefixedName(reader.getName()), extractAttributes(reader)); // Attributes を生成するメソッド } /** ContentHandler#startPrefixMapping() を呼び出すメソッド */ private void startPrefixMappings(XMLStreamReader reader)throws SAXException{ List<Namespace> nsSet = new LinkedList<Namespace>(); for(int i = 0, n = reader.getNamespaceCount(); i < n; i++){ Namespace ns = new Namespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i)); this.contentHandler.startPrefixMapping(ns.prefix, ns.uri); nsSet.add(ns); } this.nsQueue.push(nsSet); } /** Attributes を生成するメソッド */ private Attributes extractAttributes(XMLStreamReader reader){ AttributesImpl atts = new AttributesImpl(); for(int i = 0, n = reader.getNamespaceCount(); i < n; i++){ atts.addAttribute( reader.getAttributeNamespace(i), reader.getAttributeLocalName(i), QNameUtils.toPrefixedName(reader.getAttributeName(i)), att.getAttributeType(i), att.getAttributeValue(i)); } return atts; }
toPrefixedName(QName) メソッドを定義している QNameUtils クラスは自作クラスです(こちらを参照)。 QNameUtils.toPrefixedName(QName) メソッドは、引数の QName オブジェクトから接頭辞付の名前("xsl:stylesheet" のような文字列)を返します。
要素の終了
要素の終了を処理する case 節は次の通り:
case END_ELEMENT: handleEndElement(XMLStreamReader reader); break;
要素の終了時に行う処理は、要素の開始に比べれば簡単です。 行う処理は
- ContentHandler#endElement(..) を呼び出す。
- スコープが終わる名前空間宣言に対して ContentHandler#endPrefixMapping() メソッドを呼び出す
です。
private void handleEndElement(XMLStreamReader reader)throws SAXException{ this.contentHandler.endElement( reader.getNamespaceURI(), reader.getName().getLocalPart(), QNameUtils.toPrefixedName(reader.getName())); endPrefixMappings(); } private void endPrefixMappings()throws SAXException{ List<Namespace> nsSet = this.nsQueue.pop(); for(Namespace ns: nsSet) this.contentHandler.endPrefixMapping(ns.prefix); }
*1:Locator のセット(ContentHandler#setDocumentLocator(Locator))は無視します。