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

import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.model.NativeType;
import org.opengion.hayabusa.common.HybsSystemException;
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.1 (2016/02/12) refactoring
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Locale ;

/**
 * DBTableModel インターフェースを継承した TableModel の実装クラスです。
 * sql文を execute( query ) する事により,データベースを検索した結果を
 * DBTableModel に割り当てます。
 *
 * メソッドを宣言しています
 * DBTableModel インターフェースは，データベースの検索結果(Resultset)をラップする
 * インターフェースとして使用して下さい。
 *
 * @og.group テーブル管理
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class DBTableModelImpl implements DBTableModel {
	/** カラムオブジェクト配列 */
	protected	DBColumn[]			dbColumns	;
	/** カラム名称配列 */
	protected	String[]			names		;
	/** テータリスト */
	protected	List<String[]>		data		;
	/** 行ヘッダー情報 */
	protected	List<DBRowHeader>	rowHeader	;
	/**
	 * カラムアドレスマップ情報
	 * 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	*/
	protected	ConcurrentMap<String,Integer>	columnMap	;
	/** オーバーフローフラグ */
	protected	boolean				overflow	;

	/** カラム数 */
	protected   int			numberOfColumns		;

	// 3.5.5.5 (2004/04/23) 整合性キー(オブジェクトの作成時刻)追加
	/** 整合性キー(オブジェクトの作成時刻) */
	protected	String		consistencyKey	= String.valueOf( System.currentTimeMillis() );
	private 	String[]	lastData		;
	private 	int 		lastRow 		= -1;

	// 4.1.2.1 (2008/03/13) カラム(列)にmustタイプ値を割り当てます。
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private	final	ConcurrentMap<String,Set<String>> mustMap = new ConcurrentHashMap<>() ;	// 4.3.1.1 (2008/08/23) final化

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

	/**
	 * このオブジェクトを初期化します。
	 * 指定の引数分の内部配列を作成します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 実装を、Vector ，Hashtable から、ArrayList ，HashMapに、変更。
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 *
	 * @param   columnCount カラム数
	 */
	@Override	// DBTableModel
	public void init( final int columnCount ) {
		data			= new ArrayList<>( BUFFER_MIDDLE );
		rowHeader		= new ArrayList<>( BUFFER_MIDDLE );
		names			= new String[columnCount];
		dbColumns		= new DBColumn[ columnCount ];
		numberOfColumns	= columnCount;
		columnMap		= new ConcurrentHashMap<>();	// 6.4.3.1 (2016/02/12)
		lastRow			= -1;							// 3.5.5.7 (2004/05/10)
	}

	/**
	 * このオブジェクトをヘッダー部分をコピーし、データを初期化します。
	 * これは、カラムなどヘッダー系の情報は、元と同じオブジェクトを共有し、
	 * データ部のみ空にした DBTableModel を作成することを意味します。
	 * この際、consistencyKey も複写しますので、整合性は崩れないように、
	 * データ登録を行う必要があります。
	 *
	 * @og.rev 4.0.0.0 (2007/06/28) 新規作成
	 *
	 * @return  DBTableModelオブジェクト
	 */
	@Override	// DBTableModel
	public DBTableModel newModel() {
		final DBTableModelImpl table = new DBTableModelImpl();

		table.data				= new ArrayList<>( BUFFER_MIDDLE );
		table.rowHeader			= new ArrayList<>( BUFFER_MIDDLE );
		table.names				= names;
		table.dbColumns			= dbColumns;
		table.numberOfColumns	= numberOfColumns;
		table.columnMap			= columnMap;
		table.lastRow			= -1;
		table.consistencyKey	= consistencyKey;

		return table ;
	}

	/**
	 * カラム名配列を返します。
	 *
	 * @og.rev 3.0.0.0 (2002/12/25) カラム名配列を取得するメソッドを追加する。
	 * @og.rev 3.5.6.0 (2004/06/18) 配列をそのまま返さずに、clone して返します。
	 * @og.rev 3.6.0.0 (2004/09/22) names が null の場合は、初期設定エラーとします。
	 *
	 * @return	カラム名配列
	 * @og.rtnNotNull
	 */
	@Override	// DataModel
	public String[] getNames() {
		if( names != null ) {
			return names.clone();
		}

		final String errMsg = "カラム名配列が、初期化されていません。";
		throw new HybsSystemException( errMsg );
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//   DBTableModelImpl 独自の実装部分
	//
	//////////////////////////////////////////////////////////////////////////

	/**
	 * column に対応した 値を登録します。
	 * column には、番号ではなく､ラベルを指定します。
	 * 指定の行番号が、内部のデータ件数より多い場合は、データを追加します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow    値が変更される行
	 * @param   columnName    値が変更されるカラム名
	 * @param   value   新しい値。null も可
	 */
	public void setValue( final int aRow, final String columnName, final String value ) {
		final int aColumn = getColumnNo( columnName );
		final int size = getRowCount();
		if( size > aRow ) {
			setRowHeader( aRow,UPDATE_TYPE );
			setValueAt( value , aRow, aColumn );
		}
		else {
			for( int i=0; i< (aRow-size)+1; i++ ) {
				final String[] columnValues = new String[numberOfColumns];
				Arrays.fill( columnValues,"" );					// 6.1.0.0 (2014/12/26) refactoring
				addColumnValues( columnValues );
			}
			setValueAt( value , aRow, aColumn );
		}
	}

	/**
	 * 行を削除します。
	 * 物理削除ではなく、論理削除です。
	 * データを取り込むことは可能です。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow    論理削除される行
	 */
	public void rowDelete( final int aRow ) {
		setRowHeader( aRow,DELETE_TYPE );
	}

	/**
	 * row にあるセルのオブジェクト値を置き換えて、行を削除します。
	 * 物理削除ではなく、論理削除です。
	 * 値を置き換えたデータを取り込むことが可能です。
	 *
	 * @og.rev 3.5.4.2 (2003/12/15) 新規追加
	 *
	 * @param   values  新しい配列値。
	 * @param   aRow    論理削除される行
	 *
	 */
	public void rowDelete( final String[] values, final int aRow ) {
		if( numberOfColumns == values.length ) {		// 3.5.5.7 (2004/05/10)
			setRowHeader( aRow,DELETE_TYPE );
			data.set( aRow,values );
			lastRow = -1;				// 3.5.5.7 (2004/05/10)
		}
		else {
			final String errMsg = "カラム名の個数が不一致です。 [" + numberOfColumns + "] : [" + values.length + "]"
								+ " values=" + StringUtil.array2csv( values ) ;		// 5.1.8.0 (2010/07/01) errMsg 修正
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 行を物理削除します。
	 * メモリ上で編集する場合に使用しますが,一般アプリケーションからの
	 * 使用は、物理削除の為,お勧めいたしません。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow    物理削除される行
	 *
	 */
	public void removeValue( final int aRow ) {
		data.remove( aRow );
		rowHeader.remove( aRow );
		lastRow = -1;				// 3.5.5.7 (2004/05/10)
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//   DBTableModel インターフェースの実装部分
	//
	//////////////////////////////////////////////////////////////////////////

	/**
	 * カラムのラベル名を返します。
	 * カラムの項目名に対して,見える形の文字列を返します。
	 * 一般には,リソースバンドルと組合せて,各国ロケール毎にラベルを
	 * 切替えます。
	 *
	 * @param   column カラム番号
	 *
	 * @return  カラムのラベル名
	 */
	@Override	// DBTableModel
	public String getColumnLabel( final int column ) {
		return dbColumns[column].getLabel();
	}

	/**
	 * row および column にあるセルの属性値をStringに変換して返します。
	 *
	 * @og.rev 3.5.5.7 (2004/05/10) 連続同一 row アクセスのキャッシュ利用対応
	 *
	 * @param   aRow     値が参照される行
	 * @param   aColumn  値が参照される列
	 *
	 * @return  指定されたセルの値 String
	 */
	@Override	// DataModel
	public String getValue( final int aRow, final int aColumn ) {
		if( aRow != lastRow ) {
			lastData = data.get(aRow);
			lastRow = aRow ;
		}
		return lastData[aColumn] ;
	}

	/**
	 * row および columnName にあるセルの属性値をStringに変換して返します。
	 *
	 * @param   aRow       値が参照される行
	 * @param   columnName 値が参照されるカラム名
	 *
	 * @return  指定されたセルの値 String
	 * @see #getValue( int , int )
	 */
	@Override	// DBTableModel
	public String getValue( final int aRow, final String columnName ) {
		return getValue( aRow,getColumnNo( columnName ) );
	}

	/**
	 * カラム(列)にカラムオブジェクトを割り当てます。
	 * カラムオブジェクトは，ラベルやネームなど，そのカラム情報を
	 * 保持したオブジェクトです。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   clm        ヘッダーを適応するカラム(列)
	 * @param   dbColumn   カラムオブジェクト
	 */
	@Override	// DBTableModel
	public void setDBColumn( final int clm, final DBColumn dbColumn ) {
		dbColumns[clm] = dbColumn;
		names[clm]     = dbColumn.getName();
		columnMap.put( names[clm].toUpperCase(Locale.JAPAN),Integer.valueOf( clm ) );
	}

	/**
	 * カラム(列)のカラムオブジェクトを返します。
	 * カラムオブジェクトは，ラベルやネームなど，そのカラム情報を
	 * 保持したオブジェクトです。
	 *
	 * @param	clm	ヘッダーを適応するカラム(列)
	 *
	 * @return	カラムオブジェクト
	 */
	@Override	// DBTableModel
	public DBColumn getDBColumn( final int clm ) {
		return dbColumns[ clm ];
	}

	/**
	 * カラムオブジェクト配列を返します。
	 * カラムオブジェクトは，ラベルやネームなど，そのカラム情報を
	 * 保持したオブジェクトです。
	 *
	 * @og.rev 4.0.0.0 (2005/12/31) 新規追加
	 *
	 * @return	カラムオブジェクト配列
	 */
	@Override	// DBTableModel
	public DBColumn[] getDBColumns() {
		final int size = dbColumns.length;
		final DBColumn[] clms = new DBColumn[size];
		System.arraycopy( dbColumns,0,clms,0,size );
		return clms;
	}

	/**
	 * カラム名をもとに、そのカラム番号を返します。
	 * カラム名が存在しない場合は､ HybsSystemException を throw します。
	 *
	 * @param   columnName   カラム名
	 *
	 * @return  カラム番号
	 * @see #getColumnNo( String ,boolean )
	 */
	@Override	// DataModel
	public int getColumnNo( final String columnName ) {
		return getColumnNo( columnName,true );
	}

	/**
	 * カラム名をもとに、そのカラム番号を返します。
	 * useThrow が、true の場合は、カラム名が存在しない場合は､ HybsSystemException を
	 * throw します。useThrow が、false の場合は、カラム名が存在しない場合は､ -1 を返します。
	 *
	 * @og.rev 4.0.0.0 (2005/12/31) 新規追加
	 *
	 * @param   columnName   カラム名
	 * @param   useThrow     カラム名が存在しない場合に、Exception を throw するかどうか
	 *
	 * @return  カラム番号
	 * @see #getColumnNo( String )
	 */
	@Override	// DBTableModel
	public int getColumnNo( final String columnName,final boolean useThrow ) {
		if( columnName != null ) {
			final Integer no = columnMap.get( columnName.toUpperCase(Locale.JAPAN) );
			if( no != null ) { return no.intValue() ; }
		}

		if( useThrow ) {
			final String errMsg = "カラム名が存在しません:[" + columnName + "]" ;
			throw new HybsSystemException( errMsg );
		}
		else {
			return -1;
		}
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//   DBTableModel クラスのオーバーライド部分
	//
	//////////////////////////////////////////////////////////////////////////

	/**
	 * row の下に属性値配列を追加登録します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   values  属性値配列
	 * @param   aRow    値が参照される行
	 *
	 */
	@Override	// DBTableModel
	public void addValues( final String[] values ,final int aRow ) {
		addValues( values, aRow, true ); // 4.3.1.0 (2008/09/04)
	}

	/**
	 * row の下に属性値配列を追加登録します。
	 * isWritableをfalseにした場合、編集不可能な状態で追加されます。
	 *
	 * @og.rev 4.3.1.0 (2008/09/04) interface に新規登録
	 *
	 * @param   values  属性値配列
	 * @param   aRow    値が参照される行
	 * @param   isWritable 編集不可能な状態で追加するか
	 *
	 */
	@Override	// DBTableModel
	public void addValues( final String[] values ,final int aRow, final boolean isWritable ) {
		data.add( aRow,values );
		lastRow = -1;				// 3.5.5.7 (2004/05/10)

		final DBRowHeader rowhed = new DBRowHeader();
		if( isWritable ) {
			rowhed.setType( INSERT_TYPE );
		}
		else {
			rowhed.setWritable( false );
			rowhed.setChecked( false );
		}
		rowHeader.add( aRow,rowhed );
	}

	/**
	 * row あるセルの属性値配列を追加登録します。
	 * これは,初期登録時のみに使用します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   values  属性値配列
	 */
	@Override	// DBTableModel
	public void addColumnValues( final String[] values ) {
		data.add( values );
		lastRow = -1;				// 3.5.5.7 (2004/05/10)
		rowHeader.add( new DBRowHeader() );
	}

	/**
	 * row あるセルの属性値配列を追加登録します。
	 * これは,初期登録時のみに使用します。
	 * このメソッドでは、同時に、変更タイプ と、書込み許可を指定できます。
	 *
	 * @og.rev 6.2.2.0 (2015/03/27) interface に変更タイプ と、書込み許可を追加
	 *
	 * @param   values   属性値配列
	 * @param   modType  変更タイプ(追加/変更/削除)
	 * @param   rw 書込み可能(true)／不可能(false)
	 */
	@Override	// DBTableModel
	public void addColumnValues( final String[] values , final String modType , final boolean rw ) {
		data.add( values );
		lastRow = -1;				// 3.5.5.7 (2004/05/10)

		final DBRowHeader rowhed = new DBRowHeader();
		if( modType != null ) {
			rowhed.setType( modType );
		}
		rowhed.setWritable( rw );

		rowHeader.add( rowhed );
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//             Implementation of the TableModel Interface
	//
	//////////////////////////////////////////////////////////////////////////

	// MetaData

	/**
	 * カラム名を取得します。
	 *
	 * @param   column  最初のカラムは 0、2番目のカラムは 1、などとする。
	 *
	 * @return  カラム名
	 *
	 */
	public String getColumnName( final int column ) {
		return names[column];
	}

	/**
	 * データテーブル内の列の数を返します。
	 *
	 * @return  モデルの列数
	 *
	 */
	public int getColumnCount() {
		return numberOfColumns ;
	}

	/**
	 * データテーブル内の行の数を返します。
	 *
	 * @return  モデルの行数
	 *
	 */
	@Override	// DataModel
	public int getRowCount() {
		return data.size() ;
	}

	/**
	 * column および row にあるセルのオブジェクト値を設定します。
	 * このメソッドは、行番号の範囲チェックや、列番号のチェックを行いません。
	 * また、登録に際して、更新マーカー(UPDATE_TYPE等)を設定しません。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 3.5.3.1 (2003/10/31) インターフェースの見直しにより、private 化する。
	 * @og.rev 4.0.0.0 (2007/05/24) インターフェースの見直しにより、public 化する。
	 *
	 * @param   value   新しい値。null も可
	 * @param   aRow    値が変更される行
	 * @param   aColumn 値が変更される列
	 */
	public void setValueAt( final String value, final int aRow, final int aColumn ) {
		String[] row = data.get(aRow);
		row[ aColumn ] = value;
		data.set( aRow,row );
		lastRow = -1;				// 3.5.5.7 (2004/05/10)
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//             DBTableModel 独自追加分
	//
	//////////////////////////////////////////////////////////////////////////

	/**
	 * row にあるセルの属性値を配列で返します。
	 *
	 * @param   aRow     値が参照される行
	 *
	 * @return  指定されたセルの属性値
	 *
	 */
	@Override	// DataModel
	public String[] getValues( final int aRow ) {
		return data.get(aRow);
	}

	/**
	 * row にあるセルのオブジェクト値を置き換えます。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   values  新しい配列値。
	 * @param   aRow    値が変更される行
	 *
	 */
	@Override	// DataModel
	public void setValues( final String[] values, final int aRow ) {
		if( numberOfColumns == values.length ) {		// 3.5.5.7 (2004/05/10)
			setRowHeader( aRow,UPDATE_TYPE );
			data.set( aRow,values );
			lastRow = -1;				// 3.5.5.7 (2004/05/10)
		}
		else {
			final String errMsg = "カラム名の個数が不一致です。 [" + numberOfColumns + "] : [" + values.length + "]"
								+ " values=" + StringUtil.array2csv( values ) ;		// 5.1.8.0 (2010/07/01) errMsg 修正
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 変更済みフラグを元に戻します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * 一般には,データベースにテーブルモデルを登録するタイミングで、
	 * 変更済みフラグを元に戻します。
	 *
	 */
	@Override	// DBTableModel
	public void resetModify() {
		final int size = rowHeader.size() ;
		DBRowHeader row ;
		for( int i=0; i<size; i++ ) {
			row = rowHeader.get( i );
			row.clear();
		}
	}

	/**
	 * 変更済みフラグを元に戻します。
	 *
	 * 一般には,データベースにテーブルモデルを登録するタイミングで、
	 * 変更済みフラグを元に戻します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow     値が参照される行
	 */
	@Override	// DBTableModel
	public void resetModify( final int aRow ) {
		final DBRowHeader row = rowHeader.get( aRow );
		row.clear();
	}

	/**
	 * row 単位に変更されたタイプ(追加/変更/削除)を返します。
	 * タイプは始めに一度登録するとそれ以降に変更はかかりません。
	 * つまり、始めに 追加で作成したデータは、その後変更があっても追加のままです。
	 * なにも変更されていない場合は, ""(ゼロストリング)を返します。
	 *
	 * @param   aRow     値が参照される行
	 *
	 * @return  変更されたタイプの値 String
	 *
	 */
	@Override	// DataModel
	public String getModifyType( final int aRow ) {
		final DBRowHeader row = rowHeader.get( aRow );
		return row.getType();
	}

	/**
	 * row 単位に変更タイプ(追加/変更/削除)をセットします。
	 * このメソッドでは、データのバックアップは取りません。
	 * タイプは始めに一度登録するとそれ以降に変更はかかりません。
	 * なにも変更されていない場合は, ""(ゼロストリング)の状態です。
	 *
	 * @param   aRow     値が参照される行
	 * @param   modType  変更タイプ(追加/変更/削除)
	 *
	 */
	@Override	// DataModel
	public void setModifyType( final int aRow,final String modType ) {
		final DBRowHeader rowhed = rowHeader.get( aRow );
		rowhed.setType( modType );
	}

	/**
	 * row 単位に変更タイプ(追加/変更/削除)をセットします。
	 * セットすると同時に、データのバックアップを取ります。
	 * タイプは始めに一度登録するとそれ以降に変更はかかりません。
	 * つまり、始めに 追加で作成したデータは、その後変更があっても追加のままです。
	 * なにも変更されていない場合は, ""(ゼロストリング)の状態です。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) setBackupData 側で 配列をコピーしているため、こちらでは不要。
	 * @og.rev 3.5.6.4 (2004/07/16) protected 化します。
	 *
	 * @param   aRow     値が参照される行
	 * @param   modType  変更タイプ(追加/変更/削除)
	 */
	protected void setRowHeader( final int aRow,final String modType ) {
		final DBRowHeader rowhed = rowHeader.get( aRow );

		rowhed.setBackupData( data.get(aRow) );
		rowhed.setType( modType );
	}

	/**
	 * 変更データを初期値(元の取り込んだ状態)に戻します。
	 *
	 * 変更タイプ(追加/変更/削除)に応じて、処理されます。
	 * 追加時は、追加された行を削除します。
	 * 変更時は、変更された行を元に戻します。
	 * 削除時は、削除フラグを解除します。
	 * それ以外の場合(変更されていない場合)は、なにもしません。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 3.5.4.2 (2003/12/15) "DELETE" 時に値を置き換えた場合にUPDATEと同様に戻します。
	 *
	 * @param   aRow    処理を戻す(取り消す)行
	 */
	public void resetRow( final int aRow ) {
		final String modType = getModifyType(aRow) ;

		if( modType.equals( INSERT_TYPE ) ) {
			data.remove( aRow );
			rowHeader.remove( aRow );
		}
		else if( modType.equals( UPDATE_TYPE ) ||
				 modType.equals( DELETE_TYPE ) ) {
			final DBRowHeader row = rowHeader.get( aRow );
			final String[] obj = row.getBackupData();
			if( obj != null ) { data.set( aRow,obj ); }
			row.clear();
		}
		lastRow = -1;				// 3.5.5.7 (2004/05/10)
	}

	/**
	 * データが更新された行番号の配列を返します。
	 *
	 * これは、変更があったデータの行番号の配列をピックアップします。
	 *
	 * @og.rev 7.4.2.0 (2021/04/30) 変更があったデータのみを処理するかどうか[true/false]を指定します(初期値:false)
	 *
	 * @return   行番号の配列
	 */
	public int[] getChangeRowNos() {
		final List<Integer> rows = new ArrayList<>();
		final int size = data.size() ;
		for( int aRow=0; aRow<size; aRow++ ) {
			final String[] dat = data.get(aRow);
			final DBRowHeader head = rowHeader.get( aRow );
			final String[] obj = head.getBackupData();
			if( obj != null ) {
				for( int aCol=0; aCol<numberOfColumns; aCol++ ) {
					if( dat[aCol] != null && obj[aCol] != null  && !dat[aCol].equals( obj[aCol] ) ) {
						rows.add( aRow );
						break;
					}
				}
			}
		}

		// List<Integer> を、int[] に変換します。
		return rows.stream().mapToInt(i->i).toArray();
	}

	/**
	 * 書込み許可を返します。
	 *
	 * @param   aRow     値が参照される行
	 *
	 * @return  書込み可能(true)／不可能(false)
	 */
	public boolean isRowWritable( final int aRow ) {
		final DBRowHeader row = rowHeader.get( aRow );
		return row.isWritable();
	}

	/**
	 * 行が書き込み可能かどうかをセットします。
	 * デフォルト/およびなにも設定しない場合は, DEFAULT_WRITABLE が
	 * 与えられています。
	 * これが true の場合は,書込み許可です。(チェックボックスを表示)
	 * false の場合は,書込み不許可(チェックボックスは表示されません。)
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow     値が参照される行
	 * @param   rw 書込み可能(true)／不可能(false)
	 */
	@Override	// DBTableModel
	public void setRowWritable( final int aRow ,final boolean rw ) {
		final DBRowHeader row = rowHeader.get( aRow );
		row.setWritable( rw );
	}

	/**
	 * 書き込み可能な行(rowWritable == true)のチェックボックスに対して
	 * 初期値を 選択済みか、非選択済みかを返します。
	 *
	 * @param   aRow      値が参照される行
	 *
	 * @return	初期値チェックON(true)／チェックOFF(false)
	 */
	public boolean isRowChecked( final int aRow ) {
		final DBRowHeader row = rowHeader.get( aRow );
		return row.isChecked();
	}

	/**
	 * 書き込み可能な行(rowWritable == true)のチェックボックスに対して
	 * 初期値を 選択済みにするか、非選択済みにするかを指定します。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param   aRow      値が参照される行
	 * @param   rw チェックON(true)／チェックOFF(false)
	 */
	@Override	// DBTableModel
	public void setRowChecked( final int aRow ,final boolean rw ) {
		final DBRowHeader row = rowHeader.get( aRow );
		row.setChecked( rw );
	}

	/**
	 * 行指定の書込み許可を与えます。
	 * 具体的には,チェックボックスの表示/非表示を指定します。
	 * これが true の場合は,書込み許可です。(チェックボックスを表示)
	 * false の場合は,書込み不許可(チェックボックスは表示されません。)
	 * 行毎に書込み許可/不許可を指定する場合は,１カラム目に writable
	 * カラムを用意して true/false を指定します。
	 * この writable カラムとの論理積により最終的にチェックボックスの
	 * 表示の ON/OFF が決まります。
	 * なにも設定しない場合は, ViewForm.DEFAULT_WRITABLE が設定されます。
	 *
	 * @param   rw 書込み可能(true)／不可能(false)
	 */
	@Override	// DBTableModel
	public void setDefaultRowWritable( final boolean rw ) {
		final int size = rowHeader.size() ;
		DBRowHeader row ;
		for( int i=0; i<size; i++ ) {
			row = rowHeader.get( i );
			row.setWritable( rw );
		}
	}

	/**
	 * 書き込み可能な行(rowWritable == true)のチェックボックスに対して
	 * 初期値を 選択済みにするか、非選択済みにするかを指定します。
	 *
	 * @param   rw 選択状態(true)／非選択状態(false)
	 */
	@Override	// DBTableModel
	public void setDefaultRowChecked( final boolean rw ) {
		final int size = rowHeader.size() ;
		DBRowHeader row ;
		for( int i=0; i<size; i++ ) {
			row = rowHeader.get( i );
			row.setChecked( rw );
		}
	}

	/**
	 * 検索結果が オーバーフローしたかどうかをチェックします。
	 * Query で検索した場合に、DB_MAX_ROW_COUNT または、Query.setMaxRowCount( int maxRowCount )
	 * で指定された値よりも検索結果が多い場合に、DBTableModel は、先の設定値までの
	 * データを取り込みます。そのときに、オーバーフローフラグを立てておくことで、最大件数を
	 * オーバーしたかどうかを判断します。
	 *
	 * @return   オーバーフロー(true)／正常(false)
	 */
	public boolean isOverflow() {
		return overflow;
	}

	/**
	 * 検索結果が オーバーフローしたかどうかを設定します。
	 * Query で検索した場合に、DB_MAX_ROW_COUNT または、Query.setMaxRowCount( int maxRowCount )
	 * で指定された値よりも検索結果が多い場合に、DBTableModel は、先の設定値までの
	 * データを取り込みます。そのときに、オーバーフローフラグを立てておくことで、最大件数を
	 * オーバーしたかどうかを判断します。
	 *
	 * @param   of オーバーフロー(true)／正常(false)
	 */
	@Override	// DBTableModel
	public void setOverflow( final boolean of ) {
		overflow = of;
	}

	/**
	 * 検索されたDBTableModelが登録時に同一かどうかを判断する為の 整合性キーを取得します。
	 *
	 * ここでの整合性は、同一セッション(ユーザー)毎にユニークかどうかで対応します。
	 * 分散環境(複数のセッション間)での整合性は、確保できません。
	 * 整合性キー は、オブジェクト作成時刻としますが、将来変更される可能性があります。
	 *
	 * @og.rev 3.5.5.5 (2004/04/23) 新規追加
	 *
	 * @return   整合性キー(オブジェクトの作成時刻)
	 */
	public String getConsistencyKey() {
		return consistencyKey;
	}

	/**
	 * カラムに定義されたDBTypeよりNativeタイプを返します。
	 * Nativeタイプはorg.opengion.fukurou.model.NativeTypeで定義されています。
	 *
	 * @og.rev 4.1.1.2 (2008/02/28) 新規追加
	 *
	 * @param  clm      値が参照される列
	 *
	 * @return Nativeタイプ
	 * @see org.opengion.fukurou.model.NativeType
	 */
	@Override	// DataModel
	public NativeType getNativeType( final int clm ) {
		return dbColumns[clm].getNativeType();
	}

	/**
	 * カラム(列)にmustタイプ値を割り当てます。
	 * この値は、columnCheck 時の nullCheck や mustAnyCheck の
	 * チェック対象カラムとして認識されます。
	 *
	 * ※ 6.8.1.4 (2017/08/25)
	 *    type に、null を指定できるようにします。その場合は、type="must" を
	 *    削除する動きになります。
	 *    本来なら、delMustType( int ) などのメソッド追加が良いのですが、
	 *    今回は、変更箇所を少ない目にするため、このメソッドのみで対応します。
	 *
	 * @og.rev 4.1.2.1 (2008/03/13) interface に新規登録
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 * @og.rev 6.8.1.4 (2017/08/25) mustに、false 指定が出来るようにします。
	 * @og.rev 6.9.3.1 (2018/04/02) mustに、clear 指定で、mustMap すべてをクリアします。
	 *
	 * @param   dbColumn  カラムオブジェクト
	 * @param   type      mustタイプ(must,mustAny,false)
	 */
	@Override	// DBTableModel
	public void addMustType( final int dbColumn, final String type ) {
		if( "clear".equalsIgnoreCase( type ) ) {
				mustMap.clear();
		}
		else if( type != null && names[dbColumn] != null ) {
			if( "false".equalsIgnoreCase( type ) ) {
				mustMap.remove( "must" );
			}
			else {
				// たぶん、やりすぎ
				// Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
				mustMap.computeIfAbsent( type , k -> new HashSet<>() ).add( names[dbColumn] );
			}
		}
	}

	/**
	 * mustType="must"時のカラム名を、文字列配列として返します。
	 * この値は、columnCheck 時の nullCheck のチェック対象カラムとして
	 * 認識されます。
	 * カラム名配列は、ソート済みです。
	 *
	 * @og.rev 4.1.2.1 (2008/03/13) interface に新規登録
	 *
	 * @return  mustType="must"時のカラム名配列(ソート済み)
	 */
	@Override	// DBTableModel
	public String[] getMustArray() {
		String[] rtn = null;

		final Set<String> set = mustMap.get( "must" );
		if( set != null && ! set.isEmpty() ) {
			rtn = set.toArray( new String[set.size()] );
			Arrays.sort( rtn );
		}
		return rtn ;
	}

	/**
	 * mustType="mustAny" 他のカラム名を、文字列配列として返します。
	 * この値は、columnCheck 時の mustAnyCheck のチェック対象カラムとして
	 * 認識されます。
	 * カラム名配列は、ソート済みです。
	 *
	 * @og.rev 4.1.2.1 (2008/03/13) interface に新規登録
	 *
	 * @return  mustType="mustAny"時のカラム名配列(ソート済み)
	 */
	@Override	// DBTableModel
	public String[] getMustAnyArray() {

		final List<String> list = new ArrayList<>();

		final String[] keys = mustMap.keySet().toArray( new String[mustMap.size()] );
		for( int i=0; i<keys.length; i++ ) {
			final String key = keys[i];
			if( ! "must".equals( key ) ) {
				final Set<String> set = mustMap.get( key );
				if( set != null && !set.isEmpty() ) {
					final String str = StringUtil.iterator2line( set.iterator(),"|" );
					list.add( str );
				}
			}
		}

		String[] rtn = null;
		if( ! list.isEmpty() ) {
			rtn = list.toArray( new String[list.size()] );
			Arrays.sort( rtn );
		}

		return rtn ;
	}
}
