/*
 * 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 java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.LogWriter;
import org.opengion.fukurou.xml.DomParser;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * DB設定XMLの内容をJAXBを利用してロードする
 * Driverをロードする
 * 上記２つの機能を備えたクラスです
 *
 * 外部からはgetDbidメソッドを利用してDB設定(ExpandedDbid型)を取得します。
 * DB設定情報が無い場合にXMLを読みにいきます。
 * このDBIDを決めるキーは、内部取り込み字に、大文字変換されますので、大文字・
 * 小文字の区別はありません。
 *
 * @og.rev 4.0.0 (2007/10/25) 新規作成
 * @og.rev 5.1.7.0 (2010/06/01) org.opengion.fukurou.xml.jaxb.dbid 関係 廃止
 * @og.group 初期化
 *
 * @version  4.0
 * @author 高橋正和
 * @since   JDK6.0,
 */
public class DatabaseConfig {

	// fukurou内で完結させるため、HybsDataからは読み込まずにここに書く
	private static final String DEFAULT_DRIVER	 = "oracle.jdbc.OracleDriver";

	// XMLファイル関連
//	private transient final String XmlFilename;
	private final String XmlFilename;
//	private static final String SCHEMA_FILENAME = "DBConfig.xsd";//xsdはfukurou.xml.jaxb.dbidパッケージに置く

//	private transient final Map<String, EDbid> dbidMap = new HashMap<String, EDbid>();
//	private transient List<String> driverList = new ArrayList<String>();
	private Map<String, EDbid> dbidMap = new HashMap<String, EDbid>();
//	private List<String> driverList = new ArrayList<String>();
	private List<String> driverList = new ArrayList<String>();

//	private static final String CR = System.getProperty( "line.separator" );

	/* DBIDのキーの一覧を管理します。5.1.9.0 (2010/08/01) */
	// 5.5.2.0 (2012/05/01) property追加
//	private static final String[] DBID_INFO_KEYS
//				= { "dbidKey", "url", "user", "password", "readonly"
//					, "mincount", "maxcount", "pooltime", "applicationInfo" };
	private static final String[] DBID_INFO_KEYS
				= { "dbidKey", "url", "user", "password", "readonly"
					, "mincount", "maxcount", "pooltime", "applicationInfo","property" };

	/* DBDRIVERのキーのを管理します。5.1.9.0 (2010/08/01) */
	private static final String DBDRIVER_CLASS_KEY = "class";

	/**
	 * 初期値を使ってXMLを読み込む
	 * XmlFilenameの初期値は../DBConfig.xml
	 *
	 * @og.rev 4.3.1.1 (2008/08/23) 自分のコンストラクターを呼ぶように修正
	 */
	public DatabaseConfig() {
		this( "../DBConfig.xml" );
	}

	/**
	 * XMLファイルの名前を指定して読み込む
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) クラスローダー外からでもDBConfig.xmlを取得できるようにする
	 *
	 * @param	xmlfile	XMLファイルの名前
	 */
	public DatabaseConfig( final String xmlfile ) {
//		XmlFilename = xmlfile;
		String fileName = null;

		ClassLoader clsl	= getClass().getClassLoader();
		URL xmlURL			= clsl.getResource( xmlfile );
		if( xmlURL != null ) {
			fileName = xmlURL.getFile();
		}

		// 5.1.9.0 (2010/08/01)  クラスローダー外からでもDBConfig.xmlを取得できるようにする
		if( fileName == null && new File( xmlfile ).exists() ) {
			fileName = xmlfile;
		}

		if( fileName == null ) {
			throw new RuntimeException( "DBConfig.xmlが見つかりません。File=[" + xmlfile + "]" );
		}

		XmlFilename			= fileName;
//		System.out.println( XmlFilename );
	}

