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.system.OgCharacterException ; // 6.5.0.1 (2016/10/21) 020import org.opengion.fukurou.util.Argument; 021import org.opengion.fukurou.util.StringUtil; 022import org.opengion.fukurou.util.FileUtil; 023import org.opengion.fukurou.system.Closer ; 024import org.opengion.fukurou.system.LogWriter; 025 026import java.util.Map ; 027import java.util.LinkedHashMap ; 028 029import java.io.File; 030import java.io.BufferedReader; 031import java.io.IOException; 032import java.nio.charset.CharacterCodingException; // 6.3.1.0 (2015/06/28) 033 034/** 035 * Process_TableReaderは、ファイルから読み取った内容を、LineModel に設定後、 036 * 下流に渡す、FirstProcess インターフェースの実装クラスです。 037 * 038 * DBTableModel 形式のファイルを読み取って、各行を LineModel にセットして、 039 * 下流(プロセスチェインのデータは上流から下流に渡されます。)に渡します。 040 * 041 * columns 属性は、#NAME で列カラムを外部から指定する場合に使用します。 042 * この属性とuseNumber属性は独立していますが、一般には、#NAME を指定 043 * する場合は、useNumber="true"として、行番号欄は使用しますし、外部から 044 * 指定する場合は、useNumber="false"にして先頭から読み取ります。 045 * (自動セットではないので、必要に応じて設定してください) 046 * useNumber の初期値は、"true" です。 047 * 048 * ※ 注意 049 * Process_TableReader では、セパレータ文字 で区切って読み込む処理で、前後のスペースを 050 * 削除しています。 051 * 052 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。 053 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に 054 * 繋げてください。 055 * 056 * @og.formSample 057 * Process_TableReader -infile=INFILE -sep=, -encode=UTF-8 -columns=AA,BB,CC 058 * 059 * -infile=入力ファイル名 :入力ファイル名 060 * [-existCheck=存在確認 ] :ファイルが存在しない場合エラーにする(初期値:true) 061 * [-sep=セパレータ文字 ] :区切り文字(初期値:タブ) 062 * [-encode=文字エンコード ] :入力ファイルのエンコードタイプ 063 * [-columns=読み取りカラム名] :入力カラム名(CSV形式) 064 * [-useNumber=[true/false] ] :行番号を使用する(true)か使用しない(false)か。 065 * [-display=[false/true] ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 066 * [-debug=[false/true] ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない]) 067 * 068 * @version 4.0 069 * @author Kazuhiko Hasegawa 070 * @since JDK5.0, 071 */ 072public class Process_TableReader extends AbstractProcess implements FirstProcess { 073 private char separator = TAB; // 6.0.2.5 (2014/10/31) TAB を char 化 074 private String infile ; 075 private String encode ; // 6.3.1.0 (2015/06/28) デバッグ時に使用 076 private BufferedReader reader ; 077 private LineModel model ; 078 private String line ; 079 private int[] clmNos ; // ファイルのヘッダーのカラム番号 080 private boolean useNumber = true; // 5.2.2.0 (2010/11/01) 行番号を使用する(true)か使用しない(false)か 081 private boolean nameNull ; // 0件データ時 true 082 private boolean display ; // 表示しない 083 private boolean debug ; // 5.7.3.0 (2014/02/07) デバッグ情報 084 085 private int inCount ; 086 private int outCount ; 087 088 /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */ 089 private static final Map<String,String> MUST_PROPARTY ; // [プロパティ]必須チェック用 Map 090 /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */ 091 private static final Map<String,String> USABLE_PROPARTY ; // [プロパティ]整合性チェック Map 092 093 static { 094 MUST_PROPARTY = new LinkedHashMap<>(); 095 MUST_PROPARTY.put( "infile", "入力ファイル名 (必須)" ); 096 097 USABLE_PROPARTY = new LinkedHashMap<>(); 098 USABLE_PROPARTY.put( "existCheck", "ファイルが存在しない場合エラーにする(初期値:true)" ); 099 USABLE_PROPARTY.put( "sep", "区切り文字(初期値:タブ)" ); 100 USABLE_PROPARTY.put( "encode", "入力ファイルのエンコードタイプ" ); 101 USABLE_PROPARTY.put( "columns", "入力カラム名(CSV形式)" ); 102 USABLE_PROPARTY.put( "useNumber", "行番号を使用する(true)か使用しない(false)か" ); // 5.2.2.0 (2010/11/01) 103 USABLE_PROPARTY.put( "display", "結果を標準出力に表示する(true)かしない(false)か" + 104 CR + " (初期値:false:表示しない)" ); 105 USABLE_PROPARTY.put( "debug", "デバッグ情報を標準出力に表示する(true)かしない(false)か" + 106 CR + "(初期値:false:表示しない)" ); // 5.7.3.0 (2014/02/07) デバッグ情報 107 } 108 109 /** 110 * デフォルトコンストラクター。 111 * このクラスは、動的作成されます。デフォルトコンストラクターで、 112 * super クラスに対して、必要な初期化を行っておきます。 113 * 114 */ 115 public Process_TableReader() { 116 super( "org.opengion.fukurou.process.Process_TableReader",MUST_PROPARTY,USABLE_PROPARTY ); 117 } 118 119 /** 120 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。 121 * 初期処理(ファイルオープン、DBオープン等)に使用します。 122 * 123 * @og.rev 5.2.2.0 (2010/11/01) useNumber属性の追加 124 * 125 * @param paramProcess データベースの接続先情報などを持っているオブジェクト 126 */ 127 public void init( final ParamProcess paramProcess ) { 128 final Argument arg = getArgument(); 129 130 infile = arg.getProparty( "infile" ); 131 encode = arg.getProparty( "encode" , System.getProperty( "file.encoding" ) ); // 6.3.1.0 (2015/06/28) デバッグ時に使用 132 useNumber = arg.getProparty( "useNumber" , useNumber ); // 5.2.2.0 (2010/11/01) 133 display = arg.getProparty( "display" , display ); 134 debug = arg.getProparty( "debug" , debug ); // 5.7.3.0 (2014/02/07) デバッグ情報 135 136 // 6.0.2.5 (2014/10/31) TAB を char 化 137 final String sep = arg.getProparty( "sep",null ); 138 if( sep != null ) { separator = sep.charAt(0); } 139 140 if( infile == null ) { 141 final String errMsg = "ファイル名が指定されていません。" ; 142 throw new OgRuntimeException( errMsg ); 143 } 144 145 final File file = new File( infile ); 146 147 if( ! file.exists() ) { 148 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point. 149 final boolean existCheck = arg.getProparty("existCheck",true); 150 if( existCheck ) { 151 final String errMsg = "ファイルが存在しません。File=[" + file + "]" ; 152 throw new OgRuntimeException( errMsg ); 153 } 154 else { 155 nameNull = true; return ; 156 } 157 } 158 159 if( ! file.isFile() ) { 160 final String errMsg = "ファイル名を指定してください。File=[" + file + "]" ; 161 throw new OgRuntimeException( errMsg ); 162 } 163 164 reader = FileUtil.getBufferedReader( file,encode ); 165 166 final String[] names ; 167 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point. 168 final String clms = arg.getProparty("columns" ); 169 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..; 170 if( clms == null ) { 171 // 5.2.2.0 (2010/11/01) names の外部指定の処理を先に行う。 172 final String[] clmNames = readName( reader ); // ファイルのカラム名配列 173 if( clmNames == null || clmNames.length == 0 ) { nameNull = true; return ; } 174 names = clmNames; 175 } 176 else { 177 names = StringUtil.csv2Array( clms ); // 指定のカラム名配列 178 } 179 180 model = new LineModel(); 181 model.init( names ); 182 183 if( display ) { println( model.nameLine() ); } 184 185 clmNos = new int[names.length]; 186 for( int i=0; i<names.length; i++ ) { 187 final int no = model.getColumnNo( names[i] ); 188 // 5.2.2.0 (2010/11/01) useNumber="true"の場合は、行番号分を+1しておく。 189 if( no >= 0 ) { clmNos[no] = useNumber ? i+1 : i ; } 190 } 191 } 192 193 /** 194 * プロセスの終了を行います。最後に一度だけ、呼び出されます。 195 * 終了処理(ファイルクローズ、DBクローズ等)に使用します。 196 * 197 * @param isOK トータルで、OKだったかどうか[true:成功/false:失敗] 198 */ 199 public void end( final boolean isOK ) { 200 Closer.ioClose( reader ); 201 reader = null; 202 } 203 204 /** 205 * このデータの処理において、次の処理が出来るかどうかを問い合わせます。 206 * この呼び出し1回毎に、次のデータを取得する準備を行います。 207 * 208 * @og.rev 5.2.2.0 (2010/11/01) ""で囲われているデータに改行が入っていた場合の対応 209 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 210 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 211 * 212 * @return 処理できる:true / 処理できない:false 213 */ 214 @Override // FirstProcess 215 public boolean next() { 216 if( nameNull ) { return false; } 217 218 boolean flag = false; 219 try { 220 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ); // 6.1.0.0 (2014/12/26) refactoring 221 while((line = reader.readLine()) != null) { 222 inCount++ ; 223 if( line.isEmpty() || line.charAt(0) == '#' ) { continue; } 224 else { 225 // 5.2.2.0 (2010/11/01) findbugs 対策(文字列の + 連結と、奇数判定ロジック) 226 int quotCount = StringUtil.countChar( line, '"' ); 227 if( quotCount % 2 != 0 ) { 228 String addLine = null; 229 buf.setLength(0); // 6.1.0.0 (2014/12/26) refactoring 230 buf.append( line ); // 6.1.0.0 (2014/12/26) refactoring 231 while(quotCount % 2 != 0 && (addLine = reader.readLine()) != null) { 232 if( addLine.isEmpty() || addLine.charAt(0) == '#' ) { continue; } 233 buf.append( CR ).append( addLine ); 234 quotCount += StringUtil.countChar( addLine, '"' ); 235 } 236 line = buf.toString(); 237 } 238 flag = true; 239 break; 240 } 241 } 242 } 243 // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 244 catch( final CharacterCodingException ex ) { 245 final String errMsg = "文字のエンコード・エラーが発生しました。" + CR 246 + " ファイルのエンコードが指定のエンコードと異なります。" + CR 247 + " [" + infile + "] , Encode=[" + encode + "]" ; 248 throw new OgCharacterException( errMsg,ex ); // 6.5.0.1 (2016/10/21) 249 } 250 catch( final IOException ex) { 251 final String errMsg = "ファイル読込みエラーが発生しました。" + CR 252 + " [" + infile + "] , Encode=[" + encode + "]" ; 253 throw new OgRuntimeException( errMsg,ex ); 254 } 255 if( debug ) { println( line ); } // 5.7.3.0 (2014/02/07) デバッグ情報 256 return flag; 257 } 258 259 /** 260 * 最初に、 行データである LineModel を作成します 261 * FirstProcess は、次々と処理をチェインしていく最初の行データを 262 * 作成して、後続の ChainProcess クラスに処理データを渡します。 263 * 264 * ファイルより読み込んだ1行のデータを テーブルモデルに 265 * セットするように分割します 266 * なお、読込みは、NAME項目分を読み込みます。データ件数が少ない場合は、 267 * "" をセットしておきます。 268 * 269 * @param rowNo 処理中の行番号 270 * 271 * @return 処理変換後のLineModel 272 */ 273 @Override // FirstProcess 274 public LineModel makeLineModel( final int rowNo ) { 275 outCount++ ; 276 final String[] vals = StringUtil.csv2Array( line ,separator ); // 6.0.2.5 (2014/10/31) TAB を char 化 277 278 final int len = vals.length; 279 for( int clmNo=0; clmNo<model.size(); clmNo++ ) { 280 final int no = clmNos[clmNo]; 281 if( len > no ) { 282 model.setValue( clmNo,vals[no] ); 283 } 284 else { 285 // EXCEL が、終端TABを削除してしまうため、少ない場合は埋める。 286 model.setValue( clmNo,"" ); 287 } 288 } 289 model.setRowNo( rowNo ) ; 290 291 if( display ) { println( model.dataLine() ); } 292 293 return model; 294 } 295 296 /** 297 * BufferedReader より、#NAME 行の項目名情報を読み取ります。 298 * データカラムより前に、項目名情報を示す "#Name" が存在する仮定で取り込みます。 299 * この行は、ファイルの形式に無関係に、TAB で区切られています。 300 * 301 * @og.rev 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を優先して利用する。 302 * @og.rev 6.0.4.0 (2014/11/28) #NAME 判定で、桁数不足のエラーが発生する箇所を修正。 303 * @og.rev 6.3.9.0 (2015/11/06) #NAME 行の区切り文字判定が間違っていたので修正。 304 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 305 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 306 * 307 * @param reader PrintWriterオブジェクト 308 * 309 * @return カラム名配列(存在しない場合は、サイズ0の配列) 310 * @og.rtnNotNull 311 */ 312 private String[] readName( final BufferedReader reader ) { 313 try { 314 // 4.0.0 (2005/01/31) line 変数名変更 315 String line1; 316 while((line1 = reader.readLine()) != null) { 317 inCount++ ; 318 if( line1.isEmpty() ) { continue; } 319 if( line1.charAt(0) == '#' ) { 320 // 6.0.4.0 (2014/11/28) #NAME 判定で、桁数不足のエラーが発生する箇所を修正。 321 if( line1.length() >= 5 && "#NAME".equalsIgnoreCase( line1.substring( 0,5 ) ) ) { 322 // 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を優先して利用する。 323 final char sep ; 324 if( TAB != separator && line1.indexOf( separator ) >= 0 ) { // 6.3.9.0 (2015/11/06) バグ? 325 sep = separator; 326 } 327 else { 328 sep = TAB; 329 } 330 // 超イレギュラー処理。#NAME をカラム列に入れない(#NAME+区切り文字 の 6文字分、飛ばす)。 331 return StringUtil.csv2Array( line1.substring( 6 ) ,sep ); 332 } 333 else { continue; } 334 } 335 else { 336 final String errMsg = "#NAME が見つかる前にデータが見つかりました。"; 337 throw new OgRuntimeException( errMsg ); 338 } 339 } 340 } 341 // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 342 catch( final CharacterCodingException ex ) { 343 final String errMsg = "文字のエンコード・エラーが発生しました。" + CR 344 + " ファイルのエンコードが指定のエンコードと異なります。" + CR 345 + " [" + infile + "] , Encode=[" + encode + "]" ; 346 throw new OgCharacterException( errMsg,ex ); // 6.5.0.1 (2016/10/21) 347 } 348 catch( final IOException ex ) { 349 final String errMsg = "ファイル読込みエラーが発生しました。" + CR 350 + " [" + infile + "] , Encode=[" + encode + "]" ; 351 throw new OgRuntimeException( errMsg,ex ); 352 } 353 return new String[0]; 354 } 355 356 /** 357 * プロセスの処理結果のレポート表現を返します。 358 * 処理プログラム名、入力件数、出力件数などの情報です。 359 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような 360 * 形式で出してください。 361 * 362 * @return 処理結果のレポート 363 */ 364 public String report() { 365 // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX' 366 return "[" + getClass().getName() + "]" + CR 367// final String report = "[" + getClass().getName() + "]" + CR 368 + TAB + "Input File : " + infile + CR 369 + TAB + "Input Count : " + inCount + CR 370 + TAB + "Output Count : " + outCount ; 371 372// return report ; 373 } 374 375 /** 376 * このクラスの使用方法を返します。 377 * 378 * @og.rev 5.2.2.0 (2010/11/01) useNumber属性のコメント追加 379 * 380 * @return このクラスの使用方法 381 * @og.rtnNotNull 382 */ 383 public String usage() { 384 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ) 385 .append( "Process_TableReaderは、ファイルから読み取った内容を、LineModel に設定後、" ).append( CR ) 386 .append( "下流に渡す、FirstProcess インターフェースの実装クラスです。" ).append( CR ) 387 .append( CR ) 388 .append( "DBTableModel 形式のファイルを読み取って、各行を LineModel にセットして、" ).append( CR ) 389 .append( "下流(プロセスチェインのデータは上流から下流に渡されます。)に渡します。" ).append( CR ) 390 .append( CR ) 391 .append( "columns 属性は、#NAME で列カラムを外部から指定する場合に使用します。" ).append( CR ) 392 .append( "この属性とuseNumber属性は独立していますが、一般には、#NAME を指定" ).append( CR ) 393 .append( "する場合は、useNumber=\"true\"として、行番号欄は使用しますし、外部から" ).append( CR ) 394 .append( "指定する場合は、useNumber=\"false\"にして先頭から読み取ります。" ).append( CR ) 395 .append( "(自動セットではないので、必要に応じて設定してください)" ).append( CR ) 396 .append( "useNumber の初期値は、\"true\" です。" ).append( CR ) 397 .append( CR ) 398 .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR ) 399 .append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に" ).append( CR ) 400 .append( "繋げてください。" ).append( CR ) 401 .append( CR ).append( CR ) 402 .append( getArgument().usage() ).append( CR ); 403 404 return buf.toString(); 405 } 406 407 /** 408 * このクラスは、main メソッドから実行できません。 409 * 410 * @param args コマンド引数配列 411 */ 412 public static void main( final String[] args ) { 413 LogWriter.log( new Process_TableReader().usage() ); 414 } 415}