/*
 * 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.HybsSystemException;
import org.opengion.fukurou.util.LogWriter;
import org.opengion.fukurou.util.FileUtil ;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import org.opengion.fukurou.process.MainProcess;
import org.opengion.fukurou.process.HybsProcess;
import org.opengion.fukurou.process.LoggerProcess;
import org.opengion.fukurou.process.Process_Logger;

import static org.opengion.fukurou.util.StringUtil.nval ;

import javax.servlet.jsp.JspWriter ;
import javax.servlet.http.HttpServletRequest ;
import javax.servlet.http.HttpServletResponse;

import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

import java.io.PrintWriter ;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.IOException;

/**
 * HybsProcess を継承した、ParamProcess,FirstProcess,ChainProcess の実装クラスを
 * 実行する MainProcess を起動するクラスです。
 * LoggerProcess は、最初に定義するクラスで、画面ログ、ファイルログ、を定義します。
 * また、エラー発生時に、指定のメールアドレスにメール送信できます。
 * Process_Logger は、なくても構いませんが、指定する場合は、最も最初に指定しなければ
 * なりません。
 *
 * ParamProcess は、一つだけ定義できるクラスで、データベース接続情報を定義します。
 * (データベース接続しなければ)なくても構いません。
 *
 * FirstProcess は、処理を実行する最初のクラスで、このクラスでデータが作成されます。
 * ループ処理は、この FirstProcess で順次作成された LineModel オブジェクトを
 * １行づつ下位の ChainProcess に流していきます。
 * ChainProcess は、FirstProcess で作成されたデータを、受け取り、処理します。
 * 処理対象から外れる場合は、LineModel を null に設定する為、下流には流れません。
 * フィルタチェインの様に使用します。なくても構いませんし、複数存在しても構いません。
 *
 * @og.formSample
 * ●形式：&lt;og:mainProcess
 *           useJspLog ="[true/false]"
 *           useDisplay="[true/false]" &gt;
 *             &lt;og:process processID="ZZZ" &gt;
 *                 &lt;og:param key="AAA" value="111" /&gt;
 *             &lt;/og:process &gt;
 *         &lt;/og:mainProcess &gt;
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を解析します)
 *
 * ●Tag定義：
 *   &lt;og:mainProcess
 *       command            【TAG】(通常使いません)処理の実行を指定する command を設定できます(初期値:NEW)
 *       useJspLog          【TAG】ログ出力先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)
 *       useDisplay         【TAG】画面表示先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)
 *       useThread          【TAG】独立した別スレッドで実行するかどうか[true/false]を指定します(初期値:false)
 *       delayTime          【TAG】要求に対して、処理の実行開始を遅延させる時間を指定します(初期値:0秒)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   &gt;   ... Body ...
 *   &lt;/og:mainProcess&gt;
 *
 * ●使用例
 *   &lt;og:mainProcess
 *        useJspLog="true" &gt;
 *     &lt;og:process processID="DBReader" &gt;
 *        &lt;og:param key="dbid" value="FROM" /&gt;
 *        &lt;og:param key="sql"  value="select * from GE02" /&gt;
 *     &lt;/og:process &gt;
 *     &lt;og:process processID="DBWriter" &gt;
 *        &lt;og:param key="dbid"  value="TO" /&gt;
 *        &lt;og:param key="table" value="GE02" /&gt;
 *     &lt;/og:process &gt;
 *   &lt;/og:mainProcess &gt;
 *
 * @og.group 画面表示
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class MainProcessTag extends CommonTagSupport {
	//* このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "4.0.0.0 (2006/09/31)" ;

	private static final long serialVersionUID = 400020060931L ;

	/** command 引数に渡す事の出来る コマンド  新規 {@value} */
	public static final String CMD_NEW	 = "NEW" ;

	private static final Set<String> lockSet = new HashSet<String>();

	private List<HybsProcess> list ;

	private String  command		= CMD_NEW ;
	private boolean isJspLog	;
	private boolean isDisplay	;
	private boolean useThread	;

	private int		delayTime	;	// 処理の遅延時間(秒)
	private String  urlKey		;
	private boolean skipFlag	;

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doStartTag() {
		final HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
		urlKey = getUrlKey( request );

		synchronized( lockSet ) {
			// 新規追加は、true , すでに存在すれば、false を返します。
			final boolean lock = lockSet.add( urlKey );
			skipFlag = !CMD_NEW.equalsIgnoreCase( command ) || !lock && delayTime > 0 ;
		}

		if( skipFlag ) {
			System.out.println( "Skip Process : " + urlKey );
			return SKIP_BODY ;		// 処理しません。
		}
		else {
			list = new ArrayList<HybsProcess>();
			return EVAL_BODY_BUFFERED ;		// Body を評価する
		}
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)

		if( skipFlag ) { return SKIP_PAGE ; }

		// ログの出力先を切り替えます。
		if( isJspLog || isDisplay ) {
			initLoggerProcess();
		}

		boolean isOK = true;
		try {
			final DelayedProcess process = new DelayedProcess( delayTime,urlKey,list );
			if( useThread ) {
				new Thread( process ).start();
			}
			else {
				process.run();
			}

			// 実行結果を、"DB.ERR_CODE" キーでリクエストにセットする。
			final int errCode = process.getKekka();
			setRequestAttribute( "DB.ERR_CODE", String.valueOf( errCode ) );
		}
		catch( Throwable th ) {
			isOK = false;
			LogWriter.log( th );
			try {
				final HttpServletResponse responce = (HttpServletResponse)pageContext.getResponse();
				responce.sendError( 304 , "ERROR:" + th.getMessage() );
			}
			catch( IOException ex ) {
				LogWriter.log( ex );
			}
		}

		if( isOK )	{ return EVAL_PAGE ; }
		else		{ return SKIP_PAGE ; }
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 */
	@Override
	protected void release2() {
		super.release2();
		command		= CMD_NEW ;
		isJspLog	= false;
		isDisplay	= false;
		useThread	= false;
		delayTime	= 0;	// 処理の遅延時間(秒)
		list		= null;
	}

	/**
	 * 親クラスに登録するプロセスをセットします。
	 *
	 * @param	process	登録するプロセス
	 */
	protected void addProcess( final HybsProcess process ) {
		if( ! list.isEmpty() && process instanceof LoggerProcess ) {
			final String errMsg = "LoggerProcess は、最も最初に指定しなければなりません。";
			throw new HybsSystemException( errMsg );
		}
		list.add( process );
	}

	/**
	 * 【TAG】(通常使いません)処理の実行を指定する command を設定できます(初期値:NEW)。
	 *
	 * @og.tag
	 * この処理は、command="NEW" の場合のみ実行されます。RENEW時にはなにも行いません。
	 * 初期値は、NEW です。
	 *
	 * @param	cmd コマンド
	 * @see		<a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.MainProcessTag.CMD_NEW">コマンド定数</a>
	 */
	public void setCommand( final String cmd ) {
		command = nval( getRequestParameter( cmd ),command );
	}

	/**
	 * 【TAG】ログ出力先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * ログファイルは、processタグで、Logger を指定する場合に、パラメータ logFile にて
	 * ファイル名/System.out/System.err 形式で指定します。
	 * この場合、JSP 特有のWriterである、JspWriter(つまり、HTML上の返り値)は指定
	 * できません。
	 * ここでは、特別に ログの出力先を、JspWriter に切り替えるかどうかを指示
	 * できます。
	 * true を指定すると、画面出力(JspWriter) に切り替わります。
	 * 初期値は、false です。
	 *
	 * @param   flag JspWriter出力 [true:行う/false:行わない]
	 */
	public void setUseJspLog( final String flag ) {
		isJspLog = nval( getRequestParameter( flag ),isJspLog );
	}

	/**
	 * 【TAG】画面表示先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * 画面表示は、processタグで、Logger を指定する場合に、パラメータ dispFile にて
	 * ファイル名/System.out/System.err 形式で指定します。
	 * この場合、JSP 特有のWriterである、JspWriter(つまり、HTML上の返り値)は指定
	 * できません。
	 * ここでは、特別に ログの出力先を、JspWriter に切り替えるかどうかを指示
	 * できます。
	 * true を指定すると、画面出力(JspWriter) に切り替わります。
	 * 初期値は、false です。
	 *
	 * @param   flag JspWriter出力 [true:行う/false:行わない]
	 */
	public void setUseDisplay( final String flag ) {
		isDisplay = nval( getRequestParameter( flag ),isDisplay );
	}

	/**
	 * 【TAG】独立した別スレッドで実行するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * MainProcess 処理を実行する場合、比較的実行時間が長いケースが考えられます。
	 * そこで、実行時に、スレッドを生成して処理を行えば、非同期に処理を行う
	 * 事が可能です。
	 * ただし、その場合の出力については、JspWriter 等で返すことは出来ません。
	 * 起動そのものを、URL指定の http で呼び出すのであれば、返り値を無視する
	 * ことで、アプリサーバー側のスレッドで処理できます。
	 * 初期値は、順次処理(false)です。
	 *
	 * @param   flag 独立スレッド実行 [true:スレッドを使う/false:順次処理で行う]
	 */
	public void setUseThread( final String flag ) {
		useThread = nval( getRequestParameter( flag ),useThread );
	}

	/**
	 * 【TAG】要求に対して、処理の実行開始を遅延させる時間を指定します(初期値:0秒)。
	 *
	 * @og.tag
	 * プロセス起動が、同時に大量に発生した場合に、すべての処理を行うのではなく、
	 * ある程度待って、複数の処理を１回だけで済ますことが出来る場合があります。
	 * 例えば、更新データ毎にトリガが起動されるケースなどです。
	 * それらの開始時刻を遅らせる事で、同時発生のトリガを１回のプロセス処理で
	 * 実行すれば、処理速度が向上します。
	 * ここでは、処理が開始されると、タイマーをスタートさせ、指定時間経過後に、
	 * 処理を開始するようにしますが、その間、受け取ったリクエストは、すべて
	 * 処理せず破棄されます。
	 * ここでは、リクエストのタイミングと処理の開始タイミングは厳密に制御して
	 * いませんので、処理が重複する可能性があります。よって、アプリケーション側で
	 * リクエストが複数処理されても問題ないように、制限をかける必要があります。
	 * 遅延は、リクエスト引数単位に制御されます。
	 *
	 * @param	time	処理開始する遅延時間(秒)
	 */
	public void setDelayTime( final String time ) {
		delayTime = nval( getRequestParameter( time ),delayTime );
	}

	/**
	 * ログの出力先を切り替えます。
	 *
	 * LoggerProcess が存在すれば、そのログに、PrintWriter を直接指定します。
	 * 存在しない場合は、デフォルト LoggerProcess を作成して、指定します。
	 */
	private void initLoggerProcess() {
		final LoggerProcess logger ;
		final HybsProcess process = list.get(0);
		if( process instanceof LoggerProcess ) {
			logger = (LoggerProcess)process;
		}
		else {
			logger = new Process_Logger();
			list.add( 0,logger );
		}

		final JspWriter out = pageContext.getOut();
		final PrintWriter writer = FileUtil.getNonFlushPrintWriter( out );
		if( isJspLog ) {
			logger.setLoggingWriter( writer );
		}

		if( isDisplay ) {
			logger.setDisplayWriter( writer );
		}
	}

	/**
	 * このリクエストの引数を返します。
	 *
	 * @param	request	HttpServletRequestオブジェクト
	 *
	 * @return	request.getRequestURL() + "?" + request.getQueryString()
	 * @og.rtnNotNull
	 */
	private String getUrlKey( final HttpServletRequest request ) {
		final StringBuffer address = request.getRequestURL();
		final String		 query = request.getQueryString();
		if( query != null ) {
			address.append( '?' ).append( query );
		}
		return address.toString();
	}

	/**
	 * シリアライズ用のカスタムシリアライズ書き込みメソッド。
	 *
	 * @og.rev 4.0.0.0 (2006/09/31) 新規追加
	 * @serialData 一部のオブジェクトは、シリアライズされません。
	 *
	 * @param	strm	ObjectOutputStreamオブジェクト
	 * @throws IOException	入出力エラーが発生した場合
	 */
	private void writeObject( final ObjectOutputStream strm ) throws IOException {
		strm.defaultWriteObject();
	}

	/**
	 * シリアライズ用のカスタムシリアライズ読み込みメソッド
	 *
	 * ここでは、transient 宣言された内部変数の内、初期化が必要なフィールドのみ設定します。
	 *
	 * @og.rev 4.0.0.0 (2006/09/31) 新規追加
	 * @serialData 一部のオブジェクトは、シリアライズされません。
	 *
	 * @param	strm	ObjectInputStreamオブジェクト
	 * @see #release2()
	 * @throws IOException	シリアライズに関する入出力エラーが発生した場合
	 * @throws ClassNotFoundException	クラスを見つけることができなかった場合
	 */
	private void readObject( final ObjectInputStream strm ) throws IOException , ClassNotFoundException {
		strm.defaultReadObject();
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return ToString.title( this.getClass().getName() )
				.println( "VERSION"				,VERSION			)
				.println( "list"				,list				)
				.fixForm().toString() ;
	}

	/**
	 * Runnable インターフェースのを継承した、内部実装クラス
	 *
	 */
	private static final class DelayedProcess implements Runnable {
		private final int delayTime ;
		private final String urlKey;
		private final List<HybsProcess> list;
		private int errCode = MainProcess.RETURN_INIT ;

		/**
		 * 引数を指定して、作成するコンストラクタ
		 *
		 * @param	delayTime	遅延時間(秒)
		 * @param	urlKey		リクエスト引数のパラメータ
		 * @param	list		登録されたプロセスのリスト
		 */
		public DelayedProcess( final int delayTime,final String urlKey,final List<HybsProcess> list ) {
			this.delayTime = delayTime;
			this.urlKey    = urlKey;
			this.list      = list;
		}

		/**
		 * 実行結果を返します。
		 *
		 * @return	結果(MainProcess#RETURN_INIT,RETURN_OK,RETURN_WARN,RETURN_NG)
		 * @see		org.opengion.fukurou.process.MainProcess#RETURN_OK
		 */
		public int getKekka() { return errCode; }

		/**
		 * スレッドで実行します。
		 */
		public void run() {
			if( delayTime > 0 ) {
				try {
					Thread.sleep( delayTime * 1000L );
				}
				catch( InterruptedException ex2 ) {
					System.out.println( "InterruptedException:" + ex2.getMessage() );
				}
			}
			synchronized( lockSet ) {
				lockSet.remove( urlKey );	// 処理の開始前に解除します。取りこぼし対策
			}

			try {
				final MainProcess process = new MainProcess();
				process.setList( list );
				process.run();
				errCode = process.getKekka();
			}
			catch( Throwable th ) {
				errCode = MainProcess.RETURN_NG;
				LogWriter.log( th );
			}
		}
	}
}
