    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;                                                      // 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
    030
    031import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    032
    033/**
    034 * CommentLineParser.java は、ファイルを行単位に処理して、コメントを除去するクラスです。
    035 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
    036 *
    037 * ブロックコメントの状態や、コメント除外の状態を管理しています。
    038 * オブジェクト作成後、line( String ) メソッドに、ファイルから読み取った１行分の文字列を渡せば、
    039 * コメントが除外された形で返されます。
    040 *
    041 * コメントが除去された行は、rTrim しますが、行の削除は行いません。
    042 * これは、Grep等で、文字列を発見した場合に、ファイルの行番号がずれるのを防ぐためです。
    043 * 逆に、Diff等で、複数のコメント行は、１行の空行にしたい場合や、空行自体をなくして
    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         * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
    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 １行の文字列
    135         * @return      コメント削除後の１行の文字列
    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         * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
    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  １行の文字列
    154         * @param       cmntSet コメントを管理するオブジェクト
    155         * @return      コメント削除後の１行の文字列
    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
    162                final StringBuilder buf = new StringBuilder( size );
    163
    164                for( int st=0; st<size; st++ ) {
    165                        final char ch = inLine.charAt(st);
    166
    167                        if( !cmntSet.checkEsc( ch ) ) {                                                 // エスケープ文字でないなら、判定処理を進める
    168                                // ブロックコメント継続中か、先頭がブロックコメント
    169                                if( cmntSet.isBlockIn( inLine,st ) ) {
    170                                        final int ed = cmntSet.blockOut( inLine,st ) ;  // 終了を見つける
    171                                        if( ed >= 0 ) {                                                                 // 終了があれば、そこまで進める。
    172                                                st = ed;
    173                                                continue;                                                                       // ブロックコメント脱出。再読み込み
    174                                        }
    175                                        break;                                                                                  // ブロックコメント継続中。次の行へ
    176                                }
    177
    178                                // ラインコメント発見。次の行へ
    179                                if( cmntSet.isLineCmnt( inLine,st ) ) { break; }
    180                        }
    181
    182                        // 通常の文字なので、追加する。
    183                        buf.append( ch );
    184                }
    185
    186                // rTrim() と同等の処理
    187                int len = buf.length();
    188                while( 0 < len && buf.charAt(len-1) <= ' ' ) {
    189                        len--;
    190                }
    191                buf.setLength( len );
    192
    193                return buf.toString() ;
    194        }
    195
    196        /**
    197         * コメントセットを管理する内部クラスです。
    198         *
    199         * コメントの種類を指定します。
    200         *
    201         * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
    202         * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
    203         * ORACLE系             -- , /&#042; , &#042;/
    204         * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
    205         *
    206         * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
    207         * @og.rev 6.4.0.2 (2015/12/11) CommentSet の見直し。
    208         * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    209         */
    210        private static final class CommentSet {
    211                private final String    LINE_CMNT       ;                       // ラインコメント
    212                private final String    BLOCK_CMNT1     ;                       // ブロックコメントの開始
    213                private final String[]  BLOCK_CMNT2     ;                       // ブロックコメントの終了
    214
    215                private static final char ESC_CHAR1 = '"'; ;    // コメント判定除外("")
    216                private static final char ESC_CHAR2 = '\'' ;    // コメント判定除外('')
    217                private static final char CHAR_ESC  = '\\' ;    // エスケープ文字('\\')
    218
    219                private boolean useCharEsc ;                                    // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    220
    221                private boolean escIn1   ;                                              // コメント判定除外中かどうか("")
    222                private boolean escIn2   ;                                              // コメント判定除外中かどうか('')
    223                private boolean chEsc    ;                                              // コメント除外のエスケープ処理中かどうか
    224
    225                private boolean isBlkIn ;                                               // ブロックコメントが継続しているかどうか          6.4.1.1 (2016/01/16) refactoring isBlockIn → isBlkIn
    226
    227                /**
    228                 * コメントの種類を指定するコンストラクタです。
    229                 *
    230                 * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
    231                 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
    232                 * ORACLE系             -- , /&#042; , &#042;/
    233                 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
    234                 *
    235                 * @param       lineCmnt        ラインコメント
    236                 * @param       blockCmnt1      ブロックコメントの開始
    237                 * @param       blockCmnt2      ブロックコメントの終了(可変長配列)
    238                 */
    239                CommentSet( final String lineCmnt,final String blockCmnt1,final String... blockCmnt2 ) {
    240                        LINE_CMNT   = lineCmnt ;                // ラインコメント
    241                        BLOCK_CMNT1 = blockCmnt1 ;              // ブロックコメントの開始
    242                        BLOCK_CMNT2 = blockCmnt2 ;              // ブロックコメントの終了
    243                }
    244
    245                /**
    246                 * エスケープ文字を使用する設定を行います。
    247                 *
    248                 * 本来は、コンストラクタで、指示すべきですが、String... 定義を使っているため、
    249                 * 最後に、引数を追加できません。かといって、途中や先頭に入れるのも、嫌です。
    250                 * とりあえず、メソッドを作成して、自分自身を返せば、List にセットする処理は、
    251                 * そのまま使用できるため、このような形にしています。
    252                 *
    253                 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    254                 *
    255                 * @return      自分自身
    256                 */
    257                /* default */ CommentSet useEsc() {
    258                        useCharEsc = true;
    259                        return this;
    260                }
    261
    262                /**
    263                 * ブロック外で、エスケープ文字の場合は、内外反転します。
    264                 *
    265                 * @og.rev 6.4.1.1 (2016/01/16) Avoid if (x != y) ..; else ..; refactoring
    266                 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    267                 *
    268                 * @param       ch      チェックするコメント除外char
    269                 * @return      エスケープ文字中の場合は、true
    270                 */
    271                /* default */ boolean checkEsc( final char ch ) {
    272                        if( isBlkIn || chEsc ) {
    273                                chEsc = false;
    274                        }
    275                        else {
    276//                              chEsc = CHAR_ESC == ch;
    277                                chEsc = useCharEsc && CHAR_ESC == ch;                                           // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    278                                if( !escIn2 && ESC_CHAR1 == ch ) { escIn1 = !escIn1 ; }         // escIn2 でない場合に、escIn1 の判定を行う。
    279                                if( !escIn1 && ESC_CHAR2 == ch ) { escIn2 = !escIn2 ; }         // 同様にその逆
    280                        }
    281                        return escIn1 || escIn2;        // どちらかで、エスケープ中
    282                }
    283
    284                /**
    285                 * ブロックコメント中かどうかの判定を行います。
    286                 *
    287                 * @og.rev 6.4.5.1 (2016/04/28) ブロックコメントを指定しないケースに対応。
    288                 *
    289                 * @param       line    チェックする行データ
    290                 * @param       st              チェック開始文字数
    291                 * @return      ブロックコメント中の場合は、true を返します。
    292                 */
    293                /* default */ boolean isBlockIn( final String line , final int st ) {
    294                        if( !isBlkIn && BLOCK_CMNT1 != null ) { isBlkIn = line.startsWith( BLOCK_CMNT1,st ); }
    295
    296                        return isBlkIn ;
    297                }
    298
    299                /**
    300                 * ラインコメントかどうかの判定を行います。
    301                 *
    302                 * @param       line    チェックする行データ
    303                 * @param       st              チェック開始文字数
    304                 * @return      ラインコメントの場合は、true を返します。
    305                 */
    306                /* default */ boolean isLineCmnt( final String line , final int st ) {
    307                        return LINE_CMNT != null && line.startsWith( LINE_CMNT,st ) ;
    308                }
    309
    310                /**
    311                 * ブロックコメントの終了を見つけます。
    312                 * 終了は、複数指定でき、それらのもっとも最初に現れる方が有効です。
    313                 * 例：XMLタグで、BODYが、あるなしで、終了条件が異なるケースなど。
    314                 * この処理では、ブロックコメントが継続中かどうかの判定は行っていないため、
    315                 * 外部(呼び出し元)で、判定処理してください。
    316                 *
    317                 * ※ このメソッドは、ブロックコメント中にしか呼ばれないため、
    318                 *    ブロックコメントを指定しないケース(BLOCK_CMNT1==null)では呼ばれません。
    319                 *
    320                 * @og.rev 8.0.0.0 (2021/09/30) ブロックコメントの終了処理を修正
    321                 *
    322                 * @param       line    チェックする行データ
    323                 * @param       st              チェック開始文字数
    324                 * @return      ブロックコメントの終了の位置。なければ、-1
    325                 */
    326                /* default */ int blockOut( final String line , final int st ) {
    327                        int ed = line.length();
    328                        for( final String key : BLOCK_CMNT2 ) {
    329        //                      final int tmp = line.indexOf( key,st + BLOCK_CMNT1.length() );  // 6.4.1.0 (2016/01/09) 開始位置の計算ミス
    330        //                      if( tmp >= 0 && tmp < ed ) {    // 存在して、かつ小さい方を選ぶ。
    331        //                              ed = tmp + key.length();        // アドレスは、終了コメント記号の後ろまで。
    332                //              final int tmp = line.indexOf( key,st );                                                 // 8.0.0.0 (2021/09/30) 開始位置の計算ミス
    333                //              if( tmp >= 0 ) {                                                        // 存在した場合は、
    334                //                      ed = Math.min(ed,tmp+key.length());             // 小さい方を選ぶ。
    335                //                      isBlkIn = false;                                                // 見つかった事をキープしておく
    336
    337                                // 8.0.0.0 (2021/09/30) 開始位置の計算ミス
    338                                final int tmp;
    339                                // チェックする行データにブロックコメントの開始が存在する
    340                                if ( line.indexOf( BLOCK_CMNT1,st ) >= 0 ) {
    341                                        tmp = line.indexOf( key,st + BLOCK_CMNT1.length() );
    342                                // チェックする行データにブロックコメントの開始が存在しない
    343                                } else {
    344                                        tmp = line.indexOf( key,st );
    345                                }
    346
    347                                // ブロックコメントの終了が存在する
    348                                if ( tmp >= 0 ) {
    349                                        ed = Math.min(ed,tmp+key.length());                                                     // 小さい方を選ぶ
    350                                        isBlkIn = false;                                                                                        // 見つかった事をキープしておく
    351                                }
    352                        }
    353
    354                        return isBlkIn ? -1 : ed ;              // 見つからない(継続中)場合は、-1 を返す。
    355                }
    356        }
    357
    358        /**
    359         * このクラスの動作確認用の、main メソッドです。
    360         *
    361         * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]
    362         *
    363         * -rowTrim を指定すると、空行も削除します。これは、コメントの増減は、ソースレベルで比較する場合に
    364         * 関係ないためです。デフォルトは、空行は削除しません。grep 等で検索した場合、オリジナルの
    365         * ソースの行数と一致させるためです。
    366         *
    367         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
    368         * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
    369         *
    370         * @param       args    コマンド引数配列
    371         */
    372        public static void main( final String[] args ) {
    373                if( args.length < 2 ) {
    374                        System.out.println( "Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]" );
    375                }
    376
    377                final File inFile  = new File( args[0] );
    378                final File outFile = new File( args[1] );
    379                String  encode  = "UTF-8" ;
    380                boolean rowTrim = false;
    381                for( int i=2; i<args.length; i++ ) {
    382                        if( "-rowTrim".equalsIgnoreCase( args[i] ) ) { rowTrim = true; }
    383                        else { encode = args[i]; }
    384                }
    385
    386                final BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode );
    387                final PrintWriter    writer = FileUtil.getPrintWriter(   outFile ,encode );
    388
    389                final CommentLineParser clp = new CommentLineParser( FileInfo.getSUFIX( inFile ) );
    390
    391                try {
    392                        String line1;
    393                        while((line1 = reader.readLine()) != null) {
    394                                line1 = clp.line( line1 );
    395                                if( !rowTrim || !line1.isEmpty() ) {
    396                                        writer.println( line1 );
    397                                }
    398                        }
    399                }
    400                // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
    401                catch( final CharacterCodingException ex ) {
    402                        final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
    403                                                                +       "  ファイルのエンコードが指定のエンコードと異なります。" + CR
    404                                                                +       " [" + inFile.getPath() + "] , Encode=[" + encode + "]" ;
    405                        throw new OgCharacterException( errMsg,ex );    // 6.5.0.1 (2016/10/21)
    406                }
    407                catch( final IOException ex ) {
    408                        final String errMsg = "ファイルコピー中に例外が発生しました。\n"
    409                                                + " inFile=[" + inFile + "] , outFile=[" + outFile + "]\n" ;
    410                        throw new OgRuntimeException( errMsg,ex );
    411                }
    412                finally {
    413                        Closer.ioClose( reader ) ;
    414                        Closer.ioClose( writer ) ;
    415                }
    416        }
    417}