倭マン's BLOG

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

XMLStreamReader で SAX 解析

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))は無視します。