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

import static org.opengion.fukurou.util.StringUtil.nval;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Collections;
// import java.util.Iterator;								// 6.7.8.0 (2017/04/21)
import java.util.Locale ;								// 6.7.6.1 (2017/03/17)
import java.util.concurrent.ConcurrentMap;				// 6.7.8.0 (2017/04/21)
import java.util.concurrent.ConcurrentHashMap;			// 6.7.8.0 (2017/04/21)

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.db.DBTableModel;

import static org.opengion.hayabusa.taglib.ValueMapParamTag.VMP_KEYS;	// 6.7.8.0 (2017/04/21)

/**
 * DBTableModelオブジェクトから、指定のキー情報と、レコードから、Mapオブジェクトを作成し、それを、
 * BODY部のフォーマットに対応して、出力します。
 *
 * valueタグの、command="MAPOBJ" や、ALL_MAPOBJ に相当する処理を、複数キーと行レベルのデータで
 * 管理します。
 *
 * 設定した値は、Mapを優先した、特殊な、{&#064;XXXX} 形式で 取り出すことができます。
 *
 * keys で、CSV形式でカラム名を指定し、これらを、連結した文字列を、Mapのキー情報に使います。
 * Mapの値情報は、そのﾚｺｰﾄﾞの配列になります。
 * keys を指定しない場合は、最初のカラムの値が、キーになります。
 * キーが重複する場合、先に現れたデータが優先されます。
 *
 * 値の取出し方法は、キーに対して、{&#064;XXXX} 形式を、適用します。
 * Map に存在しないキーは、リクエスト変数や、通常のvalus変数を見ます。
 * valClm で、{&#064;XXXX} 形式で取り出す値のカラムを指定できます。
 * valClm を指定しない場合は、２番目のカラムを使用します。
 * 
 * 特殊機能
 *   ・holdTag属性：{&#064;XXXX} を、指定のタグで囲います。
 *     例えば、holdTag="span" とすると、&lt;span class="YYYYの値" &gt;XXXXの値&lt;/span&gt;
 *     という文字列を作成します。
 *   ・clsClms属性：先の指定のタグで囲う場合、そのタグのclass属性を指定できます。
 *     複数指定した場合は、スペースで、連結します。
 *   ・{&#064;XXXX cls="B"} とすると、個別の clsClms の値を使用せず、この値を、class属性として使います。
 *     clsClms と同様に、holsTag属性を指定しておく必要があります。
 *   ・tipsClms属性：先の指定のタグで囲う場合、そのタグのtitle属性を指定できます。
 *     マウスオーバーで、チップス表示されます。
 *   ・{&#064;XXXX tips="YYYY"} とすると、個別の tipsClms の値を使用せず、この値を、title属性として使います。
 *     tipsClms と同様に、holsTag属性を指定しておく必要があります。
 *   ・nnClms属性：この属性で指定された値が、nullの場合、{&#064;XXXX} の解析を行いません。
 *     正確に言うと、Mapに取り込みません。この場合、先のholdTag属性で指定したタグそのものも
 *     出力しません。
 *   ・{&#064;XXXX* str="val"} とすると、キーワードのあいまい検索部分に、strで指定した
 *     val文字列が存在する場合のみ有効とします。
 *   ・キーに対して、Mapは、行データ全部を持っています。{&#064;XXXX} は、最初のカラムの値です。
 *   ・２番目を取得する場合は、{&#064;XXXX 1}と、３番目は、{&#064;XXXX 2}と指定します。
 *   ・{&#064;XXXX*} を指定すると、キーのあいまい検索で、キーそのものを複数選択することが出来ます。
 *     この場合、spanタグで囲う機能と併用すると、複数のspanタグを持つ文字列を合成できます。
 *     この特定のタグは、holdTag 属性で指定します。
 *     このキーのあいまい検索で、表示する順番は、DBTableModelでMapに読み込んだ順番になります。
 *     {&#064;XXXX* 2} のような、取得カラムの指定も可能です。
 *     取得カラムの指定も可能ですが、カラム番号は、常に一番最後に記述してください。
 *   ・{&#064;XXXX!*} とすると、表示する順番を、逆順にすることが出来ます。取得カラムの指定も可能です。
 *   ・{&#064;$XXXX} とすると、holdTagも、clsClms も使用せず、設定値のみ出力します。
 *   ・{&#064;*XXXX!*} とすると、キーのあいまい指定の残り部分の文字列を出力します。連番の場合の番号を取り出せます。
 *     この場合は、固定値になるため、holsTagも、clsClms も使用しません。
 *   ・{&#064;^XXXX} とすると、request.getAttribute()の値を優先して使用します。{&#064;^XXXX*}などのあいまい指定も可能です。
 *     この場合、オリジナルのキーは、DBTableModel上に必要です。値の入れ替えのみ、行う感じです。
 *
 * ※ このタグは、Transaction タグの対象です。
 *
 * @og.formSample
 * ●形式：&lt;og:valueMap /&gt;
 * ●body：あり(EVAL_BODY_BUFFERED:BODYを評価し、{&#064;XXXX} を特殊な方法で解析します)
 *
 * ●Tag定義：
 *   &lt;og:valueMap
 *       keys               【TAG】パラメータから取り出すキーとなるカラム名を、CSV形式で指定します(初期値:最初のカラム)
 *       valClm             【TAG】パラメータから取り出す値のカラム名を指定します(初期値:２番目のカラム)
 *       selectedAll        【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:true)
 *       holdTag            【TAG】値の前後を、指定のタグで挟みます
 *       clsClms            【TAG】holdTagを使用するとき、そのタグの属性にclass属性を出力する場合のカラム名をCSV形式で指定します
 *       tipsClms           【TAG】holdTagを使用するとき、そのタグの属性にtitle属性を出力する場合のカラム名をCSV形式で指定します
 *       nnClms             【TAG】パラメータが NULL の時に、設定しないカラム名を、CSV形式で指定します
 *       separator          【TAG】キーとなるカラム名の値を連結する項目区切り文字をセットします(初期値:"_")
 *       tableId            【TAG】sessionから取得する DBTableModelオブジェクトの ID(初期値:HybsSystem.TBL_MDL_KEY)
 *       scope              【TAG】DBTableModelオブジェクトを取得する場合の、scope(初期値:session)
 *       xssCheck           【TAG】パラメータの HTMLTag開始/終了文字(&gt;&lt;) 存在チェックを実施するかどうか[true/false]を設定します (初期値:USE_XSS_CHECK[=true])
 *       caseKey            【TAG】このタグ自体を利用するかどうかの条件キーを指定します(初期値:null)
 *       caseVal            【TAG】このタグ自体を利用するかどうかの条件値を指定します(初期値:null)
 *       caseNN             【TAG】指定の値が、null/ゼロ文字列 でない場合(Not Null=NN)は、このタグは使用されます(初期値:判定しない)
 *       caseNull           【TAG】指定の値が、null/ゼロ文字列 の場合は、このタグは使用されます(初期値:判定しない)
 *       caseIf             【TAG】指定の値が、true/TRUE文字列の場合は、このタグは使用されます(初期値:判定しない)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   &gt;   ... Body ...
 *   &lt;/og:valueMap&gt;
 *
 * ●使用例
 * &lt;og:query command="{&#064;command}" debug="{&#064;debug}" maxRowCount="{&#064;maxRowCount}"&gt;
 *         select CLM,NAME_JA,LABEL_NAME,KBSAKU,SYSTEM_ID,LANG,
 *                 FGJ,USRSET,DYSET,USRUPD,DYUPD
 *         from GF41
 *     &lt;og:where&gt;
 *         &lt;og:and value = "SYSTEM_ID  =  '{&#064;SYSTEM_ID}'"  /&gt;
 *         &lt;og:and value = "LANG       =  '{&#064;LANG}'"       /&gt;
 *         &lt;og:and value = "CLM        like '{&#064;CLM}'"      /&gt;
 *         &lt;og:and value = "NAME_JA    like '{&#064;NAME_JA}'"  /&gt;
 *         &lt;og:and value = "LABEL_NAME like '{&#064;LABEL_NAME}'" /&gt;
 *         &lt;og:and value = "KBSAKU     =    '{&#064;KBSAKU}'"   /&gt;
 *     &lt;/og:where&gt;
 *     &lt;og:appear startKey = "order by" value = "{&#064;ORDER_BY}"
 *                 defaultVal = "SYSTEM_ID,CLM,LANG" /&gt;
 * &lt;/og:query&gt;
 *
 * &lt;og:valueMap keys="SYSTEM_ID,CLM" holdTag="div" separator="_" clsClms="LANG,FGJ" &gt;
 * {&#064;XX_AA0001} &lt;br /&gt;   SYSTEM_IDとCLMの値を、separatorで連結。値は、キーの次(LABEL_NAME)
 * {&#064;XX_AA0001 1} &lt;br /&gt; 行番号の２番目(上のSQLではNAME_JA)の値
 * {&#064;XX_AA0001 2} &lt;br /&gt; 行番号の３番目(上のSQLではLABEL_NAME)の値
 * 
 * {&#064;XX_AA001* 2} &lt;br /&gt; キーの前方一致する行の３番目の値
 * 
 * {&#064;XX_AA000!* 1} キーの前方一致する行の２番目の値を、逆順で表示
 * 
 * &lt;/og:valueMap&gt;
 *
 *  ・ キーは、select文の１番目のカラム
 *     &lt;og:og:valueMap &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *  ・ キーが複数で、ユニークになる。(keys)
 *     &lt;og:og:valueMap keys="SYSTEM_ID,CLM" &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *  ・ 値をdivタグで囲う。(holdTag)
 *     &lt;og:og:valueMap keys="SYSTEM_ID,CLM" holdTag="div" &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *  ・ キーの連結のセパレータを指定。(separator)
 *     &lt;og:og:valueMap keys="SYSTEM_ID,CLM" holdTag="div" separator="_" &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *  ・ 値をdivタグで囲う時に、クラス属性を追加します。(clsClms)
 *     &lt;og:og:valueMap keys="SYSTEM_ID,CLM" holdTag="div" separator="_" clsClms="LANG,FGJ" &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *  ・ 値をdivタグで囲う時に、チップス表示(title属性)を追加します。(tipsClms)
 *     &lt;og:og:valueMap keys="SYSTEM_ID,CLM" holdTag="div" separator="_" clsClms="LANG,FGJ" tipsClms="NAME_JA,LABEL_NAME" &gt;  ･･･フォーマット･･･ &lt;/og:valueMap&gt;
 *
 * @og.group その他部品
 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
 *
 * @version  6.7
 * @author   Kazuhiko Hasegawa
 * @since    JDK8.0,
 */
