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系、&#47;&#47; , /&#042; , &#042;/
206         * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
207         * ORACLE系             -- , /&#042; , &#042;/
208         * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
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系、&#47;&#47; , /&#042; , &#042;/
235                 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
236                 * ORACLE系             -- , /&#042; , &#042;/
237                 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
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}