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

import org.opengion.fukurou.util.AbstractObjectPool;
import org.opengion.fukurou.util.ApplicationInfo;
import org.opengion.fukurou.util.Closer;

import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import java.util.Properties;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.DriverManager;
import java.sql.DatabaseMetaData;

/**
 * データベースのコネクションオブジェクトを取得する為に使用する，ファクトリクラスです。
 *
 * Connection.connection() メソッドで，Connectionオブジェクトを取得します。
 * Connection#close() メソッドで，内部的に ConnectionFactory にオブジェクトを戻す
 * 事によって,Connectionオブジェクトのプーリングを行なっています。
 *
 * コネクションオブジェクトは，プールから貸し出します。
 * つまり，貸し出し中には,プールには，オブジェクトは残っていません。
 * その状態で,コネクションオブジェクトをclose()しない場合は,オブジェクトが破棄されて,
 * 貸し出し中カウントと実際のオブジェクト数が食い違い,リソースが不足します。
 * 必ず,作成したオブジェクトは,close()メソッドを呼び出して,プールに返して下さい。
 *
 * システムリソースの USE_DB_APPLICATION_INFO=true の場合、コネクションにアプリケーション
 * 情報を追記するため、ApplicationInfoオブジェクトを使用します。
 * このオブジェクトは、jsp/common/session-init.jsp にてユーザー情報とアプリケーション
 * 情報を画面アクセスごとに設定します。
 *
 * @og.group ＤＢ/Shell制御
 * @og.rev 4.0.0.0 (2007/10/16) パッケージ移動(hayabusa/db ⇒ fukurou/db)
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class ConnectionFactory {
	private static final Map<String,ConnectionPool> map = new HashMap<String,ConnectionPool>();

	// 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	// 4.0.0.0 (2007/10/29) 初期値をここでセットする
	private static  String  DBID = "DEFAULT";
	private static  ConnectionPool DEF_POOL ;

	// 4.0.0.0 (2007/10/17) システム依存の改行記号をセットします。
	private static final String CR = System.getProperty( "line.separator" );

	// 4.0.0.0 (2007/10/25) hayabusa依存を切るために追加します
	private static final int BUFFER_MIDDLE = 200;

	private static DatabaseConfig  dbc;

	/**
	 *  デフォルトコンストラクターをprivateにして、
	 *  オブジェクトの生成をさせないようにする。
	 *
	 */
	private ConnectionFactory() {
	}

	/**
	 * 初期化メソッドです。
	 * <pre>
	 * １)第二引数にXMLファイルをクラスローダ基底からの相対パスで指定した場合は
	 * 　　そのXMLを利用してDBConfigオブジェクトを作成します。例：ConnectionFactory.init( CONTEXT_NAME, "../DBConfig.xml")
	 * 　　nullの場合はWEB-INF/DBConfig.xmlを利用します。例：ConnectionFactory.init( CONTEXT_NAME, null)
	 * ２)キャッシュ初期ConnectionPoolのキーを設定してキャッシュプールを作ります。
	 * 　　この値がnullの場合は"DEFAULT"が設定されます。
	 * </pre>
	 * 
	 * <strong>このクラスを利用する場合は必ず最初にこのメソッドを実行する必要があります。</strong>
	 * キャッシュとDBConfigオブジェクトの同期化はされていないので初期化以外での利用は避けて下さい。
	 *
	 * @og.rev 4.0.0.0 (2007/11/05) 新規作成
	 *
	 * @param defPoolKey  初期DBID名(nullの場合は、"DEFAULT")
	 * @param xmlFileName DBConfig.xmlファイルのファイル名(nullの場合は、WEB-INF/DBConfig.xml)
	 */
	public static void init( final String defPoolKey, final String xmlFileName ) {
		// DBConfigオブジェクトの作成
		if( xmlFileName == null || xmlFileName.length() == 0 ) {
			dbc = new DatabaseConfig();
		}
		else {
			dbc = new DatabaseConfig( xmlFileName );
		}

		if( defPoolKey == null || defPoolKey.length() == 0 || dbc.getDbid( defPoolKey ) == null ) {
			DBID = "DEFAULT";
		}
		else {
			DBID = defPoolKey;
		}
		EDbid edbid = dbc.getDbid( DBID );
		if( edbid == null ) {
			final String errMsg = "初期化時に、指定のDBIDキーが存在しません。"
				+ "[Key ="
				+ DBID
				+ "]";
			throw new RuntimeException( errMsg );
		}

		DEF_POOL = new ConnectionPool( edbid );
	}

	/**
	 * コネクションオブジェクトを取得します。
	 * 遅い初期化を行なう事により,実際に必要となるまでコネクションオブジェクトは
	 * 作成しません。
	 * 最大プール数に達して,なおかつ,すべてのConnectionが貸し出し中の場合,
	 *
	 * @og.rev 2.1.1.3 (2002/11/22) コネクションID が null の場合に DEFAULT から所得するように変更。
	 * @og.rev 3.1.0.0 (2003/03/20) Hashtable を使用している箇所で、非同期でも構わない箇所を、HashMap に置換え。
	 * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。
	 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定
	 * @og.rev 3.8.8.2 (2007/01/26) USE_DB_APPLICATION_INFO ⇒ pool.useApplicationInfo() 変更
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 * @og.rev 4.1.0.1 (2008/01/21) 登録時に、大文字に変換する。
	 *
	 * @param   dbid 接続先ID
	 * @param   appInfo アプリ情報オブジェクト
	 *
	 * @return  コネクションオブジェクト
	 */
	public static Connection connection( final String dbid , final ApplicationInfo appInfo ) {
		ConnectionPool pool ;
		if( dbid == null || dbid.length() == 0 || DBID.equalsIgnoreCase( dbid ) ) {
			pool = DEF_POOL ;
		}
		else {
			String udbid = dbid.toUpperCase( Locale.JAPAN );	// 大文字化
			synchronized( map ) {
				pool = map.get( udbid );
				// 接続IDが、map に存在しない場合
				if( pool == null ) {
					EDbid edbid = dbc.getDbid( udbid );
					if( edbid == null ) {
						final String errMsg = "指定のDBIDキーが存在しません。"
							+ "[Key ="
							+ udbid
							+ "]";
						throw new RuntimeException( errMsg );
					}
					pool = new ConnectionPool( edbid );
					map.put( udbid,pool );
				}
			}
		}

		Connection conn = pool.newInstance();

		// 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを使用
		// 3.8.8.2 (2007/01/26) ORACLE 以外は、使用しません。
		// 4.0.0.0 (2007/11/29) 入れ子if の統合
		if( appInfo != null && pool.useApplicationInfo() ) {
			appInfo.callAppInfo( conn );
		}
		return conn;
	}

	/**
	 * コネクションオブジェクトをプールに戻します。
	 * Connectionオブジェクトは,close()メソッドで,自分自身を ConnectionFactory の
	 * プールに戻します。
	 * それ以外の コネクションオブジェクトをプールに戻す場合は,このメソッドを使用します。
	 *
	 * @og.rev 2.1.1.3 (2002/11/22) コネクションID が null の場合に DEFAULT から所得するように変更。
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 * @og.rev 4.1.0.1 (2008/01/21) 登録時に、大文字に変換する。
	 *
	 * @param   conn コネクションオブジェクト
	 * @param   dbid 接続先ID
	 */
	public static void close( final Connection conn,final String dbid ) {
		if( conn != null ) {
			if( dbid == null || dbid.length() == 0 || DBID.equalsIgnoreCase( dbid ) ) {
				DEF_POOL.release( conn ) ;
			}
			else {
				String udbid = dbid.toUpperCase( Locale.JAPAN );	// 大文字化
				synchronized( map ) {
					ConnectionPool pool = map.get( udbid );
					if( pool != null ) {
						pool.release( conn );
					}
				}
			}
		}
	}

	/**
	 * コネクションオブジェクトを物理的に削除(クローズ)戻します。
	 * これは、コネクション等がエラーを起こした場合に、プールに戻すのではなく、
	 * 接続を閉じる場合に、使用されます。
	 *
	 * @og.rev 2.1.1.3 (2002/11/22) コネクションID が null の場合に DEFAULT から所得するように変更。
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 * @og.rev 4.1.0.1 (2008/01/21) 登録時に、大文字に変換する。
	 *
	 * @param   conn コネクションオブジェクト
	 * @param   dbid 接続先ID
	 */
	public static void remove( final Connection conn,final String dbid ) {
		if( conn != null ) {
			if( dbid == null || dbid.length() == 0 || DBID.equalsIgnoreCase( dbid ) ) {
				DEF_POOL.remove( conn ) ;
			}
			else {
				String udbid = dbid.toUpperCase( Locale.JAPAN );	// 大文字化
				synchronized( map ) {
					ConnectionPool pool = map.get( udbid );
					if( pool != null ) {
						pool.remove( conn );
		//				map.put( udbid,pool );
					}
				}
			}
		}
	}

	/**
	 * コネクションオブジェクトを実際にすべてクローズします。
	 * コネクションプールの再編成や，管理者による強制クローズに使用します。
	 *
	 * クローズに失敗(コネクションが貸し出し中)の場合は,内部的に
	 * DB_CLOSE_RETRY_TIME だけ待機して, DB_CLOSE_RETRY_COUNT 回数だけ,試行します。
	 * それでもクローズできない場合は, RuntimeException を throw します。
	 *
	 * @og.rev 4.0.0.0 (2005/01/31) ロジック見直し。 pool.clear() で、基本的にはすべて削除されます。
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 */
	public static void realClose() {
		synchronized( DEF_POOL ) {
			if( ! DEF_POOL.isEmpty() ) {
				DEF_POOL.clear();
			}
		}

		final ConnectionPool[] pools ;
		synchronized( map ) {
			if( map.isEmpty() ) { return; }

			pools = map.values().toArray( new ConnectionPool[map.size()] ) ;
			map.clear();
		}

		ConnectionPool pool ;
		for( int i=0; i<pools.length ; i++ ) {
			pool = pools[i];
			if( pool != null && ! pool.isEmpty() ) {
				pool.clear();
			}
		}
	}

	/**
	 * ConnectionFactory の現在の状況(詳細メッセージ)を返します。
	 * これは，コネクションプールの数(最大値，作成済み数など)を確認する為のものです。
	 *
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 *
	 * @return  現在の状態表示
	 */
	public static String information() {
		return information( true );
	}

	/**
	 * ConnectionFactory の現在の状況を返します。
	 * これは，コネクションプールの数(最大値，作成済み数など)を確認する為のものです。
	 * 引数により詳細メッセージかどうかを指定できます。
	 *
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 * @og.rev 5.3.4.0 (2011/04/01) 詳細メッセージ用引数を追加
	 * @og.rev 5.6.7.3 (2013/08/23) 若干の修正
	 *
	 * @param	isDetail	詳細メッセージかどうか [true:詳細メッセージ/false:簡易メッセージ]
	 *
	 * @return  現在の状態表示
	 */
	public static String information(final boolean isDetail ) {
		// 4.0.0.0 (2007/10/25) hybsとの依存関係を弱めるため。
		final StringBuilder strBuf = new StringBuilder( BUFFER_MIDDLE );

		strBuf.append( "<b>【Connection Information】</b>" ).append( CR );	// 5.6.7.3 (2013/08/23) 若干の修正

		int rtnCnt = 0;
		synchronized( DEF_POOL ) {
			if( ! DEF_POOL.isEmpty() ) {
				rtnCnt += DEF_POOL.size();
	 			// 5.3.4.0 (2011/04/01) 詳細メッセージ用引数を追加
				if( isDetail ) {
					strBuf.append( DEF_POOL.toString() );
					strBuf.append( "<br /><hr />" );
				}
				else {
					strBuf.append( DEF_POOL.dbidInfo() );
				}
			}
		}

		ConnectionPool[] pools = null;
		synchronized( map ) {
			if( !map.isEmpty() ) {
				pools = map.values().toArray( new ConnectionPool[map.size()] ) ;
			}
		}

		if( pools != null ) {
			for( int i=0; i<pools.length ; i++ ) {
				ConnectionPool pool = pools[i];
				if( pool != null && ! pool.isEmpty() ) {
					rtnCnt += pool.size();
		 			// 5.3.4.0 (2011/04/01) 詳細メッセージ用引数を追加
					if( isDetail ) {
						strBuf.append( pool.toString() );
						strBuf.append( "<br /><hr />" );
					}
					else {
						strBuf.append( pool.dbidInfo() );
					}
				}
			}
		}

		strBuf.append( CR );

		return strBuf.toString();
	}

	/**
	 * この接続が、PreparedStatement#getParameterMetaData() を使用するかどうかを判定します。
	 *
	 * PreparedStatement に対して、String化された 数字などを setObject( int,String ) するとき、
	 * ORACLE と SQLServer は、そのまま設定すれば、自動的に変換されます。
	 * postgreSQL では、ParameterMetaData#getParameterType(int) で、カラムタイプを取得し、
	 * setObject( int,String,int ) する必要があります。
	 * その判定に、このメソッドを使用します。
	 * この結果は、あくまで、各種データベース毎の実地調査の結果を元に、判定結果を
	 * 返すようにしています。
	 * ORACLE の場合は、使用しない(false)が返るように設定しています。
	 * SQLServer では、ORACLEと同様に、false を返します。
	 *
	 * このメソッドは、元々、ApplicationInfo#useParameterMetaData(Connection) に有ったものを
	 * EDbid から取得するように修正したものです。
	 *
	 * @og.rev 5.3.8.0 (2011/08/01) 新規追加
	 *
	 * @param dbid 接続先ID
	 *
	 * @return	[true:使用する/false:その他]
	 */
	public static boolean useParameterMetaData( final String dbid ) {
		final String udbid ;
		if( dbid == null || dbid.length() == 0 ) {
			udbid = DBID ;
		}
		else {
			udbid = dbid.toUpperCase( Locale.JAPAN );	// 大文字化
		}

		EDbid edbid = dbc.getDbid( udbid );

		return edbid.useParamMetaData();
	}

	/**
	 * 接続先のDB名に対応した、enum (DBName) を返します(toUpperCase)。
	 *
	 * @og.rev 5.1.4.0 (2010/03/01) getDBFullName の代わりに新規作成
	 * @og.rev 5.7.7.2 (2014/06/20) 最初の取得時のエラー回避
	 *
	 * @param dbid 接続先ID
	 *
	 * @return  接続先のDB名
	 */
	public static String getDBName( final String dbid ) {
		final String dbName;

		if( dbid == null || dbid.length() == 0 || DBID.equalsIgnoreCase( dbid ) ) {
			dbName = DEF_POOL.getDBName();
		}
		else {
			String udbid = dbid.toUpperCase( Locale.JAPAN );	// 大文字化
			ConnectionPool pool = null;
			synchronized( map ) {
				pool = map.get( udbid );
				if( pool == null ) {
//					close( connection( dbid, null ), dbid );	// 5.7.7.2 (2014/06/20) 最初の取得時のエラー回避
					connection( dbid, null );	// ダミーで、コネクトする。
					pool = map.get( udbid );	// もう一度、設定する。
				}
			}
			if( pool != null ) {
				dbName = pool.getDBName();
			}
			else {
				final String errMsg = "指定のDBIDキーに対応するデータベース名を取得出来ません。"
					+ "[Key =" + dbid + "]";
				throw new RuntimeException( errMsg );
			}
		}

		return dbName.toUpperCase( Locale.JAPAN );
	}
}

