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.util; 017 018import java.io.BufferedReader; 019import java.io.PrintWriter; 020import java.io.File; 021import java.io.IOException; 022import java.util.List; // 6.3.1.1 (2015/07/10) 023import java.util.Arrays; // 6.3.1.1 (2015/07/10) 024import java.nio.charset.CharacterCodingException; // 6.3.1.0 (2015/06/28) 025import java.util.Locale; // 6.4.0.2 (2015/12/11) 026 027import org.opengion.fukurou.system.OgRuntimeException ; // 6.4.2.0 (2016/01/29) 028import org.opengion.fukurou.system.OgCharacterException ; // 6.5.0.1 (2016/10/21) 029import org.opengion.fukurou.system.Closer; // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応 030 031import static org.opengion.fukurou.system.HybsConst.CR; // 6.1.0.0 (2014/12/26) refactoring 032 033/** 034 * CommentLineParser.java は、ファイルを行単位に処理して、コメントを除去するクラスです。 035 * 1行分の文字列を読み取って、コメント部分を削除した文字列を返します。 036 * 037 * ブロックコメントの状態や、コメント除外の状態を管理しています。 038 * オブジェクト作成後、line( String ) メソッドに、ファイルから読み取った1行分の文字列を渡せば、 039 * コメントが除外された形で返されます。 040 * 041 * コメントが除去された行は、rTrim しますが、行の削除は行いません。 042 * これは、Grep等で、文字列を発見した場合に、ファイルの行番号がずれるのを防ぐためです。 043 * 逆に、Diff等で、複数のコメント行は、1行の空行にしたい場合や、空行自体をなくして 044 * 比較したい場合は、戻ってきた行が、空行かどうかで判定して呼び出し元で処理してください。 045 * 046 * 引数の行文字列が、null の場合は、null を返します。(読み取り行がなくなった場合) 047 * 048 * 文字列くくり指定 は、例えば、ラインコメント(//) が、文字列指定("//") や、"http://xxxx" などの 049 * プログラム本文で使用する場合のエスケープ処理になります。 050 * つまり、文字列くくり指定についても、IN-OUT があり、その範囲内は、コメント判定外になります。 051 * 052 * ※ 6.3.1.1 (2015/07/10) 053 * コメントセットを、add で、追加していく機能を用意します。 054 * 現状では、Java,ORACLE,HTML のコメントを意識せず処理したいので、すべてを 055 * 処理することを前提に考えておきます。 056 * 057 * ※ 6.4.0.2 (2015/12/11) 058 * 行コメントが先頭行のみだったのを修正します。 059 * og:comment タグを除外できるようにします。そのため、 060 * 終了タグに、OR 条件を加味する必要があるため、CommentSet クラスを見直します。 061 * 可変長配列を使うため、文字列くくり指定を前に持ってきます。 062 * 063 * @og.rev 5.7.4.0 (2014/03/07) 新規追加 064 * @og.rev 6.3.1.1 (2015/07/10) 内部構造大幅変更 065 * @og.group ユーティリティ 066 * 067 * @version 6.0 068 * @author Kazuhiko Hasegawa 069 * @since JDK7.0, 070 */ 071public class CommentLineParser { 072 private final List<CommentSet> cmntSetList ; 073 074 /** 075 * 処理するコメントの種類を拡張子で指定するコンストラクターです。 076 * これは、ORACLE系のラインコメント(--)が、Java系の演算子(i--;など)と 077 * 判定されるため、ひとまとめに処理できません。 078 * ここで指定する拡張子に応じて、CommentSet を割り当てます。 079 * 080 * ・sql , tri , spc は、ORACLE系を使用。 081 * ・xml , htm , html , は、Java,C,JavaScript系 + HTML,XML系を使用。 082 * ・jsp は、Java,C,JavaScript系 + HTML,XML系 + ORACLE系 + openGion JSP系 を使用。 083 * ・それ以外は、Java,C,JavaScript系を使用。 084 * css は、それ以外になりますが、//(ラインコメント)はありませんが、コメントアウトされます。 085 * 086 * @og.rev 6.4.0.2 (2015/12/11) sufix によるコメント処理方法の変更。 087 * @og.rev 6.4.1.0 (2016/01/09) comment="***"のコメント処理方法の追加。 088 * @og.rev 6.4.1.1 (2016/01/16) sufixを小文字化。 089 * @og.rev 6.8.1.7 (2017/10/13) COMMENT ON で始まる行(大文字限定)は、コメントとして扱う 090 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 091 * 092 * @param sufix 拡張子 093 */ 094 public CommentLineParser( final String sufix ) { 095 final String type = sufix == null ? "null" : sufix.toLowerCase( Locale.JAPAN ); 096 097 if( "sql , tri , spc".contains( type ) ) { 098 cmntSetList = Arrays.asList( 099 new CommentSet( "--" , "/*" , "*/" ) // ORACLE系 100 , new CommentSet( "COMMENT ON" , null , (String)null ) // 大文字のみ除外する。 101 ); 102 } 103 else if( "xml , htm , html".contains( type ) ) { 104 cmntSetList = Arrays.asList( 105 new CommentSet( "//" , "/*" , "*/" ) // Java,C,JavaScript系 106 , new CommentSet( null , "<!--" , "-->" ) // HTML,XML系 107 ); 108 } 109 else if( "jsp".contains( type ) ) { 110 cmntSetList = Arrays.asList( 111 new CommentSet( "//" , "/*" , "*/" ) // Java,C,JavaScript系 112 , new CommentSet( null , "<!--" , "-->" ) // HTML,XML系 113 , new CommentSet( "--" , "/*" , "*/" ) // ORACLE系 114 , new CommentSet( null , "<og:comment" , "/>" , "</og:comment>" ) // openGion JSP系 XML なので、このまま。 115 , new CommentSet( null , "comment=\"" , "\"" ) // openGion comment="***" 6.4.1.0 (2016/01/09) 116 ); 117 } 118 else { 119 cmntSetList = Arrays.asList( 120 // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 121// new CommentSet( "//" , "/*" , "*/" ) // Java,C,JavaScript系 122 new CommentSet( "//" , "/*" , "*/" ).useEsc() // Java,C,JavaScript系 123 ); 124 } 125 } 126 127 /** 128 * 1行分の文字列を読み取って、コメント部分を削除した文字列を返します。 129 * 行として存在しない場合は、null を返します。 130 * 131 * @og.rev 5.7.4.0 (2014/03/07) 新規追加 132 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。 133 * 134 * @param inLine 1行の文字列 135 * @return コメント削除後の1行の文字列 136 */ 137 public String line( final String inLine ) { 138 139 String outLine = inLine ; 140 for( final CommentSet cmntSet : cmntSetList ) { 141 outLine = line( outLine,cmntSet ); 142 } 143 return outLine ; 144 } 145 146 /** 147 * 1行分の文字列を読み取って、コメント部分を削除した文字列を返します。 148 * 行として存在しない場合は、null を返します。 149 * 150 * @og.rev 5.7.4.0 (2014/03/07) 新規追加 151 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。 152 * 153 * @param inLine 1行の文字列 154 * @param cmntSet コメントを管理するオブジェクト 155 * @return コメント削除後の1行の文字列 156 */ 157 private String line( final String inLine , final CommentSet cmntSet ) { 158 if( inLine == null ) { return null; } 159 160 final int size = inLine.length(); 161 final StringBuilder buf = new StringBuilder( size ); 162 163 // 8.5.4.2 (2024/01/12) PMD 7.0.0 AvoidReassigningLoopVariables 164// for( int st=0; st<size; st++ ) { 165 int st = 0; 166 while( st<size ) { 167 final char ch = inLine.charAt(st); 168 169 if( !cmntSet.checkEsc( ch ) ) { // エスケープ文字でないなら、判定処理を進める 170 // ブロックコメント継続中か、先頭がブロックコメント 171 if( cmntSet.isBlockIn( inLine,st ) ) { 172 final int ed = cmntSet.blockOut( inLine,st ) ; // 終了を見つける 173 if( ed >= 0 ) { // 終了があれば、そこまで進める。 174// st = ed; 175 st = ed + 1; // 8.5.4.2 (2024/01/12) for → while にしたので、1 加算 176 continue; // ブロックコメント脱出。再読み込み 177 } 178 break; // ブロックコメント継続中。次の行へ 179 } 180 181 // ラインコメント発見。次の行へ 182 if( cmntSet.isLineCmnt( inLine,st ) ) { break; } 183 } 184 185 // 通常の文字なので、追加する。 186 buf.append( ch ); 187 st++ ; // 8.5.4.2 (2024/01/12) for → while にしたので、1 加算 188 } 189 190 // rTrim() と同等の処理 191 int len = buf.length(); 192 while( 0 < len && buf.charAt(len-1) <= ' ' ) { 193 len--; 194 } 195 buf.setLength( len ); 196 197 return buf.toString() ; 198 } 199 200 /** 201 * コメントセットを管理する内部クラスです。 202 * 203 * コメントの種類を指定します。 204 * 205 * Java,C,JavaScript系、// , /* , */ 206 * HTML,XML系、 // , <!-- , --> 207 * ORACLE系 -- , /* , */ 208 * openGion JSP系 null , <og:comment , /> ,</og:comment> 209 * 210 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。 211 * @og.rev 6.4.0.2 (2015/12/11) CommentSet の見直し。 212 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 213 */ 214 private static final class CommentSet { 215 private final String LINE_CMNT ; // ラインコメント 216 private final String BLOCK_CMNT1 ; // ブロックコメントの開始 217 private final String[] BLOCK_CMNT2 ; // ブロックコメントの終了 218 219 private static final char ESC_CHAR1 = '"' ; // コメント判定除外("") 220 private static final char ESC_CHAR2 = '\'' ; // コメント判定除外('') 221 private static final char CHAR_ESC = '\\' ; // エスケープ文字('\\') 222 223 private boolean useCharEsc ; // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 224 225 private boolean escIn1 ; // コメント判定除外中かどうか("") 226 private boolean escIn2 ; // コメント判定除外中かどうか('') 227 private boolean chEsc ; // コメント除外のエスケープ処理中かどうか 228 229 private boolean isBlkIn ; // ブロックコメントが継続しているかどうか 6.4.1.1 (2016/01/16) refactoring isBlockIn → isBlkIn 230 231 /** 232 * コメントの種類を指定するコンストラクタです。 233 * 234 * Java,C,JavaScript系、// , /* , */ 235 * HTML,XML系、 // , <!-- , --> 236 * ORACLE系 -- , /* , */ 237 * openGion JSP系 null , <og:comment , /> ,</og:comment> 238 * 239 * @param lineCmnt ラインコメント 240 * @param blockCmnt1 ブロックコメントの開始 241 * @param blockCmnt2 ブロックコメントの終了(可変長配列) 242 */ 243 /* default */ CommentSet( final String lineCmnt,final String blockCmnt1,final String... blockCmnt2 ) { 244 LINE_CMNT = lineCmnt ; // ラインコメント 245 BLOCK_CMNT1 = blockCmnt1 ; // ブロックコメントの開始 246 BLOCK_CMNT2 = blockCmnt2 ; // ブロックコメントの終了 247 } 248 249 /** 250 * エスケープ文字を使用する設定を行います。 251 * 252 * 本来は、コンストラクタで、指示すべきですが、String... 定義を使っているため、 253 * 最後に、引数を追加できません。かといって、途中や先頭に入れるのも、嫌です。 254 * とりあえず、メソッドを作成して、自分自身を返せば、List にセットする処理は、 255 * そのまま使用できるため、このような形にしています。 256 * 257 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 258 * 259 * @return 自分自身 260 */ 261 /* default */ CommentSet useEsc() { 262 useCharEsc = true; 263 return this; 264 } 265 266 /** 267 * ブロック外で、エスケープ文字の場合は、内外反転します。 268 * 269 * @og.rev 6.4.1.1 (2016/01/16) Avoid if (x != y) ..; else ..; refactoring 270 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 271 * 272 * @param ch チェックするコメント除外char 273 * @return エスケープ文字中の場合は、true 274 */ 275 /* default */ boolean checkEsc( final char ch ) { 276 if( isBlkIn || chEsc ) { 277 chEsc = false; 278 } 279 else { 280// chEsc = CHAR_ESC == ch; 281 chEsc = useCharEsc && CHAR_ESC == ch; // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否 282 if( !escIn2 && ESC_CHAR1 == ch ) { escIn1 = !escIn1 ; } // escIn2 でない場合に、escIn1 の判定を行う。 283 if( !escIn1 && ESC_CHAR2 == ch ) { escIn2 = !escIn2 ; } // 同様にその逆 284 } 285 return escIn1 || escIn2; // どちらかで、エスケープ中 286 } 287 288 /** 289 * ブロックコメント中かどうかの判定を行います。 290 * 291 * @og.rev 6.4.5.1 (2016/04/28) ブロックコメントを指定しないケースに対応。 292 * 293 * @param line チェックする行データ 294 * @param st チェック開始文字数 295 * @return ブロックコメント中の場合は、true を返します。 296 */ 297 /* default */ boolean isBlockIn( final String line , final int st ) { 298 if( !isBlkIn && BLOCK_CMNT1 != null ) { isBlkIn = line.startsWith( BLOCK_CMNT1,st ); } 299 300 return isBlkIn ; 301 } 302 303 /** 304 * ラインコメントかどうかの判定を行います。 305 * 306 * @param line チェックする行データ 307 * @param st チェック開始文字数 308 * @return ラインコメントの場合は、true を返します。 309 */ 310 /* default */ boolean isLineCmnt( final String line , final int st ) { 311 return LINE_CMNT != null && line.startsWith( LINE_CMNT,st ) ; 312 } 313 314 /** 315 * ブロックコメントの終了を見つけます。 316 * 終了は、複数指定でき、それらのもっとも最初に現れる方が有効です。 317 * 例:XMLタグで、BODYが、あるなしで、終了条件が異なるケースなど。 318 * この処理では、ブロックコメントが継続中かどうかの判定は行っていないため、 319 * 外部(呼び出し元)で、判定処理してください。 320 * 321 * ※ このメソッドは、ブロックコメント中にしか呼ばれないため、 322 * ブロックコメントを指定しないケース(BLOCK_CMNT1==null)では呼ばれません。 323 * 324 * @og.rev 8.0.0.0 (2021/09/30) ブロックコメントの終了処理を修正 325 * 326 * @param line チェックする行データ 327 * @param st チェック開始文字数 328 * @return ブロックコメントの終了の位置。なければ、-1 329 */ 330 /* default */ int blockOut( final String line , final int st ) { 331 int ed = line.length(); 332 for( final String key : BLOCK_CMNT2 ) { 333 // final int tmp = line.indexOf( key,st + BLOCK_CMNT1.length() ); // 6.4.1.0 (2016/01/09) 開始位置の計算ミス 334 // if( tmp >= 0 && tmp < ed ) { // 存在して、かつ小さい方を選ぶ。 335 // ed = tmp + key.length(); // アドレスは、終了コメント記号の後ろまで。 336 // final int tmp = line.indexOf( key,st ); // 8.0.0.0 (2021/09/30) 開始位置の計算ミス 337 // if( tmp >= 0 ) { // 存在した場合は、 338 // ed = Math.min(ed,tmp+key.length()); // 小さい方を選ぶ。 339 // isBlkIn = false; // 見つかった事をキープしておく 340 341 // 8.0.0.0 (2021/09/30) 開始位置の計算ミス 342 final int tmp; 343 // チェックする行データにブロックコメントの開始が存在する 344 if ( line.indexOf( BLOCK_CMNT1,st ) >= 0 ) { 345 tmp = line.indexOf( key,st + BLOCK_CMNT1.length() ); 346 // チェックする行データにブロックコメントの開始が存在しない 347 } else { 348 tmp = line.indexOf( key,st ); 349 } 350 351 // ブロックコメントの終了が存在する 352 if ( tmp >= 0 ) { 353 ed = Math.min(ed,tmp+key.length()); // 小さい方を選ぶ 354 isBlkIn = false; // 見つかった事をキープしておく 355 } 356 } 357 358 return isBlkIn ? -1 : ed ; // 見つからない(継続中)場合は、-1 を返す。 359 } 360 } 361 362 /** 363 * このクラスの動作確認用の、main メソッドです。 364 * 365 * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim] 366 * 367 * -rowTrim を指定すると、空行も削除します。これは、コメントの増減は、ソースレベルで比較する場合に 368 * 関係ないためです。デフォルトは、空行は削除しません。grep 等で検索した場合、オリジナルの 369 * ソースの行数と一致させるためです。 370 * 371 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 372 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。 373 * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応 374 * 375 * @param args コマンド引数配列 376 */ 377 public static void main( final String[] args ) { 378 if( args.length < 2 ) { 379 System.out.println( "Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]" ); 380 } 381 382 final File inFile = new File( args[0] ); 383 final File outFile = new File( args[1] ); 384 String encode = "UTF-8" ; 385 boolean rowTrim = false; 386 for( int i=2; i<args.length; i++ ) { 387 if( "-rowTrim".equalsIgnoreCase( args[i] ) ) { rowTrim = true; } 388 else { encode = args[i]; } 389 } 390 391 final BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode ); 392 final PrintWriter writer = FileUtil.getPrintWriter( outFile ,encode ); 393 394 final CommentLineParser clp = new CommentLineParser( FileInfo.getSUFIX( inFile ) ); 395 396 // 8.5.4.2 (2024/01/12) PMD 7.0.0 CloseResource 対応 397 try { 398// try ( BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode ); 399// PrintWriter writer = FileUtil.getPrintWriter( outFile ,encode ) ) { 400 String line1; 401 while((line1 = reader.readLine()) != null) { 402 line1 = clp.line( line1 ); 403 if( !rowTrim || !line1.isEmpty() ) { 404 writer.println( line1 ); 405 } 406 } 407 } 408 // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。 409 catch( final CharacterCodingException ex ) { 410 final String errMsg = "文字のエンコード・エラーが発生しました。" + CR 411 + " ファイルのエンコードが指定のエンコードと異なります。" + CR 412 + " [" + inFile.getPath() + "] , Encode=[" + encode + "]" ; 413 throw new OgCharacterException( errMsg,ex ); // 6.5.0.1 (2016/10/21) 414 } 415 catch( final IOException ex ) { 416 final String errMsg = "ファイルコピー中に例外が発生しました。\n" 417 + " inFile=[" + inFile + "] , outFile=[" + outFile + "]\n" ; 418 throw new OgRuntimeException( errMsg,ex ); 419 } 420 finally { 421 Closer.ioClose( reader ) ; 422 Closer.ioClose( writer ) ; 423 } 424 } 425}