    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系
    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
    216                private static final char ESC_CHAR1 = '"'; ;    // コメント判定除外("")
    217                private static final char ESC_CHAR2 = '\'' ;    // コメント判定除外('')
    218                private static final char CHAR_ESC  = '\\' ;    // エスケープ文字('\\')
    219
    220                private boolean useCharEsc ;                                    // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    221
    222                private boolean escIn1   ;                                              // コメント判定除外中かどうか("")
    223                private boolean escIn2   ;                                              // コメント判定除外中かどうか('')
    224                private boolean chEsc    ;                                              // コメント除外のエスケープ処理中かどうか
    225
    226                private boolean isBlkIn ;                                               // ブロックコメントが継続しているかどうか          6.4.1.1 (2016/01/16) refactoring isBlockIn → isBlkIn
    227
    228                /**
    229                 * コメントの種類を指定するコンストラクタです。
    230                 *
    231                 * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
    232                 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
    233                 * ORACLE系             -- , /&#042; , &#042;/
    234                 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
    235                 *
    236                 * @param       lineCmnt        ラインコメント
    237                 * @param       blockCmnt1      ブロックコメントの開始
    238                 * @param       blockCmnt2      ブロックコメントの終了(可変長配列)
    239                 */
    240                CommentSet( final String lineCmnt,final String blockCmnt1,final String... blockCmnt2 ) {
    241                        LINE_CMNT   = lineCmnt ;                // ラインコメント
    242                        BLOCK_CMNT1 = blockCmnt1 ;              // ブロックコメントの開始
    243                        BLOCK_CMNT2 = blockCmnt2 ;              // ブロックコメントの終了
    244                }
    245
    246                /**
    247                 * エスケープ文字を使用する設定を行います。
    248                 *
    249                 * 本来は、コンストラクタで、指示すべきですが、String... 定義を使っているため、
    250                 * 最後に、引数を追加できません。かといって、途中や先頭に入れるのも、嫌です。
    251                 * とりあえず、メソッドを作成して、自分自身を返せば、List にセットする処理は、
    252                 * そのまま使用できるため、このような形にしています。
    253                 *
    254                 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    255                 *
    256                 * @return      自分自身
    257                 */
    258                /* default */ CommentSet useEsc() {
    259                        useCharEsc = true;
    260                        return this;
    261                }
    262
    263                /**
    264                 * ブロック外で、エスケープ文字の場合は、内外反転します。
    265                 *
    266                 * @og.rev 6.4.1.1 (2016/01/16) Avoid if (x != y) ..; else ..; refactoring
    267                 * @og.rev 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    268                 *
    269                 * @param       ch      チェックするコメント除外char
    270                 * @return      エスケープ文字中の場合は、true
    271                 */
    272                /* default */ boolean checkEsc( final char ch ) {
    273                        if( isBlkIn || chEsc ) {
    274                                chEsc = false;
    275                        }
    276                        else {
    277//                              chEsc = CHAR_ESC == ch;
    278                                chEsc = useCharEsc && CHAR_ESC == ch;                                           // 6.9.8.0 (2018/05/28) エスケープ文字の使用可否
    279                                if( !escIn2 && ESC_CHAR1 == ch ) { escIn1 = !escIn1 ; }         // escIn2 でない場合に、escIn1 の判定を行う。
    280                                if( !escIn1 && ESC_CHAR2 == ch ) { escIn2 = !escIn2 ; }         // 同様にその逆
    281                        }
    282                        return escIn1 || escIn2;        // どちらかで、エスケープ中
    283                }
    284
    285                /**
    286                 * ブロックコメント中かどうかの判定を行います。
    287                 *
    288                 * @og.rev 6.4.5.1 (2016/04/28) ブロックコメントを指定しないケースに対応。
    289                 *
    290                 * @param       line    チェックする行データ
    291                 * @param       st              チェック開始文字数
    292                 * @return      ブロックコメント中の場合は、true を返します。
    293                 */
    294                /* default */ boolean isBlockIn( final String line , final int st ) {
    295                        if( !isBlkIn && BLOCK_CMNT1 != null ) { isBlkIn = line.startsWith( BLOCK_CMNT1,st ); }
    296
    297                        return isBlkIn ;
    298                }
    299
    300                /**
    301                 * ラインコメントかどうかの判定を行います。
    302                 *
    303                 * @param       line    チェックする行データ
    304                 * @param       st              チェック開始文字数
    305                 * @return      ラインコメントの場合は、true を返します。
    306                 */
    307                /* default */ boolean isLineCmnt( final String line , final int st ) {
    308                        return LINE_CMNT != null && line.startsWith( LINE_CMNT,st ) ;
    309                }
    310
    311                /**
    312                 * ブロックコメントの終了を見つけます。
    313                 * 終了は、複数指定でき、それらのもっとも最初に現れる方が有効です。
    314                 * 例：XMLタグで、BODYが、あるなしで、終了条件が異なるケースなど。
    315                 * この処理では、ブロックコメントが継続中かどうかの判定は行っていないため、
    316                 * 外部(呼び出し元)で、判定処理してください。
    317                 *
    318                 * ※ このメソッドは、ブロックコメント中にしか呼ばれないため、
    319                 *    ブロックコメントを指定しないケース(BLOCK_CMNT1==null)では呼ばれません。
    320                 *
    321                 * @param       line    チェックする行データ
    322                 * @param       st              チェック開始文字数
    323                 * @return      ブロックコメントの終了の位置。なけらば、-1
    324                 */
    325                /* default */ int blockOut( final String line , final int st ) {
    326                        int ed = line.length();
    327                        for( final String key : BLOCK_CMNT2 ) {
    328                                final int tmp = line.indexOf( key,st + BLOCK_CMNT1.length() );                  // 6.4.1.0 (2016/01/09) 開始位置の計算ミス
    329                                if( tmp >= 0 && tmp < ed ) {    // 存在して、かつ小さい方を選ぶ。
    330                                        ed = tmp + key.length();        // アドレスは、終了コメント記号の後ろまで。
    331                                        isBlkIn = false;                        // ブロックコメントから抜ける。
    332                                }
    333                        }
    334
    335                        return isBlkIn ? -1 : ed ;              // 見つからない場合は、-1 を返す。
    336                }
    337        }
    338
    339        /**
    340         * このクラスの動作確認用の、main メソッドです。
    341         *
    342         * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]
    343         *
    344         * -rowTrim を指定すると、空行も削除します。これは、コメントの増減は、ソースレベルで比較する場合に
    345         * 関係ないためです。デフォルトは、空行は削除しません。grep 等で検索した場合、オリジナルの
    346         * ソースの行数と一致させるためです。
    347         *
    348         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
    349         * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
    350         *
    351         * @param       args    コマンド引数配列
    352         */
    353        public static void main( final String[] args ) {
    354                if( args.length < 2 ) {
    355                        System.out.println( "Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]" );
    356                }
    357
    358                final File inFile  = new File( args[0] );
    359                final File outFile = new File( args[1] );
    360                String  encode  = "UTF-8" ;
    361                boolean rowTrim = false;
    362                for( int i=2; i<args.length; i++ ) {
    363                        if( "-rowTrim".equalsIgnoreCase( args[i] ) ) { rowTrim = true; }
    364                        else { encode = args[i]; }
    365                }
    366
    367                final BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode );
    368                final PrintWriter    writer = FileUtil.getPrintWriter(   outFile ,encode );
    369
    370                final CommentLineParser clp = new CommentLineParser( FileInfo.getSUFIX( inFile ) );
    371
    372                try {
    373                        String line1;
    374                        while((line1 = reader.readLine()) != null) {
    375                                line1 = clp.line( line1 );
    376                                if( !rowTrim || !line1.isEmpty() ) {
    377                                        writer.println( line1 );
    378                                }
    379                        }
    380                }
    381                // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
    382                catch( final CharacterCodingException ex ) {
    383                        final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
    384                                                                +       "  ファイルのエンコードが指定のエンコードと異なります。" + CR
    385                                                                +       " [" + inFile.getPath() + "] , Encode=[" + encode + "]" ;
    386                        throw new OgCharacterException( errMsg,ex );    // 6.5.0.1 (2016/10/21)
    387                }
    388                catch( final IOException ex ) {
    389                        final String errMsg = "ファイルコピー中に例外が発生しました。\n"
    390                                                + " inFile=[" + inFile + "] , outFile=[" + outFile + "]\n" ;
    391                        throw new OgRuntimeException( errMsg,ex );
    392                }
    393                finally {
    394                        Closer.ioClose( reader ) ;
    395                        Closer.ioClose( writer ) ;
    396                }
    397        }
    398}