/*
 * 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.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Collections;

import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.HybsDateUtil;				// 6.9.2.1 (2018/03/12)
import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.system.HybsConst;				// 7.2.3.1 (2020/04/17)
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;
import org.w3c.dom.NamedNodeMap;							// 6.9.2.0 (2018/03/05)

import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * DB設定XMLの内容をJAXBを利用してロードする
 * Driverをロードする
 * 上記２つの機能を備えたクラスです
 *
 * 外部からはgetDbidメソッドを利用してDB設定(ExpandedDbid型)を取得します。
 * DB設定情報が無い場合にXMLを読みにいきます。
 * このDBIDを決めるキーは、内部取り込み字に、大文字変換されますので、大文字・
 * 小文字の区別はありません。
 *
 * @og.rev 4.0.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";

	// 6.4.3.3 (2016/03/04) 初期 DBConfig.xml ファイルの設定。
	public static final String DB_CONFIG_FILE = "../DBConfig.xml" ;

	// XMLファイル関連
	private final String xmlFilename;				// 5.7.2.2 (2014/01/24)

	/** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。  */
	private final Map<String, EDbid> dbidMap = Collections.synchronizedMap( new LinkedHashMap<>() );	// 5.6.7.0 (2013/07/27)
	/** 6.4.3.1 (2016/02/12) Collections.synchronizedList で同期処理を行います。  */
	private final List<String>    driverList = Collections.synchronizedList( new ArrayList<>() );

	// 5.6.7.0 (2013/07/27) プルダウンメニュー用の情報の、キャッシュ用変数。
	private String codeKeyVal ;						// 初めて要求されたときに、セットします。

	// 5.6.6.0 (2013/07/05) 表題(title)属性を追加
	private static final String[] DBID_INFO_KEYS
				= { "dbidKey", "title", "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( DB_CONFIG_FILE );
	}

	/**
	 * XMLファイルの名前を指定して読み込む
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) クラスローダー外からでもDBConfig.xmlを取得できるようにする
	 * @og.rev 5.6.7.0 (2013/07/27) オブジェクト作成時に初期化も行っておきます。
	 * @og.rev 5.6.8.2 (2013/09/20) Tomcat8で、クラスローダーが変更されているのでその対応
	 * @og.rev 5.7.2.2 (2014/01/24) WEB-INF/classes フォルダがないと、xmlURL がnull になる対応。
	 * @og.rev 5.7.2.3 (2014/01/31) ファイルの存在チェックを追加します。
	 * @og.rev 6.4.3.3 (2016/03/04) 初期 DBConfig.xml ファイルの設定。
	 * @og.rev 6.6.0.0 (2016/12/01) コンテキストパスから、##バージョン番号を取り去った値を返すようにします。
	 * @og.rev 6.8.5.1 (2018/01/15) ClassLoader変数名を、clsl から、loader に変更(他の変数名と整合性を持たす)。
	 *
	 * @param	infile	XMLファイルの名前
	 */
	public DatabaseConfig( final String infile ) {
		final String xmlfile = infile == null || infile.isEmpty() ? DB_CONFIG_FILE : infile ;		// 引数が無い場合の初期設定を行います。
		String fileName = null;

		// 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected.  Use Thread.currentThread().getContextClassLoader() instead.(PMD)
		final ClassLoader loader	= Thread.currentThread().getContextClassLoader();
		URL xmlURL					= loader.getResource( xmlfile );

		// 5.6.8.2 (2013/09/20) Tomcat8で、xmlURL が取得できなくなっている・・・ようだ。
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		if( xmlURL == null ) {
			xmlURL = loader.getResource( "/" );		// クラスパスのベースURL
			// 5.7.2.2 (2014/01/24) Tomcat7で、WEB-INF/classes フォルダがないと、xmlURL がnull になる。
			if( xmlURL != null ) {
				final File temp = new File( xmlURL.getPath() , xmlfile );
				if( temp.exists() ) { fileName = temp.getAbsolutePath(); }
			}
		}
		else {
			fileName = xmlURL.getFile().replaceAll( "%23%23","##" );		// 6.6.0.0 (2016/12/01)
			// 5.7.2.3 (2014/01/31) ファイルの存在チェックを追加します。
			if( ! new File( fileName ).exists() ) { fileName = null; }
		}

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

		if( fileName == null ) {
			// 5.5.7.2 (2012/10/09) コメント追加
			final String errMsg = "DBConfig.xmlが見つかりません。File=[" + xmlfile + "]\n"
								+ " WEB-INF/classes フォルダがないと、相対パスで見つけることができません。" ;
			throw new OgRuntimeException( errMsg );
		}

		xmlFilename = fileName;

		init();			// 5.6.7.0 (2013/07/27)
	}

	/**
	 * dbidKeyをキーにしてExpandedDbid型でマップの内容を返す。
	 * 存在しない場合はNULLを返します。
	 * キーが無い場合に初期化を行う。
	 *
	 * @og.rev 4.0.0.1 (2007/12/04) EDbid#clone() 廃止
	 * @og.rev 5.6.7.0 (2013/07/27) synchronized メソッドにします。
	 * @og.rev 6.0.0.1 (2014/04/25) Collections.synchronizedMap を使用します。
	 *
	 * @param key XMLで登録したdbidKey
	 *
	 * @return EDbid型オブジェクト
	 */
	public EDbid getDbid( final String key ) {
		return dbidMap.get( key.toUpperCase( Locale.JAPAN ) ) ;
	}

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

	/**
	 * 初期化処理
	 *
	 * 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 関係 廃止
	 * @og.rev 5.6.7.0 (2013/07/27) dbidMap,driverList を書き込むのではなく、作成します。
	 * @og.rev 7.2.3.1 (2020/04/17) System.getenv → HybsConst.getenv 変更(サービス化対応)
	 */
	private void init() {
		final Document doc = DomParser.read( new File(xmlFilename) ) ;
		final Element firstRoot = doc.getDocumentElement();

		makeDriverList( firstRoot );				// 5.6.7.0 (2013/07/27)

		// 5.6.7.0 (2013/07/27) を、かけておきます。
		synchronized ( this ) {
			for( final String dr : driverList ) {
				try {
					Class.forName( dr );
//				} catch( final ClassNotFoundException ex ) {		// 7.0.0.1 (2018/10/09) LinkageErrorが発生したので、いっそすべてをcatchします。
				} catch( final Throwable th ) {
					final String errMsg = "ドライバクラスが見つかりません。[" + dr + "]" ;
					LogWriter.log( errMsg );
					LogWriter.log( th );
				}
			}
		}

		final EDbid defDdbid = new EDbid();		// 初期値
//		defDdbid.setUrl(		System.getenv( "REALM_URL" ) );
//		defDdbid.setUser(		System.getenv( "REALM_NAME" ) );
//		defDdbid.setPassword(	System.getenv( "REALM_PASSWORD" ) );
		defDdbid.setUrl(		HybsConst.getenv( "REALM_URL"      ) );	// 7.2.3.1 (2020/04/17)
		defDdbid.setUser(		HybsConst.getenv( "REALM_NAME"     ) );	// 7.2.3.1 (2020/04/17)
		defDdbid.setPassword(	HybsConst.getenv( "REALM_PASSWORD" ) );	// 7.2.3.1 (2020/04/17)

		makeDbidMap( firstRoot,defDdbid );				// 5.6.7.0 (2013/07/27)
	}

	/**
	 * ドライバーリストを取得します。
	 *
	 * 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をオブジェクト変数化
	 * @og.rev 5.6.7.0 (2013/07/27) driverList を書き込むのではなく、作成します。
	 * @og.rev 5.6.7.0 (2013/07/27) synchronized メソッドにします。
	 * @og.rev 7.2.3.1 (2020/04/17) System.getenv → HybsConst.getenv 変更(サービス化対応)
	 *
	 * @param	element	DB設定XMLファイルのElementオブジェクト
	 */
	private void makeDriverList( final Element element ) {

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

		if( driverList.isEmpty() ) {
//			final String realmDriver = System.getenv( "REALM_DRIVER" );
			final String realmDriver = HybsConst.getenv( "REALM_DRIVER" );	// 7.2.3.1 (2020/04/17)
			if( realmDriver != null && realmDriver.length() > 0 ) {
				driverList.add( realmDriver );
			}
		}

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

	/**
	 * EDbidオブジェクトのマップを取得します。
	 *
	 * DB設定XMLファイル(DBConfig.xml)の、dbid タグを取り込みます。
	 * このファイルから、EDbidオブジェクトの属性情報を取得し、オブジェクトを構築します。
	 *
	 * EDbidオブジェクト は、初期値をコピーして、作成していきます。
	 * EDbidオブジェクトをマップから取り出すキーとなる、dbidKey は、大文字化して設定します。
	 *
	 * 7.0.5.0 (2019/09/09)
	 *  begin,end,step,var,items 属性に対して、stepを年月指定できるように、type属性を追加しました。
	 *  type="month" で、begin,end を、年月として処理します。
	 *
	 * @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追加
	 * @og.rev 5.6.6.0 (2013/07/05) 表題(title)属性の取得
	 * @og.rev 5.6.7.0 (2013/07/27) 内部MapをDBConfig.xmlの読み込み順に変更。
	 * @og.rev 5.6.7.0 (2013/07/27) dbidMap を書き込むのではなく、作成します。
	 * @og.rev 5.6.7.0 (2013/07/27) synchronized メソッドにします。
	 * @og.rev 5.6.7.1 (2013/08/09) DEFAULT と、RESOURCE の DBIDキーがなければ、内部的に作成します。
	 * @og.rev 5.6.8.0 (2013/09/06) RESOURCE の DBIDキーを内部作成時に、title も設定します。
	 * @og.rev 6.4.3.4 (2016/03/11) computeIfAbsent と、新しい clone(String) メソッドを使用する。
	 * @og.rev 6.8.2.4 (2017/11/20) 環境変数( ${env.XXXX} ) を処理できるようにします。
	 * @og.rev 6.9.2.0 (2018/03/05) dbidに、begin,end,step,var,items 属性を追加します(影響は、ForEachObject内)。
	 * @og.rev 7.0.5.0 (2019/09/09) dbidに、type 属性を追加します(影響は、ForEachObject内)。
	 *
	 * @param  element DB設定XMLファイルのElementオブジェクト
	 * @param  defDdbid 初期情報の設定された、EDbidオブジェクト
	 */
	private void makeDbidMap( final Element element , final EDbid defDdbid ) {
		final NodeList list = element.getElementsByTagName( "dbid" ) ;
		final int num = list.getLength();
		for( int i=0; i<num; i++ ) {
			final Element ele = (Element)list.item(i);
			final ForEachObject forEach = new ForEachObject( ele );

			while( forEach.hasNext() ) {
				final NodeList childs   = ele.getChildNodes();
				final int      numChild = childs.getLength();
				final EDbid    dbid     = defDdbid.clone();				// 初期値をコピーして、作成
				for( int j=0; j<numChild; j++ ) {
					final Node nd = childs.item(j);
					if( nd.getNodeType() == Node.ELEMENT_NODE ) {
						final Element el = (Element)nd;
						final String tag = el.getTagName();
//						// 6.8.2.4 (2017/11/20) 環境変数( ${env.XXXX} ) を処理できるようにします。
//						final String txt = envText( el.getTextContent() );
						// 6.9.2.0 (2018/03/05) 変数の置き換え( ${var} )と、環境変数( ${env.XXXX} ) を処理します。
						final String txt = forEach.getText( el.getTextContent() );

						// dbidKey は、toUpperCase して、大文字のみとする。
						// 6.8.2.4 (2017/11/20) 環境変数( ${env.XXXX} ) を処理できるようにします。
						if( "dbidKey".equals( tag ) )	{
							if( txt != null && txt.length() > 0 ) {
								dbid.setDbidKey( txt.toUpperCase( Locale.JAPAN ) );
							}
						}
						else if( "title".equals( tag ) )	{ dbid.setTitle(	txt ); }		// 5.6.6.0 (2013/07/05) 表題(title)属性の取得
						else if( "url".equals( tag ) )		{ dbid.setUrl(		txt ); }
						else if( "user".equals( tag ) )		{ dbid.setUser(		txt ); }
						else if( "password".equals( tag ) ) { dbid.setPassword( txt ); }
						else if( "readonly".equals( tag ) ) { dbid.setReadonly( txt ); }
						else if( "mincount".equals( tag ) ) { dbid.setMincount( txt ); }
						else if( "maxcount".equals( tag ) ) { dbid.setMaxcount( txt ); }
						else if( "pooltime".equals( tag ) ) { dbid.setPooltime( txt ); }
						else if( "applicationInfo".equals( tag ) ) { dbid.setApplicationInfo( txt ); }
						else if( "property".equals( tag ) ) { dbid.addProp(		txt ); } // 5.5.2.0 (2012/05/01)
						else {
							System.err.println( "警告：dbid に新しい属性が、追加されています。" );
						}
					}
				}
				dbidMap.put( dbid.getDbidKey(), dbid );		// 5.6.7.0 (2013/07/27) 復活
			}
		}

		// 5.6.7.1 (2013/08/09) DEFAULT と、RESOURCE の DBIDキーがなければ、内部的に作成します。
		// 6.4.3.4 (2016/03/11) computeIfAbsent と、新しい clone(String) メソッドを使用する。
		// Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
		dbidMap.computeIfAbsent( "DEFAULT", k -> defDdbid.clone( "DEFAULT" ) );	// DEFAULT が存在するか確認する。

		// 6.4.3.4 (2016/03/11) computeIfAbsent と、新しい clone(String) メソッドを使用する。
		// Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
		dbidMap.computeIfAbsent( "RESOURCE", k -> defDdbid.clone( "RESOURCE" ) );	// RESOURCE が存在するか確認する。
	}

//	/**
//	 * 環境変数( ${env.XXX} )を含むテキストを処理します。
//	 *
//	 * 環境変数を含まない場合は、引数の orgTxt を返します。
//	 *
//	 * @og.rev 6.8.2.4 (2017/11/20) 環境変数( ${env.XXXX} ) を処理できるようにします。
//	 *
//	 * @param  orgTxt 環境変数( ${env.XXX} )を含む可能性のあるテキスト
//	 * @return  環境変数を処理したテキスト
//	 */
//	private String envText( final String orgTxt ) {
//		final int st = orgTxt.indexOf( "${env." );
//		if( st >= 0 ) {
//			final int ed = orgTxt.indexOf( '}' , st );
//			if( ed >= 0 ) {
//				final String envKey = orgTxt.substring( st + "${env.".length() , ed );
//				final String envTxt = orgTxt.substring( 0,st ) + System.getenv( envKey ) + orgTxt.substring( ed+1 );
//				return envText( envTxt );			// ループすべきだが、再帰処理で対応します。
//			}
//			else {
//				final String errMsg = xmlFilename + " の環境変数( ${env.XXX} )の整合性が取れていません。" + orgTxt ;
//				throw new OgRuntimeException( errMsg );
//			}
//		}
//
//		return orgTxt;
//	}

	/**
	 * dbidの属性処理が、煩雑になってきたので、内部クラスにまとめます。
	 *
	 * begin,end,step,var,items 属性で、ループ処理を行うための設定を取得します。
	 * items が未指定の場合は、begin,end,step による、数字(文字列)の配列を返します。
	 * itemsが、定義されている場合は、ｶﾝﾏで分割後、文字列の配列を返します。
	 * それぞれ、初期値があり、begin=0 , end=0 or items要素数,step=1,
	 *
	 * @og.rev 7.0.5.0 (2019/09/09) type属性を追加。
	 *  begin,end,step,var,items 属性に対して、stepを年月指定できるように、type属性を追加しました。
	 *  type="month" で、begin,end を、年月として処理します。
	 *
	 * @og.rev 6.9.2.1 (2018/03/12) dbidに、begin,end,step,var,items 属性を追加します。
	 * @og.rev 7.0.5.0 (2019/09/09) dbidに、type 属性を追加します。
	 */
	private static final class ForEachObject {
		private final String   attVar ;				// var 属性の値を、${xxxx} に置き換えた文字列。未定義は、null
		private final String[] items ;				// items 属性のCSV分割か、begin,end,step の数字文字列の配列

		private int   idx      = -1;				// hasNext() で、先にアップするので、一つ引いておきます。

		/**
		 * Elementオブジェクトを指定して、インスタンスを生成するコンストラクタです。
		 *
		 * @og.rev 6.9.2.1 (2018/03/12) dbidに、begin,end,step,var,items 属性を追加します。
		 * @og.rev 7.0.5.0 (2019/09/09) dbidに、type 属性を追加します。
		 *
		 * @param  element DB設定XMLファイルのElementオブジェクト
		 */
		public ForEachObject( final Element ele ) {
			// 6.9.2.0 (2018/03/05) dbidに、begin,end,step,var 属性を追加します。
			if( ele.hasAttributes() ) {
				final NamedNodeMap attNode = ele.getAttributes();
//				final int attBgn = StringUtil.nval( getAttri( attNode,"begin" ),0 );	// 7.0.5.0 (2019/09/09) 削除
//				final int attStp = StringUtil.nval( getAttri( attNode,"step"  ),1 );	// 7.0.5.0 (2019/09/09) (ifの中へ)移動
				final String tmpVar = getAttri( attNode,"var" );
				attVar = StringUtil.isNull( tmpVar ) ? null : "${" + tmpVar + "}" ;		// 置換元定数を作成します。

//				final String endStr = getAttri( attNode,"end" );						// 7.0.5.0 (2019/09/09) (ifの中へ)移動
				final String itms   = getAttri( attNode,"items" );
				if( StringUtil.isNull( itms ) ) {
					final String bgiStr = getAttri( attNode,"begin" );					// 7.0.5.0 (2019/09/09) 追加
					final String endStr = getAttri( attNode,"end" );					// 7.0.5.0 (2019/09/09) (ifの中へ)移動
					final int attStp = StringUtil.nval( getAttri( attNode,"step"  ),1 );

					final String typStr = getAttri( attNode,"type" );					// 7.0.5.0 (2019/09/09) 新規追加
					if( "month".equalsIgnoreCase( typStr ) ) {							// 7.0.5.0 (2019/09/09) 新規追加
						items = HybsDateUtil.stepYM( bgiStr,endStr,attStp );
					}
					else {
						final int attBgn = StringUtil.nval( bgiStr,0 );					// 7.0.5.0 (2019/09/09) 文字列化をint化

						final int attEnd = StringUtil.nval( endStr,attBgn+1 );			// とりあえず １回は回すため。
						final List<String> list = new ArrayList<>();
						for( int i=attBgn; i<attEnd; i+=attStp ) {
							list.add( String.valueOf( i ) );
						}

						items = list.toArray( new String[list.size()] ) ;
					}
				}
				else {
					items = itms.split( "," );
				}
			}
			else {
				attVar = null;
				items  = new String[] { "0" };			// 最低１回は回せるように。
			}
		}

		/**
		 * 反復処理でさらに要素がある場合にtrueを返します。
		 *
		 * この判定で、内部カウンタを、プラスしていますので、
		 * ループ判定時にのみ、呼び出してください。
		 *
		 * @og.rev 6.9.2.1 (2018/03/12) 新規追加
		 *
		 * @return 反復処理でさらに要素がある場合はtrue
		 */
		public boolean hasNext() { return ++idx < items.length; }

		/**
		 * 引数の文字列に対して、var 属性と item要素の置換を行います。
		 *
		 * さらに、${env.XXXX} 環境変数の置換と、日付文字列{&#064;DATE.XXXX}の置換も
		 * 行います。
		 * この処理では、内部カウンタはアップしませんので、inTxt を替えて、何度でも
		 * 呼び出すことが可能です。
		 *
		 * @og.rev 6.9.2.1 (2018/03/12) 新規追加
		 * @og.rev 7.2.5.0 (2020/06/01) ${env.XXXX} 環境変数 は、{&#064;ENV.XXXX} も対応します。
		 *
		 * @param  inTxt 置換元の文字列
		 * @return items属性に対応して置換された、文字列
		 */
		public String getText( final String inTxt ) {
			String rtnTxt = StringUtil.replace( inTxt , attVar , items[idx] );		// attVar が null の場合、inTxt が戻される。
			rtnTxt = StringUtil.replaceText( rtnTxt , "${env."  , "}" , System::getenv );				// 環境変数置換
			rtnTxt = StringUtil.replaceText( rtnTxt , "{@ENV."  , "}" , System::getenv );				// 7.2.5.0 (2020/06/01) 環境変数置換
			rtnTxt = StringUtil.replaceText( rtnTxt , "{@DATE." , "}" , HybsDateUtil::getDateFormat );	// 日付文字列置換

			return rtnTxt;
		}

		/**
		 * 属性を取得する簡易メソッドです。
		 *
		 * 引数に、ノードのコレクションと、属性のキーワードを指定することで、属性値を返します。
		 *
		 * @og.rev 6.9.2.0 (2018/03/05) 新規追加
		 * @og.rev 7.2.5.0 (2020/06/01) ${env.XXXX} 環境変数 は、{&#064;ENV.XXXX} も対応します。
		 *
		 * @param  nodeMap 名前を指定してアクセスできるノードのコレクション
		 * @param  key    属性のキーワード
		 * @return 属性値(キーに対応する属性が無い場合は、null)
		 */
		private String getAttri( final NamedNodeMap nodeMap,final String key ) {
			final Node attNode = nodeMap.getNamedItem( key );

//			return attNode == null ? null : attNode.getNodeValue() ;

			String rtnTxt = attNode == null ? null : attNode.getNodeValue() ;

			rtnTxt = StringUtil.replaceText( rtnTxt , "${env."  , "}" , System::getenv );				// 環境変数置換
			rtnTxt = StringUtil.replaceText( rtnTxt , "{@ENV."  , "}" , System::getenv );				// 7.2.5.0 (2020/06/01) 環境変数置換
			rtnTxt = StringUtil.replaceText( rtnTxt , "{@DATE." , "}" , HybsDateUtil::getDateFormat );	// 日付文字列置換

			return rtnTxt;
		}
	}

	/* ------------------------------------------------------------------------------------
	 *
	 * 以下は、DBConfig.xml編集用のメソッドです。
	 * 編集用のメソッドでは、オブジェクト化されたDBID及びDBDRIVERの情報は使用せずに、
	 * DBConfig.xmlからその情報を再度読み出して、SET/GETしています。
	 * (オブジェクトとして依存しているのは、DBConfig.xmlのファイル名のみです)
	 *
	 * -------------------------------------------------------------------------------------
	 */
	/**
	 * DBIDとして管理している項目のキーの一覧を配列形式で返します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 *
	 * @return 項目のキー一覧
	 * @og.rtnNotNull
	 */
	public static String[] getDbidInfoKeys() {
		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を出力
	 * @og.rev 5.6.6.0 (2013/07/05) 表題(title)属性を追加
	 * @og.rev 5.6.7.0 (2013/07/27) 内部MapをDBConfig.xmlの読み込み順に変更。
	 * @og.rev 6.3.9.0 (2015/11/06) Use block level rather than method level synchronization.(PMD)
	 *
	 * @return 全てのDBIDの属性情報のリスト(配列)
	 * @og.rtnNotNull
	 * @see #getDbidInfoKeys()
	 */
	public String[][] getDbidInfo() {
		// 6.3.9.0 (2015/11/06) 色々やりたいが、今はsynchronizedブロックにするだけにします。
		synchronized( dbidMap ) {
			String[][] dbidInfo = new String[dbidMap.size()][DBID_INFO_KEYS.length];
			int idx = 0;
			for( final EDbid dbid : dbidMap.values() ) {
				dbidInfo[idx][0] = dbid.getDbidKey();
				dbidInfo[idx][1] = dbid.getTitle();				// 5.6.6.0 (2013/07/05) 表題(title)属性を追加
				dbidInfo[idx][2] = dbid.getUrl();
				dbidInfo[idx][3] = dbid.getUser();
				dbidInfo[idx][4] = dbid.getPassword();
				dbidInfo[idx][5] = String.valueOf( dbid.isReadonly() );
				dbidInfo[idx][6] = String.valueOf( dbid.getMincount() );
				dbidInfo[idx][7] = String.valueOf( dbid.getMaxcount() );
				dbidInfo[idx][8] = String.valueOf( dbid.getPooltime() );
				dbidInfo[idx][9] = String.valueOf( dbid.isApplicationInfo() );
				dbidInfo[idx][10]= String.valueOf( dbid.getProps().toString() ); // 5.5.2.1 (2012/05/07)
				idx++;
			}

			return dbidInfo;
		}
	}

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

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

		if( dbidVals != null && dbidVals.length > 0 ) {
		 	// 5.6.7.0 (2013/07/27) 内部MapをDBConfig.xmlの読み込み順に変更(なので、廃止)。
			for( int i=0; i<dbidVals.length; i++ ) {
				final Element newEle = doc.createElement( "dbid" );
				for( int j=0; j<dbidVals[i].length; j++ ) {
					final 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 );

		reload();		// 5.6.7.0 (2013/07/27) DBIDの属性情報のリストを更新後、初期化します。
	}

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

	/**
	 * DBドライバーのリスト(配列)を返します。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) 新規作成
	 * @og.rev 5.6.7.0 (2013/07/27) driverList を書き込むのではなく、作成します。
	 *
	 * @return	DBドライバーリスト(配列)
	 * @og.rtnNotNull
	 */
	public String[] getDriverList() {
		return driverList.toArray( new String[driverList.size()] );
	}

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

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

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

		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
		if( drivers != null && drivers.length > 0 ) {
			for( int i=0; i<drivers.length; i++ ) {
				final Element newEle = doc.createElement( "class" );
				newEle.setTextContent( drivers[i] );
				parent.appendChild( newEle );
			}
		}

		DomParser.write( new File(xmlFilename), doc );

		reload();		// 5.6.7.0 (2013/07/27) DBドライバーのリストを更新後、初期化します。
	}

	/**
	 * DBID情報のキーとタイトルから、プルダウンメニューを作成するための情報を取得します。
	 *
	 * このメソッドを呼び出すと、DBConfig.xmlで定義されている dbidKey と、 title 属性から、
	 * 「key1:val1 key2:val2 ・・・」という文字列を作成します。
	 * これを利用すれば、プルダウンメニューが簡単に作成できます。
	 *
	 * @og.rev 5.6.7.0 (2013/07/27) プルダウンメニュー用の情報を作成します。
	 * @og.rev 5.6.7.1 (2013/08/09) 表題(title)属性のスペース対策
	 * @og.rev 6.2.6.0 (2015/06/19) 表題(title)属性のスペース対策(KEY:LBL をダブルクオートで囲う)
	 * @og.rev 6.3.9.0 (2015/11/06) Use block level rather than method level synchronization.(PMD)
	 * @og.rev 7.1.0.0 (2020/01/20) DB接続できないDBIDはプルダウンメニューに含めない。
	 * @og.rev 7.2.3.1 (2020/04/17) 超特殊処理：Derby の場合は、ｴﾗｰﾒｯｾｰｼﾞを出さない。
	 *
	 * @return プルダウンメニューを作成するための情報
	 */
	public String getCodeKeyVal() {
		if( codeKeyVal == null ) {
			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
			// 6.3.9.0 (2015/11/06) 色々やりたいが、今はsynchronizedブロックにするだけにします。
			synchronized( dbidMap ) {
				for( final EDbid dbid : dbidMap.values() ) {
					final String key = dbid.getDbidKey();
					final String dbName = dbid.getDbProductName();		// 7.2.3.1 (2020/04/17)
					try {												// 7.1.0.0 (2020/01/20)
//						if( dbid.getDbProductName() == null ) {
						if( dbName == null ) {
							ConnectionFactory.getDBName( key );			// 7.1.0.0 (2020/01/20) DB接続テスト
						}

						final String lbl = StringUtil.nval( dbid.getTitle() , key );
						// 6.0.2.5 (2014/10/31) char を append する。
						if( lbl.indexOf( ' ' ) >= 0 ) {					// 5.6.7.1 (2013/08/09) スペース対策
							buf.append( '"' ).append( key ).append( ':' ).append( lbl ).append( '"' );
						}
						else {
							buf.append( key ).append( ':' ).append( lbl );
						}
						buf.append( ' ' );
					}
	//				catch( final OgRuntimeException ex ) {				// 7.1.0.0 (2020/01/20) エラー時
					catch( final RuntimeException ex ) {				// 7.1.0.0 (2020/01/20) エラー時
						// 7.2.3.1 (2020/04/17) 超特殊処理：Derby の場合は、ｴﾗｰﾒｯｾｰｼﾞを出さない。
						// ProductやdbNameはnullなので、URLから判定します。
						final String dbUrl = dbid.getUrl();
						if( dbUrl != null && dbUrl.contains( "derby" ) ) {
		//					final String errMsg = "DBID=[" + key + "] 接続できません" ;		// ｴﾗｰﾒｯｾｰｼﾞの簡素化
		//					System.err.println( errMsg );
						}
						else {
							System.err.println( "警告：" + dbUrl + "\t\n" +  ex.getMessage() );
						}
					}
				}
			}

			buf.setLength( buf.length()-1 );		// 最後のスペースを削除
			codeKeyVal = buf.toString();
		}

		return codeKeyVal;
	}

	/**
	 * 親要素を基点として、引数で指定されたタグ名を持つ子要素を削除します。
	 *
	 * @og.rev 5.6.7.0 (2013/07/27) staticメソッド を イスタンスメソッドに変更
	 *
	 * @param parent 親要素
	 * @param childTagName 削除する子要素のタグ名
	 */
	private 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 ) {
				// 6.4.4.1 (2016/03/18)
				isDel = ((Element)child).getTagName().equalsIgnoreCase( childTagName );
			}

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