/*
 * 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.system.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 jakarta.servlet.jsp.JspWriter ;
import jakarta.servlet.http.HttpServletRequest ;
import jakarta.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.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秒)
 *       caseKey            【TAG】このﾀｸﾞ自体を利用するかどうかの条件ｷｰを指定します(初期値:null)
 *       caseVal            【TAG】このﾀｸﾞ自体を利用するかどうかの条件値を指定します(初期値:null)
 *       caseNN             【TAG】指定の値が､null/ｾﾞﾛ文字列 でない場合(Not Null=NN)は､このﾀｸﾞは使用されます(初期値:判定しない)
 *       caseNull           【TAG】指定の値が､null/ｾﾞﾛ文字列 の場合は､このﾀｸﾞは使用されます(初期値:判定しない)
 *       caseIf             【TAG】指定の値が､true/TRUE文字列の場合は､このﾀｸﾞは使用されます(初期値:判定しない)
 *       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 = "8.1.0.3 (2022/01/21)" ;
	private static final long serialVersionUID = 810320220121L ;

	/** command 引数に渡す事の出来る ｺﾏﾝﾄﾞ  新規 {@value} */
	public static final String CMD_NEW	 = "NEW" ;

	private static final Set<String> LOCK_SET = new HashSet<>();		// 6.4.1.1 (2016/01/16) lockSet  → LOCK_SET  refactoring

	private transient List<HybsProcess> list ;							// 6.3.9.0 (2015/11/06) transient 追加

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

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

	/**
	 * ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰ
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public MainProcessTag() { super(); }		// これも､自動的に呼ばれるが､空のﾒｿｯﾄﾞを作成すると警告されるので､明示的にしておきます｡

	/**
	 * Taglibの開始ﾀｸﾞが見つかったときに処理する doStartTag() を ｵｰﾊﾞｰﾗｲﾄﾞします｡
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) caseKey,caseVal,caseNN,caseNull,caseIf 属性対応
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doStartTag() {
		if( !useTag() ) { return SKIP_BODY ; }	// 6.3.4.0 (2015/08/01)

		final HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
		urlKey = getUrlKey( request );

		synchronized( LOCK_SET ) {
			// 新規追加は､true , すでに存在すれば､false を返します｡
			final boolean lock = LOCK_SET.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<>();
			return EVAL_BODY_BUFFERED ;		// Body を評価する
		}
	}

	/**
	 * Taglibの終了ﾀｸﾞが見つかったときに処理する doEndTag() を ｵｰﾊﾞｰﾗｲﾄﾞします｡
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) caseKey,caseVal,caseNN,caseNull,caseIf 属性対応
	 * @og.rev 8.1.0.3 (2022/01/21) ｽﾚｯﾄﾞに名前を付けておきます｡
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)
		if( !useTag() ) { return EVAL_PAGE ; }	// 6.3.4.0 (2015/08/01)

		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();
				new Thread( process,"MainProcessTag" ).start();		// 8.1.0.3 (2022/01/21)
			}
			else {
				process.run();
			}

			// 実行結果を､"DB.ERR_CODE" ｷｰでﾘｸｴｽﾄにｾｯﾄする｡
			final int errCode = process.getKekka();
			setRequestAttribute( "DB.ERR_CODE", String.valueOf( errCode ) );
		}
		catch( final Throwable th ) {
			isOK = false;
			LogWriter.log( th );
			try {
				final HttpServletResponse responce = (HttpServletResponse)pageContext.getResponse();
				responce.sendError( 304 , "ERROR:" + th.getMessage() );
			}
			catch( final 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();
	}

	/**
	 * このｵﾌﾞｼﾞｪｸﾄの文字列表現を返します｡
	 * 基本的にﾃﾞﾊﾞｯｸﾞ目的に使用します｡
	 *
	 * @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( final InterruptedException ex2 ) {
					System.out.println( "InterruptedException:" + ex2.getMessage() );
				}
			}
			synchronized( LOCK_SET ) {
				LOCK_SET.remove( urlKey );	// 処理の開始前に解除します｡取りこぼし対策
			}

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