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.process; 017 018import org.opengion.fukurou.system.OgRuntimeException ; // 6.4.2.0 (2016/01/29) 019import org.opengion.fukurou.util.Argument; 020import org.opengion.fukurou.util.SystemParameter; 021import org.opengion.fukurou.util.FileUtil; 022import org.opengion.fukurou.util.HybsDateUtil; 023import org.opengion.fukurou.system.LogWriter; 024import org.opengion.fukurou.util.HybsEntry ; 025import org.opengion.fukurou.system.Closer; 026import org.opengion.fukurou.model.Formatter; 027import org.opengion.fukurou.db.DBUtil ; 028import org.opengion.fukurou.db.ConnectionFactory; 029 030import java.io.File ; 031import java.io.PrintWriter ; 032import java.util.Map ; 033import java.util.LinkedHashMap ; 034import java.util.Calendar ; 035 036import java.sql.Connection; 037import java.sql.ResultSet; 038import java.sql.PreparedStatement; 039import java.sql.SQLException; 040 041/** 042 * Process_DBFileout は、SELECT文 を指定し データベースの値を抜き出して、 043 * 個々のファイルにセーブする、ChainProcess インターフェースの実装クラスです。 044 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から 045 * 受け取った LineModel を元に、1行単位に、SELECT文を実行します。 046 * 047 * 上流のカラムを、[カラム]変数で使用できます。 048 * また、セーブするファイル名、更新日付等も、都度、更新可能です。 049 * 050 * データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に 051 * 設定された接続(Connection)を使用します。 052 * 053 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。 054 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に 055 * 繋げてください。 056 * 057 * SQL文には、{@DATE.YMDH}等のシステム変数が使用できます。 058 * 059 * @og.formSample 060 * Process_DBFileout -dbid=DBGE -insertTable=GE41 061 * 062 * [ -dbid=DB接続ID ] : -dbid=DBGE (例: Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定) 063 * [ -select=検索SQL文 ] : -select="SELECT * FROM GE41 WHERE SYSTEM_ID = [SYSTEM_ID] AND CLM = [CLM]" 064 * [ -selectFile=登録SQLファイル ] : -selectFile=select.sql 065 * : -select や -selectFile が指定されない場合は、エラーです。 066 * [ -select_XXXX=固定値 ] : -select_SYSTEM_ID=GE 067 * SQL文中の{@XXXX}文字列を指定の固定値で置き換えます。 068 * WHERE SYSTEM_ID='{@SYSTEM_ID}' ⇒ WHERE SYSTEM_ID='GE' 069 * [ -const_XXXX=固定値 ] : -const_FGJ=1 070 * LineModel のキー(const_ に続く文字列)の値に、固定値を設定します。 071 * キーが異なれば、複数のカラム名を指定できます。 072 * [ -addHeader=ヘッダー ] : -addHeader="CREATE OR REPLACE " 073 * [ -addFooter=フッター ] : -addFooter="/\nSHOW ERROR;" 074 * [ -outFile=出力ファイル名 ] : -outFile=[NAME].sql 075 * [ -append=[false/true] ] : 出力ファイルを、追記する(true)か新規作成する(false)か。 076 * [ -sep=セパレータ文字 ] : 各カラムを区切る文字列(初期値:TAB) 077 * [ -useLineCR=[false/true] ] : 各行の最後に、改行文字をつかるかどうか(初期値:true[付ける]) 078 * [ -timestamp=更新日付 ] : -timestamp="LAST_DDL_TIME" 079 * [ -fetchSize=1000 ] :フェッチする行数(初期値:1000) 6.9.4.1 (2018/04/09) 080 * [ -display=[false/true] ] : 結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 081 * [ -debug=[false/true] ] : デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 082 * 083 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 084 * 085 * @version 4.0 086 * @author Kazuhiko Hasegawa 087 * @since JDK5.0, 088 */ 089public class Process_DBFileout extends AbstractProcess implements ChainProcess { 090 private static final String SELECT_KEY = "select_" ; 091 private static final String CNST_KEY = "const_" ; 092 093 private static final String ENCODE = "UTF-8" ; 094 095// /** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ {@value} */ 096// private static final int DB_FETCH_SIZE = 1001 ; 097 098 private Connection connection ; 099 private PreparedStatement selPstmt ; 100 101 private String dbid ; 102 private String select ; 103 private int[] selClmNos ; // select 時のファイルのヘッダーのカラム番号 104 private String outFilename ; // 出力ファイル名 105 private boolean append ; // ファイル追加(true:追加/false:通常) 106 private String timestamp ; // 出力ファイルの更新日時 107 private int tmstmpClm = -1; // 出力ファイルの更新日時のカラム番号 108 private String separator = "\t"; // 各カラムを区切る文字列(初期値:TAB) 109 private String addHeader ; // ヘッダー 110 private String addFooter ; // フッター 111 private boolean useLineCR = true; // 各行の最後に、改行文字をつかるかどうか(初期値:true[付ける]) 112 private int fetchSize = 1000; // 6.9.4.1 (2018/04/09) 初期値を 1000 に設定 113 private boolean display ; // false:表示しない 114 private boolean debug ; // 5.7.3.0 (2014/02/07) デバッグ情報 115 116 private String[] cnstClm ; // 固定値を設定するカラム名 117 private int[] cnstClmNos ; // 固定値を設定するカラム番号 118 private String[] constVal ; // カラム番号に対応した固定値 119 120 private boolean firstRow = true; // 最初の一行目 121 private int count ; 122 123 /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */ 124 private static final Map<String,String> MUST_PROPARTY ; // [プロパティ]必須チェック用 Map 125 /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */ 126 private static final Map<String,String> USABLE_PROPARTY ; // [プロパティ]整合性チェック Map 127 128 static { 129 MUST_PROPARTY = new LinkedHashMap<>(); 130 131 USABLE_PROPARTY = new LinkedHashMap<>(); 132 USABLE_PROPARTY.put( "dbid", "Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定" ); 133 USABLE_PROPARTY.put( "select", "検索SQL文(select or selectFile 必須)" + 134 CR + "例: \"SELECT * FROM GE41 WHERE SYSTEM_ID = [SYSTEM_ID] AND CLM = [CLM]\"" ); 135 USABLE_PROPARTY.put( "selectFile", "検索SQLファイル(select or selectFile 必須)例: select.sql" ); 136 USABLE_PROPARTY.put( "select_", "SQL文中の{@XXXX}文字列を指定の固定値で置き換えます。" + 137 CR + "WHERE SYSTEM_ID='{@SYSTEM_ID}' ⇒ WHERE SYSTEM_ID='GE'" ); 138 USABLE_PROPARTY.put( "const_", "LineModel のキー(const_ に続く文字列)の値に、固定値を" + 139 CR + "設定します。キーが異なれば、複数のカラム名を指定できます。" + 140 CR + "例: -sql_SYSTEM_ID=GE" ); 141 USABLE_PROPARTY.put( "addHeader" , "ヘッダー" ); 142 USABLE_PROPARTY.put( "addFooter" , "フッター" ); 143 USABLE_PROPARTY.put( "outFile" , "出力ファイル名 例: [NAME].sql" ); 144 USABLE_PROPARTY.put( "append" , "出力ファイルを、追記する(true)か新規作成する(false)か。" ); 145 USABLE_PROPARTY.put( "sep" , "各カラムを区切る文字列(初期値:TAB)" ); 146 USABLE_PROPARTY.put( "useLineCR", "各行の最後に、改行文字をつかるかどうか(初期値:true[付ける])" ); 147 USABLE_PROPARTY.put( "timestamp", "出力ファイルの更新日付例: [LAST_DDL_TIME]" ); 148 USABLE_PROPARTY.put( "fetchSize","フェッチする行数 (初期値:1000)" ); // 6.9.4.1 (2018/04/09) 初期値を 1000 に設定 149 USABLE_PROPARTY.put( "display", "結果を標準出力に表示する(true)かしない(false)か" + 150 CR + "(初期値:false:表示しない)" ); 151 USABLE_PROPARTY.put( "debug", "デバッグ情報を標準出力に表示する(true)かしない(false)か" + 152 CR + "(初期値:false:表示しない)" ); // 5.7.3.0 (2014/02/07) デバッグ情報 153 } 154 155 /** 156 * デフォルトコンストラクター。 157 * このクラスは、動的作成されます。デフォルトコンストラクターで、 158 * super クラスに対して、必要な初期化を行っておきます。 159 * 160 */ 161 public Process_DBFileout() { 162 super( "org.opengion.fukurou.process.Process_DBFileout",MUST_PROPARTY,USABLE_PROPARTY ); 163 } 164 165 /** 166 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。 167 * 初期処理(ファイルオープン、DBオープン等)に使用します。 168 * 169 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 170 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。 171 * 172 * @param paramProcess データベースの接続先情報などを持っているオブジェクト 173 */ 174 public void init( final ParamProcess paramProcess ) { 175 final Argument arg = getArgument(); 176 177 select = arg.getFileProparty( "select","selectFile",false ); 178 separator = arg.getProparty( "sep" , separator ); 179 outFilename = arg.getProparty( "outFile" , outFilename ); 180 append = arg.getProparty( "append" , append ); 181 addHeader = arg.getProparty( "addHeader" , addHeader ); 182 addFooter = arg.getProparty( "addFooter" , addFooter ); 183 useLineCR = arg.getProparty( "useLineCR" , useLineCR ); 184 timestamp = arg.getProparty( "timestamp" , timestamp ); 185 fetchSize = arg.getProparty( "fetchSize" , fetchSize ); // 6.9.4.1 (2018/04/09) fetchSize 指定 186 display = arg.getProparty( "display" , display ); 187 debug = arg.getProparty( "debug" , debug ); 188 189 addHeader = addHeader.replaceAll( "\\\\t" , "\t" ).replaceAll( "\\\\n" , "\n" ); // 「\t」と、「\n」の文字列を、タブと改行に変換します。 190 addFooter = addFooter.replaceAll( "\\\\t" , "\t" ).replaceAll( "\\\\n" , "\n" ); // 「\t」と、「\n」の文字列を、タブと改行に変換します。 191 192 dbid = arg.getProparty( "dbid" ); 193 connection = paramProcess.getConnection( dbid ); 194 195 if( select == null ) { 196 final String errMsg = "select または、selectFile は必ず指定してください。"; 197 throw new OgRuntimeException( errMsg ); 198 } 199 200 // 3.8.0.1 (2005/06/17) {@DATE.XXXX} 変換処理の追加 201 // {@DATE.YMDH} などの文字列を、yyyyMMddHHmmss 型の日付に置き換えます。 202 // SQL文の {@XXXX} 文字列の固定値への置き換え 203 final HybsEntry[] entry =arg.getEntrys(SELECT_KEY); // 配列 204 final SystemParameter sysParam = new SystemParameter( select ); 205 select = sysParam.replace( entry ); 206 207 final HybsEntry[] cnstKey = arg.getEntrys( CNST_KEY ); // 配列 208 final int csize = cnstKey.length; 209 cnstClm = new String[csize]; 210 constVal = new String[csize]; 211 for( int i=0; i<csize; i++ ) { 212 cnstClm[i] = cnstKey[i].getKey(); 213 constVal[i] = cnstKey[i].getValue(); 214 } 215 } 216 217 /** 218 * プロセスの終了を行います。最後に一度だけ、呼び出されます。 219 * 終了処理(ファイルクローズ、DBクローズ等)に使用します。 220 * 221 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 222 * 223 * @param isOK トータルで、OKだったかどうか[true:成功/false:失敗] 224 */ 225 public void end( final boolean isOK ) { 226 final boolean flag1 = Closer.stmtClose( selPstmt ); 227 selPstmt = null; 228 229 // close に失敗しているのに commit しても良いのか? 230 if( isOK ) { 231 Closer.commit( connection ); 232 } 233 else { 234 Closer.rollback( connection ); 235 } 236 ConnectionFactory.remove( connection,dbid ); 237 238 if( ! flag1 ) { 239 final String errMsg = "select ステートメントをクローズ出来ません。" + CR 240 + " select=[" + select + "] , commit=[" + isOK + "]" ; 241 System.err.println( errMsg ); 242 } 243 } 244 245 /** 246 * 引数の LineModel を処理するメソッドです。 247 * 変換処理後の LineModel を返します。 248 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、 249 * null データを返します。つまり、null データは、後続処理を行わない 250 * フラグの代わりにも使用しています。 251 * なお、変換処理後の LineModel と、オリジナルの LineModel が、 252 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。 253 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、 254 * 各処理ごとに自分でコピー(クローン)して下さい。 255 * 256 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 257 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。 258 * @og.rev 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド(mkdirs) 259 * 260 * @param data オリジナルのLineModel 261 * 262 * @return 処理変換後のLineModel 263 */ 264 @Override // ChainProcess 265 public LineModel action( final LineModel data ) { 266 count++ ; 267 try { 268 if( firstRow ) { 269 makePrepareStatement( data ); 270 271 final int size = cnstClm.length; 272 cnstClmNos = new int[size]; 273 for( int i=0; i<size; i++ ) { 274 cnstClmNos[i] = data.getColumnNo( cnstClm[i] ); 275 } 276 277 if( display ) { println( data.nameLine() ); } // 5.7.3.0 (2014/02/07) デバッグ情報 278 279 if( timestamp != null ) { 280 tmstmpClm = data.getColumnNo( timestamp ); 281 } 282 firstRow = false; 283 } 284 285 // 固定値置き換え処理 286 for( int j=0; j<cnstClmNos.length; j++ ) { 287 data.setValue( cnstClmNos[j],constVal[j] ); 288 } 289 290 if( selClmNos != null ) { 291 for( int i=0; i<selClmNos.length; i++ ) { 292 selPstmt.setObject( i+1,data.getValue(selClmNos[i]) ); 293 } 294 } 295 296 final Formatter fileFmt = new Formatter( data,outFilename ); 297 final File outFile = new File( fileFmt.getFormatString(0) ); 298 // 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド 299// if( !outFile.getParentFile().exists() ) { 300// outFile.getParentFile().mkdirs(); 301// } 302 final File parent = outFile.getParentFile(); // 親フォルダを取得。nullもありえる 303 if( parent == null || !parent.exists() && !parent.mkdirs() ) { 304 final String errMsg = "親フォルダを作成できませんでした。[" + data.getRowNo() + "]件目" + CR 305 + " outFile=[" + fileFmt.getFormatString(0) + "]" + CR ; 306 throw new OgRuntimeException( errMsg ); 307 } 308 309 final String[][] rtn ; 310 try( ResultSet resultSet = selPstmt.executeQuery() ) { 311 rtn = DBUtil.resultToArray( resultSet,false ); // useHeader = false 312 } 313 314 // 0件の場合は、ヘッダーもフッターも出力しません。 315 if( rtn.length > 0 ) { 316 try( PrintWriter writer = FileUtil.getPrintWriter( outFile,ENCODE,append ) ) { 317 if( addHeader != null ) { 318 final Formatter headerFmt = new Formatter( data,addHeader ); 319 final String header = headerFmt.getFormatString(0); 320 writer.print( header ); 321 } 322 for( int i=0; i<rtn.length; i++ ) { 323 for( int j=0; j<rtn[i].length; j++ ) { 324 writer.print( rtn[i][j] ); 325 writer.print( separator ); 326 } 327 if( useLineCR ) { writer.println(); } 328 } 329 if( addFooter != null ) { 330 final Formatter footerFmt = new Formatter( data,addFooter ); 331 final String footer = footerFmt.getFormatString(0); 332 writer.print( footer ); 333 } 334 } 335 } 336 337 if( tmstmpClm >= 0 ) { 338 final String tmStmp = String.valueOf( data.getValue( tmstmpClm ) ); 339 final Calendar cal = HybsDateUtil.getCalendar( tmStmp ); 340 // 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド 341// outFile.setLastModified( cal.getTimeInMillis() ); 342 if( !outFile.setLastModified( cal.getTimeInMillis() ) ) { 343 final String errMsg = "タイムスタンプの更新が出来ませんでした。[" + data.getRowNo() + "]件目" + CR 344 + " outFile= [" + outFile + "]" + CR ; 345 System.err.println( errMsg ); 346 } 347 } 348 349 if( display ) { println( data.dataLine() ); } 350 } 351 catch( final SQLException ex) { 352 final String errMsg = "検索処理でエラーが発生しました。[" + data.getRowNo() + "]件目" + CR 353 + " select=[" + select + "]" + CR 354 + " errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR 355 + " data=[" + data.dataLine() + "]" + CR ; 356 throw new OgRuntimeException( errMsg,ex ); 357 } 358 return data; 359 } 360 361 /** 362 * 内部で使用する PreparedStatement を作成します。 363 * 引数指定の SQL または、LineModel から作成した SQL より構築します。 364 * 365 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 366 * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。 367 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。 368 * 369 * @param data 処理対象のLineModel 370 */ 371 private void makePrepareStatement( final LineModel data ) { 372 373 final Formatter format = new Formatter( data,select ); // 6.4.3.4 (2016/03/11) 374 select = format.getQueryFormatString(); 375 selClmNos = format.getClmNos(); 376 377 for( int i=0; i<selClmNos.length; i++ ) { 378 // 指定のカラムが存在しない場合は、エラーにします。 379 if( selClmNos[i] < 0 ) { 380 final String errMsg = "フォーマットに対応したカラムが存在しません。" + CR 381 + "select=[" + select + "]" + CR 382 + "ClmKey=[" + format.getClmKeys()[i] + "]" + CR 383 + "nameLine=[" + data.nameLine() + "]" + CR 384 + "data=[" + data.dataLine() + "]" + CR ; 385 throw new OgRuntimeException( errMsg ); 386 } 387 } 388 389 try { 390 selPstmt = connection.prepareStatement( select ); 391// selPstmt.setFetchSize( DB_FETCH_SIZE ); // 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ 392 selPstmt.setFetchSize( fetchSize ); // 6.9.4.1 (2018/04/09) fetchSize 指定 393 } 394 catch( final SQLException ex) { 395 // 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。 396 final String errMsg = "PreparedStatement を取得できませんでした。" + CR 397 + "errMsg=[" + ex.getMessage() + "]" + CR 398 + "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR 399 + "select=[" + select + "]" + CR 400 + "nameLine=[" + data.nameLine() + "]" + CR 401 + "data=[" + data.dataLine() + "]" + CR ; 402 throw new OgRuntimeException( errMsg,ex ); 403 } 404 } 405 406 /** 407 * プロセスの処理結果のレポート表現を返します。 408 * 処理プログラム名、入力件数、出力件数などの情報です。 409 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような 410 * 形式で出してください。 411 * 412 * @return 処理結果のレポート 413 */ 414 public String report() { 415 // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX' 416 return "[" + getClass().getName() + "]" + CR 417// final String report = "[" + getClass().getName() + "]" + CR 418 + TAB + "DBID : " + dbid + CR 419 + TAB + "Input Count : " + count ; 420 421// return report ; 422 } 423 424 /** 425 * このクラスの使用方法を返します。 426 * 427 * @return このクラスの使用方法 428 * @og.rtnNotNull 429 */ 430 public String usage() { 431 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ) 432 .append( "Process_DBFileout は、SELECT文 を指定し データベースの値を抜き出して、" ).append( CR ) 433 .append( "個々のファイルにセーブする、ChainProcess インターフェースの実装クラスです。" ).append( CR ) 434 .append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から" ).append( CR ) 435 .append( "受け取った LineModel を元に、1行単位に、SELECT文を実行します。" ).append( CR ) 436 .append( CR ) 437 .append( "上流のカラムを、[カラム]変数で使用できます。" ).append( CR ) 438 .append( "また、セーブするファイル名、更新日付等も、都度、更新可能です。" ).append( CR ) 439 .append( CR ) 440 .append( "データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に" ).append( CR ) 441 .append( "設定された接続(Connection)を使用します。" ).append( CR ) 442 .append( CR ) 443 .append( "引数文字列中にスペースを含む場合は、ダブルコーテーション(\"\") で括って下さい。").append( CR ) 444 .append( "引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に" ).append( CR ) 445 .append( "繋げてください。" ).append( CR ) 446 .append( CR ) 447 .append( "SQL文には、{@DATE.YMDH}等のシステム変数が使用できます。" ).append( CR ) 448 .append( CR ).append( CR ) 449 .append( getArgument().usage() ).append( CR ); 450 451 return buf.toString(); 452 } 453 454 /** 455 * このクラスは、main メソッドから実行できません。 456 * 457 * @param args コマンド引数配列 458 */ 459 public static void main( final String[] args ) { 460 LogWriter.log( new Process_DBFileout().usage() ); 461 } 462}