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 * 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         *
091         * @param       sufix 拡張子
092         */
093        public CommentLineParser( final String sufix ) {
094                final String type = sufix == null ? "null" : sufix.toLowerCase( Locale.JAPAN );
095
096                if( "sql , tri , spc".contains( type ) ) {
097                        cmntSetList = Arrays.asList(
098                                        new CommentSet( "--" , "/*"                      , "*/"  )                                              // ORACLE系
099                                ,       new CommentSet( "COMMENT ON" , null      , (String)null  )                              // 大文字のみ除外する。
100                        );
101                }
102                else if( "xml , htm , html".contains( type ) ) {
103                        cmntSetList = Arrays.asList(
104                                        new CommentSet( "//" , "/*"                      , "*/"  )                                              // Java,C,JavaScript系
105                                ,       new CommentSet( null , "<!--"            , "-->" )                                              // HTML,XML系
106                        );
107                }
108                else if( "jsp".contains( type ) ) {
109                        cmntSetList = Arrays.asList(
110                                        new CommentSet( "//" , "/*"                      , "*/"  )                                              // Java,C,JavaScript系
111                                ,       new CommentSet( null , "<!--"            , "-->" )                                              // HTML,XML系
112                                ,       new CommentSet( "--" , "/*"                      , "*/"  )                                              // ORACLE系
113                                ,       new CommentSet( null , "<og:comment" , "/>" , "</og:comment>"  )        // openGion JSP系
114                                ,       new CommentSet( null , "comment=\""  , "\""  )                                          // openGion comment="***"               6.4.1.0 (2016/01/09)
115                        );
116                }
117                else {
118                        cmntSetList = Arrays.asList(
119                                        new CommentSet( "//" , "/*"                      , "*/"  )                                              // Java,C,JavaScript系
120                        );
121                }
122        }
123
124        /**
125         * 1行分の文字列を読み取って、コメント部分を削除した文字列を返します。
126         * 行として存在しない場合は、null を返します。
127         *
128         * @og.rev 5.7.4.0 (2014/03/07) 新規追加
129         * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
130         *
131         * @param       inLine 1行の文字列
132         * @return      コメント削除後の1行の文字列
133         */
134        public String line( final String inLine ) {
135
136                String outLine = inLine ;
137                for( final CommentSet cmntSet : cmntSetList ) {
138                        outLine = line( outLine,cmntSet );
139                }
140                return outLine ;
141        }
142
143        /**
144         * 1行分の文字列を読み取って、コメント部分を削除した文字列を返します。
145         * 行として存在しない場合は、null を返します。
146         *
147         * @og.rev 5.7.4.0 (2014/03/07) 新規追加
148         * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
149         *
150         * @param       inLine  1行の文字列
151         * @param       cmntSet コメントを管理するオブジェクト
152         * @return      コメント削除後の1行の文字列
153         */
154        private String line( final String inLine , final CommentSet cmntSet ) {
155                if( inLine == null ) { return null; }
156
157                final int size = inLine.length();
158
159                final StringBuilder buf = new StringBuilder( size );
160
161                for( int st=0; st<size; st++ ) {
162                        final char ch = inLine.charAt(st);
163
164                        if( !cmntSet.checkEsc( ch ) ) {                                                 // エスケープ文字でないなら、判定処理を進める
165                                // ブロックコメント継続中か、先頭がブロックコメント
166                                if( cmntSet.isBlockIn( inLine,st ) ) {
167                                        final int ed = cmntSet.blockOut( inLine,st ) ;  // 終了を見つける
168                                        if( ed >= 0 ) {                                                                 // 終了があれば、そこまで進める。
169                                                st = ed;
170                                                continue;                                                                       // ブロックコメント脱出。再読み込み
171                                        }
172                                        break;                                                                                  // ブロックコメント継続中。次の行へ
173                                }
174
175                                // ラインコメント発見。次の行へ
176                                if( cmntSet.isLineCmnt( inLine,st ) ) { break; }
177                        }
178
179                        // 通常の文字なので、追加する。
180                        buf.append( ch );
181                }
182
183                // rTrim() と同等の処理
184                int len = buf.length();
185                while( 0 < len && buf.charAt(len-1) <= ' ' ) {
186                        len--;
187                }
188                buf.setLength( len );
189
190                return buf.toString() ;
191        }
192
193        /**
194         * コメントセットを管理する内部クラスです。
195         *
196         * コメントの種類を指定します。
197         *
198         * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
199         * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
200         * ORACLE系             -- , /&#042; , &#042;/
201         * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
202         *
203         * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
204         * @og.rev 6.4.0.2 (2015/12/11) CommentSet の見直し。
205         */
206        private static final class CommentSet {
207                private final String   LINE_CMNT        ;                       // ラインコメント
208                private final String   BLOCK_CMNT1      ;                       // ブロックコメントの開始
209                private final String[] BLOCK_CMNT2      ;                       // ブロックコメントの終了
210
211                private static final char ESC_CHAR1 = '"'; ;    // コメント判定除外("")
212                private static final char ESC_CHAR2 = '\'' ;    // コメント判定除外('')
213                private static final char CHAR_ESC  = '\\' ;    // エスケープ文字('\\')
214
215                private boolean escIn1   ;                                              // コメント判定除外中かどうか("")
216                private boolean escIn2   ;                                              // コメント判定除外中かどうか('')
217                private boolean chEsc    ;                                              // コメント除外のエスケープ処理中かどうか
218
219                private boolean isBlkIn ;                                               // ブロックコメントが継続しているかどうか          6.4.1.1 (2016/01/16) refactoring isBlockIn → isBlkIn
220
221                /**
222                 * コメントの種類を指定するコンストラクタです。
223                 *
224                 * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
225                 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
226                 * ORACLE系             -- , /&#042; , &#042;/
227                 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
228                 *
229                 * @param       lineCmnt        ラインコメント
230                 * @param       blockCmnt1      ブロックコメントの開始
231                 * @param       blockCmnt2      ブロックコメントの終了(可変長配列)
232                 */
233                CommentSet( final String lineCmnt,final String blockCmnt1,final String... blockCmnt2 ) {
234                        LINE_CMNT   = lineCmnt ;                // ラインコメント
235                        BLOCK_CMNT1 = blockCmnt1 ;              // ブロックコメントの開始
236                        BLOCK_CMNT2 = blockCmnt2 ;              // ブロックコメントの終了
237                }
238
239                /**
240                 * ブロック外で、エスケープ文字の場合は、内外反転します。
241                 *
242                 * @og.rev 6.4.1.1 (2016/01/16) Avoid if (x != y) ..; else ..; refactoring
243                 *
244                 * @param       ch      チェックするコメント除外char
245                 * @return      エスケープ文字中の場合は、true
246                 */
247                /* default */ boolean checkEsc( final char ch ) {
248                        if( isBlkIn || chEsc ) {
249                                chEsc = false;
250                        }
251                        else {
252                                chEsc = CHAR_ESC == ch;
253                                if( !escIn2 && ESC_CHAR1 == ch ) { escIn1 = !escIn1 ; }         // escIn2 でない場合に、escIn1 の判定を行う。
254                                if( !escIn1 && ESC_CHAR2 == ch ) { escIn2 = !escIn2 ; }         // 同様にその逆
255                        }
256                        return escIn1 || escIn2;        // どちらかで、エスケープ中
257                }
258
259                /**
260                 * ブロックコメント中かどうかの判定を行います。
261                 *
262                 * @og.rev 6.4.5.1 (2016/04/28) ブロックコメントを指定しないケースに対応。
263                 *
264                 * @param       line    チェックする行データ
265                 * @param       st              チェック開始文字数
266                 * @return      ブロックコメント中の場合は、true を返します。
267                 */
268                /* default */ boolean isBlockIn( final String line , final int st ) {
269                        if( !isBlkIn && BLOCK_CMNT1 != null ) { isBlkIn = line.startsWith( BLOCK_CMNT1,st ); }
270
271                        return isBlkIn ;
272                }
273
274                /**
275                 * ラインコメントかどうかの判定を行います。
276                 *
277                 * @param       line    チェックする行データ
278                 * @param       st              チェック開始文字数
279                 * @return      ラインコメントの場合は、true を返します。
280                 */
281                /* default */ boolean isLineCmnt( final String line , final int st ) {
282                        return LINE_CMNT != null && line.startsWith( LINE_CMNT,st ) ;
283                }
284
285                /**
286                 * ブロックコメントの終了を見つけます。
287                 * 終了は、複数指定でき、それらのもっとも最初に現れる方が有効です。
288                 * 例:XMLタグで、BODYが、あるなしで、終了条件が異なるケースなど。
289                 * この処理では、ブロックコメントが継続中かどうかの判定は行っていないため、
290                 * 外部(呼び出し元)で、判定処理してください。
291                 *
292                 * ※ このメソッドは、ブロックコメント中にしか呼ばれないため、
293                 *    ブロックコメントを指定しないケース(BLOCK_CMNT1==null)では呼ばれません。
294                 *
295                 * @param       line    チェックする行データ
296                 * @param       st              チェック開始文字数
297                 * @return      ブロックコメントの終了の位置。なけらば、-1
298                 */
299                /* default */ int blockOut( final String line , final int st ) {
300                        int ed = line.length();
301                        for( final String key : BLOCK_CMNT2 ) {
302                                final int tmp = line.indexOf( key,st + BLOCK_CMNT1.length() );                  // 6.4.1.0 (2016/01/09) 開始位置の計算ミス
303                                if( tmp >= 0 && tmp < ed ) {    // 存在して、かつ小さい方を選ぶ。
304                                        ed = tmp + key.length();        // アドレスは、終了コメント記号の後ろまで。
305                                        isBlkIn = false;                        // ブロックコメントから抜ける。
306                                }
307                        }
308
309                        return isBlkIn ? -1 : ed ;              // 見つからない場合は、-1 を返す。
310                }
311        }
312
313        /**
314         * このクラスの動作確認用の、main メソッドです。
315         *
316         * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]
317         *
318         * -rowTrim を指定すると、空行も削除します。これは、コメントの増減は、ソースレベルで比較する場合に
319         * 関係ないためです。デフォルトは、空行は削除しません。grep 等で検索した場合、オリジナルの
320         * ソースの行数と一致させるためです。
321         *
322         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
323         * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
324         *
325         * @param       args    コマンド引数配列
326         */
327        public static void main( final String[] args ) {
328                if( args.length < 2 ) {
329                        System.out.println( "Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]" );
330                }
331
332                final File inFile  = new File( args[0] );
333                final File outFile = new File( args[1] );
334                String  encode  = "UTF-8" ;
335                boolean rowTrim = false;
336                for( int i=2; i<args.length; i++ ) {
337                        if( "-rowTrim".equalsIgnoreCase( args[i] ) ) { rowTrim = true; }
338                        else { encode = args[i]; }
339                }
340
341                final BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode );
342                final PrintWriter    writer = FileUtil.getPrintWriter(   outFile ,encode );
343
344                final CommentLineParser clp = new CommentLineParser( FileInfo.getSUFIX( inFile ) );
345
346                try {
347                        String line1;
348                        while((line1 = reader.readLine()) != null) {
349                                line1 = clp.line( line1 );
350                                if( !rowTrim || !line1.isEmpty() ) {
351                                        writer.println( line1 );
352                                }
353                        }
354                }
355                // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
356                catch( final CharacterCodingException ex ) {
357                        final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
358                                                                +       "  ファイルのエンコードが指定のエンコードと異なります。" + CR
359                                                                +       " [" + inFile.getPath() + "] , Encode=[" + encode + "]" ;
360                        throw new OgCharacterException( errMsg,ex );    // 6.5.0.1 (2016/10/21)
361                }
362                catch( final IOException ex ) {
363                        final String errMsg = "ファイルコピー中に例外が発生しました。\n"
364                                                + " inFile=[" + inFile + "] , outFile=[" + outFile + "]\n" ;
365                        throw new OgRuntimeException( errMsg,ex );
366                }
367                finally {
368                        Closer.ioClose( reader ) ;
369                        Closer.ioClose( writer ) ;
370                }
371        }
372}