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 * <ROWSET tableName="XX" > 076 * <EXEC_SQL> 最初に記載して、初期処理(データクリア等)を実行させる。 077 * delete from GEXX where YYYYY 078 * </EXEC_SQL> 079 * <MERGE_SQL> このSQL文で UPDATEして、結果が0件ならINSERTを行います。 080 * update GEXX set AA=[AA] , BB=[BB] where CC=[CC] 081 * </MERGE_SQL> 082 * <ROW num="1"> 083 * <カラム1>値1</カラム1> 084 * ・・・ 085 * <カラムn>値n</カラムn> 086 * </ROW> 087 * ・・・ 088 * <ROW num="n"> 089 * ・・・ 090 * </ROW> 091 * <EXEC_SQL> 最後に記載して、項目の設定(整合性登録)を行う。 092 * update GEXX set AA='XX' , BB='XX' where YYYYY 093 * </EXEC_SQL> 094 * <ROWSET> 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 * <ROWSET> タグの一番最初に呼び出されます。 181 * ROWSET の属性である、table 属性と、dbid 属性 を、TagElement の 182 * get メソッドで取得できます。 183 * 取得時のキーは、それぞれ、"TABLE" と "DBID" です。 184 * 185 * @og.rev 8.1.0.3 (2022/01/21) "tableName" を、HybsXMLHandler.ROWSET_TABLE に変更。 186 * 187 * @param tag タグエレメント 188 * @see org.opengion.fukurou.xml.TagElement 189 * @see HybsXMLHandler#setTagElementListener( TagElementListener ) 190 */ 191 @Override // TagElementListener 192 public void actionInit( final TagElement tag ) { 193// final String table = tag.get( "tableName" ); 194 final String table = tag.get( HybsXMLHandler.ROWSET_TABLE ); // 8.1.0.3 (2022/01/21) 195 if( table != null ) { tableName = table; } 196 } 197 198 /** 199 * <ROW> タグの endElement 処理毎に呼び出されます。 200 * この Listener をセットすることにより、行データを取得都度、 201 * TagElement オブジェクトを作成し、このメソッドが呼び出されます。 202 * 203 * @og.rev 4.0.0.0 (2007/05/09) ParameterMetaData を使用したパラメータ設定追加。 204 * @og.rev 4.0.0.0 (2007/09/25) isOracle から useParamMetaData に変更 205 * @og.rev 4.3.7.0 (2009/06/01) HSQLDB対応 206 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData setNull 対応(PostgreSQL対応) 207 * @og.rev 5.6.6.1 (2013/07/12) lastSQL 対応。デバッグ用に、最後に使用したSQL文を残します。 208 * 209 * @param tag タグエレメント 210 * @see org.opengion.fukurou.xml.TagElement 211 * @see HybsXMLHandler#setTagElementListener( TagElementListener ) 212 */ 213 @Override // TagElementListener 214 public void actionRow( final TagElement tag ) { 215 tag.setAfterMap( afterMap ); 216 217 String[] vals = null; // 5.6.6.1 (2013/07/12) デバッグ用 218 try { 219 // 更新SQL(MERGE_SQLタグ)が存在する場合の処理 220 int tempCnt = 0; 221 if( updPstmt != null ) { 222 vals = tag.getValues( updClms ); // 5.6.6.1 (2013/07/12) デバッグ用 223 for( int j=0; j<vals.length; j++ ) { 224 // 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え 225 if( vals[j] != null && vals[j].isEmpty() ){ 226 vals[j] = null; 227 } 228 229 // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加 230 if( useParamMetaData ) { 231 final int type = updMeta.getParameterType( j+1 ); 232 // 5.3.8.0 (2011/08/01) setNull 対応 233 final String val = vals[j]; 234 if( val == null || val.isEmpty() ) { 235 updPstmt.setNull( j+1, type ); 236 } 237 else { 238 updPstmt.setObject( j+1, val, type ); 239 } 240 } 241 else { 242 updPstmt.setObject( j+1,vals[j] ); 243 } 244 } 245 tempCnt = updPstmt.executeUpdate(); 246 if( tempCnt > 1 ) { 247 final String errMsg = "Update キーが重複しています。" 248 + "TABLE=[" + tableName + "] ROW=[" 249 + tag.getRowNo() + "]" + CR 250 + " SQL=[" + lastSQL + "]" + CR // 5.6.6.1 (2013/07/12) デバッグ用 251 + tag.toString() + CR 252 + Arrays.toString( vals ) + CR ; // 5.6.6.1 (2013/07/12) デバッグ用 253 throw new OgRuntimeException( errMsg ); 254 } 255 updCnt += tempCnt; 256 } 257 // 更新が 0件の場合は、INSERT処理を行います。 258 if( tempCnt == 0 ) { 259 // 初回INSERT時のタグより、DB登録SQL文を構築します。 260 if( insPstmt == null ) { 261 insClms = tag.getKeys(); 262 lastSQL = insertSQL( insClms,tableName ); // 5.6.6.1 (2013/07/12) デバッグ用 263 insPstmt = connection.prepareStatement( lastSQL ); 264 // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加 265 if( useParamMetaData ) { insMeta = insPstmt.getParameterMetaData(); } 266 } 267 vals = tag.getValues( insClms ); // 5.6.6.1 (2013/07/12) デバッグ用 268 for( int j=0; j<vals.length; j++ ) { 269 // 4.3.7.0 (2009/06/01) HSQLDB対応。空文字の場合nullに置換え 270 if( vals[j] != null && vals[j].isEmpty() ){ 271 vals[j] = null; 272 } 273 274 // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加 275 if( useParamMetaData ) { 276 final int type = insMeta.getParameterType( j+1 ); 277 // 5.3.8.0 (2011/08/01) setNull 対応 278 final String val = vals[j]; 279 if( val == null || val.isEmpty() ) { 280 insPstmt.setNull( j+1, type ); 281 } 282 else { 283 insPstmt.setObject( j+1, val, type ); 284 } 285 } 286 else { 287 insPstmt.setObject( j+1,vals[j] ); 288 } 289 } 290 insCnt += insPstmt.executeUpdate(); 291 } 292 } 293 catch( final SQLException ex ) { 294 final String errMsg = "DB登録エラーが発生しました。" 295 + "TABLE=[" + tableName + "] ROW=[" 296 + tag.getRowNo() + "]" + CR 297 + " SQL=[" + lastSQL + "]" + CR // 5.6.6.1 (2013/07/12) デバッグ用 298 + tag.toString() + CR 299 + Arrays.toString( vals ) + CR // 5.6.6.1 (2013/07/12) デバッグ用 300 + ex.getMessage() + ":" + ex.getSQLState() + CR ; 301 throw new OgRuntimeException( errMsg,ex ); 302 } 303 } 304 305 /** 306 * <EXEC_SQL> タグの 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 // TagElementListener 328 public void actionExecSQL( final TagElement tag ) { 329 // 6.4.2.1 (2016/02/05) try-with-resources 文 330 lastSQL = tag.getBody(); // 5.6.6.1 (2013/07/12) デバッグ用 6.4.2.1 (2016/02/05) try の前に出します。 331 try( Statement execSQL = connection.createStatement() ) { 332 // 5.6.7.0 (2013/07/27) DDL(データ定義言語:Data Definition Language)の処理件数追加 333 // 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。 334// final String[] sqls = getExecSQLs( lastSQL ) ; 335 final List<String> sqls = getExecSQLs( lastSQL ) ; // 8.1.0.3 (2022/01/21) Listに変更 336 337 // 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。 338 // SQL文が、2個以上あり、exists属性が存在する場合のみ、最初のSQL文を実行して判定する。 339 if( sqls.size() > 1 ) { 340 final String exists = tag.get( HybsXMLHandler.EXEC_EXISTS ); // "0"か、"1"(!=0)か 341 if( exists != null && exists.length() > 0 ) { 342 final boolean isZero = exists.charAt(0) == '0' ; 343 lastSQL = sqls.remove(0); // 先頭のSQL文を取り出し、lastSQL に設定 344 try( ResultSet resultSet = execSQL.executeQuery( lastSQL ) ) { 345 if( resultSet.next() ) { 346 final int rtnCnt = resultSet.getInt(1); 347 // exists=='0' と、カウント==0 のXORが true の場合(つまり、条件が不一致の場合)は、抜ける。 348 if( isZero ^ rtnCnt == 0 ) { return; } 349 } 350 } 351 } 352 } 353 354 for( final String sql : sqls ) { 355 // 8.1.0.3 (2022/01/21) EXEC_SQLで、『;』分割時に、ゼロ文字列が含まれるかもしれない。 356 if( sql.trim().isEmpty() ) { continue; } // 本当は、trim() は必要ない。 357 358 lastSQL = sql; // 8.1.0.3 (2022/01/21) lastSQL に設定 359 final int cnt = execSQL.executeUpdate( sql ) ; 360 361 // 件数カウント用 362 final String upSQL = sql.trim().toUpperCase( Locale.JAPAN ); 363 if( upSQL.startsWith( "DELETE" ) ) { delCnt += cnt; } 364 else if( upSQL.startsWith( "INSERT" ) ) { insCnt += cnt; } 365 else if( upSQL.startsWith( "UPDATE" ) ) { updCnt += cnt; } 366 else { ddlCnt ++ ; } // DLLの場合は、件数=0が返される。 367 } 368 } 369 catch( final SQLException ex ) { // catch は、close() されてから呼ばれます。 370 final String errMsg = "DB登録エラーが発生しました。" 371 + "TABLE=[" + tableName + "] ROW=[" 372 + tag.getRowNo() + "]" + CR 373 + " SQL=[" + lastSQL + "]" + CR // 5.6.6.1 (2013/07/12) デバッグ用 374 + tag.toString() + CR 375 + ex.getMessage() + ":" + ex.getSQLState() + CR ; 376 377 // 5.6.9.2 (2013/10/18) EXEC_SQL のエラー時に Exception を発行するかどうかを指定 378 if( isExecErr ) { // 6.0.2.5 (2014/10/31) refactoring 379 throw new OgRuntimeException( errMsg,ex ); 380 } 381 else { 382 System.err.println( errMsg ); 383 errBuf.append( errMsg ); 384 } 385 } 386 } 387 388 /** 389 * EXEC_SQLで、";" で複数のSQL文に分割します。 390 * 391 * 厳密に処理していません。 392 * SQL文の中に文字として";"が使われている場合の考慮がされていません。 393 * 394 * 7.3.2.0 (2021/03/19) 395 * 暫定的に、BEGIN~END 構文(大文字のみ)を持つ場合は、";" で複数のSQL文に分割しません。 396 * 397 * 8.1.0.3 (2022/01/21) 398 * ・先頭行が 『SELECT』の場合は、";" で分割する。 399 * ・先頭行が 『CREATE』で、FUNCTION、PACKAGE、PROCEDURE、TRIGGER を含む場合は、分割しない。 400 * つまり、それ以降の分割判定は行わないため、後続に複数SQL文は記述できません。 401 * ・上記以外の場合は、";" で分割する。 402 * 403 * @og.rev 7.0.1.3 (2018/11/12) EXEC_SQLで、";" で複数のSQL文に分割、実行します。 404 * @og.rev 7.3.2.0 (2021/03/19) TRIGGER など、BEGIN~END 構文を持つ場合は、";" で複数のSQL文に分割しません。 405 * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。 406 * 407 * @param sqlText EXEC_SQL内部に書かれたSQL文 408 * 409 * @return 分割されたSQL文のList 410 */ 411// private String[] getExecSQLs( final String sqlText ) { 412 private List<String> getExecSQLs( final String sqlText ) { // List に変更 413 final List<String> sqlList = new ArrayList<>(); 414 415 final String orgStr = sqlText.trim(); 416 final String uppStr = orgStr.toUpperCase( Locale.JAPAN ); // 判定用 417 418 int st = 0; 419 while( st < orgStr.length() ) { 420 final int ed = orgStr.indexOf( ';',st ); 421 422 if( ed < 0 ) { 423 sqlList.add( orgStr.substring( st ).trim() ); // trim() したSQL文を返す。 424 break; 425 } 426 else { 427 final String sql = uppStr.substring( st,ed ).trim(); // 大文字で判定(先頭~; まで) 428 429 if( sql.startsWith( "SELECT" ) ) { // 大文字で先頭比較 430 sqlList.add( orgStr.substring( st,ed ).trim() ); // 先頭~; までを登録 431 } 432 else { 433 if( sql.startsWith( "CREATE" ) && ( 434 sql.contains( "FUNCTION" ) || sql.contains( "PACKAGE" ) || 435 sql.contains( "PROCEDURE" ) || sql.contains( "TRIGGER" ) ) ) { 436 sqlList.add( orgStr.substring( st ).trim() ); // 残りすべてを登録 437 break; 438 } 439 else { 440 sqlList.add( orgStr.substring( st,ed ).trim() ); // 部分先頭 ~ ; までを登録 441 } 442 } 443 } 444 st = ed + 1; 445 } 446 447 return sqlList ; 448 449// if( sqlText.contains( "BEGIN" ) && sqlText.contains( "END" ) ) { // 7.3.2.0 (2021/03/19) 450// return new String[] { sqlText }; 451// } 452// else { 453// return sqlText.split( ";" ); 454// } 455 } 456 457 /** 458 * <MERGE_SQL> タグの 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 // TagElementListener 479 public void actionMergeSQL( final TagElement tag ) { 480 if( updPstmt != null ) { 481 final String errMsg = "MERGE_SQLタグが、複数回記述されています。" 482 + "TABLE=[" + tableName + "] ROW=[" 483 + tag.getRowNo() + "]" + CR 484 + " SQL=[" + lastSQL + "]" + CR // 5.6.6.1 (2013/07/12) デバッグ用 485 + tag.toString() + CR; 486 throw new OgRuntimeException( errMsg ); 487 } 488 489 final String orgSql = tag.getBody(); 490 final Matcher matcher = PATTERN.matcher( orgSql ); 491 updClms = new ArrayList<>(); 492 while( matcher.find() ) { 493 // ここでは、[XXX]にマッチする為、前後の[]を取り除きます。 494 updClms.add( orgSql.substring( matcher.start()+1,matcher.end()-1 ) ); 495 } 496 lastSQL = matcher.replaceAll( "?" ); // 5.6.6.1 (2013/07/12) デバッグ用 497 498 try { 499 updPstmt = connection.prepareStatement( lastSQL ); 500 // 4.0.0.0 (2007/09/25) ParameterMetaData を使用したパラメータ設定追加 501 if( useParamMetaData ) { updMeta = updPstmt.getParameterMetaData(); } 502 } 503 catch( final SQLException ex ) { 504 final String errMsg = "Statement作成時にエラーが発生しました。" 505 + "TABLE=[" + tableName + "] ROW=[" 506 + tag.getRowNo() + "]" + CR 507 + " SQL=[" + lastSQL + "]" + CR // 5.6.6.1 (2013/07/12) デバッグ用 508 + tag.toString() + CR 509 + ex.getMessage() + ":" + ex.getSQLState() + CR ; 510 throw new OgRuntimeException( errMsg,ex ); 511 } 512 } 513 514 // /** 515 // * UPDATE,DELETE を行う場合の WHERE 条件になるキー配列 516 // * このキーの AND 条件でカラムを特定し、UPDATE,DELETE などの処理を 517 // * 行います。 518 // * 519 // * @og.rev 6.3.9.0 (2015/11/06) 現時点で使われていないため、一旦取り消しておきます。 520 // * 521 // * @param keyCols WHERE条件になるキー配列(可変長引数) 522 // */ 523 // public void setKeyColumns( final String... keyCols ) { 524 // keyColumns = new String[keyCols.length]; 525 // System.arraycopy( keyCols,0,keyColumns,0,keyColumns.length ); 526 // } 527 528 /** 529 * XMLファイルを読み取る前に指定するカラムと値のペア(マップ)情報をセットします。 530 * 531 * このカラムと値のペアのマップは、オブジェクト構築前に設定される為、 532 * XMLファイルにキーが存在している場合は、値が書き変わります。(XML優先) 533 * XMLファイルにキーが存在していない場合は、ここで指定するMapの値が 534 * 初期設定値として使用されます。 535 * ここで指定する Map に LinkedHashMap を使用する場合、カラム順も 536 * 指定することが出来ます。 537 * 538 * @param map 初期設定するカラムデータマップ 539 * @see #setAfterMap( Map ) 540 */ 541 public void setDefaultMap( final Map<String,String> map ) { defaultMap = map; } 542 543 /** 544 * XMLファイルを読み取った後で指定するカラムと値のペア(マップ)情報をセットします。 545 * 546 * このカラムと値のペアのマップは、オブジェクト構築後に設定される為、 547 * XMLファイルのキーの存在に関係なく、Mapのキーと値が使用されます。(Map優先) 548 * null を設定した場合は、なにも処理されません。 549 * 550 * @param map 後設定するカラムデータマップ 551 * @see #setDefaultMap( Map ) 552 */ 553 public void setAfterMap( final Map<String,String> map ) { afterMap = map; } 554 555 /** 556 * データベースに追加処理(INSERT)を行います。 557 * 558 * 先に指定されたコネクションを用いて、指定のテーブルに INSERT します。 559 * 引数には、XMLファイルを指定したリーダーをセットします。 560 * コネクションは、終了後、コミットされます。(close されません。) 561 * リーダーのクローズは、ここでは行っていません。 562 * 563 * @og.rev 5.1.1.0 (2009/11/11) insMeta , updMeta のクリア(気休め) 564 * 565 * @param reader XMLファイルを指定するリーダー 566 */ 567 public void insertXML( final Reader reader ) { 568 try { 569 final HybsXMLHandler handler = new HybsXMLHandler(); 570 handler.setTagElementListener( this ); 571 handler.setDefaultMap( defaultMap ); 572 573 handler.parse( reader ); 574 } 575 finally { 576 Closer.stmtClose( insPstmt ); 577 Closer.stmtClose( updPstmt ); 578 insPstmt = null; 579 updPstmt = null; 580 insMeta = null; // 5.1.1.0 (2009/11/11) 581 updMeta = null; // 5.1.1.0 (2009/11/11) 582 } 583 } 584 585 /** 586 * インサート用のSQL文を作成します。 587 * 588 * @og.rev 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。 589 * 590 * @param columns インサートするカラム名 591 * @param tableName インサートするテーブル名 592 * 593 * @return インサート用のSQL文 594 * @og.rtnNotNull 595 */ 596 private String insertSQL( final String[] columns,final String tableName ) { 597 if( tableName == null ) { 598 final String errMsg = "tableName がセットされていません。" + CR 599 + "tableName は、コンストラクタで指定するか、ROWSETのtableName属性で" 600 + "指定しておく必要があります" + CR ; 601 throw new OgRuntimeException( errMsg ); 602 } 603 604 // 6.0.2.5 (2014/10/31) char を append する。 605 // 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。 606 final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE ) 607 .append( "INSERT INTO " ).append( tableName ) 608 .append( " ( " ) 609 .append( String.join( "," , columns ) ) // 6.2.3.0 (2015/05/01) 610 .append( " ) VALUES ( ?" ); 611 for( int i=1; i<columns.length; i++ ) { 612 sql.append( ",?" ); 613 } 614 sql.append( " )" ); 615 616 return sql.toString(); 617 } 618 619 /** 620 * データベースに追加した件数を返します。 621 * 622 * @return 登録件数 623 */ 624 public int getInsertCount() { return insCnt; } 625 626 /** 627 * データベースを更新した件数を返します。 628 * これは、拡張XDK形式で、MERGE_SQL タグを使用した場合の更新処理件数を 629 * 合計した値を返します。 630 * 631 * @return 更新件数 632 */ 633 public int getUpdateCount() { return updCnt; } 634 635 /** 636 * データベースに変更(更新、削除を含む)した件数を返します。 637 * これは、拡張XDK形式で、EXEC_SQL タグを使用した場合の実行件数を合計した 638 * 値を返します。 639 * よって、更新か、追加か、削除かは、判りませんが、通常 登録前に削除する 640 * ケースで使われることから、deleteCount としています。 641 * 642 * @return 変更件数(主に、削除件数) 643 */ 644 public int getDeleteCount() { return delCnt; } 645 646 /** 647 * データベースにDDL(データ定義言語:Data Definition Language)処理した件数を返します。 648 * これは、拡張XDK形式で、EXEC_SQL タグを使用した場合の実行件数を合計した 649 * 値を返します。 650 * EXEC_SQL では、登録前に削除する delete 処理も、EXEC_SQL タグを使用して実行しますが 651 * その処理と分けてカウントします。 652 * 653 * @og.rev 5.6.7.0 (2013/07/27) DDL(データ定義言語:Data Definition Language)の処理件数追加 654 * 655 * @return DDL(データ定義言語:Data Definition Language)処理した件数 656 */ 657 public int getDDLCount() { return ddlCnt; } 658 659 /** 660 * 実際に登録された テーブル名を返します。 661 * 662 * テーブル名は、拡張XDK形式のROWSETタグのtableName属性に 663 * 記述しておくか、コンストラクターで引数として渡します。 664 * 両方指定された場合は、ROWSETタグのtableName属性が優先されます。 665 * ここでの返り値は、実際に使用された テーブル名です。 666 * 667 * @return テーブル名 668 */ 669 public String getTableName() { return tableName; } 670 671 /** 672 * isExecErr でfalseを指定した場合に、エラー内容の文字列を取り出します。 673 * エラーが発生しなかった場合は、ゼロ文字列が返ります。 674 * 675 * @og.rev 7.3.2.0 (2021/03/19) isExecErr でfalseを指定した場合に、エラー内容の文字列を取り出します。 676 * 677 * @return エラー内容の文字列 678 */ 679 public String getErrorMessage() { return errBuf.toString(); } 680 681 /** 682 * この接続が、PreparedStatement#getParameterMetaData() を使用するかどうかを判定します。 683 * 本来は、ConnectionFactory#useParameterMetaData(String)を使うべきだが、dbid が無いため、直接取得します。 684 * 685 * ※ 6.1.0.0 (2014/12/26) で、直接取得に変更します。DBUtil 経由で取得する方が、ソースコードレベルでの 686 * 共通化になるので良いのですが、org.opengion.fukurou.db と、org.opengion.fukurou.xml パッケージが 687 * 循環参照(相互参照)になるため、どちらかを切り離す必要があります。 688 * db パッケージ側では、DBConfig.xml の処理の関係で、org.opengion.fukurou.xml.DomParser を 689 * 使っているため、こちらの処理を、内部処理に変更することで、対応します。 690 * 691 * @og.rev 5.3.8.0 (2011/08/01) 新規作成 ( ApplicationInfo#useParameterMetaData(Connection) からコピー ) 692 * @og.rev 5.6.7.0 (2013/07/27) dbProductName は、DBUtil 経由で取得する。 693 * @og.rev 6.1.0.0 (2014/12/26) dbProductName は、DBUtil 経由ではなく、直接取得する。 694 * 695 * @param conn 接続先(コネクション) 696 * 697 * @return 使用する場合:true / その他:false 698 */ 699 private static boolean useParameterMetaData( final Connection conn ) { 700 701 String dbName ; 702 try { 703 dbName = conn.getMetaData().getDatabaseProductName().toLowerCase( Locale.JAPAN ); 704 } 705 catch( final SQLException ex ) { 706 dbName = "none"; 707 } 708 709 return "PostgreSQL".equalsIgnoreCase( dbName ) ; 710 } 711 712 /** 713 * テスト用のメインメソッド 714 * 715 * Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER] 716 * USER : DB接続ユーザー(GE) 717 * PASSWD : DB接続パスワード(GE) 718 * URL : DB接続JDBCドライバURL(jdbc:oracle:thin:@localhost:1521:HYBS 719 * TABLE : 登録するテーブルID(GE21) 720 * FILE : 登録するORACLE XDK 形式 XMLファイル(GE21.xml) 721 * [ENCODE]: ファイルのエンコード 初期値:UTF-8 722 * [DRIVER]: JDBCドライバー 初期値:oracle.jdbc.OracleDriver 723 * 724 * ※ ファイルが存在しなかった場合、FileNotFoundException を RuntimeException に変換して、throw します。 725 * ※ 指定のエンコードが存在しなかった場合、UnsupportedEncodingException を RuntimeException に変換して、throw します。 726 * 727 * @og.rev 5.1.1.0 (2009/12/01) MySQL対応 明示的に、TRANSACTION_READ_COMMITTED を指定する。 728 * @og.rev 5.6.7.0 (2013/07/27) DDL(データ定義言語:Data Definition Language)の処理件数追加 729 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。 730 * 731 * @param args コマンド引数配列 732 * @throws ClassNotFoundException クラスを見つけることができなかった場合。 733 * @throws SQLException データベース接続エラーが発生した場合。 734 */ 735 public static void main( final String[] args ) 736 throws ClassNotFoundException , SQLException { 737 if( args.length < 5 ) { 738 LogWriter.log( "Usage: java org.opengion.fukurou.xml.HybsXMLSave USER PASSWD URL TABLE FILE [ENCODE] [DRIVER]" ); 739 LogWriter.log( " USER : DB接続ユーザー(GE)" ); 740 LogWriter.log( " PASSWD: DB接続パスワード(GE)" ); 741 LogWriter.log( " URL : DB接続JDBCドライバURL(jdbc:oracle:thin:@localhost:1521:HYBS)" ); 742 LogWriter.log( " TABLE : 登録するテーブルID(GE21)" ); 743 LogWriter.log( " FILE : 登録するORACLE XDK 形式 XMLファイル(GE21.xml)" ); 744 LogWriter.log( " [ ENCODE: ファイルのエンコード 初期値:UTF-8 ]" ); 745 LogWriter.log( " [ DRIVER: JDBCドライバー 初期値:oracle.jdbc.OracleDriver ]" ); 746 return ; 747 } 748 749 final String user = args[0] ; 750 final String passwd = args[1] ; 751 final String url = args[2] ; 752 final String table = args[3] ; 753 final String file = args[4] ; 754 final String encode = ( args.length == 6 ) ? args[5] : "UTF-8" ; 755 final String driver = ( args.length == 7 ) ? args[6] : "oracle.jdbc.OracleDriver" ; 756 757 Class.forName(driver); 758 759 int insCnt; 760 int updCnt; 761 int delCnt; 762 int ddlCnt; // 5.6.7.0 (2013/07/27) DDL処理件数追加 763 // 6.4.2.1 (2016/02/05) try-with-resources 文 764 try( Connection conn = DriverManager.getConnection( url,user,passwd ) ) { 765 conn.setAutoCommit( false ); 766 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 5.1.1.0 (2009/12/01) 767 final HybsXMLSave save = new HybsXMLSave( conn,table ); 768 769 // 6.4.2.1 (2016/02/05) try-with-resources 文 770 try( Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream( file ) ,encode ) ) ) { 771 save.insertXML( reader ); 772 insCnt = save.getInsertCount(); 773 updCnt = save.getUpdateCount(); 774 delCnt = save.getDeleteCount(); 775 ddlCnt = save.getDDLCount(); // 5.6.7.0 (2013/07/27) DDL処理件数追加 776 } 777 // FileNotFoundException , UnsupportedEncodingException 778 catch( final java.io.FileNotFoundException ex ) { // catch は、close() されてから呼ばれます。 779 final String errMsg = "ファイルが存在しません。" + ex.getMessage() 780 + CR + "Table=[" + table + "] File =[" + file + "]" ; 781 throw new OgRuntimeException( errMsg,ex ); 782 } 783 catch( final java.io.UnsupportedEncodingException ex ) { // catch は、close() されてから呼ばれます。 784 final String errMsg = "指定のエンコードが存在しません。" + ex.getMessage() 785 + CR + "Table=[" + table + "] Encode =[" + encode + "]" ; 786 throw new OgRuntimeException( errMsg,ex ); 787 } 788 catch( final java.io.IOException ex ) { // catch は、close() されてから呼ばれます。 789 final String errMsg = "ファイル読み込み処理でエラーが発生しました。" + ex.getMessage() 790 + CR + "Table=[" + table + "] File =[" + file + "]" ; 791 throw new OgRuntimeException( errMsg,ex ); 792 } 793 Closer.commit( conn ); 794 } 795 796 System.out.println( "XML File[" + file + "] Into [" + table + "] Table" ); 797 System.out.println( " Insert Count : [" + insCnt + "]" ); 798 System.out.println( " Update Count : [" + updCnt + "]" ); 799 System.out.println( " Delete Count : [" + delCnt + "]" ); 800 System.out.println( " DDL Count : [" + ddlCnt + "]" ); 801 } 802}