StAX 解析のサンプルとして、XMLEventReader を用いて SAX 解析を行うクラス StaxSAXParser を実装してみましょう。 ただし、XML イベントとして扱うのは org.xml.sax.ContentHandler が扱うもの*1とします(org.xml.sax.ext.LexicalHandler が扱うイベントは無視します)。
StaxSAXParser の使用方法は、javax.xml.parsers.SAXParser と大体同じようにします。 StaxSAXParser に XML 文書の入力とハンドラ・クラスを渡して解析を行う parse(javax.xml.transform.Source, org.xml.sax.ContentHandler) メソッドを定義します。
フィールド・コンストラクタなど
StaxSAXParser のフィールドは以下のようになります。
フィールド | 型 | 説明 |
---|---|---|
factory | XMLInputFactory | XMLEventReader オブジェクトを生成するファクトリ・クラス |
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>>(); } }
parse() メソッド
parse() メソッドでは、各解析ごとに行う初期化と XMLEventReader の生成を行った後、XML イベントの列挙を行っています。 後処理を finally 節で行っていることも注意。
public void parse(Source source, ContentHandler handler)throws SAXException, XMLStreamException{ this.contentHandler = handler; XMLEventReader reader = null; try{ reader = this.factory.createXMLEventReader(source); // XML イベントの列挙 while(reader.hasNext()) parse(reader.nextEvent()); }finally{ if(reader != null)reader.close(); this.contentHandler = null; this.nsQueue.clear(); } } /** 渡された XML イベントの種類によって処理を振り分けます。 */ private void parse(XMLEvent event)throws SAXException{ switch(event.getEventType()){ // XML イベントの種類によって処理を振り分ける } }
以下で、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: ProcessingInstruction pi = (ProcessingInstruction)event; this.contentHandler.processingInstruction(pi.getTarget(), pi.getData()); break;
テキスト・無視できる空白
次は Characters 関連の XML イベント。 通常のテキスト、CDATA セクション、無視できる空白の3種類があります。 テキストと CDATA セクションは(LexicalHandler を無視するので)同じ処理。 無視できる空白は XMLStreamConstant.SPACE によって判定することに注意。 付加する case 節は次のようになります:
case CHARACTERS: case CDATA: Characters chars = event.asCharacters(); char[] charArray = chars.getData().toCharArray(); this.contentHandler.characters(charArray, 0, charArray.length); break; case SPACE: Characters chars = event.asCharacters(); char[] charArray = chars.getData().toCharArray(); this.contentHandler.ignorableWhitespace(charArray, 0, charArray.length); break;
要素の開始
要素の開始を処理する case 節は次の通り:
case START_ELEMENT: handleStartElement(event.asStartElement()); break;
要素の開始時に行う処理は幾つかあります。 行う処理は
- その要素に付加されている名前空間宣言を処理する
- javax.xml.stream.events.Attribute の Iterator から org.xml.sax.Attributes オブジェクトを生成する
- ContentHandler#startElement(..) を呼び出す
です。
private void handleStartElement(StartElement start)throws SAXException{ // Namespace の Iterator から ContentHandler#startPrefixMapping() を呼び出すメソッド startPrefixMappings(start.getNamespaces()); this.contentHandler.startElement( start.getName().getNamespaceURI(), start.getName().getLocalPart(), QNameUtils.toPrefixedName(start.getName()), extractAttributes(start.getAttributes())); // Attribute の Iterator から Attributes を生成するメソッド } /** Namespace の Iterator から ContentHandler#startPrefixMapping() を呼び出すメソッド */ private void startPrefixMappings(Iterator<?> nsIterator)throws SAXException{ List<Namespace> nsSet = new LinkedList<Namespace>(); while(nsIterator.hasNext()){ Namespace ns = (Namespace)nsIterator.next(); this.contentHandler.startPrefixMapping(ns.getPrefix(), ns.getNamespaceURI()); nsSet.add(ns); } this.nsQueue.push(nsSet); } /** Attribute の Iterator から Attributes を生成するメソッド */ private Attributes extractAttributes(Iterator<?> attIterator){ AttributesImpl atts = new AttributesImpl(); while(attIterator.hasNext()){ Attribute att = (Attribute)attIterator.next(); atts.addAttribute( att.getName().getNamespaceURI(), att.getName().getLocalPart(), QNameUtils.toPrefixedName(att.getName()), att.getDTDType(), att.getValue()); } return atts; }
toPrefixedName(QName) メソッドを定義している QNameUtils クラスは自作クラスです(こちらを参照)。 QNameUtils.toPrefixedName(QName) メソッドは、引数の QName オブジェクトから接頭辞付の名前("xsl:stylesheet" のような文字列)を返します。
要素の終了
要素の終了を処理する case 節は次の通り:
case END_ELEMENT: handleEndElement(event.asEndElement()); break;
要素の終了時に行う処理は、要素の開始に比べれば簡単です。 行う処理は
- ContentHandler#endElement(..) を呼び出す。
- スコープが終わる名前空間宣言に対して ContentHandler#endPrefixMapping() メソッドを呼び出す
です。
private void handleEndElement(EndElement end)throws SAXException{ this.contentHandler.endElement( end.getName().getNamespaceURI(), end.getName().getLocalPart(), QNameUtils.toPrefixedName(end.getName())); endPrefixMappings(); } private void endPrefixMappings()throws SAXException{ List<Namespace> nsSet = this.nsQueue.pop(); for(Namespace ns: nsSet) this.contentHandler.endPrefixMapping(ns.getPrefix()); }
*1:Locator のセット(ContentHandler#setDocumentLocator(Locator))は無視します。