/*
 * 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.ThrowUtil;
import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.lang.reflect.Field;
import java.security.AccessController;				// 6.1.0.0 (2014/12/26) findBugs
import java.security.PrivilegedAction;				// 6.1.0.0 (2014/12/26) findBugs

import com.sun.javadoc.Tag;
import com.sun.javadoc.Doc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ProgramElementDoc;

/**
 * Doclet を処理するプログラムで共通して使用される簡易メソッド群(ユーティリティクラス)です。
 *
 * @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 DocletUtil {

	private static final String CLS = "org.opengion.fukurou.system.BuildNumber";		// package.class
	private static final String FLD = "VERSION_NO";										// field
	private static final String VERSION_NO = getStaticField( CLS , FLD );				// 6.4.1.1 (2016/01/16) versionNo → VERSION_NO refactoring

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

	/**
	 * target 文字列に含まれる from 文字列を to 文字列に置き換えます。
	 *
	 * @param	target	元の文字列
	 * @param	from	置換元FROM
	 * @param	to		置換先TO
	 *
	 * @return	変換後文字列
	 */
	public static String replace( final String target,final String from,final String to ) {
		if( target == null || from == null || to == null ) { return target; }
		final StringBuilder strBuf = new StringBuilder( BUFFER_MIDDLE );

		int start = 0;
		int end   = target.indexOf( from,start );
		while( end >= 0  ) {
			strBuf.append( target.substring( start,end ) );
			strBuf.append( to );
			start = end + from.length();
			end   = target.indexOf( from,start );
		}
		strBuf.append( target.substring( start ) );

		return strBuf.toString();
	}

	/**
	 * セッターメソッドの setXXXX の set を削除し、次の文字を小文字化します。
	 * つまり、セッターメソッドから属性値を推測します。
	 * (超特殊処理)セッターメソッドのset以下２文字目が大文字の場合は、
	 * １文字目も大文字と考えて小文字化を行いません。
	 * 例えば、setSYS や setUSER など、RequestValueTag.javaに使用するケースです。
	 *
	 * @param target 処理対象となる文字列
	 *
	 * @return オプション文字列
	 */
	public static String removeSetter( final String target ) {
		if( target != null && target.startsWith( "set" ) ) {
			char[] chs = target.toCharArray();
			if( chs.length > 4 && !( chs[4] >= 'A' && chs[4] <= 'Z' ) ) {
				chs[3] = Character.toLowerCase( chs[3] ) ;
			}
			return new String( chs,3,chs.length-3 );
		}
		return target;
	}

	/**
	 * オプション配列文字列より、指定のキーに対応するオプション値を返します。
	 *
	 * @param	key		キー
	 * @param	options	オプション配列文字列
	 *
	 * @return	オプション文字列
	 */
	public static String getOption( final String key , final String[][] options ) {
		String rtn = "";
		if( key == null || options == null ) { return rtn; }

		for( int i=0; i<options.length; i++ ) {
			if( key.equalsIgnoreCase( options[i][0] ) ) {
				rtn = options[i][1];
				break ;
			}
		}
		return rtn ;
	}

	/**
	 * {&#064;og.value package.class#field} 形式のvalueタグを文字列に置き換えます。
	 *
	 * 処理的には、リフレクションで、値を取得します。値は、staticフィールドのみ取得可能です。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) 新規追加
	 * @og.rev 5.5.5.6 (2012/08/31) クラス名の取得で、ProgramElementDoc で処理するように変更
	 *
	 * @param tag Tagオブジェクト
	 *
	 * @return valueタグの解析結果の文字列
	 */
	public static String valueTag( final Tag tag ) {
		return valueTag( tag.text() , tag );
	}

	/**
	 * {&#064;og.value package.class#field} 形式のvalueタグを文字列に置き換えます。
	 *
	 * 処理的には、リフレクションで、値を取得します。値は、staticフィールドのみ取得可能です。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) 新規追加
	 * @og.rev 5.5.5.6 (2012/08/31) クラス名の取得で、ProgramElementDoc で処理するように変更
	 * @og.rev 6.3.5.1 (2015/08/12) HybsSystem とSystemData のみパッケージ無しでも処理できるように対応
	 *
	 * @param	txt	Tagテキスト (og.value 以下の文字列のみ)
	 * @param	tag	Tagオブジェクト (正確な class 名を求める場合に使用)
	 *
	 * @return valueタグの解析結果の文字列
	 */
	public static String valueTag( final String txt , final Tag tag ) {
		String text = txt;
		if( txt != null ) {
			String cls = null;		// package.class
			String fld = null;		// field
			// package.class#field 形式の解析。
			final int adrs = txt.indexOf('#') ;
			if( adrs > 0 ) {
				cls = txt.substring( 0,adrs );		// package.class
				fld = txt.substring( adrs+1 );		// field
			}
			else if( adrs == 0 ) {
				fld = txt.substring( 1 );			// #field
			}
			else {
				final String errMsg = "警告:{@value package.class#field} 形式のフィールド名 #field がありません。" + CR
								+ txt + CR ;
				System.err.println( errMsg );
				// # を付け忘れたと考え、自分自身のクラスを利用
				fld = txt;							// field
			}

			// package.class をきちんと作成する。
			final Doc doc = tag.holder();

			// 5.5.5.6 (2012/08/31) ProgramElementDoc で処理するように変更
			if( doc instanceof ProgramElementDoc ) {
				final ProgramElementDoc pdoc = (ProgramElementDoc)doc;
				final ClassDoc cdoc = pdoc.containingClass();
				// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
				if( cdoc == null ) {
					if( cls == null ) {					// package.class が登録されていない場合。
						cls = pdoc.qualifiedName();
					}
					else if( cls.indexOf('.') < 0 ) {
						// 6.3.5.1 (2015/08/12) HybsSystem とSystemData のみパッケージ無しでも処理できるように対応
						if( "HybsSystem".equals( cls ) || "SystemData".equals( cls ) ) {
							cls = "org.opengion.hayabusa.common." + cls ;
						}
						else {										// 同一パッケージ内と仮定
							final String tmp = pdoc.qualifiedName();
							final int ad = tmp.lastIndexOf('.');
							if( ad > 0 ) {
								cls = tmp.substring( 0,ad+1 ) + cls ;
							}
						}
					}
				}
				else {
					if( cls == null ) {					// package.class が登録されていない場合。
						cls = cdoc.qualifiedName() ;
					}
					else if( cls.indexOf('.') < 0 ) {	// class のみが登録されている場合。findClass で、package 込の正式クラス名を検索する。
						final ClassDoc fdoc = cdoc.findClass( cls );
						if( fdoc != null ) {
							cls = fdoc.qualifiedName() ;
						}
					}
				}
			}

			// 6.3.5.1 (2015/08/12) HybsSystem とSystemData のみパッケージ無しでも処理できるように対応
			if( "HybsSystem".equals( cls ) || "SystemData".equals( cls ) ) {
				cls = "org.opengion.hayabusa.common." + cls ;
			}

			//  5.6.3.3 (2013/04/19) メソッド化で共有します。
			text = getStaticField( cls,fld );
		}
		return text ;
	}

	/**
	 * {&#064;og.doc03Link queryType Query_**** クラス} 形式のdoc03Linkタグをリンク文字列に置き換えます。
	 *
	 * <a href="/gf/jsp/DOC03/index.jsp?command=NEW&GAMENID=DOC03&VERNO=X.X.X.X&VALUENAME=queryType" target="CONTENTS">Query_**** クラス</a>
	 * のようなリンクを作成します。
	 * 第一引数は、VALUENAME の引数です。
	 * それ以降のテキストは、リンク文字列のドキュメントになります。
	 * DOC03 画面へのリンクを作成するに当たり、バージョンが必要です。org.opengion.fukurou.system.BuildNumber#VERSION_NO から取得しますが、
	 * パッケージの優先順の関係で、リフレクションを使用します。
	 *
	 * @og.rev 5.6.3.3 (2013/04/19) 新規作成
	 * @og.rev 6.0.2.0 (2014/09/19) 処理を #doc03LinkTag( String ) で行う。
	 *
	 * @param tag Tagオブジェクト
	 *
	 * @return valueタグの解析結果の文字列
	 * @og.rtnNotNull
	 */
	public static String doc03LinkTag( final Tag tag ) {
		return doc03LinkTag( tag.text() );
	}

	/**
	 * {&#064;og.doc03Link queryType Query_****クラス} 形式のdoc03Linkタグをリンク文字列に置き換えます。
	 *
	 * &lt;a href="/gf/jsp/DOC03/index.jsp?command=NEW&amp;GAMENID=DOC03&amp;VERNO=X.X.X.X&amp;VALUENAME=queryType"
	 *          target="CONTENTS" &gt;Query_****クラス&lt;/a&gt;
	 * のようなリンクを作成します。
	 * 第一引数は、VALUENAME の引数です。
	 * それ以降のテキストは、リンク文字列のドキュメントになります。
	 * DOC03 画面へのリンクを作成するに当たり、バージョンが必要です。
	 * org.opengion.fukurou.system.BuildNumber#VERSION_NO から取得しますが、
	 * パッケージの優先順の関係で、リフレクションを使用します。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規追加
	 *
	 * @param txt Tagテキスト(og.doc03Link 以下の文字列のみ)
	 *
	 * @return valueタグの解析結果の文字列
	 * @og.rtnNotNull
	 */
	public static String doc03LinkTag( final String txt ) {
		if( txt != null ) {
			final String text = txt.trim();

			final int adrs = text.indexOf(' ') ;			// 最初のスペースで分離します。
			if( adrs > 0 ) {
				final String valnm = text.substring( 0,adrs ).trim();	// VALUENAME
				final String body  = text.substring( adrs+1 ).trim();	// ドキュメント

				return "&lt;a href=\"/gf/jsp/DOC03/index.jsp?command=NEW&amp;GAMENID=DOC03"
						+ "&amp;VERNO="     + VERSION_NO
						+ "&amp;VALUENAME=" + valnm
						+ "\" target=\"CONTENTS\"&gt;"
						+ body
						+ "&lt;/a&gt;" ;
			}
		}
		return "doc03Link 不明:" + txt ;
	}

	/**
	 * このタグレットがインラインタグで {&#064;link XXXX YYYY} を処理するように
	 * 用意された、カスタムメソッドです。
	 *
	 * @og.rev 6.0.2.0 (2014/09/19) 新規追加
	 *
	 * @param	txt	オリジナルの文字列
	 *
	 * @return	インラインタグの link を処理した結果の文字列
	 * @og.rtnNotNull
	 */
	public static String linkTag( final String txt ) {
		// {@link XXXX YY} の XXXX YY部分を処理
		if( txt != null ) {
			final String text = txt.trim() ;

			final int adrs = text.indexOf( ' ' );
			if( adrs > 0 ) {
				final String xxx = text.substring( 0,adrs ).trim();	// 前半：アドレス変換
				final String yyy = text.substring( adrs   ).trim();	// 後半：ラベル
				final String zzz = xxx.replace( '.','/' );

				return "<a href=\"../../../../" + zzz + ".html\" " 
							+ "title=\"" + xxx + "\">" + yyy + "</a>" ;
			}
		}

		return "";
	}

	/**
	 * パッケージ.クラス名 と、フィールド名 から、staticフィールドの値を取得します。
	 *
	 * Field fldObj = Class.forName( cls ).getDeclaredField( fld ); で、Fieldオブジェクトを呼出し、
	 * String.valueOf( fldObj.get( null ) ); で、値を取得しています。
	 * static フィールドは、引数 null で値を取得できます。
	 *
	 * ※ 超特殊処理として、cls名が、HybsSystem とSystemData のみ、パッケージ無しで処理
	 *    できるように対応します。
	 *
	 * 例；
     *      String cls = "org.opengion.fukurou.system.BuildNumber";        // package.class
     *      String fld = "VERSION_NO";                                      // field
	 *
	 * @og.rev 5.6.3.3 (2013/04/19) 新規作成
	 * @og.rev 6.4.2.0 (2016/01/29) StringUtil#ogStackTrace(Throwable) を、ThrowUtil#ogStackTrace(String,Throwable) に置き換え。
	 *
	 * @param cls パッケージ.クラス名
	 * @param fld フィールド名
	 * @return 取得値
	 */
	public static String getStaticField( final String cls , final String fld ) {
		String txt = null;
		try {
			final Field fldObj = Class.forName( cls ).getDeclaredField( fld );
			// privateフィールドへのアクセス。(セキュリティーマネージャーによってアクセス制限がかけられていない場合)
			// 6.1.0.0 (2014/12/26) findBugs: Bug type DP_DO_INSIDE_DO_PRIVILEGED (click for details) 
			// reflect.Field.setAccessible(boolean) の呼び出しは doPrivileged ブロックの中から呼び出すべきです。
			if( !fldObj.isAccessible() ) {
				AccessController.doPrivileged( new PrivilegedAction<DocletUtil>() {
					/**
					 * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。
					 *
					 * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。 
					 *
					 * @return  DocletUtilオブジェクト
					 */
					public DocletUtil run() {
						// privileged code goes here
						fldObj.setAccessible( true );
						return null; // nothing to return
					}
				});
			}
			txt = String.valueOf( fldObj.get( null ) );		// static フィールドは、引数 null で値を取得
		}
		// 6.0.2.5 (2014/10/31) findBugs:Bug type REC_CATCH_EXCEPTION 対応
		catch( final Throwable th ) {
			// 6.4.2.0 (2016/01/29) StringUtil#ogStackTrace(Throwable) を、ThrowUtil#ogStackTrace(String,Throwable) に置き換え。
			final String errMsg = "package.class=[" + cls + "],field=[" + fld + "] の取得に失敗しました。" + CR ;
			System.err.println( ThrowUtil.ogStackTrace( errMsg , th ) );
		}

		return txt ;
	}
}
