倭マン's BLOG

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

XMLEventReader で SAX 解析

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