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         * @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         * 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
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}