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}