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