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 public LineModel action( final LineModel data ) { 265 count++ ; 266 try { 267 if( firstRow ) { 268 makePrepareStatement( data ); 269 270 final int size = cnstClm.length; 271 cnstClmNos = new int[size]; 272 for( int i=0; i<size; i++ ) { 273 cnstClmNos[i] = data.getColumnNo( cnstClm[i] ); 274 } 275 276 if( display ) { println( data.nameLine() ); } // 5.7.3.0 (2014/02/07) デバッグ情報 277 278 if( timestamp != null ) { 279 tmstmpClm = data.getColumnNo( timestamp ); 280 } 281 firstRow = false; 282 } 283 284 // 固定値置き換え処理 285 for( int j=0; j<cnstClmNos.length; j++ ) { 286 data.setValue( cnstClmNos[j],constVal[j] ); 287 } 288 289 if( selClmNos != null ) { 290 for( int i=0; i<selClmNos.length; i++ ) { 291 selPstmt.setObject( i+1,data.getValue(selClmNos[i]) ); 292 } 293 } 294 295 final Formatter fileFmt = new Formatter( data,outFilename ); 296 final File outFile = new File( fileFmt.getFormatString(0) ); 297 // 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド 298// if( !outFile.getParentFile().exists() ) { 299// outFile.getParentFile().mkdirs(); 300// } 301 final File parent = outFile.getParentFile(); // 親フォルダを取得。nullもありえる 302 if( parent == null || !parent.exists() && !parent.mkdirs() ) { 303 final String errMsg = "親フォルダを作成できませんでした。[" + data.getRowNo() + "]件目" + CR 304 + " outFile=[" + fileFmt.getFormatString(0) + "]" + CR ; 305 throw new OgRuntimeException( errMsg ); 306 } 307 308 final String[][] rtn ; 309 try( final ResultSet resultSet = selPstmt.executeQuery() ) { 310 rtn = DBUtil.resultToArray( resultSet,false ); // useHeader = false 311 } 312 313 // 0件の場合は、ヘッダーもフッターも出力しません。 314 if( rtn.length > 0 ) { 315 try( final PrintWriter writer = FileUtil.getPrintWriter( outFile,ENCODE,append ) ) { 316 if( addHeader != null ) { 317 final Formatter headerFmt = new Formatter( data,addHeader ); 318 final String header = headerFmt.getFormatString(0); 319 writer.print( header ); 320 } 321 for( int i=0; i<rtn.length; i++ ) { 322 for( int j=0; j<rtn[i].length; j++ ) { 323 writer.print( rtn[i][j] ); 324 writer.print( separator ); 325 } 326 if( useLineCR ) { writer.println(); } 327 } 328 if( addFooter != null ) { 329 final Formatter footerFmt = new Formatter( data,addFooter ); 330 final String footer = footerFmt.getFormatString(0); 331 writer.print( footer ); 332 } 333 } 334 } 335 336 if( tmstmpClm >= 0 ) { 337 final String tmStmp = String.valueOf( data.getValue( tmstmpClm ) ); 338 final Calendar cal = HybsDateUtil.getCalendar( tmStmp ); 339 // 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド 340// outFile.setLastModified( cal.getTimeInMillis() ); 341 if( !outFile.setLastModified( cal.getTimeInMillis() ) ) { 342 final String errMsg = "タイムスタンプの更新が出来ませんでした。[" + data.getRowNo() + "]件目" + CR 343 + " outFile= [" + outFile + "]" + CR ; 344 System.err.println( errMsg ); 345 } 346 } 347 348 if( display ) { println( data.dataLine() ); } 349 } 350 catch( final SQLException ex) { 351 final String errMsg = "検索処理でエラーが発生しました。[" + data.getRowNo() + "]件目" + CR 352 + " select=[" + select + "]" + CR 353 + " errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR 354 + " data=[" + data.dataLine() + "]" + CR ; 355 throw new OgRuntimeException( errMsg,ex ); 356 } 357 return data; 358 } 359 360 /** 361 * 内部で使用する PreparedStatement を作成します。 362 * 引数指定の SQL または、LineModel から作成した SQL より構築します。 363 * 364 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。 365 * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。 366 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。 367 * 368 * @param data 処理対象のLineModel 369 */ 370 private void makePrepareStatement( final LineModel data ) { 371 372 final Formatter format = new Formatter( data,select ); // 6.4.3.4 (2016/03/11) 373 select = format.getQueryFormatString(); 374 selClmNos = format.getClmNos(); 375 376 for( int i=0; i<selClmNos.length; i++ ) { 377 // 指定のカラムが存在しない場合は、エラーにします。 378 if( selClmNos[i] < 0 ) { 379 final String errMsg = "フォーマットに対応したカラムが存在しません。" + CR 380 + "select=[" + select + "]" + CR 381 + "ClmKey=[" + format.getClmKeys()[i] + "]" + CR 382 + "nameLine=[" + data.nameLine() + "]" + CR 383 + "data=[" + data.dataLine() + "]" + CR ; 384 throw new OgRuntimeException( errMsg ); 385 } 386 } 387 388 try { 389 selPstmt = connection.prepareStatement( select ); 390// selPstmt.setFetchSize( DB_FETCH_SIZE ); // 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ 391 selPstmt.setFetchSize( fetchSize ); // 6.9.4.1 (2018/04/09) fetchSize 指定 392 } 393 catch( final SQLException ex) { 394 // 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。 395 final String errMsg = "PreparedStatement を取得できませんでした。" + CR 396 + "errMsg=[" + ex.getMessage() + "]" + CR 397 + "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR 398 + "select=[" + select + "]" + CR 399 + "nameLine=[" + data.nameLine() + "]" + CR 400 + "data=[" + data.dataLine() + "]" + CR ; 401 throw new OgRuntimeException( errMsg,ex ); 402 } 403 } 404 405 /** 406 * プロセスの処理結果のレポート表現を返します。 407 * 処理プログラム名、入力件数、出力件数などの情報です。 408 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような 409 * 形式で出してください。 410 * 411 * @return 処理結果のレポート 412 */ 413 public String report() { 414 final String report = "[" + getClass().getName() + "]" + CR 415 + TAB + "DBID : " + dbid + CR 416 + TAB + "Input Count : " + count ; 417 418 return report ; 419 } 420 421 /** 422 * このクラスの使用方法を返します。 423 * 424 * @return このクラスの使用方法 425 * @og.rtnNotNull 426 */ 427 public String usage() { 428 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ) 429 .append( "Process_DBFileout は、SELECT文 を指定し データベースの値を抜き出して、" ).append( CR ) 430 .append( "個々のファイルにセーブする、ChainProcess インターフェースの実装クラスです。" ).append( CR ) 431 .append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から" ).append( CR ) 432 .append( "受け取った LineModel を元に、1行単位に、SELECT文を実行します。" ).append( CR ) 433 .append( CR ) 434 .append( "上流のカラムを、[カラム]変数で使用できます。" ).append( CR ) 435 .append( "また、セーブするファイル名、更新日付等も、都度、更新可能です。" ).append( CR ) 436 .append( CR ) 437 .append( "データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に" ).append( CR ) 438 .append( "設定された接続(Connection)を使用します。" ).append( CR ) 439 .append( CR ) 440 .append( "引数文字列中にスペースを含む場合は、ダブルコーテーション(\"\") で括って下さい。").append( CR ) 441 .append( "引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に" ).append( CR ) 442 .append( "繋げてください。" ).append( CR ) 443 .append( CR ) 444 .append( "SQL文には、{@DATE.YMDH}等のシステム変数が使用できます。" ).append( CR ) 445 .append( CR ).append( CR ) 446 .append( getArgument().usage() ).append( CR ); 447 448 return buf.toString(); 449 } 450 451 /** 452 * このクラスは、main メソッドから実行できません。 453 * 454 * @param args コマンド引数配列 455 */ 456 public static void main( final String[] args ) { 457 LogWriter.log( new Process_DBFileout().usage() ); 458 } 459}