	/**
	 * dbidKeyをキーにしてExpandedDbid型でマップの内容を返す。
	 * 存在しない場合はNULLを返します。
	 * キーが無い場合に初期化を行う。
	 *
	 * @og.rev 4.0.0.1 (2007/12/04) EDbid#clone() 廃止
	 *
	 * @param key XMLで登録したdbidKey
	 *
	 * @return EDbid型オブジェクト
	 */
	public EDbid getDbid( final String key ) {
		synchronized ( dbidMap ) {
			if( dbidMap.isEmpty() ) {
				init();
			}

			return dbidMap.get( key.toUpperCase( Locale.JAPAN ) ) ;
		}
	}

	/**
	 * マップをクリアします。
	 * XMLファイルを再読み込みする場合に使用します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) ドライバーのリストもクリアする。
	 */
	public void reload() {
		synchronized ( dbidMap ) {
			dbidMap.clear();
		}
		synchronized ( driverList ) {
			driverList.clear();
		}
	}

	/**
	 * 初期化処理
	 *
	 * DB設定XMLファイル(DBConfig.xml)を読み込みます。
	 * このファイルから、ドライバーリストの取得、DBIDのオブジェクトマップの作成を
	 * 行います。
	 * EDbidオブジェクト は、環境変数で、共通の初期値を定義しておくことが可能です。
	 * 項目として、REALM_URL、REALM_NAME、REALM_PASSWORD が設定可能です。
	 *
	 * ドライバーリストの取得後、Class.forName で、ドライバの登録も行います。
	 *
	 * @og.rev 5.1.7.0 (2010/06/01) org.opengion.fukurou.xml.jaxb.dbid 関係 廃止
	 */
	private void init() {
		Document doc = DomParser.read( new File(XmlFilename) ) ;
		Element firstRoot = doc.getDocumentElement();

//		List<String> driverList = getDriverList( firstRoot );
		driverList = getDriverList( firstRoot );
		for( String dr : driverList ) {
			try {
				Class.forName( dr );
			} catch ( ClassNotFoundException ex ) {
				String errMsg = "ドライバクラスが見つかりません。[" + dr + "]" ;
				LogWriter.log( errMsg );
				LogWriter.log( ex );
			}
		}

		EDbid defDdbid = new EDbid();		// 初期値
		defDdbid.setUrl(		System.getenv( "REALM_URL" ) );
		defDdbid.setUser(		System.getenv( "REALM_NAME" ) );
		defDdbid.setPassword(	System.getenv( "REALM_PASSWORD" ) );

		dbidMap = getDbidMap( firstRoot,defDdbid );
	}

	/**
	 * ドライバーリストを取得します。
	 *
	 * DB設定XMLファイル(DBConfig.xml)の、class タグを取り込みます。
	 * このファイルから、ドライバーリストを取得します。
	 *
	 * 内部的に３段階の処理が実行されます。
	 * 第１Step:DBConfig.xml から、ドライバーリストを取得
	 * 第２Step:ドライバーリストが存在しない場合、環境変数の REALM_DRIVER からドライバーを取得
	 * 第３Step:それでも存在しない場合、このクラスの DEFAULT_DRIVER 定数 からドライバーを取得
	 *
	 * @og.rev 5.1.7.0 (2010/06/01) org.opengion.fukurou.xml.jaxb.dbid 関係 廃止
	 * @og.rev 5.1.9.0 (2010/08/01) ドライバ一覧のListをオブジェクト変数化
	 *
	 * @param	element	DB設定XMLファイルのElementオブジェクト
	 *
	 * @return	ドライバーリスト
	 */
	private static List<String> getDriverList( final Element element ) {
		List<String> dList = new ArrayList<String>();

		NodeList list = element.getElementsByTagName( "class" ) ;
		int num = list.getLength();
		for (int i = 0; i < num; i++) {
			Element cls = (Element)list.item(i);
			dList.add( cls.getTextContent() );
		}

		if( dList.isEmpty() ) {
			String realmDriver = System.getenv( "REALM_DRIVER" );
			if( realmDriver != null && realmDriver.length() > 0 ) {
				dList.add( realmDriver );
			}
		}

		if( dList.isEmpty() ) { dList.add( DEFAULT_DRIVER ); }

		return dList ;
	}

