(m_rowCount+10);
		// actual row count or timeout
		if (m_rowCount > 0 || m_rowCountTimeout) 
		{
			m_loader.setContext(ServerContext.getCurrentInstance());
			m_loaderFuture = Adempiere.getThreadPoolExecutor().submit(m_loader);
		}
		else
			m_loader.close();
		//
		m_changed = false;
		m_rowChanged = -1;
		m_inserting = false;
		return true;
	}	//	open
	/**
	 * Verify whether use of virtual buffer is supported
	 */
	private void verifyVirtual()
	{
		if (m_indexKeyColumn == -1)
		{
			m_virtual = false;
			return;
		}
		GridField[] fields = getFields();
		for(int i = 0; i < fields.length; i++)
		{
			if (fields[i].isKey() && i != m_indexKeyColumn)
			{
				m_virtual = false;
				return;
			}
		}
	}
	/**
	 *  Wait until asynchronous loading of Table and Lookup Fields is complete.
	 */
	public void loadComplete()
	{
		//  Wait for loader
		if (m_loaderFuture != null)
		{
			if (!m_loaderFuture.isDone())
			{
				try
				{
					m_loaderFuture.get();
				}
				catch (Exception ie)
				{
					log.log(Level.SEVERE, "Interrupted", ie);
				}
			}
		}
		//  wait for field lookup loaders
		for (int i = 0; i < m_fields.size(); i++)
		{
			GridField field = (GridField)m_fields.get(i);
			field.lookupLoadComplete();
		}
	}   //  loadComplete
	/**
	 *  Is Loading
	 *  @return true if loading is in progress
	 */
	public boolean isLoading()
	{
		if (m_loaderFuture != null && !m_loaderFuture.isDone())
			return true;
		return false;
	}   //  isLoading
	/**
	 * wait for the loading of data
	 * @param timeout timeout in milisecond. pass 0 or negative value for infinite wait
	 * @throws InterruptedException
	 * @throws ExecutionException
	 * @throws TimeoutException 
	 */
	public void waitLoading(long timeout) throws InterruptedException, ExecutionException, TimeoutException
	{
		if (m_loaderFuture != null && !m_loaderFuture.isDone()) {
			if (timeout > 0)
				m_loaderFuture.get(timeout, TimeUnit.MILLISECONDS);
			else
				m_loaderFuture.get();
		}
	}
	
	/**
	 *	Is it open?
	 *  @return true if opened
	 */
	public boolean isOpen()
	{
		return m_open;
	}	//	isOpen
	/**
	 *	Close Resultset
	 *  @param finalCall true for final call and perform clean up
	 */
	public void close (boolean finalCall)
	{
		if (!m_open)
			return;
		if (log.isLoggable(Level.FINE)) log.fine("final=" + finalCall);
		//  remove listeners
		if (finalCall)
		{
			DataStatusListener evl[] = (DataStatusListener[])listenerList.getListeners(DataStatusListener.class);
			for (int i = 0; i < evl.length; i++)
				listenerList.remove(DataStatusListener.class, evl[i]);
			TableModelListener ev2[] = (TableModelListener[])listenerList.getListeners(TableModelListener.class);
			for (int i = 0; i < ev2.length; i++)
				listenerList.remove(TableModelListener.class, ev2[i]);
			VetoableChangeListener vcl[] = m_vetoableChangeSupport.getVetoableChangeListeners();
			for (int i = 0; i < vcl.length; i++)
				m_vetoableChangeSupport.removeVetoableChangeListener(vcl[i]);
		}
		//	Stop loader
		while (m_loaderFuture != null && !m_loaderFuture.isDone())
		{
			if (log.isLoggable(Level.FINE))
				log.fine("Interrupting Loader ...");
			m_loaderFuture.cancel(true);
			try
			{
				Thread.sleep(200);		//	.2 second
			}
			catch (InterruptedException ie)
			{}
			m_loaderFuture = null;
		}
		if (!m_inserting)
			dataSave(false);	//	not manual
		if (m_buffer != null)
		{
			m_buffer.clear();
		}
		if (m_sort != null)
		{
			m_sort.clear();
		}
		if (m_virtualBuffer != null)
		{
			m_virtualBuffer.clear();
		}
		if (finalCall) {
			dispose();
			m_buffer = null;
			m_sort = null;
			m_virtualBuffer = null;
		}
		//  Fields are disposed from MTab
		if (log.isLoggable(Level.FINE))
			log.fine("");
		m_open = false;
	}	//	close
	/**
	 *  Clean up.
	 *  Called by close-final.
	 */
	private void dispose()
	{
		//  GridFields
		for (int i = 0; i < m_fields.size(); i++)
			((GridField)m_fields.get(i)).dispose();
		m_fields.clear();
		m_fields = null;
		//
		m_vetoableChangeSupport = null;
		//
		m_parameterSELECT.clear();
		m_parameterSELECT = null;
		m_parameterWHERE.clear();
		m_parameterWHERE = null;
		//  clear data arrays
		m_buffer = null;
		m_virtualBuffer = null;
		m_sort = null;
		m_rowData = null;
		m_oldValue = null;
		m_loader = null;
		m_loaderFuture = null;
	}   //  dispose
	/**
	 *	Get column count
	 *  @return column count
	 */
	public int getColumnCount()
	{
		return m_fields.size();
	}	//	getColumnCount
	/**
	 *	Get field count
	 *  @return field count
	 */
	public int getFieldCount()
	{
		return m_fields.size();
	}	//	getFieldCount
	/**
	 *  Return number of rows
	 *  @return Number of rows or 0 if not opened
	 */
	public int getRowCount()
	{
		return m_rowCount;
	}	//	getRowCount
	/**
	 *	Set the Column to determine the color of the row
	 *  @param columnName column name
	 */
	public void setColorColumn (String columnName)
	{
		m_indexColorColumn = findColumn(columnName);
	}	//  setColorColumn
	/**
	 *	Get ColorCode for Row.
	 *  
	 *	If numerical value in compare column is
	 *		negative = -1,
	 *      positive = 1,
	 *      otherwise = 0
	 *   
	 *  @see #setColorColumn
	 *  @param row row
	 *  @return color code
	 */
	public int getColorCode (int row)
	{
		if (m_indexColorColumn  == -1)
			return 0;
		Object data = getValueAt(row, m_indexColorColumn);
		//	We need to have a Number
		if (data == null || !(data instanceof BigDecimal))
			return 0;
		BigDecimal bd = (BigDecimal)data;
		return bd.signum();
	}	//	getColorCode
	/**
	 *	Sort records by Column.
	 *  
	 *  Actually the rows are not sorted, just the access pointer ArrayList
	 *  with the same size as m_buffer ({@link #m_sort}).
	 *  @param col col
	 *  @param ascending ascending
	 */
	public void sort (int col, boolean ascending)
	{
		if (log.isLoggable(Level.INFO)) log.info("#" + col + " " + ascending);
		if (col < 0) {
			return;
		}
		if (getRowCount() == 0)
			return;
		GridField field = getField(col);
		// Ignoring new record while sorting
		if (field.getGridTab().isQuickForm())
			dataIgnore();
		boolean isSameSortEntries = (col == m_lastSortColumnIndex && ascending == m_lastSortedAscending);
		if (!isSameSortEntries)
		{
			m_lastSortColumnIndex = col;
			m_lastSortedAscending = ascending;
		}
		//cache changed row
		MSort changedRow = m_rowChanged >= 0 ? (MSort)m_sort.get(m_rowChanged) : null;
		if (m_rowChanged == m_newRow)
			changedRow = null;
		Object[] changedRowData = changedRow != null ? getDataAtRow(m_rowChanged) : null;
		
		MSort newRow = m_newRow >= 0 ? (MSort)m_sort.get(m_newRow) : null;
		
		MSort currentRow = m_currentRow >= 0 && m_currentRow < m_sort.size() ? (MSort)m_sort.get(m_currentRow) : null;
		//	RowIDs are not sorted
		if (field.getDisplayType() == DisplayType.RowID)
			return;
		boolean isLookup = DisplayType.isLookup(field.getDisplayType());
		boolean isASI = DisplayType.PAttribute == field.getDisplayType();
		//	fill MSort entities with data entity
		for (int i = 0; i < m_sort.size(); i++)
		{
			MSort sort = (MSort)m_sort.get(i);
			Object[] rowData = getDataAtRow(i);
			if (rowData[col] == null)
				sort.data = null;
			else if (isLookup || isASI)
				sort.data = field.getLookup().getDisplay(rowData[col]);	//	lookup
			else
				sort.data = rowData[col];								//	data
		}
		if (log.isLoggable(Level.INFO)) log.info(field.toString() + " #" + m_sort.size());
		//	sort it
		MSort sort = new MSort(0, null);
		sort.setSortAsc(ascending);
		Collections.sort(m_sort, sort);
		if (m_virtual)
		{
			Object[] newRowData = newRow != null ? m_virtualBuffer.get(NEW_ROW_ID) : null;
			m_virtualBuffer.clear();
			if (newRow != null)
				m_virtualBuffer.put(NEW_ROW_ID, newRowData);
			if (changedRow != null)
			{				
				for(int i = 0; i < m_sort.size(); i++)
				{
					if (m_sort.get(i) == changedRow)
					{
						m_rowChanged = i;
						m_virtualBuffer.put(changedRow.index, changedRowData);
						break;
					}
				}
			}
			//release sort memory
			for (int i = 0; i < m_sort.size(); i++)
			{
				m_sort.get(i).data = null;
				if (newRow != null && m_sort.get(i) == newRow)
				{
					if (m_rowChanged == m_newRow)
						m_rowChanged = i;
					m_newRow = i;
				}
				
				if (currentRow != null && m_sort.get(i) == currentRow)
					m_currentRow = i;
			}
		}
		else
		{
			for (int i = 0; i < m_sort.size(); i++)
			{
				if (newRow != null && m_sort.get(i) == newRow)
				{
					if (m_rowChanged == m_newRow)
						m_rowChanged = i;
					m_newRow = i;
				}
				
				if (currentRow != null && m_sort.get(i) == currentRow)
					m_currentRow = i;
			}
		}
		
		if (!isSameSortEntries)
		{
			//  Info detected by MTab.dataStatusChanged and current row set to 0
			fireDataStatusIEvent(SORTED_DSE_EVENT, "#" + m_sort.size());
			//	update UI
			fireTableDataChanged();
		}
	}	//	sort
	/**
	 *	Get Key ID or -1 of none
	 *  @param row row index
	 *  @return ID or -1
	 */
	public int getKeyID (int row)
	{
		if (m_indexKeyColumn != -1)
		{
			try
			{
				Integer ii = (Integer)getValueAt(row, m_indexKeyColumn);
				if (ii == null)
					return -1;
				return ii.intValue();
			}
			catch (Exception e)     //  Alpha Key
			{
				return -1;
			}
		}
		return -1;
	}	//	getKeyID
	/**
	 *	Get Key UUID or null of none
	 *  @param row row index
	 *  @return UUID or null
	 */
	public String getKeyUUID (int row)
	{
		if (m_indexUUIDColumn != -1)
		{
			try
			{
				String ii = (String)getValueAt(row, m_indexUUIDColumn);
				if (ii == null)
					return null;
				return ii;
			}
			catch (Exception e)
			{
				return null;
			}
		}
		return null;
	}	//	getKeyUUID
	/**
	 *	Get UUID or null of none
	 *  @param row row index
	 *  @return UUID or null
	 */
	public UUID getUUID (int row)
	{
		String keyUUID = getKeyUUID(row);
		if (keyUUID != null)
			return UUID.fromString(keyUUID);
		return null;
	}	//	getUUID
	/**
	 *	Get Key ColumnName
	 *  @return key column name
	 */
	public String getKeyColumnName()
	{
		if (m_indexKeyColumn != -1)
			return getColumnName(m_indexKeyColumn);
		return "";
	}	//	getKeyColumnName
	/**
	 * 	Get Value at row and column
	 *  @param row row
	 *  @param col col
	 *  @return Value at row/column
	 */
	public Object getValueAt (int row, int col)
	{
		if (!m_open || row < 0 || col < 0 || row >= m_rowCount)
		{
			return null;
		}
		waitLoadingForRow(row);
		//	empty buffer
		if (row >= m_sort.size())
		{
			return null;
		}
		//	return Data item
		Object[] rowData = getDataAtRow(row);
		//	out of bounds
		if (rowData == null || col > rowData.length)
		{
			return null;
		}
		return rowData[col];
	}	//	getValueAt
	/**
	 * wait for loading of row
	 * @param row
	 */
	public void waitLoadingForRow(int row) {
		//	need to wait for data read into buffer
		int loops = 0;
		//wait for [timeout] seconds
		int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx()));
		while (row >= m_sort.size() && m_loaderFuture != null && !m_loaderFuture.isDone() && loops < timeout)
		{
			if (log.isLoggable(Level.FINE)) log.fine("Waiting for loader row=" + row + ", size=" + m_sort.size());
			try
			{
				m_loaderFuture.get(1000, TimeUnit.MILLISECONDS);
			}
			catch (Exception ie)
			{}
			loops++;
		}
		if (m_sort.size() == 0) {
			// check if there is a DB error saved to show
			Exception savedEx = CLogger.retrieveException();
			if (savedEx != null)
				throw new IllegalStateException(savedEx);
		}
		// zero rows found without load timeout
		if (row == 0 && m_sort.size() == 0 && m_rowCountTimeout && !m_rowLoadTimeout)
			throw new AdempiereException(Msg.getMsg(Env.getCtx(), "FindZeroRecords"));
		if (row >= m_sort.size()) {
			log.warning("Reached " + timeout + " seconds timeout loading row " + (row+1) + " for SQL=" + m_SQL);
			//adjust row count
			m_rowCount = m_sort.size();
			throw new AdempiereException(Msg.getMsg(Env.getCtx(), LOAD_TIMEOUT_ERROR_MESSAGE));
		}
	}
	/**
	 * @param row row index
	 * @return data at row index
	 */
	private Object[] getDataAtRow(int row)
	{
		return getDataAtRow(row, true);
	}
	/**
	 * @param row row index
	 * @param fetchIfNotFound
	 * @return data at row index
	 */
	private Object[] getDataAtRow(int row, boolean fetchIfNotFound)
	{
		waitLoadingForRow(row);
		MSort sort = (MSort)m_sort.get(row);
		Object[] rowData = null;
		if (m_virtual)
		{
			if (sort.index != NEW_ROW_ID && !(m_virtualBuffer.containsKey(sort.index)) && fetchIfNotFound)
			{
				fillBuffer(row, DEFAULT_FETCH_SIZE);
			}
			rowData = (Object[])m_virtualBuffer.get(sort.index);
		}
		else
		{
			rowData = (Object[])m_buffer.get(sort.index);
		}
		return rowData;
	}
	/**
	 * @param row row index
	 * @param rowData
	 */
	private void setDataAtRow(int row, Object[] rowData) {
		MSort sort = m_sort.get(row);
		if (m_virtual)
		{
			if (sort.index != NEW_ROW_ID && !(m_virtualBuffer.containsKey(sort.index)))
			{
				fillBuffer(row, DEFAULT_FETCH_SIZE);
			}
			m_virtualBuffer.put(sort.index, rowData);
		}
		else
		{
			m_buffer.set(sort.index, rowData);
		}
	}
	/**
	 * Fill virtual buffer ({@link #m_virtualBuffer}.
	 * @param start
	 * @param fetchSize
	 */
	private void fillBuffer(int start, int fetchSize)
	{
		//adjust start if needed
		if (start > 0)
		{
			if (start + fetchSize >= m_sort.size())
			{
				start = start - (fetchSize - ( m_sort.size() - start ));
				if (start < 0)
					start = 0;
			}
		}
		StringBuilder sql = new StringBuilder();
		sql.append(m_SQL_Select)
			.append(" WHERE ")
			.append(getKeyColumnName())
			.append(" IN (");
		Maprowmap = new LinkedHashMap(DEFAULT_FETCH_SIZE);
		int count = 0;
		for(int i = start; i < start+fetchSize && i < m_sort.size(); i++)
		{
			if (m_sort.get(i).index == NEW_ROW_ID)
					continue;
			if(count > 0)
				sql.append(",");
			sql.append(m_sort.get(i).index);
			rowmap.put(m_sort.get(i).index, i);
			count++;
		}
		sql.append(")");
		Object[] newRow = m_virtualBuffer.get(NEW_ROW_ID);
		//cache changed row
		Object[] changedRow = m_rowChanged >= 0 ? getDataAtRow(m_rowChanged, false) : null;
		m_virtualBuffer = new HashMap(210);
		if (newRow != null && newRow.length > 0)
			m_virtualBuffer.put(NEW_ROW_ID, newRow);		
		PreparedStatement stmt = null;
		ResultSet rs = null;
		m_rowLoadTimeout = false;
		try
		{
			stmt = DB.prepareStatement(sql.toString(), null);
			int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx()));
			if (timeout > 0)
				stmt.setQueryTimeout(timeout);
			rs = stmt.executeQuery();
			while(rs.next())
			{
				Object[] data = readData(rs);
				rowmap.remove(data[m_indexKeyColumn]);
				m_virtualBuffer.put((Integer)data[m_indexKeyColumn], data);
			}
			if (!rowmap.isEmpty())
			{
				List toremove = new ArrayList();
				for(Map.Entry entry : rowmap.entrySet())
				{
					toremove.add(entry.getValue());
				}
				Collections.reverse(toremove);
				for(Integer row : toremove)
				{
					m_sort.remove(row.intValue());
				}
			}
			
			if (changedRow != null && changedRow.length > 0)
			{
				if (changedRow[m_indexKeyColumn] != null && (Integer)changedRow[m_indexKeyColumn] > 0)
				{
					m_virtualBuffer.put((Integer)changedRow[m_indexKeyColumn], changedRow);
				}
			}
		}
		catch (SQLException e)
		{
			if (DB.getDatabase().isQueryTimeout(e))
				m_rowLoadTimeout = true;
			log.log(Level.SEVERE, e.getLocalizedMessage(), e);
		}
		finally
		{
			DB.close(rs, stmt);
		}
	}
	
	/**
	 *	Indicate that there will be a change
	 *  @param changed changed
	 */
	public void setChanged (boolean changed)
	{
		//	Can we edit?
		if (!m_open || m_readOnly)
			return;
		//	Indicate Change
		m_changed = changed;
		if (!changed)
			m_rowChanged = -1;
	}	//	setChanged
	/**
	 * 	Set value at row and column
	 *
	 *  @param  value value to assign to cell
	 *  @param  row row index of cell
	 *  @param  col column index of cell
	 */
	public final void setValueAt (Object value, int row, int col)
	{
		setValueAt (value, row, col, false, false);
	}	//	setValueAt
	/**
	 * 	call {@link #setValueAt(Object, int, int, boolean, boolean)} with isInitEdit = false
	 *
	 *  @param  value value to assign to cell
	 *  @param  row row index of cell
	 *  @param  col column index of cell
	 * 	@param	force force setting new value
	 */
	public final void setValueAt (Object value, int row, int col, boolean force)
	{
		setValueAt (value, row, col, force, false);
	}	//	setValueAt
	
	/**
	 * 	Set value in row data and update GridField.
	 *
	 *  @param  value value to assign to cell
	 *  @param  row row index of cell
	 *  @param  col column index of cell
	 * 	@param	force force setting new value
	 *  @param	isInitEdit indicate event rise by start edit a field. just want change status to edit, don't change anything else
	 */
	public final void setValueAt (Object value, int row, int col, boolean force, boolean isInitEdit)
	{
		//	Can we edit?
		if (!m_open || m_readOnly       //  not accessible
				|| row < 0 || col < 0   //  invalid index
				|| m_rowCount == 0	//  no rows
				|| row >= m_rowCount )     //invalid row
		{
			if (log.isLoggable(Level.FINEST)) log.finest("r=" + row + " c=" + col + " - R/O=" + m_readOnly + ", Rows=" + m_rowCount + " - Ignored");
			return;
		}
		dataSave(row, false);
		//	Has anything changed?
		Object oldValue = getValueAt(row, col);
		if (!force && !isInitEdit && !isValueChanged(oldValue, value) )
		{
			if (log.isLoggable(Level.FINEST)) log.finest("r=" + row + " c=" + col + " - New=" + value + "==Old=" + oldValue + " - Ignored");
			return;
		}
		if (log.isLoggable(Level.FINE)) log.fine("r=" + row + " c=" + col + " = " + value + " (" + oldValue + ")");
		//  Save old value
		m_oldValue = new Object[3];
		m_oldValue[0] = Integer.valueOf(row);
		m_oldValue[1] = Integer.valueOf(col);
		m_oldValue[2] = oldValue;
		//	Set Data item
		
		Object[] rowData = getDataAtRow(row);
		m_rowChanged = row;
		//	save original value - shallow copy
		if (m_rowData == null)
		{
			int size = m_fields.size();
			m_rowData = new Object[size];
			for (int i = 0; i < size; i++)
				m_rowData[i] = rowData[i];
		}
		//	save & update
		rowData[col] = value;
		setDataAtRow(row, rowData);
		//  update Table
		fireTableCellUpdated(row, col);
		//  update GridField
		GridField field = getField(col);
		field.setValue(value, m_inserting);
		//  inform
		DataStatusEvent evt = createDSE();
		evt.setIsInitEdit(isInitEdit);
		evt.setChangedColumn(col, field.getColumnName());
		fireDataStatusChanged(evt);
	}	//	setValueAt
	/**
	 *  Get Old Value
	 *  @param row row
	 *  @param col col
	 *  @return old value
	 */
	public Object getOldValue (int row, int col)
	{
		if (m_oldValue == null)
			return null;
		if (((Integer)m_oldValue[0]).intValue() == row
				&& ((Integer)m_oldValue[1]).intValue() == col)
			return m_oldValue[2];
		return null;
	}   // getOldValue
	/**
	 *	Check if {@link #m_rowChanged} needs to be saved.
	 *  @param  onlyRealChange if true the value of a field was actually changed
	 *  (e.g. for new records, which have not been changed) - default false
	 *	@return true if needs to be saved
	 */
	public boolean needSave(boolean onlyRealChange)
	{
		return needSave(m_rowChanged, onlyRealChange);
	}   //  needSave
	/**
	 *	Check if {@link #m_rowChanged} needs to be saved.
	 *	@return true if needs to be saved
	 */
	public boolean needSave()
	{
		return needSave(m_rowChanged, false);
	}   //  needSave
	/**
	 *	Check if newRow needs to be saved.
	 *	@param	newRow to check
	 *	@return true if needs to be saved
	 */
	public boolean needSave(int newRow)
	{
		return needSave(newRow, false);
	}   //  needSave
	/**
	 *	Check if the row needs to be saved.
	 *  - only when row changed
	 *  - only if nothing was changed
	 *	@param	newRow to check
	 *  @param  onlyRealChange if true, only if the value of a field was actually changed
	 *  (e.g. for new record with default value, which have not been changed) - default false
	 *	@return true it needs to be saved
	 */
	public boolean needSave(int newRow, boolean onlyRealChange)
	{
		if (log.isLoggable(Level.FINE))
			log.fine("Row=" + newRow +
					", Changed=" + m_rowChanged + "/" + m_changed);  //  m_rowChanged set in setValueAt
		//  nothing done
		if (!m_changed && m_rowChanged == -1)
			return false;
		//  E.g. New unchanged records
		if (m_changed && m_rowChanged == -1 && onlyRealChange)
			return false;
		//  same row
		if (newRow == m_rowChanged)
			return false;
		return true;
	}	//	needSave
	/*************************************************************************/
	/** Save OK - O		*/
	public static final char	SAVE_OK = 'O';			//	the only OK condition
	/** Save Error - E	*/
	public static final char	SAVE_ERROR = 'E';
	/** Save Access Error - A	*/
	public static final char	SAVE_ACCESS = 'A';
	/** Save Mandatory Error - M	*/
	public static final char	SAVE_MANDATORY = 'M';
	/** Save Abort Error - U	*/
	public static final char	SAVE_ABORT = 'U';
	/**
	 *	Check if it needs to be saved and save it.
	 *  @param newRow row index
	 *  @param manualCmd true if initiated from user action
	 *	@return true if not needed to be saved or successful saved
	 */
	public boolean dataSave (int newRow, boolean manualCmd)
	{
		if (log.isLoggable(Level.FINE)) log.fine("Row=" + newRow +
			", Changed=" + m_rowChanged + "/" + m_changed);  //  m_rowChanged set in setValueAt
		//  nothing done
		if (!m_changed && m_rowChanged == -1)
			return true;
		//  same row, don't save yet
		if (newRow == m_rowChanged)
			return true;
		return (dataSave(manualCmd) == SAVE_OK);
	}   //  dataSave
	/**
	 *	Save changes.
	 *  @param manualCmd if true (i.e initiated from user action), no vetoable PropertyChange will be fired for save confirmation
	 *	@return OK Or Error condition
	 *  Error info (Access*, FillMandatory, SaveErrorNotUnique,
	 *  SaveErrorRowNotFound, SaveErrorDataChanged) is saved in the log
	 */
	public char dataSave (boolean manualCmd)
	{
		//	cannot save
		if (!m_open)
		{
			log.warning ("Error - Open=" + m_open);
			return SAVE_ERROR;
		}
		//	no need - not changed - row not positioned - no Value changed
		if (m_rowChanged == -1)
		{
			if (log.isLoggable(Level.CONFIG)) log.config("NoNeed - Changed=" + m_changed + ", Row=" + m_rowChanged);
			if (!manualCmd)
				return SAVE_OK;
		}
		//  Value not changed
		if (m_rowData == null)
		{
			//reset out of sync variable
			m_rowChanged = -1;
			if (log.isLoggable(Level.FINE))
				log.fine("No Changes");
			return SAVE_ERROR;
		}
		if (m_readOnly)
		//	If Processed - not editable (Find always editable)  -> ok for changing payment terms, etc.
		{
			if (log.isLoggable(Level.WARNING))
				log.warning("IsReadOnly - ignored");
			dataIgnore();
			return SAVE_ACCESS;
		}
		//	row not positioned - no Value changed
		if (m_rowChanged == -1)
		{
			if (m_newRow != -1)     //  new row and nothing changed - might be OK
				m_rowChanged = m_newRow;
			else
			{
				fireDataStatusEEvent("SaveErrorNoChange", "", true);
				return SAVE_ERROR;
			}
		}
		//	Can we change?
		int[] co = getClientOrg(m_rowChanged);
		int AD_Client_ID = co[0]; 
		int AD_Org_ID = co[1];
		if (!MRole.getDefault(m_ctx, false).canUpdate(AD_Client_ID, AD_Org_ID, m_AD_Table_ID, 0, true))
		{
			fireDataStatusEEvent(CLogger.retrieveError());
			dataIgnore();
			return SAVE_ACCESS;
		}
		if (log.isLoggable(Level.INFO)) log.info("Row=" + m_rowChanged);
		//  inform about data save action, if not manually initiated
		try
		{
			if (!manualCmd)
				m_vetoableChangeSupport.fireVetoableChange(PROPERTY, -1, m_rowChanged);
		}
		catch (PropertyVetoException pve)
		{
			log.warning(pve.getMessage());
			return SAVE_ABORT;
		}
		//	get updated row data
		Object[] rowData = getDataAtRow(m_rowChanged);
		//	Check Mandatory
		String missingColumns = getMandatory(rowData);
		if (missingColumns.length() != 0)
		{
			fireDataStatusEEvent("FillMandatory", missingColumns + "\n", true);
			return SAVE_MANDATORY;
		}
		/**
		 *	Update row *****
		 */
		int Record_ID = 0;
		if (!m_inserting)
			Record_ID = getKeyID(m_rowChanged);
		try
		{
			return dataSavePO (Record_ID);
		}
		catch (Throwable e)
		{
			if (e instanceof ClassNotFoundException)
				log.warning(m_tableName + " - " + e.getLocalizedMessage());
			else
			{
				log.log(Level.SEVERE, "Persistency Issue - " 
					+ m_tableName + ": " + e.getLocalizedMessage(), e);
				log.saveError("Error", e.getLocalizedMessage());
			}
		}
		return SAVE_ERROR;
	}	//	dataSave
	/**
	 * 	Save via PO
	 *	@param Record_ID
	 *	@return SAVE_ERROR or SAVE_OK
	 *	@throws Exception
	 */
	private char dataSavePO (int Record_ID) throws Exception
	{
		if (log.isLoggable(Level.FINE)) log.fine("ID=" + Record_ID);
		//
		Object[] rowData = getDataAtRow(m_rowChanged);
		//
		MTable table = MTable.get (m_ctx, m_AD_Table_ID);
		PO po = null;
		if (! m_importing) // Just use trx when importing
			m_trxName = null;
		if (Record_ID != -1)
		{
			if (Record_ID == 0 && !m_inserting && MTable.isZeroIDTable(table.getTableName())) {
				String uuidFromZeroID = table.getUUIDFromZeroID();
				po = table.getPOByUU(uuidFromZeroID, m_trxName);
			} else {
				po = table.getPO(Record_ID, m_trxName);
			}
		}
		else	//	Multi - Key
			po = table.getPO(getWhereClause(rowData), m_trxName);
		//	No Persistent Object
		if (po == null)
			throw new ClassNotFoundException ("No Persistent Object");
		
		if (!po.is_new())
		{
			if (hasChanged(po))
			{				
				// return error stating that current record has changed and it cannot be saved
				String adMessage = "CurrentRecordModified";
				String msg = Msg.getMsg(Env.getCtx(), adMessage);
				fireDataStatusEEvent(adMessage, msg, true);
				return SAVE_ERROR;
			}
		}
		
		int size = m_fields.size();
		for (int col = 0; col < size; col++)
		{
			GridField field = (GridField)m_fields.get (col);
			if (field.isVirtualColumn())
				continue;
			String columnName = field.getColumnName ();
			Object value = rowData[col];
			Object oldValue = m_rowData[col];
			//	RowID
			if (field.getDisplayType() == DisplayType.RowID)
				; 	//	ignore
			//	Nothing changed & null
			else if (oldValue == null && value == null)
				;	//	ignore
			
			//	***	Data changed ***
			else if (m_inserting || isValueChanged(oldValue, value) )
			{
				//	Check existence
				int poIndex = po.get_ColumnIndex(columnName);
				if (poIndex < 0)
				{
					//	Custom Fields not in PO
					po.set_CustomColumn(columnName, value);
				//	log.log(Level.SEVERE, "Column not found: " + columnName);
					continue;
				}
				
				Object dbValue = po.get_Value(poIndex);
				if (m_inserting 
					|| !m_compareDB
					//	Original == DB
					|| (oldValue == null && dbValue == null)
					|| (oldValue != null && oldValue.equals (dbValue))
					//	Target == DB (changed by trigger to new value already)
					|| (value == null && dbValue == null)
					|| (value != null && value.equals (dbValue)) 
					|| ((oldValue != null && dbValue != null && oldValue.getClass().equals(byte[].class) && dbValue.getClass().equals(byte[].class)) && Arrays.equals((byte[])oldValue, (byte[])dbValue))
					|| ((value != null && dbValue != null && value.getClass().equals(byte[].class) && dbValue.getClass().equals(byte[].class)) && Arrays.equals((byte[])oldValue, (byte[])dbValue)) 
						)
				{
					if (!po.set_ValueNoCheck (columnName, value))
					{
						ValueNamePair lastError = CLogger.retrieveError();
						if (lastError != null) {
							String adMessage = lastError.getValue();
							String adMessageArgument = lastError.getName().trim();
							
							StringBuilder info = new StringBuilder(adMessageArgument);
							
							if (!adMessageArgument.endsWith(";")) info.append(";");
							info.append(field.getHeader());
							
							fireDataStatusEEvent(adMessage, info.toString(), true);
						} else {
							fireDataStatusEEvent("Set value failed", field.getHeader(), true);
						}
						return SAVE_ERROR;
					}
				}
				//	Original != DB
				else
				{
					String msg = columnName 
						+ "= " + oldValue 
							+ (oldValue==null ? "" : "(" + oldValue.getClass().getName() + ")")
						+ " != DB: " + dbValue 
							+ (dbValue==null ? "" : "(" + dbValue.getClass().getName() + ")")
						+ " -> New: " + value 
							+ (value==null ? "" : "(" + value.getClass().getName() + ")");
					dataRefresh(m_rowChanged);
					fireDataStatusEEvent("SaveErrorDataChanged", msg, true);
					return SAVE_ERROR;
				}
			}	//	Data changed
		}	//	for every column
		if (!po.save())
		{
			String msg = "SaveError";
			String info = "";
			ValueNamePair ppE = CLogger.retrieveError();
			if (ppE != null)
			{
				msg = ppE.getValue();
				info = ppE.getName();
				if ("DBExecuteError".equals(msg))
					info = "DBExecuteError:" + info;
			}
			fireDataStatusEEvent(msg, info, true);
			return SAVE_ERROR;
		}
		else if (m_virtual && po.get_ID() > 0)
		{
			//update ID
			MSort sort = m_sort.get(m_rowChanged);
			int oldid = sort.index;
			if (oldid != po.get_ID())
			{
				sort.index = po.get_ID();
				Object[] data = m_virtualBuffer.remove(oldid);
				data[m_indexKeyColumn] = sort.index;
				m_virtualBuffer.put(sort.index, data);
			}
		}
		
		//	Refresh - update buffer
		String whereClause = po.get_WhereClause(true);
		if (log.isLoggable(Level.FINE)) log.fine("Reading ... " + whereClause);
		StringBuilder refreshSQL = new StringBuilder(m_SQL_Select)
			.append(" WHERE ").append(whereClause);
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement(refreshSQL.toString(), get_TrxName());
			rs = pstmt.executeQuery();
			if (rs.next())
			{
				Object[] rowDataDB = readData(rs);
				//	update buffer
				setDataAtRow(m_rowChanged, rowDataDB);
				fireTableRowsUpdated(m_rowChanged, m_rowChanged);
			}
		}
		catch (SQLException e)
		{
			String msg = "SaveError";
			log.log(Level.SEVERE, refreshSQL.toString(), e);
			fireDataStatusEEvent(msg, e.getLocalizedMessage(), true);
			return SAVE_ERROR;
		}
		finally
		{
			DB.close(rs, pstmt);
			rs = null;
			pstmt = null;
		}
		//	everything ok
		m_rowData = null;
		m_changed = false;
		m_compareDB = true;
		m_rowChanged = -1;
		m_newRow = -1;
		m_inserting = false;
		//
		ValueNamePair pp = CLogger.retrieveWarning();
		if (pp != null)
		{
			String msg = pp.getValue();
			String info = pp.getName();
			fireDataStatusEEvent(msg, info, false);
		}
		else
		{
			pp = CLogger.retrieveInfo();
			String msg = "Saved";
			String info = "";
			if (pp != null)
			{
				msg = pp.getValue();
				info = pp.getName();
			}
			fireDataStatusIEvent(msg, info);
		}
		//
		log.config("fini");
		return SAVE_OK;
	}	//	dataSavePO
	
	/**
	 * 	Get Record Where Clause from data (single key or multi-key)
	 *	@param rowData data
	 *	@return where clause or null
	 */
	private String getWhereClause (Object[] rowData)
	{
		int size = m_fields.size();
		StringBuilder singleRowWHERE = null;
		StringBuilder singleRowUUWHERE = null;
		StringBuilder multiRowWHERE = null;
		String tableName = getTableName();
		int uidColumn = -1;
		for (int col = 0; col < size; col++)
		{
			GridField field = (GridField)m_fields.get (col);
			String columnName = field.getColumnName();
			if (field.isKey())
			{
				Object value = rowData[col]; 
				if (value == null)
				{
					log.log(Level.WARNING, "PK data is null - " + columnName);
					return null;
				}
				if (columnName.endsWith ("_ID"))
					singleRowWHERE = new StringBuilder(tableName).append(".").append(columnName)
						.append ("=").append (value);
				else
					singleRowWHERE = new StringBuilder(tableName).append(".").append(columnName)
						.append ("=").append (DB.TO_STRING(value.toString()));
				break;
			}
			else if (field.isParentColumn())
			{
				Object value = rowData[col]; 
				if (value == null)
				{
					if (log.isLoggable(Level.INFO))log.log(Level.INFO, "FK data is null - " + columnName);
					continue;
				}
				if (multiRowWHERE == null)
					multiRowWHERE = new StringBuilder();
				else
					multiRowWHERE.append(" AND ");
				if (columnName.endsWith ("_ID"))
					multiRowWHERE.append (tableName).append(".").append(columnName)
						.append ("=").append (value);
				else if (value instanceof Timestamp) {
					multiRowWHERE.append (tableName).append(".").append(columnName)
					.append ("=").append (DB.TO_DATE((Timestamp)value, false));
				}else
					multiRowWHERE.append (tableName).append(".").append(columnName)
						.append ("=").append (DB.TO_STRING(value.toString()));
			}
			else if (columnName.equals(PO.getUUIDColumnName(tableName)))
			{
				uidColumn = col;
			}
		}	//	for all columns
						
		if (singleRowWHERE != null)
			return singleRowWHERE.toString();
		if (multiRowWHERE != null)
			return multiRowWHERE.toString();
		if (uidColumn >= 0)
		{
			Object value = rowData[uidColumn]; 
			if (value == null && multiRowWHERE == null)
			{
				log.log(Level.WARNING, "UUID data is null - " + uidColumn);
				return null;
			}
			else
			{
				singleRowUUWHERE = new StringBuilder(tableName).append(".").append(PO.getUUIDColumnName(tableName))
						.append ("=").append (DB.TO_STRING(value.toString()));
			}
		}
		if (singleRowUUWHERE != null)
			return singleRowUUWHERE.toString();
		log.log(Level.WARNING, "No key Found");
		return null;
	}	//	getWhereClause
	
	/*************************************************************************/
	/**
	 *	Get Mandatory empty columns
	 *  @param rowData row data
	 *  @return Mandatory columns that's empty (separated by comma)
	 */
	private String getMandatory(Object[] rowData)
	{
		//  see also => ProcessParameter.saveParameter
		StringBuilder sb = new StringBuilder();
		//	Check all columns
		int size = m_fields.size();
		for (int i = 0; i < size; i++)
		{
			GridField field = (GridField)m_fields.get(i);
			if (field.isMandatory(true))        //  check context
			{
				if (rowData[i] == null || rowData[i].toString().length() == 0)
				{
					field.setInserting (true);  //  set editable otherwise deadlock
					field.setError(true);
					if (sb.length() > 0)
						sb.append(", ");
					sb.append(field.getHeader());
				}
				else
					field.setError(false);
			}
		}
		if (sb.length() == 0)
			return "";
		return sb.toString();
	}	//	getMandatory
	/**
	 * @return true if need save and all mandatory field has value
	 */
	public boolean isNeedSaveAndMandatoryFill() {
		if (!m_open)
		{
			return false;
		}
		//	no need - not changed - row not positioned - no Value changed
		if (m_rowChanged == -1)
		{
			return false;
		}
		//  Value not changed
		if (m_rowData == null)
		{
			return false;
		}
		if (m_readOnly)
		{
			return false;
		}
		//	row not positioned - no Value changed
		if (m_rowChanged == -1)
		{
			if (m_newRow != -1)     //  new row and nothing changed - might be OK
				m_rowChanged = m_newRow;
			else
			{
				return false;
			}
		}
		
		//	get updated row data
		Object[] rowData = getDataAtRow(m_rowChanged);
		//	Check Mandatory
		String missingColumns = getMandatory(rowData);
		if (missingColumns.length() != 0) {
			return false;
		}
		
		return true;
	}
	
	/*************************************************************************/
	// IDEMPIERE-454 Easy import
	private boolean m_importing = false;
	private String m_trxName = null;
	private int m_currentRow = -1;
	/**
	 *	Append new row after current row
	 *  @param currentRow row
	 *  @param copyCurrent true to copy value from current row
	 *  @return true if success -
	 *  Error info (Access*, AccessCannotInsert) is saved in the log
	 */
	public boolean dataNew (int currentRow, boolean copyCurrent)
	{
		if (log.isLoggable(Level.INFO)) log.info("Current=" + currentRow + ", Copy=" + copyCurrent);
		//  Read only
		if (m_readOnly)
		{
			fireDataStatusEEvent("AccessCannotInsert", "", true);
			return false;
		}
		//  see if we need to save
		dataSave(-2, false);
		m_inserting = true;
		
		// Setup the buffer first so that event will be handle properly
		// Create default data
		int size = m_fields.size();
		m_rowData = new Object[size];	//	"original" data
		Object[] rowData = new Object[size];
		
		m_changed = true;
		m_compareDB = true;		
		m_newRow = currentRow + 1;
		//  if there is no record, the current row could be 0 (and not -1)
		if (m_sort.size() < m_newRow)
			m_newRow = m_sort.size();
		//	add Data at end of buffer
		MSort newSort = m_virtual
				? new MSort(NEW_ROW_ID, null)
				: new MSort(m_sort.size(), null);	//	index
		if (m_virtual)
		{
			m_virtualBuffer.put(NEW_ROW_ID, rowData);
		}
		else
		{
			m_buffer.add(rowData);
		}
		//	add Sort pointer
		m_sort.add(m_newRow, newSort);
		m_rowCount++;
		
		//	fill data
		if (copyCurrent)
		{
			boolean hasDocTypeTargetField = (getField("C_DocTypeTarget_ID") != null);
			Object[] origData = getDataAtRow(currentRow);
			for (int i = 0; i < size; i++)
			{
				GridField field = (GridField)m_fields.get(i);
				MColumn column = null;
				if (field.getAD_Column_ID() > 0)
					column = MColumn.get(m_ctx, field.getAD_Column_ID());
				if (field.isVirtualColumn())
					;
				else if (   field.isKey()		//	KeyColumn
						  || (column != null && column.isUUIDColumn()) // IDEMPIERE-67
						  || (column != null && column.isStandardColumn() && !column.getColumnName().equals("AD_Org_ID")) // AD_Org_ID can be copied
						  // Bug [ 1807947 ]
						  || (hasDocTypeTargetField && field.getColumnName().equals("C_DocType_ID"))
						  || ! field.isAllowCopy())
				{
					Object value = field.getDefault();
					field.setValue(value, m_inserting);
					field.validateValueNoDirect();
					rowData[i] = field.getValue();
				}
				else {
					Object value = origData[i];
					field.setValue(value, m_inserting);
					field.validateValueNoDirect();
					rowData[i] = field.getValue();
				}
			}
		}
		else	//	new
		{
			for (int i = 0; i < size; i++)
			{
				GridField field = (GridField)m_fields.get(i);
				if (field.getGridTab() != null) {
					//avoid getting default from previous row
					String key = field.getVO().WindowNo+"|"+field.getVO().TabNo+"|"+field.getVO().ColumnName;
					field.getVO().ctx.remove(key);
				}
				Object value = field.getDefault();
				field.setValue(value, m_inserting);
				field.validateValueNoDirect();
				rowData[i] = field.getValue();
			}
		}
		
		m_rowChanged = -1;  //  only changed in setValueAt
		//	inform
		if (log.isLoggable(Level.FINE)) log.fine("Current=" + currentRow + ", New=" + m_newRow);
		fireTableRowsInserted(m_newRow, m_newRow);
		fireDataStatusIEvent(copyCurrent ? DATA_UPDATE_COPIED_MESSAGE : DATA_INSERTED_MESSAGE, "");
		if (log.isLoggable(Level.FINE)) log.fine("Current=" + currentRow + ", New=" + m_newRow + " - complete");
		return true;
	}	//	dataNew
	/**
	 *	Delete data at row index
	 *  @param row row index
	 *  @return true if success -
	 *  Error info (Access*, AccessNotDeleteable, DeleteErrorDependent,
	 *  DeleteError) is saved in the log
	 */
	public boolean dataDelete (int row)
	{
		if (log.isLoggable(Level.INFO)) log.info("Row=" + row);
		if (row < 0)
			return false;
		//	Tab R/O
		if (m_readOnly)
		{
			fireDataStatusEEvent("AccessCannotDelete", "", true);	//	privileges
			return false;
		}
		//	Is this record deletable?
		if (!m_deleteable)
		{
			fireDataStatusEEvent("AccessNotDeleteable", "", true);	//	audit
			return false;
		}
		//	Processed Column and not an Import Table
		if (m_indexProcessedColumn > 0 && !m_tableName.startsWith("I_"))
		{
			Boolean processed = (Boolean)getValueAt(row, m_indexProcessedColumn);
			if (processed != null && processed.booleanValue())
			{
				fireDataStatusEEvent("CannotDeleteTrx", "", true);
				return false;
			}
		}
		// Carlos Ruiz - globalqss - IDEMPIERE-111
		// Check if the role has access to this client
		//	Can we change?
		int[] co = getClientOrg(row);
		int AD_Client_ID = co[0];
		int AD_Org_ID = co[1];
		if (!MRole.getDefault(m_ctx, false).canUpdate(AD_Client_ID, AD_Org_ID, m_AD_Table_ID, 0, true))
		{
			fireDataStatusEEvent("AccessCannotDelete", "", true);
			return false;
		}
		MSort sort = (MSort)m_sort.get(row);
		Object[] rowData = getDataAtRow(row);
		//
		PO po = getPO(row);
		
		//	Delete via PO 
		if (po != null)
		{
			boolean ok = false;
			try
			{
				ok = po.delete(false);
			}
			catch (Throwable t)
			{
				log.log(Level.SEVERE, "Delete", t);
			}
			if (!ok)
			{
				ValueNamePair vp = CLogger.retrieveError();
				if (vp != null && !(Util.isEmpty(vp.getValue()) || Util.isEmpty(vp.getName())))
					fireDataStatusEEvent(vp);
				else
					fireDataStatusEEvent("DeleteError", "", true);
				return false;
			}
		}
		else	//	Delete via SQL
		{
			StringBuilder sql = new StringBuilder("DELETE FROM ");
			sql.append(m_tableName).append(" WHERE ").append(getWhereClause(rowData));
			int no = 0;
			PreparedStatement pstmt = null;
			try
			{
				pstmt = DB.prepareStatement (sql.toString(), 
						ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE, null);
				no = pstmt.executeUpdate();
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, sql.toString(), e);
				String msg = "DeleteError";
				String dbMsg = DBException.getDefaultDBExceptionMessage(e);
				if (!Util.isEmpty(dbMsg))
					msg = dbMsg;
				fireDataStatusEEvent(msg, e.getLocalizedMessage(), true);
				return false;
			}
			finally
			{
				DB.close(pstmt);
				pstmt = null;
			}
			//	Check Result
			if (no != 1)
			{
				log.log(Level.SEVERE, "Number of deleted rows = " + no);
				return false;
			}
		}
		//	Get Sort
		if (m_virtual)
		{
			m_virtualBuffer.remove(sort.index);
		}
		else
		{
			//	Delete row in Buffer and shifts all below up
			m_buffer.remove(sort.index);
		}
		m_rowCount--;
		//	Delete row in Sort
		m_sort.remove(row);
		if (!m_virtual)
		{
			//	Correct pointer in Sort
			for (int i = 0; i < m_sort.size(); i++)
			{
				MSort ptr = (MSort)m_sort.get(i);
				if (ptr.index > sort.index)
					ptr.index--;	//	move up
			}
		}
		//	inform
		m_changed = false;
		m_rowChanged = -1;
		fireTableRowsDeleted(row, row);
		fireDataStatusIEvent("Deleted", "");
		if (log.isLoggable(Level.FINE)) log.fine("Row=" + row + " complete");
		return true;
	}	//	dataDelete
	
	/**
	 *	Ignore/Undo changes
	 */
	public void dataIgnore()
	{
		if (!m_inserting && !m_changed && m_rowChanged < 0)
		{
			if (log.isLoggable(Level.FINE))
				log.fine("Nothing to ignore");
			m_newRow = -1;
			return;
		}
		if (log.isLoggable(Level.INFO)) log.info("Inserting=" + m_inserting);
		//	Inserting - delete new row
		if (m_inserting)
		{
			//	Get Sort
			MSort sort = (MSort)m_sort.get(m_newRow);
			if (m_virtual)
			{
				m_virtualBuffer.remove(NEW_ROW_ID);
			}
			else
			{
				//	Delete row in Buffer and shifts all below up
				m_buffer.remove(sort.index);
			}
			m_rowCount--;
			//	Delete row in Sort
			m_sort.remove(m_newRow);	//	pintint to the last column, so no adjustment
			//
			m_changed = false;
			m_rowData = null;
			m_rowChanged = -1;
			m_inserting = false;
			//	inform
			fireTableRowsDeleted(m_newRow, m_newRow);
		}
		else
		{
			//	update buffer
			if (m_rowData != null)
			{
				setDataAtRow(m_rowChanged, m_rowData);
			}
			m_changed = false;
			m_rowData = null;
			m_rowChanged = -1;
			m_inserting = false;
			//	inform
		//	fireTableRowsUpdated(m_rowChanged, m_rowChanged); >> messes up display?? (clearSelection)
		}
		m_newRow = -1;
		fireDataStatusIEvent(DATA_IGNORED_MESSAGE, "");
	}	//	dataIgnore
	/**
	 *	Refresh Row - ignore changes
	 *  @param row row
	 */
	public void dataRefresh (int row)
	{
		dataRefresh(row, true);
	}
	/**
	 * get where clause for row
	 * @param row
	 * @return where clause
	 */
	public String getWhereClause(int row)
	{
		if (row < 0 || m_sort.size() == 0 || m_inserting)
			return null;
		Object[] rowData = getDataAtRow(row);
		if (rowData == null)
			return null;
		String where = getWhereClause(rowData);
		return where;
	}
	/**
	 *	Refresh Row - ignore changes
	 *  @param row row
	 *  @param fireStatusEvent
	 */
	public void dataRefresh (int row, boolean fireStatusEvent)
	{
		if (log.isLoggable(Level.INFO)) log.info("Row=" + row);
		if (row < 0 || m_sort.size() == 0 || m_inserting)
			return;
		Object[] rowData = getDataAtRow(row);
		//  ignore
		dataIgnore();
		//	Create SQL
		String where = getWhereClause(rowData);
		if (where == null || where.length() == 0)
			where = "1=2";
		String sql = m_SQL_Select + " WHERE " + where;
		Object[] rowDataDB = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement(sql,get_TrxName());
			rs = pstmt.executeQuery();
			//	only one row
			if (rs.next())
				rowDataDB = readData(rs);
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, sql, e);
			fireTableRowsUpdated(row, row);
			fireDataStatusEEvent("RefreshError", sql, true);
			return;
		}
		finally
		{
			DB.close(rs, pstmt);
			rs = null;
			pstmt = null;
		}
		//	update buffer
		if (rowDataDB!=null)
			setDataAtRow(row, rowDataDB);
		//	info
		m_rowData = null;
		m_changed = false;
		m_rowChanged = -1;
		m_inserting = false;
		fireTableRowsUpdated(row, row);
		if (fireStatusEvent)
			fireDataStatusIEvent(DATA_REFRESH_MESSAGE, "");
	}	//	dataRefresh
	/**
	 *	Refresh all Rows - ignore changes
	 */
	public void dataRefreshAll()
	{
		dataRefreshAll(true);
	}
	/**
	 *	Refresh all Rows - ignore changes
	 *  @param fireStatusEvent
	 */
	public void dataRefreshAll(boolean fireStatusEvent)
	{
		dataRefreshAll(fireStatusEvent, -1);
	}
	/**
	 *	Refresh all Rows - ignore changes
	 *  @param fireStatusEvent
	 *  @param rowToRetained
	 */
	public void dataRefreshAll(boolean fireStatusEvent, int rowToRetained)
	{
		m_inserting = false;	//	should not happen
		dataIgnore();
		String retainedWhere = null;
		if (rowToRetained >= 0)
		{
			retainedWhere = getWhereClause(rowToRetained);
		}
		close(false);
		if (retainedWhere != null)
		{
			if (m_whereClause != null && m_whereClause.trim().length() > 0)
			{
				StringBuilder orRetainedWhere = new StringBuilder(") OR (").append(retainedWhere).append(")) ");
				if (! m_whereClause.contains(orRetainedWhere.toString()))
					m_whereClause = "((" + m_whereClause + orRetainedWhere.toString();
			}
			open(m_maxRows);
		}
		else
		{
			open(m_maxRows);
		}
		//	Info
		m_rowData = null;
		m_changed = false;
		m_rowChanged = -1;
		m_inserting = false;
		if (m_lastSortColumnIndex >= 0)
		{
			loadComplete();
			sort(m_lastSortColumnIndex, m_lastSortedAscending);
		}
		fireTableDataChanged();
		if (fireStatusEvent)
			fireDataStatusIEvent(DATA_REFRESH_MESSAGE, "");
	}	//	dataRefreshAll
	/**
	 *	Re-query with new whereClause
	 *  @param whereClause sql where clause
	 *  @param onlyCurrentRows only current rows
	 *  @param onlyCurrentDays how many days back
	 *  @param fireEvents if tabledatachanged and datastatusievent must be fired
	 *  @return true if success
	 */
	public boolean dataRequery (String whereClause, boolean onlyCurrentRows, int onlyCurrentDays, boolean fireEvents)
	{
		if (log.isLoggable(Level.INFO)) log.info(whereClause + "; OnlyCurrent=" + onlyCurrentRows);
		close(false);
		m_onlyCurrentDays = onlyCurrentDays;
		setSelectWhereClause(whereClause, onlyCurrentRows, m_onlyCurrentDays);
		open(m_maxRows);
		//  Info
		m_rowData = null;
		m_changed = false;
		m_rowChanged = -1;
		m_inserting = false;
		if (m_lastSortColumnIndex >= 0)
		{
			loadComplete();
			sort(m_lastSortColumnIndex, m_lastSortedAscending);
		}
		if (fireEvents) {
			fireTableDataChanged();
			fireDataStatusIEvent(DATA_REFRESH_MESSAGE, "");
		}
		return true;
	}	//	dataRequery
	/**
	 * Delegate to {@link #dataRequery(String, boolean, int, boolean)} with fireEvents=true
	 * @param whereClause
	 * @param onlyCurrentRows
	 * @param onlyCurrentDays
	 * @return true if success
	 */
	public boolean dataRequery (String whereClause, boolean onlyCurrentRows, int onlyCurrentDays)
	{
		return dataRequery (whereClause, onlyCurrentRows, onlyCurrentDays, true);
	}	//	dataRequery
	/**
	 *	Is Cell Editable.
	 *  @param  row row index
	 *  @param  col column index
	 *  @return true, if editable
	 */
	public boolean isCellEditable (int row, int col)
	{
		//	Entire Table not editable
		if (m_readOnly)
			return false;
		//	Key not editable
		if (col == m_indexKeyColumn)
			return false;
		/** @todo check link columns */
		//	Check column range
		if (col < 0 && col >= m_fields.size())
			return false;
		//  IsActive Column always editable if no processed exists
		if (col == m_indexActiveColumn && m_indexProcessedColumn == -1)
			return true;
		//	Row
		if (!isRowEditable(row))
			return false;
		//	Column
		return ((GridField)m_fields.get(col)).isEditable(false);
	}	//	IsCellEditable
	/**
	 *	Is row editable
	 *  @param row row index
	 *  @return true if editable
	 */
	public boolean isRowEditable (int row)
	{
		//	Entire Table not editable or no row
		if (m_readOnly || row < 0)
			return false;
		//	If not Active - not editable
		if (m_indexActiveColumn > 0)		//	&& m_TabNo != Find.s_TabNo)
		{
			Object value = getValueAt(row, m_indexActiveColumn);
			if (value instanceof Boolean)
			{
				if (!((Boolean)value).booleanValue())
					return false;
			}
			else if ("N".equals(value)) 
				return false;
		}
		//	If Processed - not editable (Find always editable)
		if (m_indexProcessedColumn > 0)		//	&& m_TabNo != Find.s_TabNo)
		{
			Object processed = getValueAt(row, m_indexProcessedColumn);
			if (processed instanceof Boolean)
			{
				if (((Boolean)processed).booleanValue())
					return false;
			}
			else if ("Y".equals(processed)) 
				return false;
		}
		//
		int[] co = getClientOrg(row);
		int AD_Client_ID = co[0]; 
		int AD_Org_ID = co[1];
		int Record_ID = getKeyID(row);
		return MRole.getDefault(m_ctx, false).canUpdate
			(AD_Client_ID, AD_Org_ID, m_AD_Table_ID, Record_ID, false);
	}	//	isRowEditable
	/**
	 * 	Get Client Org for row
	 *	@param row row index
	 *	@return array [0] = Client [1] = Org - a value of -1 is not defined/found
	 */
	private int[] getClientOrg (int row)
	{
		int AD_Client_ID = -1;
		if (m_indexClientColumn != -1)
		{
			Integer ii = (Integer)getValueAt(row, m_indexClientColumn);
			if (ii != null)
				AD_Client_ID = ii.intValue();
		}
		int AD_Org_ID = 0;
		if (m_indexOrgColumn != -1)
		{
			Integer ii = (Integer)getValueAt(row, m_indexOrgColumn);
			if (ii != null)
				AD_Org_ID = ii.intValue();
		}
		return new int[] {AD_Client_ID, AD_Org_ID};
	}	//	getClientOrg
	/**
	 *	Set entire table as read only
	 *  @param value new read only value
	 */
	public void setReadOnly (boolean value)
	{
		if (log.isLoggable(Level.FINE)) log.fine("ReadOnly=" + value);
		m_readOnly = value;
	}	//	setReadOnly
	/**
	 *  Is entire Table Read/Only
	 *  @return true if read only
	 */
	public boolean isReadOnly()
	{
		return m_readOnly;
	}   //  isReadOnly
	/**
	 *  Is inserting
	 *  @return true if inserting
	 */
	public boolean isInserting()
	{
		return m_inserting;
	}   //  isInserting
	/**
	 *	Set Compare DB.
	 * 	If Set to false, save overwrites the record, regardless of DB changes.
	 *  (When a payment is changed in Sales Order, the payment reversal clears the payment id)
	 * 	@param compareDB compare DB - false forces overwrite
	 *  @deprecated
	 */
	@Deprecated
	public void setCompareDB (boolean compareDB)
	{
		m_compareDB = compareDB;
	}  	//	setCompareDB
	/**
	 *	Get Compare DB.
	 * 	@return false if save overwrites the record, regardless of DB changes
	 * 	(false forces overwrite).
	 *  @deprecated
	 */
	@Deprecated
	public boolean getCompareDB ()
	{
		return m_compareDB;
	}  	//	getCompareDB
	/**
	 *	Can Table rows be deleted
	 *  @param value new deleteable value
	 */
	public void setDeleteable (boolean value)
	{
		if (log.isLoggable(Level.FINE)) log.fine("Deleteable=" + value);
		m_deleteable = value;
	}	//	setDeleteable
	
	/**
	 *	Read data from result set
	 *  @param rs result set
	 *  @return Data Array
	 */
	private Object[] readData (ResultSet rs)
	{
		int size = m_fields.size();
		Object[] rowData = new Object[size];
		String columnName = null;
		int displayType = 0;
		//	Types see also GridField.createDefault
		try
		{
			//	get row data
			for (int j = 0; j < size; j++)
			{
				//	Column Info
				GridField field = (GridField)m_fields.get(j);
				columnName = field.getColumnName();
				displayType = field.getDisplayType();
				//	Integer, ID, Lookup
				if (displayType == DisplayType.Integer || (DisplayType.isID(displayType) && !(columnName.equals("EntityType") || columnName.equals("AD_Language"))))
				{
					rowData[j] = Integer.valueOf(rs.getInt(j+1));	//	Integer
					if (rs.wasNull())
						rowData[j] = null;
				}
				//	Number
				else if (DisplayType.isNumeric(displayType))
					rowData[j] = rs.getBigDecimal(j+1);			//	BigDecimal
				//	Date
				else if (DisplayType.isDate(displayType))
					rowData[j] = rs.getTimestamp(j+1);			//	Timestamp
				//	RowID or Key (and Selection)
				else if (displayType == DisplayType.RowID)
					rowData[j] = null;
				//	YesNo
				else if (displayType == DisplayType.YesNo)
				{
					String str = rs.getString(j+1);
					if (field.isEncryptedColumn())
						str = (String)decrypt(str, getAD_Client_ID(rs));
					rowData[j] = Boolean.valueOf("Y".equals(str));	//	Boolean
				}
				//	LOB
				else if (DisplayType.isLOB(displayType))
				{
					Object value = rs.getObject(j+1);
					if (rs.wasNull())
						rowData[j] = null;
					else if (value instanceof Clob) 
					{
						Clob lob = (Clob)value;
						long length = lob.length();
						rowData[j] = lob.getSubString(1, (int)length);
					}
					else if (value instanceof Blob)
					{
						Blob lob = (Blob)value;
						long length = lob.length();
						rowData[j] = lob.getBytes(1, (int)length);
					}
					else if (value instanceof String)
						rowData[j] = value;
					else if (value instanceof byte[])
						rowData[j] = value;
				}
				//	String
				else
					rowData[j] = rs.getString(j+1);				//	String
				//	Encrypted
				if (field.isEncryptedColumn() && displayType != DisplayType.YesNo)
					rowData[j] = decrypt(rowData[j], getAD_Client_ID(rs));
			}
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, columnName + ", DT=" + displayType, e);
		}
		return rowData;
	}	//	readData
	/**
	 * 	Decrypt
	 *	@param yy encrypted data
	 *  @param AD_Client_ID
	 *	@return clear data
	 */
	private Object decrypt (Object yy, int AD_Client_ID)
	{
		if (yy == null)
			return null;
		return SecureEngine.decrypt(yy, AD_Client_ID);
	}	//	decrypt
	
	/**
	 * @param rs
	 * @return AD_Client_ID or -1
	 */
	private int getAD_Client_ID(ResultSet rs) {
		int AD_Client_ID = -1;
		try {
			AD_Client_ID = rs.getInt("AD_Client_ID");
			if (rs.wasNull())
				AD_Client_ID = -1;
		} catch (SQLException e) {
			AD_Client_ID = -1;
		}
		if (AD_Client_ID == -1)
			AD_Client_ID = getAD_Client_ID();
		return AD_Client_ID;
	}
	/**
	 * @return AD_Client_ID
	 */
	private int getAD_Client_ID() 
	{
		int AD_Client_ID = Env.getAD_Client_ID(Env.getCtx());
		GridField field = getField("AD_Client_ID");
		if (field != null && field.getValue() != null) {
			AD_Client_ID = ((Number)field.getValue()).intValue();
		}
		return AD_Client_ID;
	}
	
	/**
	 *	Remove Data Status Listener
	 *  @param l listener
	 */
	public synchronized void removeDataStatusListener(DataStatusListener l)
	{
		listenerList.remove(DataStatusListener.class, l);
	}	//	removeDataStatusListener
	/**
	 *	Add Data Status Listener
	 *  @param l listener
	 */
	public synchronized void addDataStatusListener(DataStatusListener l)
	{
		listenerList.add(DataStatusListener.class, l);
	}	//	addDataStatusListener
	/**
	 *	Fire data status changed event
	 *  @param e event
	 */
	private void fireDataStatusChanged (DataStatusEvent e)
	{
		DataStatusListener[] listeners = listenerList.getListeners(DataStatusListener.class);
        for (int i = 0; i < listeners.length; i++) 
        	listeners[i].dataStatusChanged(e);
	}	//	fireDataStatusChanged
	/**
	 *  Create new Data Status Event instance
	 *  @return new data status event instance
	 */
	private DataStatusEvent createDSE()
	{
		boolean changed = m_changed;
		if (m_rowChanged != -1)
			changed = true;
		DataStatusEvent dse = new DataStatusEvent(this, m_rowCount, changed,
			Env.isAutoCommit(m_ctx, m_WindowNo), m_inserting);
		dse.AD_Table_ID = m_AD_Table_ID;
		dse.Record_ID = null;
		return dse;
	}   //  createDSE
	/**
	 *  Create and fire Data Status Info Event
	 *  @param AD_Message message
	 *  @param info additional info
	 */
	protected void fireDataStatusIEvent (String AD_Message, String info)
	{
		DataStatusEvent e = createDSE();
		e.setInfo(AD_Message, info, false,false);
		if (SORTED_DSE_EVENT.equals(AD_Message) && m_currentRow >= 0)
			e.setCurrentRow(m_currentRow);
		fireDataStatusChanged (e);
	}   //  fireDataStatusIEvent
	/**
	 *  Create and fire Data Status Error Event
	 *  @param AD_Message message
	 *  @param info info
	 *  @param isError error
	 */
	protected void fireDataStatusEEvent (String AD_Message, String info, boolean isError)
	{
		//
		DataStatusEvent e = createDSE();
		if (info != null && info.startsWith("DBExecuteError:")) {
			String firstline;
			int nl = info.indexOf("\n");
			if (nl > 0)
				firstline = info.substring(0, nl);
			else
				firstline = info;
			String newinfo = Msg.getMsg(m_ctx, firstline);
			if (firstline.equals(newinfo))
				newinfo = info.substring(15); // size of "DBExecuteError:"
			e.setInfo(AD_Message, newinfo, isError, !isError);
		} else {
			e.setInfo(AD_Message, info, isError, !isError);
		}
		if (isError)
			log.saveWarning(AD_Message, info);
		fireDataStatusChanged (e);
	}   //  fireDataStatusEEvent
	/**
	 *  Create and fire Data Status Error Event (from Error Log)
	 *  @param errorLog error log info
	 */
	protected void fireDataStatusEEvent (ValueNamePair errorLog)
	{
		if (errorLog != null)
			fireDataStatusEEvent (errorLog.getValue(), errorLog.getName(), true);
	}   //  fireDataStatusEEvent
	
	/**
	 *  Remove Vetoable change listener for row changes
	 *  @param l listener
	 */
	public synchronized void removeVetoableChangeListener(VetoableChangeListener l)
	{
		m_vetoableChangeSupport.removeVetoableChangeListener(l);
	}   //  removeVetoableChangeListener
	/**
	 *  Add Vetoable change listener for row changes
	 *  @param l listener
	 */
	public synchronized void addVetoableChangeListener(VetoableChangeListener l)
	{
		m_vetoableChangeSupport.addVetoableChangeListener(l);
	}   //  addVetoableChangeListener
	/**
	 *  Fire Vetoable change listener for row changes
	 *  @param e event
	 *  @throws PropertyVetoException
	 */
	protected void fireVetoableChange(PropertyChangeEvent e) throws java.beans.PropertyVetoException
	{
		m_vetoableChangeSupport.fireVetoableChange(e);
	}   //  fireVetoableChange
	/**
	 *  toString
	 *  @return String representation
	 */
	public String toString()
	{
		return new StringBuilder("MTable[").append(m_tableName)
			.append(",WindowNo=").append(m_WindowNo)
			.append(",Tab=").append(m_TabNo).append("]").toString();
	}   //  toString
	/**
	 * 
	 * @return new row added
	 */
	public int getNewRow()
	{
		return m_newRow;
	}
	
	/**
	 *	Asynchronous Loader
	 */
	protected class Loader implements Serializable, Runnable
	{
		/**
		 * generated serial id
		 */
		private static final long serialVersionUID = -6866671239509705988L;
		/**
		 *  Construct Loader
		 */
		public Loader()
		{
			super();
		}	//	Loader
		private PreparedStatement   m_pstmt = null;
		private ResultSet 		    m_rs = null;
		private Trx trx = null;
		private Properties m_context = null;
		private int maxRows;
		private int rows;
		
		public void setContext(Properties context)
		{
			m_context = context;
		}
		/**
		 *	Open ResultSet
		 *	@param maxRows maximum number of rows or 0 for all
		 *	@return number of records
		 */
		protected int open (int maxRows)
		{
			this.maxRows = maxRows;
			//	Get Number of Rows
			rows = 0;
			PreparedStatement pstmt = null;
			ResultSet rs = null;		
			m_rowCountTimeout = false;
			try
			{
				pstmt = DB.prepareStatement(m_SQL_Count, get_TrxName());
				setParameter (pstmt, true);
		        int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_INITIAL_COUNT_TIMEOUT_IN_SECONDS, 
		        		DEFAULT_GRIDTABLE_COUNT_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx()));
				if (timeout > 0)
					pstmt.setQueryTimeout(timeout);
				rs = pstmt.executeQuery();
				if (rs.next())
					rows = rs.getInt(1);
			}
			catch (SQLException e0)
			{
				if (DB.getDatabase().isQueryTimeout(e0))
				{
					m_rowCountTimeout = true;
					return 0;
				}
				else
					throw new DBException(e0);
			}
			finally
			{
				DB.close(rs, pstmt);				
			}
			StringBuilder info = new StringBuilder("Rows=");
			info.append(rows);
			if (rows == 0)
				info.append(" - ").append(m_SQL_Count);
				
			if (maxRows > 0 && rows > maxRows)
			{
				info.append(" - MaxRows=").append(maxRows);					
				rows = maxRows;
			}
					
			if (log.isLoggable(Level.FINE)) log.fine(info.toString());
			
			return rows;
		}	//	open
		private void openResultSet() {
			String trxName = get_TrxName();
			//postgresql need trx to use cursor based resultset
			//https://jdbc.postgresql.org/documentation/head/query.html#query-with-cursor
			if (trxName == null) {
				trxName = m_virtual ? Trx.createTrxName("Loader") : null;
				trx  = trxName != null ? Trx.get(trxName, true) : null;
				if (trx != null)
					trx.setDisplayName(getClass().getName()+"_openResultSet");
			}
			//	open Statement (closed by Loader.close)
			try
			{
				m_pstmt = DB.prepareStatement(m_SQL, trxName);
				//ensure not all rows are fetch into memory for virtual table
				if (m_virtual)
					m_pstmt.setFetchSize(100);
				setParameter (m_pstmt, false);
				int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx()));
				if (timeout > 0)
					m_pstmt.setQueryTimeout(timeout);
				m_rs = m_pstmt.executeQuery();
			}
			catch (SQLException e)
			{
				if (DB.getDatabase().isQueryTimeout(e)) {
					m_rowLoadTimeout = true;
					throw new AdempiereException(Msg.getMsg(Env.getCtx(), LOAD_TIMEOUT_ERROR_MESSAGE), e);
				} else {
					log.saveError(e.getLocalizedMessage(), e);
					throw new DBException(e);
				}
			}
		}
		/**
		 *	Close RS and Statement
		 */
		private void close()
		{
			DB.close(m_rs, m_pstmt);
			m_rs = null;
			m_pstmt = null;
			if (trx != null)
			{
				trx.close();
				trx = null;
			}
		}	//	close
		/**
		 *	Fill Buffer to include Row
		 */
		public void run()
		{
			try {
				if (m_context != null)
					ServerContext.setCurrentInstance(m_context);
				doRun();
			} finally {
				if (m_context != null)
					ServerContext.dispose();
			}
		}	//	run
		/**
		 * Fill buffer from result set
		 */
		private void doRun() {
			boolean isFindOverMax = false;
			try
			{
				openResultSet();
				if (m_rs == null)
					return;
				while (m_rs.next())
				{
					if (maxRows > 0 && m_sort.size() == maxRows) {
						isFindOverMax = true;
			            break;
					}
					if (Thread.interrupted())
					{
						if (log.isLoggable(Level.FINE)) log.fine("Interrupted");
						close();
						return;
					}
					//  Get Data
					int recordId = 0;
					Object[] rowData = null;
					if (m_virtual)
						recordId = m_rs.getInt(getKeyColumnName());
					else
						rowData = readData(m_rs);
					//	add Data
					MSort sort = m_virtual
						? new MSort(recordId, null)
						: new MSort(m_buffer.size(), null);	//	index
					if (!m_virtual)
					{
						m_buffer.add(rowData);
					}
					m_sort.add(sort);
					// Start with rowCount=0, inform loading of first row
					if (m_rowCountTimeout)
					{
						m_rowCount++;
						if (m_rowCount == 1)
						{
							DataStatusEvent evt = createDSE();
							evt.setLoading(m_sort.size());
							evt.setInfo("CountQueryTimeoutLoadBackground", null, false, true);
							fireDataStatusChanged(evt);
						}
					}
					//	Statement all 1000 rows & sleep
					if (m_sort.size() % 1000 == 0)
					{
						DataStatusEvent evt = createDSE();
						evt.setLoading(m_sort.size());
						fireDataStatusChanged(evt);
						//	give the other processes a chance
						try
						{
							Thread.yield();
							Thread.sleep(10);		//	.01 second
						}
						catch (InterruptedException ie)
						{
							if (log.isLoggable(Level.FINE)) log.fine("Interrupted while sleeping");
							close();
							return;
						}
					}
				}	//	while(rs.next())
			}
			catch (Exception e)
			{
				log.log(Level.SEVERE, "run", e);
			}
			finally
			{
				close();
			}
			
			// Background loading without initial rowCount, inform final loaded rows
			if (m_rowCountTimeout && m_sort.size() > 0)
			{
				DataStatusEvent evt = createDSE();
				evt.setLoading(m_sort.size());
				if (isFindOverMax)
					evt.setInfo("FindOverMax", " > " + m_sort.size(), false, true);
				fireDataStatusChanged(evt);
			}
			fireDataStatusIEvent("", "");
		}
		/**
		 *	Set Parameter for Query.
		 *		elements must be Integer, BigDecimal, String (default)
		 *  @param pstmt prepared statement
		 *  @param countSQL count
		 */
		private void setParameter (PreparedStatement pstmt, boolean countSQL)
		{
			if (m_parameterSELECT.size() == 0 && m_parameterWHERE.size() == 0)
				return;
			try
			{
				int pos = 1;	//	position in Statement
				//	Select Clause Parameters
				for (int i = 0; !countSQL && i < m_parameterSELECT.size(); i++)
				{
					Object para = m_parameterSELECT.get(i);
					if (para != null)
						if (log.isLoggable(Level.FINE)) log.fine("Select " + i + "=" + para);
					//
					if (para == null)
						;
					else if (para instanceof Integer)
					{
						Integer ii = (Integer)para;
						pstmt.setInt (pos++, ii.intValue());
					}
					else if (para instanceof BigDecimal)
						pstmt.setBigDecimal (pos++, (BigDecimal)para);
					else
						pstmt.setString(pos++, para.toString());
				}
				//	Where Clause Parameters
				for (int i = 0; i < m_parameterWHERE.size(); i++)
				{
					Object para = m_parameterWHERE.get(i);
					if (para != null)
						if (log.isLoggable(Level.FINE)) log.fine("Where " + i + "=" + para);
					//
					if (para == null)
						;
					else if (para instanceof Integer)
					{
						Integer ii = (Integer)para;
						pstmt.setInt (pos++, ii.intValue());
					}
					else if (para instanceof BigDecimal)
						pstmt.setBigDecimal (pos++, (BigDecimal)para);
					else
						pstmt.setString(pos++, para.toString());
				}
			}
			catch (SQLException e)
			{
				log.log(Level.SEVERE, "parameter", e);
			}
		}	//	setParameter
	}	//	Loader
	/**
	 * Feature Request [1707462]
	 * Enable runtime change of VFormat
	 * @param identifier column name
	 * @param strNewFormat new input mask
	 * author fer_luck
	 */
	protected void setFieldVFormat (String identifier, String strNewFormat)
	{
		int cols = m_fields.size();
		for (int i = 0; i < cols; i++)
		{
			GridField field = (GridField)m_fields.get(i);
			if (identifier.equalsIgnoreCase(field.getColumnName())){
				field.setVFormat(strNewFormat);
				m_fields.set(i, field);
				break;
			}
		}
	}	//	setFieldVFormat	
	/**
	 * Verify if the record at row has been changed at DB (by other user or process)
	 * @param row row index
	 * @return true if record at row has been changed at DB
	 */
	public boolean hasChanged(int row) {
		// not so aggressive (it can has still concurrency problems)
		// compare Updated, IsProcessed
		if (getKeyID(row) > 0) {
			int colUpdated = findColumn("Updated");
			int colProcessed = findColumn("Processed");
			
			boolean hasUpdated = (colUpdated >= 0);
			boolean hasProcessed = (colProcessed >= 0);
			
			String columns = null;
			if (hasUpdated && hasProcessed) {
				columns = new String("Updated, Processed");
			} else if (hasUpdated) {
				columns = new String("Updated");
			} else if (hasProcessed) {
				columns = new String("Processed");
			} else {
				// no columns updated or processed to compare
				return false;
			}
						
			// todo: temporary fix for carlos assumption that all windows have _id column
			if ( findColumn(m_tableName + "_ID") == -1)
				return false;
	    	Timestamp dbUpdated = null;
	    	String dbProcessedS = null;
	    	PreparedStatement pstmt = null;
	    	ResultSet rs = null;
	    	String sql = "SELECT " + columns + " FROM " + m_tableName + " WHERE " + m_tableName + "_ID=?";
	    	try
	    	{
	    		pstmt = DB.prepareStatement(sql, get_TrxName());
	    		pstmt.setInt(1, getKeyID(row));
	    		rs = pstmt.executeQuery();
	    		if (rs.next()) {
	    			int idx = 1;
	    			if (hasUpdated)
	    				dbUpdated = rs.getTimestamp(idx++);
	    			if (hasProcessed)
	    				dbProcessedS = rs.getString(idx++);
	    		}
	    		else
	    			if (log.isLoggable(Level.INFO)) log.info("No Value " + sql);
	    	}
	    	catch (SQLException e)
	    	{
	    		throw new DBException(e, sql);
	    	}
	    	finally
	    	{
	    		DB.close(rs, pstmt);
	    		rs = null; pstmt = null;
	    	}
	    	
	    	if (hasUpdated) {
				Timestamp memUpdated = null;
				memUpdated = (Timestamp) getOldValue(row, colUpdated);
				if (memUpdated == null)
					memUpdated = (Timestamp) getValueAt(row, colUpdated);
				if (memUpdated != null && ! memUpdated.equals(dbUpdated))
					return true;
	    	}
	    	
	    	if (hasProcessed) {
				Boolean memProcessed = null;
				memProcessed = (Boolean) getOldValue(row, colProcessed);
				if (memProcessed == null){
					if(getValueAt(row, colProcessed) instanceof Boolean )
					   memProcessed = (Boolean) getValueAt(row, colProcessed); 
					else if (getValueAt(row, colProcessed) instanceof String )
					   memProcessed = Boolean.valueOf((String)getValueAt(row, colProcessed)); 
				}
	    			
				Boolean dbProcessed = Boolean.TRUE;
				if (! dbProcessedS.equals("Y"))
					dbProcessed = Boolean.FALSE;
				if (memProcessed != null && ! memProcessed.equals(dbProcessed))
					return true;
	    	}
		}
		// @TODO: configurable aggressive - compare each column with the DB
		return false;
	}
	/**
	 * Verify if po has been changed at DB (by other user or process)
	 * @param po
	 * @return true if po has been changed at DB
	 */
	private boolean hasChanged(PO po) {
		if (m_rowChanged < 0)
			return false;
		
		// not so aggressive (it can has still concurrency problems)
		// compare Updated, IsProcessed
		int colUpdated = findColumn("Updated");
		int colProcessed = findColumn("Processed");
		
		boolean hasUpdated = colUpdated >= 0;
		boolean hasProcessed = colProcessed >= 0;
		if (!hasUpdated && !hasProcessed) {
			// no columns updated or processed to compare
			return false;
		}
				
    	Timestamp dbUpdated = (Timestamp) po.get_Value("Updated");
    	if (hasUpdated) {
			Timestamp memUpdated = null;
			memUpdated = (Timestamp) getOldValue(m_rowChanged, colUpdated);
			if (memUpdated == null)
				memUpdated = (Timestamp) getValueAt(m_rowChanged, colUpdated);
			if (memUpdated != null && ! memUpdated.equals(dbUpdated))
				return true;
    	}
    	
    	if (hasProcessed) {
			Boolean memProcessed = null;
			memProcessed = (Boolean) getOldValue(m_rowChanged, colProcessed);
			if (memProcessed == null){
				if(getValueAt(m_rowChanged, colProcessed) instanceof Boolean )
				   memProcessed = (Boolean) getValueAt(m_rowChanged, colProcessed); 
				else if (getValueAt(m_rowChanged, colProcessed) instanceof String )
				   memProcessed = Boolean.valueOf((String)getValueAt(m_rowChanged, colProcessed)); 
			}
    			
			Boolean dbProcessed = po.get_ValueAsBoolean("Processed");
			if (memProcessed != null && ! memProcessed.equals(dbProcessed))
				return true;
    	}
		return false;
	}
		
	/**
	 * get Parent Tab No
	 * @return Parent Tab No
	 */
	private int getParentTabNo()
	{
		int tabNo = m_TabNo;
		int currentLevel = Env.getContextAsInt(m_ctx, m_WindowNo, tabNo, GridTab.CTX_TabLevel);
		int parentLevel = currentLevel-1;
		if (parentLevel < 0)
			return tabNo;
			while (parentLevel != currentLevel)
			{
				tabNo--;				
				currentLevel = Env.getContextAsInt(m_ctx, m_WindowNo, tabNo, GridTab.CTX_TabLevel);
				if (tabNo == 0)
					break;
			}
		return tabNo;
	}
	/**
	 * get Tab No
	 * @return Tab No
	 */
	public int getTabNo()
	{
		return m_TabNo;
	}
	
	/**
	 * @param value
	 * @return true if value is not null and is empty string
	 */
	private boolean isNotNullAndIsEmpty (Object value) {
		if (value != null 
				&& (value instanceof String) 
				&& value.toString().equals("")
			) 
		{
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * @param oldValue
	 * @param value
	 * @return true if oldValue and value is different
	 */
	@SuppressWarnings("unchecked")
	private boolean	isValueChanged(Object oldValue, Object value)
	{
		if ( isNotNullAndIsEmpty(oldValue) ) {
			oldValue = null;
		}
		if ( isNotNullAndIsEmpty(value) ) {
			value = null;
		}
		boolean bChanged = (oldValue == null && value != null) 
							|| (oldValue != null && value == null);
		if (!bChanged && oldValue != null)
		{
			if (oldValue.getClass().equals(value.getClass()))
			{
				if (oldValue instanceof Comparable>)
				{
					bChanged = (((Comparable)oldValue).compareTo(value) != 0);
				}
				else
				{
					bChanged = !oldValue.equals(value);
				}
			}
			else if(value != null)
			{
				bChanged = !(oldValue.toString().equals(value.toString()));
			}
		}
		return bChanged;	
	}
	/**
	 * Load PO for row
	 * @param row row index
	 * @return PO
	 */
	public PO getPO(int row) {
		MTable table = MTable.get (m_ctx, m_AD_Table_ID);
		PO po = null;
		int Record_ID = getKeyID(row);
		if (Record_ID != -1)
		{
			if (Record_ID == 0 && MTable.isZeroIDTable(table.getTableName())) {
				String uuidFromZeroID = table.getUUIDFromZeroID();
				po = table.getPOByUU(uuidFromZeroID, m_trxName);
			} else {
				po = table.getPO(Record_ID, m_trxName);
			}
		}
		else	//	Multi - Key
			po = table.getPO(getWhereClause(getDataAtRow(row)), m_trxName);
		return po;
	}
	/**
	 * @param importing import mode
	 * @param trxName optional trx name
	 */
	public void setImportingMode(boolean importing, String trxName) {
		m_importing = importing;
		m_trxName = trxName;
	}
	/**
	 * @return true if it is in import mode
	 */
	public boolean isImporting() {
		return m_importing;
	}
	
	/**
	 * @return trx name
	 */
	public String get_TrxName() {
		return m_trxName;
	}
	
	/**
	 * reset the cache sort state ( sort column and sort ascending )
	 */
	public void resetCacheSortState() {
		m_lastSortColumnIndex = -1;
		m_lastSortedAscending = true;
	}
	/**
	 * @return index of primary key column
	 */
	public int getKeyColumnIndex() {
		return m_indexKeyColumn;
	}
	/**
	 * Index of change row
	 */
	public int getRowChanged()
	{
		return m_rowChanged;
	}
	
	/**
	 * reset to empty
	 */
	public void reset() 
	{
		if (m_buffer != null)
			m_buffer.clear();
		m_changed = false;
		m_rowChanged = -1;
		if (m_sort != null)
			m_sort.clear();
		if (m_virtualBuffer != null)
			m_virtualBuffer.clear();
		m_rowCount = 0;
		m_rowData = null;
		m_oldValue = null;
		m_inserting = false;
		m_lastSortColumnIndex = -1;
		m_lastSortedAscending = false;
	}
	/**
	 * set current row of gridtable container (gridtab). use in sort to create dse event with new current row (after sort) data
	 * @param m_currentRow
	 */
	protected void setCurrentRow(int m_currentRow) {
		this.m_currentRow  = m_currentRow;
	}
}