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

import java.util.List;
import java.util.ArrayList;

import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.html.ViewTimeTableParam;

/**
 * 時間軸を持つタイムテーブルの表示を行うクラスです。
 *
 * パラメータが必要な場合は、ViewTimeTableParamTag を使用してください。
 *
 * パラメータが設定されていない場合は、ViewForm_HTMLTimeTable の初期値が使用されます。
 * (パラメータを使用するには、viewタグのuseParam 属性をtrueに設定する必要があります。)
 *
 * SELECT文は、日付、キー、備考、開始時刻、終了時刻、リンクが、必須項目で、この並び順は、
 * 完全に固定です。よって、カラム位置を指定する必要はありませんが、SELECT文を自由に
 * 設定することも出来ませんので、ご注意ください。
 * この固定化に伴い、WRITABLE 指定も使用できません。
 * なお、日付、キー、備考 に関しては、columnDisplay 属性で、表示の ON/OFF 制御は可能です。
 * また、日付ブレイク、キーブレイクの設定で、カラム自体をテーブルの外に出すことが可能です。
 * (キーと備考はセットになっています。)
 *
 * タイムテーブルが空きの場合のリンクを指定できます。(ViewTimeTableParam.NULL_LINK_CLM_ID)
 * (ViewTimeTableParam の nullLinkColumn 属性)
 * 指定しない場合は、空きのリンクは作成されません。
 * このリンクは、特殊で、引数に、パラメータを追加できますが、"($1)"、"($2)" で指定します。
 * この($1)、($2)は、開始時刻、終了時刻がセットされますが、SELECT文の固定カラムと同じ
 * 並び順ですが、DBTableModelの値を設定しているわけではありません。
 * 空きの場合は、データ自体が存在しない場合と、日付、キー のみが 外部結合で生成された
 * レコードが実際に存在する場合がありますが、外部結合で生成されたレコードには、
 * 開始時刻、終了時刻はありません。($1) と($2)には、それぞれ、最小開始時刻と最大終了時刻を
 * セットします。
 *
 * 例として、&amp;TMSTART=($1)&amp;TMEND=($2) という文字列の ($*) 部分を解析して割当ます。
 *
 * ブレーク処理を行うカラムＩＤをCSV形式でセットできます。(ViewTimeTableParam.BREAK_CLMS)
 * (ViewTimeTableParam の breakClms 属性)
 * これは、ブレイク毎にテーブルが分かれて、テーブルの先頭に、ブレイクした
 * 値が表示されます。
 * 例えば、日付カラムをブレイクカラムとして設定すると、日付がブレイクするたび、
 * 日付をヘッダーに出して、テーブルを作成します。
 * ブレークカラムは、CSV形式で複数指定できます。その場合は、複数指定のカラムの
 * 合成された値で、キーブレイクの判定を行います。(簡単に言うとＯＲ判定になります。)
 * なお、ブレイクカラムを指定した場合は、自動的に、noDisplay 属性にその値をセット
 * します。
 *
 * @og.rev 5.4.0.0 (2011/10/01) 新規追加
 * @og.group 画面表示
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ViewForm_HTMLTimeTable extends ViewForm_HTMLTable {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "7.0.1.0 (2018/10/15)" ;

	private int		intval		= 30;			// 30分 単位に colspan を設定する。
	private int		minStTime	= 480;			// 08:00 のこと。8H=480M
	private int		maxEdTime	= 1260;			// 21:00 のこと。21H=1260M

	// 6.4.1.1 (2016/01/16) dyClmNo → DY_CLMNO , keyClmNo → KEY_CLMNO , tmstClmNo → TMST_CLMNO , tmedClmNo → TMED_CLMNO , linkClmNo → LINK_CLMNO , clmCnt → CLM_CNT refactoring
	private static final int	DY_CLMNO		= 0;	// ヘッダー1：ただし、グループ化する場合は、外出し
	private static final int	KEY_CLMNO	= 1;	// ヘッダー2：ただし、グループ化する場合は、外出し
	// private static final int	bikoClmNo	= 2;	// ヘッダー3：(内部では使用していません。)

	private static final int	TMST_CLMNO	= 3;	// 時間枠の開始時刻(含む)
	private static final int	TMED_CLMNO	= 4;	// 時間枠の終了時刻(含まない)
	private static final int	LINK_CLMNO	= 5;	// 時間枠に表示する予約情報(変更画面へのリンク)

	private static final int	CLM_CNT		= 3;	// 決め打ち。今は、dyClm,keyClm の２つだけ表示

	// 引数パース機能付き データが存在しない場合のリンクのベースを設定。
	private int		nullLinkClmNo	= -1 ;		// 固定ではなく可変の場合に利用するリンクカラムNo

	private int		tdClassColumnNo	= -1 ;		// 5.4.3.7 (2012/01/20) データを入れるTDタグにclass属性を付与する場合のカラムNo

	private boolean isDyBreak		= true;		// 日付でブレイクするかどうかを指定
	private boolean isBookingMerge	;			// 5.4.4.2 (2012/02/03) ブッキングデータをマージするかどうかを指定

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public ViewForm_HTMLTimeTable() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * 内容をクリア(初期化)します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) tdClassColumnNo 追加
	 * @og.rev 5.4.4.2 (2012/02/03) isBookingMerge 追加
	 *
	 */
	@Override
	public void clear() {
		super.clear();
		intval		= 30;			// 30分 単位に colspan を設定する。
		minStTime	= 480;			// 08:00 のこと。8H=480M
		maxEdTime	= 1260;			// 21:00 のこと。21H=1260M

		nullLinkClmNo	= -1;			// 固定ではなく可変の場合に利用するリンクカラム
		tdClassColumnNo	= -1 ;			// 5.4.3.7 (2012/01/20) データを入れるTDタグにclass属性を付与する場合のカラムNo
		isDyBreak		= true;			// 日付でブレイクするかどうかを指定
		isBookingMerge	= false;		// 5.4.4.2 (2012/02/03) ブッキングデータをマージするかどうかを指定
	}

	/**
	 * DBTableModel から HTML文字列を作成して返します。
	 * startNo(表示開始位置)から、pageSize(表示件数)までのView文字列を作成します。
	 * 表示残りデータが pageSize 以下の場合は,残りのデータをすべて出力します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) tdClassColumnNo 追加
	 * @og.rev 5.4.4.2 (2012/02/03) isBookingMerge 追加
	 * @og.rev 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"／＞" 止めを、"＞" に変更します)。
	 *
	 * @param  startNo	  表示開始位置
	 * @param  pageSize   表示件数
	 *
	 * @return	DBTableModelから作成された HTML文字列
	 * @og.rtnNotNull
	 */
	@Override
	public String create( final int startNo, final int pageSize )  {
		if( getRowCount() == 0 ) { return ""; }	// 暫定処置

		paramInit();
		headerLine	 = null;
		final int lastNo = getLastNo( startNo, pageSize );
		final int hsc = getHeaderSkipCount();
		int hscCnt = 1;

		final StringBuilder out = new StringBuilder( BUFFER_LARGE );

		if( isDyBreak ) {
			out.append( getRendererValue( 0,DY_CLMNO ) ).append( CR );
			setColumnDisplay( DY_CLMNO,false );	// 日付ブレイクなら、setColumnDisplay をfalse にセット
		}

		out.append( getHeader() )
			.append("<tbody>").append( CR );

		int bgClrCnt = 0;

		final int maxColspan = (maxEdTime-minStTime)/intval ;		// この数が、TDの数になる。
		int rowColspan = 0;									// 5.5.0.3(2012/03/12) １行の累積TD数。最大は、maxColspan で、
		int stTime     = minStTime;

		String backData    = "";	// 初期値。１回目にキーブレイクさせる。

		final StringBuilder buf2 = new StringBuilder( BUFFER_MIDDLE );	// 6.1.0.0 (2014/12/26) refactoring

		final List<String> dblBooking = new ArrayList<>();		// 重複データがあったときのデータ格納
		String nlVal	 = null;		// 空リンクのベース値
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		String bkNlVal	 = null;		// キーブレイク時の元の空リンクのベース値
		String bkDyVal	 = "";			// キーブレイク時の元の日付
		for( int row=startNo; row<lastNo; row++ ) {
			// キーブレイクの判定
			bkNlVal   = nlVal;

			nlVal  = nullLinkClmNo   < 0 ? null : getRendererValue( row,nullLinkClmNo );
			final String tdCls  = tdClassColumnNo < 0 ? null : getValue( row,tdClassColumnNo );		// 5.4.3.7 (2012/01/20) tdClassColumnNo 追加
			final String dyVal  = getValue( row,DY_CLMNO );											// 日付項目の値
			final String keyVal = getValue( row,KEY_CLMNO );										// キー項目の値
			if( row==startNo ) { bkDyVal = dyVal; }		// 初期データをセット。

			String linkVal = getRendererValue( row,LINK_CLMNO );

			// キーブレイク判定。キーブレイクは、一番初めから来る。
			if( !backData.equals( dyVal + keyVal ) ) {
				backData = dyVal + keyVal;			// null は来ないはず

				// minStTime < stTime の時だけ、処理を行う。(最初のキーブレイクは処理しないため)
				if( minStTime < stTime ) {
					// まずは、前の td の残りを出力する。ここでは、キーブレイク前の値を使用する。
					if( stTime < maxEdTime ) {
						out.append("  ");		// td タグの出力前の段落
						// 残データの書き出しは、最大TD数-それまでにセットした数。
						final int td = maxColspan - rowColspan;		// 5.5.0.3(2012/03/12)
						appendTDTag( out , null , td , 	// 5.5.0.3(2012/03/12)
										makeLinkValue( bkNlVal , stTime , maxEdTime ) );
					}
					out.append("</tr>").append( CR );
				}
				stTime = minStTime;		// 初期化

				// データかぶりが発生したときの処理
				if( !dblBooking.isEmpty() ) {
					for( final String bkdt : dblBooking ) {
						// 6.0.2.5 (2014/10/31) char を append する。
						out.append(" <tr").append( getBgColorCycleClass( bgClrCnt-1 ) ).append('>').append( CR )
							.append( bkdt )
							.append("</tr>").append( CR );
					}
					dblBooking.clear();
				}

				// 日付ブレイク処理
				if( isDyBreak && row > startNo && !bkDyVal.equals( dyVal ) ) {
					bkDyVal = dyVal;
					out.append(" <tr class=\"dummy\">");
					for( int column=0; column<CLM_CNT; column++ ) {
						if( isColumnDisplay( column ) ) {
//							out.append("<td/>");
							out.append("<td></td>");			// 7.0.1.0 (2018/10/15)
						}
					}
					for( int i=0; i<maxColspan ; i++ ) {		// 5.5.0.3(2012/03/12) 空td の出力。TD の colspan の基準になる。
//						out.append("<td/>");
						out.append("<td></td>");				// 7.0.1.0 (2018/10/15)
					}
					out.append("</tr>").append( CR )
						.append("</tbody>").append( CR )
						.append("</table>").append( CR )
						.append( getRendererValue( row,DY_CLMNO ) ).append( CR )
						.append( getHeader() )
						.append("<tbody>").append( CR );
					hscCnt = 1;
				}

				// ヘッダー繰り返し属性( headerSkipCount )を採用
				if( hsc > 0 && hscCnt % hsc == 0 ) {
					out.append( getHeadLine() );
					hscCnt = 1;
				}
				else {
					// 特殊処理：ここの処理では、一番最初も、実行されるので、＋＋しないように加工しておく。
					if( row > startNo ) { hscCnt ++ ; }
				}

				// ここから、新しい行が始まる。
				// 6.0.2.5 (2014/10/31) char を append する。
				out.append(" <tr").append( getBgColorCycleClass( bgClrCnt++ ) ).append('>').append( CR );
				rowColspan = 0 ;		// 5.5.0.3(2012/03/12) 初期化

				for( int column=0; column<CLM_CNT; column++ ) {
					if( isColumnDisplay( column ) ) {
						// ヘッダー部分に加工しやすいように、class 属性を与えておく。
						out.append("  <td class=\"" ).append( getColumnName( column ) ).append( "\">" )
							.append( getValueLabel(row,column) )
							.append("</td>").append( CR );
					}
				}
			}

			// 文字列型の時分情報を数字に変換する。
			int clStTime = getStr2Time( getValue( row,TMST_CLMNO ) , -1 );
			final boolean nullData = clStTime < 0 ;						// 開始時刻が	null の場合、-1 が返されるのでフラグをセットする。
			if( clStTime < minStTime ) { clStTime = minStTime; }	// 最小値以下の場合は、最小値に合せる。

			int clEdTime = getStr2Time( getValue( row,TMED_CLMNO ) , maxEdTime );
			if( clEdTime > maxEdTime ) { clEdTime = maxEdTime; }	// 最大値以上の場合は、最大値に合せる。

			if( clStTime == clEdTime ) { clEdTime = clEdTime + intval ; }	// 最初と最後が同じ場合は、intval分 進めておく。

			// 最初と最後が異なる場合は、間に空欄が入る。同じ場合は、連続なので、空欄は入らない。
			if( stTime < clStTime ) {
				out.append("  ");		// td タグの出力前の段落

				// 5.5.0.3(2012/03/12) 間に空欄が入る場合、その大きさが最小TD単位より大きければ、分割する。
				// ただし、直前の TD 個数が、最小でない場合のみ。
				final int td = (clStTime-stTime)/intval;		// 5.5.0.3(2012/03/12)
				rowColspan += td;
				appendTDTag( out , null , td , 		// 5.5.0.3(2012/03/12)
										makeLinkValue( nlVal , stTime , clStTime ) ).append( CR );
			}
			// 前のデータとかぶっている。つまり、ブッキングデータがある。
			else if( stTime > clStTime ) {
				// 5.4.4.2 (2012/02/03) ブッキングデータをマージする機能を追加
				if( isBookingMerge ) {
					if( stTime < clEdTime ) {
						final int td = (clEdTime-stTime)/intval;		// 5.5.0.3(2012/03/12)
						rowColspan += td ;
						appendTDTag( out , tdCls , td , linkVal ).append( CR );
						stTime = clEdTime;
					}
					continue;
				}

				buf2.setLength(0);		// 6.1.0.0 (2014/12/26) refactoring
				buf2.append("  ");		// td タグの出力前の段落
				for( int column=0; column<CLM_CNT; column++ ) {
					if( isColumnDisplay( column ) ) {
						// ヘッダー部分に加工しやすいように、class 属性を与えておく。
//						buf2.append("<td class=\"" ).append( getColumnName( column ) ).append( "\"/>" );
						buf2.append("<td class=\"" ).append( getColumnName( column ) ).append( "\"></td>" );	// 7.0.1.0 (2018/10/15)
					}
				}

				// 5.4.3.7 (2012/01/20)
				appendTDTag( buf2 , null  , (clStTime-minStTime)/intval );				// 最初からデータまで
				appendTDTag( buf2 , tdCls , (clEdTime-clStTime)/intval , linkVal );		// データ部: 5.4.3.7 (2012/01/20) td に class属性追加
				appendTDTag( buf2 , null  , (maxEdTime-clEdTime)/intval );				// データから最後まで

				dblBooking.add( buf2.toString() );
				continue;
			}
			// 前も後ろも最小と最大になっているのは、予約レコードが無いため。
			// stTime == clStTime のケース。nullData = true で、予約レコードが無いため。
			else if( nullData ) {
				linkVal = makeLinkValue( nlVal , minStTime , maxEdTime );
			}
			// 5.4.3.7 (2012/01/20) linkVal を共通に使用している箇所を修正

			out.append("  ");		// td タグの出力前の段落
			final int td = (clEdTime-clStTime)/intval;		// 5.5.0.3(2012/03/12)
			rowColspan += td ;
			appendTDTag( out , tdCls , td , linkVal ).append( CR );	// 5.5.0.3(2012/03/12)

			stTime = clEdTime ;
		}

		// 残処理：データが残っている場合は、書き出す必要がある。
		if( minStTime < stTime && stTime < maxEdTime ) {
			out.append("  ");		// td タグの出力前の段落
			// 残データの書き出しは、最大TD数-それまでにセットした数。
			final int td = maxColspan - rowColspan;		// 5.5.0.3(2012/03/12)
			appendTDTag( out , null , td ,		// 5.5.0.3(2012/03/12)
										makeLinkValue( nlVal , stTime , maxEdTime ) );
		}
		out.append("</tr>").append( CR );

		// データかぶりが発生したときの処理
		if( !dblBooking.isEmpty() ) {
			for( final String bkdt : dblBooking ) {
				// 6.0.2.5 (2014/10/31) char を append する。
				out.append(" <tr").append( getBgColorCycleClass( bgClrCnt-1 ) ).append('>').append( CR )
					.append( bkdt )
					.append("</tr>").append( CR );
			}
		}

		// カラムの結合があるため、td タグを個別に出力しておかないと、レイアウトがずれる。
		out.append(" <tr class=\"dummy\">");
		for( int column=0; column<CLM_CNT; column++ ) {
			if( isColumnDisplay( column ) ) {
//				out.append("<td/>");
				out.append("<td></td>");			// 7.0.1.0 (2018/10/15)
			}
		}
		for( int i=0; i<maxColspan ; i++ ) {		// 5.5.0.3(2012/03/12) 空td の出力。TD の colspan の基準になる。
//			out.append("<td/>");
			out.append("<td></td>");				// 7.0.1.0 (2018/10/15)
		}
		out.append("</tr>").append( CR )
			.append("</tbody>").append( CR )
			.append("</table>").append( CR )
			.append( getScrollBarEndDiv() );		// 3.8.0.3 (2005/07/15)
		return out.toString();
	}

	/**
	 * パラメータ内容を初期化します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) tdClassColumnNo 追加。intval の実値化
	 * @og.rev 5.4.4.2 (2012/02/03) isBookingMerge 追加
	 *
	 */
	private void paramInit() {
		final String s_intval		= getParam( ViewTimeTableParam.TIME_INTERVAL	, null );
		final String s_minStTime	= getParam( ViewTimeTableParam.MIN_START_TIME	, null );
		final String s_maxEdTime	= getParam( ViewTimeTableParam.MAX_END_TIME		, null );

		isDyBreak		= Boolean.valueOf( getParam( ViewTimeTableParam.USE_DY_BREAK, "true" ) );

		// 5.4.4.2 (2012/02/03) ブッキングデータをマージするかどうかを指定
		isBookingMerge	= Boolean.valueOf( getParam( ViewTimeTableParam.USE_BOOKING_MERGE, "false" ) );

		// nullリンクのカラム名指定。nullClm が優先順位が高い。
		final String s_nullClm	= getParam( ViewTimeTableParam.NULL_LINK_CLM_ID	, null );
		if( s_nullClm != null ) {
			nullLinkClmNo = getColumnNo( s_nullClm );
		}

		// 5.4.3.7 (2012/01/20) データを入れるTDタグにclass属性を付与する場合のカラムNo
		final String s_tdClsClm	= getParam( ViewTimeTableParam.TD_CLASS_COLUMN_ID	, null );
		if( s_tdClsClm != null ) {
			tdClassColumnNo = getColumnNo( s_tdClsClm );
		}

		if( s_intval != null ) {
			intval = Integer.parseInt( s_intval ) ;	// 5.4.3.7 (2012/01/20)
		}
		minStTime	= getStr2Time( s_minStTime	, minStTime );			// 最小開始時刻。0800 なら、80。 30分=5 換算
		maxEdTime	= getStr2Time( s_maxEdTime	, maxEdTime );			// 最大終了時刻。2100 なら、210。30分=5 換算
	}

	/**
	 * DBTableModel から テーブルのタグ文字列を作成して返します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) colgroup は不要
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getTableHead() {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append("<thead id=\"header\">").append( CR )
			.append( getHeadLine() )
			.append("</thead>").append( CR );

		return buf.toString();
	}

	/**
	 * ヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 *
	 * @og.rev 6.1.2.0 (2015/01/24) キャッシュを返すのを、#getHeadLine() に移動。
	 *
	 * @param	thTag タグの文字列
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getHeadLine( final String thTag ) {

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "<tr class=\"row_h\" >" ).append( CR );

		for( int column=0; column<CLM_CNT; column++ ) {
			if( isColumnDisplay( column ) ) {
				buf.append( thTag )
					.append(" class=\"" ).append( getColumnName( column ) ).append( "\">" )
					.append( getColumnLabel(column) )
					.append("</th>").append( CR );
			}
		}

		final String thTagTmp = thTag + " class=\"timeVar\" colspan=\"" + 60/intval + "\">" ;

		for( int tm=minStTime; tm<maxEdTime; tm+=60 ) {	// ヘッダーは、１時間単位とする。
			buf.append( thTagTmp ).append( tm/60 ).append("</th>").append( CR );
		}

		buf.append("</tr>").append( CR );

		return buf.toString();				// 6.1.2.0 (2015/01/24)
	}

	/**
	 * TDタグ文字列を簡易的に合成します。
	 *
	 * ここでは、主に、class 属性、colspan 属性を設定することを目的にしています。
	 * colspan の値によって、動作を変化させています。
	 *   0: タグそのものを生成しません。これは、第一引数をそのまま返します。
	 *   1: colspan 属性を出力しません。(Default値なので)
	 *   それ以外は、colspan に引数を設定します。
	 * BODY 部も、無指定の場合は、/&gt; 止めのタグを出力します。
	 *
	 * 返り値の StringBuilder は、第一引数そのものを返します。よって、このメソッドに、
	 * append を連結していくことも可能です。
	 * (return値を使わなくても、第一引数の StringBuilder は変化しています。副作用というやつ。)
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) tdタグ専用に変更。
	 * @og.rev 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"／＞" 止めを、"＞" に変更します)。
	 *
	 * @param	buf		このStringBuilderに append します。
	 * @param	cls		追加する class 属性
	 * @param	colspan	td , th タグ属性に追加する colspan値。
	 *                     0 の場合は、タグ自体を出力しません。
	 *                     1 の場合は、colspan を出力しません。
	 * @param	body	タグの BODY 部に出力する内容(String可変長引数)０件の場合は、BODYなし
	 *
	 * @return	appendされたStringBuilder(第一引数と同じオブジェクト)
	 * @og.rtnNotNull
	 */
	private StringBuilder appendTDTag( final StringBuilder buf , final String cls , final int colspan , final String... body ) {
		// 6.3.9.1 (2015/11/27) A method should have only one exit point, and that should be the last statement in the method.(PMD)
		if( colspan != 0 ) {
			buf.append( "<td" );
			// class 属性の追加
			if( cls != null && !cls.isEmpty() ) {
				buf.append( " class=\"" ).append( cls ).append( '"' );			// 6.0.2.5 (2014/10/31) char を append する。
			}

			// colspan 属性の追加
			if( colspan > 1 ) {
				buf.append( " colspan=\"" ).append( colspan ).append( '"' );	// 6.0.2.5 (2014/10/31) char を append する。
			}

			// 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"／＞" 止めを、"＞" に変更します)。
			if( body == null || body.length == 0 ) {		// 6.3.9.1 (2015/11/27) 可変長引数でもnullは来る。
//				buf.append( " />" );
				buf.append( " ></td>" );					// 7.0.1.0 (2018/10/15)
			}
			else {
				buf.append( '>' );							// 6.0.2.5 (2014/10/31) char を append する。
				for( final String bd : body ) {
					buf.append( bd ).append( ' ' );			// 6.3.9.1 (2015/11/27) スペースで連結追加
				}
				buf.append( "</td>" );
			}
		}

		return buf;
	}

	/**
	 * 時間文字列を数字に変換します。
	 *
	 * "0800" は、480に、"2100" は、1260 に変換します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) 計算方法の変更
	 *
	 * @param	val  	時間文字列の値(0800 など)
	 * @param	defTm	引数の時間文字列が null の場合の初期値
	 *
	 * @return	時間文字列を数字に変換した結果( 80 など)
	 */
	private int getStr2Time( final String val , final int defTm ) {
		// 6.3.9.1 (2015/11/27) A method should have only one exit point, and that should be the last statement in the method.(PMD)
		final int rtn ;
		if( val == null || val.isEmpty() ) { rtn = defTm; }
		else if( val.length() == 4 ) {
			rtn = Integer.parseInt( val.substring( 0,2 ) )*60 +
					Integer.parseInt( val.substring( 2 ) ) ;	// 5.4.3.7 (2012/01/20)
		}
		else {
			final String errMsg = "時間引数は、４桁必要です。" +
							"  val=[" + val + "]";
			throw new HybsSystemException( errMsg );
		}

		return rtn ;
	}

	/**
	 * 数字を時間文字列に変換します。
	 *
	 * 480は、"0800" に、"1260"は、2100 に変換します。
	 *
	 * @og.rev 5.4.3.7 (2012/01/20) 計算方法の変更
	 *
	 * @param	timeVal	引数の時間文字列が null の場合の初期値
	 *
	 * @return	数字を時間文字列に変換した結果( "0800" など)
	 * @og.rtnNotNull
	 */
	private String getInt2StrTime( final int timeVal ) {
		int tmpVal = timeVal;
		if(      tmpVal < minStTime ) { tmpVal = minStTime; }	// 最小値以下の場合は、最小値に合せる
		else if( tmpVal > maxEdTime ) { tmpVal = maxEdTime; }	// 最大値以上の場合は、最大値に合せる。

		return String.valueOf(100+ tmpVal/60).substring(1) + String.valueOf(100+tmpVal%60).substring(1);
	}

	// 引数に、 を追加する。

	/**
	 * リンク文字列をパースします。
	 *
	 * データの空欄にリンクを作成するときに、元となるリンク文字列の引数を設定します。
	 * 引数は、&TMSTART=(stTime)&TMEND=(edTime) を追加するイメージです。
	 * stTime、edTime は、それぞれ、($1)、($2) の変数が割り当てられます。
	 * stTime、edTime は、#getInt2StrTime( int ) メソッドで変換した文字列を利用します。
	 *
	 * @param	lnkVal 	リンクのベースとなる値
	 * @param	stTime 	開始時刻の数字表記
	 * @param	edTime 	終了時刻の数字表記
	 *
	 * @return	リンク文字列をパースした結果
	 */
	private String makeLinkValue( final String lnkVal,final int stTime,final int edTime ) {
		// 6.3.9.0 (2015/11/06) Found 'DD'-anomaly for variable(PMD)
		return lnkVal == null ? ""
							  : lnkVal.replace( "%28%241%29",getInt2StrTime( stTime ) )
										.replace( "%28%242%29",getInt2StrTime( edTime ) ) ;
	}

	/**
	 * 表示項目の編集(並び替え)が可能かどうかを返します。
	 *
	 * @return	表示項目の編集(並び替え)が可能かどうか(false:不可能)
	 */
	@Override
	public boolean isEditable() {
		return false;
	}
}