/**
 * ConnectionPool は、AbstractObjectPool を継承した オブジェクトプールです。
 *
 * コネクションオブジェクトをプールすることにより、ConnectionFactory で
 * 管理する Map オブジェクトの実態として、各ID毎の コネクションをキープします。
 *
 * @og.group ＤＢ/Shell制御
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class ConnectionPool extends AbstractObjectPool<Connection> {
	private final transient EDbid edbid;

	// 4.0.0.0 (2007/10/17) システム依存の改行記号をセットします。
	private static final String CR = System.getProperty( "line.separator" );

	/**
	 *  DBID を指定して作成する コンストラクター
	 *  DBID をキーに､ HybsSystem.sys メソッドのデータベース変数を取得します。
	 *  取得するのは､ DBID + _DB_URL／_DB_USER／_DB_PASSWD／_DB_MINCOUNT／_DB_MAXCOUNT
	 *  です。
	 *  DBID が null の場合は，"DEFAULT" が使用されます。
	 *
	 * @og.rev 3.5.4.3 (2004/01/05) キャッシュの寿命を指定
	 * @og.rev 3.5.4.7 (2004/02/06) DBID のゼロストリングチェック追加
	 * @og.rev 4.0.0.0 (2007/10/10) キャッシュされた、初期ConnectionPool を使用
	 * @og.rev 4.0.0.0 (2007/10/25) DB設定情報のXML化に伴う変更
	 *
	 * @param   edbid 接続先情報オブジェクト
	 */
	public ConnectionPool( final EDbid edbid ) {
		// 4.0.0.0 XML化に伴いロード先を変更
		this.edbid	=	edbid;
		init( edbid.getMincount(),edbid.getMaxcount(),true,edbid.getPooltime() );
	}

	/**
	 * オブジェクトプールから削除するときに呼ばれます。
	 * このメソッドで各オブジェクトごとの終了処理を行います。
	 * 例えば､データベースコネクションであれば､close() 処理などです。
	 *
	 * @og.rev 3.5.4.8 (2004/02/23) SQLException は無視します。
	 * @og.rev 3.5.6.0 (2004/06/18) synchronized を解除します。
	 *
	 * @param  obj 終了処理を行うオブジェクト
	 */
	protected void objectFinal( final Connection obj ) {
		Closer.connClose( obj );
	}

	/**
	 * コネクションオブジェクトを作成します。
	 * DriverManager.getConnection により作成されたConnection を Connection で
	 * ラッピングします。
	 * Connectionオブジェクトは,close()メソッドで,自分自身を ConnectionFactory の
	 * プールに戻します。
	 *
	 * @og.rev 3.3.3.3 (2003/08/06) コネクションに対して、setTransactionIsolation を、設定しておく。
	 * @og.rev 3.5.2.0 (2003/10/20) 接続情報に、データベース名、ドライバ名情報を追加する。
	 * @og.rev 3.5.6.0 (2004/06/18) synchronized を解除します。
	 * @og.rev 3.8.8.2 (2007/01/26) useAppInfo を設定します。
	 * @og.rev 4.0.0.0 (2007/10/30) 保持情報オブジェクト化に伴う変更
	 * @og.rev 5.1.2.0 (2010/01/01) MySQL対応 明示的に、TRANSACTION_READ_COMMITTED を指定する。
	 * @og.rev 5.5.2.0 (2012/05/01) properties対応
	 *
	 * @return  コネクションオブジェクト
	 */
	protected Connection createInstance() {
		Connection conn = null;
		try {
	//		DriverManager.setLogWriter( HybsSystem.out );			// ドライバーのログ

			// 5.5.2.0 (2012/05/01) propertyの追加処理と、接続のproperties化
			Properties prop = new Properties (edbid.getProps());
			prop.put ( "user", edbid.getUser() );
			prop.put ( "password", edbid.getPassword() );

			conn = DriverManager.getConnection( edbid.getUrl(), prop );
	//		conn.setReadOnly( true );
			conn.setReadOnly( edbid.isReadonly() );

			conn.setAutoCommit( false );
			conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);  // 初期値
	//		((OracleConnection)conn).setDefaultExecuteBatch(1);  // 初期値
	//		((OracleConnection)conn).setDefaultRowPrefetch(20);  // 初期値

			// 3.5.2.0 (2003/10/20) 接続情報に、データベース名、ドライバ名情報を追加する。
			// 4.0.0.0 (2007/10/26) 登録先をオブジェクト化
			if( edbid.getDbProductName() == null ) {
				DatabaseMetaData meta = conn.getMetaData();
				edbid.setMetaDataInfo( meta );
			}
			return conn ;
		}
		catch ( SQLException ex ) {
			String errMsg = "コネクトすることが出来ません。" + CR
						+ "DBID=[" + edbid.getDbidKey() + "]" + CR
						+ ex.getMessage() + " , status=" + ex.getSQLState();
			Closer.connClose( conn );
			throw new RuntimeException( errMsg,ex );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
		}
	}

	/**
	 * アクセスログ取得の為のDBMS_APPLICATION_INFOの使用可否を取得します(初期値:true)。
	 *
	 * データベースへのアクセスログ取得の為、エンジンで接続するコネクションについて
	 * DBMS_APPLICATION_INFO.SET_CLIENT_INFO と SET_MODULE を呼び出しています。
	 * この処理は、ORACLEとの接続のみ有効ですので、接続先データベースが ORACLE 以外は
	 * false を返します。
	 * ORACLE の場合は、システムリソースの USE_DB_APPLICATION_INFO 属性の設定値を
	 * 返します。
	 * この設定値の初期値は、true です。
	 *
	 * @og.rev 3.8.8.2 (2007/01/26) 新規追加
	 *
	 * @return  true:使用する/false:使用しない
	 */
	public boolean useApplicationInfo() { return edbid.isApplicationInfo(); }

	/**
	 * 接続先のDB名を返します。
	 *
	 * @og.rev 4.3.7.0 (2009/06/01) 新規作成
	 *
	 * @return  接続先のDB名
	 */
	public String getDBName() {
		return edbid.getDbProductName();
	}

	/**
	 * 接続先のDBバージョンを返します。
	 *
	 * @og.rev 4.3.7.0 (2009/06/01) 新規作成
	 *
	 * @return 接続先のDBバージョン
	 */
	public String getDBVersion() {
		return edbid.getDbProductVersion();
	}

	/**
	 * 接続先の簡易な内部情報を返します。
	 *
	 * @og.rev 5.3.4.0 (2011/04/01) toString() の簡易版
	 *
	 * @return 接続先の簡易な内部情報
	 */
	public String dbidInfo() {
		return edbid.info();
	}

	/**
	 * 内部状況を簡易的に表現した文字列を返します。
	 * DBID／URL／USER／プールサイズ を返します。
	 *
	 * @og.rev 3.5.2.0 (2003/10/20) 接続情報に、データベース名、ドライバ名情報を追加する。
	 * @og.rev 3.5.6.6 (2004/08/23) 同期化方法を統一する為、synchronized をつけます。(別途 要検討)
	 * @og.rev 4.0.0.0 (2007/10/29) EDbidのtoStringを呼ぶように変更
	 *
	 * @return   このオブジェクトプールの文字列表現
	 */
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append( edbid.toString() );
		buf.append( super.toString() );
		return buf.toString();
	}
}
