/*
 * 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.db.DBTableModel;
import org.opengion.hayabusa.db.Query;
import org.opengion.hayabusa.db.DBSysArg;
import org.opengion.hayabusa.db.DBUserArg;
import org.opengion.hayabusa.resource.GUIInfo;

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

/**
 * PLSQLをCALLしてデータベースにアクセスするタグです。
 * queryType = "JDBCPLSQL" が、標準で用意されています。
 * queryType と 実際のJavaクラスとの関連付けは、システムリソースの Query_JDBCPLSQL 属性です。
 *
 * DBTableModel内のデータを 配列でPL/SQLに渡してDB登録します。
 *
 * ※ このタグは、Transaction タグの対象です。
 *
 * @og.formSample
 * ●形式：&lt;og:plsqlUpdate command="…" names="…" dbType="…" queryType="JDBCPLSQL" &gt;{plsql(?,?,?,?,?)} &lt;og:plsqlUpdate&gt;
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を解析します)
 *
 * ●Tag定義：
 *   &lt;og:plsqlUpdate
 *       queryType          【TAG】Query を発行する為のクラスIDを指定します({@og.doc03Link queryType 初期値:JDBCPLSQL})
 *       command            【TAG】コマンド(NEW,RENEW)をセットします(PlsqlUpdateTag,UpdateTag の場合は、ENTRY)
 *       scope              【TAG】キャッシュする場合のスコープ[request/page/session/applicaton]を指定します(初期値:session)
 *       maxRowCount        【TAG】(通常は使いません)データの最大読み込み件数を指定します (初期値:DB_MAX_ROW_COUNT[=1000])(0:[無制限])
 *       skipRowCount       【TAG】(通常は使いません)データの読み始めの初期値を指定します
 *       notfoundMsg        【TAG】検索結果がゼロ件の場合に表示するメッセージリソースIDを指定します(初期値:MSG0077[対象データはありませんでした])
 *       names              【TAG】PL/SQLを利用する場合の引数にセットすべき データの名称をCSV形式で複数指定します
 *       dbType             【TAG】Queryオブジェクトに渡す引数のタイプ定義(例:type名_ARRAY)
 *       selectedAll        【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:false)
 *       tableId            【TAG】(通常は使いません)結果のDBTableModelを、sessionに登録するときのキーを指定します
 *       dbid               【TAG】(通常は使いません)Queryオブジェクトを作成する時のDB接続IDを指定します
 *       stopError          【TAG】PLSQL/SQL処理エラーの時に処理を中止するかどうか[true/false]を設定します(初期値:true)
 *       dispError			【TAG】エラー時にメッセージを表示するか[true/false]を設定します。通常はstopErrorと併用(初期値:true)
 *       tableModelCommit   【TAG】テーブルモデルの確定処理を行うかどうか[true/false]を設定します(初期値:true)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   &gt;   ... Body ...
 *   &lt;/og:plsqlUpdate&gt;
 *
 * ●使用例
 *    ・引数/プロシジャーを他のJSPから渡す場合
 *    【copy.jsp】
 *        &lt;og:hidden name=&quot;names&quot; value=&quot;UNIQ,USRID,ECNO,EDBN&quot; /&gt;
 *        &lt;og:hidden name=&quot;SQL&quot; value=&quot;{ call RKP0271E.RK0271E( ?,?,?,?,? ) }&quot; /&gt;
 *    【entry.jsp】
 *        &lt;og:plsqlUpdate
 *            command    = "{&#064;command}"
 *            names      = "{&#064;names}"         →PL/SQLに渡す引数(配列)のカラム名
 *            dbType     = "RK0271ARG"             →PL/SQLに渡す引数(配列)の定義ファイル名
 *            queryType  = "JDBCPLSQL" &gt;
 *        {&#064;SQL}                              →CALLするPL/SQL
 *        &lt;/og:plsqlUpdate&gt;
 *
 *    ・引数/プロシジャーを直接書く場合
 *    【entry.jsp】
 *        &lt;og:plsqlUpdate
 *            command    = "{&#064;command}"
 *            names      = "UNIQ,USRID,ECNO,EDBN"  →PL/SQLに渡す引数(配列)のカラム名
 *            dbType     = "RK0271ARG"             →PL/SQLに渡す引数(配列)の定義ファイル名
 *            queryType  = "JDBCPLSQL" &gt;
 *        { call RKP0271E.RK0271E( ?,?,?,?,? )}    →CALLするPL/SQL
 *        &lt;/og:plsqlUpdate&gt;
 *
 *    ＜＜参考＞＞
 *    ・RKP0271E.RK0271E( ?,?,?,?,? )の｢?｣の意味
 *        (RKP0271E.spc)------------------------------------------------------------
 *        CREATE OR REPLACE PACKAGE RKP0271E AS
 *        PROCEDURE RK0271E(
 *             P_KEKKA    OUT    NUMBER           -- 1個目の｢?｣⇒結果 0:正常 1:警告 2:異常
 *            ,P_ERRMSGS  OUT    ERR_MSG_ARRAY    -- 2個目の｢?｣⇒エラーメッセージ配列
 *            ,P_NAMES     IN    VARCHAR2         -- 3個目の｢?｣⇒カラム名チェック用文字列
 *            ,P_SYSARGS   IN    SYSARG_ARRAY     -- 4個目の｢?｣⇒登録条件配列(改廃(A:追加/C:変更/D:削除)等がｾｯﾄされます)
 *            ,P_RK0271    IN    RK0271ARG_ARRAY  -- 5個目の｢?｣⇒登録データ配列
 *
 *    ・RK0271ARGの定義の仕方
 *        (RK0271ARG.sql)------------------------------------------------------------
 *        DROP TYPE RK0271ARG_ARRAY;
 *        CREATE OR REPLACE TYPE RK0271ARG AS OBJECT
 *        (
 *             UNIQ                VARCHAR2(11)
 *            ,USRID               VARCHAR2(5)
 *            ,ECNO                VARCHAR(7)
 *            ,EDBN                VARCHAR(2)
 *        ) ;
 *        /
 *        CREATE OR REPLACE TYPE RK0271ARG_ARRAY AS VARRAY(100) OF RK0271ARG;
 *        /
 *
 * @og.group ＤＢ登録
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class PlsqlUpdateTag extends QueryTag {
	//* このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "5.5.5.2 (2012/08/10)" ;

	private static final long serialVersionUID = 555220120810L ;

	/** command 引数に渡す事の出来る コマンド  登録{@value} */
	public static final String CMD_ENTRY  = "ENTRY" ;
	/** command 引数に渡す事の出来る コマンド リスト  */
	private static  final String COMMAND_LIST = CMD_ENTRY;

	/** 引数のタイプ定義  */
	protected String  userDBType = null;

	// 3.5.2.0 (2003/10/20) 内部オブジェクトタイプ名を システムパラメータ で定義します。
	private static final String SYSARG = "SYSARG";
	private boolean selectedAll = false;
	
	private boolean isTableModelCommit = true; // 5.5.5.2 (2012/08/10)

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

		table = (DBTableModel)getObject( tableId );
		if( table == null || table.getRowCount() == 0 ||
			! check( command, COMMAND_LIST ) ) { return(SKIP_BODY); }

		startQueryTransaction( tableId );		// 3.6.0.8 (2004/11/19)
		return( EVAL_BODY_BUFFERED );	// Body を評価する。( extends BodyTagSupport 時)
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @og.rev 3.1.1.2 (2003/04/04) Tomcat4.1 対応。release2() を doEndTag()で呼ぶ。
	 * @og.rev 3.5.5.2 (2004/04/02) TaglibUtil.makeHTMLErrorTable メソッドを利用
	 * @og.rev 3.6.0.8 (2004/11/19) DBTableModel をセーブする時に、トランザクションチェックを行います。
	 * @og.rev 3.6.1.0 (2005/01/05) オーバーフロー時と登録件数の表示をコメントします。
	 * @og.rev 4.3.3.0 (2008/09/22) 検索結果を、"DB.ERR_CODE" キーでリクエストにセットする。
	 * @og.rev 4.3.3.0 (2008/09/22) 属性 stopError の設定により、JSP処理を中止するかどうかを制御します。
	 * @og.rev 4.3.5.7 (2009/03/22) アクセスカウント不具合対応
	 * @og.rev 5.9.26.1 (2017/11/10) dispError対応
	 * @og.rev 5.10.2.2 (2018/08/24) 一部のエラーをjspPrintから標準のErrorMessageに変更
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)

		String label = HybsSystem.BR;				// 検索しなかった場合。
		if( check( command, COMMAND_LIST ) ) {
			//  5.10.2.2 (2018/08/24) 6.9.9.0 ｢ERR0041:検索処理中に割り込みの検索要求がありました｣エラーを、標準のErrorMessageに追加するようにします。
			if( table != null && ! commitTableObject( tableId, table ) ) {
				if( errMessage == null ) { errMessage = new ErrorMessage( "PlsqlUpdateTag Query Error!" ); }
				// ERR0041:検索処理中に割り込みの検索要求がありました。処理されません。
				errMessage.addMessage( 0,ErrorMessage.NG,"ERR0041" );	
				errCode = ErrorMessage.NG;
			}

			// 3.5.5.2 (2004/04/02) TaglibUtil.makeHTMLErrorTable メソッドを利用
			String err = TaglibUtil.makeHTMLErrorTable( errMessage,getResource() );
			if( err != null && err.length() > 0 ) {
				if( errCode >= ErrorMessage.NG ) {		// 異常の場合
					label = err;
				}
				setSessionAttribute( errMsgId,errMessage );
			}
			else {
				removeSessionAttribute( errMsgId );
			}
			// 4.3.3.0 (2008/09/22) 検索結果を、"DB.ERR_CODE" キーでリクエストにセットする。
			setRequestAttribute( "DB.ERR_CODE", String.valueOf( errCode ) );
			
			// 5.9.26.1 (2017/11/10) エラーメッセージをリクエスト変数で持つようにしておく
			setRequestAttribute( "DB.ERR_MSG", label );

//			// 5.10.2.2 (2018/08/24) 6.9.9.0
//			// 3.6.0.8 (2004/11/19) トランザクションチェックを行います。
//			// 4.0.0.0 (2007/11/29) 入れ子if の統合
//			if( table != null && ! commitTableObject( tableId, table ) ) {
//				jspPrint( "PlsqlUpdateTag Query処理が割り込まれました。DBTableModel は登録しません。" );
//				return (SKIP_PAGE);
//			}
		}

		// 5.9.26.1 (2017/11/10) dispErrorで表示をコントロール
		if( dispError ) {
			jspPrint( label );
		}

