/*
 * 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.fukurou.process;

import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.util.Argument;
import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.model.Formatter;
import org.opengion.fukurou.db.ConnectionFactory;

import java.util.Map ;
import java.util.LinkedHashMap ;

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

/**
 * Process_DBCountFilter は、データベースの存在件数でフィルタリングする
 * ChainProcess インターフェースの実装クラスです。
 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から受け取った
 * LineModel を元に、データベースの存在チェックを行い、下流への処理を振り分けます。
 * 具体的には、指定する SELECT 文は、必ず、『select count(*) from ･･･』形式にして下さい。
 * 検索カラムは、一つだけで、そこには数字が入ります。
 *
 * データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に
 * 設定された接続(Connection)を使用します。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  Process_DBCountFilter -dbid=DBGE -sql="select count(*) from GEA03"
 *
 *   [ -dbid=DB接続ID           ] ： -dbid=DBGE (例: Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定)
 *   [ -sql=検索SQL文           ] ： -sql="SELECT COUNT(*) FROM GEA03
 *                                         WHERE SYSTEM_ID = [SYSTEM_ID]
 *                                         AND CLM         = [CLM]
 *                                         AND FGJ         = '1'"
 *   [ -sqlFile=検索SQLファイル ] ： -sqlFile=select.sql
 *                                ：   -sql や -sqlFile が指定されない場合は、エラーです。
 *   [ -count=スルー条件        ] ： -count=[0|1|2] は、検索値に応じたスルー条件。
 *                                     0:０件時にスルー(処理を継続) つまり、なければ継続
 *                                     1:１件時にスルー(処理を継続) つまり、あれば継続
 *                                     2:２件以上ある場合にスルー   つまり、キー重複時に継続
 *   [ -display=[false/true]    ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [ -debug=[false/true]      ] ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_DBCountFilter extends AbstractProcess implements ChainProcess {
//	/** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ  {@value} */
//	private static final int DB_FETCH_SIZE = 1001 ;

	private Connection	connection	;
	private PreparedStatement pstmt	;
	private ParameterMetaData pMeta ;		// 5.1.1.0 (2009/11/11) setObject に、Type を渡す。(PostgreSQL対応)
	private boolean useParamMetaData;		// 5.1.1.0 (2009/11/11) setObject に、Type を渡す。(PostgreSQL対応)

	private String		dbid		;
	private String		sql			;
	private int			cntFlag		= -2;		// スルー条件 [0|1|2]
	private boolean		display		;			// false:表示しない
	private boolean		debug		;			// 5.7.3.0 (2014/02/07) デバッグ情報

	private int[]		clmNos		;			// ファイルのヘッダーのカラム番号
	private boolean		firstRow	= true;		// 最初の一行目
	private int			count		;

	/** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> MUST_PROPARTY   ;		// ［プロパティ］必須チェック用 Map
	/** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> USABLE_PROPARTY ;		// ［プロパティ］整合性チェック Map

	static {
		MUST_PROPARTY = new LinkedHashMap<>();

		USABLE_PROPARTY = new LinkedHashMap<>();
		USABLE_PROPARTY.put( "dbid",	"Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定" );
		USABLE_PROPARTY.put( "sql",			"カウントSQL文(sql or sqlFile 必須)" +
									CR + "例: \"SELECT COUNT(*) FROM GEA03 " +
									CR + "WHERE SYSTEM_ID = [SYSTEM_ID] " +
									CR + "AND CLM = [CLM] AND FGJ = '1'\"" );
		USABLE_PROPARTY.put( "sqlFile",		"検索SQLファイル(sql or sqlFile 必須)例: select.sql" );
		USABLE_PROPARTY.put( "count",	"[0|1|2] は、検索値に応じたスルー条件" +
									CR + "  0:０件時にスルー(処理を継続) つまり、なければ継続" +
									CR + "  1:１件時にスルー(処理を継続) つまり、あれば継続" +
									CR + "  2:２件以上ある場合にスルー   つまり、キー重複時に継続" );
		USABLE_PROPARTY.put( "display",	"結果を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );
		USABLE_PROPARTY.put( "debug",	"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );		// 5.7.3.0 (2014/02/07) デバッグ情報
	}

	/**
	 * デフォルトコンストラクター。
	 * このクラスは、動的作成されます。デフォルトコンストラクターで、
	 * super クラスに対して、必要な初期化を行っておきます。
	 *
	 */
	public Process_DBCountFilter() {
		super( "org.opengion.fukurou.process.Process_DBCountFilter",MUST_PROPARTY,USABLE_PROPARTY );
	}

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応)
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		sql			= arg.getFileProparty("sql","sqlFile",true);
		cntFlag		= arg.getProparty("count",cntFlag);
		display		= arg.getProparty("display",display);
		debug		= arg.getProparty("debug",debug);				// 5.7.3.0 (2014/02/07) デバッグ情報

		dbid		= arg.getProparty("dbid");
		connection	= paramProcess.getConnection( dbid );
		useParamMetaData = ConnectionFactory.useParameterMetaData( dbid );	// 5.3.8.0 (2011/08/01)
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
	 *
	 * @og.rev 4.0.0.0 (2007/11/27) commit,rollback,remove 処理を追加
	 * @og.rev 5.1.2.0 (2010/01/01) pMeta のクリア
	 *
	 * @param   isOK トータルで、OKだったかどうか [true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		final boolean flag = Closer.stmtClose( pstmt );
		pstmt = null;
		pMeta = null;		// 5.1.1.0 (2009/11/11)

		ConnectionFactory.remove( connection,dbid );

		if( !flag ) {
			final String errMsg = "ステートメントをクローズ出来ません。";
			throw new OgRuntimeException( errMsg );
		}
	}

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData  setNull 対応(PostgreSQL対応)
	 * @og.rev 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
	 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
	 *
	 * @param	data オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		LineModel rtnData = data;

		count++ ;
		try {
			if( firstRow ) {
				pstmt = makePrepareStatement( data );				// 最初に作成します。最後は、end( boolean ) メソッドで、close します。
				if( useParamMetaData ) {
					pMeta = pstmt.getParameterMetaData();
				}
				firstRow = false;
				if( display ) { println( data.nameLine() ); }		// 5.7.3.0 (2014/02/07) デバッグ情報
			}

			// 5.1.1.0 (2009/11/11) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
			if( useParamMetaData ) {
				for( int i=0; i<clmNos.length; i++ ) {
					final int type = pMeta.getParameterType( i+1 );
					// 5.3.8.0 (2011/08/01) setNull 対応
					final Object val = data.getValue(clmNos[i]);
					if( val == null || ( val instanceof String && ((String)val).isEmpty() ) ) {
						pstmt.setNull( i+1, type );
					}
					else {
						pstmt.setObject( i+1, val, type );
					}
				}
			}
			else {
				for( int i=0; i<clmNos.length; i++ ) {
					pstmt.setObject( i+1,data.getValue(clmNos[i]) );
				}
			}

			int cnt = -1;
			// 6.4.2.1 (2016/02/05) try-with-resources 文
			try( final ResultSet result = pstmt.executeQuery() ) {
				if( result.next() ) {				// １行目固定
					cnt = result.getInt( 1 );		// １カラム目固定
				}
			}

			if( ( cnt > 2  && cntFlag != 2 ) ||
				( cnt <= 2 && cnt != cntFlag ) ) {
					rtnData = null;		// 不一致
			}
			if( display ) { println( data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
		}
		catch( final SQLException ex) {
			// 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
			final String errMsg = "SQL を実行できませんでした。" + CR
						+ "errMsg=[" + ex.getMessage() + "]" + CR
						+ "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR
						+ "dbid=[" + dbid + "]" + CR
						+ "sql =[" + sql + "]" + CR
						+ "data=[" + data.dataLine() + "]" + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}
		return rtnData;
	}

	/**
	 * 内部で使用する PreparedStatement を作成します。
	 * 引数指定の SQL または、LineModel から作成した SQL より構築します。
	 *
	 * @og.rev 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
	 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
	 *
	 * @param	data 処理対象のLineModel
	 *
	 * @return  PreparedStatementオブジェクト
	 */
	private PreparedStatement makePrepareStatement( final LineModel data ) {

		// カラム番号は、makeFormat の処理で設定しています。
		final Formatter format = new Formatter( data,sql );		// 6.4.3.4 (2016/03/11)
		sql = format.getQueryFormatString();
		clmNos = format.getClmNos();

		final PreparedStatement ps ;
		try {
			ps = connection.prepareStatement( sql );
	//		ps.setFetchSize( DB_FETCH_SIZE );					// データ検索時のフェッチ不要。(1件づつしか取得しないため)
		}
		catch( final SQLException ex) {
			// 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
			final String errMsg = "PreparedStatement を取得できませんでした。" + CR
							+ "errMsg=[" + ex.getMessage() + "]" + CR
							+ "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR
							+ "dbid=[" + dbid + "]" + CR
							+ "sql =[" + sql + "]" + CR
							+ "data=[" + data.dataLine() + "]" + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}

		return ps;
	}

	/**
	 * プロセスの処理結果のレポート表現を返します。
	 * 処理プログラム名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のレポート
	 */
	public String report() {
		final String report = "[" + getClass().getName() + "]" + CR
							+ TAB + "DBID         : " + dbid + CR
							+ TAB + "Output Count : " + count ;

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( BUFFER_LARGE )
			.append( "Process_DBCountFilter は、データベースの存在件数でフィルタリングする"			).append( CR )
			.append( "ChainProcess インターフェースの実装クラスです。"								).append( CR )
			.append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"				).append( CR )
			.append( "受け取った LineModel を元に、データベースの存在チェックを行い、"				).append( CR )
			.append( "下流への処理を振り分けます。"													).append( CR )
			.append( "存在チェックで指定する SELECT 文は、必ず、『select count(*) from ･･･』"		).append( CR )
			.append( "形式にして下さい。検索カラムは、一つだけで、そこには数字が入ります。"			).append( CR )
			.append( CR )
			.append( "データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に"			).append( CR )
			.append( "設定された接続(Connection)を使用します。"										).append( CR )
			.append( CR )
			.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"	).append( CR )
			.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"		).append( CR )
			.append( "繋げてください。"																).append( CR )
			.append( CR ).append( CR )
			.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

	/**
	 * このクラスは、main メソッドから実行できません。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		LogWriter.log( new Process_DBCountFilter().usage() );
	}
}