	/**
	 * EDbidオブジェクトのマップを取得します。
	 *
	 * DB設定XMLファイル(DBConfig.xml)の、dbid タグを取り込みます。
	 * このファイルから、EDbidオブジェクトの属性情報を取得し、オブジェクトを構築します。
	 *
	 * EDbidオブジェクト は、初期値をコピーして、作成していきます。
	 * EDbidオブジェクトをマップから取り出すキーとなる、dbidKey は、大文字化して設定します。
	 *
	 * @og.rev 5.1.7.0 (2010/06/01) org.opengion.fukurou.xml.jaxb.dbid 関係 廃止
	 * @og.rev 5.1.9.0 (2010/08/01) Mapを返すように変更
	 * @og.rev 5.5.2.0 (2012/05/01) property追加
	 *
	 * @param  element DB設定XMLファイルのElementオブジェクト
	 * @param  defDdbid 初期情報の設定された、EDbidオブジェクト
	 *
	 * @return EDbidオブジェクトのマップ
	 */
//	private void makeDbidMap( final Element element , EDbid defDdbid ) {
	private static Map<String,EDbid> getDbidMap( final Element element , EDbid defDdbid ) {
		Map<String,EDbid> dMap = new HashMap<String,EDbid>();

		NodeList list = element.getElementsByTagName( "dbid" ) ;
		int num = list.getLength();
		for (int i = 0; i < num; i++) {
			Element ele = (Element)list.item(i);
			NodeList childs = ele.getChildNodes();
			int numChild = childs.getLength();
//			EDbid dbid = new EDbid();
			EDbid dbid = defDdbid.clone();		// 初期値をコピーして、作成
			for (int j = 0; j < numChild; j++) {
				Node nd = childs.item(j);
				if( nd.getNodeType() == Node.ELEMENT_NODE ) {
					Element el = (Element)nd;
					String tag = el.getTagName();
					// dbidKey は、toUpperCase して、大文字のみとする。
					if( "dbidKey".equals( tag ) )	{
						String dbidKey = el.getTextContent();
						if( dbidKey != null && dbidKey.length() > 0 ) {
							dbid.setDbidKey( dbidKey.toUpperCase( Locale.JAPAN ) );
						}
					}
					else if( "url".equals( tag ) )		{ dbid.setUrl(		el.getTextContent() ); }
					else if( "user".equals( tag ) )		{ dbid.setUser(		el.getTextContent() ); }
					else if( "password".equals( tag ) ) { dbid.setPassword( el.getTextContent() ); }
					else if( "readonly".equals( tag ) ) { dbid.setReadonly( el.getTextContent() ); }
					else if( "mincount".equals( tag ) ) { dbid.setMincount( el.getTextContent() ); }
					else if( "maxcount".equals( tag ) ) { dbid.setMaxcount( el.getTextContent() ); }
					else if( "pooltime".equals( tag ) ) { dbid.setPooltime( el.getTextContent() ); }
					else if( "applicationInfo".equals( tag ) ) { dbid.setApplicationInfo( el.getTextContent() ); }
					else if ("property".equals( tag ) ) { dbid.addProp( el.getTextContent() ); } // 5.5.2.0 (2012/05/01)
					else {
						System.err.println( "警告：dbid に新しい属性が、追加されています。" );
					}
				}
			}
//			dbidMap.put( dbid.getDbidKey(), dbid );
			dMap.put( dbid.getDbidKey(), dbid );
		}

		return dMap;
	}