public class ValueMapTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.8.0.1 (2017/06/30)" ;
	private static final long serialVersionUID = 680120170630L ;

	private static final String CLS_KEY  = "cls=" ;		// 6.7.3.0 (2017/01/27) cls指定のｷｰﾜｰﾄﾞ
	private static final String TIPS_KEY = "tips=" ;	// 6.7.3.0 (2017/01/27) tips指定のｷｰﾜｰﾄﾞ
	private static final String STR_KEY  = "str=" ;		// 6.8.0.1 (2017/06/30) str指定のｷｰﾜｰﾄﾞ

	private static final String NONE1	= "<style type=\"text/css\">." ;	// 6.7.8.0 (2017/04/21)
	private static final String NONE2	= " { display : none ;} </style>" ;	// 6.7.8.0 (2017/04/21)

	private DBTableModel table		;

	private String		tableId		= HybsSystem.TBL_MDL_KEY;
	private boolean		selectedAll	= true;
	private String		keys		;
	private String		valClm		;				// 6.7.2.0 (2017/01/16)
	private String		nnClms		;				// このカラムの値が、nullのレコードは、使用しません。
	private String		holdTag		;				// nullの場合は、なにもはさまない。
	private String		clsClms		;				// holdTagで指定したタグの属性に、class属性を追加します。
	private String		tipsClms	;				// 6.7.3.0 (2017/01/27) holdTagで指定したタグの属性に、title属性を追加します。
	private String		scope		= "session";	// "request","session"
	private String		separator	= "_";  		// 項目区切り文字
	private boolean		xssCheck	= HybsSystem.sysBool( "USE_XSS_CHECK" ); // 5.1.7.0 (2010/06/01) XSS対策

	private int[]		clsClmsNo	;				// clsClmsが指定されない場合は、長さゼロの配列
	private int[]		tipsClmsNo	;				// 6.7.3.0 (2017/01/27) tipsClmsが指定されない場合は、長さゼロの配列
	private int			valClmNo	= 1;			// valClmが指定されない場合は、２番目のカラム(=1)の値を使用します。

	private String		body		;				// パラメータ処理済みのBODY部分の文字列
	private String		restChangeKey	;			// ValueMapParamTag で置き換え処理を行うキーワード
	private boolean		useNoneClsKey	;			// mapObj の残り処理をおこなうかどうか(true:行う)

	// synchronizedMap にする必要性があるのか無いのか、よく判っていません。
	/** Collections.synchronizedMap で、同期します。テーブルの行の取得順に、Mapに追加していきます。(先に登録したデータが有効) */
	private final Map<String,String[]> mapObj = Collections.synchronizedMap( new LinkedHashMap<>() );

	// ValueMapParamTagから受け取った、各種設定情報を、管理するMapオブジェクト。
	private final ConcurrentMap<VMP_KEYS,String> paramMap = new ConcurrentHashMap<>();

	// ValueMapParamTag で使用する、未使用のキーワードを管理するMap
	/** Collections.synchronizedMap で、同期します。テーブルの行の取得順に、Mapに追加していきます。(先に登録したデータが有効) */
	private final Map<String,String[]> restMap = Collections.synchronizedMap( new LinkedHashMap<>() );

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 */
	public ValueMapTag() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doStartTag() {
		if( useTag() ) {
			useXssCheck( xssCheck );
			table = (DBTableModel)getObject( tableId );
			if( table != null && table.getRowCount() > 0 && table.getColumnCount() > 0 ) {
				makeMapObj( table );		// Body の評価前にMapを作成する必要がある。

				return EVAL_BODY_BUFFERED ;	// Body を評価する
			}
		}
		return SKIP_BODY ;					// Body を評価しない
	}

	/**
	 * Taglibのタグ本体を処理する doAfterBody() を オーバーライドします。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @return	後続処理の指示(SKIP_BODY)
	 */
	@Override
	public int doAfterBody() {
		body = getBodyString();

		return SKIP_BODY ;
	}

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();		// 4.0.0 (2005/02/28)
		if( useTag() && body != null ) {
			jspPrint( body );
		}

		// mapObj の残り処理が必要かどうか。mapObj が空で、NONE_CLS_KEY が存在する場合は、残を隠す。
		if( useNoneClsKey ) {
			final String noneClassKey = paramMap.get( VMP_KEYS.NONE_CLS_KEY );
			jspPrint( NONE1 + noneClassKey + NONE2 );
		}

		return EVAL_PAGE ;
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) valClm 追加
	 * @og.rev 6.7.3.0 (2017/01/27) tipsClms 追加
	 * @og.rev 6.7.8.0 (2017/04/21) valueMapParam関連
	 */
	@Override
	protected void release2() {
		super.release2();
		table		= null;
		tableId		= HybsSystem.TBL_MDL_KEY;
		selectedAll	= true;
		keys		= null;
		valClm		= null;	 		// 6.7.2.0 (2017/01/16) 新規作成
		nnClms		= null;			// 6.7.2.0 (2017/01/16) 名称変更
		holdTag		= null;
		clsClms		= null;			// 6.7.3.0 (2017/01/27) 追加
		tipsClms	= null;			// 6.7.2.0 (2017/01/16) 名称変更
		scope		= "session";	// DBTableModel の取得先のscope
		separator	= "_";
		xssCheck	= HybsSystem.sysBool( "USE_XSS_CHECK" );	// 5.1.7.0 (2010/06/01) XSS解除対応
		body		= null;
		mapObj.clear();

		clsClmsNo	= null;			// clsClmsが指定されない場合は、長さゼロの配列
		tipsClmsNo	= null;			// 6.7.3.0 (2017/01/27) tipsClmsが指定されない場合は、長さゼロの配列
		valClmNo	= 1;			// valClmが指定されない場合は、２番目のカラム(=1)の値を使用します。

		paramMap.clear();			// 6.7.8.0 (2017/04/21) valueMapParam関連
		restMap.clear();			// 6.7.8.0 (2017/04/21) valueMapParam関連
		restChangeKey	= null ;	// ValueMapParamTag で置き換え処理を行うキーワード
		useNoneClsKey	= false	;	// mapObj の残り処理をおこなうかどうか(true:行う)
	}

	/**
	 * 指定のスコープの内部キャッシュ情報に、DBTableModel の選択された値を登録します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) valClm 追加
	 * @og.rev 6.7.3.0 (2017/01/27) tipsClms 追加
	 * @og.rev 6.7.8.0 (2017/04/21) valueMapParam関連
	 *
	 * @param table		DBTableModelオブジェクト
	 */
	private void makeMapObj( final DBTableModel table ) {
		final int[] rowNo = getParameterRows();
		if( rowNo.length == 0 ) { return; }

		final int[] keysNo		= getClmNos( table,keys    , 0 );	// keysが指定されない場合は、先頭カラムを使用します。
		final int[] nnClmsNo	= getClmNos( table,nnClms  ,-1 );	// nnClmsが指定されない場合は、長さゼロの配列
					clsClmsNo	= getClmNos( table,clsClms ,-1 );	// clsClmsが指定されない場合は、長さゼロの配列
					tipsClmsNo	= getClmNos( table,tipsClms,-1 );	// tipsClmsが指定されない場合は、長さゼロの配列

		for( int j=0; j<rowNo.length; j++ ) {
			final String[] rowData = table.getValues( j );

			// まず、nullチェックして、対象行かどうかを判定する。
			if( isNotNullCheck( rowData , nnClmsNo ) ) {
				// Map に登録するキーを連結して作成します。
				final String mapkey = getAppendKeys( rowData , keysNo , separator );
				mapObj.computeIfAbsent( mapkey, k -> rowData );		// まだ値に関連付けられていない場合、追加します。(先に登録したデータが有効)
		//		mapObj.put( mapkey, rowData );						// 後で登録したデータが、有効になります。
			}
		}
		restMap.putAll( mapObj );		// 6.7.8.0 (2017/04/21) 一旦、すべてのMapをコピーします。

		// valClmが指定されない場合は、２番目のカラム(=1)の値を使用します。
		valClmNo = valClm == null || valClm.isEmpty() ? 1 : table.getColumnNo( valClm.trim() );	// 存在しない場合は、Exception
	}

	/**
	 * カラム名のCSV文字列を、DBTableModel の列番号の配列に変換します。
	 *
	 * カラム名のCSV文字列が、無指定の場合、no で指定するカラム番号を
	 * デフォルトとして使用します。no が、マイナスの場合は、長さゼロの
	 * 配列を返します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) カラム番号の取り方を変更
	 *
	 * @param	table		DBTableModelオブジェクト
	 * @param	clms		カラム名のCSV文字列( nullではない )
	 * @param	no			clmsが、nullか、空文字の場合の、カラム番号
	 * @return	カラム名の列番号の配列
	 */
	private int[] getClmNos( final DBTableModel table , final String clms , final int no ) {
		final int[] clmNo ;
		if( clms == null || clms.isEmpty() ) {
			if( no < 0 ) { clmNo = new int[0]; }			// 長さゼロの配列
			else         { clmNo = new int[] { no }; }		// 指定のカラム番号を持つ配列。
		}
		else {
			final String[] clmAry = clms.split( "," );
			clmNo = new int[clmAry.length];
			for( int i=0; i<clmAry.length; i++ ) {
				clmNo[i] = table.getColumnNo( clmAry[i].trim() );	// 存在しない場合は、Exception
			}
		}

		return clmNo;
	}

	/**
	 * 指定のカラムの値のすべてが、nullか、空文字列でない場合は、true を返します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	rowData	行データ
	 * @param	clmNo	カラム番号配列
	 * @return	nullか、空文字列でない場合は、true
	 */
	private boolean isNotNullCheck( final String[] rowData , final int[] clmNo ) {
		boolean rtn = true;						// カラムがない場合は、true になります。

		for( int i=0; i<clmNo.length; i++ ) {
			final String val = rowData[ clmNo[i] ];
			if( val == null || val.isEmpty() ) {
				rtn = false;
				break;
			}
		}
		return rtn;
	}

	/**
	 * Mapのキーとなるキーカラムの値を連結した値を返します。
	 *
	 * @param	rowData	行データ
	 * @param	clmNo	カラム番号配列
	 * @param	sep		結合させる文字列
	 *
	 * @return	Mapのキーとなるキーカラムの値を連結した値
	 * @og.rtnNotNull
	 */
	private String getAppendKeys( final String[] rowData , final int[] clmNo , final String sep ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		if( clmNo.length > 0 ) {
			buf.append( rowData[ clmNo[0] ] );				// 最初のひとつ目
			for( int i=1; i<clmNo.length; i++ ) {
				final String val = rowData[ clmNo[i] ];
				if( val != null && !val.isEmpty() ) {
					buf.append( sep ).append( val );
				}
			}
		}
		return buf.toString().trim();
	}

	/**
	 * Mapの値となる値カラムに対応する文字列配列を返します。
	 *
	 * ここでは、行データに対して、配列の添え字(カラム番号)を元に、値を求めます。
	 * その際、holdTag や、clsClms で指定したクラス名などの付加情報もセットします。
	 * さらに、{&#064;$XXXX} などの、holdTagの抑止(生データを返す) 処理を行います。
	 *
	 * @param	rowData		行の配列データ
	 * @param	val			値データ
	 * @param	isNormal	holdTagを使用せず、ノーマル状態の値を出力するかどうか[true:ノーマルの値]
	 * @param	cls			clsClmsNoの使用を抑止し、指定の値を、class属性にセットします。(nullはclsClmsNoを使用、isEmpty() は、classの削除、それ以外は置き換え)
	 * @param	tips		tipsClmsNoの使用を抑止し、指定の値を、title属性にセットします。(nullはtipsClmsNoを使用、isEmpty() は、titleの削除、それ以外は置き換え)
	 * @param	sufix		キーのあいまい指定時に、あいまいキー以降の文字列を指定します。あれば、その値を使用します。
	 *
	 * @og.rev 6.7.3.0 (2017/01/27) tips 追加
	 * @og.rev 6.7.6.1 (2017/03/17) 値データを渡すようにします(isAttVal の追加対応)。
	 *
	 * @return	Mapのキーに対応する修飾した値
	 * @og.rtnNotNull
	 */
	private String getMapVals( final String[] rowData , final String val , final boolean isNormal , final String cls , final String tips , final String sufix ) {
		String rtnVal = sufix == null || sufix.isEmpty() ? val : sufix ;

		if( !isNormal && holdTag != null && !holdTag.isEmpty() ) {
			// 毎行ごとに、class属性の値は異なる。
			final String clazz = cls  == null ? getAppendKeys( rowData,clsClmsNo ," " ) : cls ;		// class 属性は、スペースで連結
			final String title = tips == null ? getAppendKeys( rowData,tipsClmsNo," " ) : tips ;	// title 属性は、スペースで連結

			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
			buf.append( '<' ).append( holdTag );
			if( !clazz.isEmpty() ) { buf.append( " class=\"" ).append( clazz ).append( '"' ); }
			if( !title.isEmpty() ) { buf.append( " title=\"" ).append( title ).append( '"' ); }
			buf.append( '>' ).append( rtnVal ).append( "</" ).append( holdTag ).append( '>' );

			rtnVal = buf.toString();
		}
		return rtnVal ;
	}

	/**
	 * リクエスト情報の文字列を取得します。
	 *
	 * これは、CommonTagSupportの#getRequestValue( String ) を
	 * オーバーライドして、Mapから、設定値を取得します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) valClm 追加
	 * @og.rev 6.7.3.0 (2017/01/27) tips 追加
	 * @og.rev 6.7.6.1 (2017/03/17) 値データを渡すようにします(isAttVal の追加対応)。
	 * @og.rev 6.7.8.0 (2017/04/21) valueMapParam関連
	 * @og.rev 6.8.0.1 (2017/06/30) str指定のｷｰﾜｰﾄﾞ
	 *
	 * @param    key キー
	 *
	 * @return   リクエスト情報の文字列
	 * @see		CommonTagSupport#getRequestValue( String )
	 */
	@Override
	protected String getRequestValue( final String key ) {
		if( key.equals( restChangeKey ) ) { return makeRestValue(); }		// 6.7.8.0 (2017/04/21)

		// {@!XXXX} や、{@*XXXX!*} の場合のキー対応。最初に行う。
		final char ch1 = key.charAt(0);
		final boolean isNormal = ch1 == '$' ;		// holdTag を使わず、値そのものを出します。
		final boolean isSufix  = ch1 == '*' ;		// あいまい検索時に、あいまいで削除された部分文字列を使うかどうか。
		final boolean isAttVal = ch1 == '^' ;		// 値を、request.getAttribute()の値を優先して使用します。

		final StringBuilder keyBuf = new StringBuilder( key.trim() );

		if( isNormal || isSufix || isAttVal ) { keyBuf.deleteCharAt( 0 ); }		// 先頭の文字を削除

		// カラム番号の取得のための分割。存在する場合は、必ず一番最後にします。
		final int ad1  = keyBuf.lastIndexOf( " " );		// 後ろから検索して、スペースで分割
		int       vcNo = valClmNo;
		if( ad1 > 0 ) {
			// 必要かどうかはともかく、NumberFormatException で、判定すると、遅くなる気がする。
			final char ch = keyBuf.charAt( ad1 + 1 );	// 数字であろう先頭文字
			if( '0' <= ch && ch <= '9' ) {
				try {
					vcNo   = Integer.parseInt( keyBuf.substring( ad1 + 1 ) );
					keyBuf.setLength( ad1 );									// ｽﾍﾟｰｽが残っている可能性がある
				}
				catch( NumberFormatException ex ) {		// 数値変換失敗時は、普通のパラメータだった場合。
	//				vcNo   = valClmNo;		// vcNo は、セットする前にException が発生している。
	//				mapkey = key;			// mapkey は、スペースも含むすべてのキーになる。･････ NumberFormatException が先なので、setLength されていない。
				}
			}
		}

		// cls="B" 属性の取得
		final String cls = getExtParam( keyBuf,CLS_KEY );		// 6.8.0.1 (2017/06/30) 関数化
	//	String cls = null;
	//	final int ad2 = keyBuf.lastIndexOf( CLS_KEY );			// = の前後にｽﾍﾟｰｽは入れてはいけない。
	//	if( ad2 > 0 ) {
	//		final int st = ad2 + CLS_KEY.length() ;
	//		// cls="B" や、cls='C' のように、文字列指定されているはずなので、その中身を削除します。
	//		final String qot = keyBuf.substring( st,st+1 );		// １文字取り出す。
	//		final int    ed  = keyBuf.indexOf(  qot,st+1 );		// 対になる後ろのクオートの位置を見つける。
	//		if( ed >= 0 ) {
	//			cls = keyBuf.substring( st+1 , ed );			// 前後のクオートは、含まない。
	//			keyBuf.delete( ad2 , ed+1 ) ;					// 間を抜く
	//		}
	//		else {
	//			// 文法間違い。どうするか？
	//			System.err.println( "cls指定の文法が間違っています。key=" + key );
	//		}
	//	}

		// 6.7.3.0 (2017/01/27) tips="YYYY" 属性の取得
		final String tips = getExtParam( keyBuf,TIPS_KEY );		// 6.8.0.1 (2017/06/30) 関数化
	//	String tips = null;
	//	final int ad4 = keyBuf.lastIndexOf( TIPS_KEY );			// = の前後にｽﾍﾟｰｽは入れてはいけない。
	//	if( ad4 > 0 ) {
	//		final int st = ad4 + TIPS_KEY.length() ;
	//		// tips="YYYY" や、tips='YYYY' のように、文字列指定されているはずなので、その中身を削除します。
	//		final String qot = keyBuf.substring( st,st+1 );		// １文字取り出す。
	//		final int    ed  = keyBuf.indexOf(  qot,st+1 );		// 対になる後ろのクオートの位置を見つける。
	//		if( ed >= 0 ) {
	//			tips = keyBuf.substring( st+1 , ed );			// 前後のクオートは、含まない。
	//			keyBuf.delete( ad4 , ed+1 ) ;					// 間を抜く
	//		}
	//		else {
	//			// 文法間違い。どうするか？
	//			System.err.println( "tips指定の文法が間違っています。key=" + key );
	//		}
	//	}

		// 6.8.0.1 (2017/06/30) str指定のｷｰﾜｰﾄﾞ。先に取り除かないと、type 判定時の endsWith が効かない。
		final String instr = getExtParam( keyBuf,STR_KEY );

		// 中途半端に、ｽﾍﾟｰｽが残っていると厄介なので、削除しておきます。
		final String mapkey = keyBuf.toString().trim();

		// type==0 は、ｵﾘｼﾞﾅﾙ。それ以外は、キーから取り除く文字数
		final int type = mapkey.endsWith( "!*" ) ? 2 : mapkey.endsWith( "*" ) ? 1 : 0 ;

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		if( type == 0 ) {
			final String[] rowData = mapObj.get( mapkey );
			restMap.remove( mapkey );							// 6.7.8.0 (2017/04/21) valueMapParam関連

			if( rowData != null && vcNo < rowData.length ) {
				// isAttVal == true のときは、RequestAttribute から取り出すが、キーは、大文字になっているので注意。
				final String val = isAttVal ? nval( (String)getRequestAttribute( mapkey.toUpperCase(Locale.JAPAN) ) , rowData[vcNo] ) : rowData[vcNo] ;

				buf.append( getMapVals( rowData , val , isNormal , cls , tips , null ) );
			}
			else {
				buf.append( super.getRequestValue( key , xssCheck ) );				// 添字も合わせて、上位に問い合わせる。
			}
		}
		else {
			final String subKey = mapkey.substring( 0,mapkey.length()-type );		// ほんとは、keyBuf で処理したかった。
			final List<String> list = new ArrayList<>();
			for( final Map.Entry<String,String[]> entry : mapObj.entrySet() ) {		// {@XXXX}を見つける都度、全Mapをスキャンしているので、非効率
				final String mapkey2 = entry.getKey();
				// 6.8.0.1 (2017/06/30) str指定のｷｰﾜｰﾄﾞ
				if( mapkey2.startsWith( subKey ) && ( instr == null || mapkey2.contains( instr ) ) ) {
					final String[] rowData = entry.getValue();
					if( rowData != null && vcNo < rowData.length ) {
						final String sufix = isSufix ? mapkey2.substring( subKey.length() ) : null ;	// あいまいキーの残りの文字列
						// isAttVal == true のときは、RequestAttribute から取り出すが、キーは、大文字になっているので注意。
						final String val   = isAttVal ? nval( (String)getRequestAttribute( mapkey2.toUpperCase(Locale.JAPAN) ) , rowData[vcNo] ) : rowData[vcNo] ;
						list.add( getMapVals( rowData , val , isNormal , cls , tips , sufix ) );
						restMap.remove( mapkey2 );			// 6.7.8.0 (2017/04/21) valueMapParam関連
					}
				}
			}
			if( type == 2 ) { Collections.reverse( list ); }	// 逆順
			list.forEach( v -> buf.append( v ) );
		}

		return buf.toString();
	}

	/**
	 * 指定の文字列バッファから、キーワードのパラメータを取り出します。
	 *
	 * 元の文字列バッファは、そのキーワード部分を削除し、パラメータの値は、RETURNで
	 * 返します。存在しない場合は、null を返します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	keyBuf 文字列バッファ
	 * @param	key キーワード(cls,tips,str)
	 * @return	パラメータ
	 */
	private String getExtParam( final StringBuilder keyBuf , final String key ) {
		// key="XXX" 属性の取得
		String rtn = null;
		final int ad2 = keyBuf.lastIndexOf( key );				// = の前後にｽﾍﾟｰｽは入れてはいけない。
		if( ad2 > 0 ) {
			final int st = ad2 + key.length() ;
			// cls="B" や、cls='C' のように、文字列指定されているはずなので、その中身を削除します。
			final String qot = keyBuf.substring( st,st+1 );		// １文字取り出す。
			final int    ed  = keyBuf.indexOf(  qot,st+1 );		// 対になる後ろのクオートの位置を見つける。
			if( ed >= 0 ) {
				rtn = keyBuf.substring( st+1 , ed );			// 前後のクオートは、含まない。
				keyBuf.delete( ad2 , ed+1 ) ;					// 間を抜く
			}
			else {
				// 文法間違い。どうするか？
				System.err.println( "指定の文法が間違っています。key=" + key );
			}
		}

		return rtn;
	}

	/**
	 * 表示データの HybsSystem.ROW_SEL_KEY を元に、選ばれた 行を処理の対象とします。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @return	選択行の配列
	 * @og.rtnNotNull
	 */
	@Override
	protected int[] getParameterRows() {
		final int[] rowNo ;
		if( selectedAll ) {
			final int rowCnt = table.getRowCount();
			rowNo = new int[ rowCnt ];
			for( int i=0; i<rowCnt; i++ ) {
				rowNo[i] = i;
			}
		} else {
			rowNo = super.getParameterRows();
		}
		return rowNo ;
	}

	/**
	 * ValueMapParamTagで設定された各種パラメータ を受け取ります。
	 *
	 * @og.rev 6.7.8.0 (2017/04/21) ValueMapParamTag のパラメータを追加します。
	 *
	 * @param	pMap ValueMapParamTagで設定された各種パラメータ
	 */
	protected void setParam( final ConcurrentMap<VMP_KEYS,String> pMap ) {
		paramMap.putAll( pMap );				// ValueMapParamTag と分けるために、内容をコピーします。

		restChangeKey = paramMap.get( VMP_KEYS.REST_CHANGE_KEY );
	}

	/**
	 * ValueMapParamTagで設定された各種パラメータを元に、残カラムを処理します。
	 *
	 * 処理としては、{&#064;XXXX} の XXXX 部分を、valueMap の未使用キーに変換します。
	 * その後、通常のパラメータ処理を行います。
	 * REST_MARK_CLM が指定されている場合は、DBTableModel に対して、マーク処理を行います。
	 *
	 * @og.rev 6.7.8.0 (2017/04/21) ValueMapParamTag のパラメータを追加します。
	 *
	 * @return	残カラム処理の結果
	 */
	private String makeRestValue() {
		// 先にDBTableModel に対して、マーク処理を行います。
		// パラメータ処理を行うと、キーワードによっては、restMap から値が削除されるためです。
		final String markClm = paramMap.get( VMP_KEYS.REST_MARK_CLM );
		final String markVal = paramMap.get( VMP_KEYS.REST_MARK_VAL );
		if( !restMap.isEmpty() && markClm != null && markVal != null ) {
			final int   clmNo = table.getColumnNo( markClm , false );	// ifの階層が深くなるのが嫌なので、まとめてチェックします。
			final int[] rowNo = getParameterRows();
			if( clmNo >=0 && rowNo.length > 0 ) {
				final int[] keysNo = getClmNos( table,keys , 0 );		// keysが指定されない場合は、先頭カラムを使用します。

				for( int row=0; row<rowNo.length; row++ ) {
					final String[] rowData = table.getValues( row );

					// Map に登録されているキーを連結して作成します。
					final String mapkey = getAppendKeys( rowData , keysNo , separator );

					if( restMap.containsKey( mapkey ) ) {		// 残っている場合
						table.setValueAt( markVal , row , clmNo );
					}
				}
			}
		}

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		if( restMap.isEmpty() ) {
			// restMap の残り処理が必要かどうか。restMap が空で、NONE_CLS_KEY が存在する場合は、残を隠す。
			useNoneClsKey = paramMap.containsKey( VMP_KEYS.NONE_CLS_KEY );
		}
		else {
			final String changeVal = paramMap.get( VMP_KEYS.BODY_VAL );
			// パラメータ処理を行うと、キーワードによっては、restMap から値が削除されるためです。
			final String[] restKeys = restMap.keySet().toArray( new String[restMap.size()] );

			for( final String key : restKeys ) {
				final String repStr = changeVal.replaceAll( "XXXX" , key );
				buf.append( getRequestParameter( repStr ) );
			}
		}

		return buf.toString();
	}

	/**
	 * 【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * 全てのデータを選択済みデータとして扱って処理します。
	 * 全件処理する場合に、(true/false)を指定します。
	 * 初期値は false です。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param  all 選択済み処理可否 [true:全件選択済み/false:通常]
	 */
	public void setSelectedAll( final String all ) {
		selectedAll = nval( getRequestParameter( all ),selectedAll );
	}

	/**
	 * 【TAG】(通常は使いません)結果のDBTableModelを、sessionに登録するときのキーを指定します
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @og.tag
	 * 検索結果より、DBTableModelオブジェクトを作成します。これを、下流のviewタグ等に
	 * 渡す場合に、通常は、session を利用します。その場合の登録キーです。
	 * query タグを同時に実行して、結果を求める場合、同一メモリに配置される為、
	 * この tableId 属性を利用して、メモリ空間を分けます。
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	id テーブルID (sessionに登録する時のID)
	 */
	public void setTableId( final String id ) {
		tableId   = nval( getRequestParameter( id ),tableId );
	}

	/**
	 * 【TAG】パラメータ に登録するキーをセットします。
	 *
	 * @og.tag keysが指定されない場合は、先頭カラムを使用します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	key1 登録キー
	 */
	public void setKeys( final String key1 ) {
		keys = nval( getRequestParameter( key1 ),keys ) ;
	}

	/**
	 * 【TAG】パラメータ から取り出す値カラムを指定ます。
	 *
	 * @og.tag valClmが指定されない場合は、２番目のカラムを使用します。
	 *
	 * @og.rev 6.7.2.0 (2017/01/16) 新規作成
	 *
	 * @param	clm 取り出す値カラム
	 */
	public void setValClm( final String clm ) {
		valClm = nval( getRequestParameter( clm ),valClm ) ;
	}

	/**
	 * 【TAG】パラメータが NULL の時に、設定しないカラム名を、CSV形式で指定します。
	 *
	 * @og.tag 
	 *  nnClms属性：この属性で指定された値が、nullの場合、{&#064;XXXX} の解析を行いません。
	 *  正確に言うと、Mapに取り込みません。この場合、先のholdTag属性で指定したタグそのものも
	 *  出力しません。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) 名称変更
	 *
	 * @param	clms NULL の時に、設定しないカラム名を、CSV形式で指定
	 */
	public void setNnClms( final String clms ) {
		nnClms = nval( getRequestParameter( clms ),nnClms );
	}

	/**
	 * 【TAG】値の前後を、挟むタグを指定します。
	 *
	 * @og.tag
	 *  holdTag属性：{&#064;XXXX} を、指定のタグで囲います。
	 *  例えば、holdTag="span" とすると、&lt;span class="YYYYの値" &gt;XXXXの値&lt;/span&gt;
	 *  という文字列を作成します。
	 *  clsClms 属性や、{&#064;XXXX cls="B"} を使用する場合は、holdTag 属性の指定が必要です。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	tag 値の前後を挟むタグ
	 * @see		#setClsClms( String )
	 */
	public void setHoldTag( final String tag ) {
		holdTag = nval( getRequestParameter( tag ),holdTag );
	}

	/**
	 * 【TAG】holdTagを使用するとき、そのタグの属性にclass属性を出力する場合のカラム名をCSV形式で指定します。
	 *
	 * @og.tag 
	 *   clsClms属性：先の指定のタグで囲う場合、そのタグのclass属性を指定できます。
	 *   複数指定した場合は、スペースで、連結します。
	 *   一括指定ではなく、個別に指定する場合は、{&#064;XXXX cls="B"} 構文を使用します。
	 *   holdTag属性が設定されていない場合は、どちらも無視されます。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 * @og.rev 6.7.2.0 (2017/01/16) 名称変更
	 *
	 * @param	clms class属性を出力する場合のカラム名を、CSV形式で指定
	 * @see		#setHoldTag( String )
	 */
	public void setClsClms( final String clms ) {
		clsClms = nval( getRequestParameter( clms ),clsClms );
	}

	/**
	 * 【TAG】holdTagを使用するとき、そのタグの属性にtitle属性を出力する場合のカラム名をCSV形式で指定します。
	 *
	 * @og.tag 
	 *   tipsClms属性：先の指定のタグで囲う場合、そのタグのtitle属性を指定できます。
	 *   複数指定した場合は、スペースで、連結します。
	 *   一括指定ではなく、個別に指定する場合は、{&#064;XXXX tips="YYYY"} 構文を使用します。
	 *   holdTag属性が設定されていない場合は、どちらも無視されます。
	 *
	 * @og.rev 6.7.3.0 (2017/01/16) 名称変更
	 *
	 * @param	clms title属性を出力する場合のカラム名を、CSV形式で指定
	 * @see		#setHoldTag( String )
	 */
	public void setTipsClms( final String clms ) {
		tipsClms = nval( getRequestParameter( clms ),tipsClms );
	}

	/**
	 * 【TAG】キーとなるカラム名の値を連結する項目区切り文字をセットします(初期値:"_")。
	 *
	 * @og.tag
	 * keysで、複数のキーの値を連結して、Mapのキーにしますが、そのときの連結文字列を指定します。
	 * 初期値は、"_" に設定されています。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param   sepa 連結文字列 (初期値:"_")
	 */
	public void setSeparator( final String sepa ) {
		separator = nval( getRequestParameter( sepa ),separator );
	}

	/**
	 * 【TAG】パラメータの HTMLTag開始/終了文字(&gt;&lt;) 存在チェックを実施するかどうか[true/false]を設定します
	 *		(初期値:USE_XSS_CHECK[={@og.value SystemData#USE_XSS_CHECK}])。
	 *
	 * @og.tag
	 * クロスサイトスクリプティング(XSS)対策の一環としてless/greater than signについてのチェックを行います。
	 * (&gt;&lt;) が含まれていたエラーにする(true)／かノーチェックか(false)を指定します。
	 * (初期値:システム定数のUSE_XSS_CHECK[={@og.value SystemData#USE_XSS_CHECK}])。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @param	flag	XSSチェック [true:する/false:しない]
	 * @see		org.opengion.hayabusa.common.SystemData#USE_XSS_CHECK
	 */
	public void setXssCheck( final String flag ) {
		xssCheck = nval( getRequestParameter( flag ),xssCheck );
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @og.rev 6.7.1.0 (2017/01/05) 新規作成
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		final String rtnStr = org.opengion.fukurou.util.ToString.title( this.getClass().getName() )
				.println( "VERSION"			,VERSION		)
				.println( "tableId"			,tableId		)
				.println( "selectedAll"		,selectedAll	)
				.println( "keys"			,keys			)
				.println( "holdTag"			,holdTag		)
				.println( "clsClms"			,clsClms		)
				.println( "tipsClms"		,tipsClms		)
				.println( "valClm"			,valClm			)
				.println( "nnClms"			,nnClms			)
				.println( "scope"			,scope			)
				.println( "separator"		,separator		)
				.println( "xssCheck"		,xssCheck		)
				.println( "Other..."	,getAttributes().getAttribute() )
				.fixForm().toString() ;

		return rtnStr.toString();
	}
}
