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="http://otn.oracle.co.jp/software/tech/xml/xdk/index.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="http://otn.oracle.co.jp/software/tech/xml/xdk/index.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部には、1つの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して、結果が0件なら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
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
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カウントを+1します。
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
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 し、結果が 0件の場合は、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
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}