	/* ------------------------------------------------------------------------------------
	 *
	 * 以下は、DBConfig.xml編集用のメソッドです。
	 * 編集用のメソッドでは、オブジェクト化されたDBID及びDBDRIVERの情報は使用せずに、
	 * DBConfig.xmlからその情報を再度読み出して、SET/GETしています。
	 * (オブジェクトとして依存しているのは、DBConfig.xmlのファイル名のみです)
	 *
	 * -------------------------------------------------------------------------------------
	 */
	/**
	 * DBIDとして管理している項目のキーの一覧を配列形式で返します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @return 項目のキー一覧
	 */
	public static String[] getDbidInfoKeys() {
//		return DBID_INFO_KEYS;
		return DBID_INFO_KEYS.clone();
	}

	/**
	 * 全てのDBIDの属性情報のリスト(配列)で返します。
	 *
	 * 値の順番については、{@link #getDbidInfoKeys()}で返されるキーの一覧と同じです。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 * @og.rev 5.5.2.1 (2012/05/07) propertiesを出力
	 *
	 * @return 全てのDBIDの属性情報のリスト(配列)
	 * @see #getDbidInfoKeys()
	 */
	public String[][] getDbidInfo() {
		Element ele = DomParser.read( new File(XmlFilename) ).getDocumentElement();
		Map<String,EDbid> dMap = getDbidMap( ele , new EDbid() );
		String[][] dbidInfo = new String[dMap.size()][DBID_INFO_KEYS.length];
		int idx = 0;
		for( EDbid dbid : dMap.values() ) {
			dbidInfo[idx][0] = dbid.getDbidKey();
			dbidInfo[idx][1] = dbid.getUrl();
			dbidInfo[idx][2] = dbid.getUser();
			dbidInfo[idx][3] = dbid.getPassword();
			dbidInfo[idx][4] = String.valueOf( dbid.isReadonly() );
			dbidInfo[idx][5] = String.valueOf( dbid.getMincount() );
			dbidInfo[idx][6] = String.valueOf( dbid.getMaxcount() );
			dbidInfo[idx][7] = String.valueOf( dbid.getPooltime() );
			dbidInfo[idx][8] = String.valueOf( dbid.isApplicationInfo() );
			dbidInfo[idx][8] = String.valueOf( dbid.getProps().toString() ); // 5.5.2.1 (2012/05/07)
			idx++;
		}
		dbidSort( dbidInfo );
		return dbidInfo;
	}

	/**
	 * 全てのDBIDの属性情報のリスト(配列)をセットします。
	 *
	 * このメソッドを呼び出すと、DBConfig.xmlで定義されているDBID情報一覧を"一旦削除し"、
	 * その上で、引数のDBID情報一覧をDBConfig.xmlに書き込みます。
	 *
	 * 値の順番については、{@link #getDbidInfoKeys()}で返されるキーの一覧と同じです。
	 *
	 * 書き込みの直前に、同じフォルダにタイムスタンプを付加したバックアップファイルを作成します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @param dbidVals 全てのDBIDの属性情報のリスト(配列)
	 * @see #getDbidInfoKeys()
	 */
	public void setDbidInfo( final String[][] dbidVals ) {
		FileUtil.copy( XmlFilename, XmlFilename + "_" + System.currentTimeMillis() );

		Document doc = DomParser.read( new File(XmlFilename) ) ;
		Element firstRoot = doc.getDocumentElement();
		deleteChildElements( firstRoot, "dbid" );

		if( dbidVals != null && dbidVals.length > 0 ) {
			dbidSort( dbidVals );
			for( int i=0; i<dbidVals.length; i++ ) {
				Element newEle = doc.createElement( "dbid" );
				for( int j=0; j<dbidVals[i].length; j++ ) {
					Element newChEle = doc.createElement( DBID_INFO_KEYS[j] );
					newChEle.setTextContent( dbidVals[i][j] );
					newEle.appendChild( newChEle );
				}
				firstRoot.appendChild( newEle );
				firstRoot.appendChild( doc.createTextNode( "\n\n" ) );
			}
		}

		DomParser.write( new File(XmlFilename), doc );
	}

