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