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