	/**
	 * DBIDの配列をソートします。
	 * ソートの方法としては、
	 *  ①DEFAULTのDBIDは一番初め
	 *  ②DEFAULT以外は、DBID順
	 * となります。
	 *
	 * @param dbidVals 全てのDBIDの属性情報のリスト(配列)
	 */
	private static void dbidSort( final String[][] dbidVals ) {
		Arrays.sort( dbidVals, new Comparator<String[]>() {
			 public int compare( String[] s1, String[] s2 ) {
				 if( "DEFAULT".equals( s1[0] ) ) {
					 return -1;
				 }
				 else if( "DEFAULT".equals( s2[0] ) ) {
					 return 1;
				 }
				 return s1[0].compareTo( s2[0] );
			 }
		}
		);
	}

	/**
	 * DBドライバーの属性キーを返します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @return	DBドライバーの属性キー
	 */
	public static String getDriverKey() {
		return DBDRIVER_CLASS_KEY;
	}

	/**
	 * DBドライバーのリスト(配列)を返します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @return	DBドライバーリスト(配列)
	 */
	public String[] getDriverList() {
		Element ele = DomParser.read( new File(XmlFilename) ).getDocumentElement();
		String [] rtn = getDriverList( ele ).toArray( new String[0] );
		driverSort( rtn );
		return rtn;
	}

	/**
	 * DBドライバーのリスト(配列)をセットします。
	 *
	 * このメソッドを呼び出すと、DBConfig.xmlで定義されているclass一覧を"一旦削除し"、
	 * その上で、引数のDBドライバー一覧をDBConfig.xmlに書き込みます。
	 *
	 * 書き込みの直前に、同じフォルダにタイムスタンプを付加したバックアップファイルを作成します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @param drivers DBドライバーのリスト(配列)
	 */
	public void setDriverList( final String[] drivers ) {
		FileUtil.copy( XmlFilename, XmlFilename + "_" + System.currentTimeMillis() );

		Document doc = DomParser.read( new File(XmlFilename) );
		Element firstRoot = doc.getDocumentElement();

		Element parent = (Element)firstRoot.getElementsByTagName( "dbDriver" ).item( 0 );
		deleteChildElements( parent, "class" );

		if( drivers != null && drivers.length > 0 ) {
			driverSort( drivers );
			for( int i=0; i<drivers.length; i++ ) {
				Element newEle = doc.createElement( "class" );
				newEle.setTextContent( drivers[i] );
				parent.appendChild( newEle );
			}
		}

		DomParser.write( new File(XmlFilename), doc );
	}

	/**
	 * DBドライバーの配列をソートします。
	 * ソートの方法としては、
	 *  ①DEFAULT_DRIVERのDBは一番初め
	 *  ②DEFAULT以外は、DBID順
	 * となります。
	 *
	 * @param drivers 全てのDBIDの属性情報のリスト(配列)
	 */
	private static void driverSort( final String[] drivers ) {
		Arrays.sort( drivers, new Comparator<String>() {
			 public int compare( String s1, String s2 ) {
				 if( DEFAULT_DRIVER.equals( s1 ) ) {
					 return -1;
				 }
				 else if( DEFAULT_DRIVER.equals( s2 ) ) {
					 return 1;
				 }
				 return s1.compareTo( s2 );
			 }
		}
		);
	}

	/**
	 * 親要素を基点として、引数で指定されたタグ名を持つ子要素を削除します。
	 *
	 * @param parent 親要素
	 * @param childTagName 削除する子要素のタグ名
	 */
	private static void deleteChildElements( final Element parent, final String childTagName ) {
		Node child = parent.getFirstChild();
		boolean isDel = false;
		while ( child != null ) {
			// エレメント間の改行Cも削除するため、次の異なる要素が来るまでは削除し続けます。
			if( child.getNodeType() == Node.ELEMENT_NODE ) {
				if( ((Element)child).getTagName().equals( childTagName ) ) {
					isDel = true;
				}
				else {
					isDel = false;
				}
			}

			Node next = child.getNextSibling();
			if( isDel ) {
				parent.removeChild( child );
			}
			child = next;
		}
	}
}
