/*
 * 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.hayabusa.taglib;

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.common.BuildNumber;
import org.opengion.hayabusa.resource.UserInfo;
import org.opengion.hayabusa.resource.GUIInfo;

import org.opengion.fukurou.util.EnumType ;
import org.opengion.fukurou.util.ErrorMessage;
import org.opengion.fukurou.util.LogSender;
import org.opengion.fukurou.util.StringUtil ;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import org.opengion.fukurou.mail.MailTX ;
import static org.opengion.fukurou.util.StringUtil.nval ;

/**
 * JSPのエラー発生時の処理を行うタグです。
 *
 * JSPでは、エラー発生時に、エラーページに飛ばす機能があります。現在のエンジンでは、
 * common/error.jsp ページ内で、処理を行っていますが、表示形式の整形、エラーメールの送信、
 * ログへの出力、エラー文字列の表示(Exceptionをそのままユーザーに見せるのは良くない)
 * などの、細かい対応が必要です。
 * ここでは、それらをタグ化して、属性で指定できるようにしました。
 *
 * エラー発生時にメールでエラー内容を飛ばすことも可能です。
 * これは、システムパラメータの COMMON_MAIL_SERVER に、ERROR_MAIL_TO_USERS に送信します。
 * ERROR_MAIL_TO_USERS が未設定の場合は、送信しません。
 *
 * @og.formSample
 * ●形式：
 *     &lt;og:error
 *          useMail     = "[true|false]"                    メール送信可否を指定します(初期値:true)
 *          logMsgType  = "[LONG|MEDIUM|SHORT|NONE]"        ログに書き込むメッセージの形式を指定(初期値:MEDIUM)
 *          viewMsgType = "[LONG|MEDIUM|SHORT|NONE|ALLNONE|TABLE]"  画面に表示するメッセージの形式を指定(初期値:SHORT)
 *     /&gt;
 *
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を解析します)
 *
 * ●Tag定義：
 *   &lt;og:error
 *       useMail            【TAG】メール送信可否を指定します(初期値:true)
 *       logMsgType         【TAG】ログに書き込むメッセージの形式を指定(初期値:MEDIUM)
 *       viewMsgType        【TAG】画面に書き込むメッセージの形式を指定(初期値:MEDIUM)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *       skipPage           【TAG】エラーが発生した時に、以降の処理をスキップするか(初期値:false[=スキップしない])
 *   &gt;   ... Body ...
 *   &lt;/og:error&gt;
 *
 * ●使用例
 *     &lt;og:error /&gt;
 *
 * @og.rev 4.0.0.0 (2005/08/31) 新規作成
 * @og.group エラー処理
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ErrorTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.2.1.0 (2015/03/13)" ;

	private static final long serialVersionUID = 621020150313L ;

	/**
	 * ログメッセージタイプ 属性として指定できる選択肢を定義します。
	 */
	private static final EnumType<String> LOG_MSGTYPE =
				new EnumType<>( "ログメッセージタイプ" , "MEDIUM" )
					.append( "LONG"		,"詳細メッセージを作成します。" )
					.append( "MEDIUM"	,"標準メッセージを作成します。" )
					.append( "SHORT"	,"簡易メッセージを作成します。" )
					.append( "NONE"		,"メッセージを作成しません。" ) ;

	/**
	 * 表示メッセージタイプ 属性として指定できる選択肢を定義します。
	 */
	private static final EnumType<String> VIEW_MSGTYPE =
				new EnumType<>( "表示メッセージタイプ" , "SHORT" )
					.append( "LONG"		,"詳細メッセージを作成します。" )
					.append( "MEDIUM"	,"標準メッセージを作成します。" )
					.append( "SHORT"	,"簡易メッセージを作成します。" )
					.append( "NONE"		,"メッセージを作成しません。" )
					.append( "ALLNONE"	,"何も出力しません。" )
					.append( "TABLE"	,"テーブル形式でエラーメッセージのみを表示します。" );

	private final String MAIL_SERVER = nval( HybsSystem.sys( "COMMON_MAIL_SERVER" ),null );
	private final String MAIL_USERS	 = nval( HybsSystem.sys( "ERROR_MAIL_TO_USERS" ),null ) ;
	//	 private final String FROM_USER	 = nval( HybsSystem.sys( "MAIL_DAEMON_DEFAULT_USER" ),"ENGINE" )
	// 											+ "@"
	//											+ nval( HybsSystem.sys( "ERROR_MAIL_TO_USERS" ),"DUMMY" ) ;
	private final String FROM_USER	 = nval( HybsSystem.sys( "ERROR_MAIL_FROM_USER" ),"ENGINE@DUMMY" ); // 4.4.0.1 (2009/08/08)

	private final String TITLE = "【" + HybsSystem.sys( "SYSTEM_ID" ) + "】"
											 + HybsSystem.sys( "GUI_TOP_TITLE" ) + "Error!" ;

	private boolean useMail		= true;
	private String	logMsgType	= LOG_MSGTYPE.getDefault();
	private String	viewMsgType	= VIEW_MSGTYPE.getDefault();

	private boolean	skipPage	; 			// 4.1.0.0 (2008/01/11)
	private String	messageBody	; 			// 4.1.0.0 (2008/01/11)

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @og.rev 4.1.0.0 (2008/01/11) 新規作成
	 *
	 * @return	後続処理の指示( EVAL_BODY_BUFFERED )
	 */
	@Override
	public int doStartTag() {
		return EVAL_BODY_BUFFERED ;	// Body を評価する。( extends BodyTagSupport 時)
	}

	/**
	 * Taglibのタグ本体を処理する doAfterBody() を オーバーライドします。
	 *
	 * @og.rev 4.1.0.0 (2008/01/11) 新規作成
	 *
	 * @return	後続処理の指示(SKIP_BODY)
	 */
	@Override
	public int doAfterBody() {
		messageBody = getBodyString();
		return SKIP_BODY ;
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @og.rev 4.0.0.0 (2005/12/31) UserInfo が存在しない場合の処理を追加します。
	 * @og.rev 4.1.0.0 (2008/01/11) ボディー部分のメッセージを表示する。
	 * @og.rev 5.0.0.4 (2009/08/28) ALLNONE追加
	 * @og.rev 5.1.8.0 (2010/07/01) テーブル形式メッセージ表示対応
	 * @og.rev 6.2.1.0 (2015/03/13) UserInfo が null の場合の処理を追加。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( CR )
			.append( "Title :" ).append( TITLE ).append( CR )
			.append( "Version :" ).append( BuildNumber.ENGINE_INFO ).append( CR );

	 	// 4.0.0 (2005/12/31) UserInfo が存在しない場合の処理を追加します。
		String userId = null;
		try {
			final UserInfo userInfo = (UserInfo)getSessionAttribute( HybsSystem.USERINFO_KEY );	// 6.2.1.0 (2015/03/13)
			if( userInfo != null ) {
				userId = userInfo.getUserID();
				buf.append( "ID=[" ).append( userId )
		//			.append( "] NAME=[" ).append( userInfo.getJname() )
					.append( "] LOGIN=[" ).append( HybsSystem.getDate( userInfo.getLoginTime() ) )
					.append( ']' );						// 6.0.2.5 (2014/10/31) char を append する。
			}
		}
		catch( HybsSystemException ex ) {
			buf.append( "User is null" );
		}
		buf.append( CR )
			.append( "GUI Information  : " );
		final GUIInfo guiInfo = (GUIInfo)getSessionAttribute( HybsSystem.GUIINFO_KEY );
		final String guiId ;
		if( guiInfo != null ) {
			guiInfo.addErrorCount();
			guiId = guiInfo.getKey();
			buf.append( "KEY=[" ).append( guiId )
				.append( "] LABEL=[" ).append( guiInfo.getLabel() )
				.append( ']' );						// 6.0.2.5 (2014/10/31) char を append する。
		}
		else {
			guiId = null ;
			buf.append( "GUI is null" );
		}
		buf.append( CR );

		final Throwable th = pageContext.getException() ;
		if( th != null ) {
			buf.append( th.getMessage() ).append( CR );
		}
		buf.append( "-----" ).append( CR );

		final String errHeader = buf.toString();

		// ログ情報出力
		final String logMsg = getStackTrace( th ,logMsgType );

	 	// 4.0.0 (2005/12/31) UserInfo が存在しない場合の処理を追加します。
		final LogSender log = new LogSender( userId );
		log.setGuiId( guiId );
		log.setMsgId( messageBody ); // 4.1.0.0 (2008/01/12)
		log.error( errHeader );
		log.error( logMsg );
		log.flush();

		// メール送信
		if( useMail && MAIL_SERVER != null && MAIL_USERS != null ) {
			final String[] to = StringUtil.csv2Array( MAIL_USERS );

			final MailTX tx = new MailTX( MAIL_SERVER );
			tx.setFrom( FROM_USER );
			tx.setTo( to );
			tx.setSubject( TITLE );
			tx.setMessage( errHeader + logMsg );
			tx.sendmail();
		}

		// 画面出力
		// 5.0.0.2 (2009/09/15) ALLNONE以外のみ出力
		if( !"ALLNONE".equals( viewMsgType ) ) {
			final String viewMsg ;
			if( logMsgType.equals( viewMsgType ) ) {
				viewMsg = errHeader + logMsg ;
			}
			// 5.1.8.0 (2010/07/01) テーブル形式メッセージ表示対応
			else if( "TABLE".equals( viewMsgType ) ) {
				viewMsg = getTableMsg( pageContext.getException() );
			}
			else {
				viewMsg = errHeader + getStackTrace( pageContext.getException() ,viewMsgType );
			}
			jspPrint( viewMsg );
		}

		// 6.3.6.1 (2015/08/28) なんとなくまとめました。
		return skipPage ? SKIP_PAGE : EVAL_PAGE ;

//		if( skipPage )	{
//			return SKIP_PAGE;
//		}
//		else {
//			return EVAL_PAGE;
//		}
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 */
	@Override
	protected void release2() {
		super.release2();
		useMail		= true;
		logMsgType	= LOG_MSGTYPE.getDefault();
		viewMsgType	= VIEW_MSGTYPE.getDefault();
		skipPage	= false; // 4.1.0.0 (2008/01/11)
		messageBody	= null; // 4.1.0.0 (2008/01/11)
	}

	/**
	 * この Throwable オブジェクトの詳細メッセージ文字列を返します。
	 * このクラスは、発生元の Throwable の StackTrace を、例外チェーン機能
	 * を利用して取得しています。
	 * また、"org.opengion." を含むスタックトレースのみ、メッセージとして追加します。
	 *
	 * @og.rev 5.0.0.2 (2009/09/15) ALLNONE追加
	 *
	 * @param    thr Throwableオブジェクト
	 * @param    type スタックトレースを行うタイプ [LONG/MEDIUM/SHORT/NONE]
	 *
	 * @return   メッセージ
	 * @og.rtnNotNull
	 */
	private String getStackTrace( final Throwable thr,final String type ) {
		// if( "NONE".equals( type ) ) { return ""; }
		if( "NONE".equals( type ) || "ALLNONE".equals( type ) ) { return ""; } // 5.0.0.2 (2009/09/15)

		final StringBuilder buf   = new StringBuilder( BUFFER_MIDDLE );
		StringBuilder trace = new StringBuilder( BUFFER_MIDDLE );

		Throwable th = thr ;
		while( th != null ) {
			trace = getStackData( trace,th,type );

			// 同じメッセージがあれば、登録しない。
			final String msg = th.getMessage();
			if( msg != null && buf.indexOf( msg ) < 0 ) {
				buf.append( msg ).append( CR );
			}

			th = th.getCause();
		}

		buf.append( trace.toString() )
			.append( "------------------------------------------------------" ).append( CR );

		return buf.toString();
	}

	/**
	 * タイプに応じたスタックトレース情報を StringBuilder に追加して返します。
	 * スタックトレース情報は、type が、NONE では、作成しません。
	 * SHORT の場合は、一番最初に現れた org.opengionパッケージのみを追加します。
	 *
	 * @og.rev 5.0.0.2 (2009/09/15) ALLNONE追加
	 *
	 * @param	buf	以前のエラーメッセージ
	 * @param	th	スタックトレースを取り出すThrowableオブジェクト
	 * @param	type	スタックトレースを行うタイプ [LONG/MEDIUM/SHORT/NONE]
	 *
	 * @return	メッセージ
	 */
	private StringBuilder getStackData( final StringBuilder buf,final Throwable th,final String type ) {
		// type が、NONE は、引数の StringBuilder をそのまま返します。
		// if( "NONE".equals( type ) ) { return buf; }
		if( "NONE".equals( type ) || "ALLNONE".equals( type ) ) { return buf; } // 5.0.0.2 (2009/09/15)

		String pkgKey = "org.opengion.";		// type="SHORT,MEDIUM" の初期値
		int    stcCnt = 5;			// type="MEDIUM" の初期値

		if( "LONG".equals( type ) ) {
			pkgKey = "";
			stcCnt = 100;
		}

		if( "SHORT".equals( type ) ) {
			stcCnt = 0;
		}

		if( th != null ) {
			int cnt = 0;
			final StackTraceElement[] trace = th.getStackTrace();
			for( int i=0; i<trace.length; i++ ) {
				final String msg = trace[i].toString();
				if( msg != null && buf.indexOf( msg ) < 0 ) {
					if( msg.indexOf( pkgKey ) >= 0 ) {
						buf.append( "\tat " ).append( msg ).append( CR );
						if( "SHORT".equals( type ) ) { break; }
					}
					// 6.0.2.5 (2014/10/31) refactoring
					else if( cnt < stcCnt ) {
						buf.append( "\tat " ).append( msg ).append( CR );
						cnt++ ;
					}
			//		else if( cnt++ == stcCnt ) {
			//			buf.append( "\t   ......" ).append( CR );
			//		}
				}
			}
			buf.append( "\t   ... more ..." ).append( CR );
		}
		return buf;
	}

	/**
	 * この Throwable オブジェクトのエラーメッセージ文字列をテーブル形式で返します。
	 * この形式では、スタックトレースなどは表示されず、エラーメッセージのみが表示されます。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) テーブル形式メッセージ表示対応
	 *
	 * @param    thr Throwableオブジェクト
	 *
	 * @return   メッセージ
	 * @og.rtnNotNull
	 */
	private String getTableMsg( final Throwable thr ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		Throwable th = thr;
		final ErrorMessage errMsgObj = new ErrorMessage( "System Error!" );
		while( th != null ) {
			final String msg = nval( th.getMessage(), "System Error(null)" );
			// 重複メッセージは登録しない。
			if( msg != null && buf.indexOf( msg ) < 0 ) {
				buf.append( msg );
				errMsgObj.addMessage( 0,ErrorMessage.NG,"SYSERR",msg );
			}
			th = th.getCause();
		}

		return TaglibUtil.makeHTMLErrorTable( errMsgObj, getResource() );
	}

	/**
	 * 【TAG】エラー発生時に管理者にメール送信するかどうかを指定します(初期値:true)。
	 *
	 * @og.tag
	 * メールは、システムパラメータの COMMON_MAIL_SERVER に、ERROR_MAIL_TO_USERS に送信します。
	 * ERROR_MAIL_TO_USERS が未設定の場合は、送信しません。
	 * 初期値は、true(送信する)です。
	 *
	 * @param	flag メール送信可否 [true:する/false:しない]
	 */
	public void setUseMail( final String flag ) {
		useMail = nval( getRequestParameter( flag ),useMail );
	}

	/**
	 * 【TAG】ログに書き込むメッセージの形式を指定(初期値:MEDIUM)。
	 *
	 * @og.tag
	 * ログ、および、メール送信時のメッセージの形式を指定します。
	 * エラー時のExceptionは、階層構造になっており、ルートまでさかのぼることが
	 * 可能です。また、通常は、スタックとレース情報より、エラーのプログラムを
	 * 特定することで、早く対応することが可能になります。
	 * メッセージの形式には、LONG|MEDIUM|SHORT|NONE が指定できます。
	 * ボディー部分に記述されたメッセージは全ての場合で出力されます。
	 *
	 *   ・LONG  :すべてのスタックトレース情報を取得します。
	 *   ・MEDIUM:org.opengion以下のパッケージのみスタックトレース情報を取得します。
	 *   ・SHORT :メッセージ部分のみ情報を取得します。
	 *   ・NONE  :取得しません。
	 *
	 * 初期値は、MEDIUM です。
	 *
	 * @param	logType ログに書き込むメッセージの形式 [LONG|MEDIUM|SHORT|NONE]
	 * @see		#setViewMsgType( String )
	 */
	public void setLogMsgType( final String logType ) {
		logMsgType = LOG_MSGTYPE.nval( logType );
	}

	/**
	 * 【TAG】画面に書き込むメッセージの形式を指定(初期値:MEDIUM)。
	 *
	 * @og.tag
	 * 画面に表示するメッセージの形式を指定します。
	 * エラー時のExceptionは、階層構造になっており、ルートまでさかのぼることが
	 * 可能です。また、通常は、スタックとレース情報より、エラーのプログラムを
	 * 特定することで、早く対応することが可能になります。
	 * メッセージの形式には、LONG|MEDIUM|SHORT|NONE|ALLNONE|TABLE が指定できます。
	 * ボディー部分に記述されたメッセージは全ての場合で出力されます。
	 *
	 *   ・LONG   :すべてのスタックトレース情報を取得します。
	 *   ・MEDIUM :org.opengion以下のパッケージのみスタックトレース情報を取得します。
	 *   ・SHORT  :メッセージ部分のみ情報を取得します。
	 *   ・NONE   :取得しません。
	 *   ・ALLNONE:ヘッダも表示しません。
	 *   ・TABLE  :テーブル形式でエラーメッセージのみを表示します。
	 *
	 * 初期値は、SHORT です。
	 *
	 * @param	viewType 画面に出力するメッセージの形式 [LONG|MEDIUM|SHORT|NONE|ALLNONE|TABLE]
	 * @see		#setLogMsgType( String )
	 */
	public void setViewMsgType( final String viewType ) {
		viewMsgType = VIEW_MSGTYPE.nval( viewType );
	}

	/**
	 * 【TAG】エラーが発生した時に、以降の処理をスキップするか(初期値:false[=スキップしない])。
	 *
	 * @og.tag
	 * エラーが発生した時に、以降の処理をスキップするかを設定します。
	 * trueが設定された場合は、以降の処理をスキップします。
	 *
	 * 初期値は、false(スキップしない) です。
	 *
	 * @param	flag 以降の処理のスキップ [true:する/false:しない]
	 */
	public void setSkipPage( final String flag ) {
		skipPage = nval( getRequestParameter( flag ),skipPage );
	}

	/**
	 * デバッグ時の文字列を返します。
	 *
	 * @return	このオブジェクトのデバッグ表現文字列
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return ToString.title( this.getClass().getName() )
				.println( "VERSION"						,VERSION	)
				.println( "useMail"						,useMail	)
				.println( "logMsgType"					,logMsgType	)
				.println( "viewMsgType"					,viewMsgType)
				.println( "messageBody"					,messageBody)
				.println( "skipPage"					,skipPage)
				.println( "COMMON_MAIL_SERVER"			,MAIL_SERVER	)
				.println( "ERROR_MAIL_TO_USERS"			,MAIL_USERS		)
				.println( "MAIL_DAEMON_DEFAULT_USER"	,FROM_USER		)
				.println( "Other..."					,getAttributes().getAttribute() )
				.fixForm().toString() ;
	}
}
