    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        ;                       // ０件データ時 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         * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
    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"の場合は、行番号分を＋１しておく。
    189                        if( no >= 0 ) { clmNos[no] = useNumber ? i+1 : i ; }
    190                }
    191        }
    192
    193        /**
    194         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
    195         * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
    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         * この呼び出し１回毎に、次のデータを取得する準備を行います。
    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         * ファイルより読み込んだ１行のデータを テーブルモデルに
    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      カラム名配列(存在しない場合は、サイズ０の配列)
    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}