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.util.Argument; 019import org.opengion.fukurou.util.StringUtil; 020import org.opengion.fukurou.util.FileUtil; 021import org.opengion.fukurou.util.Closer ; 022import org.opengion.fukurou.util.LogWriter; 023 024import java.util.Map ; 025import java.util.HashMap ; 026import java.util.LinkedHashMap ; 027 028import java.io.File; 029import java.io.BufferedReader; 030import java.io.IOException; 031 032/** 033 * Process_TableDiffは、ファイルから読み取った内容を、LineModel に設定後、 034 * 下流に渡す、FirstProcess インターフェースの実装クラスです。 035 * 036 * DBTableModel 形式のファイルを読み取って、各行を LineModel にセットして、 037 * 下流(プロセスチェインのデータは上流から下流に渡されます。)に渡します。 038 * 039 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。 040 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に 041 * 繋げてください。 042 * 043 * @og.formSample 044 * Process_TableDiff -infile1=INFILE -infile2=INFILE2 -action=DIFF1 -encode=UTF-8 -columns=AA,BB,CC 045 * 046 * -infile1=入力ファイル名1 :入力ファイル名1 047 * -infile2=入力ファイル名2 :入力ファイル名2 048 * -action=比較結果の方法 :ONLY,DIFF,INTERSEC 049 * [-sep1=セパレータ文字 ] :区切り文字1(初期値:タブ) 050 * [-sep2=セパレータ文字 ] :区切り文字2(初期値:タブ) 051 * [-encode1=文字エンコード ] :入力ファイルのエンコードタイプ1 052 * [-encode2=文字エンコード ] :入力ファイルのエンコードタイプ2 053 * [-columns=読み取りカラム名 ] :入力カラム名(カンマ区切り) 054 * [-keyClms=比較するカラム名 ] :比較する列の基準カラム名(カンマ区切り) 055 * [-diffClms=比較するカラム名] :比較するカラム名(カンマ区切り) 056 * [-display=[false/true] ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 057 * [-debug=[false/true] ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 058 * 059 * @og.rev 4.2.3.0 (2008/05/26) 新規作成 060 * 061 * @version 4.0 062 * @author Kazuhiko Hasegawa 063 * @since JDK5.0, 064 */ 065public class Process_TableDiff extends AbstractProcess implements FirstProcess { 066 private static final String ENCODE = System.getProperty("file.encoding"); 067 068 private String separator1 = TAB; // 項目区切り文字 069 private String separator2 = TAB; // 項目区切り文字 070 private String infile1 = null; 071 private String infile2 = null; 072 private BufferedReader reader1 = null; 073 private LineModel model = null; 074 private String line = null; 075 private int[] clmNos = null; // ファイルのヘッダーのカラム番号 076 private int[] keyClmNos = null; // 比較する列の基準カラム名のカラム番号 077 private int[] diffClmNos = null; // 比較するカラム名のカラム番号 078 private String actCmnd = null; // action から名称変更 079 private boolean display = false; // 表示しない 080 private boolean debug = false; // 表示しない 081 private boolean nameNull = false; // 0件データ時 true 082 083 private final Map<String,String> file2Map = new HashMap<String,String>(); // 4.3.1.1 (2008/08/23) final化 084 085 private int inCount1 = 0; 086 private int inCount2 = 0; 087 private int outCount = 0; 088 089 private static final Map<String,String> mustProparty ; // [プロパティ]必須チェック用 Map 090 private static final Map<String,String> usableProparty ; // [プロパティ]整合性チェック Map 091 092 static { 093 mustProparty = new LinkedHashMap<String,String>(); 094 mustProparty.put( "infile1", "入力ファイル名1 (必須)" ); 095 mustProparty.put( "infile2", "入力ファイル名2 (必須)" ); 096 mustProparty.put( "action", "(必須)ONLY,DIFF,INTERSEC" ); 097 mustProparty.put( "keyClms", "比較する列の基準カラム名(必須)(カンマ区切り)" ); 098 mustProparty.put( "diffClms", "比較するカラム名(必須)(カンマ区切り)" ); 099 100 usableProparty = new LinkedHashMap<String,String>(); 101 usableProparty.put( "sep1", "区切り文字1 (初期値:タブ)" ); 102 usableProparty.put( "sep2", "区切り文字2 (初期値:タブ)" ); 103 usableProparty.put( "encode1", "入力ファイルのエンコードタイプ1" ); 104 usableProparty.put( "encode2", "入力ファイルのエンコードタイプ2" ); 105 usableProparty.put( "columns", "入力カラム名(カンマ区切り)" ); 106 usableProparty.put( "display", "結果を標準出力に表示する(true)かしない(false)か" + 107 CR + " (初期値:false:表示しない)" ); 108 usableProparty.put( "debug", "デバッグ情報を標準出力に表示する(true)かしない(false)か" + 109 CR + " (初期値:false:表示しない)" ); 110 } 111 112 /** 113 * デフォルトコンストラクター。 114 * このクラスは、動的作成されます。デフォルトコンストラクターで、 115 * super クラスに対して、必要な初期化を行っておきます。 116 * 117 */ 118 public Process_TableDiff() { 119 super( "org.opengion.fukurou.process.Process_TableDiff",mustProparty,usableProparty ); 120 } 121 122 /** 123 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。 124 * 初期処理(ファイルオープン、DBオープン等)に使用します。 125 * 126 * @param paramProcess データベースの接続先情報などを持っているオブジェクト 127 */ 128 public void init( final ParamProcess paramProcess ) { 129 Argument arg = getArgument(); 130 131 infile1 = arg.getProparty( "infile1" ); 132 infile2 = arg.getProparty( "infile2" ); 133 actCmnd = arg.getProparty( "action" ); 134 String encode1 = arg.getProparty( "encode1",ENCODE ); 135 String encode2 = arg.getProparty( "encode2",ENCODE ); 136 separator1 = arg.getProparty( "sep1",separator1 ); 137 separator2 = arg.getProparty( "sep2",separator2 ); 138 String clms = arg.getProparty( "columns" ); 139 String keyClms = arg.getProparty( "keyClms" ); 140 String diffClms = arg.getProparty( "diffClms" ); 141 display = arg.getProparty( "display",display ); 142 debug = arg.getProparty( "debug" ,debug ); 143 144 if( infile1 == null || infile2 == null ) { 145 String errMsg = "ファイル名が指定されていません。" 146 + "File1=[" + infile1 + "] , File2=[" + infile2 + "]" ; 147 throw new RuntimeException( errMsg ); 148 } 149 150 File file1 = new File( infile1 ); 151 File file2 = new File( infile2 ); 152 153 if( ! file1.exists() || ! file2.exists() ) { 154 // 4.3.1.1 (2008/08/23) Avoid if (x != y) ..; else ..; 155 String errMsg = "ファイルが存在しません。" 156 + ( file1.exists() ? "" : "File1=[" + file1 + "] " ) 157 + ( file2.exists() ? "" : "File2=[" + file2 + "]" ); 158 throw new RuntimeException( errMsg ); 159 } 160 161 if( ! file1.isFile() || ! file2.isFile() ) { 162 // 4.3.1.1 (2008/08/23) Avoid if (x != y) ..; else ..; 163 String errMsg = "フォルダは指定できません。ファイル名を指定してください。" 164 + ( file1.isFile() ? "" : "File1=[" + file1 + "] " ) 165 + ( file2.isFile() ? "" : "File2=[" + file2 + "]" ); 166 throw new RuntimeException( errMsg ); 167 } 168 169 reader1 = FileUtil.getBufferedReader( file1,encode1 ); 170 171 final String[] names ; 172 if( clms != null ) { 173 names = StringUtil.csv2Array( clms ); // 指定のカラム名配列 174 } 175 else { 176 String[] clmNames = readName( reader1 ); // ファイルのカラム名配列 177 if( clmNames == null || clmNames.length == 0 ) { nameNull = true; return ; } 178 names = clmNames; 179 } 180 181 model = new LineModel(); 182 model.init( names ); 183 184 if( display ) { println( model.nameLine() ); } 185 186 // 入力カラム名のカラム番号 187 clmNos = new int[names.length]; 188 for( int i=0; i<names.length; i++ ) { 189 clmNos[i] = i+1; // 行番号分を+1しておく。 190 } 191 192 // 比較する列の基準カラム名 193 if( debug ) { println( "DEBUG:\tkeyClms=" + keyClms ); } 194 final String[] keyClmNms = StringUtil.csv2Array( keyClms ); 195 keyClmNos = new int[keyClmNms.length]; 196 for( int i=0; i<keyClmNms.length; i++ ) { 197 keyClmNos[i] = model.getColumnNo( keyClmNms[i] ); 198 // if( debug ) { println( "DEBUG:" + keyClmNms[i] + ":[" + keyClmNos[i] + "]" ); } 199 // int no = model.getColumnNo( keyClmNms[i] ); 200 // if( no >= 0 ) { keyClmNos[no] = i+1; } // 行番号分を+1しておく。 201 } 202 203 // 比較するカラム名 204 if( debug ) { println( "DEBUG:\tdiffClms=" + diffClms ); } 205 final String[] diffClmNms = StringUtil.csv2Array( diffClms ); 206 diffClmNos = new int[diffClmNms.length]; 207 for( int i=0; i<diffClmNms.length; i++ ) { 208 diffClmNos[i] = model.getColumnNo( diffClmNms[i] ); 209 // if( debug ) { println( "DEBUG:" + diffClmNms[i] + ":[" + diffClmNos[i] + "]" ); } 210 // int no = model.getColumnNo( diffClmNms[i] ); 211 // if( no >= 0 ) { diffClmNos[no] = i+1; } // 行番号分を+1しておく。 212 } 213 214 readF2Data( file2,encode2 ); 215 } 216 217 /** 218 * プロセスの終了を行います。最後に一度だけ、呼び出されます。 219 * 終了処理(ファイルクローズ、DBクローズ等)に使用します。 220 * 221 * @param isOK トータルで、OKだったかどうか[true:成功/false:失敗] 222 */ 223 public void end( final boolean isOK ) { 224 Closer.ioClose( reader1 ); 225 reader1 = null; 226 } 227 228 /** 229 * このデータの処理において、次の処理が出来るかどうかを問い合わせます。 230 * この呼び出し1回毎に、次のデータを取得する準備を行います。 231 * 232 * @return 処理できる:true / 処理できない:false 233 */ 234 public boolean next() { 235 if( nameNull ) { return false; } 236 237 boolean flag = false; 238 try { 239 while((line = reader1.readLine()) != null) { 240 inCount1++ ; 241 if( line.length() == 0 || line.charAt( 0 ) == '#' ) { continue; } 242 else { 243 flag = true; 244 break; 245 } 246 } 247 } 248 catch (IOException ex) { 249 String errMsg = "ファイル読込みエラー[" + infile1 + "]:(" + inCount1 + ")" ; 250 throw new RuntimeException( errMsg,ex ); 251 } 252 return flag; 253 } 254 255 /** 256 * 最初に、 行データである LineModel を作成します 257 * FirstProcess は、次々と処理をチェインしていく最初の行データを 258 * 作成して、後続の ChainProcess クラスに処理データを渡します。 259 * 260 * ファイルより読み込んだ1行のデータを テーブルモデルに 261 * セットするように分割します 262 * なお、読込みは,NAME項目分を読み込みます。データ件数が少ない場合は、 263 * "" をセットしておきます。 264 * 265 * @param rowNo 処理中の行番号 266 * 267 * @return 処理変換後のLineModel 268 */ 269 public LineModel makeLineModel( final int rowNo ) { 270 outCount++ ; 271 String[] vals = StringUtil.csv2Array( line ,separator1.charAt(0) ); 272 273 int len = vals.length; 274 for( int clmNo=0; clmNo<model.size(); clmNo++ ) { 275 int no = clmNos[clmNo]; 276 if( len > no ) { 277 model.setValue( clmNo,vals[no] ); 278 } 279 else { 280 // EXCEL が、終端TABを削除してしまうため、少ない場合は埋める。 281 model.setValue( clmNo,"" ); 282 } 283 } 284 model.setRowNo( rowNo ) ; 285 286 // if( display ) { println( model.dataLine() ); } // 5.1.2.0 (2010/01/01) display の条件変更 287 288 return action( model ); 289 } 290 291 /** 292 * キーと、DIFF設定値を比較し、action に応じた LineModel を返します。 293 * action には、ONLY,DIFF,INTERSEC が指定できます。 294 * ONLY inFile1 のみに存在する行の場合、inFile1 のレコードを返します。 295 * DIFF inFile1 と inFile2 に存在し、かつ、DIFF値が異なる、inFile1 のレコードを返します。 296 * INTERSEC inFile1 と inFile2 に存在し、かつ、DIFF値も同じ、inFile1 のレコードを返します。 297 * inFile2 側をキャッシュしますので、inFile2 側のデータ量が少ない様に選んでください。 298 * 299 * @param model LineModelオブジェクト 300 * 301 * @return 実行後のLineModel 302 */ 303 private LineModel action( final LineModel model ) { 304 LineModel rtn = null; 305 Object[] obj = model.getValues(); 306 307 // キーのカラムを合成します。 308 StringBuilder keys = new StringBuilder(); 309 for( int i=0; i<keyClmNos.length; i++ ) { 310 keys.append( obj[keyClmNos[i]] ).append( "," ); 311 } 312 313 String data = file2Map.get( keys.toString() ); 314 // if( debug ) { println( "DEBUG:" + keys.toString() + ":" + data ); } 315 316 if( "ONLY".equalsIgnoreCase( actCmnd ) && data == null ) { 317 if( debug ) { println( "DEBUG:ONLY\t" + keys.toString() ); } 318 rtn = model; 319 } 320 else { 321 // DIFF値のカラムを合成します。 322 StringBuilder vals = new StringBuilder(); 323 for( int i=0; i<diffClmNos.length; i++ ) { 324 vals.append( obj[diffClmNos[i]] ).append( "," ); 325 } 326 327 boolean eq = vals.toString().equals( data ); 328 329 if( "DIFF".equalsIgnoreCase( actCmnd ) && ! eq ) { 330 if( debug ) { println( "DEBUG:DIFF\t" + keys.toString() + "\t" + data + "\t" + vals.toString() ); } 331 rtn = model; 332 } 333 else if( "INTERSEC".equalsIgnoreCase( actCmnd ) && eq ) { 334 if( debug ) { println( "DEBUG:INTERSEC\t" + keys.toString() + "\t" + data ); } 335 rtn = model; 336 } 337 } 338 if( display && rtn != null ) { println( rtn.dataLine() ); } 339 return rtn; 340 } 341 342 /** 343 * BufferedReader より、#NAME 行の項目名情報を読み取ります。 344 * データカラムより前に、項目名情報を示す "#Name" が存在する仮定で取り込みます。 345 * この行は、ファイルの形式に無関係に、TAB で区切られています。 346 * 347 * @param reader PrintWriterオブジェクト 348 * 349 * @return カラム名配列(存在しない場合は、サイズ0の配列) 350 */ 351 private String[] readName( final BufferedReader reader ) { 352 try { 353 // 4.0.0 (2005/01/31) line 変数名変更 354 String line1; 355 while((line1 = reader.readLine()) != null) { 356 inCount1++ ; 357 if( line1.length() == 0 ) { continue; } 358 if( line1.charAt(0) == '#' ) { 359 String key = line1.substring( 0,5 ); 360 if( "#NAME".equalsIgnoreCase( key ) ) { 361 // 超イレギュラー処理 最初の TAB 以前の文字は無視する。 362 String line2 = line1.substring( line1.indexOf( TAB )+1 ); 363 return StringUtil.csv2Array( line2 ,TAB.charAt(0) ); 364 } 365 else { continue; } 366 } 367 else { 368 String errMsg = "#NAME が見つかる前にデータが見つかりました。"; 369 throw new RuntimeException( errMsg ); 370 } 371 } 372 } 373 catch (IOException ex) { 374 String errMsg = "ファイル読込みエラー[" + infile1 + "]:(" + inCount1 + ")" ; 375 throw new RuntimeException( errMsg,ex ); 376 } 377 return new String[0]; 378 } 379 380 /** 381 * ファイル属性を読取り、キー情報を作成し、内部メモリマップにキャッシュします。 382 * このマップをもとに、inFile1 のデータを逐次読み取って、処理を進めます。 383 * 384 * @param file2 読取り元のファイル 385 * @param encode2 ファイルのエンコード 386 */ 387 private void readF2Data( final File file2, final String encode2 ) { 388 BufferedReader reader2 = null; 389 try { 390 if( debug ) { println( "DEBUG:\tFile2="+ file2 + " 初期処理" ); } 391 reader2 = FileUtil.getBufferedReader( file2,encode2 ); 392 // 4.0.0 (2005/01/31) line 変数名変更 393 String line1; 394 char sep2 = separator2.charAt(0); 395 while((line1 = reader2.readLine()) != null) { 396 inCount2++ ; 397 if( line1.length() == 0 ) { continue; } 398 if( line1.charAt(0) == '#' ) { continue; } 399 else { 400 // 超イレギュラー処理 最初の TAB 以前の文字は無視する。 401 String line2 = line1.substring( line1.indexOf( separator2 )+1 ); 402 Object[] obj = StringUtil.csv2Array( line2 , sep2 ); 403 404 // キーのカラムを合成します。 405 StringBuilder keys = new StringBuilder(); 406 for( int i=0; i<keyClmNos.length; i++ ) { 407 keys.append( obj[keyClmNos[i]] ).append( "," ); 408 } 409 410 // DIFF値のカラムを合成します。 411 StringBuilder vals = new StringBuilder(); 412 for( int i=0; i<diffClmNos.length; i++ ) { 413 vals.append( obj[diffClmNos[i]] ).append( "," ); 414 } 415 416 if( debug ) { println( "DEBUG:\t" + keys.toString() + "\t" + vals.toString() ); } 417 418 file2Map.put( keys.toString(), vals.toString() ); 419 } 420 } 421 if( debug ) { println( "DEBUG:\t======初期処理終了======" ); } 422 } 423 catch (IOException ex) { 424 String errMsg = "ファイル読込みエラー[" + infile2 + "]:(" + inCount2 + ")" ; 425 throw new RuntimeException( errMsg,ex ); 426 } 427 finally { 428 Closer.ioClose( reader2 ); 429 } 430 } 431 432 /** 433 * プロセスの処理結果のレポート表現を返します。 434 * 処理プログラム名、入力件数、出力件数などの情報です。 435 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような 436 * 形式で出してください。 437 * 438 * @return 処理結果のレポート 439 */ 440 public String report() { 441 String report = "[" + getClass().getName() + "]" + CR 442 + TAB + "Input File1 : " + infile1 + CR 443 + TAB + "Input File2 : " + infile2 + CR 444 + TAB + "Input Count1 : " + inCount1 + CR 445 + TAB + "Input Count2 : " + inCount2 + CR 446 + TAB + "Output Count : " + outCount ; 447 448 return report ; 449 } 450 451 /** 452 * このクラスの使用方法を返します。 453 * 454 * @return このクラスの使用方法 455 */ 456 public String usage() { 457 StringBuilder buf = new StringBuilder(); 458 459 buf.append( "Process_TableDiffは、ファイルから読み取った内容を、LineModel に設定後、" ).append( CR ); 460 buf.append( "下流に渡す、FirstProcess インターフェースの実装クラスです。" ).append( CR ); 461 buf.append( CR ); 462 buf.append( "DBTableModel 形式のファイルを読み取って、各行を LineModel にセットして、" ).append( CR ); 463 buf.append( "下流(プロセスチェインのデータは上流から下流に渡されます。)に渡します。" ).append( CR ); 464 buf.append( CR ); 465 buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR ); 466 buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に" ).append( CR ); 467 buf.append( "繋げてください。" ).append( CR ); 468 buf.append( CR ).append( CR ); 469 470 buf.append( getArgument().usage() ).append( CR ); 471 472 return buf.toString(); 473 } 474 475 /** 476 * このクラスは、main メソッドから実行できません。 477 * 478 * @param args コマンド引数配列 479 */ 480 public static void main( final String[] args ) { 481 LogWriter.log( new Process_TableDiff().usage() ); 482 } 483}