    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.opengion.fukurou.system.Closer;
    020import org.opengion.fukurou.system.LogWriter;
    021import static org.opengion.fukurou.system.HybsConst.CR;                                                 // 6.1.0.0 (2014/12/26) refactoring
    022import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;                              // 6.1.0.0 (2014/12/26) refactoring
    023
    024import java.io.Reader;
    025import java.io.BufferedReader;
    026import java.io.InputStreamReader;
    027import java.io.FileInputStream;
    028import java.util.Map;
    029import java.util.List;
    030import java.util.ArrayList;
    031import java.util.regex.Pattern;
    032import java.util.regex.Matcher;
    033import java.util.Arrays;
    034import java.util.Locale;
    035
    036import java.sql.DriverManager;
    037import java.sql.Connection;
    038import java.sql.Statement;
    039import java.sql.PreparedStatement;
    040import java.sql.ParameterMetaData;
    041import java.sql.SQLException;
    042import java.sql.ResultSet;                                                                                                              // 8.1.0.3 (2022/01/21)
    043
    044/**
    045 * このｸﾗｽは、ｵﾗｸﾙ XDKの oracle.xml.sql.dml.OracleXMLSave ｸﾗｽと
    046 * ほぼ同様の目的で使用できるｸﾗｽです。
    047 * 拡張XDK形式のXMLﾌｧｲﾙを読み込み、ﾃﾞｰﾀﾍﾞｰｽに INSERT します。
    048 *
    049 * 拡張XDK形式の元となる ｵﾗｸﾙ XDK(Oracle XML Developer's Kit)については、以下の
    050 * ﾘﾝｸを参照願います。
    051 * <a href="https://docs.oracle.com/cd/F19136_01/adxdk/introduction-to-XDK.html" target="_blank" >
    052 * XDK(Oracle XML Developer's Kit)</a>
    053 *
    054 * このｸﾗｽでは、MAP を登録する[ setDefaultMap( Map ) ]ことにより、
    055 * XMLﾌｧｲﾙに存在しないｶﾗﾑを初期値として設定することが可能になります。
    056 * 例えば、登録日や、登録者、または、ﾃﾝﾌﾟﾚｰﾄより各ｼｽﾃﾑID毎に
    057 * 登録するなどです。
    058 * 同様に、読み取った XMLﾌｧｲﾙの情報を書き換える機能[ setAfterMap( Map ) ]ﾒｿｯﾄﾞ
    059 * により、ｶﾗﾑの値の置き換えも可能です。
    060 *
    061 * 拡張XDK形式の元となる ｵﾗｸﾙ XDK(Oracle XML Developer's Kit)については、以下の
    062 * ﾘﾝｸを参照願います。
    063 * <a href="https://docs.oracle.com/cd/F19136_01/adxdk/introduction-to-XDK.html" target="_blank" >
    064 * XDK(Oracle XML Developer's Kit)</a>
    065 *
    066 * 拡張XDK形式とは、ROW 以外に、SQL処理用ﾀｸﾞ(EXEC_SQL)を持つ XML ﾌｧｲﾙです。
    067 * また、登録するﾃｰﾌﾞﾙ(table)を ROWSETﾀｸﾞの属性情報として付与することができます。
    068 * (大文字小文字に注意)
    069 * これは、ｵﾗｸﾙXDKで処理する場合、無視されますので、同様に扱うことが出来ます。
    070 * この、EXEC_SQL は、それそれの XMLﾃﾞｰﾀをﾃﾞｰﾀﾍﾞｰｽに登録する際に、
    071 * SQL処理を自動的に流す為の、SQL文を記載します。
    072 * この処理は、ｲﾍﾞﾝﾄ毎に実行される為、その配置順は重要です。
    073 * このﾀｸﾞは、複数記述することも出来ますが、BODY部には、１つのSQL文のみ記述します。
    074 *
    075 *   &lt;ROWSET tableName="XX" &gt;
    076 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(ﾃﾞｰﾀｸﾘｱ等)を実行させる。
    077 *           delete from GEXX where YYYYY
    078 *       &lt;/EXEC_SQL&gt;
    079 *       &lt;MERGE_SQL&gt;                   このSQL文で UPDATEして、結果が０件ならINSERTを行います。
    080 *           update GEXX set AA=[AA] , BB=[BB] where CC=[CC]
    081 *       &lt;/MERGE_SQL&gt;
    082 *       &lt;ROW num="1"&gt;
    083 *           &lt;ｶﾗﾑ1&gt;値1&lt;/ｶﾗﾑ1&gt;
    084 *             ･･･
    085 *           &lt;ｶﾗﾑn&gt;値n&lt;/ｶﾗﾑn&gt;
    086 *       &lt;/ROW&gt;
    087 *        ･･･
    088 *       &lt;ROW num="n"&gt;
    089 *          ･･･
    090 *       &lt;/ROW&gt;
    091 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
    092 *           update GEXX set AA='XX' , BB='XX' where YYYYY
    093 *       &lt;/EXEC_SQL&gt;
    094 *   &lt;ROWSET&gt;
    095 *
    096 * @og.rev 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。
    097 *
    098 * @version     7.0
    099 * @author      Kazuhiko Hasegawa
    100 * @since       JDK9.0,
    101 */
    102public class HybsXMLSave implements TagElementListener {
    103
    104        private String          tableName               ;
    105        //      private String[]        keyColumns              ;                               //6.3.9.0 (2015/11/06) 現時点で使われていないため、一旦取り消しておきます。
    106        private Connection      connection              ;
    107        private PreparedStatement insPstmt      ;                                       // INSERT用の PreparedStatement
    108        private PreparedStatement updPstmt      ;                                       // UPDATE用の PreparedStatement
    109        private ParameterMetaData insMeta       ;
    110        private ParameterMetaData updMeta       ;
    111        private int insCnt              ;
    112        private int updCnt              ;
    113        private int delCnt              ;
    114        private int ddlCnt              ;                                                               // 5.6.7.0 (2013/07/27) DDL文のｶｳﾝﾀｰ
    115        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え */
    116        private Map<String,String>      defaultMap      ;
    117        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え */
    118        private Map<String,String>      afterMap        ;
    119        private List<String>            updClms         ;
    120        private String[]                        insClms         ;
    121        private String                          lastSQL         ;                               // 5.6.6.1 (2013/07/12) デバッグ用。最後に使用したSQL文
    122
    123        private final boolean useParamMetaData  ;                               // 4.0.0.0 (2007/09/25)
    124
    125        // UPDATE時の [XXX] を取り出します。\w は、単語構成文字: [a-zA-Z_0-9]と同じ
    126        private static final Pattern PATTERN = Pattern.compile( "\\[\\w*\\]" );         // 6.4.1.1 (2016/01/16) pattern → PATTERN refactoring
    127
    128        // 5.6.9.2 (2013/10/18) EXEC_SQL のｴﾗｰを無視するかどうかを指定できます。
    129        private boolean isExecErr       = true;                                         // 6.0.2.5 (2014/10/31) true は、ｴﾗｰ時に Exception を発行します。
    130
    131        // 7.3.2.0 (2021/03/19) ｴﾗｰは無視するが、履歴は返します。
    132        private final StringBuilder errBuf = new StringBuilder();
    133
    134        /**
    135         * ｺﾈｸｼｮﾝを指定して、ｵﾌﾞｼﾞｪｸﾄを構築します。
    136         * ﾃｰﾌﾞﾙ名は、拡張XDK形式のROWSETﾀｸﾞのtableName属性に
    137         * 記述しておく必要があります。
    138         *
    139         * @param       conn    ﾃﾞｰﾀﾍﾞｰｽ接続
    140         */
    141        public HybsXMLSave( final Connection conn ) {
    142                this( conn,null );
    143        }
    144
    145        /**
    146         * ｺﾈｸｼｮﾝとﾃｰﾌﾞﾙ名を指定して、ｵﾌﾞｼﾞｪｸﾄを構築します。
    147         * ここで指定するﾃｰﾌﾞﾙ名は、ﾃﾞﾌｫﾙﾄﾃｰﾌﾞﾙという扱いです。
    148         * 拡張XDK形式のROWSETﾀｸﾞのtableName属性にﾃｰﾌﾞﾙ名が記述されている場合は、
    149         * そちらが優先されます。
    150         *
    151         * @og.rev 4.0.0.0 (2007/09/25) ParameterMetaData を使用したﾊﾟﾗﾒｰﾀ設定追加。
    152         * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を このｸﾗｽで直接取得する。(PostgreSQL対応)
    153         *
    154         * @param       conn    ﾃﾞｰﾀﾍﾞｰｽ接続
    155         * @param       table   ﾃｰﾌﾞﾙ名(ROWSETﾀｸﾞのtable属性が未設定時に使用)
    156         */
    157        public HybsXMLSave( final Connection conn,final String table ) {
    158                connection = conn;
    159                tableName  = table;
    160                useParamMetaData = useParameterMetaData( connection );          // 5.3.8.0 (2011/08/01)
    161        }
    162
    163        /**
    164         * EXEC_SQL のｴﾗｰ時に Exception を発行するかどうかを指定できます(初期値:true)。
    165         * true を指定すると、ｴﾗｰ時には、 RuntimeException を throw します。
    166         * false にすると、標準ｴﾗｰ出力にのみ、出力します。
    167         * このﾌﾗｸﾞは、EXEC_SQL のみ有効です。それ以外のﾀﾌﾞの処理では、ｴﾗｰが発生すると
    168         * その時点で、Exception を発行して、処理を終了します。
    169         * 初期値は、true(Exception を発行する) です。
    170         *
    171         * @og.rev 5.6.9.2 (2013/10/18) 新規追加
    172         *
    173         * @param       flag    true:Exception を発行する/false:標準ｴﾗｰ出力に出力する
    174         */
    175        public void onExecErrException( final boolean flag ) {
    176                isExecErr = flag;                                               // 6.0.2.5 (2014/10/31) refactoring
    177        }
    178
    179        /**
    180         * &lt;ROWSET&gt; ﾀｸﾞの一番最初に呼び出されます。
    181         * ROWSET の属性である、table 属性と、dbid 属性 を、TagElement の
    182         * get ﾒｿｯﾄﾞで取得できます。
    183         * 取得時のｷｰは、それぞれ、"TABLE" と "DBID" です。
    184         *
    185         * @og.rev 8.1.0.3 (2022/01/21) "tableName" を、HybsXMLHandler.ROWSET_TABLE に変更。
    186         *
    187         * @param       tag     ﾀｸﾞｴﾚﾒﾝﾄ
    188         * @see org.opengion.fukurou.xml.TagElement
    189         * @see HybsXMLHandler#setTagElementListener( TagElementListener )
    190         */
    191        @Override       // TagElementListener
    192        public void actionInit( final TagElement tag ) {
    193//              final String table = tag.get( "tableName" );
    194                final String table = tag.get( HybsXMLHandler.ROWSET_TABLE );    // 8.1.0.3 (2022/01/21)
    195                if( table != null ) { tableName = table; }
    196        }
    197
    198        /**
    199         * &lt;ROW&gt; ﾀｸﾞの endElement 処理毎に呼び出されます。
    200         * この Listener をｾｯﾄすることにより、行ﾃﾞｰﾀを取得都度、
    201         * TagElement ｵﾌﾞｼﾞｪｸﾄを作成し、このﾒｿｯﾄﾞが呼び出されます。
    202         *
    203         * @og.rev 4.0.0.0 (2007/05/09) ParameterMetaData を使用したﾊﾟﾗﾒｰﾀ設定追加。
    204         * @og.rev 4.0.0.0 (2007/09/25) isOracle から useParamMetaData に変更
    205         * @og.rev 4.3.7.0 (2009/06/01) HSQLDB対応
    206         * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData setNull 対応(PostgreSQL対応)
    207         * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。ﾃﾞﾊﾞｯｸﾞ用に、最後に使用したSQL文を残します。
    208         *
    209         * @param       tag     ﾀｸﾞｴﾚﾒﾝﾄ
    210         * @see org.opengion.fukurou.xml.TagElement
    211         * @see HybsXMLHandler#setTagElementListener( TagElementListener )
    212         */
    213        @Override       // TagElementListener
    214        public void actionRow( final TagElement tag ) {
    215                tag.setAfterMap( afterMap );
    216
    217                String[] vals = null;                                                           // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    218                try {
    219                        // 更新SQL(MERGE_SQLタグ)が存在する場合の処理
    220                        int tempCnt = 0;
    221                        if( updPstmt != null ) {
    222                                vals = tag.getValues( updClms );                        // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    223                                for( int j=0; j<vals.length; j++ ) {
    224                                        // 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え
    225                                        if( vals[j] != null && vals[j].isEmpty() ){
    226                                                vals[j] = null;
    227                                        }
    228
    229                                        // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したﾊﾟﾗﾒｰﾀ設定追加
    230                                        if( useParamMetaData ) {
    231                                                final int type = updMeta.getParameterType( j+1 );
    232                                                // 5.3.8.0 (2011/08/01) setNull 対応
    233                                                final String val = vals[j];
    234                                                if( val == null || val.isEmpty() ) {
    235                                                        updPstmt.setNull( j+1, type );
    236                                                }
    237                                                else {
    238                                                        updPstmt.setObject( j+1, val, type );
    239                                                }
    240                                        }
    241                                        else {
    242                                                updPstmt.setObject( j+1,vals[j] );
    243                                        }
    244                                }
    245                                tempCnt = updPstmt.executeUpdate();
    246                                if( tempCnt > 1 ) {
    247                                        final String errMsg = "Update キーが重複しています。"
    248                                                        + "TABLE=[" + tableName + "] ROW=["
    249                                                        + tag.getRowNo() + "]" + CR
    250                                                        + " SQL=[" + lastSQL + "]" + CR                         // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    251                                                        + tag.toString() + CR
    252                                                        + Arrays.toString( vals ) + CR ;                        // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    253                                        throw new OgRuntimeException( errMsg );
    254                                }
    255                                updCnt += tempCnt;
    256                        }
    257                        // 更新が 0件の場合は、INSERT処理を行います。
    258                        if( tempCnt == 0 ) {
    259                                // 初回INSERT時のﾀｸﾞより、DB登録SQL文を構築します。
    260                                if( insPstmt == null ) {
    261                                        insClms  = tag.getKeys();
    262                                        lastSQL  = insertSQL( insClms,tableName );                      // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    263                                        insPstmt = connection.prepareStatement( lastSQL );
    264                                        // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
    265                                        if( useParamMetaData ) { insMeta = insPstmt.getParameterMetaData(); }
    266                                }
    267                                vals = tag.getValues( insClms );                                                // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    268                                for( int j=0; j<vals.length; j++ ) {
    269                                        // 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え
    270                                        if( vals[j] != null && vals[j].isEmpty() ){
    271                                                vals[j] = null;
    272                                        }
    273
    274                                        // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したﾊﾟﾗﾒｰﾀ設定追加
    275                                        if( useParamMetaData ) {
    276                                                final int type = insMeta.getParameterType( j+1 );
    277                                                // 5.3.8.0 (2011/08/01) setNull 対応
    278                                                final String val = vals[j];
    279                                                if( val == null || val.isEmpty() ) {
    280                                                        insPstmt.setNull( j+1, type );
    281                                                }
    282                                                else {
    283                                                        insPstmt.setObject( j+1, val, type );
    284                                                }
    285                                        }
    286                                        else {
    287                                                insPstmt.setObject( j+1,vals[j] );
    288                                        }
    289                                }
    290                                insCnt += insPstmt.executeUpdate();
    291                        }
    292                }
    293                catch( final SQLException ex ) {
    294                        final String errMsg = "DB登録エラーが発生しました。"
    295                                                + "TABLE=[" + tableName + "] ROW=["
    296                                                + tag.getRowNo() + "]" + CR
    297                                                + " SQL=[" + lastSQL + "]" + CR                         // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    298                                                + tag.toString() + CR
    299                                                + Arrays.toString( vals ) + CR                          // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    300                                                + ex.getMessage() + ":" + ex.getSQLState() + CR ;
    301                        throw new OgRuntimeException( errMsg,ex );
    302                }
    303        }
    304
    305        /**
    306         * &lt;EXEC_SQL&gt; ﾀｸﾞの endElement 処理毎に呼び出されます。
    307         * getBody ﾒｿｯﾄﾞを使用して、このﾀｸﾞのBODY部の文字列を取得します。
    308         * この Listener をｾｯﾄすることにより、EXEC_SQL ﾃﾞｰﾀを取得都度、
    309         * TagElement ｵﾌﾞｼﾞｪｸﾄを作成し、このﾒｿｯﾄﾞが呼び出されます。
    310         * EXEC_SQL ﾀｸﾞでは、delete文やupdate文など、特殊な前処理や後処理用の SQLと
    311         * DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）の処理なども記述できます。
    312         * ここでは簡易的に、何か実行された場合は、delete 処理と考え、削除ｶｳﾝﾄを加算し、
    313         * 0件で帰ってきた場合に、DDLが実行されたと考え、DDLｶｳﾝﾄを＋１します。
    314         * ただし、0件 delete も考えられるため、SQL文の先頭文字によるﾁｪｯｸは入れておきます。
    315         *
    316         * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。ﾃﾞﾊﾞｯｸﾞ用に、最後に使用したSQL文を残します。
    317         * @og.rev 5.6.7.0 (2013/07/27) DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）の処理件数追加
    318         * @og.rev 5.6.9.2 (2013/10/18) EXEC_SQL のｴﾗｰ時に Exception を発行するかどうかを指定
    319         * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
    320         * @og.rev 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。
    321         * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    322         *
    323         * @param       tag     ﾀｸﾞｴﾚﾒﾝﾄ
    324         * @see org.opengion.fukurou.xml.TagElement
    325         * @see HybsXMLHandler#setTagElementListener( TagElementListener )
    326         */
    327        @Override       // TagElementListener
    328        public void actionExecSQL( final TagElement tag ) {
    329                // 6.4.2.1 (2016/02/05) try-with-resources 文
    330                lastSQL = tag.getBody();                        // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用                6.4.2.1 (2016/02/05) try の前に出します。
    331                try( Statement execSQL = connection.createStatement() ) {
    332                        // 5.6.7.0 (2013/07/27) DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）の処理件数追加
    333                        // 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。
    334//                      final String[] sqls = getExecSQLs( lastSQL ) ;
    335                        final List<String> sqls = getExecSQLs( lastSQL ) ;                                      // 8.1.0.3 (2022/01/21) Listに変更
    336
    337                        // 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    338                        // SQL文が、2個以上あり、exists属性が存在する場合のみ、最初のSQL文を実行して判定する。
    339                        if( sqls.size() > 1 ) {
    340                                final String exists = tag.get( HybsXMLHandler.EXEC_EXISTS );    // "0"か、"1"(!=0)か
    341                                if( exists != null && exists.length() > 0 ) {
    342                                        final boolean isZero = exists.charAt(0) == '0' ;
    343                                        lastSQL = sqls.remove(0);                                                                       // 先頭のSQL文を取り出し、lastSQL に設定
    344                                        try( ResultSet resultSet = execSQL.executeQuery( lastSQL ) ) {
    345                                                if( resultSet.next() ) {
    346                                                        final int rtnCnt = resultSet.getInt(1);
    347                                                        // exists=='0' と、ｶｳﾝﾄ==0 のXORが true の場合(つまり、条件が不一致の場合)は、抜ける。
    348                                                        if( isZero ^ rtnCnt == 0 ) { return; }
    349                                                }
    350                                        }
    351                                }
    352                        }
    353
    354                        for( final String sql : sqls ) {
    355                                // 8.1.0.3 (2022/01/21) EXEC_SQLで、『;』分割時に、ｾﾞﾛ文字列が含まれるかもしれない。
    356                                if( sql.trim().isEmpty() ) { continue; }                // 本当は、trim() は必要ない。
    357
    358                                lastSQL = sql;                                                          // 8.1.0.3 (2022/01/21) lastSQL に設定
    359                                final int cnt = execSQL.executeUpdate( sql ) ;
    360
    361                                // 件数ｶｳﾝﾄ用
    362                                final String upSQL = sql.trim().toUpperCase( Locale.JAPAN );
    363                                if(      upSQL.startsWith( "DELETE" ) ) { delCnt += cnt; }
    364                                else if( upSQL.startsWith( "INSERT" ) ) { insCnt += cnt; }
    365                                else if( upSQL.startsWith( "UPDATE" ) ) { updCnt += cnt; }
    366                                else {                                                                    ddlCnt ++ ;    }              // DLLの場合は、件数=0が返される。
    367                        }
    368                }
    369                catch( final SQLException ex ) {                                        // catch は、close() されてから呼ばれます。
    370                        final String errMsg = "DB登録エラーが発生しました。"
    371                                                + "TABLE=[" + tableName + "] ROW=["
    372                                                + tag.getRowNo() + "]" + CR
    373                                                + " SQL=[" + lastSQL + "]" + CR                         // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    374                                                + tag.toString() + CR
    375                                                + ex.getMessage() + ":" + ex.getSQLState() + CR ;
    376
    377                        // 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行するかどうかを指定
    378                        if( isExecErr ) {                                                                               // 6.0.2.5 (2014/10/31) refactoring
    379                                throw new OgRuntimeException( errMsg,ex );
    380                        }
    381                        else {
    382                                System.err.println( errMsg );
    383                                errBuf.append( errMsg );
    384                        }
    385                }
    386        }
    387
    388        /**
    389         * EXEC_SQLで、";" で複数のSQL文に分割します。
    390         *
    391         * 厳密に処理していません。
    392         * SQL文の中に文字として";"が使われている場合の考慮がされていません。
    393         *
    394         * 7.3.2.0 (2021/03/19)
    395         *   暫定的に、BEGIN～END 構文(大文字のみ)を持つ場合は、";" で複数のSQL文に分割しません。
    396         *
    397         * 8.1.0.3 (2022/01/21)
    398         *   ･先頭行が 『SELECT』の場合は、";" で分割する。
    399         *   ･先頭行が 『CREATE』で、FUNCTION、PACKAGE、PROCEDURE、TRIGGER を含む場合は、分割しない。
    400         *       つまり、それ以降の分割判定は行わないため、後続に複数SQL文は記述できません。
    401         *   ･上記以外の場合は、";" で分割する。
    402         *
    403         * @og.rev 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。
    404         * @og.rev 7.3.2.0 (2021/03/19) TRIGGER など、BEGIN～END 構文を持つ場合は、";" で複数のSQL文に分割しません。
    405         * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    406         *
    407         * @param       sqlText EXEC_SQL内部に書かれたSQL文
    408         *
    409         * @return      分割されたSQL文のList
    410         */
    411//      private String[] getExecSQLs( final String sqlText ) {
    412        private List<String> getExecSQLs( final String sqlText ) {                                      // List に変更
    413                final List<String> sqlList = new ArrayList<>();
    414
    415                final String orgStr = sqlText.trim();
    416                final String uppStr = orgStr.toUpperCase( Locale.JAPAN );                               // 判定用
    417
    418                int st = 0;
    419                while( st < orgStr.length() ) {
    420                        final int ed = orgStr.indexOf( ';',st );
    421
    422                        if( ed < 0 ) {
    423                                sqlList.add( orgStr.substring( st ).trim() );                                   // trim() したSQL文を返す。
    424                                break;
    425                        }
    426                        else {
    427                                final String sql = uppStr.substring( st,ed ).trim();                    // 大文字で判定(先頭～; まで)
    428
    429                                if( sql.startsWith( "SELECT" ) ) {                                                              // 大文字で先頭比較
    430                                        sqlList.add( orgStr.substring( st,ed ).trim() );                        // 先頭～; までを登録
    431                                }
    432                                else {
    433                                        if( sql.startsWith( "CREATE" ) && (
    434                                                        sql.contains( "FUNCTION" )  || sql.contains( "PACKAGE" ) ||
    435                                                        sql.contains( "PROCEDURE" ) || sql.contains( "TRIGGER" ) ) ) {
    436                                                sqlList.add( orgStr.substring( st ).trim() );                   // 残りすべてを登録
    437                                                break;
    438                                        }
    439                                        else {
    440                                                sqlList.add( orgStr.substring( st,ed ).trim() );                // 部分先頭 ～ ; までを登録
    441                                        }
    442                                }
    443                        }
    444                        st = ed + 1;
    445                }
    446
    447                return sqlList ;
    448
    449//              if( sqlText.contains( "BEGIN" ) && sqlText.contains( "END" ) ) {                // 7.3.2.0 (2021/03/19)
    450//                      return new String[] { sqlText };
    451//              }
    452//              else {
    453//                      return sqlText.split( ";" );
    454//              }
    455        }
    456
    457        /**
    458         * &lt;MERGE_SQL&gt; ﾀｸﾞの endElement 処理時に呼び出されます。
    459         * getBody ﾒｿｯﾄﾞを使用して、このﾀｸﾞのBODY部の文字列を取得します。
    460         * MERGE_SQLﾀｸﾞは、ﾏｰｼﾞ処理したいﾃﾞｰﾀ部よりも上位に記述しておく
    461         * 必要がありますが、中間部に複数回記述しても構いません。
    462         * このﾀｸﾞが現れるまでは、INSERT のみ実行されます。このﾀｸﾞ以降は、
    463         * 一旦 UPDATE し、結果が ０件の場合は、INSERTする流れになります。
    464         * 完全に INSERT のみであるﾃﾞｰﾀを前半に、UPDATE/INSERTを行う
    465         * ﾃﾞｰﾀを後半に、その間に、MERGE_SQL ﾀｸﾞを入れることで、無意味な
    466         * UPDATE を避けることが可能です。
    467         * この Listener をｾｯﾄすることにより、MERGE_SQL ﾃﾞｰﾀを取得都度、
    468         * TagElement ｵﾌﾞｼﾞｪｸﾄを作成し、このﾒｿｯﾄﾞが呼び出されます。
    469         *
    470         * @og.rev 4.0.0.0 (2007/05/09) ParameterMetaData を使用したﾊﾟﾗﾒｰﾀ設定追加。
    471         * @og.rev 4.0.0.0 (2007/09/25) isOracle から useParamMetaData に変更
    472         * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。ﾃﾞﾊﾞｯｸﾞ用に、最後に使用したSQL文を残します。
    473         *
    474         * @param       tag     ﾀｸﾞｴﾚﾒﾝﾄ
    475         * @see org.opengion.fukurou.xml.TagElement
    476         * @see HybsXMLHandler#setTagElementListener( TagElementListener )
    477         */
    478        @Override       // TagElementListener
    479        public void actionMergeSQL( final TagElement tag ) {
    480                if( updPstmt != null ) {
    481                        final String errMsg = "MERGE_SQLタグが、複数回記述されています。"
    482                                                + "TABLE=[" + tableName + "] ROW=["
    483                                                + tag.getRowNo() + "]" + CR
    484                                                + " SQL=[" + lastSQL + "]" + CR         // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    485                                                + tag.toString() + CR;
    486                        throw new OgRuntimeException( errMsg );
    487                }
    488
    489                final String orgSql = tag.getBody();
    490                final Matcher matcher = PATTERN.matcher( orgSql );
    491                updClms = new ArrayList<>();
    492                while( matcher.find() ) {
    493                        // ここでは、[XXX]にマッチする為、前後の[]を取り除きます。
    494                        updClms.add( orgSql.substring( matcher.start()+1,matcher.end()-1 ) );
    495                }
    496                lastSQL = matcher.replaceAll( "?" );                            // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    497
    498                try {
    499                        updPstmt = connection.prepareStatement( lastSQL );
    500                        // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加
    501                        if( useParamMetaData ) { updMeta = updPstmt.getParameterMetaData(); }
    502                }
    503                catch( final SQLException ex ) {
    504                        final String errMsg = "Statement作成時にエラーが発生しました。"
    505                                                + "TABLE=[" + tableName + "] ROW=["
    506                                                + tag.getRowNo() + "]" + CR
    507                                                + " SQL=[" + lastSQL + "]" + CR         // 5.6.6.1 (2013/07/12) ﾃﾞﾊﾞｯｸﾞ用
    508                                                + tag.toString() + CR
    509                                                + ex.getMessage() + ":" + ex.getSQLState() + CR ;
    510                        throw new OgRuntimeException( errMsg,ex );
    511                }
    512        }
    513
    514        //      /**
    515        //       * UPDATE,DELETE を行う場合の WHERE 条件になるｷｰ配列
    516        //       * このｷｰの AND 条件でｶﾗﾑを特定し、UPDATE,DELETE などの処理を
    517        //       * 行います。
    518        //       *
    519        //       * @og.rev 6.3.9.0 (2015/11/06) 現時点で使われていないため、一旦取り消しておきます。
    520        //       *
    521        //       * @param       keyCols WHERE条件になるｷｰ配列(可変長引数)
    522        //       */
    523        //       public void setKeyColumns( final String... keyCols ) {
    524        //              keyColumns = new String[keyCols.length];
    525        //              System.arraycopy( keyCols,0,keyColumns,0,keyColumns.length );
    526        //       }
    527
    528        /**
    529         * XMLﾌｧｲﾙを読み取る前に指定するｶﾗﾑと値のﾍﾟｱ(ﾏｯﾌﾟ)情報をｾｯﾄします。
    530         *
    531         * このｶﾗﾑと値のﾍﾟｱのﾏｯﾌﾟは、ｵﾌﾞｼﾞｪｸﾄ構築前に設定される為、
    532         * XMLﾌｧｲﾙにｷｰが存在している場合は、値が書き変わります。(XML優先)
    533         * XMLﾌｧｲﾙにｷｰが存在していない場合は、ここで指定するMapの値が
    534         * 初期設定値として使用されます。
    535         * ここで指定する Map に LinkedHashMap を使用する場合、ｶﾗﾑ順も
    536         * 指定することが出来ます。
    537         *
    538         * @param       map     初期設定するｶﾗﾑﾃﾞｰﾀﾏｯﾌﾟ
    539         * @see #setAfterMap( Map )
    540         */
    541        public void setDefaultMap( final Map<String,String> map ) { defaultMap = map; }
    542
    543        /**
    544         * XMLﾌｧｲﾙを読み取った後で指定するｶﾗﾑと値のﾍﾟｱ(ﾏｯﾌﾟ)情報をｾｯﾄします。
    545         *
    546         * このｶﾗﾑと値のﾍﾟｱのﾏｯﾌﾟは、ｵﾌﾞｼﾞｪｸﾄ構築後に設定される為、
    547         * XMLﾌｧｲﾙのｷｰの存在に関係なく、Mapのｷｰと値が使用されます。(Map優先)
    548         * null を設定した場合は、なにも処理されません。
    549         *
    550         * @param       map     後設定するｶﾗﾑﾃﾞｰﾀﾏｯﾌﾟ
    551         * @see #setDefaultMap( Map )
    552         */
    553        public void setAfterMap( final Map<String,String> map ) { afterMap = map; }
    554
    555        /**
    556         * ﾃﾞｰﾀﾍﾞｰｽに追加処理(INSERT)を行います。
    557         *
    558         * 先に指定されたｺﾈｸｼｮﾝを用いて、指定のﾃｰﾌﾞﾙに INSERT します。
    559         * 引数には、XMLﾌｧｲﾙを指定したﾘｰﾀﾞｰをｾｯﾄします。
    560         * ｺﾈｸｼｮﾝは、終了後、ｺﾐｯﾄされます。(close されません。)
    561         * ﾘｰﾀﾞｰのｸﾛｰｽﾞは、ここでは行っていません。
    562         *
    563         * @og.rev 5.1.1.0 (2009/11/11) insMeta , updMeta のｸﾘｱ(気休め)
    564         *
    565         * @param       reader  XMLﾌｧｲﾙを指定するﾘｰﾀﾞｰ
    566         */
    567        public void insertXML( final Reader reader ) {
    568                try {
    569                        final HybsXMLHandler handler = new HybsXMLHandler();
    570                        handler.setTagElementListener( this );
    571                        handler.setDefaultMap( defaultMap );
    572
    573                        handler.parse( reader );
    574                }
    575                finally {
    576                        Closer.stmtClose( insPstmt );
    577                        Closer.stmtClose( updPstmt );
    578                        insPstmt = null;
    579                        updPstmt = null;
    580                        insMeta = null;         // 5.1.1.0 (2009/11/11)
    581                        updMeta = null;         // 5.1.1.0 (2009/11/11)
    582                }
    583        }
    584
    585        /**
    586         * ｲﾝｻｰﾄ用のSQL文を作成します。
    587         *
    588         * @og.rev 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。
    589         *
    590         * @param       columns         ｲﾝｻｰﾄするｶﾗﾑ名
    591         * @param       tableName       ｲﾝｻｰﾄするﾃｰﾌﾞﾙ名
    592         *
    593         * @return      ｲﾝｻｰﾄ用のSQL文
    594         * @og.rtnNotNull
    595         */
    596        private String insertSQL( final String[] columns,final String tableName ) {
    597                if( tableName == null ) {
    598                        final String errMsg = "tableName がセットされていません。" + CR
    599                                                + "tableName は、コンストラクタで指定するか、ROWSETのtableName属性で"
    600                                                + "指定しておく必要があります" + CR ;
    601                        throw new OgRuntimeException( errMsg );
    602                }
    603
    604                // 6.0.2.5 (2014/10/31) char を append する。
    605                // 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。
    606                final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE )
    607                        .append( "INSERT INTO " ).append( tableName )
    608                        .append( " ( " )
    609                        .append( String.join( "," , columns ) ) // 6.2.3.0 (2015/05/01)
    610                        .append( " ) VALUES ( ?" );
    611                for( int i=1; i<columns.length; i++ ) {
    612                        sql.append( ",?" );
    613                }
    614                sql.append( " )" );
    615
    616                return sql.toString();
    617        }
    618
    619        /**
    620         * ﾃﾞｰﾀﾍﾞｰｽに追加した件数を返します。
    621         *
    622         * @return      登録件数
    623         */
    624        public int getInsertCount() { return insCnt; }
    625
    626        /**
    627         * ﾃﾞｰﾀﾍﾞｰｽを更新した件数を返します。
    628         * これは、拡張XDK形式で、MERGE_SQL ﾀｸﾞを使用した場合の更新処理件数を
    629         * 合計した値を返します。
    630         *
    631         * @return      更新件数
    632         */
    633        public int getUpdateCount() { return updCnt; }
    634
    635        /**
    636         * ﾃﾞｰﾀﾍﾞｰｽに変更(更新、削除を含む)した件数を返します。
    637         * これは、拡張XDK形式で、EXEC_SQL ﾀｸﾞを使用した場合の実行件数を合計した
    638         * 値を返します。
    639         * よって、更新か、追加か、削除かは、判りませんが、通常 登録前に削除する
    640         * ｹｰｽで使われることから、deleteCount としています。
    641         *
    642         * @return      変更件数(主に、削除件数)
    643         */
    644        public int getDeleteCount() { return delCnt; }
    645
    646        /**
    647         * ﾃﾞｰﾀﾍﾞｰｽにDDL（ﾃﾞｰﾀ定義言語：Data Definition Language）処理した件数を返します。
    648         * これは、拡張XDK形式で、EXEC_SQL ﾀｸﾞを使用した場合の実行件数を合計した
    649         * 値を返します。
    650         * EXEC_SQL では、登録前に削除する delete 処理も、EXEC_SQL ﾀｸﾞを使用して実行しますが
    651         * その処理と分けてｶｳﾝﾄします。
    652         *
    653         * @og.rev 5.6.7.0 (2013/07/27) DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）の処理件数追加
    654         *
    655         * @return      DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）処理した件数
    656         */
    657        public int getDDLCount() { return ddlCnt; }
    658
    659        /**
    660         * 実際に登録された ﾃｰﾌﾞﾙ名を返します。
    661         *
    662         * ﾃｰﾌﾞﾙ名は、拡張XDK形式のROWSETﾀｸﾞのtableName属性に
    663         * 記述しておくか、ｺﾝｽﾄﾗｸﾀｰで引数として渡します。
    664         * 両方指定された場合は、ROWSETﾀｸﾞのtableName属性が優先されます。
    665         * ここでの返り値は、実際に使用された ﾃｰﾌﾞﾙ名です。
    666         *
    667         * @return      ﾃｰﾌﾞﾙ名
    668         */
    669        public String getTableName() { return tableName; }
    670
    671        /**
    672         * isExecErr でfalseを指定した場合に、ｴﾗｰ内容の文字列を取り出します。
    673         * ｴﾗｰが発生しなかった場合は、ｾﾞﾛ文字列が返ります。
    674         *
    675         * @og.rev 7.3.2.0 (2021/03/19) isExecErr でfalseを指定した場合に、ｴﾗｰ内容の文字列を取り出します。
    676         *
    677         * @return      ｴﾗｰ内容の文字列
    678         */
    679        public String getErrorMessage() { return errBuf.toString(); }
    680
    681        /**
    682         * この接続が、PreparedStatement#getParameterMetaData() を使用するかどうかを判定します。
    683         * 本来は、ConnectionFactory#useParameterMetaData(String)を使うべきだが、dbid が無いため、直接取得します。
    684         *
    685         * ※ 6.1.0.0 (2014/12/26) で、直接取得に変更します。DBUtil 経由で取得する方が、ｿｰｽｺｰﾄﾞﾚﾍﾞﾙでの
    686         *    共通化になるので良いのですが、org.opengion.fukurou.db と、org.opengion.fukurou.xml ﾊﾟｯｹｰｼﾞが
    687         *    循環参照(相互参照)になるため、どちらかを切り離す必要があります。
    688         *    db ﾊﾟｯｹｰｼﾞ側では、DBConfig.xml の処理の関係で、org.opengion.fukurou.xml.DomParser を
    689         *    使っているため、こちらの処理を、内部処理に変更することで、対応します。
    690         *
    691         * @og.rev 5.3.8.0 (2011/08/01) 新規作成 ( ApplicationInfo#useParameterMetaData(Connection) からｺﾋﾟｰ )
    692         * @og.rev 5.6.7.0 (2013/07/27) dbProductName は、DBUtil 経由で取得する。
    693         * @og.rev 6.1.0.0 (2014/12/26) dbProductName は、DBUtil 経由ではなく、直接取得する。
    694         *
    695         * @param       conn    接続先(ｺﾈｸｼｮﾝ)
    696         *
    697         * @return      使用する場合：true / その他:false
    698         */
    699        private static boolean useParameterMetaData( final Connection conn ) {
    700
    701                String dbName ;
    702                try {
    703                        dbName = conn.getMetaData().getDatabaseProductName().toLowerCase( Locale.JAPAN );
    704                }
    705                catch( final SQLException ex ) {
    706                        dbName = "none";
    707                }
    708
    709                return "PostgreSQL".equalsIgnoreCase( dbName ) ;
    710        }
    711
    712        /**
    713         * ﾃｽﾄ用のﾒｲﾝﾒｿｯﾄﾞ
    714         *
    715         * Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER]
    716         *    USER    : DB接続ﾕｰｻﾞｰ(GE)
    717         *    PASSWD  : DB接続ﾊﾟｽﾜｰﾄﾞ(GE)
    718         *    URL     : DB接続JDBCﾄﾞﾗｲﾊﾞURL(jdbc:oracle:thin:@localhost:1521:HYBS
    719         *    TABLE   : 登録するﾃｰﾌﾞﾙID(GE21)
    720         *    FILE    : 登録するORACLE XDK 形式 XMLﾌｧｲﾙ(GE21.xml)
    721         *    [ENCODE]: ﾌｧｲﾙのｴﾝｺｰﾄﾞ 初期値:UTF-8
    722         *    [DRIVER]: JDBCﾄﾞﾗｲﾊﾞｰ 初期値:oracle.jdbc.OracleDriver
    723         *
    724         * ※ ﾌｧｲﾙが存在しなかった場合、FileNotFoundException を RuntimeException に変換して、throw します。
    725         * ※ 指定のｴﾝｺｰﾄﾞが存在しなかった場合、UnsupportedEncodingException を RuntimeException に変換して、throw します。
    726         *
    727         * @og.rev 5.1.1.0 (2009/12/01) MySQL対応 明示的に、TRANSACTION_READ_COMMITTED を指定する。
    728         * @og.rev 5.6.7.0 (2013/07/27) DDL（ﾃﾞｰﾀ定義言語：Data Definition Language）の処理件数追加
    729         * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
    730         *
    731         * @param       args                                    ｺﾏﾝﾄﾞ引数配列
    732         * @throws      ClassNotFoundException  ｸﾗｽを見つけることができなかった場合。
    733         * @throws      SQLException                    ﾃﾞｰﾀﾍﾞｰｽ接続ｴﾗｰが発生した場合。
    734         */
    735        public static void main( final String[] args )
    736                        throws ClassNotFoundException , SQLException {
    737                if( args.length < 5 ) {
    738                        LogWriter.log( "Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER]" );
    739                        LogWriter.log( "   USER  : DB接続ユーザー(GE)" );
    740                        LogWriter.log( "   PASSWD: DB接続パスワード(GE)" );
    741                        LogWriter.log( "   URL   : DB接続JDBCドライバURL(jdbc:oracle:thin:@localhost:1521:HYBS)" );
    742                        LogWriter.log( "   TABLE : 登録するテーブルID(GE21)" );
    743                        LogWriter.log( "   FILE  : 登録するORACLE XDK 形式 XMLファイル(GE21.xml)" );
    744                        LogWriter.log( " [ ENCODE: ファイルのエンコード 初期値:UTF-8 ]" );
    745                        LogWriter.log( " [ DRIVER: JDBCドライバー 初期値:oracle.jdbc.OracleDriver ]" );
    746                        return ;
    747                }
    748
    749                final String user   = args[0] ;
    750                final String passwd = args[1] ;
    751                final String url    = args[2] ;
    752                final String table  = args[3] ;
    753                final String file   = args[4] ;
    754                final String encode = ( args.length == 6 ) ? args[5] : "UTF-8" ;
    755                final String driver = ( args.length == 7 ) ? args[6] : "oracle.jdbc.OracleDriver" ;
    756
    757                Class.forName(driver);
    758
    759                int insCnt;
    760                int updCnt;
    761                int delCnt;
    762                int ddlCnt;                                                                                     // 5.6.7.0 (2013/07/27) DDL処理件数追加
    763                // 6.4.2.1 (2016/02/05) try-with-resources 文
    764                try( Connection conn = DriverManager.getConnection( url,user,passwd ) ) {
    765                        conn.setAutoCommit( false );
    766                        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);    // 5.1.1.0 (2009/12/01)
    767                        final HybsXMLSave save = new HybsXMLSave( conn,table );
    768
    769                        // 6.4.2.1 (2016/02/05) try-with-resources 文
    770                        try( Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream( file ) ,encode ) ) ) {
    771                                save.insertXML( reader );
    772                                insCnt = save.getInsertCount();
    773                                updCnt = save.getUpdateCount();
    774                                delCnt = save.getDeleteCount();
    775                                ddlCnt = save.getDDLCount();                            // 5.6.7.0 (2013/07/27) DDL処理件数追加
    776                        }
    777                        // FileNotFoundException , UnsupportedEncodingException
    778                        catch( final java.io.FileNotFoundException ex ) {                               // catch は、close() されてから呼ばれます。
    779                                final String errMsg = "ファイルが存在しません。" + ex.getMessage()
    780                                                                + CR + "Table=[" + table + "] File =[" + file + "]" ;
    781                                throw new OgRuntimeException( errMsg,ex );
    782                        }
    783                        catch( final java.io.UnsupportedEncodingException ex ) {                // catch は、close() されてから呼ばれます。
    784                                final String errMsg = "指定のエンコードが存在しません。" + ex.getMessage()
    785                                                                + CR + "Table=[" + table + "] Encode =[" + encode + "]" ;
    786                                throw new OgRuntimeException( errMsg,ex );
    787                        }
    788                        catch( final java.io.IOException ex ) {                                         // catch は、close() されてから呼ばれます。
    789                                final String errMsg = "ファイル読み込み処理でエラーが発生しました。" + ex.getMessage()
    790                                                                + CR + "Table=[" + table + "] File =[" + file + "]" ;
    791                                throw new OgRuntimeException( errMsg,ex );
    792                        }
    793                        Closer.commit( conn );
    794                }
    795
    796                System.out.println( "XML File[" + file + "] Into [" + table + "] Table" );
    797                System.out.println( "   Insert Count : [" + insCnt + "]" );
    798                System.out.println( "   Update Count : [" + updCnt + "]" );
    799                System.out.println( "   Delete Count : [" + delCnt + "]" );
    800                System.out.println( "   DDL    Count : [" + ddlCnt + "]" );
    801        }
    802}