    001/*
    002 * Copyright (c) 2016 The EUROMAP63.jp 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.fileexec;
    017
    018import java.util.function.Consumer;
    019import java.util.Locale;
    020import java.util.List;
    021import java.util.ArrayList;
    022import java.nio.file.Path;
    023import java.nio.charset.Charset;
    024import static java.nio.charset.StandardCharsets.UTF_8;
    025
    026/**
    027 * LineSplitter は、１行分のデータを順次分割するクラスです。
    028 *
    029 *<pre>
    030 * ファイルは、『改行』で行分割して、カンマかタブでカラム分割します。
    031 *
    032 *  応答ファイルの解析処理を簡素化するため、以下のルール(禁止事項)を定めます。
    033 *      １．ダブルクオートの中に、ダブルクオート、改行、を含まないこと。
    034 *          （カンマとスペースは含めることが出来ます。）
    035 *      ２．１行の定義は、『改行』とします。
    036 *      ３．スペース分割時は、複数スペースの場合でも、１つの区切り文字として扱います。
    037 *          （A B    C D → ｢A｣、｢B｣、｢C｣、｢D｣ に分割されます。）
    038 *      ４．カンマ分割は、ダブルクオート間のカンマは分解しません。
    039 *          混在した場合でも、最初に見つけた方が優先されます。
    040 *      ５．カンマ分割時は、複数カンマの場合は、それぞれ空文字列に分割されます。
    041 *          カンマ分割後、それぞれの文字列は、前後スペースを削除(trim)します。
    042 *          （A, B , ,  C , D → ｢A｣、｢B｣、｢｣、｢C｣、｢D｣ に分割されます。）
    043 *      ６．カラム分解後の、ダブルクオートは、削除します。
    044 *          （（A, B , ,  "CC C,C" , D → ｢A｣、｢B｣、｢｣、｢CC C,C｣、｢D｣ に分割されます。）
    045 *
    046 *  処理手順
    047 *      １．ファイルより、１行づつ（改行コードで分割）読み込みます。
    048 *      ２．読み込んだ１行について、先頭が、『#』の行はコメント行としてスキップします。
    049 *      ３．先頭から、区切り文字(スペースかカンマかタブ)が見つかるまでを、１カラムとして取得します。
    050 *      ４．その間、ダブルクオートが見つかったら、次のダブルクオートまで、取り込みます。
    051 *      ５．カラム分割された単語の前後スペースと、前後ダブルクオートを削除します。
    052 *          trim()が先で、ダブルクオートの削除は、後から行います。(ダブルクオート内のtrim()は行いません。)
    053 *      ６．個々のカラムを配列にして返します。
    054 *      ７．これを、ファイルが終了するまで繰り返します。
    055 *
    056 * 並行性
    057 *  このクラスは、staticメソッドのみのユーティリティークラスのため、スレッドに対して、安全です。
    058 *  また、ﾌｧｲﾙの読み取りに関して、FileChannelのtryLockを行っています。
    059 *</pre>
    060 *
    061 * @og.rev 1.0.0 (2016/04/28) 新規作成
    062 *
    063 * @version  1.0
    064 * @author   Kazuhiko Hasegawa
    065 * @since    JDK1.8,
    066 */
    067public final class LineSplitter {
    068        private static final XLogger LOGGER= XLogger.getLogger( LineSplitter.class.getName() );         // ログ出力
    069
    070        private final Charset chset ;                           // ファイルのエンコード
    071        private String[] clms = new String[0];          // オリジナルのカラム列(ゼロ文字列も含む)
    072
    073        private static final String NAME_KEY = "#NAME";
    074        private static final int    NAME_LEN = NAME_KEY.length();
    075
    076        /**
    077         * デフォルトコンストラクター
    078         *
    079         * ファイル読み取りのCharsetは、UTF-8になります。
    080         * @see java.nio.charset.StandardCharsets#UTF_8
    081         */
    082        public LineSplitter() {
    083                this( UTF_8 , null );
    084        }
    085
    086        /**
    087         * Charsetに対応した文字列を指定して、オブジェクトを作成します。
    088         *
    089         * @param chStr         ファイルを読み取るときのCharset文字列
    090         * @param inClms        外部指定カラム文字列(CSV形式)
    091         */
    092        public LineSplitter( final String chStr , final String inClms ) {
    093                this( Charset.forName( chStr ) , inClms );
    094        }
    095
    096        /**
    097         * Charsetを指定して、オブジェクトを作成します。
    098         *
    099         * @param chObj         ファイルを読み取るときのCharsetオブジェクト
    100         * @param inClms        外部指定カラム文字列(CSV形式)
    101         */
    102        public LineSplitter( final Charset chObj , final String inClms ) {
    103                chset = chObj;
    104                if( inClms != null && !inClms.isEmpty() ) {
    105                        clms = inClms.split( "," );                     // CSV形式のカラム列を一旦分解します。
    106                }
    107                LOGGER.debug( () -> "[LineSplitter] Charset=" + chObj + " , inClms=" + inClms );
    108        }
    109
    110        /**
    111         * #NAME が存在すれば、そこから名前配列を返します。
    112         *
    113         * ここでは、オリジナルのカラム列(ゼロ文字列も含む)ではなく、
    114         * 存在するカラム名だけのカラム列を返します。
    115         *
    116         * 外部指定カラムがあれば、そちらを優先します。
    117         * 無ければ、長さゼロの配列 が返されます。
    118         *
    119         * @return あれば名前配列、無ければ、長さゼロの配列
    120         */
    121        public String[] getColumns() {
    122                final List<String> clmList = new ArrayList<>();         // １行分の分割したトークンのリスト
    123                for( final String clm : clms ) {
    124                        if( !clm.isEmpty() ) { clmList.add( clm ); }    // ゼロ文字列以外のカラムのみ登録します。
    125                }
    126                return clmList.toArray( new String[clmList.size()] );
    127        }
    128
    129        /**
    130         * １行づつ処理を行った結果のトークンをConsumerにセットする繰り返しメソッドです。
    131         * １行単位に、Consumer#action が呼ばれます。
    132         * セットされるリストは、１行をトークンに分割したリストで、空行の場合は、SKIPします。
    133         * また、オリジナルのカラム列がゼロ文字列の場合は、その列データを返しません。
    134         * つまり、存在するカラム名だけの値列を返します。
    135         *
    136         * ファイルを順次読み込むため、内部メモリを圧迫しません。
    137         *
    138         * @param inPath        処理対象のPathオブジェクト
    139         * @param action        行を区切り文字で分割した文字列のリストを引数に取るConsumerオブジェクト
    140         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
    141         * @see         FileUtil#lockForEach(Path,Consumer)
    142         */
    143        public void forEach( final Path inPath , final Consumer<List<String>> action ) {
    144                FileUtil.lockForEach(
    145                        inPath ,
    146                        chset  ,
    147                        line -> {
    148                                final List<String> list = split( line );                        // 行末カット、コメントカット、trim されます。
    149                                if( !list.isEmpty() ) { action.accept( list ); }        // 空のリストオブジェクトの場合、SKIPします。
    150                        }
    151                );
    152        }
    153
    154        /**
    155         * １行分の分割したトークンのリストを返します。
    156         *
    157         * ファイルの読み込みを、単独または、別に行った場合に、１行データとして、処理できます。
    158         * このクラスの特徴である、先頭が、『#』の行は、コメントとみなして、削除します。
    159         * １行分をtrim()する処理も、行います。
    160         * trim()の結果が、空文字列のみの場合は、空のリストオブジェクトを返します。
    161         *
    162         * @param       orgLine １行データ(オリジナル)
    163         * @return      １行分の分割したトークンのリスト(行末、コメント、trim処理済み)
    164         */
    165        public List<String> split( final String orgLine ) {
    166                final List<String> list = new ArrayList<>();                    // １行分の分割したトークンのリスト
    167
    168                final String line = cmntCut( orgLine );                                 // 行末カット、コメントカット、trim されます。
    169                if( line.isEmpty() ) { return list; }                                   // Zero文字列の場合、空のリストオブジェクトを返します。
    170
    171                final int maxPosition = line.length();
    172                int currentPosition = 0;
    173                int listNo = 0;
    174
    175                // "<" では末尾の項目が空(カンマで1行が終わる)場合、正しく処理できない。
    176                while( currentPosition <= maxPosition ) {
    177        //              boolean fstSpace = true;                // 先頭にスペースがある場合は、削除します
    178                        boolean inquote  = false;               // ダブルクオート内部のカンマは、スキップする。
    179        //              boolean inkakko  = false;               // 『[』と『]』の間のカンマは、スキップする。
    180
    181                        int position = currentPosition;
    182                        final int from = position;
    183
    184                        char ch = 0;
    185                        while( position < maxPosition ) {
    186                                ch = line.charAt( position );
    187        //                      if( fstSpace ) {
    188        //                              if( ch <= ' ' ) { position++ ; continue; }              // UTF-8 で、' ' より小さい文字は、空白文字(trim対象)です。
    189        //                              fstSpace = false;
    190        //                              from = position ;               // 最初に見つけた、空白文字以外の文字の位置
    191        //                      }
    192
    193        //                      if( !inquote && !inkakko && ( ch == ',' || ch <= ' ') ) { break; }
    194                                if( !inquote && ( ch == ',' || ch <= ' ') ) { break; }
    195                                else if( '"' == ch ) { inquote = !inquote; }                                    // 『"』クオート処理を行う
    196        //                      else if( '[' == ch && !inkakko ) { inkakko = true;  }                   // 『[』の開始処理
    197        //                      else if( ']' == ch && inkakko  ) { inkakko = false; }                   // 『]』の終了処理
    198                                position++;
    199                        }
    200
    201                        // 分割トークン
    202                        String token = line.substring( from,position );
    203
    204                        // トークンの前後が '"' なら、削除します。
    205                        final int len = token.length();
    206
    207                        if( len >=2 && token.charAt(0) == '"' && token.charAt(len-1) == '"' ) {
    208                                token = token.substring( 1,len-1 );
    209                        }
    210
    211                        // 超特殊処理。tokenにnullは含まれないが、文字列が、"null"の場合は、ゼロ文字列と置き換えます。
    212                        if( "null".equalsIgnoreCase( token ) ) { token = ""; }
    213
    214                        //  #NAME 列を削除します。       ゼロカラム列を削除します。
    215                        if( !clms[listNo].isEmpty() ) {
    216                                list.add( token );
    217                        }
    218                        listNo++ ;
    219
    220                        // ch の終わり方で、スペースの場合、カンマ区切りかも知れないので、もう少し様子を見る。
    221                        while( ch <= ' ' && ++position < maxPosition ) {
    222                                ch = line.charAt( position );
    223                        }
    224
    225                        // 最後がカンマでなければ、進めすぎている。
    226                        currentPosition = ch == ',' || position == maxPosition ? position+1 : position ;
    227                }
    228
    229                return list;
    230        }
    231
    232        /**
    233         * 先頭文字が、'#' の行を削除した文字列を返します。
    234         *
    235         * このメソッド上で、#NAME があれば、カラム配列を作成します。
    236         * カラム配列は、最初の一度のみ、セット可能とします。
    237         *
    238         * @param       line １行分の文字列(not null)
    239         * @return      コメント削除後の行
    240         * @throws      NullPointerException 引数lineが、nullの場合。
    241         */
    242        public String cmntCut( final String line ) {
    243                final boolean isCmnt = !line.isEmpty() && line.charAt(0) == '#';
    244
    245                // カラム配列は、最初の一度のみ、セット可能とします。
    246                if( isCmnt && clms.length == 0 && line.toUpperCase( Locale.JAPAN ).startsWith( NAME_KEY ) ) {
    247                        final String sep = line.substring( NAME_LEN,NAME_LEN+1 );               // 区切り文字。タブかカンマ
    248                        clms = line.split( sep );                                                                               // 区切り文字で配列化します。
    249                        clms[0] = "";                                                                                                   // 統一的に処理を行うため、#NAME を、ゼロ文字列化します。
    250                }
    251
    252                return isCmnt ? "" : line;
    253
    254        }
    255
    256        /** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
    257        public static final String USAGE = "Usage: java jp.euromap.eu63.util.LineSplitter inFile [-help]" ;
    258
    259        /**
    260         * 処理を実行する main メソッドです。
    261         *
    262         * LineSplitter は、１行分のデータを順次分割するクラスです。
    263         *
    264         * 応答ファイルは、次の２タイプがあります。
    265         *  ① 『;』で行分割して、スペースでカラム分割する。
    266         *         ***.RSP , **.LOG , UPLOAD 応答ファイル
    267         *  ② 『改行』で行分割して、カンマでカラム分割する。
    268         *         REPORT , EVENT , GETID 応答ファイル
    269         *
    270         * {@value #USAGE}
    271         *
    272         * @param       args    コマンド引数配列
    273         */
    274        public static void main( final String[] args ) {
    275                // ********** 【整合性チェック】 **********
    276                if( args.length < 1 ) {
    277                        System.out.println( USAGE );
    278                        return;
    279                }
    280
    281                // ********** 【引数定義】 **********
    282                String  inFile          = null;                                 // 処理する入力ファイル
    283
    284                // ********** 【引数処理】 **********
    285                for( final String arg : args ) {
    286                        if(      "-help" .equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
    287                        else {
    288                                inFile = arg;
    289                        }
    290                }
    291
    292                // ********** 【本体処理】 **********
    293                final Path inPath = FileUtil.readPath( inFile );
    294
    295                final LineSplitter split = new LineSplitter();
    296                split.forEach(
    297                        inPath ,                                        // 読み込みファイル
    298                        list -> System.out.println(
    299                                                list.stream().collect( java.util.stream.Collectors.joining( "\t" )
    300                                        )
    301                        )
    302                );
    303        }
    304}