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