/*
 * 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.plugin.query;

import org.opengion.hayabusa.db.AbstractQuery;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.util.ErrorMessage;
import org.opengion.fukurou.util.StringUtil;
// import org.opengion.fukurou.util.HybsDateUtil;		// 5.5.8.5 (2012/11/27)
import org.opengion.fukurou.model.Formatter;
import org.opengion.fukurou.db.DBUpdater;				// 6.9.3.0 (2018/03/26)
import org.opengion.fukurou.system.Closer;				// 7.3.0.0 (2021/01/06)

import java.sql.Connection;
import java.sql.PreparedStatement;
// import java.sql.ParameterMetaData;
import java.sql.SQLException;

/**
 * 引数引き当て(PreparedStatement) を利用した登録系Queryです。
 *
 * java.sql.PreparedStatement を用いて、データベース登録処理を行います。
 * 引数の指定方法は、DBTableModele のカラム名に対応する名称を、SQL文の[カラム名]形式で
 * 記述します。これを解析して、実際に実行する PreparedStatement に対応する文字列を
 * 作成します。
 * たとえば、INSERT INTO GEXX (CLM,NAME_JA,LABEL_NAME) VALUES ([CLM],[NAME_JA],[LABEL_NAME] )
 * と記述すれば、内部で、DBTableModele のカラム名に対応する値を取り出し、SQL文として、
 * INSERT INTO GEXX (CLM,NAME_JA,LABEL_NAME) VALUES (?,?,? ) を実行します。
 *
 * Query_JDBCTableUpdate との違いは、INSERT文とUPDATE文を渡しておき、
 * UPDATEの処理結果が、0件の場合は、INSERTを行います。
 * そのため、tableUpdateタグのBODY部に直接SQLを書くのではなく、tableUpdateParam ﾀｸﾞを2個書くことになります。
 *
 * 基本的に、tableUpdateタグのqueryTypeにJDBCTableUpdateと記述しておき、tableUpdateParam の
 * sqlType が MERGE の場合は、2種類のSQL文が作成され、自動的に、JDBCTableMerge が呼ばれます。
 * ※ つまり、通常は、queryType="JDBCTableUpdate" のままで、sqlType="MERGE" を指定すればよい。
 *
 * @og.formSample
 * ●使用例
 *
 *    ・JDBCTableUpdate のまま、sqlType="MERGE" を指定する場合。
 *    【entry.jsp】
 *        &lt;og:tableUpdate
 *            command   = "{&#064;command}"
 *            queryType = "JDBCTableUpdate"
 *            &lt;og:tableUpdateParam
 *                sqlType     = "MERGE"                // INSERT or UPDATE
 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
 *                names       = "{&#064;names}"         // 処理対象のカラム名
 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
 *                where       = "{&#064;where}"         // 処理対象を特定するキー（INSERT時には使われず、UPDAET時に使われる。）
 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
 *            /&gt;
 *        &lt;/og:tableUpdate&gt;
 *
 *    ・JDBCTableMerge を直接的に指定する場合。
 *    【entry.jsp】
 *        &lt;og:tableUpdate
 *            command   = "{&#064;command}"
 *            queryType = "JDBCTableMerge"
 *            &lt;og:tableUpdateParam
 *                sqlType     = "INSERT"                // INSERT or UPDATE
 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
 *                names       = "{&#064;names}"         // 処理対象のカラム名
 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
 *            /&gt;
 *            &lt;og:tableUpdateParam
 *                sqlType     = "UPDATE"                // INSERT or UPDATE
 *                table       = "{&#064;TABLE_NAME}"    // 処理対象のテーブル名
 *                names       = "{&#064;names}"         // 処理対象のカラム名
 *                omitNames   = "{&#064;omitNames}"     // 処理対象外のカラム名
 *                where       = "{&#064;where}"         // 処理対象を特定するキー
 *                constKeys   = "{&#064;constKeys}"     // 処理カラム名の中の固定情報カラム名
 *                constVals   = "{&#064;constVals}"     // 処理カラム名の中の固定情報設定値
 *            /&gt;
 *        &lt;/og:tableUpdate&gt;
 *
 * @og.rev 7.2.9.1 (2020/10/23) 新規作成
 * @og.group データ編集
 *
 * @version  7.2
 * @author   Kazuhiko Hasegawa
 * @since    JDK11.0,
 */
