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}