/*
 * 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.util.SystemParameter;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.HybsDateUtil;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.util.HybsEntry ;
import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.model.Formatter;
import org.opengion.fukurou.db.DBUtil ;
import org.opengion.fukurou.db.ConnectionFactory;

import java.io.File ;
import java.io.PrintWriter ;
import java.util.Map ;
import java.util.LinkedHashMap ;
import java.util.Calendar ;

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

/**
 * Process_DBFileout は、SELECT文 を指定し データベースの値を抜き出して、
 * 個々のファイルにセーブする、ChainProcess インターフェースの実装クラスです。
 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から
 * 受け取った LineModel を元に、１行単位に、SELECT文を実行します。
 *
 * 上流のカラムを、[カラム]変数で使用できます。
 * また、セーブするファイル名、更新日付等も、都度、更新可能です。
 *
 * データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に
 * 設定された接続(Connection)を使用します。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * SQL文には、{&#064;DATE.YMDH}等のシステム変数が使用できます。
 *
 * @og.formSample
 *  Process_DBFileout -dbid=DBGE -insertTable=GE41
 *
 *   [ -dbid=DB接続ID           ] ： -dbid=DBGE (例: Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定)
 *   [ -select=検索SQL文        ] ： -select="SELECT * FROM GE41 WHERE SYSTEM_ID = [SYSTEM_ID] AND CLM = [CLM]"
 *   [ -selectFile=登録SQLﾌｧｲﾙ  ] ： -selectFile=select.sql
 *                                ：   -select や -selectFile が指定されない場合は、エラーです。
 *   [ -select_XXXX=固定値      ] ： -select_SYSTEM_ID=GE
 *                                     SQL文中の{&#064;XXXX}文字列を指定の固定値で置き換えます。
 *                                     WHERE SYSTEM_ID='{&#064;SYSTEM_ID}' ⇒ WHERE SYSTEM_ID='GE'
 *   [ -const_XXXX=固定値       ] ： -const_FGJ=1
 *                                     LineModel のキー(const_ に続く文字列)の値に、固定値を設定します。
 *                                     キーが異なれば、複数のカラム名を指定できます。
 *   [ -addHeader=ヘッダー      ] ： -addHeader="CREATE OR REPLACE "
 *   [ -addFooter=フッター      ] ： -addFooter="/\nSHOW ERROR;"
 *   [ -outFile=出力ファイル名  ] ： -outFile=[NAME].sql
 *   [ -append=[false/true]     ] ： 出力ﾌｧｲﾙを、追記する(true)か新規作成する(false)か。
 *   [ -sep=セパレータ文字      ] ： 各カラムを区切る文字列(初期値:TAB)
 *   [ -useLineCR=[false/true]  ] ： 各行の最後に、改行文字をつかるかどうか(初期値:true[付ける])
 *   [ -timestamp=更新日付      ] ： -timestamp="LAST_DDL_TIME"
 *   [ -fetchSize=1000          ] ：フェッチする行数(初期値:1000)  6.9.4.1 (2018/04/09)
 *   [ -display=[false/true]    ] ： 結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [ -debug=[false/true]      ] ： デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_DBFileout extends AbstractProcess implements ChainProcess {
	private static final String SELECT_KEY	= "select_" ;
	private static final String CNST_KEY	= "const_" ;

	private static final String ENCODE = "UTF-8" ;

//	/** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ  {@value} */
//	private static final int DB_FETCH_SIZE = 1001 ;

	private Connection	connection		;
	private PreparedStatement selPstmt	;

	private String		dbid		;
	private String		select		;
	private int[]		selClmNos	;			// select 時のファイルのヘッダーのカラム番号
	private String		outFilename	;			// 出力ファイル名
	private boolean		append		;			// ファイル追加(true:追加/false:通常)
	private String		timestamp	;			// 出力ファイルの更新日時
	private int			tmstmpClm	= -1;		// 出力ファイルの更新日時のカラム番号
	private String		separator	= "\t";		// 各カラムを区切る文字列(初期値:TAB)
	private String		addHeader	;			// ヘッダー
	private String		addFooter	;			// フッター
	private boolean		useLineCR	= true;		// 各行の最後に、改行文字をつかるかどうか(初期値:true[付ける])
	private int			fetchSize	= 1000;		// 6.9.4.1 (2018/04/09) 初期値を 1000 に設定
	private boolean		display		;			// false:表示しない
	private boolean		debug		;			// 5.7.3.0 (2014/02/07) デバッグ情報

	private String[]	cnstClm		;			// 固定値を設定するカラム名
	private int[]		cnstClmNos	;			// 固定値を設定するカラム番号
	private String[]	constVal	;			// カラム番号に対応した固定値

	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( "select",	"検索SQL文(select or selectFile 必須)" +
									CR + "例: \"SELECT * FROM GE41 WHERE SYSTEM_ID = [SYSTEM_ID] AND CLM = [CLM]\"" );
		USABLE_PROPARTY.put( "selectFile",	"検索SQLファイル(select or selectFile 必須)例: select.sql" );
		USABLE_PROPARTY.put( "select_",		"SQL文中の{&#064;XXXX}文字列を指定の固定値で置き換えます。" +
									CR + "WHERE SYSTEM_ID='{&#064;SYSTEM_ID}' ⇒ WHERE SYSTEM_ID='GE'" );
		USABLE_PROPARTY.put( "const_",	"LineModel のキー(const_ に続く文字列)の値に、固定値を" +
									CR + "設定します。キーが異なれば、複数のカラム名を指定できます。" +
									CR + "例: -sql_SYSTEM_ID=GE" );
		USABLE_PROPARTY.put( "addHeader" ,	"ヘッダー" );
		USABLE_PROPARTY.put( "addFooter" ,	"フッター" );
		USABLE_PROPARTY.put( "outFile"	,	"出力ファイル名 例: [NAME].sql" );
		USABLE_PROPARTY.put( "append"	,	"出力ﾌｧｲﾙを、追記する(true)か新規作成する(false)か。" );
		USABLE_PROPARTY.put( "sep"		,	"各カラムを区切る文字列(初期値:TAB)" );
		USABLE_PROPARTY.put( "useLineCR",	"各行の最後に、改行文字をつかるかどうか(初期値:true[付ける])" );
		USABLE_PROPARTY.put( "timestamp",	"出力ファイルの更新日付例: [LAST_DDL_TIME]" );
		USABLE_PROPARTY.put( "fetchSize","フェッチする行数 (初期値:1000)" );					// 6.9.4.1 (2018/04/09) 初期値を 1000 に設定
		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_DBFileout() {
		super( "org.opengion.fukurou.process.Process_DBFileout",MUST_PROPARTY,USABLE_PROPARTY );
	}

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。
	 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		select		= arg.getFileProparty( "select","selectFile",false );
		separator	= arg.getProparty( "sep"		, separator		);
		outFilename	= arg.getProparty( "outFile"	, outFilename	);
		append		= arg.getProparty( "append"		, append		);
		addHeader	= arg.getProparty( "addHeader"	, addHeader		);
		addFooter	= arg.getProparty( "addFooter"	, addFooter		);
		useLineCR	= arg.getProparty( "useLineCR"	, useLineCR		);
		timestamp	= arg.getProparty( "timestamp"	, timestamp		);
		fetchSize	= arg.getProparty( "fetchSize"	, fetchSize		);		// 6.9.4.1 (2018/04/09) fetchSize 指定
		display		= arg.getProparty( "display"	, display		);
		debug		= arg.getProparty( "debug"		, debug			);

		addHeader = addHeader.replaceAll( "\\\\t" , "\t" ).replaceAll( "\\\\n" , "\n" );	// ｢\t｣と、｢\n｣の文字列を、タブと改行に変換します。
		addFooter = addFooter.replaceAll( "\\\\t" , "\t" ).replaceAll( "\\\\n" , "\n" );	// ｢\t｣と、｢\n｣の文字列を、タブと改行に変換します。

		dbid		= arg.getProparty( "dbid" );
		connection	= paramProcess.getConnection( dbid );

		if( select == null ) {
			final String errMsg = "select または、selectFile は必ず指定してください。";
			throw new OgRuntimeException( errMsg );
		}

		// 3.8.0.1 (2005/06/17) {@DATE.XXXX} 変換処理の追加
		// {@DATE.YMDH} などの文字列を、yyyyMMddHHmmss 型の日付に置き換えます。
		// SQL文の {@XXXX} 文字列の固定値への置き換え
		final HybsEntry[] entry	=arg.getEntrys(SELECT_KEY);				// 配列
		final SystemParameter sysParam = new SystemParameter( select );
		select = sysParam.replace( entry );

		final HybsEntry[] cnstKey = arg.getEntrys( CNST_KEY );		// 配列
		final int csize	= cnstKey.length;
		cnstClm		= new String[csize];
		constVal	= new String[csize];
		for( int i=0; i<csize; i++ ) {
			cnstClm[i]  = cnstKey[i].getKey();
			constVal[i] = cnstKey[i].getValue();
		}
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
	 *
	 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。
	 *
	 * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		final boolean flag1 = Closer.stmtClose( selPstmt );
		selPstmt = null;

		// close に失敗しているのに commit しても良いのか？
		if( isOK ) {
			Closer.commit( connection );
		}
		else {
			Closer.rollback( connection );
		}
		ConnectionFactory.remove( connection,dbid );

		if( ! flag1 ) {
			final String errMsg = "select ステートメントをクローズ出来ません。" + CR
								+ " select=[" + select + "] , commit=[" + isOK + "]" ;
			System.err.println( errMsg );
		}
	}

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。
	 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。
	 * @og.rev 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド(mkdirs)
	 *
	 * @param	data オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		count++ ;
		try {
			if( firstRow ) {
				makePrepareStatement( data );

				final int size   = cnstClm.length;
				cnstClmNos = new int[size];
				for( int i=0; i<size; i++ ) {
					cnstClmNos[i] = data.getColumnNo( cnstClm[i] );
				}

				if( display ) { println( data.nameLine() ); }		// 5.7.3.0 (2014/02/07) デバッグ情報

				if( timestamp != null ) {
					tmstmpClm = data.getColumnNo( timestamp );
				}
				firstRow = false;
			}

			// 固定値置き換え処理
			for( int j=0; j<cnstClmNos.length; j++ ) {
				data.setValue( cnstClmNos[j],constVal[j] );
			}

			if( selClmNos != null ) {
				for( int i=0; i<selClmNos.length; i++ ) {
					selPstmt.setObject( i+1,data.getValue(selClmNos[i]) );
				}
			}

			final Formatter fileFmt = new Formatter( data,outFilename );
			final File outFile = new File( fileFmt.getFormatString(0) );
			// 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド
//			if( !outFile.getParentFile().exists() ) {
//				outFile.getParentFile().mkdirs();
//			}
			final File parent = outFile.getParentFile();	// 親フォルダを取得。nullもありえる
			if( parent == null || !parent.exists() && !parent.mkdirs() ) {
				final String errMsg = "親フォルダを作成できませんでした。[" + data.getRowNo() + "]件目"	+ CR
									+ " outFile=[" + fileFmt.getFormatString(0) + "]"	+ CR ;
				throw new OgRuntimeException( errMsg );
			}

			final String[][] rtn ;
			try( final ResultSet resultSet = selPstmt.executeQuery() ) {
				rtn = DBUtil.resultToArray( resultSet,false );		// useHeader = false
			}

			// ０件の場合は、ヘッダーもフッターも出力しません。
			if( rtn.length > 0 ) {
				try( final PrintWriter writer = FileUtil.getPrintWriter( outFile,ENCODE,append ) ) {
					if( addHeader != null ) {
						final Formatter headerFmt = new Formatter( data,addHeader );
						final String header = headerFmt.getFormatString(0);
						writer.print( header );
					}
					for( int i=0; i<rtn.length; i++ ) {
						for( int j=0; j<rtn[i].length; j++ ) {
							writer.print( rtn[i][j] );
							writer.print( separator );
						}
						if( useLineCR ) { writer.println(); }
					}
					if( addFooter != null ) {
						final Formatter footerFmt = new Formatter( data,addFooter );
						final String footer = footerFmt.getFormatString(0);
						writer.print( footer );
					}
				}
			}

			if( tmstmpClm >= 0 ) {
				final String tmStmp = String.valueOf( data.getValue( tmstmpClm ) );
				final Calendar cal = HybsDateUtil.getCalendar( tmStmp );
				// 6.9.8.0 (2018/05/28) FindBugs:例外的戻り値を無視しているメソッド
//				outFile.setLastModified( cal.getTimeInMillis() );
				if( !outFile.setLastModified( cal.getTimeInMillis() ) ) {
					final String errMsg = "タイムスタンプの更新が出来ませんでした。[" + data.getRowNo() + "]件目"	+ CR
											+ " outFile= [" + outFile + "]" + CR ;
					System.err.println( errMsg );
				}
			}

			if( display ) { println( data.dataLine() ); }
		}
		catch( final SQLException ex) {
			final String errMsg = "検索処理でエラーが発生しました。[" + data.getRowNo() + "]件目"	+ CR
								+ " select=[" + select + "]"										+ CR
								+ " errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR
								+ " data=[" + data.dataLine() + "]" + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}
		return data;
	}

	/**
	 * 内部で使用する PreparedStatement を作成します。
	 * 引数指定の SQL または、LineModel から作成した SQL より構築します。
	 *
	 * @og.rev 6.4.8.3 (2016/07/15) 新規作成。
	 * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。
	 * @og.rev 6.9.4.1 (2018/04/09) fetchSize 指定を行います。
	 *
	 * @param	data	処理対象のLineModel
	 */
	private void makePrepareStatement( final LineModel data ) {

		final Formatter format = new Formatter( data,select );		// 6.4.3.4 (2016/03/11)
		select = format.getQueryFormatString();
		selClmNos = format.getClmNos();

		for( int i=0; i<selClmNos.length; i++ ) {
			// 指定のカラムが存在しない場合は、エラーにします。
			if( selClmNos[i] < 0 ) {
				final String errMsg = "フォーマットに対応したカラムが存在しません。" + CR
									+ "select=[" + select + "]" + CR
									+ "ClmKey=[" + format.getClmKeys()[i] + "]" + CR
									+ "nameLine=[" + data.nameLine() + "]" + CR
									+ "data=[" + data.dataLine() + "]" + CR ;
				throw new OgRuntimeException( errMsg );
			}
		}

		try {
			selPstmt = connection.prepareStatement( select );
//			selPstmt.setFetchSize( DB_FETCH_SIZE );				// 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ
			selPstmt.setFetchSize( fetchSize );					// 6.9.4.1 (2018/04/09) fetchSize 指定
		}
		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
								+ "select=[" + select + "]" + CR
								+ "nameLine=[" + data.nameLine() + "]" + CR
								+ "data=[" + data.dataLine() + "]" + CR ;
			throw new OgRuntimeException( errMsg,ex );
		}
	}

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

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( BUFFER_LARGE )
			.append( "Process_DBFileout は、SELECT文 を指定し データベースの値を抜き出して、"		).append( CR )
			.append( "個々のファイルにセーブする、ChainProcess インターフェースの実装クラスです。"	).append( CR )
			.append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"				).append( CR )
			.append( "受け取った LineModel を元に、１行単位に、SELECT文を実行します。"				).append( CR )
			.append( CR )
			.append( "上流のカラムを、[カラム]変数で使用できます。"									).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( "SQL文には、{&#064;DATE.YMDH}等のシステム変数が使用できます。"					).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_DBFileout().usage() );
	}
}