public class Query_JDBCTableMerge extends AbstractQuery {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "7.4.1.0 (2021/04/23)" ;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) 新規作成
	 */
	public Query_JDBCTableMerge() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * 引数配列付のクエリーを実行します。
	 * 処理自体は, #execute() と同様に、各サブクラスの実装に依存します。
	 * これは、PreparedQuery で使用する引数を配列でセットするものです。
	 * select * from emp where deptno = ? and job = ? などの PreparedQuery の
	 * [カラム名] 部分の引数を、DBTableModelから順番にセットしていきます。
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
	 * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 *
	 * @param   rowNo 選択された行番号配列(登録する対象行)
	 * @param   table DBTableModelオブジェクト(登録する元データ)
	 */
	@Override
	public void execute( final int[] rowNo, final DBTableModel table ) {

		int row = 0;			// エラー時に表示するエラー行番号
		int executeCount = 0;	// 処理件数

		// 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
//		final String[] sqls = getMergeStatement();		// UPDATE,INSERTの順番
		final String[] sqls = getMergeStatement();		// UPDATE,INSERT,SELECTの順番(UPDATE か SELECT はnull の可能性がある)

		// 6.9.8.0 (2018/05/28) エラー時に、わかりやすいように、引数を、パラメータ順にします。
		String[] vals    = null;
		boolean isUpdate = false;		// update/insert の区別が分かるように
		final Connection conn = getConnection();

		// 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
		// 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
		// ※ ﾄﾘｯｷｰ：updQuery は、selectの場合もあり得る。
//		final DBquery updQuery = new DBquery( table,sqls[0] );
		final DBquery updQuery = new DBquery( table,sqls[0] == null ? sqls[2] : sqls[0] );
		final DBquery insQuery = new DBquery( table,sqls[1] );

		// 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
		try {
			final boolean usePMeta = useParameterMetaData();

			final DBUpdater updDB = updQuery.getDBUpdater( conn,usePMeta );
			final DBUpdater insDB = insQuery.getDBUpdater( conn,usePMeta );

			// 5.5.5.4 (2012/08/18) Timestamp オブジェクトを登録する場合
			for( int i=0; i<rowNo.length; i++ ) {
				row = rowNo[i];
				vals = updQuery.getVals( row );

				isUpdate = true;
				int cnt = updDB.update( vals ) ;
				if( cnt == 0 ) {						// UPDATE を実行して、結果が 0 件なら
					vals = insQuery.getVals( row );
					isUpdate = false;
					cnt = insDB.update( vals );			// INSERT 処理を行う。
				}
				executeCount += cnt ;
			}

			setExecuteCount( executeCount );
			setErrorCode( ErrorMessage.OK );
		}
		catch( final SQLException ex) {		// catch は、close() されてから呼ばれます。
			setErrorCode( ErrorMessage.EXCEPTION );
			final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE )
				.append( ex.getMessage() ).append( ':' ).append( ex.getSQLState() ).append( CR )
				.append( isUpdate ? "UPDATE" : "INSERT" ).append( CR )
				.append( "  UPDATE=" ).append( sqls[0] ).append( CR )
				.append( "  INSERT=" ).append( sqls[1] ).append( CR )
				.append( "  ROW =["  ).append( (row+1) ).append( ']' ).append( CR )
				.append( "  VALS=["  ).append( StringUtil.array2csv( vals )).append( ']' )	// 6.9.8.0 (2018/05/28)
				.append( CR ) ;

			throw new HybsSystemException( errMsg.toString(),ex );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
		}
		// 7.3.0.0 (2021/01/06) SpotBugs:チェック例外でストリームやリソースのクリーンアップに失敗するかもしれないメソッド
		finally {
			updQuery.close();
			insQuery.close();
		}
	}

	/**
	 * DBUpdater で処理するにあたり、
	 * 処理自体は, #execute() と同様に、各サブクラスの実装に依存します。
	 * これは、PreparedQuery で使用する引数を配列でセットするものです。
	 * select * from emp where deptno = ? and job = ? などの PreparedQuery の
	 * [カラム名] 部分の引数を、DBTableModelから順番にセットしていきます。
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 */
	private static final class DBquery {
		private final DBTableModel	table;
		private final int[]			clmNos;
		private final String		query ;
		private final int			cnt   ;
		private final boolean[]		isTime;
		private final boolean		useTime;
		private final boolean		useSelect;		// 7.4.1.0 (2021/04/23)

		private PreparedStatement	pstmt;			// close用に持っておく

		/**
		 * DBTableModelとQUERY文を指定した、コンストラクター
		 *
		 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
		 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
		 *
		 * @param   table DBTableModelオブジェクト(登録する元データ)
		 * @param   sql 実行するQUERY文
		 */
		public DBquery( final DBTableModel table , final String sql ) {
			useSelect =sql.startsWith( "SELECT" );		// 7.4.1.0 (2021/04/23)

			this.table = table;
			final Formatter form = new Formatter( table,sql );
			clmNos = form.getClmNos();					// 引数の個数分の配列。カラム番号を保存
			query  = form.getQueryFormatString();
			cnt    = clmNos.length;						// 引数の個数(カラムの個数ではありません。)

			isTime = new boolean[cnt];

			boolean useTimeStamp = false;
			for( int j=0; j<cnt; j++ ) {
				isTime[j] = table.getDBColumn( clmNos[j] ).isDateType();	// 6.4.6.0 (2016/05/27)
				if( !useTimeStamp && isTime[j] ) { useTimeStamp = true; }	// isTime[j] == true 時に、一度だけ実行される。
			}
			useTime = useTimeStamp;
		}

		/**
		 * Connection から、DBUpdater を作成して返します。
		 *
		 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
		 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
		 *
		 * @param   conn Connectionオブジェクト
		 * @param   usePMeta Meta情報を使用する場合は、true
		 * @return  DBUpdaterｵﾌﾞｼﾞｪｸﾄ
		 */
		public DBUpdater getDBUpdater( final Connection conn,final boolean usePMeta ) throws SQLException {
			pstmt = conn.prepareStatement( query );
			pstmt.setQueryTimeout( DB_MAX_QUERY_TIMEOUT );

//			return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null );
			return new DBUpdater( cnt,pstmt,usePMeta,useTime ? isTime : null,useSelect );	// 7.4.1.0 (2021/04/23)
		}

		/**
		 * パラメータの個数を返します。
		 *
		 * @og.rev 7.3.0.0 (2021/01/06) PreparedStatement 関連の処理の見直し
		 *
		 * @return  パラメータの個数
		 */
		public void close() {
			Closer.stmtClose( pstmt );
		}

		/**
		 * 指定行のデータのうち、パラメータカラムの値の配列を返します。
		 *
		 * これは、[カラム]を? に置き換えた処理の、? の順番にDBTableModelの値を再設定します。
		 * 取り出した行データは、rTrim して、右側の空白文字を削除しています。
		 *
		 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
		 *
		 * @param   row 指定行
		 * @return  指定行のデータ配列
		 */
		public String[] getVals( final int row ) {
			final String[] vals = new String[cnt];
			final String[] data = table.getValues( row );
			for( int j=0; j<cnt; j++ ) {
				vals[j] = StringUtil.rTrim( data[ clmNos[j] ] );
			}
			return vals ;
		}
	}
}
