    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.taglet;            // 7.4.4.0 (2021/06/30) openGionV8事前準備(taglet2→taglet)
    017
    018import jdk.javadoc.doclet.DocletEnvironment      ;
    019// import jdk.javadoc.doclet.Doclet  ;
    020// import jdk.javadoc.doclet.Reporter ;
    021import javax.lang.model.element.Element ;
    022import javax.lang.model.element.Modifier ;
    023import javax.lang.model.element.TypeElement;
    024// import javax.lang.model.element.ElementKind  ;
    025import javax.lang.model.element.VariableElement;
    026import javax.lang.model.element.ExecutableElement;
    027// import javax.lang.model.SourceVersion ;
    028import javax.lang.model.util.ElementFilter ;
    029// import javax.lang.model.util.Elements         ;
    030import javax.tools.Diagnostic.Kind ;
    031import com.sun.source.doctree.DocCommentTree  ;
    032import com.sun.source.doctree.DocTree  ;
    033import com.sun.source.util.DocTrees  ;
    034
    035// import java.util.Locale ;
    036import java.util.Set;
    037import java.util.Map;
    038import java.util.List;
    039import java.util.ArrayList;
    040import java.util.HashSet;
    041// import java.util.HashMap;
    042import java.util.TreeMap;                                               // 8.4.0.0 (2023/01/31)
    043import java.util.Arrays;
    044import java.util.Comparator;                                    // 8.4.0.0 (2023/01/31)
    045
    046// import java.io.IOException;
    047// import java.io.File;
    048// import java.io.PrintWriter;
    049
    050// import org.opengion.fukurou.util.FileUtil;
    051// import org.opengion.fukurou.util.StringUtil;
    052
    053/**
    054 * ソースコメントから、タグ情報を取り出す Doclet クラスです。
    055 * パラメータの version に一致する og.rev タグの 書かれたメソッドを、
    056 * ピックアップします。
    057 * og.rev タグ で、まとめて表示します。
    058 *
    059 * ルールとしては、X.X.X.X だけが引数で渡されますので、 X.X.X.X (YYYY/MM/DD) が
    060 * コメントとして記載されているとして、処理します。
    061 * そして、それ以降の記述の、「。」までを、キーワードとして、まとめます。
    062 * キーワード以下は、それぞれのコメントとして、各メソッドの直前に表示します。
    063 * このクラスは、RELEASE-NOTES.txt を作成する場合に、変更箇所をピックアップするのに使用します。
    064 *
    065 * @version  7.3
    066 * @author      Kazuhiko Hasegawa
    067 * @since        JDK11.0,
    068 */
    069public class DocTreeVerCheck extends AbstractDocTree {
    070        private static final String SELECT_PACKAGE      = "org.opengion." ;
    071
    072        private static final String OG_REV                      = "og.rev";
    073
    074        private String  version ;
    075        private String  outfile ;
    076        private int omitPackage = SELECT_PACKAGE.length()       ;       // パッケージ名の先頭をカットするため
    077
    078        private DocTrees docUtil;
    079
    080        /**
    081         * デフォルトコンストラクター
    082         *
    083         * @og.rev 7.3.0.0 (2021/01/06) PMD refactoring. Each class should declare at least one constructor.
    084         */
    085        public DocTreeVerCheck() { super(); }           // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
    086
    087        /**
    088         * Doclet のエントリポイントメソッドです(昔の startメソッド)。
    089         *
    090         * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
    091         *
    092         * @param docEnv ドックレットを1回呼び出す操作環境
    093         *
    094         * @return 正常実行時 true
    095         */
    096        @Override
    097        public boolean run( final DocletEnvironment docEnv ) {
    098                try( DocTreeWriter writer = new DocTreeWriter( outfile,ENCODE ) ) {
    099                        writeContents( docEnv,writer );
    100                }
    101                catch( final Throwable th ) {
    102                        reporter.print(Kind.ERROR, th.getMessage());
    103                }
    104
    105                return true;
    106        }
    107
    108        /**
    109         * DocletEnvironmentよりコンテンツを作成します。
    110         *
    111         * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
    112         * @og.rev 8.0.2.1 (2021/12/10) ｺﾒﾝﾄ分割で『。』と半角の『｡』の両方対応しておく。
    113         * @og.rev 8.4.0.0 (2023/01/31) 同一ﾊﾞｰｼﾞｮﾝの日付違いも区別できるようにする。
    114         *
    115         * @param docEnv        ドックレットの最上位
    116         * @param writer        DocTreeWriterオブジェクト
    117         */
    118        private void writeContents( final DocletEnvironment docEnv, final DocTreeWriter writer ) {
    119                docUtil = docEnv.getDocTrees();
    120
    121                // 8.4.0.0 (2023/01/31) ｷｰに日付を入れて逆順にｿｰﾄする(null値は存在しないという前提)。
    122        //      final Map<String,List<String>> verMap = new HashMap<>();
    123                final Map<String,List<String>> verMap = new TreeMap<>( Comparator.reverseOrder() );
    124                String revYMD = null;   // 初めてのRev.日付をセットする。
    125
    126                // get the DocTrees utility class to access document comments
    127                final DocTrees docTrees = docEnv.getDocTrees();
    128//              final Elements eleUtil  = docEnv.getElementUtils();
    129
    130                // クラス単位にループする。
    131                for( final TypeElement typEle : ElementFilter.typesIn(docEnv.getIncludedElements())) {
    132                        final String fullName = String.valueOf(typEle).substring( omitPackage );                // パッケージ名を簡略化しておきます。
    133                        writer.setClassName( fullName );
    134
    135        //              // クラスに書かれている private static final String VERSION フィールドを先に取得する。
    136        //              String clsVer = null;
    137        //              for( final VariableElement ele : ElementFilter.fieldsIn(typEle.getEnclosedElements())) {                // フィールドだけに絞る
    138        //                      final Set<Modifier> modi = ele.getModifiers();
    139        //                      final String typ = String.valueOf( ele.asType() );
    140        //                      if( modi.contains(  Modifier.PRIVATE  ) && modi.contains( Modifier.STATIC ) && typ.contains( "String" )
    141        //                                                      && "VERSION".equals( ele.getSimpleName() ) ) {
    142        //                              clsVer = String.valueOf( ele.getConstantValue() );
    143        //                      }
    144        //              }
    145
    146                        // 5.6.6.0 (2013/07/05) VERSION staticフィールドと、@og.rev コメントの比較チェック
    147                        // while 以下で、fullName と classDoc を順番に上にさかのぼっているので、先にチェックします。
    148                        final String clsVer = checkTag2( typEle );
    149
    150                        for( final Element ele : typEle.getEnclosedElements()) {
    151                                final DocCommentTree doc = docTrees.getDocCommentTree(ele);             // ﾄﾞｷｭﾒﾝﾃｰｼｮﾝ・ｺﾒﾝﾄが見つからない場合、null が返る。
    152                                if( doc == null ) { continue; }
    153
    154                                for( final DocTree dt : doc.getBlockTags() ) {
    155                                        final String tag = String.valueOf(dt);
    156                                        if( tag.contains( OG_REV ) ) {
    157                                                final String[] tags = tag.split( " ",4 );               // ﾀｸﾞ , ﾊﾞｰｼﾞｮﾝ , 日付 , ｺﾒﾝﾄ (@og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応)
    158                                                if( tags.length >= 2 ) {                                                // 最低限、ﾊﾞｰｼﾞｮﾝ は記載されている
    159                                                        final String tagVer = tags[1];                          // ﾊﾞｰｼﾞｮﾝ
    160
    161                                                        // Ver チェックで、旧の場合
    162                                                        if( clsVer != null && clsVer.compareTo( tagVer ) < 0 ) {
    163                                                                final String msg = "旧Ver：" + fullName + ":" + version + " : " + tagVer ;
    164                                                                reporter.print(Kind.WARNING, msg );                                     // NOTE:情報 WARNING:警告 ERROR:エラー
    165                                                        }
    166
    167                                                        if( tags.length >= 4 && tagVer.equals( version ) ) {    // ﾊﾞｰｼﾞｮﾝが一致して、ｺﾒﾝﾄ まで記載済み
    168                                                                // ｺﾒﾝﾄを「。」で分割する。まずは、convertToOiginal で、ｵﾘｼﾞﾅﾙに戻しておく
    169                                                                final String cmnt = writer.convertToOiginal( tags[3] );
    170                                                                // 8.0.2.1 (2021/12/10) ｺﾒﾝﾄ分割で『。』と半角の『｡』の両方対応しておく。
    171//                                                              final int idx = cmnt.indexOf( '。' );                            // '。' の区切り文字の前半部分が、タイトル。
    172                                                                final int idx = Math.max( cmnt.indexOf( '。' ) , cmnt.indexOf( '｡' ) );
    173                                                                final String key = idx > 0 ? cmnt.substring( 0,idx ).trim() : cmnt ;
    174//                                                              final String key = tags[3];                                                     // ｺﾒﾝﾄがｷｰ
    175                                                                final String val = fullName + "#" + ele ;                       // ﾒｿｯﾄﾞが値
    176//                                                              verMap.computeIfAbsent( key, k -> new ArrayList<>() ).add( val );
    177                                                                verMap.computeIfAbsent( tags[2] + " " + key, k -> new ArrayList<>() ).add( val );       // 8.4.0.0 (2023/01/31) ｷｰの先頭に日付
    178
    179                                                                if( revYMD == null ) {                                                          // 一番最初だけセットする
    180//                                                                      revYMD = tagVer + " " + tags[2];                                // ﾊﾞｰｼﾞｮﾝ + 日付
    181                                                                        revYMD = tagVer ;                                                               // 8.4.0.0 (2023/01/31) ﾊﾞｰｼﾞｮﾝのみ
    182                                                                }
    183                                                        }
    184                                                }
    185                                        }
    186                                }
    187                        }
    188                }
    189
    190                // 書き出し
    191                // 8.4.0.0 (2023/01/31) 同一ﾊﾞｰｼﾞｮﾝの日付違いも区別できるようにする。
    192                if( revYMD != null ) {
    193//                      writer.printTag( revYMD );
    194                        String keyBreak = "";                                                                                   // 8.4.0.0 (2023/01/31) 日付ﾌﾞﾚｲｸ
    195                        for( final Map.Entry<String,List<String>> entry : verMap.entrySet() ) {
    196                                final String msg = entry.getKey().trim();
    197                                final String[] day_msg = msg.split( " ",2 );                            // 日付とｺﾒﾝﾄを分離
    198                                if( !keyBreak.isEmpty() ) { writer.printTag(); }                        // 一番先頭以外は、改行を入れる。
    199
    200                                if( !keyBreak.equals( day_msg[0] ) ) {                                          // 日付ﾌﾞﾚｲｸ
    201                                        writer.printTag( revYMD , "" , " " , day_msg[0] );
    202                                        keyBreak = day_msg[0];
    203                                }
    204        //                      writer.printTag( "\n\t[" , entry.getKey() , "]" );
    205        //                      writer.printTag( "\n\t[" , msg , "]" );                                         // 8.0.0.0 (2021/07/31)
    206                                writer.printTag( "\t[" , day_msg[1] , "]" );                            // 8.4.0.0 (2023/01/31)
    207                                for( final String str : entry.getValue() ) {                            // リスト
    208                                        writer.printTag( "\t\t" , str );
    209                                }
    210                        }
    211                }
    212        }
    213
    214        /**
    215         * PMDで、チェックしている処理のうち、Docletでフォローできる分をチェックします。
    216         *
    217         * ※ このチェックは、警告レベル5 のみ集約していますので、呼出元で、制限します。
    218         *
    219         * ※ DocTreeSpecific から、移植しました。
    220         *
    221         * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
    222         *
    223         * @param typEle TypeElementオブジェクト
    224         * @return VERSIONフィールドの値
    225         */
    226        private String checkTag2( final TypeElement typEle ) {
    227                String cnstVar = null ;         // 初期値
    228                String seriUID = null ;
    229
    230                // フィールドのみフィルタリングして取得する
    231                for( final VariableElement varEle : ElementFilter.fieldsIn(typEle.getEnclosedElements())) {             // フィールドだけに絞る
    232                        final Set<Modifier> modi = varEle.getModifiers();
    233                        if( modi.contains( Modifier.PRIVATE ) && modi.contains( Modifier.STATIC ) ) {
    234                                final String key = String.valueOf( varEle.getSimpleName() );
    235                                if( "VERSION".equals( key ) ) {
    236                                        cnstVar = String.valueOf( varEle.getConstantValue() );
    237                                }
    238                                else if( "serialVersionUID".equals( key ) ) {
    239                                        seriUID = varEle.getConstantValue() + "L";                      // 旧JavaDocと違い、"L" まで取ってこれないみたい
    240                                }
    241                        }
    242                }
    243
    244                if( cnstVar == null ) { return null; }                  // VERSION が未定義のクラスは処理しない
    245
    246                String maxRev = cnstVar ;                                               // 5.7.1.1 (2013/12/13) 初期値
    247                boolean isChange = false;                                               // max が入れ替わったら、true
    248
    249                // メソッドのみフィルタリングして取得する
    250                for( final ExecutableElement exEle : ElementFilter.methodsIn(typEle.getEnclosedElements())) {
    251                        final DocCommentTree dct = docUtil.getDocCommentTree(exEle);            // ﾄﾞｷｭﾒﾝﾃｰｼｮﾝ・ｺﾒﾝﾄが見つからない場合、null が返る。
    252                        final Map<String,List<String>> blkTagMap = blockTagsMap(dct);
    253                        final List<String> revTags = blkTagMap.get("og.rev");
    254
    255                        if( revTags != null ) {
    256                                for( final String tag :revTags ) {                                                              // 複数存在しているはず
    257                                        final String[] tags = tag.split( " ",3 );                                       // 最小3つに分割する。
    258
    259                                        if( tags.length >= 2 ) {
    260                                                final String rev = ( tags[0] + ' ' + tags[1] ).trim();
    261                                                if( maxRev.compareTo( rev ) < 0 ) {                                             // revTags の og.rev が大きい場合
    262                                                        maxRev = rev ;
    263                                                        isChange = true;
    264                                                }
    265                                        }
    266                                }
    267                        }
    268                }
    269
    270                final String src = "\tsrc/" + String.valueOf(typEle).replace('.','/') + ".java:100" ;                   // 行が判らないので、100行目 決め打ち
    271
    272                // VERSION 文字列 の定義があり、かつ、max の入れ替えが発生した場合のみ、警告4:VERSIONが古い
    273                if( isChange ) {                        // 5.7.1.1 (2013/12/13) 入れ替えが発生した場合
    274                        System.err.println( "警告4:VERSIONが古い=\t" + cnstVar + " ⇒ " + maxRev + src );
    275                }
    276
    277                // serialVersionUID の定義がある。
    278                if( seriUID != null ) {
    279                        final StringBuilder buf = new StringBuilder();
    280                        // maxRev は、最大の Revか、初期のVERSION文字列 例:5.6.6.0 (2013/07/05)
    281                        for( int i=0; i<maxRev.length(); i++ ) {        //
    282                                final char ch = maxRev.charAt( i );
    283                                if( ch >= '0' && ch <= '9' ) { buf.append( ch ); }      // 数字だけ取り出す。 例:566020130705
    284                        }
    285                        buf.append( 'L' );      // 強制的に、L を追加する。
    286                        final String maxSeriUID = buf.toString() ;
    287
    288                        // 5.7.1.1 (2013/12/13) 値の取出し。Long型を表す "L" も含まれている。
    289                        if( !maxSeriUID.equals( seriUID ) ) {   // 一致しない
    290                                System.err.println( "警告4:serialVersionUIDが古い=\t" + seriUID + " ⇒ " + maxSeriUID + src );
    291                        }
    292                }
    293
    294                return cnstVar ;
    295        }
    296
    297        /**
    298         * サポートされているすべてのオプションを返します。
    299         *
    300         * @return サポートされているすべてのオプションを含むセット、存在しない場合は空のセット
    301         */
    302        @Override
    303        public Set<? extends Option> getSupportedOptions() {
    304                final Option[] options = {
    305                        new AbstractOption( "-outfile", "-version", "-omitPackage" ) {
    306
    307                                /**
    308                                 * 必要に応じてオプションと引数を処理します。
    309                                 *
    310                                 * @param  opt オプション名
    311                                 * @param  arguments 引数をカプセル化したリスト
    312                                 * @return 操作が成功した場合はtrue、そうでない場合はfalse
    313                                 */
    314                                @Override
    315                                public boolean process(final String opt, final List<String> arguments) {
    316                                        if( "-outfile".equalsIgnoreCase(opt) ) {
    317                                                outfile = arguments.get(0);
    318                                        }
    319                                        else if( "-version".equalsIgnoreCase(opt) ) {
    320                                                version = arguments.get(0);
    321                                        }
    322                                        else if( "-omitPackage".equalsIgnoreCase(opt) ) {
    323                                                omitPackage = arguments.get(0).length()+1;
    324                                        }
    325                                        return true;
    326                                }
    327                        }
    328                };
    329                return new HashSet<>(Arrays.asList(options));
    330        }
    331}