/*
 * 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.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

import org.opengion.fukurou.util.StringUtil;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.db.DBColumn;
import org.opengion.hayabusa.db.DBColumnConfig;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.db.DBTableModelSorter;
import org.opengion.hayabusa.db.DBTableModelUtil;
// import org.opengion.hayabusa.db.Selection;
// import org.opengion.hayabusa.db.SelectionFactory;			// 6.0.4.0 (2014/11/28)
import org.opengion.hayabusa.html.CrossMap;
import org.opengion.hayabusa.html.ViewCrossTableParam;
import org.opengion.hayabusa.resource.ResourceManager;
import org.opengion.hayabusa.resource.LabelData;			// 7.0.1.5 (2018/12/10)

/**
 * クロス集計テーブル作成クラスです。
 *
 *   select dept.dname,emp.deptno,substrb(job,1,2) as X,job,mgr,sum(sal),count(*)
 *   from emp,dept
 *   where emp.deptno = dept.deptno
 *   group by dept.dname,emp.deptno,cube(job,mgr)
 *   order by emp.deptno,job,mgr;
 *
 *   HEAD1   ：ヘッダー。前段と同じデータは表示させない。
 *   HEAD2   ：キーブレイクさせるカラム。また、前段と同じデータは表示させない。
 *   HEAD3   ：キーブレイクさせないカラム。また、前段と同じデータでも表示させる。
 *   ROW     ：行データのヘッダーになるカラム
 *   COL     ：列データのヘッダーになるカラム。下記のSUM1,SUM2の両方のヘッダーになる。
 *   SUM1    ：列データの値になるカラム。
 *   SUM2    ：列データの値になるカラム。
 *
 *   SUMカラムの数、キーブレイクのカラム名、グループ化するカラム名を
 *   指定することで、これらのクロス集計結果の表示方法を指定します。
 *
 *   breakColumn    = "DEPTNO"             キーブレイクのカラム名
 *   noGroupColumns = "X"                  グループ化するカラム名
 *   sumNumber      = "2"                  SUMカラムの数
 *   cubeXColumn    = "JOB"                CUBE計算の１つ目(X)カラムを指定
 *   cubeYColumn    = "MGR"                CUBE計算の２つ目(Y)カラムを指定
 *   cubeSortType   = "NUMBER"             CUBE Y の列ヘッダーのソート方法を指定
 *   gokeiSortDir   = "false"              合計カラムのソート方向を指定(初期値:ソートしない)
 *   shokeiLabel    = "SHOKEI"             列小計のカラムに表示するラベルID
 *   gokeiLabel     = "GOKEI"              列合計のカラムに表示するラベルID
 *   useHeaderColumn= "false"              ヘッダーカラムにレンデラー、エディターを適用するかを指定
 *   useClassAdd    = "false"              各列情報のclass属性に、カラム名などを付与するかどうかを指定
 *   useHeaderResource = "false"           ヘッダー表示にラベルリソースを利用するか
 *
 *   各カラムの属性(HEAD,SUM等)を認識する方法
 *
 *     HEAD1 HEAD2 HEAD3 ROW COL SUM1 SUM2 という並びを認識する方法は、
 *     多数の前提条件を利用して、出来るだけ少ないパラメータで自動認識
 *     させています。
 *     若干理解しにくいかもしれませんが、慣れてください。
 *
 *     前提条件：
 *       ROW,COL は、必ず１個ずつ存在する。
 *       HEAD群、ROW,COL,SUM群 という並びになっている。
 *       SUM群の数は、パラメータで指定する。
 *     計算方法：
 *       HEAD数=カラム数(7)-SUM数(2)-1(ROW,COL分) ＝ ４ 個 (0 ～ 3)
 *       ROWアドレス＝cubeXColumn 設定                       (3)      ※ アドレスは０から始まる為
 *       COLアドレス＝cubeYColumn 設定                       (4)
 *       SUMアドレス＝HEAD数＋１ ～ カラム数(7)-1            (5 ～ 6)
 *
 * @og.rev 3.5.4.0 (2003/11/25) 新規作成
 * @og.group 画面表示
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ViewForm_HTMLCrossTable extends ViewForm_HTMLTable {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "7.0.1.7 (2019/01/21)" ;

	private static final Comparator<String> NUMBER_SORT = new NumberComparator();	// 6.4.1.1 (2016/01/16) numberSort  → NUMBER_SORT  refactoring

	private String[] groupByData	;
	private String[] groupByCls		;

	// 3.5.4.8 (2004/02/23) 機能改善
	private int			rowClmNo	= -1;		// ROWカラムのカラム番号
	private int			colClmNo	= -1;		// CLMカラムのカラム番号
	private int			headCount	;			// HEADカラムの数
	private int			sumCount	= 1;		// 合計カラムの数
	private int			breakClmNo	= -1;		// ブレークするカラムのカラム番号
	private boolean[]	noGroupClm	;			// グループ化する/しないのフラグ配列
	private String		shokeiLabel	= "小計";	// 列小計のカラムに表示するラベルID
	private String		gokeiLabel	= "合計";	// 列合計のカラムに表示するラベルID
	private String		gokeiSortDir;			// 列合計のカラムをソートする方向

	// 3.5.6.3 (2004/07/12) ソート方式[STRING,NUMBER,LOAD]
	private String		cubeSortType = "LOAD";

	private DBTableModel table2		;
	private boolean		 firstStep	= true;

	private String[]	clmKeys		;			// 集計部のカラムキー(集計カラムが複数でも一つ)の配列
	private String[]	clsAdd		;			// 5.2.2.0 (2010/11/01) class属性に付与されるカラムキー

	private String		noDisplayKeys		;	// 3.7.0.4 (2005/03/18)
	private String		columnDisplayKeys	;	// 5.2.2.0 (2010/11/01)

	private boolean		firstClmGokei		;	// 5.0.0.3 (2009/09/22)
	private boolean		useHeaderColumn		;	// 5.2.2.0 (2010/11/01)
	private boolean		useClassAdd			;	// 5.2.2.0 (2010/11/01) class属性にカラムキーを追加するかどうか
	private boolean		useHeaderResource	; 	// 5.5.5.0 (2012/07/28)
//	private String		headerCode			;	// 5.5.5.0 (2012/07/28)  7.0.1.5 (2018/12/10) 廃止

	private DBColumn[]	sumClms				;	// 7.0.1.7 (2019/01/21)

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

	/**
	 * 初期化します。
	 * ここでは、内部で使用されているキャッシュをクリアし、
	 * 新しいモデル(DBTableModel)と言語(lang) を元に内部データを再構築します。
	 * ただし、設定情報は、以前の状態がそのままキープされています。
	 *
	 * @og.rev 3.5.4.8 (2004/02/23) paramInit メソッドで、初期化を行います。
	 * @og.rev 3.5.6.1 (2004/06/25) lang 言語コード 属性を削除します。
	 * @og.rev 6.0.2.2 (2014/10/03) 初期化漏れでエラーになっていたので、修正します。
	 *
	 * @param	table	DBTableModelオブジェクト
	 */
	@Override
	public void init( final DBTableModel table ) {
		table2		= table;
		firstStep	= true;
		super.init( table );	// 6.0.2.2 (2014/10/03) 初期化漏れでエラーになっていたので、修正します。
	}

	/**
	 * 内容をクリア(初期化)します。
	 *
	 * @og.rev 3.5.6.3 (2004/07/12) cubeSortType , gokeiSortDir 属性を追加します。
	 * @og.rev 3.7.0.4 (2005/03/18) noDisplayKeys 属性を追加します。
	 * @og.rev 3.7.1.1 (2005/05/31) shokeiLabel,gokeiLabel の初期値変更
	 * @og.rev 5.2.2.0 (2010/11/01) columnDisplayKeys、clsAdd、useClassAdd 属性を追加します
	 * @og.rev 5.5.5.0 (2012/07/20) useHeaderResource追加
	 */
	@Override
	public void clear() {
		super.clear();
		groupByData = null;
		groupByCls  = null;
		rowClmNo	= -1;			// ROWカラムのカラム番号
		colClmNo	= -1;			// CLMカラムのカラム番号
		headCount	= 0;			// HEADカラムの数
		sumCount	= 1;			// 合計カラムの数
		breakClmNo	= -1;			// ブレークするカラムのカラム番号
		noGroupClm	= null;			// グループ化する/しないのフラグ配列
		table2		= null;
		firstStep	= true;
		clmKeys		= null;
		clsAdd		= null;			// 5.2.2.0 (2010/11/01)
		shokeiLabel	= "小計";		// 列小計のカラムに表示するラベルID
		gokeiLabel	= "合計";		// 列合計のカラムに表示するラベルID
		cubeSortType = "LOAD";		// 3.5.6.3 (2004/07/12)
		gokeiSortDir = null;		// 3.5.6.3 (2004/07/12) 列合計のカラムをソートする方向
		noDisplayKeys		= null;	// 3.7.0.4 (2005/03/18)
		columnDisplayKeys	= null;	// 5.2.2.0 (2010/11/01)
		firstClmGokei		= false;	// 5.2.2.0 (2010/11/01)
		useHeaderColumn		= false;	// 5.2.2.0 (2010/11/01)
		useClassAdd			= false;	// 5.2.2.0 (2010/11/01)
		useHeaderResource	= false;	// 5.5.5.0 (2012/07/20)
//		headerCode			= null;		// 5.5.5.0 (2012/07/28)  7.0.1.5 (2018/12/10) 廃止
	}

	/**
	 * DBTableModel から HTML文字列を作成して返します。
	 * startNo(表示開始位置)から、pageSize(表示件数)までのView文字列を作成します。
	 * 表示残りデータが pageSize 以下の場合は,残りのデータをすべて出力します。
	 *
	 * @og.rev 3.5.5.0 (2004/03/12) No 欄そのものの作成判断ロジックを追加
	 * @og.rev 3.5.6.1 (2004/06/25) lang 言語コード 属性を削除します。
	 * @og.rev 3.5.6.4 (2004/07/16) ヘッダーとボディー部をJavaScriptで分離
	 * @og.rev 3.7.0.4 (2005/03/18) setNoDisplay メソッドを追加
	 * @og.rev 4.3.1.0 (2008/09/08) 編集行のみを表示する属性(isSkipNoEdit)追加
	 * @og.rev 5.0.0.3 (2009/09/22) 合計列をcubeの先頭に出せるようにする
	 * @og.rev 5.1.0.0 (2009/11/04) ↑で合計列が複数カラム存在する場合に正しく表示されないバグを修正
	 * @og.rev 5.2.2.0 (2010/11/01) setColumnDisplay メソッドを追加
	 * @og.rev 6.8.1.1 (2017/07/22) ckboxTD変数は、&lt;td&gt; から &lt;td に変更します(タグの最後が記述されていない状態でもらう)。
	 *
	 * @param  startNo	  表示開始位置
	 * @param  pageSize   表示件数
	 *
	 * @return	DBTableModelから作成された HTML文字列
	 * @og.rtnNotNull
	 */
	@Override
	public String create( final int startNo, final int pageSize )  {
		if( firstStep ) {
			paramInit( table2 );
			super.init( makeCrossTable(table2) );
			super.setNoDisplay( noDisplayKeys ) ;				// 3.7.0.4 (2005/03/18)
			super.setColumnDisplay( columnDisplayKeys ) ;		// 5.2.2.0 (2010/11/01)
			markerSet( this );		// 3.5.6.4 (2004/07/16)
			firstStep = false;
		}

		if( getRowCount() == 0 ) { return ""; }	// 暫定処置

		final int clmCnt = getColumnCount();	// 3.5.5.7 (2004/05/10)

		headerLine	 = null;

		final int lastNo = getLastNo( startNo, pageSize );
		final int blc = getBackLinkCount();
		String backData = null;

		final StringBuilder out = new StringBuilder( BUFFER_LARGE )
			.append( getCountForm( startNo,pageSize ) )
			.append( getHeader() );

		final String ckboxTD = "  <td class=\"" + ViewCrossTableParam.HEADER1 + "\"";		// 6.8.1.1 (2017/07/22)

		out.append("<tbody>").append( CR );
		int bgClrCnt = 0;
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		for( int row=startNo; row<lastNo; row++ ) {
			if( isSkip( row ) || isSkipNoEdit( row ) ) { continue; } // 4.3.1.0 (2008/09/08)
			// キーブレイク時のヘッダー設定
			if( breakClmNo >= 0 ) {
				final String val = getValue( row,breakClmNo );
				if( backData == null ) {	// キーブレイクの初期データ設定。
					backData = val;
				}
				else {
					if( ! backData.equals( val ) ) {
						backData = val;
						out.append( getHeadLine() );
					}
				}
			}
			// 小計ヘッダー時のクラス設定
			final boolean shokei = getValue( row,rowClmNo ).isEmpty();	// 6.3.9.1 (2015/11/27)
			if( shokei ) {				// 6.3.9.1 (2015/11/27)
				out.append(" <tr class=\"").append( ViewCrossTableParam.SHOKEI ).append("\">");
			}
			else {
				out.append(" <tr").append( getBgColorCycleClass( bgClrCnt++ ) ).append('>');		// 6.0.2.5 (2014/10/31) char を append する。
			}
			out.append( CR );
			// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
			if( isNumberDisplay() ) {
				out.append( makeCheckbox( ckboxTD, row, blc ) ).append( CR );
			}
			for( int column=0; column<clmCnt; column++ ) {
				if( isColumnDisplay( column ) ) {
					if( column < headCount-1 ) {		// CUBEではない行ヘッダー部
						final String val = getGroupData( column,getRendererValue(row,column) );
						out.append("  <td class=\"").append( groupByCls[column] ).append("\">")
							.append( val );
					}
					else if( column == headCount-1 ) {	// ヘッダーの最後尾
						if( shokei ) {
							out.append("  <td class=\"").append( ViewCrossTableParam.SHOKEI ).append("\">")
								.append( shokeiLabel );
						}
						else {
							if( breakClmNo > 0 ) {	// ヘッダーがある場合
								out.append("  <td class=\"").append( groupByCls[column-1] ).append("\">");
							}
							else {
								out.append("  <td class=\"").append( ViewCrossTableParam.HEADER1 ).append("\">");
							}
							out.append( getRendererValue(row,column) );
						}
					}
					// else if( column >= clmCnt-sumCount ) {	// CUBEの最終カラム(列合計)
					else if( column >= clmCnt-sumCount && ! firstClmGokei ) {	// 5.0.0.3 (2009/09/22) CUBEの最終カラム(列合計)
						out.append("  <td class=\"").append( ViewCrossTableParam.SHOKEI ).append("\">")
							.append( getRendererValue(row,column) );
					}
					else if( column >= headCount && column < headCount + sumCount && firstClmGokei ) {		// 5.1.0.0 (2009/11/04)
						out.append("  <td class=\"").append( ViewCrossTableParam.SHOKEI ).append("\">")
							.append( getRendererValue(row,clmCnt-sumCount+(column-headCount)) ); // 5.1.0.0 (2009/11/04)
					}
					else {		// カラム SUM列
						if( useClassAdd && clsAdd[column] != null ) {
							out.append("  <td class=\"").append( clsAdd[column] ).append("\">");
						}
						else {
							out.append("  <td>");
						}
						if( firstClmGokei ){
							out.append( getRendererValue(row,column-sumCount) ); // 5.1.0.0 (2009/11/04)
						}
						else{
							out.append( getRendererValue(row,column) );
						}
					}
					out.append("  </td>").append( CR );
				}
			}
			out.append(" </tr>").append( CR );
		}
		out.append("</tbody>").append( CR )
			.append("</table>").append( CR )
			.append( getScrollBarEndDiv() );	// 3.8.0.3 (2005/07/15)

		return out.toString();
	}

	/**
	 * パラメータ内容を初期化します。
	 *
	 * @og.rev 3.5.4.8 (2004/02/23) 新規作成
	 * @og.rev 3.5.6.3 (2004/07/12) 列ヘッダーのソート方法を指定
	 * @og.rev 5.0.0.3 (2009/09/22) 合計行をCUBEの先頭に持ってくるためのフラグ追加
	 * @og.rev 5.2.2.0 (2010/11/01) useHeaderColumn,useClassAdd 属性の追加
	 *
	 * @param	table	入力もとの DBTableModelオブジェクト
	 */
	private void paramInit( final DBTableModel table ) {
		final String breakColumn	= getParam( ViewCrossTableParam.BREAK_COLUMN_KEY     , null );
		final String noGroupColumns	= getParam( ViewCrossTableParam.NO_GROUP_COLUMNS_KEY , null );
		final String sumNumber		= getParam( ViewCrossTableParam.SUM_NUMBER_KEY       , null );
		shokeiLabel					= getParam( ViewCrossTableParam.SHOKEI_LABEL_KEY     , shokeiLabel );
		gokeiLabel					= getParam( ViewCrossTableParam.GOKEI_LABEL_KEY      , gokeiLabel );
		final String cubeXColumn	= getParam( ViewCrossTableParam.CUBE_X_COLUMN_KEY    , null );		// CUBE計算の１つ目(X)カラムを指定
		final String cubeYColumn	= getParam( ViewCrossTableParam.CUBE_Y_COLUMN_KEY    , null );		// CUBE計算の２つ目(Y)カラムを指定
		cubeSortType				= getParam( ViewCrossTableParam.CUBE_SORT_TYPE_KEY   , "LOAD" );	// 3.5.6.3 (2004/07/12)
		gokeiSortDir				= getParam( ViewCrossTableParam.GOKEI_SORT_DIR_KEY   , null );		// 3.5.6.3 (2004/07/12)
		firstClmGokei				= StringUtil.nval( getParam( ViewCrossTableParam.FIRST_CLM_GOKEI_KEY , null ), false);	// 5.0.0.3 (2009/09/22)
		useHeaderColumn				= StringUtil.nval( getParam( ViewCrossTableParam.USE_HEADER_COLUMN   , null ), false);	// 5.2.2.0 (2010/11/01)
		useClassAdd					= StringUtil.nval( getParam( ViewCrossTableParam.USE_CLASS_ADD       , null ), false);	// 5.2.2.0 (2010/11/01)
		useHeaderResource			= StringUtil.nval( getParam( ViewCrossTableParam.USE_HEADER_RSC      , null ), false);	// 5.5.5.0 (2012/07/20)
//		headerCode					= getParam( ViewCrossTableParam.HEADER_CODE_KEY      , null );		// 7.0.1.5 (2018/12/10) 廃止

		if( sumNumber != null ) {
			sumCount = Integer.parseInt( sumNumber );
		}

		// HEAD数=カラム数-SUM数-1(COL分) ROW は、HEADに含みます。
		headCount = table.getColumnCount() - sumCount - 1;

		// 3.5.5.9 (2004/06/07)
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		rowClmNo = cubeXColumn == null
						? headCount-1		// ROWカラムのカラム番号
						: table.getColumnNo( cubeXColumn );

		// 3.5.5.9 (2004/06/07)
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		colClmNo = cubeYColumn == null
						? headCount			// CLMカラムのカラム番号
						: table.getColumnNo( cubeYColumn );

		if( breakColumn != null ) {
			breakClmNo = table.getColumnNo( breakColumn );
		}

		groupByData = new String[headCount];
		groupByCls  = new String[headCount];
		Arrays.fill( groupByCls,ViewCrossTableParam.HEADER2 );		// 変であるが、最初に入れ替えが発生する為。

		noGroupClm    = new boolean[headCount];		// グループ化する/しないのフラグ配列
		Arrays.fill( noGroupClm,false );

		if( noGroupColumns != null ) {
			final String[] gClms = StringUtil.csv2Array( noGroupColumns );
			for( int i=0; i<gClms.length; i++ ) {
				noGroupClm[table.getColumnNo( gClms[i] )] = true;
			}
		}

		if( ! "true".equalsIgnoreCase( gokeiSortDir ) &&
			! "false".equalsIgnoreCase( gokeiSortDir ) ) {
				gokeiSortDir = null;
		}
	}

	/**
	 * CUBEではない行ヘッダー部の値が前と同じならば、ゼロ文字列を返します。
	 *
	 * @param	clm	カラム番号
	 * @param	val	比較する値
	 *
	 * @return	前と同じなら,""を、異なる場合は、引数の val を返します。
	 */
	private String getGroupData( final int clm,final String val ) {
		// 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 String rtn ;
		if( noGroupClm[clm] ) { rtn = val; }
		else if( val.equals( groupByData[clm] )) {
			rtn = "";
		}
		else {
			rtn = val;
			groupByData[clm] = val;
			groupByCls[clm] = groupByCls[clm].equals( ViewCrossTableParam.HEADER1 )
									? ViewCrossTableParam.HEADER2
									: ViewCrossTableParam.HEADER1 ;
		}
		return rtn ;
	}

	/**
	 * 選択用のチェックボックスと行番号と変更タイプ(A,C,D)を表示します。
	 *
	 * @og.rev 6.8.1.1 (2017/07/22) ckboxTD変数は、&lt;td&gt; から &lt;td に変更します(タグの最後が記述されていない状態でもらう)。
	 *
	 * @param  ckboxTD チェックボックスのタグ(マルチカラム時のrowspan対応)
	 * @param  row	 行番号
	 * @param  blc	 バックラインカウント(先頭へ戻るリンク間隔)
	 *
	 * @return	tdタグで囲まれたチェックボックスのHTML文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String makeCheckbox( final String ckboxTD,final int row,final int blc ) {
		final StringBuilder out = new StringBuilder( BUFFER_MIDDLE )
			.append( ckboxTD ).append( "></td>" )
			.append( ckboxTD ).append( "></td>" )
			.append( ckboxTD ).append( '>' );					// 6.8.1.1 (2017/07/22)
		// 3.5.1.0 (2003/10/03) Noカラムに、numberType 属性を追加
		if( blc != 0 && (row+1) % blc == 0 ) {
			out.append( "<a href=\"#top\">" ).append( row+1 ).append(  "</a>" );
		} else {
			out.append( row+1 );
		}
		out.append("</td>");

		return out.toString();
	}

	/**
	 * ヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 *
	 * @og.rev 3.5.4.5 (2004/01/23) 実装をgetHeadLine( String thTag )に移動
	 * @og.rev 3.5.5.0 (2004/03/12) No 欄そのものの作成判断ロジックを追加
	 * @og.rev 5.0.0.3 (2009/09/17) 合計行を出力する位置をfirstClmGokeiで変える
	 * @og.rev 5.2.2.0 (2010/11/01) 集計部の ColumnDisplay/NoDisplay 対応
	 * @og.rev 5.5.5.0 (2012/07/28) useHeaderResource利用時のヘッダのラベル/コードリソース対応
	 * @og.rev 5.7.4.2 (2014/03/20) ヘッダーのリソース適用見直し
	 * @og.rev 5.7.4.3 (2014/03/28) useHeaderResource 単独でリソース適用します。
	 * @og.rev 6.0.4.0 (2014/11/28) selection は、Column から取得するのではなく、Factory で作成する。
	 * @og.rev 7.0.1.5 (2018/12/10) headerCodeColumn 廃止
	 * @og.rev 7.0.1.7 (2019/01/21) ヘッダー作成時にオリジナルのカラムを使うので、インスタンス変数化します。
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getHeadLine() {
		if( headerLine != null ) { return headerLine; }		// キャッシュを返す。

		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		final String rowspan = sumCount > 1 ? " rowspan=\"2\"" : "";
		final String thTag = "<th" + rowspan;

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

		// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
		if( isNumberDisplay() ) {
			buf.append( thTag ).append(" colspan='3'>").append( getNumberHeader() ).append("</th>");
		}

		buf.append( CR );
		// ヘッダー部分は、そのまま表示します。
		for( int column=0; column<headCount; column++ ) {
			if( isColumnDisplay( column ) ) {
				buf.append( thTag ).append('>')				// 6.0.2.5 (2014/10/31) char を append する。
					.append( getColumnLabel(column) )
					.append("</th>").append( CR );
			}
		}

		// ヘッダー部分(上段)は、カラム配列を利用します。
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		final String colspan = sumCount > 1 ? " colspan='" + sumCount + "'" : "";

		// 5.2.2.0 (2010/11/01) 集計部の ColumnDisplay/NoDisplay 対応
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		final String gokeiClm ;
		if( isColumnDisplay( headCount+(clmKeys.length-1)*sumCount ) ) {
			String temp = clmKeys[clmKeys.length-1];
			if( temp == null || temp.isEmpty() ) {		// 6.1.0.0 (2014/12/26) refactoring
				temp = gokeiLabel;
			}

			gokeiClm = "<th" + colspan + ">" + temp + "</th>" + CR ;
		}
		else {
			gokeiClm = null ;			// 6.3.9.1 (2015/11/27)
		}

		// 5.2.2.0 (2010/11/01) 最後のカラムが、合計行。
		// 5.0.0.3 (2009/09/22) firstClmGokei が trueの場合はcubeの先頭に出すようにします。
		if( firstClmGokei && gokeiClm != null ) {
			buf.append( gokeiClm );
		}

		// 3.7.0.4 (2005/03/18) カラム配列は、カラム番号と別物
//		// 7.0.1.5 (2018/12/10) headerCodeColumn 廃止
//		final ResourceManager resource = getResourceManager();
//		Selection selection = null;
//		if( headerCode != null && headerCode.length() > 0 && resource != null ){
//			final DBColumn clmTmp = resource.getDBColumn( headerCode );
//			if( clmTmp != null ){
//				// 6.0.4.0 (2014/11/28) selection は、Column から取得するのではなく、Factory で作成する。
//				selection = SelectionFactory.newSelection( "MENU",clmTmp.getCodeData(),null );	// 6.2.0.0 (2015/02/27) キー:ラベル形式
//			}
//		}

		// 5.7.4.2 (2014/03/20) ヘッダーのリソース適用見直し
		// 5.7.4.3 (2014/03/28) useHeaderResource 単独でリソース適用します。
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
		final DBColumn colClm = useHeaderResource ? table2.getDBColumn( colClmNo ) : null ;

		for( int keyNo=0; keyNo<clmKeys.length-1; keyNo++ ) {
			// 5.2.2.0 (2010/11/01) ColumnDisplay/NoDisplay 対応
			if( isColumnDisplay( headCount+keyNo ) ) {
				buf.append( "<th").append( colspan ).append( '>' );		// 6.0.2.5 (2014/10/31) char を append する。
//				// 7.0.1.5 (2018/12/10) headerCodeColumn 廃止
//				if( selection != null ){
//					buf.append( selection.getValueLabel( clmKeys[keyNo] ) );
//				}
				// 5.7.4.3 (2014/03/28) ヘッダーのリソース適用は、CLMカラムのカラム番号のみとします。
//				else if( colClm != null ) {
				if( colClm != null ) {
					buf.append( colClm.getRendererValue( clmKeys[keyNo] ) );
				}
				else{
					buf.append( clmKeys[keyNo] );
				}
				buf.append("</th>").append( CR );
			}
		}

		// 5.2.2.0 (2010/11/01) 最後のカラムが、合計行。
		// 5.0.0.3 (2009/09/22) firstClmGokei が trueの場合はcubeの先頭に出すようにします。
		if( ! firstClmGokei && gokeiClm != null ) {
			buf.append( gokeiClm );
		}

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

		// 7.0.1.7 (2019/01/21) ヘッダー作成時にオリジナルのカラムを使うので、インスタンス変数化します。
		int sumId = 0;			// 7.0.1.7 (2019/01/21)
		final String orgLbl = useHeaderColumn ? sumClms[sumId].getLabel() : null;		// sumId == 0
		if( sumCount > 1 ) {
			buf.append("<tr class=\"row_h\" >").append( CR );
			final int clmCnt = getColumnCount();		// 3.5.5.7 (2004/05/10)
			for( int column=headCount; column<clmCnt; column++ ) {
				if( isColumnDisplay( column ) ) {
					if( useHeaderColumn && sumId == 0 ) {		// 7.0.1.7 (2019/01/21)
						buf.append( "<th>").append( orgLbl ).append("</th>").append( CR );

					}
					else {
						buf.append( "<th>").append( getColumnLabel(column) ).append("</th>").append( CR );
					}
				}
				sumId++;
				if( sumId % sumCount == 0 ) {
					sumId = 0;
				}
			}
			buf.append("</tr>").append( CR );
		}

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

	/**
	 * クロス集計結果の DBTableModelオブジェクトを作成します。
	 *
	 * @og.rev 3.5.4.8 (2004/02/23) paramInit メソッドで、初期化を行います。
	 * @og.rev 3.5.6.3 (2004/07/12) 列ヘッダーのソート可否の指定を追加
	 * @og.rev 4.0.0.0 (2007/11/27) ヘッダーカラムのエディター、レンデラー適用対応
	 * @og.rev 4.3.5.7 (2008/03/22) ↑リソースが存在しない場合は、ラベルのみ入れ替え
	 * @og.rev 5.2.2.0 (2010/11/01) useHeaderColumn,useClassAdd 属性の追加
	 * @og.rev 5.7.4.3 (2014/03/28) useHeaderColumn の適用条件を、最初の集計カラムのみに変更。
	 * @og.rev 6.4.3.2 (2016/02/19) clmKey が null の場合の処理を追加。
	 * @og.rev 7.0.1.5 (2018/12/10) useHeaderColumn=true で、横軸カラムオブジェクトを置換します。
	 * @og.rev 7.0.1.7 (2019/01/21) useHeaderResourceの判定が逆になっていた。
	 *
	 * @param	table	入力もとの DBTableModelオブジェクト
	 *
	 * @return	DBTableModelオブジェクト
	 * @og.rtnNotNull
	 */
	private DBTableModel makeCrossTable( final DBTableModel table ) {
		final Set<String> clmData = gatSortAlgorithmSet();

		// 列のキーとなるカラムの値を取得します。
		final int rowCnt = table.getRowCount();		// 3.5.5.7 (2004/05/10)
		for( int row=0; row<rowCnt; row++ ) {
			final String clm = table.getValue( row,colClmNo );
			if( clm.length() > 0 ) { clmData.add( clm ); }
		}
		// ゼロストリングは、合計行になりますので、最後に追加します。

		// 3.5.6.3 (2004/07/12) ゼロストリングは、合計行になりますので、最後に追加します。
		clmKeys = clmData.toArray( new String[clmData.size() + 1] ) ;

		clmKeys[clmKeys.length-1] = "" ;

		final int numberOfColumns =  headCount + clmKeys.length * sumCount ;

		final DBTableModel tableImpl = DBTableModelUtil.newDBTable();
		tableImpl.init( numberOfColumns );

		// ヘッダーカラム(ROWデータ含む)は、そのまま、設定します。
		for( int column=0; column<headCount; column++ ) {
			tableImpl.setDBColumn( column,table.getDBColumn(column) );
		}

		// 列情報は、合計値のカラム定義を使用します。
		// 7.0.1.7 (2019/01/21) ヘッダー作成時にオリジナルのカラムを使うので、インスタンス変数化します。
//		DBColumn[] dbColumn = new DBColumn[sumCount];
		sumClms = new DBColumn[sumCount];
		for( int i=0; i<sumCount; i++ ) {
//			dbColumn[i] = table.getDBColumn(headCount + 1 + i);
			sumClms[i] = table.getDBColumn(headCount + 1 + i);
		}

		// 列情報は、列の名前をカラムの値に変えて、合計カラム列のコピー情報を設定します。

		int sumId = 0;
		final ResourceManager resource = getResourceManager();
		useHeaderColumn = useHeaderColumn && resource != null ;	// 5.2.2.0 (2010/11/01)
		// 5.2.2.0 (2010/11/01) useClassAdd 属性の追加

		clsAdd = new String[numberOfColumns];

		// 列情報カラムは、ヘッダー分に割り当てられる為、開始が、headCount からになります。
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );	// 6.1.0.0 (2014/12/26) refactoring

		// 7.0.1.5 (2018/12/10) useHeaderColumn=true で、横軸カラムオブジェクトを置換します。
		final DBColumn colClm = useHeaderColumn ? table2.getDBColumn( colClmNo ) : null ;		// 基準となるカラムのラベル取得用カラム

		for( int column=headCount; column<numberOfColumns; column++ ) {
//			DBColumn dbClm = dbColumn[sumId];
			DBColumn dbClm = sumClms[sumId];					// 7.0.1.7 (2019/01/21)
			final String clmKey  = clmKeys[ (column-headCount)/sumCount ];

			// 5.2.2.0 (2010/11/01) useClassAdd 属性の追加
			if( useClassAdd ) {
				 // ※ 特殊対応：cssなどで指定できるIDやCLASS属性は、先頭文字が数字の場合は、
				 // 無効になります。(つまり、効きません。)
				 // 表示ヘッダーは、年月や、社員番号(数字)などのケースもあります。そこで、先頭が数字の
				 // 場合は、"x"(小文字のx)を自動的に頭に追加します。
				buf.setLength(0);
				if( clmKey != null && clmKey.length() > 0 ) {
					final char ch = clmKey.charAt(0);
					if( ch >= '0' && ch <= '9' ) {
						buf.append( 'x' );		// 6.0.2.5 (2014/10/31) char を append する。
					}
					buf.append( clmKey );
				}

				final String nm = dbClm.getName();
				if( nm != null && nm.length() > 0 ) {
					buf.append( ' ' );			// 6.0.2.5 (2014/10/31) char を append する。
					final char ch = nm.charAt(0);
					if( ch >= '0' && ch <= '9' ) {
						buf.append( 'x' );		// 6.0.2.5 (2014/10/31) char を append する。
					}
					buf.append( nm );
				}
				clsAdd[column] = buf.toString();
			}

			// 5.7.4.3 (2014/03/28) useHeaderColumn の適用条件を、最初の集計カラムのみに変更。
			if( useHeaderColumn && sumId == 0 ) {
				// 6.4.3.2 (2016/02/19) clmKey が null の場合の処理を追加。
				if( clmKey == null || clmKey.isEmpty() ) {						// 合計カラムのこと
					final DBColumnConfig dbCfg2 = dbClm.getConfig();
					dbCfg2.setLabelData( resource.getLabelData( gokeiLabel ) );
					dbClm = new DBColumn( dbCfg2 );
				}
				else {
					final DBColumn clmTmp = resource.getDBColumn( clmKey );		// リソースがあれば優先します。
					if( clmTmp == null ) {
						final DBColumnConfig dbCfg2 = dbClm.getConfig();
						dbCfg2.setName( clmKey );
						// 7.0.1.7 (2019/01/21) useHeaderResourceの判定が逆になっていた。
						if( useHeaderResource ) {								// 7.0.1.5 (2018/12/10) useHeaderResource で、以前の方法
//							dbCfg2.setLabelData( resource.getLabelData( clmKey ) );
							dbCfg2.setLabelData( new LabelData( clmKey,colClm.getRendererValue( clmKey ) ) );	// 参照カラム
						}
						else {
//							dbCfg2.setLabelData( new LabelData( clmKey,colClm.getRendererValue( clmKey ) ) );	// 参照カラム
							dbCfg2.setLabelData( resource.getLabelData( clmKey ) );
						}
						dbClm = new DBColumn( dbCfg2 );
					}
					else {
						dbClm = dbClm;
					}
				}
			}

			tableImpl.setDBColumn( column,dbClm );

			sumId++;
			if( sumId % sumCount == 0 ) {
				sumId = 0;
			}
		}

		// クロス集計データの作成
		final CrossMap cross = new CrossMap( clmKeys,headCount,sumCount );
		for( int row=0; row<rowCnt; row++ ) {
			final String[] data = table.getValues( row );
			cross.add( data );
		}

		// データ部の設定
		final int size = cross.getSize();
		for( int row=0; row<size; row++ ) {
			tableImpl.addValues( cross.get( row ), row );
		}

		tableImpl.resetModify();

		final DBTableModel model ;
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		if( gokeiSortDir == null ) {
			model = tableImpl;
		}
		else {
			final DBTableModelSorter temp = new DBTableModelSorter();
			temp.setModel( tableImpl );

			final boolean direction = Boolean.parseBoolean( gokeiSortDir );		// 6.1.0.0 (2014/12/26) refactoring
			temp.sortByColumn( numberOfColumns-1,direction );
			model = temp ;
		}

		return model ;
	}

	/**
	 * 列ヘッダーのソート方法に応じた、Setオブジェクトを返します。
	 * ここでは、NUMBER , STRING , LOAD の３種類用意しています。
	 *
	 * @og.rev 3.5.6.3 (2004/07/12) 新規作成
	 *
	 * @return	ソート方法に応じたSetオブジェクト
	 * @og.rtnNotNull
	 */
	private Set<String> gatSortAlgorithmSet() {
		final Set<String> rtnSet ;

		if( "LOAD".equalsIgnoreCase( cubeSortType ) ) {
			rtnSet = new LinkedHashSet<>();
		}
		else if( "NUMBER".equalsIgnoreCase( cubeSortType ) ) {
			rtnSet = new TreeSet<>( NUMBER_SORT );
		}
		else if( "STRING".equalsIgnoreCase( cubeSortType ) ) {
			rtnSet = new TreeSet<>();
		}
		else {
			final String errMsg = "cubeSortType は、NUMBER,STRING,LOAD 以外指定できません。" +
							"  cubeSortType=[" + cubeSortType + "]";
			throw new HybsSystemException( errMsg );
		}

		return rtnSet ;
	}

	/**
	 * 表示不可カラム名を、CSV形式で与えます。
	 * 例："OYA,KO,HJO,SU,DYSET,DYUPD"
	 * null を与えた場合は,なにもしません。
	 *
	 * 注意：このクラスでは、DBTableModel を作り直すタイミングが、
	 * create メソッド実行時です。(パラメータの初期化が必要な為)
	 * よって、このメソッドは、初期が終了後に、再セットします。
	 *
	 * @og.rev 3.7.0.4 (2005/03/18) 新規作成
	 *
	 * @param	columnName	カラム名
	 */
	@Override
	public void setNoDisplay( final String columnName ) {
		noDisplayKeys = columnName;
	}

	/**
	 * 表示可能カラム名を、CSV形式で与えます。
	 * 例："OYA,KO,HJO,SU,DYSET,DYUPD"
	 * setColumnDisplay( int column,boolean rw ) の簡易版です。
	 * null を与えた場合は,なにもしません。
	 * また、全カラムについて、有効にする場合は、columnName="*" を設定します。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 新規追加
	 *
	 * @param	columnName	カラム名
	 */
	@Override
	public void setColumnDisplay( final String columnName ) {
		columnDisplayKeys = columnName;
	}

	/**
	 * NUMBER ソート機能(整数限定) 内部クラス
	 * これは通常のソートではなく、ヘッダーに使うラベルのソートなので、
	 * 整数のみと限定します。実数の場合は、桁合わせ(小数点以下の桁数)
	 * されているという前提です。
	 *
	 * @og.rev 3.5.6.3 (2004/07/12) 新規作成
	 */
	private static final class NumberComparator implements Comparator<String>,Serializable {
		private static final long serialVersionUID = 400020050131L ;	// 4.0.0.0 (2005/01/31)

		/**
		 * 順序付けのために2つの引数を比較します。
		 *
		 * Comparator<String> インタフェースの実装です。
		 *
		 * @param	s1	比較対象の最初のString
		 * @param	s2	比較対象の2番目のString
		 * @return	最初の引数が2番目の引数より小さい場合は負の整数、両方が等しい場合は0、最初の引数が2番目の引数より大きい場合は正の整数。
		 */
		@Override
		public int compare( final String s1, final String s2 ) {
			// 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(      s1.length() > s2.length() ) { rtn = 1;  }
			else if( s1.length() < s2.length() ) { rtn = -1; }
			else {
				rtn = s1.compareTo( s2 );
			}
			return rtn;
		}
	}

	/**
	 * 表示項目の編集(並び替え)が可能かどうかを返します。
	 *
	 * @og.rev 5.1.6.0 (2010/05/01) 新規追加
	 *
	 * @return	表示項目の編集(並び替え)が可能かどうか(false:不可能)
	 */
	@Override
	public boolean isEditable() {
		return false;
	}
}
