    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.xml;
    017
    018import org.xml.sax.InputSource;
    019import org.xml.sax.SAXException;
    020import org.xml.sax.Attributes;
    021import org.xml.sax.helpers.DefaultHandler;
    022
    023import javax.xml.parsers.SAXParserFactory;
    024import javax.xml.parsers.SAXParser;
    025import javax.xml.parsers.ParserConfigurationException;
    026
    027import java.io.Reader;
    028import java.io.IOException;
    029import java.util.Map;
    030
    031/**
    032 * このクラスは、拡張オラクル XDK形式のXMLファイルを処理するハンドラです。
    033 * オラクルXDK形式のXMLとは、下記のような ROWSET をトップとする ROW の
    034 * 集まりで１レコードを表し、各ROWには、カラム名をキーとするXMLになっています。
    035 *
    036 *   &lt;ROWSET&gt;
    037 *       &lt;ROW num="1"&gt;
    038 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
    039 *             ･･･
    040 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
    041 *       &lt;/ROW&gt;
    042 *        ･･･
    043 *       &lt;ROW num="n"&gt;
    044 *          ･･･
    045 *       &lt;/ROW&gt;
    046 *   &lt;ROWSET&gt;
    047 *
    048 * この形式であれば、XDK(Oracle XML Developer's Kit)を利用すれば、非常に簡単に
    049 * データベースとXMLファイルとの交換が可能です。
    050 * <a href="http://otn.oracle.co.jp/software/tech/xml/xdk/index.html" target="_blank" >
    051 * XDK(Oracle XML Developer's Kit)</a>
    052 *
    053 * 拡張XDK形式とは、ROW 以外に、SQL処理用タグ(EXEC_SQL)を持つ XML ファイルです。
    054 * また、登録するテーブル(table)を ROWSETタグの属性情報として付与することができます。
    055 * (大文字小文字に注意)
    056 * これは、オラクルXDKで処理する場合、無視されますので、同様に扱うことが出来ます。
    057 * この、EXEC_SQL は、それそれの XMLデータをデータベースに登録する際に、
    058 * SQL処理を自動的に流す為の、SQL文を記載します。
    059 * この処理は、イベント毎に実行される為、その配置順は重要です。
    060 * このタグは、複数記述することも出来ますが、BODY部には、１つのSQL文のみ記述します。
    061 *
    062 *   &lt;ROWSET tableName="XX" &gt;
    063 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(データクリア等)を実行させる。
    064 *           delete from GEXX where YYYYY
    065 *       &lt;/EXEC_SQL&gt;
    066 *       &lt;MERGE_SQL&gt;                   このSQL文で UPDATEして、結果が０件ならINSERTを行います。
    067 *           update GEXX set AA=[AA] , BB=[BB] where CC=[CC]
    068 *       &lt;/MERGE_SQL&gt;
    069 *       &lt;ROW num="1"&gt;
    070 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
    071 *             ･･･
    072 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
    073 *       &lt;/ROW&gt;
    074 *        ･･･
    075 *       &lt;ROW num="n"&gt;
    076 *          ･･･
    077 *       &lt;/ROW&gt;
    078 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
    079 *           update GEXX set AA='XX' , BB='YY' where CC='ZZ'
    080 *       &lt;/EXEC_SQL&gt;
    081 *   &lt;ROWSET&gt;
    082 *
    083 * DefaultHandler クラスを拡張している為、通常の処理と同様に、使用できます。
    084 *
    085 *      InputSource input = new InputSource( reader );
    086 *      HybsXMLHandler hndler = new HybsXMLHandler();
    087 *
    088 *      SAXParserFactory f = SAXParserFactory.newInstance();
    089 *      SAXParser parser = f.newSAXParser();
    090 *      parser.parse( input,hndler );
    091 *
    092 * また、上記の処理そのものを簡略化したメソッド:parse( Reader ) を持っているため、
    093 * 通常そのメソッドを使用します。
    094 *
    095 * HybsXMLHandler には、TagElementListener をセットすることができます。
    096 * これは、ROW 毎に 内部情報を TagElement オブジェクト化し、action( TagElement )
    097 * が呼び出されます。この Listener を介して、１レコードずつ処理することが
    098 * 可能です。
    099 *
    100 * @version  4.0
    101 * @author   Kazuhiko Hasegawa
    102 * @since    JDK5.0,
    103 */
    104public class HybsXMLHandler extends DefaultHandler {
    105        /** システム依存の改行記号をセットします。 */
    106        private static final String CR = System.getProperty("line.separator");
    107
    108        /** このハンドラのトップタグ名       {@value}        */
    109        public static final     String ROWSET           = "ROWSET";
    110        /** このハンドラで取り扱える ROWSETタグの属性    */
    111        public static final     String ROWSET_TABLE = "tableName";
    112
    113        /** このハンドラで取り扱えるタグ名     {@value}        */
    114        public static final     String ROW                      = "ROW";
    115        /** このハンドラで取り扱える ROWタグの属性       {@value}        */
    116        public static final     String ROW_NUM          = "num";
    117        /** このハンドラで取り扱えるタグ名     {@value}        */
    118        public static final     String EXEC_SQL         = "EXEC_SQL";
    119        /** このハンドラで取り扱えるタグ名     {@value}        */
    120        public static final     String MERGE_SQL        = "MERGE_SQL";
    121
    122        private Map<String,String>        defaultMap      = null;
    123        private TagElementListener listener     = null;
    124        private TagElement              element         = null;
    125        private String                  key                     = null;
    126        private StringBuilder   body            = null;
    127        private boolean                 bodyIn          = false;
    128        private int                             level           = 0;
    129
    130        /**
    131         *      パース処理を行います。
    132         *      通常のパース処理の簡易メソッドになっています。
    133         *
    134         * @param reader パース処理用のReaderオブジェクト
    135         */
    136        public void parse( final Reader reader ) {
    137                try {
    138                        SAXParserFactory fact = SAXParserFactory.newInstance();
    139                        SAXParser parser = fact.newSAXParser();
    140
    141                        InputSource input = new InputSource( reader );
    142
    143                        try {
    144                                parser.parse( input,this );
    145                        }
    146                        catch( SAXException ex ) {
    147                                if( ! "END".equals( ex.getMessage() ) ) {
    148                                        String errMsg = "XMLパースエラー key=" + key + CR
    149                                                                + "element=" + element + CR
    150                                                                + ex.getMessage() ;
    151                                        if( body != null ) {
    152                                                errMsg = errMsg + CR + body.toString();
    153                                        }
    154                                        throw new RuntimeException( errMsg,ex );
    155                                }
    156                        }
    157                }
    158                catch( ParserConfigurationException ex1 ) {
    159                        String errMsg = "SAXParser のコンフィグレーションが構築できません。"
    160                                                + "key=" + key + CR + ex1.getMessage();
    161                        throw new RuntimeException( errMsg,ex1 );
    162                }
    163                catch( SAXException ex2 ) {
    164                        String errMsg = "SAXParser が構築できません。"
    165                                                + "key=" + key + CR + ex2.getMessage();
    166                        throw new RuntimeException( errMsg,ex2 );
    167                }
    168                catch( IOException ex3 ) {
    169                        String errMsg = "InputSource の読み取り時にエラーが発生しました。"
    170                                                + "key=" + key + CR + ex3.getMessage();
    171                        throw new RuntimeException( errMsg,ex3 );
    172                }
    173        }
    174
    175        /**
    176         * 内部に TagElementListener を登録します。
    177         * これは、&lt;ROW&gt; タグの endElement 処理毎に呼び出されます。
    178         * つまり、行データを取得都度、TagElement オブジェクトを作成し、
    179         * この TagElementListener の action( TagElement ) メソッドを呼び出します。
    180         * 何もセットしない、または、null がセットされた場合は、何もしません。
    181         *
    182         * @param listener TagElementListenerオブジェクト
    183         */
    184        public void setTagElementListener( final TagElementListener listener ) {
    185                this.listener = listener;
    186        }
    187
    188        /**
    189         * TagElement オブジェクトを作成する時の 初期カラム/値を設定します。
    190         * TagElements オブジェクトは、XMLファイルより作成する為、項目(カラム)も
    191         * XMLファイルのROW属性に持っている項目と値で作成されます。
    192         * このカラム名を、外部から初期設定することが可能です。
    193         * その場合、ここで登録したカラム順(Mapに、LinkedHashMap を使用した場合)
    194         * が保持されます。また、ROW属性に存在しないカラムがあれば、値とともに
    195         * 初期値として設定しておくことが可能です。
    196         * なお、ここでのMapは、直接設定していますので、ご注意ください。
    197         *
    198         * @param       map     初期カラムマップ
    199         */
    200        public void setDefaultMap( final Map<String,String> map ) {
    201                defaultMap = map;
    202        }
    203
    204        /**
    205         * 要素内の文字データの通知を受け取ります。
    206         * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
    207         * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
    208         * データのファイルへの出力など) を実行することができます。
    209         *
    210         * @param       buffer  文字データ配列
    211         * @param       start   配列内の開始位置
    212         * @param       length  配列から読み取られる文字数
    213         * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
    214         */
    215        @Override
    216        public void characters( final char[] buffer, final int start, final int length ) throws SAXException {
    217                if( ! ROW.equals( key ) && ! ROWSET.equals( key ) && length > 0 ) {
    218                        body.append( buffer,start,length );
    219                        bodyIn = true;
    220                }
    221        }
    222
    223        /**
    224         * 要素の開始通知を受け取ります。
    225         * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
    226         * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
    227         * 各 startElement イベントには対応する endElement イベントがあります。
    228         * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
    229         * 要素のコンテンツ全部が順番に報告されます。
    230         * ここでは、タグがレベル３以上の場合は、上位タグの内容として取り扱います。よって、
    231         * タグに名前空間が定義されている場合、その属性は削除します。
    232         *
    233         * @param       namespace       名前空間 ＵＲＩ
    234         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
    235         * @param       qname           前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
    236         * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
    237         * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
    238         */
    239        @Override
    240        public void startElement(final String namespace, final String localName,
    241                                                         final String qname, final Attributes attributes) throws SAXException {
    242                if( ROWSET.equals( qname ) ) {
    243                        if( listener != null ) {
    244                                element = new TagElement( ROWSET,defaultMap );
    245                                element.put( ROWSET_TABLE,attributes.getValue( ROWSET_TABLE ) );
    246                                listener.actionInit( element );
    247                        }
    248                        element = null;
    249                }
    250                else if( ROW.equals( qname ) ) {
    251                        element = new TagElement( ROW,defaultMap );
    252                        String num = attributes.getValue( ROW_NUM );
    253                        element.setRowNo( num );
    254                }
    255                else if( EXEC_SQL.equals( qname ) ) {
    256                        element = new TagElement( EXEC_SQL );
    257                }
    258                else if( MERGE_SQL.equals( qname ) ) {
    259                        element = new TagElement( MERGE_SQL );
    260                }
    261
    262                if( level <= 2 ) {
    263                        key = qname;
    264                        body = new StringBuilder();
    265                }
    266                else {
    267                        // レベル３ 以上のタグは上位タグの内容として扱います。
    268                        body.append( "<" ).append( qname );
    269                        int len = attributes.getLength();
    270                        for( int i=0; i<len; i++ ) {
    271                                // 名前空間の宣言は、削除しておきます。あくまでデータとして取り扱う為です。
    272                                String attr = attributes.getQName(i);
    273                                if( ! attr.startsWith( "xmlns:" ) ) {
    274                                        body.append( " " );
    275                                        body.append( attr ).append( "=\"" );
    276                                        body.append( attributes.getValue(i) ).append( "\"" );
    277                                }
    278                        }
    279                        body.append( ">" );
    280                }
    281
    282                bodyIn = false;         // 入れ子状のタグのBODY部の有無
    283                level ++ ;
    284        }
    285
    286        /**
    287         * 要素の終了通知を受け取ります。
    288         * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
    289         * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
    290         * 各 endElement イベントには対応する startElement イベントがあります。
    291         * これは、要素が空である場合も変わりません。
    292         *
    293         * @param       namespace       名前空間 URI
    294         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
    295         * @param       qname   前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
    296         * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
    297         */
    298        @Override
    299        public void endElement(final String namespace, final String localName, final String qname) throws SAXException {
    300                level -- ;
    301                if( ROW.equals( qname ) ) {
    302                        if( listener != null ) {
    303                                listener.actionRow( element );
    304                        }
    305                        element = null;
    306                }
    307                else if( EXEC_SQL.equals( qname ) ) {
    308                        element.setBody( body.toString().trim() );
    309                        if( listener != null ) {
    310                                listener.actionExecSQL( element );
    311                        }
    312                        element = null;
    313                }
    314                else if( MERGE_SQL.equals( qname ) ) {
    315                        element.setBody( body.toString().trim() );
    316                        if( listener != null ) {
    317                                listener.actionMergeSQL( element );
    318                        }
    319                        element = null;
    320                }
    321                else if( level <= 2 ) {
    322                        if( element != null ) {
    323                                element.put( key , body.toString().trim() );
    324                        }
    325                }
    326                else {
    327                        if( bodyIn ) {
    328                                body.append( "</" ).append( qname ).append( ">" );
    329                        }
    330                        else {
    331                                body.insert( body.length()-1, " /" );           // タグの最後を " />" とする。
    332                        }
    333                }
    334        }
    335}