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