/*
 * 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.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.common.HybsOverflowException;		// 6.2.5.0 (2015/06/05)
import org.opengion.hayabusa.resource.GUIInfo;
import org.opengion.hayabusa.db.DBColumn;
import org.opengion.hayabusa.db.ColumnActionListener;			// 6.2.2.0 (2015/03/27)
import org.opengion.hayabusa.io.TableReader;
import org.opengion.fukurou.db.Transaction;
import org.opengion.fukurou.util.ErrorMessage;					// 6.2.5.0 (2015/06/05)
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.system.Closer ;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import org.opengion.fukurou.util.FileInfo;						// 6.2.3.0 (2015/05/01)
import org.opengion.fukurou.model.Formatter;
import org.opengion.fukurou.model.ArrayDataModel;

import static org.opengion.fukurou.util.StringUtil.nval ;
import static org.opengion.fukurou.system.HybsConst.BR;			// 6.1.0.0 (2014/12/26) refactoring

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

import java.io.File;

/**
 * 指定のファイルを直接データベースに登録するデータ入力タグです。
 *
 * 通常の readTable などは、DBTableModelオブジェクトを介して全件メモリに
 * ロードしてから表示させる為、大量データ処理ができません。
 * このタグでは、直接ファイルを読み取りながらデータベース登録するので
 * 大量データをバッチ的に登録する場合に使用します。
 *
 * 読み取るファイルは、先頭(または実データが現れるまでに) #NAME 行が必要です。
 * これは、ファイルデータのカラム名を指定しています。また、columns 属性を使用すれば、
 * ファイルの#NAME 行より優先して(つまり存在していなくても良い)データのカラム名を
 * 指定することが出来ます。
 * この#NAME 行は、ファイルのセパレータと無関係に必ずタブ区切りで用意されています。
 * タグのBODY部に、実行するSQL文を記述します。
 * このSQL文は、
 * INSERT INTO GE41 (CLM,NAME_JA,SYSTEM_ID,FGJ,DYSET)
 * VALUES ([CLM],[NAME_JA],[SYSTEM_ID],'1','{&#064;USER.YMDH}')
 * と、いう感じで、ファイルから読み込んだ値は、[カラム名]に割り当てられます。
 * もちろん、通常の固定値(FGJに'1'をセット)や、リクエスト変数(DYSETの{&#064;USER.YMDH})
 * なども使用できます。
 *
 * ※ 6.2.3.0 (2015/05/01)
 *    BODY部にSQL文を記述しない場合は、table 属性に、INSERTするテーブルIDを指定します。
 * ※ 6.2.4.0 (2015/05/15)
 *    omitNames に、WRITABLE と ROWID を、強制的に含めます(無条件)。
 *
 * ※ このタグは、Transaction タグの対象です。
 *
 * @og.formSample
 * ●形式：&lt;og:directTableInsert filename="[･･･]" ･･･ &gt;INSERT INTO ･･･ &lt;/og:directTableInsert &gt;
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を解析します)
 *
 * ●Tag定義：
 *   &lt;og:directTableInsert
 *       readerClass        【TAG】実際に読み出すクラス名の略称(TableReader_**** の ****)をセットします
 *                                  (初期値:TABLE_READER_DEFAULT_CLASS[={@og.value SystemData#TABLE_READER_DEFAULT_CLASS}])
 *  専   commitBatch        【TAG】指定数毎にコミットを発行します(初期値:0 終了までコミットしません)
 *  専   dbid               【TAG】(通常は使いません)検索時のDB接続IDを指定します(初期値:DEFAULT)
 *  専   table              【TAG】BODYのSQL文を指定しない場合に使用するテーブルIDを指定します
 *       command            【TAG】コマンド (NEW,RENEW)をセットします(初期値:NEW)
 *       fileURL            【TAG】読取元ディレクトリ名を指定します(初期値:FILE_URL)
 *       filename           【TAG】ファイルを作成するときのファイル名をセットします (初期値:FILE_FILENAME[=file.xls])
 *       encode             【TAG】ファイルを作成するときのファイルエンコーディング名をセットします(初期値:FILE_ENCODE)
 *       skipRowCount       【TAG】(通常は使いません)データの読み飛ばし件数を設定します
 *       maxRowCount        【TAG】読取時の最大取り込み件数をセットします (初期値:0:[無制限])
 *       errRowCount        【TAG】読取時の最大エラー件数をセットします (初期値:{@og.value ReadTableTag#ERROR_ROW_COUNT})(0:[無制限])
 *       separator          【TAG】可変長ファイルを作成するときの項目区切り文字をセットします
 *       columns            【TAG】読取元ファイルのカラム列を、外部(タグ)よりCSV形式で指定します
 *       omitNames          【TAG】読取対象外のカラム列を、外部(タグ)よりCSV形式で指定します
 *       modifyType         【TAG】ファイル取り込み時の モディファイタイプ(A(追加),C(更新),D(削除))を指定します
 *       displayMsg         【TAG】query の結果を画面上に表示するメッセージIDを指定します(初期値:VIEW_DISPLAY_MSG[={@og.value SystemData#VIEW_DISPLAY_MSG}]))
 *       overflowMsg        【TAG】読取データが最大検索数をオーバーした場合に表示するメッセージリソースIDを指定します (初期値:MSG0007[検索結果が、制限行数を超えましたので、残りはカットされました])
 *       notfoundMsg        【TAG】検索結果がゼロ件の場合に表示するメッセージリソースIDを指定します(初期値:MSG0077[対象データはありませんでした])
 *  ※   sheetName          【TAG】EXCELファイルを読み込むときのシート名を設定します(初期値:指定なし)
 *  ※   sheetNos           【TAG】EXCELファイルを読み込むときのシート番号を複数設定できます(初期値:0)
 *  ※   sheetConstKeys     【TAG】EXCELファイルを読み込むときの固定値となるカラム名(CSV形式)
 *  ※   sheetConstAdrs     【TAG】EXCELファイルを読み込むときの固定値となるアドレス(行-列,行-列,・・・)
 *       nullBreakClm       【TAG】カラム列に NULL が現れた時点で読取を中止します(複数Sheetの場合は、次のSheetを読みます)。
 *       nullSkipClm        【TAG】カラム列に NULL が現れたレコードは読み飛ばします。
 *       useNumber          【TAG】行番号情報を、使用している/していない[true/false]を指定します(初期値:true)
 *       useRenderer        【TAG】読取処理でKEY:VAL形式のコードリソースから、KEYを取り出す処理を行うかどうかを指定します(初期値:USE_TABLE_READER_RENDERER[=false])
 *       adjustColumns      【TAG】読取元ファイルのデータ変換を行うカラム列をカンマ指定します
 *       checkColumns       【TAG】読取元ファイルの整合性チェックを行うカラム列をカンマ指定します
 *       nullCheck          【TAG】NULL チェックすべきカラム列をCSV形式(CVS形式)で指定します
 *       matchKeys          【TAG】レコードの読取条件指定時のカラム列をCSV形式で指定します 6.4.6.0 (2016/05/27)
 *       matchVals          【TAG】レコードの読取条件指定時のカラム列に対応する正規表現データをCSV形式で指定します 6.4.6.0 (2016/05/27)
 *       language           【TAG】タグ内部で使用する言語コード[ja/en/zh/…]を指定します
 *       stopZero           【TAG】読込件数が０件のとき処理を続行するかどうか[true/false]を指定します(初期値:false[続行する])
 *       mainTrans          【TAG】(通常は使いません)タグで処理される処理がメインとなるトランザクション処理かどうかを指定します(初期値:false)
 *       tableId            【TAG】(通常は使いません)sessionから所得する DBTableModelオブジェクトの ID
 *       scope              【TAG】キャッシュする場合のスコープ[request/page/session/application]を指定します(初期値:session)
 *       useTimeView        【TAG】処理時間を表示する TimeView を表示するかどうかを指定します
 *										(初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
 *       caseKey            【TAG】このタグ自体を利用するかどうかの条件キーを指定します(初期値:null) 5.7.7.2 (2014/06/20)
 *       caseVal            【TAG】このタグ自体を利用するかどうかの条件値を指定します(初期値:null) 5.7.7.2 (2014/06/20)
 *       caseNN             【TAG】指定の値が、null/ゼロ文字列 でない場合(Not Null=NN)は、このタグは使用されます(初期値:判定しない) 5.7.7.2 (2014/06/20)
 *       caseNull           【TAG】指定の値が、null/ゼロ文字列 の場合は、このタグは使用されます(初期値:判定しない) 5.7.7.2 (2014/06/20)
 *       caseIf             【TAG】指定の値が、true/TRUE文字列の場合は、このタグは使用されます(初期値:判定しない)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   &gt;   ... Body ...
 *   &lt;/og:directTableInsert&gt;
 *
 * ●使用例
 *     &lt;og:directTableInsert
 *         dbid         = "ORCL"                接続データベースID(初期値:DEFAULT)
 *         separator    = ","                   ファイルの区切り文字(初期値:タブ)
 *         fileURL      = "{&#064;USER.ID}"     読み取り元ディレクトリ名
 *         filename     = "{&#064;filename}"    読み取り元ファイル名
 *         encode       = "Shift_JIS"           読み取り元ファイルエンコード名
 *         displayMsg   = "MSG0040"             登録完了後のメッセージ
 *         columns      = "CLM,NAME_JA,LABEL_NAME,KBSAKU,SYSTEM_ID,LANG"
 *                                              #NAME の代わりに使用するカラム列名
 *         commitBatch  = "100"                 この件数ずつコミットを発行(初期値:無制限)
 *         useColumnCheck  = "true"             カラムチェックを行うかどうか(初期値:false)
 *         useColumnAdjust = "true"             カラム変換を行うかどうか(初期値:false)
 *         nullCheck       = "CLM,SYSTEM_ID"    NULLチェックを実行します。
 *     &gt;
 *          INSERT INTO GE41
 *              (CLM,NAME_JA,LABEL_NAME,KBSAKU,SYSTEM_ID,LANG,
 *                 FGJ,DYSET,DYUPD,USRSET,USRUPD,PGUPD)
 *          VALUES
 *              ([CLM],[NAME_JA],[LABEL_NAME],[KBSAKU],[SYSTEM_ID],[LANG],
 *                '1','{&#064;USER.YMDH}','{&#064;USER.YMDH}','{&#064;USER.ID}','{&#064;USER.ID}','{&#064;GUI.KEY}')
 *     &lt;/og:directTableInsert &gt;
 *
 * @og.group ファイル入力
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class DirectTableInsertTag extends ReadTableTag {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.6.0.1 (2016/12/07)" ;
	private static final long serialVersionUID = 660120161207L ;

	// 6.2.4.0 (2015/05/15) 無条件でOMITする名称を指定します(WRITABLE,ROWID)
	private static final String DEFAULT_OMIT = "WRITABLE,ROWID" ;

	// 4.0.0.0 (2007/10/10) dbid の初期値を、"DEFAULT" から null に変更
	private String		dbid		;
	private int			commitBatch	;				// コミットするまとめ件数
	private String		sql			;
	private String		table		;				// 6.2.3.0 (2015/05/01)
	private long		dyStart		;				// 実行時間測定用のDIV要素を出力します。
	private boolean	useTimeView		= HybsSystem.sysBool( "VIEW_USE_TIMEBAR" );		// 6.3.6.0 (2015/08/16)

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public DirectTableInsertTag() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
	 * @og.rev 6.2.3.0 (2015/05/01) table属性追加。BODYのSQL文が無くても、table属性で自動生成します。
	 * @og.rev 6.2.4.0 (2015/05/15) 無条件でOMITする名称を指定します。
	 *
	 * @return	後続処理の指示( EVAL_BODY_BUFFERED )
	 */
	@Override
	public int doStartTag() {
		dyStart = System.currentTimeMillis();

		// 6.2.4.0 (2015/05/15) 無条件でOMITする名称を指定します。
		addOmitNames( DEFAULT_OMIT );

		// 6.2.3.0 (2015/05/01) table属性追加
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		return table == null || table.isEmpty()
					? EVAL_BODY_BUFFERED		// Body を評価する。( extends BodyTagSupport 時)
					: SKIP_BODY ;				// Body を評価しない

	}

	/**
	 * Taglibのタグ本体を処理する doAfterBody() を オーバーライドします。
	 *
	 * @og.rev 3.6.0.2 (2004/10/04) SQL文の [カラム] 対応とパーサー機能追加
	 * @og.rev 3.8.6.3 (2006/11/30) SQL 文の前後のスペースを取り除きます。
	 * @og.rev 6.2.3.0 (2015/05/01) table属性追加。BODYのSQL文が無くても、table属性で自動生成します。
	 *
	 * @return	後続処理の指示(SKIP_BODY)
	 */
	@Override
	public int doAfterBody() {
		sql = getBodyString();
		if( sql == null || sql.isEmpty() || sql.trim().isEmpty() ) {
			final String errMsg = "table属性を指定しない場合は、BODY 部の登録用 Insert/Update文は、必須です。";
			throw new HybsSystemException( errMsg );
		}

		return SKIP_BODY ;				// Body を評価しない
	}

	/**
	 * #doEndTag() の後続処理を記述します。
	 * 
	 * これは、サブクラスで、DBTableModel以外の処理を行う場合に、
	 * 処理内容を分けるために用意します。
	 *
	 * @og.rev 6.2.2.0 (2015/03/27) #afterEnd() メソッド 新規作成。
	 * @og.rev 6.2.5.0 (2015/06/05) AutoReaderの仕様変更。checkColumns エラー処理が抜けていたので、追加します。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	protected int afterEnd() {
		// 6.2.5.0 (2015/06/05) エラー処理の追加
		final ErrorMessage errMsg = clmAct.getErrorMessage();
		if( !errMsg.isOK() ) {
			jspPrint( TaglibUtil.makeHTMLErrorTable( errMsg,getResource() ) );
			return SKIP_PAGE ;
		}

		// 実行件数の表示
		// 4.0.0 (2005/11/30) 出力順の変更。一番最初に出力します。
		if( displayMsg != null && displayMsg.length() > 0 ) {
			final String status = executeCount + getResource().getLabel( displayMsg ) ;
			jspPrint( status + BR );
		}

		// 5.7.6.2 (2014/05/16) 検索結果の件数を、"DB.COUNT" キーでリクエストにセットする。
		setRequestAttribute( "DB.COUNT" , String.valueOf( executeCount ) );

		// 5.7.6.2 (2014/05/16) 件数０件かつ stopZero = true
		if( executeCount == 0 && stopZero ) { return SKIP_PAGE; }

		final long dyTime = System.currentTimeMillis()-dyStart;

		// 4.0.0 (2005/01/31) セキュリティチェック(データアクセス件数登録)
		final GUIInfo guiInfo = (GUIInfo)getSessionAttribute( HybsSystem.GUIINFO_KEY );
		if( guiInfo != null ) { guiInfo.addWriteCount( executeCount,dyTime,sql ); }

		if( useTimeView ) {		// 6.3.6.0 (2015/08/16)
			// 時間測定用の DIV 要素を出力
			jspPrint( "<div id=\"queryTime\" value=\"" + (dyTime) + "\"></div>" );	// 3.5.6.3 (2004/07/12)
		}
		return EVAL_PAGE ;
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 3.6.0.2 (2004/10/04) useColumnCheck,useColumnAdjust 属性追加
	 * @og.rev 3.8.0.2 (2005/06/30) nullCheck 属性追加
	 * @og.rev 4.0.0.0 (2007/10/10) dbid の初期値を、"DEFAULT" から null に変更
	 * @og.rev 5.5.7.1 (2012/10/05) skipRowCount追加
	 * @og.rev 5.7.6.2 (2014/05/16) stopZero属性追加
	 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
	 * @og.rev 6.2.3.0 (2015/05/01) table属性追加
	 */
	@Override
	protected void release2() {
		super.release2();
		dbid			= null;
		commitBatch		= 0;					// コミットするまとめ件数
		table			= null;					// 6.2.3.0 (2015/05/01) 
		useTimeView	= HybsSystem.sysBool( "VIEW_USE_TIMEBAR" );	// 6.3.6.0 (2015/08/16)
	}

	/**
	 * ファイルオブジェクト より読み込み、データベースに書き込みます。
	 *
	 * @og.rev 3.6.0.2 (2004/10/04) カラムオブジェクトのDBType属性の整合性チェック
	 * @og.rev 3.8.0.2 (2005/06/30) nullチェック確認
	 * @og.rev 3.8.5.1 (2006/05/08) 取込データが name 列より少ない場合の対応を追加
	 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定
	 * @og.rev 4.0.0.0 (2005/01/31) CheckColumnDataクラス static 化、引数にResourceManager追加
	 * @og.rev 4.0.0.1 (2007/12/03) try ～ catch ～ finally をきちんと行う。
	 * @og.rev 5.1.9.0 (2010/08/01) Transaction 対応
	 * @og.rev 5.2.2.0 (2010/11/01)) ""で囲われているデータに改行が入っていた場合の対応
	 * @og.rev 5.3.7.0 (2011/07/01) TransactionReal の引数変更
	 * @og.rev 5.3.8.0 (2011/08/01) pstmt.setObject で、useParamMetaData の判定を避けるため、pstmt.setString で代用(PostgreSQL対応)
	 * @og.rev 5.5.7.1 (2012/10/05) omitFirstLine対応
	 * @og.rev 5.7.0.3 (2013/11/22) BufferedReaderのclose処理をこのメソッド内のfinallyで行う
	 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
	 * @og.rev 6.2.3.0 (2015/05/01) 行読み飛ばし nullSkipClm追加
	 * @og.rev 6.2.4.2 (2015/05/29) executeCount の設定がおかしい。DirectTableInsertTagでは、初期値が -1 のため、件数が１件少なくなっていた。
	 * @og.rev 6.2.5.0 (2015/06/05) AutoReaderの仕様変更。checkColumns エラー処理が抜けていたので、追加します。
	 * @og.rev 6.3.6.1 (2015/08/28) Transaction でAutoCloseableを使用したtry-with-resources構築に対応。
	 * @og.rev 6.4.3.3 (2016/03/04) HybsSystem.newInstance(String,String) への置き換え。
	 * @og.rev 6.6.0.1 (2016/12/07) エンコードが複数ある場合、SQLのパースで、上書きするとカラム名が取れないバグ修正。
	 *
	 * @param   file ファイルオブジェクト
	 */
	@Override
	protected void create( final File file )  {

		// 6.3.6.1 (2015/08/28) Transaction で処理
		try( final Transaction tran = getTransaction() ) {
			/**
			 * ColumnActionListenerインターフェースの内部無名クラス
			 *
			 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
			 * @og.rev 6.3.6.1 (2015/08/28) Transaction で処理
			 *
			 * @param names カラム名配列
			 */
			final ColumnActionListener listener = new ColumnActionListener() {
				private DBColumn[]	dbClms ;
				private String[]	clmKeys		;				// SQL文の[カラム名]配列
				private int			commitCount ;

				private int[] clmNos 	;
				private int   clmNosLen ;

				private PreparedStatement pstmt  ;
				private final Connection conn = tran.getConnection( dbid );		// 6.3.6.1 (2015/08/28) 

				/**
				 * 一連の作業終了時に呼ばれます。
				 *
				 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
				 * @og.rev 6.3.6.1 (2015/08/28) Transaction で処理
				 *
				 */
				@Override
				public void end() {
					Closer.stmtClose( pstmt );
					tran.commit();					// 6.3.6.1 (2015/08/28) ここでは常にcommit()させる。
				}

				/**
				 * カラム名の配列が設定された場合に、呼び出されます。
				 *
				 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
				 * @og.rev 6.2.4.2 (2015/05/29) executeCount の設定がおかしい。DirectTableInsertTagでは、初期値が -1 のため、件数が１件少なくなっていた。
				 * @og.rev 6.3.6.1 (2015/08/28) Transaction で処理
				 * @og.rev 6.4.1.1 (2016/01/16) HybsOverflowException をthrow するとき、最大件数を引数に渡す。
				 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
				 * @og.rev 6.6.0.1 (2016/12/07) エンコードが複数ある場合、SQLのパースで、上書きするとカラム名が取れないバグ修正。
				 * @og.rev 6.9.5.0 (2018/04/23) カラム名が存在しない場合に、Exception を throw するかどうかを指定可能にする。
				 *
				 * @param names カラム名配列
				 */
				@Override
				public void columnNames( final String[] names ) {
					String pstSql = null;			// 6.6.0.1 (2016/12/07)

					final String[] nms = clmAct.makeNames( names );

					// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
					if( table == null || table.isEmpty() ) {
						if( sql != null && !sql.isEmpty() ) {
//							final ArrayDataModel nmdata = new ArrayDataModel( nms );
							final ArrayDataModel nmdata = new ArrayDataModel( nms,true );	// 6.9.5.0 (2018/04/23) カラム名が存在しない場合に、Exception を throw する
							final Formatter format = new Formatter( nmdata,sql.trim() );	// 6.4.3.4 (2016/03/11)
							clmNos		= format.getClmNos();								// 指定されたセルのカラム番号。存在しなければ、Exception を throw する
							clmNosLen	= clmNos.length ;
							clmKeys		= format.getClmKeys();
							pstSql		= format.getQueryFormatString();					// 6.6.0.1 (2016/12/07)
						}
					}
					else {
						if( "FILE.NAME".equals( table ) ) {
							table = FileInfo.getNAME( file );
						}

						clmNosLen = nms.length;
						clmNos  = new int[clmNosLen];
						clmKeys = nms;

						final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
						buf.append( "INSERT INTO " ).append( table ).append( " ( " )
							.append( String.join( ",",nms ) )
							.append( " ) VALUES ( ?" );
						clmNos[0] = 0;
						for( int i=1; i<clmNosLen; i++ ) {
							clmNos[i] = i;
							buf.append( ",?" );
						}
						buf.append( ')' );
						pstSql = buf.toString();			// 6.6.0.1 (2016/12/07)
					}

					try {
						pstmt = conn.prepareStatement( pstSql );			// 6.6.0.1 (2016/12/07)
					}
					catch( final SQLException ex ) {
						final String errMsg = CR + ex.getMessage()									+ CR
											+ " sql   =["		 + sql				 		+ "]"	+ CR
											+ " names =[" + StringUtil.array2csv( names )	+ "]"	+ CR		// 6.6.0.1 (2016/12/07)
											+ " encode=[" + encode							+ "]"	+ CR		// 6.6.0.1 (2016/12/07)
											+ " ErrorCode=[" + ex.getErrorCode()			+ "]"
											+ " State=["     + ex.getSQLState()				+ "]"	+ CR ;
						jspPrint( errMsg );
						tran.rollback();										// 6.3.6.1 (2015/08/28) Transaction で処理
				//		tran.close();											// 6.3.6.1 (2015/08/28) Transaction で処理
						sqlError = true;										// 6.3.6.1 (2015/08/28) DirectTableInsertTag でSQLException発生時
						throw new HybsSystemException( errMsg,ex );				// 6.2.4.2 (2015/05/29) refactoring
					}

					dbClms = new DBColumn[nms.length];
					for( int no=0; no<nms.length; no++ ) {
						dbClms[no] = getDBColumn( nms[no] );
					}
					// 6.2.4.2 (2015/05/29) executeCount の設定がおかしい。DirectTableInsertTagでは、初期値が -1 のため、件数が１件少なくなっていた。
					executeCount++ ;
				}

				/**
				 * １行分のデータが設定された場合に、呼び出されます。
				 *
				 * @og.rev 6.2.2.0 (2015/03/27) ColumnActionListener 対応。
				 * @og.rev 6.2.5.0 (2015/06/05) 読み取り件数オーバーフロー時、HybsOverflowException を throw します。
				 * @og.rev 6.3.6.1 (2015/08/28) Transaction で処理
				 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
				 * @og.rev 6.4.6.0 (2016/05/27) レコードの読取条件指定を追加。条件に一致しなければ、null が返されます。
				 *
				 * @param   vals    文字列値の１行分の配列
				 * @param   rowNo   行番号(0～)
				 */
				@Override
				public void values( final String[] vals, final int rowNo ) {
					if( maxRowCount <= 0 || executeCount <= maxRowCount ) {		// 読み取り件数無制限か、最大件数以下の場合
						// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
						// 6.4.1.1 (2016/01/16) PMD refactoring. 処理の順番を入れ替えます。
						if( pstmt == null || clmNos == null ) {
							final String errMsg = "#columnNames(String[])を先に実行しておいてください。"  ;
							throw new OgRuntimeException( errMsg );
						}

						try {
							final String[] newVals = clmAct.clmAction( vals , dbClms , rowNo );		// この処理結果が、getErrorMessage() に反映するので、移動不可

							// 6.2.5.0 (2015/06/05) エラー発生時は、以下の処理は行いません。
							// 6.4.6.0 (2016/05/27) レコードの読取条件指定を追加。条件に一致しなければ、null が返されます。
							if( newVals == null || !clmAct.getErrorMessage().isOK() || sqlError ) { return; }		// 6.3.6.1 (2015/08/28)

							for( int i=0; i<clmNosLen; i++ ) {
								pstmt.setString( i+1,newVals[clmNos[i]] );
							}
							pstmt.execute();
							if( commitBatch > 0 && ( executeCount%commitBatch == 0 ) ) {
								conn.commit();			// 5.1.9.0 (2010/08/01) Transaction 対応
								commitCount = executeCount;
							}
							executeCount++ ;
						}
						catch( final SQLException ex ) {
							final String errMsg = CR + ex.getMessage()							+ CR
										+ " sql   =[" + sql								+ "]"	+ CR
										+ " encode=[" + encode							+ "]"	+ CR		// 6.6.0.1 (2016/12/07)
										+ " keys  =[" + StringUtil.array2csv( clmKeys )	+ "]"	+ CR
										+ " vals  =[" + StringUtil.array2csv( vals )	+ "]"	+ CR
										+ " 行番号=["    + (rowNo+1)         + "]"
										+ " 登録件数=["  + commitCount       + "]"  + CR
										+ " ErrorCode=[" + ex.getErrorCode() + "]"
										+ " State=["     + ex.getSQLState()  + "]"  + CR ;
							jspPrint( errMsg );
							tran.rollback();								// 6.3.6.1 (2015/08/28) Transaction で処理
					//		tran.close();									// 6.3.6.1 (2015/08/28) Transaction で処理
							sqlError = true;								// 6.3.6.1 (2015/08/28) DirectTableInsertTag でSQLException発生時
							throw new HybsSystemException( errMsg,ex );
						}
					}
					else {
						throw new HybsOverflowException( maxRowCount );	// 6.4.1.1 (2016/01/16)
					}
				}
			};

			final TableReader reader = HybsSystem.newInstance( "TableReader_" , readerClass );	// 3.5.5.3 (2004/04/09)

			reader.setSeparator( separator );
			reader.setColumns( columns );					// 3.5.4.5 (2004/01/23) 、6.2.0.0 (2015/02/27) 削除
			reader.setUseNumber( useNumber );				// 3.7.0.5 (2005/04/11)
			reader.setSkipRowCount( skipRowCount );			// 5.1.6.0 (2010/05/01)
			reader.setDebug( isDebug() );					// 5.5.7.2 (2012/10/09) デバッグ情報を出力するかどうかを指定
			// 6.2.0.0 (2015/02/27) EXCELでない場合でも、メソッドは呼び出す。(空振りします)
			reader.setSheetName( sheetName );				// 3.5.4.2 (2003/12/15)
			reader.setSheetNos( sheetNos );					// 5.5.7.2 (2012/10/09) 複数シートを指定できるようにシート番号を指定できるようにする。
			reader.setSheetConstData( sheetConstKeys,sheetConstAdrs ) ;		// 5.5.8.2 (2012/11/09) 固定値となるカラム名、アドレスの指定
			reader.setNullBreakClm( nullBreakClm ) ;		// 5.5.8.2 (2012/11/09) 取込み条件/Sheet BREAK条件
			reader.setNullSkipClm( nullSkipClm ) ;			// 6.2.3.0 (2015/05/01) 行読み飛ばし
			// 6.6.0.1 (2016/12/07) setColumnActionListener は、内部処理が走るため、他の設定が終わってから呼び出す。
			reader.setColumnActionListener( listener );		// 6.2.2.0 (2015/03/27)

			reader.readDBTable( file,encode );				// 6.2.0.0 (2015/02/27) 追加

			listener.end();									// 6.3.6.1 (2015/08/28) tran.commit() は、この、end メソッドで行われる。
		}
	}

		// 6.3.6.1 (2015/08/28) Transaction で処理

	/**
	 * 【TAG】(通常は使いません)検索時のDB接続IDを指定します(初期値:DEFAULT)。
	 *
	 * @og.tag
	 *   検索時のDB接続IDを指定します。初期値は、DEFAULT です。
	 *
	 * @param	id データベース接続ID
	 */
	public void setDbid( final String id ) {
		dbid = nval( getRequestParameter( id ),dbid );
	}

	/**
	 * 【TAG】指定数毎にコミットを発行します(初期値:0 終了までコミットしません)。
	 *
	 * @og.tag
	 * 通常は、全ての処理が正常に終了するか、なにもしないか(トランザクション)
	 * を判断すべきで、途中でのコミットはしません。
	 * しかし、場合によって、件数が異常に多い場合や、再実行可能な場合は、
	 * 途中でコミットして、都度、処理できるものだけを処理してしまうという方法があります。
	 * また、ロールバックエリアの関係などで、データ量が多い場合に、処理時間が異常に
	 * 長くなる事があり、指定件数ごとのコミット機能を用意しています。
	 * 0 に設定すると、終了までコミットしません。初期値は、0 です。
	 *
	 * @param   cmtBat コミットを発行する行数 (初期値:0)
	 */
	public void setCommitBatch( final String cmtBat ) {
		commitBatch = nval( getRequestParameter( cmtBat ),commitBatch );
	}

	/**
	 * 【TAG】BODYのSQL文を指定しない場合に使用するテーブルIDを指定します。
	 *
	 * @og.tag
	 * 通常は、BODYに記述したSQL文を実行しますが、テーブルIDを指定すると、
	 * INSERT用のSQL文を自動作成します。
	 * その場合は、BODYのSQL文は設定不要です。
	 * また、FILE.NAME という文字列を指定した場合は、file1 に指定した
	 * ファイル名から、拡張子を取り除いた名称をテーブル名として使用します。
	 *
	 * @og.rev 6.2.3.0 (2015/05/01) table属性追加
	 *
	 * @param   tbl テーブルID
	 */
	public void setTable( final String tbl ) {
		table = nval( getRequestParameter( tbl ),table );
	}

	/**
	 * 【TAG】処理時間を表示する TimeView を表示するかどうか[true:する/false:しない]を指定します
	 *		(初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
	 *
	 * @og.tag
	 * true に設定すると、処理時間を表示するバーイメージが表示されます。
	 * これは、DB検索、APサーバー処理、画面表示の各処理時間をバーイメージで
	 * 表示させる機能です。処理時間の目安になります。
	 * (初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
	 *
	 * @og.rev 6.3.6.0 (2015/08/16) useTimeView の初期値を、VIEW_USE_TIMEBAR にする。
	 *
	 * @param	flag	処理時間を表示 [true:する/false:しない]
	 */
	public void setUseTimeView( final String flag ) {
		useTimeView = nval( getRequestParameter( flag ),useTimeView );
	}

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