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 public boolean next() { 215 if( nameNull ) { return false; } 216 217 boolean flag = false; 218 try { 219 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ); // 6.1.0.0 (2014/12/26) refactoring 220 while((line = reader.readLine()) != null) { 221 inCount++ ; 222 if( line.isEmpty() || line.charAt(0) == '#' ) { continue; } 223 else { 224 // 5.2.2.0 (2010/11/01) findbugs 対策(文字列の + 連結と、奇数判定ロジック) 225 int quotCount = StringUtil.countChar( line, '"' ); 226 if( quotCount % 2 != 0 ) { 227 String addLine = null; 228 buf.setLength(0); // 6.1.0.0 (2014/12/26) refactoring 229 buf.append( line ); // 6.1.0.0 (2014/12/26) refactoring 230 while(quotCount % 2 != 0 && (addLine = reader.readLine()) != null) { 231 if( addLine.isEmpty() || addLine.charAt(0) == '#' ) { continue; } 232 buf.append( CR ).append( addLine ); 233 quotCount += StringUtil.countChar( addLine, '"' ); 234 } 235 line = buf.toString(); 236 } 237 flag = true; 238 break; 239 } 240 } 241 } 242 // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 243 catch( final CharacterCodingException ex ) { 244 final String errMsg = "文字のエンコード・エラーが発生しました。" + CR 245 + " ファイルのエンコードが指定のエンコードと異なります。" + CR 246 + " [" + infile + "] , Encode=[" + encode + "]" ; 247 throw new OgCharacterException( errMsg,ex ); // 6.5.0.1 (2016/10/21) 248 } 249 catch( final IOException ex) { 250 final String errMsg = "ファイル読込みエラーが発生しました。" + CR 251 + " [" + infile + "] , Encode=[" + encode + "]" ; 252 throw new OgRuntimeException( errMsg,ex ); 253 } 254 if( debug ) { println( line ); } // 5.7.3.0 (2014/02/07) デバッグ情報 255 return flag; 256 } 257 258 /** 259 * 最初に、 行データである LineModel を作成します 260 * FirstProcess は、次々と処理をチェインしていく最初の行データを 261 * 作成して、後続の ChainProcess クラスに処理データを渡します。 262 * 263 * ファイルより読み込んだ1行のデータを テーブルモデルに 264 * セットするように分割します 265 * なお、読込みは,NAME項目分を読み込みます。データ件数が少ない場合は、 266 * "" をセットしておきます。 267 * 268 * @param rowNo 処理中の行番号 269 * 270 * @return 処理変換後のLineModel 271 */ 272 public LineModel makeLineModel( final int rowNo ) { 273 outCount++ ; 274 final String[] vals = StringUtil.csv2Array( line ,separator ); // 6.0.2.5 (2014/10/31) TAB を char 化 275 276 final int len = vals.length; 277 for( int clmNo=0; clmNo<model.size(); clmNo++ ) { 278 final int no = clmNos[clmNo]; 279 if( len > no ) { 280 model.setValue( clmNo,vals[no] ); 281 } 282 else { 283 // EXCEL が、終端TABを削除してしまうため、少ない場合は埋める。 284 model.setValue( clmNo,"" ); 285 } 286 } 287 model.setRowNo( rowNo ) ; 288 289 if( display ) { println( model.dataLine() ); } 290 291 return model; 292 } 293 294 /** 295 * BufferedReader より、#NAME 行の項目名情報を読み取ります。 296 * データカラムより前に、項目名情報を示す "#Name" が存在する仮定で取り込みます。 297 * この行は、ファイルの形式に無関係に、TAB で区切られています。 298 * 299 * @og.rev 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を優先して利用する。 300 * @og.rev 6.0.4.0 (2014/11/28) #NAME 判定で、桁数不足のエラーが発生する箇所を修正。 301 * @og.rev 6.3.9.0 (2015/11/06) #NAME 行の区切り文字判定が間違っていたので修正。 302 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 303 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 304 * 305 * @param reader PrintWriterオブジェクト 306 * 307 * @return カラム名配列(存在しない場合は、サイズ0の配列) 308 * @og.rtnNotNull 309 */ 310 private String[] readName( final BufferedReader reader ) { 311 try { 312 // 4.0.0 (2005/01/31) line 変数名変更 313 String line1; 314 while((line1 = reader.readLine()) != null) { 315 inCount++ ; 316 if( line1.isEmpty() ) { continue; } 317 if( line1.charAt(0) == '#' ) { 318 // 6.0.4.0 (2014/11/28) #NAME 判定で、桁数不足のエラーが発生する箇所を修正。 319 if( line1.length() >= 5 && "#NAME".equalsIgnoreCase( line1.substring( 0,5 ) ) ) { 320 // 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を優先して利用する。 321 final char sep ; 322 if( TAB != separator && line1.indexOf( separator ) >= 0 ) { // 6.3.9.0 (2015/11/06) バグ? 323 sep = separator; 324 } 325 else { 326 sep = TAB; 327 } 328 // 超イレギュラー処理。#NAME をカラム列に入れない(#NAME+区切り文字 の 6文字分、飛ばす)。 329 return StringUtil.csv2Array( line1.substring( 6 ) ,sep ); 330 } 331 else { continue; } 332 } 333 else { 334 final String errMsg = "#NAME が見つかる前にデータが見つかりました。"; 335 throw new OgRuntimeException( errMsg ); 336 } 337 } 338 } 339 // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 340 catch( final CharacterCodingException ex ) { 341 final String errMsg = "文字のエンコード・エラーが発生しました。" + CR 342 + " ファイルのエンコードが指定のエンコードと異なります。" + CR 343 + " [" + infile + "] , Encode=[" + encode + "]" ; 344 throw new OgCharacterException( errMsg,ex ); // 6.5.0.1 (2016/10/21) 345 } 346 catch( final IOException ex ) { 347 final String errMsg = "ファイル読込みエラーが発生しました。" + CR 348 + " [" + infile + "] , Encode=[" + encode + "]" ; 349 throw new OgRuntimeException( errMsg,ex ); 350 } 351 return new String[0]; 352 } 353 354 /** 355 * プロセスの処理結果のレポート表現を返します。 356 * 処理プログラム名、入力件数、出力件数などの情報です。 357 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような 358 * 形式で出してください。 359 * 360 * @return 処理結果のレポート 361 */ 362 public String report() { 363 // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX' 364 return "[" + getClass().getName() + "]" + CR 365// final String report = "[" + getClass().getName() + "]" + CR 366 + TAB + "Input File : " + infile + CR 367 + TAB + "Input Count : " + inCount + CR 368 + TAB + "Output Count : " + outCount ; 369 370// return report ; 371 } 372 373 /** 374 * このクラスの使用方法を返します。 375 * 376 * @og.rev 5.2.2.0 (2010/11/01) useNumber属性のコメント追加 377 * 378 * @return このクラスの使用方法 379 * @og.rtnNotNull 380 */ 381 public String usage() { 382 final StringBuilder buf = new StringBuilder( BUFFER_LARGE ) 383 .append( "Process_TableReaderは、ファイルから読み取った内容を、LineModel に設定後、" ).append( CR ) 384 .append( "下流に渡す、FirstProcess インターフェースの実装クラスです。" ).append( CR ) 385 .append( CR ) 386 .append( "DBTableModel 形式のファイルを読み取って、各行を LineModel にセットして、" ).append( CR ) 387 .append( "下流(プロセスチェインのデータは上流から下流に渡されます。)に渡します。" ).append( CR ) 388 .append( CR ) 389 .append( "columns 属性は、#NAME で列カラムを外部から指定する場合に使用します。" ).append( CR ) 390 .append( "この属性とuseNumber属性は独立していますが、一般には、#NAME を指定" ).append( CR ) 391 .append( "する場合は、useNumber=\"true\"として、行番号欄は使用しますし、外部から" ).append( CR ) 392 .append( "指定する場合は、useNumber=\"false\"にして先頭から読み取ります。" ).append( CR ) 393 .append( "(自動セットではないので、必要に応じて設定してください)" ).append( CR ) 394 .append( "useNumber の初期値は、\"true\" です。" ).append( CR ) 395 .append( CR ) 396 .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR ) 397 .append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に" ).append( CR ) 398 .append( "繋げてください。" ).append( CR ) 399 .append( CR ).append( CR ) 400 .append( getArgument().usage() ).append( CR ); 401 402 return buf.toString(); 403 } 404 405 /** 406 * このクラスは、main メソッドから実行できません。 407 * 408 * @param args コマンド引数配列 409 */ 410 public static void main( final String[] args ) { 411 LogWriter.log( new Process_TableReader().usage() ); 412 } 413}