/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.taglet;

import org.opengion.fukurou.system.LogWriter;
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.util.Set;
import java.util.HashSet;
import java.io.IOException;
import java.util.regex.Pattern;										// 6.3.9.1 (2015/11/27) final化に伴う整理

import java.util.stream.Stream;										// 6.4.3.4 (2016/03/11)
import java.util.stream.Collectors;									// 6.4.3.4 (2016/03/11)

import com.sun.javadoc.RootDoc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.Tag;
import com.sun.javadoc.SourcePosition;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationTypeDoc;

/**
 * ソースコメントから、タグ情報を取り出す Doclet クラスです。
 * クラスファイルの仕様を表現する為、og.formSample , og.rev , og.group ,
 * version , author , since の各タグコメントより値を抽出します。
 * また、各クラスの継承関係、インターフェース、メソッドなども抽出します。
 * これらの抽出結果をDB化し、EXCELファイルに帳票出力する事で、クラスファイルの
 * ソースから仕様書を逆作成します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
@SuppressWarnings(value={"deprecation","removal"})			// Ver7.0.0.0 , 7.2.2.0 (2020/03/27)
public final class DocletSpecific {
	private static final String  SELECT_PACKAGE	= "org.opengion" ;
	private static final boolean USE_PRIVATE	= false ;
	private static final String  ENCODE			= "UTF-8";

	private static final String	OG_FOR_SMPL		= "og.formSample";
	private static final String	OG_REV			= "og.rev";
	private static final String OG_TAG_NAME		= "og.tag";				// 6.1.2.0 (2015/01/24) チェック用
	private static final String	OG_GROUP		= "og.group";
	private static final String	DOC_VERSION		= "version";
	private static final String	DOC_AUTHOR		= "author";
	private static final String	DOC_SINCE		= "since";

	private static final String	DOC_PARAM		= "param";				// 5.1.9.0 (2010/08/01) チェック用
	private static final String	DOC_RETURN		= "return";				// 5.1.9.0 (2010/08/01) チェック用

	private static final String	CONSTRUCTOR		= "コンストラクタ" ;
	private static final String	METHOD			= "メソッド" ;
	private static final Set<String> METHOD_SET	= new HashSet<>();		// 6.4.1.1 (2016/01/16) methodSet → METHOD_SET refactoring

	private static       int	debugLevel		;		// 0:なし 1:最小チェック 2:日本語化 3:体裁 4:Verﾁｪｯｸ 5:taglibﾗﾍﾞﾙ

	// 5.1.9.0 (2010/08/01) ソースチェック用(半角文字＋空白文字のみ)
	private static final Pattern PTN = Pattern.compile("[\\w\\s]+");		// 6.3.9.1 (2015/11/27)

	/**
	 * すべてが staticメソッドなので、コンストラクタを呼び出さなくしておきます。
	 *
	 */
	private DocletSpecific() {}

	/**
	 * Doclet のエントリポイントメソッドです。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) Tag出力時の CR → BR 変換を行わない様にする。
	 * @og.rev 5.7.1.1 (2013/12/13) タグのインデントを止める。
	 *
	 * @param	root	エントリポイントのRootDocオブジェクト
	 *
	 * @return 正常実行時 true
	 */
	public static boolean start( final RootDoc root ) {
		final String version	= DocletUtil.getOption( "-version" , root.options() );
		final String file		= DocletUtil.getOption( "-outfile" , root.options() );
		final String dbgLvl		= DocletUtil.getOption( "-debugLevel" , root.options() );		// 5.5.4.1 (2012/07/06) パラメータ引数
		if( dbgLvl != null ) { debugLevel = Integer.parseInt( dbgLvl ); }

		DocletTagWriter writer = null;
		try {
			writer = new DocletTagWriter( file,ENCODE );		// 5.5.4.1 (2012/07/06)

			// 5.7.1.1 (2013/12/13) タグのインデントを止める。
			writer.printTag( "<?xml version=\"1.0\" encoding=\"", ENCODE, "\" ?>" );
			writer.printTag( "<javadoc>" );
			writer.printTag( "  <version>",version,"</version>" );
			writer.printTag( "  <description></description>" );
			writeContents( root.classes(),writer );
			writer.printTag( "</javadoc>" );
		}
		catch( final IOException ex ) {
			LogWriter.log( ex );
		}
		finally {
			if( writer != null ) { writer.close(); }
		}
		return true;
	}

	/**
	 * ClassDoc 配列よりコンテンツを作成します。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) コメントは文字列でなく、Tag配列として処理させる。
	 * @og.rev 5.6.6.0 (2013/07/05) VERSION staticフィールドと、@og.rev コメントの比較チェック
	 * @og.rev 5.7.1.1 (2013/12/13) タグのインデントを止める。
	 * @og.rev 6.4.3.0 (2016/02/05) PMDチェックのDocletでのフォロー。
	 * @og.rev 6.4.3.4 (2016/03/11) Stream と Collectors で、カンマ連結を行う。
	 *
	 * @param classes	ClassDoc配列
	 * @param writer	Tagを書き出すWriterオブジェクト
	 */
	private static void writeContents( final ClassDoc[] classes,final DocletTagWriter writer ) {
		for( int i=0; i< classes.length; i++ ) {
			ClassDoc classDoc	= classes[i] ;
			final String className	= classDoc.name();
			String fullName			= classDoc.qualifiedName() ;
			final String modifiers	= (classDoc.modifiers() + ( classDoc.isClass() ? " class" : "" ) ).trim();

			final Type superType 	= classDoc.superclassType();
			final String superClass = ( superType == null ) ? "" : superType.qualifiedTypeName();

			// 6.4.3.4 (2016/03/11) Stream と Collectors で、カンマ連結を行う。
			final String intFase = Stream.of( classDoc.interfaceTypes() )
										.map( typ -> typ.qualifiedTypeName() )
										.collect( Collectors.joining( "," ) );

			final Tag[] desc 		= classDoc.firstSentenceTags();
			final Tag[] cmnt 		= classDoc.inlineTags();							// 5.5.4.1 (2012/07/06)
			final Tag[] smplTags	= classDoc.tags(OG_FOR_SMPL);
			final Tag[] revTags		= classDoc.tags(OG_REV);
			final Tag[] createVer	= classDoc.tags(DOC_VERSION);
			final Tag[] author		= classDoc.tags(DOC_AUTHOR);
			final Tag[] since		= classDoc.tags(DOC_SINCE);
			final Tag[] grpTags		= classDoc.tags(OG_GROUP);

			// 5.7.1.1 (2013/12/13) タグのインデントを止める。
			writer.printTag( "<classDoc>" );
			writer.printTag( "  <fullName>"		,fullName		,"</fullName>"		);
			writer.printTag( "  <modifiers>"	,modifiers		,"</modifiers>"		);
			writer.printTag( "  <className>"	,className		,"</className>"		);
			writer.printTag( "  <superClass>"	,superClass		,"</superClass>"	);
			writer.printTag( "  <interface>"	,intFase		,"</interface>"		);
			writer.printTag( "  <createVer>"	,createVer		,"</createVer>"		);
			writer.printTag( "  <author>"		,author			,"</author>"		);
			writer.printTag( "  <since>"		,since			,"</since>"			);
			writer.printTag( "  <description>"	,desc			,"</description>"	);
			writer.printTag( "  <contents>"		,cmnt			,"</contents>"		);
			writer.printTag( "  <classGroup>"	);
			writer.printCSVTag(		grpTags		);
			writer.printTag( "  </classGroup>"	);
			writer.printTag( "  <formSample>"	,smplTags		,"</formSample>"	);
			writer.printTag( "  <history>"		,revTags		,"</history>"		);

			// 5.1.9.0 (2010/08/01) ソースチェック用(コメントや概要が無い場合。スーパークラスは省く)
			if( debugLevel >= 2 && ( cmnt.length == 0 || desc.length == 0 ) && superClass.isEmpty() ) {
				System.err.println( "警告2:コメントC=\t" + classDoc.position() );
			}

			int extendFlag = 0;		// 0:オリジナル 1:org.opengion関連Extend 2:Java関連Extend
	//		while( fullName.startsWith( SELECT_PACKAGE ) ) {

			// 6.4.3.0 (2016/02/05) PMDチェックのDocletでのフォロー。
			checkPMD( classDoc );

			// 5.6.6.0 (2013/07/05) VERSION staticフィールドと、@og.rev コメントの比較チェック
			// while 以下で、fullName と classDoc を順番に上にさかのぼっているので、先にチェックします。
			checkTag2( classDoc );

			METHOD_SET.clear();
			while( true ) {
				final ConstructorDoc[] cnstrctrs = classDoc.constructors( false );	// 5.1.9.0 (2010/08/01) チェック用
				for( int j=0; j<cnstrctrs.length; j++ ) {
					if( isAction( cnstrctrs[j],extendFlag ) ) {
	//					if( extendFlag < 2 ) {
							checkTag( cnstrctrs[j] );		// 5.5.4.1 (2012/07/06)  チェックを分離
							menberTag( cnstrctrs[j],CONSTRUCTOR,writer,extendFlag );
	//					}
					}
				}

				final MethodDoc[] methods = classDoc.methods( false );	// 5.1.9.0 (2010/08/01) チェック用
				for( int j=0; j<methods.length; j++ ) {
					if( isAction( methods[j],extendFlag ) ) {
	//					if( extendFlag < 2 ) {
							checkTag( methods[j] );		// 5.5.4.1 (2012/07/06)  チェックを分離
							menberTag( methods[j],METHOD,writer,extendFlag );
	//					}
					}
				}

				// 対象クラス(オリジナル)から、上に上がっていく。
				final Type type = classDoc.superclassType();
				if( type == null ) { break; }
				classDoc  = type.asClassDoc() ;
				fullName = classDoc.qualifiedName();
				// 上位は処理しない。
				if( fullName.startsWith( SELECT_PACKAGE ) ) {
					extendFlag = 1;
				}
				else {
					break;
				}

//				// java.lang.Object クラスは対象が多いため、処理しません。
//				if( "java.lang.Object".equals( fullName ) || classDoc.isEnum() ) {
//					break;
//				}
//				else if( fullName.startsWith( SELECT_PACKAGE ) ) {
//					extendFlag = 1;
//				}
//				else {
//					extendFlag = 2;		// 上位は処理しない。
//				}
			}

			writer.printTag( "  </classDoc>" );
		}
	}

	/**
	 * メンバークラスのXML化を行うかどうか[true/false]を判定します。
	 *
	 * 以下の条件に合致する場合は、処理を行いません。(false を返します。)
	 *
	 * １．同一クラスを処理中にEXTENDで継承元をさかのぼる場合、すでに同じシグネチャのメソッドが
	 *     存在している。
	 * ２．USE_PRIVATE が true の時の private メソッド
	 * ３．extendFlag が 0以上(1,2)の時の private メソッド
	 * ４．メソッド名におかしな記号(&lt;など)が含まれている場合
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) メソッドの重複処理判定は、クラス名も含めて行う
	 *
	 * @param	menber ExecutableMemberDocオブジェクト
	 * @param	extendFlag	継承状態 [0:オリジナル/1:org.opengion関連Extend/2:Java関連Extend]
	 *
	 * @return	XML化を行うかどうか[true/false]
	 */
	private static boolean isAction( final ExecutableMemberDoc menber,final int extendFlag ) {
		final String menberName = menber.name() ;
		final boolean rtn = ! METHOD_SET.add( menber.toString() )	// 5.5.4.1 (2012/07/06) メソッドの重複処理判定は、クラス名も含めて行う
						||	USE_PRIVATE    && menber.isPrivate()
						||	extendFlag > 0 && menber.isPrivate()
						||	menberName.length() > 0 && menberName.charAt(0) == '<' ;		// PMD Useless parentheses.

		return ! rtn ;
	}

	/**
	 * param,return 等の整合性をチェックします。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) 新規作成。
	 * @og.rev 5.6.6.1 (2013/07/12) Deprecated アノテーション のチェック
	 * @og.rev 6.0.4.0 (2014/11/28) 警告3:PRMタイプは、警告にしない。
	 * @og.rev 6.4.4.0 (2016/03/11) 型パラメータの @param を除外する。
	 *
	 * @param menber ExecutableMemberDocオブジェクト
	 * @param menber ExecutableMemberDocオブジェクト
	 */
	private static void checkTag( final ExecutableMemberDoc menber ) {

		// 親が Enum クラスの場合、処理しません。
		final Type prntType = menber.containingClass().superclassType();
		final String prntClass = ( prntType == null ) ? "" : prntType.qualifiedTypeName();
		if( "java.lang.Enum".equals( prntClass ) ) { return; }

		final SourcePosition posi = menber.position();

		if( menber instanceof MethodDoc ) {
			// メソッドの処理(コンストラクターを省く)
			final Type		rtnType	= ((MethodDoc)menber).returnType();
			final String	typNm	= rtnType.typeName();

			// 6.4.3.1 (2016/02/12) refactoring
			final StringBuilder modifyBuf = new StringBuilder( BUFFER_MIDDLE )
				.append( posi ).append( "\t" )
				.append( menber.modifiers() )
				.append( ' ' ).append( typNm );
			if( rtnType.dimension() != null ) { modifyBuf.append( rtnType.dimension() ); }

			String wormMsg = modifyBuf.toString();

			// 5.1.9.0 (2010/08/01) ソースチェック用(@return との整合性チェック)
			final Tag[] docReturn	= menber.tags(DOC_RETURN);				// 5.1.9.0 (2010/08/01) チェック用
			if( docReturn.length > 0 ) {
				final String data = docReturn[0].text().trim();			// 5.5.4.1 (2012/07/06) trim でスペース等の削除

				wormMsg = wormMsg + "\t" + data ;

				// 5.5.4.1 (2012/07/06) ソースチェック用(@return と引数の個数が異なる場合)
				if( debugLevel >= 1 && "void".equals( typNm ) ) {
					System.err.println( "警告1:RTNコメント不要=\t" + wormMsg );
				}
				// ｢@return 解説｣ の形式で、解説に日本語がなければ、警告
				if( debugLevel >= 2 && PTN.matcher( data ).matches() ) {
					System.err.println( "警告2:RTN未解説=\t" + wormMsg );
				}
				// ｢@return	String｣ の形式の場合は警告
				if( debugLevel >= 2 && data.equals( typNm ) ) {
					System.err.println( "警告2:RTN一致=\t" + wormMsg );
				}
				// ｢@return	String[]｣ など、配列や、<String>などが含まれる場合は警告。
				// ただし、<T>などの総称型(ジェネリックなクラス)は除外したいので、１文字は、OKとします。
				if( debugLevel >= 2 && ( data.indexOf( "[]" ) >= 0 || data.indexOf( '>' ) - data.indexOf( '<' ) > 3 ) ) {
					System.err.println( "警告2:RTN配列=\t" + wormMsg );
				}
				// ｢@return	String 解説｣ の場合は警告(後ろにスペースか、タブがある場合)
				if( debugLevel >= 3 && (data.indexOf( typNm + " " ) >= 0 || data.indexOf( typNm + "\t" ) >= 0 ) ) {
					System.err.println( "警告3:RTNタイプ=\t" + wormMsg );
				}
				// ｢@return	xxxx 解説｣ の場合で、最初のスペースまでが、すべて英数字のみの場合は警告
				final int adrs1 = data.indexOf( ' ' );
				if( debugLevel >= 3 && adrs1 > 0 ) {
					boolean flag = true;
					for( int j=0; j<adrs1; j++ ) {
						final char ch = data.charAt( j );
						if( ( ch < '0' || ch > '9' ) && ( ch < 'a' || ch > 'z' ) && ( ch < 'A' || ch > 'Z' )  && ch != '[' && ch != ']' ) {
							flag = false;	// 英数字でない記号が現れた場合
							break;
						}
					}
					if( flag ) {	// すべてが英数字の場合は、
						System.err.println( "警告3:RTN値=\t" + wormMsg );
					}
				}
			}
			else {	// Tag上には、@return 記述が存在しない。
				// 5.5.4.1 (2012/07/06) ソースチェック用(@return と引数の個数が異なる場合)
				if( debugLevel >= 1 && !"void".equals( typNm ) ) {
					System.err.println( "警告1:RTNｺﾒﾝﾄなし=\t" + wormMsg );
				}
			}

			// オーバーライドチェック：アノテーションの記述漏れ
			// その逆は、コンパイラが警告してくれる。
			final MethodDoc mdoc= ((MethodDoc)menber).overriddenMethod();
			if( debugLevel >= 3 && mdoc != null ) {
				final AnnotationDesc[] annotations = menber.annotations();
				// 本来は、Override の有無を調べるべきだが、Deprecated と SuppressWarnings の付いている
				// 旧のメソッドに、いちいちOverrideを付けないので、何もなければと条件を緩めます。
				if( annotations.length == 0 ) {
					System.err.println( "警告3:@Overrideなし=\t" + wormMsg );
				}

				// 6.1.2.0 (2015/01/24) 移動
				// 5.6.6.1 (2013/07/12) Deprecated アノテーション のチェック
				for( int i=0; i<annotations.length; i++ ) {
					final AnnotationTypeDoc annDoc = annotations[i].annotationType();
					if( "Deprecated".equalsIgnoreCase( annDoc.name() ) ) {
						final String text = menber.commentText();
						if( text != null && text.indexOf( "【廃止】" ) < 0 ) {
							System.err.println( "警告3:【廃止】=\t" + posi + "\t" + menber.name() );
						}
					}
				}
			}
		}

		final Parameter[] prm = menber.parameters();

		// 5.1.9.0 (2010/08/01) ソースチェック用(@param と引数の個数が異なる場合)
		// 6.4.4.0 (2016/03/11) 型パラメータの @param を除外する。
		final Tag[] docParam	= menber.tags(DOC_PARAM);		// 5.1.9.0 (2010/08/01) チェック用

		// 6.4.4.0 (2016/03/11) 型パラメータを含むjavadocの方が、メソッドのパラメータより多い可能性が高い

		int prmId = 0;		// JavaDoc側のカウンタ。型パラメータがあると、ずれてくる。
		int dcCnt = 0;		// JavaDoc側のparam個数
		for( int k=0; k<docParam.length; k++ ) {			// k は、javadocのパラメータ個数

			// 5.1.9.0 (2010/08/01) ソースチェック用(@param と引数の個数が異なる場合)
			if( prm.length > prmId ) {
				final String typNm = prm[prmId].type().typeName();
				final String prmNm = prm[prmId].name();

				final String data1 = docParam[k].text().trim();				// 5.5.4.1 (2012/07/06) trim でスペース等の削除
				// 型パラメータの場合は、パスする必要がある。continue する。

				final String[] txSplit = data1.split( "[\\s]+" );			// 空白文字で分解
				if( txSplit[0].length() == 3 && txSplit[0].charAt(0) == '<' && txSplit[0].charAt(2) == '>' ) { continue; }	// 6.4.4.0 (2016/03/11)
				dcCnt++ ;		// javadocパラメータを処理した個数

				final String data2 = data1.replaceAll( prmNm,"" ).trim();
				final String data3 = data2.replaceAll( typNm,"" ).replaceAll( "\\[\\]","" ).trim();
				final String wormMsg = posi + "\t" + data1 ;

				// ｢@param aaa 解説｣形式で、aaa(引数名)がない場合
				if( debugLevel >= 1 && data1.indexOf( prmNm ) < 0 ) {
					System.err.println( "警告1:PRM引数名=\t" + wormMsg );
				}
				// 引数の文字列の長さが、１文字の場合
				if( debugLevel >= 2 && prmNm.length() == 1 ) {
					System.err.println( "警告2:PRM短い=\t" + wormMsg );
				}
				// ｢@param aaa 解説｣形式で、解説に日本語がない、または、解説がなければ、警告
				if( debugLevel >= 2 && ( PTN.matcher( data2 ).matches() || data3.isEmpty() ) ) {
					System.err.println( "警告2:PRM未解説=\t" + wormMsg );
				}
				// ｢@param aaa String[]｣など、配列や、<String>などが含まれる場合は警告
				if( debugLevel >= 2 && ( data1.indexOf( "[]" ) >= 0 || data1.indexOf( '<' ) >= 0 ) ) {
					System.err.println( "警告2:PRM配列=\t" + wormMsg );
				}
				// ｢@param aaa 解説｣形式で、String が有って、その後ろにスペースか、タブがあれば警告
				// data2 を使うのは、パラメータ名(xxxMap)にタイプ名(Map)が含まれているケースの対応
				// 6.0.4.0 (2014/11/28) 警告3:PRMタイプは、警告にしない。
	//			if( debugLevel >= 3 && (data2.indexOf( typNm + " " ) >= 0 || data2.indexOf( typNm + "\t" ) >= 0 ) ) {
	//				System.err.println( "警告3:PRMタイプ" + wormMsg );
	//			}
				// taglibﾗｲﾌﾞﾗﾘで、｢@param aaa ﾗﾍﾞﾙ [ｺｰﾄﾞ]｣形式で、ﾗﾍﾞﾙと[ｺｰﾄﾞ] の間に空白文字が無ければ警告
				// 6.1.1.0 (2015/01/17)
				if( debugLevel >= 3 && wormMsg.contains( "taglib" ) ) {			// 6.3.9.0 (2015/11/06) String.indexOf の結果が正かどうか確かめている
					if( data2.indexOf( '[' ) > 0 && data2.indexOf( " [" ) < 0 && !typNm.startsWith( "boolean" ) ) {
						System.err.println( "警告3:ﾗﾍﾞﾙ [ｺｰﾄﾞ]=\t" + wormMsg );
					}
					if( data2.indexOf( '[' ) < 0 && !prmNm.startsWith( "user" ) && !prmNm.startsWith( "usemap" ) && !prmNm.startsWith( "ismap" ) &&
	//						( prmNm.startsWith( "is" ) || prmNm.startsWith( "use" ) || prmNm.startsWith( "flag" ) ) ) {
							typNm.startsWith( "boolean" ) ) {
						System.err.println( "警告3:bool[]無=\t" + wormMsg );
					}
					final String dim = prm[prmId].type().dimension();		// ディメンション
					if( menber.isVarArgs() && dim.endsWith( "[]" ) && !data2.contains( "可変長引数" ) ||
							!dim.endsWith( "[]" ) && data2.contains( "可変長引数" ) ) {
						System.err.println( "警告3:可変長引数=\t" + wormMsg );
					}
				}
			}
			prmId++ ;
		}

		// 6.4.4.0 (2016/03/11) 型パラメータの @param を除外する。
		// 型パラメータは、除外する。
		if( debugLevel >= 1 && dcCnt != prm.length ) {
			System.err.println( "警告1:PRM個数違い=\t" + posi );
		}

		final Tag[]	desc	= menber.firstSentenceTags();
		final Tag[]	cmnt	= menber.inlineTags();									// 5.5.4.1 (2012/07/06)

		// 5.1.9.0 (2010/08/01) ソースチェック用
		if( menber instanceof MethodDoc					// メソッドに限定
			&& !menber.isSynthetic()					// コンパイラによって合成されていない
			&& !menber.isNative()						// ネイティブメソッドでない
			&& debugLevel >= 2 ) {						// debugLevel が 2 以上

			final String wormMsg = posi + "\t" + menber.name() ;

			final StringBuilder descBuf = new StringBuilder( BUFFER_MIDDLE );
			for( int i=0; i<desc.length; i++ ) {
				descBuf.append( desc[i].text().trim() );
			}

			if( cmnt.length == 0 || desc.length == 0 ) {	// コメントや概要が無い
				// さらに、親が Enum クラス以外
				System.err.println( "警告2:コメントM=\t" + wormMsg );
			}
			else if( desc.length > 0 ) {				// 概要がある
				final int adrs = descBuf.indexOf( "。" );		// 概要をチェック
				if( adrs < 0 || adrs != descBuf.length()-1 ) {
					System.err.println( "警告2:概要(。)=\t" + wormMsg );
					System.err.println( "「" + descBuf + "」" );
				}
			}

			// 6.1.2.0 (2015/01/24) 新規追加。カスタムタグの記述方法チェック
			final Tag[] tags = menber.tags(OG_TAG_NAME);	// カスタムタグに記述
			if( debugLevel >= 3 && descBuf.indexOf( "【廃止】" ) < 0 &&
				!( tags.length > 0 ^ descBuf.indexOf( "【" ) < 0 ) ) {		// !XOR 条件
					System.err.println( "警告3:ogTag=\t" + wormMsg );
			}

			// 6.1.2.0 (2015/01/24) writeContents から移す。
			if( debugLevel >= 5 && docParam.length > 0 && tags.length > 0 ) {		// カスタムタグの判定
				final String data1 = docParam[0].text().trim();			// 5.5.4.1 (2012/07/06) trim でスペース等の削除
				final String[] str = data1.split( "[\\s]+" );			// 空白記号で分割する。[0]:prm , [1]:説明 , [2]:補足説明、選択肢他
				if( str.length > 1 && str[1].length() > 10 ) {			// １０文字より長い説明文
					System.err.println( "警告5:PRM抜出=\t" + wormMsg + " " + str[1] );
				}
			}
		}
	}

	/**
	 * VERSION staticフィールドと、@og.rev コメントの比較チェックを行います。
	 * エンジン内部では、serialVersionUID は、この、VERSION を元に作成しているため、
	 * その値もチェックします。
	 *
	 * ※ このチェックは、警告レベル4 のみ集約していますので、呼出元で、制限します。
	 *
	 * @og.rev 5.6.6.0 (2013/07/05) 新規作成
	 * @og.rev 5.7.1.1 (2013/12/13) VERSION の値を、Class.forName ではなく、FieldDoc から取得する。
	 * @og.rev 6.0.0.1 (2014/04/25) fullName 削除
	 *
	 * @param classDoc ClassDocオブジェクト
	 */
	private static void checkTag2( final ClassDoc classDoc ) {
		final FieldDoc cnstVarFld = findFieldDoc( classDoc , "VERSION" ) ;				// VERSION 文字列    例:5.6.6.0 (2013/07/05)

		// VERSION 文字列 か、serialVersionUID のどちらかがあれば処理します。
		// 6.1.2.0 (2015/01/24) 警告レベルの追加
		if( cnstVarFld != null && debugLevel >= 4 ) {							// 5.7.1.1 (2013/12/13) cnstVarFid のみ初めにチェック
			String cnstVar = cnstVarFld.constantValueExpression() ;
			// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
			if( cnstVar == null ) {
				cnstVar = "0.0.0.0 (0000/00/00)";		// 初期値
			}
			else {
				if( cnstVar.length() > 1 &&
					cnstVar.charAt(0) == '"' && cnstVar.charAt( cnstVar.length()-1) == '"' ) {
						cnstVar = cnstVar.substring( 1,cnstVar.length()-1 );
				}
			}

			String maxRev    = cnstVar ;						// 5.7.1.1 (2013/12/13) 初期値
			final int lenVar = maxRev.length();					// 比較時に使用する長さ
			boolean isChange = false;							// max が入れ替わったら、true

			// 本体、コンストラクタ、フィールド、メソッド内から、最大の @og.rev の値を取得します。
			Doc[][] docs = new Doc[4][] ;

			docs[0] = new Doc[] { classDoc } ;
			docs[1] = classDoc.constructors( false ) ;
			docs[2] = classDoc.fields( false ) ;
			docs[3] = classDoc.methods( false ) ;

			for( int i=0; i<docs.length; i++ ) {
				for( int j=0; j<docs[i].length; j++ ) {
					final Doc doc = docs[i][j];

					final Tag[] revTags = doc.tags(OG_REV);
					for( int k=0 ; k<revTags.length; k++ ) {
						String rev = revTags[k].text();

						if( rev.length() < lenVar ) {
							System.err.println( "警告4:og.revが短い=\t" + rev + "\t" + doc.position() );
							continue;
						}

						rev = rev.substring( 0,lenVar );

						if( maxRev.compareTo( rev ) < 0 ) {			// revTags の og.rev が大きい場合
							maxRev = rev ;
					//		posi   = doc.position();				// 最後に入れ替わった位置 = 最大のrevの位置
							isChange = true;
						}
					}
				}
			}

			// VERSION 文字列 の定義があり、かつ、max の入れ替えが発生した場合のみ、警告4:VERSIONが古い
			if( isChange ) {			// 5.7.1.1 (2013/12/13) 入れ替えが発生した場合
				System.err.println( "警告4:VERSIONが古い=\t" + cnstVar + " ⇒ " + maxRev + "\t" + cnstVarFld.position() );
			}

			// serialVersionUID の定義がある。
			final FieldDoc seriUIDFld = findFieldDoc( classDoc , "serialVersionUID" ) ;		// serialVersionUID  例:566020130705L
			if( seriUIDFld != null ) {		// 5.7.1.1 (2013/12/13)
				final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
				// maxRev は、最大の Revか、初期のVERSION文字列 例:5.6.6.0 (2013/07/05)
				for( int i=0; i<maxRev.length(); i++ ) {	//
					final char ch = maxRev.charAt( i );
					if( ch >= '0' && ch <= '9' ) { buf.append( ch ); }	// 数字だけ取り出す。 例:566020130705
				}
				buf.append( 'L' );	// 強制的に、L を追加する。
				final String maxSeriUID = buf.toString() ;

				// 5.7.1.1 (2013/12/13) 値の取出し。Long型を表す "L" も含まれている。
				final String seriUID = seriUIDFld.constantValueExpression() ;
				if( !maxSeriUID.equals( seriUID ) ) {	// 一致しない
					System.err.println( "警告4:serialVersionUIDが古い=\t" + seriUID + " ⇒ " + maxSeriUID + "\t" + seriUIDFld.position() );
				}
			}
		}
	}

	/**
	 * PMDで、チェックしている処理のうち、Docletでフォローできる分をチェックします。
	 *
	 * ※ このチェックは、警告レベル5 のみ集約していますので、呼出元で、制限します。
	 *
	 * @og.rev 6.4.3.0 (2016/02/05) PMDチェックのDocletでのフォロー。
	 * @og.rev 6.8.0.1 (2017/06/30) unmodifiable という文字列もチェック対象外にします。
	 *
	 * @param classDoc ClassDocオブジェクト
	 */
	private static void checkPMD( final ClassDoc classDoc ) {

		// 警告5:private static final
		// PMD にはないが、インナークラスで、static で、enum でないのに、public や、final でない場合、警告します。
//		if( classDoc.isStatic() && !( classDoc.isEnum() || classDoc.isEnumConstant() ) && !classDoc.isFinal() ) {
		// 7.3.0.0 (2021/01/06) Abstract でないという条件も追加
		if( classDoc.isStatic() && !( classDoc.isEnum() || classDoc.isEnumConstant() ) && !classDoc.isFinal() && !classDoc.isAbstract()  ) {
			final String text = classDoc.commentText();
			// ごくまれに、isEnum() も isEnumConstant() も false で、superClass().name() が、"Enum" の場合がある。
			// private static class で、継承させたい場合もあるため、final を付けない場合は、コメントに、『継承される』と記述しておく
			if( ( text == null || !text.contains( "継承される" ) ) && classDoc.superclass() != null && !"Enum".equalsIgnoreCase( classDoc.superclass().name() ) ) {
				final String wormMsg = classDoc.modifiers() + " " + classDoc.name() + "\t" + classDoc.position();
				System.err.println( "警告5:private static final=\t" + wormMsg );
			}
		}

		// 警告5:ConcurrentHashMap チェック(マルチスレッドでの安全性確保)
		// 内部クラスは、都度作成するようにするので、対象外。
	//	final String clsmod = classDoc.modifiers();
	//	if( !"private static final".equals( clsmod ) ) {
		if( !( classDoc.isPrivate() && classDoc.isStatic() && classDoc.isFinal() ) ) {
			for( final FieldDoc fld : classDoc.fields( false ) ) {
				final String text = fld.commentText();
				// コメントが無いか、Concurrent という文字が無く、synchronized もなく、unmodifiable という文字列もない場合は、対象フィールド
				if( text == null || !text.contains( "Concurrent" ) && !text.contains( "synchronized" ) && !text.contains( "unmodifiable" ) ) {		// 6.9.7.0 (2018/05/14) PMD
					final String type = fld.type().simpleTypeName();
					// フィールド定義が、Map の場合、対象になる。(ConcurrentMap は、対象外になる)
					if( "Map".equals( type ) ) {
						final String wormMsg = fld.modifiers() + " " + type + " " + fld.name() + "\t" + fld.position();
						System.err.println( "警告5:ConcurrentHashMap=\t" + wormMsg );
					}
				}
			}
		}
	}

	/**
	 * メンバークラス(コンストラクタ、メソッド)をXML化します。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) コメントは文字列でなく、Tag配列として処理させる。
	 *
	 * @param	menber		ExecutableMemberDocオブジェクト
	 * @param	menberType	メンバータイプ(コンストラクタ、メソッド)
	 * @param	writer		Tagを書き出すWriterオブジェクト
	 * @param	extendFlag	継承状態 [0:オリジナル/1::org.opengion関連Extend/2:Java関連Extend]
	 */
	private static void menberTag(	final ExecutableMemberDoc menber,
									final String menberType,
									final DocletTagWriter writer,
									final int extendFlag ) {

		final String modifiers ;
		if( menber instanceof MethodDoc ) {
			// メソッドの処理
			final Type rtnType = ((MethodDoc)menber).returnType();
			final StringBuilder modifyBuf = new StringBuilder( BUFFER_MIDDLE )
				.append( menber.modifiers() )
				.append( ' ' ).append( rtnType.typeName() );		// 6.0.2.5 (2014/10/31) char を append する。
			if( rtnType.dimension() != null ) { modifyBuf.append( rtnType.dimension() ); }

			modifiers = modifyBuf.toString();
		}
		else {
			// コンストラクター処理
			modifiers  = menber.modifiers();
		}

		final String menberName = menber.name();

		// 6.0.2.5 (2014/10/31) char を append する。
		final StringBuilder sigBuf = new StringBuilder( BUFFER_MIDDLE )
			.append( menberName ).append( '(' ) ;
		final Parameter[] prm = menber.parameters();

		for( int k=0; k<prm.length; k++ ) {
			final Type ptyp = prm[k].type();
			final String prmNm =prm[k].name();

			sigBuf.append( ptyp.typeName() ).append( ptyp.dimension() ).append( ' ' )
					.append( prmNm ).append( ',' );
		}

		if( prm.length > 0 ) { sigBuf.deleteCharAt( sigBuf.length()-1 ); }
		sigBuf.append( ')' );
		final String signature = sigBuf.toString();

		final Tag[]		desc	= menber.firstSentenceTags();
		final Tag[]		cmnt	= menber.inlineTags();									// 5.5.4.1 (2012/07/06)
		final Tag[]		tags	= menber.tags();
		final Tag[]		revTags = menber.tags(OG_REV);
		final String	extend  = String.valueOf( extendFlag );
		final String	extClass = ( extendFlag == 0 ) ? "" : menber.containingClass().qualifiedName() ;

		final String	position = String.valueOf( menber.position().line() );

		writer.printTag( "  <menber>" );
		writer.printTag( "    <type>"		,menberType	,"</type>"			);
		writer.printTag( "    <name>"		,menberName	,"</name>"			);
		writer.printTag( "    <modifiers>"	,modifiers	,"</modifiers>"		);
		writer.printTag( "    <signature>"	,signature	,"</signature>"		);
		writer.printTag( "    <position>"	,position	,"</position>"		);
		writer.printTag( "    <extendClass>",extClass	,"</extendClass>"	);
		writer.printTag( "    <extendFlag>"	,extend		,"</extendFlag>"	);
		writer.printTag( "    <description>",desc		,"</description>"	);
		writer.printTag( "    <contents>"	,cmnt		,"</contents>"		);
		writer.printTag( "    <tagText>" );
		writer.printTagsInfo(   tags );
		writer.printTag( "    </tagText>" );
		writer.printTag( "    <history>"	,revTags	,"</history>" );
		writer.printTag( "  </menber>");
	}

	/**
	 * 指定のキーの  FieldDoc オブジェクトを、ClassDoc から見つけて返します。
	 *
	 * キー情報は、大文字、小文字の区別なく、最初に見つかった、フィールド名の
	 * FieldDoc オブジェクトを見つけます。
	 * 内部的には、ループを回して検索していますので、非効率です。
	 * 見つからない場合は、null を返します。
	 *
	 * @og.rev 5.7.1.1 (2013/12/13) 新規作成
	 *
	 * @param	classDoc	検索元のClassDoc
	 * @param	key			検索するキー
	 * @return	FieldDocオブジェクト
	 */
	private static FieldDoc findFieldDoc( final ClassDoc classDoc ,final String key ) {
		FieldDoc rtn = null;

		final FieldDoc[] fld = classDoc.fields( false ) ;

		for( int i=0; i<fld.length; i++ ) {
	//		if( key.equalsIgnoreCase( fld[i].name() ) ) {
			if( key.equals( fld[i].name() ) ) {					// 7.3.0.0 (2021/01/06) taglet2 で小文字の version を採用したので。
				rtn = fld[i];
				break;
			}
		}
		return rtn;
	}

	/**
	 * カスタムオプションを使用するドックレットの必須メソッド optionLength(String) です。
	 *
	 * ドックレットに認識させる各カスタムオプションに、 optionLength がその
	 * オプションを構成する要素 (トークン) の数を返さなければなりません。
	 * このカスタムオプションでは、 -tag オプションそのものと
	 * その値の 2 つの要素で構成されるので、作成するドックレットの
	 * optionLengthメソッドは、 -tag オプションに対して 2 を返さなくては
	 * なりません。また、認識できないオプションに対しては、0 を返します。
	 *
	 * @param	option	オプション文字列
	 *
	 * @return	要素(トークン) の数
	 */
	public static int optionLength( final String option ) {
		if( "-version".equalsIgnoreCase(option) ) {
			return 2;
		}
		else if( "-outfile".equalsIgnoreCase(option) ) {
			return 2;
		}
		else if( "-debugLevel".equalsIgnoreCase(option) ) {
			return 2;
		}
		return 0;
	}
}
