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 は、1行分のデータを順次分割するクラスです。 028 * 029 *<pre> 030 * ファイルは、『改行』で行分割して、カンマかタブでカラム分割します。 031 * 032 * 応答ファイルの解析処理を簡素化するため、以下のルール(禁止事項)を定めます。 033 * 1.ダブルクオートの中に、ダブルクオート、改行、を含まないこと。 034 * (カンマとスペースは含めることが出来ます。) 035 * 2.1行の定義は、『改行』とします。 036 * 3.スペース分割時は、複数スペースの場合でも、1つの区切り文字として扱います。 037 * (A B C D → 「A」、「B」、「C」、「D」 に分割されます。) 038 * 4.カンマ分割は、ダブルクオート間のカンマは分解しません。 039 * 混在した場合でも、最初に見つけた方が優先されます。 040 * 5.カンマ分割時は、複数カンマの場合は、それぞれ空文字列に分割されます。 041 * カンマ分割後、それぞれの文字列は、前後スペースを削除(trim)します。 042 * (A, B , , C , D → 「A」、「B」、「」、「C」、「D」 に分割されます。) 043 * 6.カラム分解後の、ダブルクオートは、削除します。 044 * ((A, B , , "CC C,C" , D → 「A」、「B」、「」、「CC C,C」、「D」 に分割されます。) 045 * 046 * 処理手順 047 * 1.ファイルより、1行づつ(改行コードで分割)読み込みます。 048 * 2.読み込んだ1行について、先頭が、『#』の行はコメント行としてスキップします。 049 * 3.先頭から、区切り文字(スペースかカンマかタブ)が見つかるまでを、1カラムとして取得します。 050 * 4.その間、ダブルクオートが見つかったら、次のダブルクオートまで、取り込みます。 051 * 5.カラム分割された単語の前後スペースと、前後ダブルクオートを削除します。 052 * trim()が先で、ダブルクオートの削除は、後から行います。(ダブルクオート内のtrim()は行いません。) 053 * 6.個々のカラムを配列にして返します。 054 * 7.これを、ファイルが終了するまで繰り返します。 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<>(); // 1行分の分割したトークンのリスト 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 * 1行づつ処理を行った結果のトークンをConsumerにセットする繰り返しメソッドです。 131 * 1行単位に、Consumer#action が呼ばれます。 132 * セットされるリストは、1行をトークンに分割したリストで、空行の場合は、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 * 1行分の分割したトークンのリストを返します。 156 * 157 * ファイルの読み込みを、単独または、別に行った場合に、1行データとして、処理できます。 158 * このクラスの特徴である、先頭が、『#』の行は、コメントとみなして、削除します。 159 * 1行分をtrim()する処理も、行います。 160 * trim()の結果が、空文字列のみの場合は、空のリストオブジェクトを返します。 161 * 162 * @param orgLine 1行データ(オリジナル) 163 * @return 1行分の分割したトークンのリスト(行末、コメント、trim処理済み) 164 */ 165 public List<String> split( final String orgLine ) { 166 final List<String> list = new ArrayList<>(); // 1行分の分割したトークンのリスト 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 1行分の文字列(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 は、1行分のデータを順次分割するクラスです。 263 * 264 * 応答ファイルは、次の2タイプがあります。 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}