//		int rtnCode = EVAL_PAGE;
//		if( errCode >= ErrorMessage.NG )  { 	// 異常
//			rtnCode = SKIP_PAGE;
//		}
//		else {
//			rtnCode = EVAL_PAGE;
//		}

		// 4.0.0 (2005/01/31) 処理時間集計
		long dyTime = System.currentTimeMillis()-dyStart;

		// 4.0.0 (2005/01/31) セキュリティチェック(データアクセス件数登録)
		GUIInfo guiInfo = (GUIInfo) getSessionAttribute( HybsSystem.GUIINFO_KEY );
		executeCount = getParameterRows().length ; // 4.3.5.7 (2009/03/16) アクセス件数不具合対応。チェック行と仮定
		if( guiInfo != null ) { guiInfo.addWriteCount( executeCount,dyTime,sql ); }
		// 4.3.3.0 (2008/09/22) 属性 stopError の設定により、処理を中止するかを判断します。
		int rtnCode = ( ( errCode >= ErrorMessage.NG ) && ( stopError ) ) ? SKIP_PAGE : EVAL_PAGE;
		return( rtnCode );
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 2.0.0.4 (2002/09/27) カスタムタグの release() メソッドを、追加
	 * @og.rev 3.1.1.2 (2003/04/04) Tomcat4.1 対応。release2() を doEndTag()で呼ぶ。
	 * @og.rev 3.5.2.0 (2003/10/20) sysDBType 廃止。SYSARG は、システムパラメータ で定義します。
	 * @og.rev 5.5.5.2 (2012/08/10) isTableModelCommit追加
	 *
	 */
	@Override
	protected void release2() {
		super.release2();
		userDBType	= null;
		selectedAll	= false;
		isTableModelCommit = true; // 5.5.5.2 (2012/08/10)
	}

	/**
	 * Query を実行します。
	 *
	 * @og.rev 2.1.2.3 (2002/12/02) データベース更新時に、更新フラグをセットするように変更
	 * @og.rev 3.5.0.0 (2003/09/17) カラム名ではなく、カラム番号を先に求めておく方式に変更。
	 * @og.rev 3.5.2.0 (2003/10/20) 内部オブジェクトタイプ名を システムパラメータ で定義します。
	 * @og.rev 3.5.4.2 (2003/12/15) HTMLTableViewForm クラス名変更(⇒ ViewForm_HTMLTable)
	 * @og.rev 3.5.6.0 (2004/06/18) DBRowHeader のパッケージプライベート化に伴なう変更
	 * @og.rev 4.0.0.0 (2005/01/31) setArguments 廃止、Query#execute に、引数をすべて追加
	 * @og.rev 4.3.0.0 (2008/07/22) DBSysArgの引数に日付、PG、ユーザーIDを追加
	 * @og.rev 5.5.5.2 (2012/08/10) isTableModelCommitによるテーブルモデル確定処理のコントロール
	 *
	 * @param   query オブジェクト
	 */
	@Override
	protected void execute( final Query query ) {
		try {
			if( names == null ) {
				String errMsg = "names 属性が、設定されていません。" + HybsSystem.CR
							+ sql + HybsSystem.CR ;
				throw new HybsSystemException( errMsg );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
			}
			else {
				int[] rowNo = getParameterRows();
				int rowCount = rowNo.length ;
				if( rowCount > 0 ) {
					String[] nameArray = StringUtil.csv2Array( names );
					int[]    clmNo     = getTableColumnNo( nameArray );		// 3.5.0.0

					String curdate = HybsSystem.getDate( "yyyyMMddHHmmss" );	// 4.3.0.0
					String pgid = getGUIInfoAttri( "KEY" );						// 4.3.0.0
					String userid = getUser().getAttribute( "ID" );				// 4.3.0.0

					DBSysArg[]  sysArg  = new DBSysArg[rowCount];
					DBUserArg[] userArg = new DBUserArg[rowCount];
					for( int i=0; i<rowCount; i++ ) {
						int    row  = rowNo[i];
						String cdkh = table.getModifyType( row );
//						sysArg[i]  = new DBSysArg( SYSARG,row,cdkh );		// 3.5.2.0 // 4.3.0.0
						sysArg[i]  = new DBSysArg( SYSARG,row,cdkh,curdate,pgid,userid );
						String[] values = getTableModelData( clmNo,row );	// 3.5.0.0
						userArg[i] = new DBUserArg( userDBType,nameArray,values );
					}
					query.execute( names,userDBType + "_ARRAY",sysArg,userArg );
					errCode = query.getErrorCode();
					errMessage = query.getErrorMessage();

					if( errCode < ErrorMessage.NG ) {		// 異常以外の場合
						query.commit();
						if( isTableModelCommit ) { // 5.5.5.2 (2012/08/10))
							for( int j=rowCount-1; j>=0; j-- ) {
								int row = rowNo[j];
								if( DBTableModel.DELETE_TYPE.equals( table.getModifyType( row ) ) ) {
									table.removeValue( row );
								}
								else {
									table.resetModify( row );
								}
							}
						}
					}
					else {
						query.rollback();
					}
				}
			}
		}
		catch( HybsSystemException ex ) {
			query.rollback();
			throw ex;
		}
		finally {
			if( query != null ) { query.close(); }
		}
	}

	/**
	 *  カラム名配列(String[])より、対応するカラムNo配列(int[])を作成します。
	 *
	 * @og.rev 3.5.0.0 (2003/09/17) 新規追加
	 *
	 * @param	nameArray カラム名配列
	 *
	 * @return	カラムNo配列
	 */
	private int[] getTableColumnNo( final String[] nameArray ) {
		int[] clmNo = new int[ nameArray.length ];
		for( int i=0; i<clmNo.length; i++ ) {
			clmNo[i] = table.getColumnNo( nameArray[i] );
		}
		return clmNo;
	}

	/**
	 *  指定の行番号の、カラムNo配列(int[])に対応した値の配列を返します。
	 *
	 * 表示データの HybsSystem.ROW_SEL_KEY を元に、選ばれた 行を
	 * 処理の対象とします。
	 *
	 * @og.rev 3.5.0.0 (2003/09/17) カラム名ではなく、カラム番号を受け取るように修正。
	 *
	 * @param	clmNo カラムNo配列
	 * @param	row   行番号
	 *
	 * @return	行番号とカラムNo配列に対応した、値の配列
	 */
	private String[] getTableModelData( final int[] clmNo,final int row ) {
		String[] values = new String[ clmNo.length ];
		for( int i=0; i<values.length; i++ ) {
			values[i] = table.getValue( row,clmNo[i] ) ;
			// NUMBER タイプのキャストエラーを防ぐ為の対応
			if( values[i] != null && values[i].length() == 0 ) { values[i] = null; }
		}
		return values;
	}

	/**
	 * 表示データの HybsSystem.ROW_SEL_KEY を元に、選ばれた 行を処理の対象とします。
	 *
	 * @og.rev 4.0.0.0 (2005/01/31) getParameterRows() を使用するように変更
	 *
	 * @return	選択行の配列
	 */
	@Override
	protected int[] getParameterRows() {
		final int[] rowNo ;
		if( selectedAll ) {
			int rowCnt = table.getRowCount();		// 3.5.5.7 (2004/05/10)
			rowNo = new int[ rowCnt ];
			for( int i=0; i<rowCnt; i++ ) {
				rowNo[i] = i;
			}
		} else {
			rowNo = super.getParameterRows();		// 4.0.0 (2005/01/31)
		}
		return rowNo ;
	}

	/**
	 * 【TAG】Queryオブジェクトに渡す引数のタイプ定義(例:type名_ARRAY)。
	 *
	 * @og.tag
	 * ここでは、type 定義のPL/SQL名を指定します。
	 * 行を表す配列は、type名_ARRAY という名称です。
	 *
	 * @param  type 定義のPL/SQL名
	 */
	public void setDbType( final String type ) {
		userDBType = getRequestParameter( type );
	}

	/**
	 * 【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * 全てのデータを選択済みデータとして扱って処理します。
	 * 全件処理する場合に、(true/false)を指定します。
	 * 初期値は false です。
	 *
	 * @param  all データを全件選択済み [true:全件選択済み/false:通常]
	 */
	public void setSelectedAll( final String all ) {
		selectedAll = nval( getRequestParameter( all ),selectedAll );
	}

	/**
	 * 【TAG】Query を発行する為のクラスIDを指定します({@og.doc03Link queryType 初期値:JDBCPLSQL})。
	 *
	 * @og.tag
	 * 引数指定のINSERT/UPDATE文を実行する場合の、queryType 属性を使用します。
	 * このタグでは、execute( String ,String , DBSysArg[] , DBUserArg[] )を実行します。
	 * 代表的なクラスとして、"JDBCPLSQL" が標準で用意されています。
	 *
	 * タグにより使用できる/出来ないがありますが、これは、org.opengion.hayabusa.db
	 * 以下の Query_**** クラスの **** を与えます。
	 * これらは、Query インターフェースを継承したサブクラスです。
	 * {@og.doc03Link queryType Query_**** クラス}
	 *
	 * @og.rev 3.5.4.2 (2003/12/15) JavaDocコメント用にメソッド追加。
	 *
	 * @param	id Query を発行する為の実クラス ID
	 * @see		org.opengion.hayabusa.db.Query  Queryのサブクラス
	 * @see		org.opengion.hayabusa.db.Query#execute( String ,String , DBSysArg[] , DBUserArg[] )
	 */
	@Override
	public void setQueryType( final String id ) {
		super.setQueryType( nval( id,"JDBCPLSQL" ) );
	}
	
	/**
	 * 【TAG】テーブルモデルに対する確定処理を行うかどうかを指定します(初期値:true)。
	 *
	 * @og.tag
	 * PlsqlUpdateタグで、エラーがなかった場合は通常、テーブルモデルの改廃に従って処理が行われます。
	 * （改廃Dについては削除処理を行い、その他については改廃を元に戻す）
	 *
	 * このパラメータをfalseに指定すると、テーブルモデルに対する処理を行いません。
	 * これは、例えばPL/SQLでエラーチェックのみを行いたい場合に有効です。
	 * 初期値はtrue(処理を行う）です。
	 *
	 * @og.rev 5.5.5.2 (2012/08/10) 新規作成
	 *
	 * @param  flag テーブルモデルに対する処理を行うかどうか
	 */
	public void setTableModelCommit( final String flag ) {
		isTableModelCommit = nval( getRequestParameter( flag ),isTableModelCommit );
	}

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