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