/******************************************************************************
 * Product: Compiere ERP & CRM Smart Business Solution                        *
 * Copyright (C) 1999-2007 ComPiere, Inc. All Rights Reserved.                *
 * This program is free software, you can redistribute it and/or modify it    *
 * under the terms version 2 of the GNU General Public License as published   *
 * by the Free Software Foundation. This program is distributed in the hope   *
 * that it will be useful, but WITHOUT ANY WARRANTY, without even the implied *
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.           *
 * See the GNU General Public License for more details.                       *
 * You should have received a copy of the GNU General Public License along    *
 * with this program, if not, write to the Free Software Foundation, Inc.,    *
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.                     *
 * For the text or an alternative of this public license, you may reach us    *
 * ComPiere, Inc., 3600 Bridge Parkway #102, Redwood City, CA 94065, USA      *
 * or via info@compiere.org or http://www.compiere.org/license.html           *
 *****************************************************************************/
package org.compiere.controller;

import java.lang.reflect.*;
import java.math.*;
import java.sql.*;
import java.util.*;
import java.util.logging.*;

import org.compiere.api.*;
import org.compiere.common.*;
import org.compiere.framework.*;
import org.compiere.model.*;
import org.compiere.util.*;

/**
 *	User Interface Tab
 *	
 *  @author Jorg Janke
 *  @version $Id: UITab.java,v 1.1 2008/01/16 07:34:32 jrmt Exp $
 */
public class UITab extends UITabVO
{
	/**
	 * 	UI Tab
	 *	@param vo vo
	 */
	public UITab(UITabVO vo)
	{
		super(vo);
	}	//	UITab

	/** Tab No					*/
	private int					m_tabNo = 0;
	/** Column Names			*/
	private String[]			m_columnNames = null;
	/**	Key Columns				*/
	private ArrayList<String>	m_keyColumns = null;
	/**	Key Column position				*/
	private int	m_keyColumnPos = -1;

	/** Parent Columns			*/
	private ArrayList<String>	m_parentColumns = null;
	/** Identifier Columns		*/
	private ArrayList<String>	m_identifierColumns = null;
	/** Selection Columns		*/
	private ArrayList<String>	m_selectionColumns = null;
	/** Summary Columns			*/
	private ArrayList<String>	m_summaryColumns = null;
	/** Order By Clause			*/
	private String[]	    	m_orderBys = new String[3];
	/** Tab depends on the following fields for UI	*/
	private ArrayList<String>	m_dependsOnUI = new ArrayList<String>();
	/** Used Mnemonics			*/
	private ArrayList<Character> m_mnemonics = new ArrayList<Character>(30);
	/** Sales Order Window/Tab	*/
	private boolean				m_isSOTrx = true;
	/**	Logger	*/
	private static CLogger log = CLogger.getCLogger (UITab.class);

	/**
	 * 	Get relative Tab No
	 *	@return
	 */
	public int getTabNo()
	{
		return m_tabNo;
	}	//	getTabNo

	/**
	 * 	Get Saved Query Names
	 * 	@param AD_Client_ID client
	 *	@return saved Queries
	 */
	public ArrayList<String> getSavedQueryNames(int AD_Client_ID)
	{
		return MUserQuery.getSavedQueryNames(AD_Client_ID, getAD_Tab_ID());
	}	//	getSavedQueryNames

	/**
	 * 	Get Saved Query Names
	 * 	@param AD_User_ID user
	 *	@return saved Queries
	 */
	public ArrayList<String> getSavedQueryNamesForUser(int AD_User_ID)
	{
		return MUserQuery.getSavedQueryNamesForUser(AD_User_ID, getAD_Tab_ID());
	}	//	getSavedQueryNames

	
	/**
	 * 	Initialize
	 *	@param fields fields
	 *	@param previousTabs previous tabs
	 *	@param ctx context
	 *	@param windowNo window
	 *	@param tabNo relative tab
	 */
	protected void initialize (ArrayList<UIField> fields, ArrayList<UITab> previousTabs,
			CContext ctx, int windowNo, int tabNo, boolean isSOTrx)
	{
		log.fine(toString());
		m_columnNames = null;
		m_tabNo = tabNo;
		m_isSOTrx = isSOTrx;
		//
		if (fields == null)
			p_vos = null;
		else
		{
			p_vos = new ArrayList<VO>(fields.size());
			ctx.setIsSOTrx(windowNo, isSOTrx);
			for (int i = 0; i < fields.size(); i++)
			{
				UIField field = fields.get(i);
				field.initialize (ctx, windowNo);
				field.getLookupData(ctx, windowNo, false);
				p_vos.add (field);
			}
		}
		createColumnLists();
		createFieldMnemonics();

		//	Set Link Column
		if (isDetailTab())
		{
			//	explicit
			String linkColumnName = getLinkColumnName();
			//	implicit
			if (linkColumnName == null || linkColumnName.length() == 0)
			{
				ArrayList<String> parents = getParentColumnNames();
				//	Single Parent
				if (parents.size() == 1)
				{
					linkColumnName = parents.get(0);
					setLinkColumnName(linkColumnName);
				}
				//	Multiple Parents
				else
				{
					for (int i = 0; i < previousTabs.size(); i++)
					{
						UITab previousTab = previousTabs.get(i);
						String previousKeyColumn = previousTab.getKeyColumnName();
						if (previousTab.getTabLevel() < getTabLevel())
						{
							for (int j = 0; j < parents.size(); j++)
							{
								String parentColumnName = parents.get(j);
								if (parentColumnName.equals(previousKeyColumn))
								{
									//find the linkColumn as the previous parent's key
									linkColumnName = parents.get(j);
									setLinkColumnName(linkColumnName);
									break;
								}
							}
						}
					}
				}
			}
			if (linkColumnName == null || linkColumnName.length() == 0)
				log.warning("No Link Column: " + toString());
			else
				log.fine ("LinkColumnName=" + linkColumnName);
		}
		createDependencyRelations();
	}	//	initialize

	/**	Column with list of Columns depending */
	HashMap<String, ArrayList<String>> m_allDependents = new HashMap<String, ArrayList<String>>();

	/**
	 * 	Create Dependency Relations.
	 * 	Field Logic contains the columns it depends on [displayed if Active=Y] 
	 * 	(dependensOn - created via Field.initialize()/createDependsOnLists())
	 * 	This routine calculates the Impact [Active impacts]
	 * 	and saves that in the field.	
	 */
	private void createDependencyRelations() 
	{
		//	Tab Display dependent
		Evaluator.parseDepends(m_dependsOnUI, getDisplayLogic());
		Evaluator.parseDepends(m_dependsOnUI, getReadOnlyLogic());
		for (int i = 0; i < m_dependsOnUI.size(); i++)
		{
			String impactColumnName = m_dependsOnUI.get(i);
			UIField impactField = getField(impactColumnName);
			if (impactField == null)
				log.finer("Not found (TabUI): " + impactColumnName);
			else
				impactField.setImpactsUITab(true);
		}

		//	All Field DependsOn
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			String columnName = field.getColumnName();
			ArrayList<String> uis = field.getDependsOnUI();
			for (int j = 0; j < uis.size(); j++)
			{
				String impactColumnName = uis.get(j);
				UIField impactField = getField(impactColumnName);
				if (impactField == null)
					log.finer("Not found (FieldUI): " + impactColumnName);
				else
					impactField.addImpactsUIColumn(columnName);
			}
			ArrayList<String> values = field.getDependsOnValue();
			for (int j = 0; j < values.size(); j++)
			{
				String impactColumnName = values.get(j);
				UIField impactField = getField(impactColumnName);
				if (impactField == null)
					log.finer("Not found (FieldValue): " + impactColumnName);
				else
					impactField.addImpactsValueColumn(columnName);
			}
		}

		if (CLogMgt.isLevelFiner())
		{
			for (int i = 0; i < p_vos.size(); i++)
			{
				UIField field = (UIField)p_vos.get(i);
				if (field.getImpactsUI().size() > 0)
					log.fine(field.getColumnName() + ": UI Impact on: " + field.getImpactsUI());
				if (field.getImpactsValue().size() > 0)
					log.fine(field.getColumnName() + ": Value Impact on: " + field.getImpactsValue());
				if (field.isImpactsUITab())
					log.fine(field.getColumnName() + ": Tab Impact");
			}
		}	//	debug
	}	//	createDependencyRelations

	/**
	 * 	Get the list of column names this tab Depends On for UI.
	 * 	(Display, ReadOnly, Mandatory)
	 *	@return list of columns
	 */
	public ArrayList<String> getDependsOnUI()
	{
		return m_dependsOnUI;
	}	//	getDependsOnUI

	/**
	 * 	Get Fields
	 *	@return Fields
	 */
	public ArrayList<UIField> getFields()
	{
		if (p_vos == null)
			return null;
		ArrayList<UIField> retValue = new ArrayList<UIField>(p_vos.size());
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			retValue.add(field);
		}
		return retValue;
	}	//	getFields

	/**
	 * 	Get Field
	 *	@return Field with name or null
	 */
	public UIField getField (String columnName)
	{
		if (p_vos == null)
			return null;
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			if (field.getColumnName().equals(columnName))
				return field;
		}
		return null;
	}	//	getField

	/**
	 * 	Get Field Index
	 *	@return Field with name or null
	 */
	public int getFieldIndex (String columnName)
	{
		if (p_vos == null)
			return -1;
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			if (field.getColumnName().equals(columnName))
				return i;
		}
		return -1;
	}	//	getField

	/**
	 * 	Get Column Names
	 *	@return column names
	 */
	public String[] getColumnNames()
	{
		if (m_columnNames == null && p_vos != null)
		{
			m_columnNames = new String[p_vos.size()];
			for (int i = 0; i < p_vos.size(); i++)
			{
				UIField field = (UIField)p_vos.get(i);
				m_columnNames[i] = field.getColumnName();
			}
		}
		return m_columnNames;
	}	//	getColumnNames

	/**
	 * 	Detail Tab
	 *	@return true if tab level is 0
	 */
	public boolean isDetailTab()
	{
		return getAD_Column_ID() != 0
		|| getParentColumnNames().size() > 0;
	}	//	isDetailTab

	/**
	 * 	Get Key Columns
	 *	@return list with one or more columns (if no PK)
	 */
	public ArrayList<String> getKeyColumnNames()
	{
		if (m_keyColumns == null)
			createColumnLists();
		return m_keyColumns;
	}	//	getKeyColumnNames

	/**
	 * 	Get Key Column Name
	 *	@return
	 */
	public String getKeyColumnName()
	{
		ArrayList<String> keyColumns = getKeyColumnNames();
		int size = keyColumns.size();
		if (size == 0)
		{
			log.warning("None - " + toString());
			return "";
		}
		else if (size > 1)
			log.warning("More than one KeyColumn - " + toString());
		return keyColumns.get(0); 
	}	//	getKeyColumnName

	/**
	 * 	Get parent Columns
	 *	@return list with none or more columns
	 */
	public ArrayList<String> getParentColumnNames()
	{
		if (m_parentColumns == null)
			createColumnLists();
		return m_parentColumns;
	}	//	getParentColumnNames

	/**
	 * 	Get Identifier Column Names ordered
	 *	@return list with identifier columns
	 */
	public ArrayList<String> getIdentifierColumnNames()
	{
		if (m_identifierColumns == null)
			createColumnLists();
		return m_identifierColumns;
	}	//	getIdentifierColumnNames

	/**
	 * 	Get Selection Column Names ordered
	 *	@return list with selection columns
	 */
	public ArrayList<String> getSelectionColumnNames()
	{
		if (m_selectionColumns == null)
			createColumnLists();
		return m_selectionColumns;
	}	//	getSelectionColumnNames

	/**
	 * 	Get Summary Column Names ordered
	 *	@return list with summary columns
	 */
	public ArrayList<String> getSummaryColumnNames()
	{
		if (m_summaryColumns == null)
			createColumnLists();
		return m_summaryColumns;
	}	//	getSummaryColumnNames

	/**
	 * 	Create all Column Lists
	 */
	@SuppressWarnings("unchecked")
	private synchronized void createColumnLists()
	{
		if (p_vos == null)
			return;
		//
		m_keyColumns = new ArrayList<String>(2);
		m_parentColumns = new ArrayList<String>(2);
		m_identifierColumns = new ArrayList<String>();
		m_selectionColumns = new ArrayList<String>();
		m_summaryColumns = new ArrayList<String>();

		m_keyColumnPos = -1;

		int parentColumnPos = -1;
		//	Intermediate
		ArrayList<KeyNamePair> identifierPos = new ArrayList<KeyNamePair>();
		ArrayList<KeyNamePair> selectionPos = new ArrayList<KeyNamePair>();
		ArrayList<KeyNamePair> summaryPos = new ArrayList<KeyNamePair>();
		//	Loop
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			String columnName = field.getColumnName();
			//	Key
			if (field.isKey())
			{
				m_keyColumns = new ArrayList<String>(1);
				m_keyColumns.add (columnName);
				if(m_keyColumnPos == -1)
					m_keyColumnPos = i;
			}
			//	Parent
			if (field.isParent()) {
				m_parentColumns.add (columnName);
				if(parentColumnPos == -1)
					parentColumnPos = i;
			}
			//	Identifier
			if (field.isIdentifier())
			{
				KeyNamePair pp = new KeyNamePair(field.getSeqNo(), columnName);
				pp.setSortByName(false);
				identifierPos.add (pp);
			}
			//	Selection
			if (field.isSelectionColumn())
			{
				KeyNamePair pp = new KeyNamePair(field.getSelectionSeqNo(), columnName);
				pp.setSortByName(false);
				selectionPos.add (pp);
			}
			else if (columnName.startsWith("DocumentNo") 
					|| columnName.equals ("Value") || columnName.equals ("Name"))
			{
				KeyNamePair pp = new KeyNamePair(0, columnName);
				pp.setSortByName(false);
				selectionPos.add (pp);
			}
			//	Summary
			if (field.isSummaryColumn())
			{
				KeyNamePair pp = new KeyNamePair(field.getSummarySeqNo(), columnName);
				pp.setSortByName(false);
				summaryPos.add (pp);
			}
			else if (columnName.startsWith("DocumentNo") 
					|| columnName.equals ("Value") || columnName.equals ("Name"))
			{
				KeyNamePair pp = new KeyNamePair(0, columnName);
				pp.setSortByName(false);
				summaryPos.add (pp);
			}
			//	OrderBy
			BigDecimal sort = field.getSortNo();
			if (sort != null && sort.signum() != 0)
			{
				int index = sort.abs().intValue() - 1;
				if (index < 3)
				{
					if (m_orderBys[index] == null)
					{
						m_orderBys[index] = columnName;
						if (sort.signum() < 0)
							m_orderBys[index] += " DESC";
					}
					else
						log.warning("Ignored OrderBy Duplicate Position " 
								+ (index + 1) + ": " + columnName + " - " + toString());
				}
				else
					log.warning("Ignored OrderBy " + columnName + " - " + toString());
			}
		}	//	column loop
		if (m_keyColumns.size() == 0) {
			m_keyColumns = m_parentColumns;
			m_keyColumnPos = parentColumnPos;
		}

		//	Sort them
		Collections.sort(identifierPos);
		for (int i = 0; i < identifierPos.size(); i++)
			m_identifierColumns.add(identifierPos.get(i).getName());
		Collections.sort(selectionPos);
		for (int i = 0; i < selectionPos.size(); i++)
			m_selectionColumns.add(selectionPos.get(i).getName());
		Collections.sort(summaryPos);
		for (int i = 0; i < summaryPos.size(); i++)
			m_summaryColumns.add(summaryPos.get(i).getName());
	}	//	createColumnLists

	/**
	 * 	Create Field Mnemonics
	 */
	private void createFieldMnemonics()
	{
		//	Predefined in the text via &
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			if (field.isCreateMnemonic())
			{
				String text = field.getName();
				//	Predefined
				int pos = text.indexOf('&');
				if (pos != -1 && text.length() > pos)	//	We have a mnemonic - creates Ctrl_Shift_
				{
					char mnemonic = text.toUpperCase().charAt(pos+1);
					if (mnemonic != ' ')
					{
						if (!m_mnemonics.contains(mnemonic))
						{
							field.setMnemonic(mnemonic);
							m_mnemonics.add(mnemonic);
						}
						else
							log.warning(field.getColumnName() 
									+ " - Conflict - Already exists: " + mnemonic + " (" + text + ")");
					}
				}
			}	//	mnemonic
			else
				field.setMnemonic((char)0);
		}	//	for all

		//	Search for first letter in word, then any character
		for (int i = 0; i < p_vos.size(); i++)
		{
			UIField field = (UIField)p_vos.get(i);
			if (field.getMnemonic() != 0)
				continue;	//	already set
			String text = field.getName();
			String oText = text;
			text = text.trim().toUpperCase();
			char mnemonic = text.charAt(0);
			if (m_mnemonics.contains(mnemonic))
			{
				mnemonic = 0;
				//	Beginning new word
				int index = text.indexOf(' ');
				while (index != -1 && text.length() > index)
				{
					char c = text.charAt(index+1);
					if (Character.isLetterOrDigit(c) && !m_mnemonics.contains(c))
					{
						mnemonic = c;
						break;
					}
					index = text.indexOf(' ', index+1);
				}
				//	Any character
				if (mnemonic == 0)
				{
					for (int j = 1; j < text.length(); j++)
					{
						char c = text.charAt(j);
						if (Character.isLetterOrDigit(c) && !m_mnemonics.contains(c))
						{
							mnemonic = c;
							break;
						}
					}
				}
				//	Nothing found
				if (mnemonic == 0)
					log.finest("None for: " + oText);
			}
			if (mnemonic != 0)
			{
				field.setMnemonic(mnemonic);
				m_mnemonics.add(mnemonic);
			}
		}	//	for all fields
	}	//	createMnemonics

	/**
	 * 	Is Window in SO Context
	 *	@return true if window SO
	 */
	public boolean isSOTrx()
	{
		return m_isSOTrx;
	}	//	isSOTrx

	/**************************************************************************
	 * 	Evaluate Query
	 *	@param queryVO optional query
	 *	@param context record context for link columns and other variables
	 *	@param ctx user context
	 *	@return number of records or -1 if error
	 */
	public int evaluateQuery (QueryVO queryVO,  
			HashMap<String,String> context, CContext ctx)
	{
		int AD_Role_ID = ctx.getAD_Role_ID();
		int AD_User_ID = ctx.getAD_User_ID();
		MRole role = MRole.get(ctx, AD_Role_ID, AD_User_ID, false); 
		String whereClause = getWhereClause(queryVO, role, context, ctx, false);
		StringBuffer sql0 = new StringBuffer("SELECT COUNT(*) FROM ")
		.append (getTableName())
		.append (whereClause);
		String sql1 = role.addAccessSQL(sql0.toString(), getTableName(), 
				MRole.SQL_FULLYQUALIFIED, MRole.SQL_RO);
		int no = DB.getSQLValue (null, sql1);
		return no;
	}	//	evaluateQuery

	/**
	 * 	Execute Query for Tab
	 *	@param queryVO optional query
	 *	@param context record context for link columns and other variables
	 *	@param ctx user context
	 *	@return number of records
	 */
	public ArrayList<Object[]> executeQuery (QueryVO queryVO,  
			HashMap<String,String> context, CContext ctx)
			{
		int AD_Role_ID = ctx.getAD_Role_ID();
		MRole role = MRole.get(ctx, AD_Role_ID, ctx.getAD_User_ID(), true);
		String whereClause = getWhereClause(queryVO, role, context, ctx, true);
		StringBuffer sql0 = new StringBuffer("SELECT ");
		ArrayList<UIField> fields = getFields();
		for (int i = 0; i < fields.size(); i++)
		{
			if (i > 0)
				sql0.append(",");
			UIField field = fields.get(i);
			if (field.isVirtualColumn())
				sql0.append(field.getColumnSQL()).append(" AS ");
			sql0.append(field.getColumnName());
		}
		sql0.append(" FROM ")
		.append (getTableName())
		.append (whereClause);
		String sql1 = role.addAccessSQL(sql0.toString(), getTableName(), 
				MRole.SQL_FULLYQUALIFIED, MRole.SQL_RO);
		//
		ArrayList<Object[]> results = new ArrayList<Object[]>(); 
		String[] columns = getColumnNames();
		PreparedStatement pstmt = null;
		try
		{
			pstmt = DB.prepareStatement (sql1, null);
			ResultSet rs = pstmt.executeQuery ();
			while (rs.next ())
			{
				Object[] row = new Object[columns.length];
				for (int i = 0; i < columns.length; i++)
				{
					
					Object oo = rs.getObject(columns[i]);
					//to retrieve the time portion
					if(oo instanceof java.sql.Date)
						oo = rs.getTimestamp(columns[i]);
					row[i] = oo;
				}
				results.add(row);
			}
			rs.close ();
			pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			log.log (Level.SEVERE, sql1, e);
		}
		try
		{
			if (pstmt != null)
				pstmt.close ();
			pstmt = null;
		}
		catch (Exception e)
		{
			pstmt = null;
		}
		if (results.size() > 0)
		{
			MSession session = MSession.get(ctx, true);
			session.queryLog(ctx.getAD_Client_ID(), ctx.getAD_Org_ID(), getAD_Table_ID(), 
					whereClause, results.size());
		}
		//	Results
		return results;
			}	//	executeQuery

	/**
	 * 	Execute Query for Tab and return results as String
	 *	@param queryVO optional query
	 *	@param context record context for link columns and other variables
	 *	@param ctx user context
	 *	@return number of records
	 */
	public ArrayList<String[]> executeQueryString (QueryVO queryVO,  
			HashMap<String,String> context, CContext ctx)
			{
		ArrayList<Object[]> from = executeQuery(queryVO, context, ctx);
		ArrayList<String[]> to = new ArrayList<String[]>(from.size());
		for (int i = 0; i < from.size(); i++)
		{
			Object[] fromRow = from.get(i);
			to.add(convertToString(fromRow));
		}
		return to;
			}	//	executeQueryString

	/**
	 * 	Convert To String
	 *	@param fromRow objects 
	 *	@return strings
	 */
	public String[] convertToString(Object[] fromRow)
	{
		String[] toRow = new String[fromRow.length];
		for (int i = 0; i < fromRow.length; i++)
		{
			Object fromValue = fromRow[i];
			if (fromValue == null)
				toRow[i] = null;
			else if (fromValue instanceof java.sql.Timestamp)
			{
				long time = ((java.sql.Timestamp)fromValue).getTime();
				toRow[i] = String.valueOf(time);
			}

			else if (fromValue instanceof java.sql.Date)
			{
				long time = ((java.sql.Date)fromValue).getTime();
				toRow[i] = String.valueOf(time);
			}
			else if(fromValue instanceof Boolean)
			{
				if(((Boolean)fromValue).booleanValue())
					toRow[i] = "Y";
				else
					toRow[i] = "N";
			}
			else
				toRow[i] = fromValue.toString();
		}
		return toRow;
	}	//	convertToString

	/**
	 * 	Get Where & Order By Clause
	 *	@param queryVO optional restrictions
	 *	@param role security role
	 *	@param context context for link columns and other variables
	 *	@param ctx user context
	 *	@param addOrderBy if true add order by clause
	 *	@return where clause with tailoring space
	 */
	private String getWhereClause(QueryVO queryVO, MRole role, 
			HashMap<String,String> context, CContext ctx,  
			boolean addOrderBy)
	{
		StringBuffer sb = new StringBuffer();
		//	Detail Tab - Link
		if (isDetailTab())
		{
			String linkColumnName = getLinkColumnName();
			if (linkColumnName.length() == 0)
			{
				log.warning("No LinkColumn - " + toString());
				sb.append(" WHERE 2=3");
				return sb.toString();
			}
			String linkColumnValue = context.get(linkColumnName);
			if (linkColumnValue == null)
			{
				log.warning("No Value for LinkColumn=" + linkColumnName + " - " + toString());
				sb.append(" WHERE 2=4");
				return sb.toString();
			}
			else if (linkColumnName.endsWith("_ID"))
				sb.append(" WHERE ").append(linkColumnName)
				.append("=").append(linkColumnValue);
			else
				sb.append(" WHERE ").append(linkColumnName)
				.append("='").append(linkColumnValue).append("'");
		}

		//	Query
		int onlyCurrentDays = 0;
		boolean onlyCurrentCreated = true;
		if (queryVO != null && queryVO.onlyCurrentDays > 0)
		{
			onlyCurrentDays = queryVO.onlyCurrentDays;
			onlyCurrentCreated = queryVO.onlyCurrentCreated;
			//
			if (sb.length() == 0)
				sb.append(" WHERE ");
			else
				sb.append(" AND ");
			boolean showNotProcessed = getField("Processed") != null;
			if (showNotProcessed)
				sb.append("(Processed='N' OR ");
			if (onlyCurrentCreated)
				sb.append("Created>=");
			else
				sb.append("Updated>=");
			sb.append("addDays(SysDate, -")
			.append(onlyCurrentDays).append(")");
			if (showNotProcessed)
				sb.append(")");
		}
		else if (queryVO != null)
		{
			Query query = createQuery(ctx, queryVO);
			//	Validate Query (zoom to sub-tabs)
			if (query != null && query.isActive())
			{
				if (sb.length() == 0)
					sb.append(" WHERE ");
				else
					sb.append(" AND ");
				sb.append(validateQuery(query));
			}
		}

		//	Static Tab Where Clause
		String where = getWhereClause();
		if (where != null && where.length() > 0)
		{
			if (sb.length() == 0)
				sb.append(" WHERE ");
			else
				sb.append(" AND ");
			if (where.indexOf("@") != -1)
			{
				int windowNo = 111111;
				ctx.addWindow(windowNo, context);
				where = Env.parseContext(ctx, windowNo, where, false);
			}
			sb.append(where);
		}

		if (addOrderBy)
			sb.append(getOrderByClause(onlyCurrentDays, onlyCurrentCreated));
		return sb.toString();
	}	//	getWhereClause

	/**
	 * 	Create Query
	 *	@param qieryVO optional query restrictions
	 *	@return query or null
	 */
	private Query createQuery (CContext ctx, QueryVO queryVO)
	{
		if (queryVO == null)
			return null;

		//	Query Restrictions
		Query query = null;
		if (queryVO.restrictions != null && queryVO.restrictions.size() > 0)
		{
			query = new Query(getTableName());
			for (int i = 0; i < queryVO.restrictions.size(); i++)
			{
				QueryRestrictionVO vo = (QueryRestrictionVO) queryVO.restrictions.get(i);
				Object qCode = DisplayType.convertFromString(vo.DisplayType, vo.Code);
				Object qCode_to = DisplayType.convertFromString(vo.DisplayType, vo.Code_to);
				QueryRestriction restriction = new QueryRestriction(vo.ColumnName, 
						qCode, qCode_to, vo.InfoName, vo.InfoDisplay, vo.InfoDisplay_to,
						vo.Operator, vo.DirectWhereClause, vo.AndCondition);	
				query.addRestriction(restriction);
			}
		}

		//	Save Query
		if (queryVO.savedQueryName != null && queryVO.savedQueryName.length() > 0
				&& queryVO.saveQuery && query != null)
		{
			//overwrite old saved query with new code if it already exists
			MUserQuery userQ = MUserQuery.getForUser(ctx, getAD_Tab_ID(), queryVO.savedQueryName);
			if(userQ == null){
				userQ = new MUserQuery(ctx, 0, null);
				userQ.setName(queryVO.savedQueryName);
				userQ.setAD_Tab_ID(getAD_Tab_ID());
				userQ.setAD_Table_ID(getAD_Table_ID());
				userQ.setAD_User_ID(ctx.getAD_User_ID());
			}
			userQ.setCode(query.getWhereClause());

			userQ.save();
		}
		//	Saved Query
		else if (queryVO.savedQueryName != null && queryVO.savedQueryName.length() > 0)
		{
			MUserQuery userQ = MUserQuery.get(ctx, getAD_Tab_ID(), queryVO.savedQueryName);
			if (userQ == null)
				log.warning("SavedQuery nor found: " + queryVO.savedQueryName + " - " + toString());
			else
			{
				query = new Query(getTableName());
				query.addRestriction(userQ.getCode());
			}
		}
		return query;
	}	//	createQuery

	/**
	 * 	Get Order By Clause
	 *	@param currentDays if > 1 query only recent records
	 *	@param currentCreated if true get recent records based on Created otherwise Updated
	 *	@return order by clause or ""
	 */
	private String getOrderByClause (int currentDays, boolean currentCreated)
	{
		StringBuffer ob = new StringBuffer(" ORDER BY ");
		//	First Prio: Tab Order By
		String order = super.getOrderByClause();
		if (order != null && order.length() > 0)
			return ob.append(order).toString();

		//	Second Prio: Fields (save it)
		StringBuffer fieldOrder = new StringBuffer();
		for (int i = 0; i < 3; i++)
		{
			order = m_orderBys[i];
			if (order != null && order.length() > 0)
			{
				if (fieldOrder.length() > 0)
					fieldOrder.append(",");
				fieldOrder.append(order);
			}
		}
		if (fieldOrder.length() > 0)
		{
			setOrderByClause(fieldOrder.toString());	//	save for next
			return ob.append(fieldOrder).toString();
		}

		//	Third Prio: currentRows
		if (currentDays > 0)
		{
			if (currentCreated)
				ob.append("Created DESC");
			else
				ob.append("Updated DESC");
			return ob.toString();
		}
		//
		log.warning("No OrderBy - " + toString()); 
		return "";
	}	//	getOrderByClause

	/**
	 * 	Validate Query.
	 *  If query column is not a tab column create EXISTS query
	 * 	@param query query
	 * 	@return where clause
	 */
	private String validateQuery (Query query)
	{
		if (query == null || query.getRestrictionCount() == 0)
			return null;

		//	Check: only one restriction
		if (query.getRestrictionCount() != 1)
		{
			log.fine("Ignored(More than 1 Restriction): " + query);
			return query.getWhereClause();
		}

		String colName = query.getColumnName(0);
		if (colName == null)
		{
			log.fine("Ignored(No Column): " + query);
			return query.getWhereClause();
		}
		//	a '(' in the name = function - don't try to resolve
		if (colName.indexOf('(') != -1)
		{
			log.fine("Ignored(Function): " + colName);
			return query.getWhereClause();
		}
		//	OK - Query is valid 

		//	Zooms to the same Window (Parents, ..)
		String refColName = null;
		if (colName.equals("R_RequestRelated_ID"))
			refColName = "R_Request_ID";
		else if (colName.startsWith("C_DocType"))
			refColName = "C_DocType_ID";
		else if (colName.equals("Orig_Order_ID")) 
			refColName = "C_Order_ID";
		else if (colName.equals("Orig_InOut_ID"))
			refColName = "M_InOut_ID";

		
		if (refColName != null)
		{
			query.setColumnName(0, refColName);
			if (getField(refColName) != null)
			{
				log.fine("Column " + colName + " replaced with synonym " + refColName);
				return query.getWhereClause();
			}
			refColName = null;
		}

		//	Simple Query. 
		if (getField(colName) != null)
		{
			log.fine("Field Found: " + colName);
			return query.getWhereClause();
		}

		//	Find Refernce Column e.g. BillTo_ID -> C_BPartner_Location_ID
		String sql = "SELECT cc.ColumnName "
			+ "FROM AD_Column c"
			+ " INNER JOIN AD_Ref_Table r ON (c.AD_Reference_Value_ID=r.AD_Reference_ID)"
			+ " INNER JOIN AD_Column cc ON (r.Column_Key_ID=cc.AD_Column_ID) "
			+ "WHERE c.AD_Reference_ID IN (18,30)" 	//	Table/Search
			+ " AND c.ColumnName=?";
		try
		{
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setString(1, colName);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
				refColName = rs.getString(1);
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, "(ref) - Column=" + colName, e);
			return query.getWhereClause();
		}
		//	Reference Column found
		if (refColName != null)
		{
			query.setColumnName(0, refColName);
			if (getField(refColName) != null)
			{
				log.fine("Column " + colName + " replaced with " + refColName);
				return query.getWhereClause();
			}
			colName = refColName;
		}

		//	Column NOT in Tab - create EXISTS subquery
		String tableName = null;
		String tabKeyColumn = getKeyColumnName();
		//	Column=SalesRep_ID, Key=AD_User_ID, Query=SalesRep_ID=101

		sql = "SELECT t.TableName "
			+ "FROM AD_Column c"
			+ " INNER JOIN AD_Table t ON (c.AD_Table_ID=t.AD_Table_ID) "
			+ "WHERE c.ColumnName=? AND IsKey='Y'"		//	#1 Link Column
			+ " AND EXISTS (SELECT * FROM AD_Column cc"
			+ " WHERE cc.AD_Table_ID=t.AD_Table_ID AND cc.ColumnName=?)";	//	#2 Tab Key Column
		try
		{
			PreparedStatement pstmt = DB.prepareStatement(sql, null);
			pstmt.setString(1, colName);
			pstmt.setString(2, tabKeyColumn);
			ResultSet rs = pstmt.executeQuery();
			if (rs.next())
				tableName = rs.getString(1);
			rs.close();
			pstmt.close();
		}
		catch (SQLException e)
		{
			log.log(Level.SEVERE, "Column=" + colName + ", Key=" + tabKeyColumn, e);
			return null;
		}

		//	Special Reference Handling
		if (tabKeyColumn.equals("AD_Reference_ID"))
		{
			//	Column=AccessLevel, Key=AD_Reference_ID, Query=AccessLevel='6'
			sql = "SELECT AD_Reference_ID FROM AD_Column WHERE ColumnName=?";
			int AD_Reference_ID = DB.getSQLValue(null, sql, colName);
			return "AD_Reference_ID=" + AD_Reference_ID;
		}

		//	Causes could be functions in query
		//	e.g. Column=UPPER(Name), Key=AD_Element_ID, Query=UPPER(AD_Element.Name) LIKE '%CUSTOMER%'
		if (tableName == null)
		{
			log.info ("Not successfull - Column=" 
					+ colName + ", Key=" + tabKeyColumn 
					+ ", Query=" + query);
			return query.getWhereClause();
		}

		query.setTableName("xx");
		StringBuffer result = new StringBuffer ("EXISTS (SELECT * FROM ")
		.append(tableName).append(" xx WHERE ")
		.append(query.getWhereClause(true))
		.append(" AND xx.").append(tabKeyColumn).append("=")
		.append(getTableName()).append(".").append(tabKeyColumn).append(")");
		log.fine(result.toString());
		return result.toString();
	}	//	validateQuery

	private ArrayList<String> getUpdatedFieldsByOthers(PO po, String[] cachedRow) {
		ArrayList<String> conflictedFields = new ArrayList<String>();
			int size = p_vos.size();
			for (int i = 0; i < size; i++)
			{
				UIField field = (UIField)p_vos.get (i);
				String colName = field.getColumnName();
				if (colName.startsWith("Created") 
						|| colName.startsWith("Updated")
						|| colName.equals("IsActive")
						|| colName.equals("AD_Client_ID") 
						|| colName.equals("AD_Org_ID"))
					continue;

				String v = cachedRow[i] == null ? "" : cachedRow[i];
				if(!v.equals(po.get_ValueAsString(colName)))
							conflictedFields.add(colName);
			}
				
		return conflictedFields;
		
	}

	/**************************************************************************
	 * 	Save (Insert/Update) Row of Tab
	 * 	@param ctx general context
	 *	@param context current (relevant) context of new row
	 *	@return error message or null
	 */
	@SuppressWarnings("unchecked")
	public ChangeVO saveRow (CContext ctx, int windowNo, boolean newRecord) {
		return saveRow(ctx, windowNo, newRecord, null);
	}

	
	/**************************************************************************
	 * 	Save (Insert/Update) Row of Tab
	 * 	@param ctx general context
	 *	@param context current (relevant) context of new row
	 *  @param cachedRow the row cached for this user session
	 *	@return error message or null
	 */
	@SuppressWarnings("unchecked")
	public ChangeVO saveRow (CContext ctx, int windowNo, boolean newRecord, String[] cachedRow)
	{
		//	ReadOnly
		String error = checkReadOnly(ctx, windowNo);
		if (error != null)
			return new ChangeVO(true, Msg.parseTranslation(ctx, error));

		//	Mandatory Fields
		error = checkMandatory (ctx, windowNo);
		if (error != null)
			return new ChangeVO(true, Msg.parseTranslation(ctx, error));

		PO po = getPO (ctx, windowNo, newRecord);
		Map<String,String> windowMap = ctx.getMap (windowNo);

		//preprocess windowMap
		//those 4 columns should not be updated for po
		windowMap.remove("CreatedBy");
		windowMap.remove("UpdatedBy");
		windowMap.remove("CreatedOn");
		windowMap.remove("UpdatedOn");
		
		
		int size = p_vos.size();
		for (int i = 0; i < size; i++)
		{
			UIField field = (UIField)p_vos.get (i);
			//return an empty string for passwords etc
			if(field.isEncryptedField() || field.isEncryptedColumn()|| "Password".equals(field.getColumnName())) {
				String colName = field.getColumnName();
				if(windowMap.get(colName) == null || windowMap.get(colName).length() == 0)
					windowMap.remove(colName);
			}
		}

		
		
		//dzhao, here we compare db values from the cached values and then compare with
		//values that need to be changed, if no conflicts, then we update
		if(!newRecord && cachedRow != null) {
			ArrayList<String> fields = getUpdatedFieldsByOthers(po, cachedRow);
			if(fields.size() > 0) {
				ChangeVO c = new ChangeVO(true, "Please refresh the row. The following fileds are updated by others:"+fields);
				return c;
			}
		}
		if (po == null)
			return new ChangeVO(true, Msg.getMsg(ctx, "Error") + " - No PO for " + toString());

		//	Can we update?
		
		error = po.update (windowMap);
		if (error != null)
			return new ChangeVO(true, Msg.getMsg(ctx, "Error") + " - " + error);
		//	Save
		if (!po.save()) {
			ChangeVO change = new ChangeVO(true, Msg.getMsg(ctx, "NotSaved"));
			ValueNamePair e = CLogger.retrieveError();

			change.addError(Msg.getMsg(ctx, e.getID(), e.getName()));
			Exception ex = CLogger.retrieveException();
			if(ex != null) {
			if(ex.getCause() != null)
							change.addError(ex.getCause().getLocalizedMessage());
					else
					change.addError(ex.getLocalizedMessage());
			}

			return change;
		}
		ChangeVO retValue = new ChangeVO(Msg.getMsg(ctx, "Saved"));
		po.load((String)null);		//	reload
		
		addLog(ctx, retValue);
		retValue.rowData = po.get_ValuesAsString(getColumnNames());
		return retValue;
	}	//	saveRow
	
	private void addLog(CContext ctx, ChangeVO change) {
		ValueNamePair p = CLogger.retrieveWarning();
		if(p != null)
			change.addWarning(Msg.getMsg(ctx, p.getID(), p.getName()));
			
		p = CLogger.retrieveInfo();
		if(p != null)
			change.addSuccess(Msg.getMsg(ctx, p.getID(), p.getName()));
	}

	/**************************************************************************
	 * 	Refresh (Insert/Update) Row of Tab
	 * 	@param ctx general context
	 *	@param context current (relevant) context of new row
	 *	@return error message or null
	 */
	@SuppressWarnings("unchecked")
	public ChangeVO refreshRow (CContext ctx, int windowNo)
	{
		PO po = getPO (ctx, windowNo, false);
		//if po is null, just return a message,
		//this is so when a process delete a record, the process will not be interrupted

		if (po == null) {
			ChangeVO c = new ChangeVO(false, "No PO for " + toString());
			c.rowData = new String[this.m_columnNames.length];
			for(int i=0; i<c.rowData.length; i++)
				c.rowData[i] = null;
			return c;
		}

		ChangeVO retValue = new ChangeVO();
		retValue.rowData = po.get_ValuesAsString(getColumnNames());
		return retValue;
	}	//	refreshRow

	/**
	 * 	Delete existing Row
	 * 	@param ctx general context
	 *	@param context current (relevant) context of row
	 *	@return error message or null
	 */
	public ChangeVO deleteRow (CContext ctx, int windowNo)
	{
		String error = checkReadOnly(ctx, windowNo);
		if (error != null)
			return new ChangeVO(true, error);

		PO po = getPO (ctx, windowNo, false);
		if (po == null)
			return new ChangeVO(true, Msg.getMsg(ctx, "Error") + " - No PO for " + toString());
		if (!po.delete(false)) {
			ValueNamePair e = CLogger.retrieveError();
			ChangeVO change = new ChangeVO(true, Msg.getMsg(ctx, e.getID(), e.getName()));
			return change;

		}
		ChangeVO retValue = new ChangeVO(Msg.getMsg(ctx, "Deleted")); 
		addLog(ctx, retValue);
		return retValue;
	}	//	deleteRow

	/**
	 * 	New Row
	 *	@param ctx context
	 *	@param windowNo window
	 *	@return default values for new row
	 */
	public ChangeVO newRow (CContext ctx, int windowNo)
	{
		int size = p_vos.size();
		ChangeVO retValue = new ChangeVO();
		Map<String,String> changedValues = new HashMap<String,String>();
		retValue.changedFields = (HashMap<String,String>)changedValues;
		for (int i = 0; i < size; i++)
		{
			UIField field = (UIField)p_vos.get(i);
			String key = field.getColumnName();
			String value = field.getDefaultAsString(ctx, windowNo, this);
			if (field.isKey() && field.getColumnName().endsWith("_ID")) {
				value = "0";	//	explicit init
			}
			changedValues.put(key, value);
			//wipe out noise ctx from probably previous child record
			//if(!field.isParent())
				//ctx.setContext(windowNo, key, "");

		}
		if(org.compiere.common.constants.Build.isDebug())
			log.info("changed values are  before callout:"+changedValues);

		//dizhao, very important
		//context here can only contains parent context not this child's context
		//setContext would eliminate fields that has "" or null
		ctx.setContext(windowNo, changedValues);
		//for()
		//	Callouts
		for (int i = 0; i < size; i++)
		{
			UIField field = (UIField)p_vos.get(i);
			if (field.isCallout() 
					|| (field.getCallout() != null && field.getCallout().length() > 0))
			{
				String key = field.getColumnName();
				String newValue = (String)changedValues.get(key);
				ChangeVO calloutChange = processCallout (ctx, windowNo, field, 
						null, newValue);
				if(org.compiere.common.constants.Build.isDebug())
					log.info("After callout for "+field.getColumnName()+" the change is"+calloutChange);
				if( calloutChange != null )
				{
					ctx.setContext(windowNo, calloutChange.changedContext);
					ctx.setContext(windowNo, calloutChange.changedFields);
				}
				retValue.addAll(calloutChange);
			}
		}
		if(org.compiere.common.constants.Build.isDebug())
			log.info("field values are  after callout:"+retValue.changedFields);
		return retValue;
	}	//	newRow

	/**
	 * 	Get PO
	 * 	@param ctx general context
	 *	@param windowNo Window for context current\
	 *	@param newRecord create new record
	 *	@return PO or null
	 */
	private PO getPO (CContext ctx, int windowNo, boolean newRecord)
	{
		MTable table = MTable.get(ctx, getAD_Table_ID());
		if (newRecord)
			return table.getPO(ctx, 0, null);	//	always returns new
		//
		ArrayList<String> keys = getKeyColumnNames();
		if (keys.size() < 1)
		{
			log.config("No Keys for " + toString());
			return null;
		}

		//	One Key
		if (keys.size() == 1)
		{
			String keyColumn = keys.get(0);
			String stringID = ctx.getContext(windowNo, keyColumn);
			if (stringID == null || stringID.length() == 0)
			{
				log.config("@NotFound@ " + keyColumn + " - " + toString());
				//	New
				if (keyColumn.endsWith("_ID"))
					stringID = "0";
				else
					return null;
			}
			if (keyColumn.endsWith("_ID"))
			{
				int Record_ID = Integer.parseInt(stringID);
				if (Record_ID == 0)		//	valid for Role/User/Org/Client/System
				{
					PO po = table.getPO(ctx, Record_ID, null, false);
					return po;
				}
				PO po = table.getPO(ctx, Record_ID, null);
				return po;
			}
			else	//	KeyColumn not ID
			{
				StringBuffer where = new StringBuffer(keyColumn)
				.append("='").append(stringID).append("'");
				PO po = table.getPO(ctx, where.toString(), null);
				return po;
			}
		}
		else 	//	more then one key
		{
			StringBuffer where = new StringBuffer();
			for (int i = 0; i < keys.size(); i++)
			{
				String keyColumn = keys.get(i);
				String stringID = ctx.getContext(windowNo, keyColumn);
				if (stringID == null || stringID.length() == 0)
				{
					log.config("@NotFound@ " + keyColumn + " - " + toString());
					return null;
				}
				if (i > 0)
					where.append(" AND ");
				where.append(keyColumn);
				if (keyColumn.endsWith("_ID"))
					where.append("=").append(stringID);
				else
					where.append("='").append(stringID).append("'");
			}
			PO po = table.getPO(ctx, where.toString(), null);
			return po;
		}
	}	//	getPO

	/**
	 * 	Check ReadOnly
	 * 	@param ctx context
	 * 	@param context row context
	 *	@return null if not read only or error
	 */
	private String checkReadOnly(CContext ctx, int windowNo)
	{
		if (isReadOnly())
			return "@IsReadOnly@";

		//	Check Role
		int AD_Client_ID = ctx.getAD_Client_ID(windowNo);
		int AD_Org_ID = ctx.getAD_Org_ID(windowNo);
		int Record_ID = ctx.getContextAsInt(windowNo, getKeyColumnName());
		boolean createError = true;
		//if (!MRole.getDefault(ctx, false)
			//	.canUpdate(AD_Client_ID, AD_Org_ID, getAD_Table_ID(), Record_ID, createError))
		MRole role = MRole.get(ctx, ctx.getAD_Role_ID(), ctx.getAD_User_ID(), false);
		if (!role
				.canUpdate(AD_Client_ID, AD_Org_ID, getAD_Table_ID(), Record_ID, createError))
		{
			String errorMsg = "@CannotUpdate@";
			ValueNamePair error = CLogger.retrieveError();
			if (error != null)
				errorMsg =Msg.getMsg(ctx, error.getID(), error.getName());

			return errorMsg;
		}

		String logic = getReadOnlyLogic();
		if (logic == null || logic.length() == 0)
			return null;
		//	TODO: R/O Logic

		return null;
	}	//	checkReadOnly

	/**
	 * 	Check Mandatory
	 *	@param ctx context
	 *	@param context row context
	 *	@return column names with missing values or null
	 */
	private String checkMandatory (CContext ctx, int windowNo)
	{
		//  see also => ProcessParameter.saveParameter
		StringBuffer sb = new StringBuffer();

		//	Check all columns
		int size = p_vos.size();
		for (int i = 0; i < size; i++)
		{
			UIField field = (UIField)p_vos.get (i);
			if (field.isMandatory(ctx, windowNo))	//  check context
			{
				Object data = ctx.getContext(windowNo, field.getColumnName());
				if (data == null 
						|| data.toString().length() == 0
						|| Null.NULLString.equals(data)
				)
				{
					//	field.setInserting (true);  //  set editable otherwise deadlock
					field.setError(true);
					if (sb.length() > 0)
						sb.append(", ");
					sb.append("@").append(field.getName()).append("@");
				}
				else
					field.setError(false);
			}
		}

		if (sb.length() == 0)
			return null;
		sb.insert(0, "@FillMandatory@: ");
		return sb.toString();
	}	//	checkMandatory

	/**
	 * 	Field Changed Consequences
	 * 	@param ctx context
	 *	@param field Field
	 *	@param oldValue old field value
	 *	@param newValue new Field value
	 *	@param context current row context
	 *	@return Field Change VO
	 */
	public ChangeVO fieldChanged (CContext ctx, int windowNo, UIField field, 
			String oldValue, String newValue)
	{
		ChangeVO retValue = new ChangeVO();
		/** Changed Values
		 * 	Map<ColumnName,ColumnValue>					*/
		retValue.changedFields = new HashMap<String,String>();
		/** Changed Drop Down Lists		
		 * 	Map<FieldName,ArrayList<NewValues>>			*/
		retValue.changedDropDowns = new HashMap<String,ArrayList<NamePair>>();
		//
		String columnName = field.getColumnName();

		//	New Value
		retValue.newConfirmedFieldValue = field.validateValueAsString(oldValue, newValue);
		//	TODO: set error message if value is not confirmed
		//	retValue.addError(message)
		retValue.addChangedValue(columnName, retValue.newConfirmedFieldValue);
		ctx.setContext(windowNo, columnName, retValue.newConfirmedFieldValue);
		//
		if (field.isImpactsValue())
		{
			//	Data Value Changes
			ArrayList<String> impactedColumnNames = field.getImpactsValue();
			for (int i = 0; i < impactedColumnNames.size(); i++)
			{
				String impactedColumnName = impactedColumnNames.get(i);
				UIField impactedField = getField(impactedColumnName);
				if (impactedField == null)
				{
					log.warning(columnName + ": Impact Not found - " + impactedColumnName);
					continue;
				}
				String oldImpactedValue = ctx.getContext(windowNo, impactedColumnName);
				String newImpactedValue = oldImpactedValue;
				if (impactedField.isLookup())
				{
					Lookup impactedLookup = impactedField.getLookup();
					//	Update the Locator info with new value
					if (impactedLookup instanceof MLocatorLookup)
					{
						MLocatorLookup locLookup = (MLocatorLookup)impactedLookup;
						int valueAsInt = 0;
						if (newValue != null)
						{
							try
							{
								valueAsInt = Integer.parseInt(newValue);
							}
							catch (Exception e)
							{
								log.warning("Cannot Parse " + columnName + "=" + newValue);
							}
						}
						if (columnName.equals("M_Warehouse_ID"))
							locLookup.setOnly_Warehouse_ID(valueAsInt);
						if (columnName.equals("M_Product_ID"))
							locLookup.setOnly_Product_ID( valueAsInt );
						locLookup.setOnly_Outgoing(ctx.isSOTrx(windowNo));
						locLookup.refresh();
						int M_Locator_ID = ctx.getContextAsInt(windowNo, impactedColumnName);
						if (!locLookup.isValid(new Integer(M_Locator_ID)))
						{
							newImpactedValue = null;
							retValue.addChangedValue(impactedColumnName, newImpactedValue);
							ctx.setContext(windowNo, impactedColumnName, newImpactedValue);
						}
					}
					else
					{
						//  if the lookup is dynamic (i.e. contains this columnName as variable)
						//	if (mLookup.getValidation().indexOf("@"+columnName+"@") != -1)
						ArrayList<NamePair> data = impactedField.getLookupData(ctx, windowNo, false);
						retValue.addChangedDropDown(impactedColumnName, data);
						newImpactedValue = null;
						retValue.addChangedValue(impactedColumnName, newImpactedValue);
						ctx.setContext(windowNo, impactedColumnName, newImpactedValue);
					}
				}
				else
					log.warning(columnName + ": Impact Not Lookup - " + impactedColumnName);

				//	Cascading Dependencies
				if (impactedField.isImpactsValue())
				{
					UIField impactField = getField(impactedColumnName);
					ChangeVO cascadingChange = fieldChanged(ctx, windowNo, impactField, 
							oldImpactedValue, newImpactedValue);
					retValue.addAll(cascadingChange);
				}
			}	//	for all impacted fields

			ChangeVO calloutChange = processCallout (ctx, windowNo, field, 
					oldValue, newValue);
			retValue.addAll(calloutChange);
		}
		//
		return retValue.cleanup();
	}	//	fieldChanged


	/**************************************************************************
	 *  Process Callout(s).
	 *  <p>
	 *  The Callout is in the string of
	 *  "class.method;class.method;"
	 * If there is no class name, i.e. only a method name, the class is regarded
	 * as CalloutSystem.
	 * The class needs to comply with the Interface Callout.
	 *
	 * For a limited time, the old notation of Sx_matheod / Ux_menthod is maintained.
	 *
	 * @see org.compiere.model.Callout
	 */
	private ChangeVO processCallout (CContext ctx, int windowNo, UIField field, 
			String oldValue, String newValue)
	{
		String callout = field.getCallout();
		if (!field.isCallout() && callout.length() == 0)
			return null;
		//
		if (ctx.isProcessed(windowNo)) {		//	only active records
			log.warning("record processed. no callout for "+field.getColumnName());
			return null;		//	"DocProcessed";
		}
		ChangeVO retValue = null;
		if (field.isCallout())
		{
			log.fine(field.getColumnName() + "=" + newValue + " - old=" + oldValue);
			retValue = processCalloutDirect(ctx, windowNo, field, oldValue, newValue);
			if (callout.length() == 0)		//	nothing else
				return retValue;
		}

		//	External Callout
		if (true)	//	need to change reference
		{
			log.info(field.getColumnName() + "=" + newValue
					+ " (" + callout + " IGNORED)");
			return retValue;
		}
		log.fine(field.getColumnName() + "=" + newValue
				+ " (" + callout + ") - old=" + oldValue);

		StringTokenizer st = new StringTokenizer(callout, ";,", false);
		while (st.hasMoreTokens())      //  for each callout
		{
			String cmd = st.nextToken().trim();
			ChangeVO change = processCalloutExternal (ctx, windowNo, field, 
					oldValue, newValue, cmd);
			if (retValue == null)
				retValue = change;
			else
				retValue.addAll(change);
			if (retValue.hasError())
				break;
		}   //  for each callout
		return retValue;
	}	//	processCallout

	/**
	 * 	Process individual external Callout
	 *	@param ctx context
	 *	@param windowNo window no
	 *	@param field field
	 *	@param oldValue old value
	 *	@param newValue new value
	 *	@param cmd command for individual callout (class.method) 
	 *	@return change vo
	 */
	private ChangeVO processCalloutExternal (CContext ctx, int windowNo, UIField field, 
			String oldValue, String newValue, String cmd)
	{
		ChangeVO retValue = new ChangeVO();
		CalloutInterface call = null;
		String method = null;
		int methodStart = cmd.lastIndexOf(".");
		try
		{
			if (methodStart != -1)      //  no class
			{
				Class<?> cClass = Class.forName(cmd.substring(0,methodStart));
				call = (CalloutInterface)cClass.newInstance();
				method = cmd.substring(methodStart+1);
			}
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "Class: " + cmd, e);
			retValue.addError("Callout Invalid: " + cmd + " (" + e.toString() + ")");
			return retValue;
		}
		if (call == null || method == null || method.length() == 0)
		{
			retValue.addError("No Callout: " + cmd);
			return retValue;
		}
		PO po = getPO(ctx, windowNo, false);
		if (po == null)
		{
			retValue.addError("No PO: (" + getKeyColumnName() + ") - " + cmd);
			return retValue;
		}

		try
		{	
			retValue = call.start(ctx, windowNo, po, 
					field, oldValue, newValue, method);
		}
		catch (Exception e)
		{
			log.log(Level.WARNING, "Start: " + cmd, e);
			retValue.addError("Callout Error: " + e.toString());
		}
		return retValue;
	}	//	processCalloutExternal


	/**
	 * 	Preferred Callout.
	 *	@param ctx context
	 *	@param windowNo
	 *	@param field
	 *	@param oldValue
	 *	@param newValue
	 *	@return change
	 */
	public ChangeVO processCalloutDirect (CContext ctx, int windowNo, UIField field, 
			String oldValue, String newValue)
	{
		ChangeVO change = new ChangeVO();
		MTable table = MTable.get(ctx, getAD_Table_ID());
		Map<String,String> context = ctx.getMap(windowNo);

		PO po = null;
		try
		{
			po = table.getPO(ctx, context);
			po.set_ChangeVO(change);
		}
		catch (Exception e1)
		{
			change.addError("Cannot create PO - " + e1.getLocalizedMessage());
			log.log(Level.WARNING, "Cannot create PO", e1);
			return change;
		}

		Method method = null;
		String methodName = "set" + field.getColumnName();
		try
		{
			Class<?>[] params = new Class[]{String.class, String.class, Integer.TYPE };
			method = po.getClass().getMethod(methodName, params);
		}
		catch (Exception e2)
		{
			String msg = po.getClass() + ": No Method " + methodName;
			change.addError(msg + " - " + e2.getLocalizedMessage());
			log.log(Level.WARNING, msg, e2);
			return change;
		}

		try
		{
			Object[] args = new Object[]{oldValue, newValue, windowNo}; 
			method.invoke(po, args);	//	void method
		}
		catch (Exception e3)
		{
			String msg;
			if(e3.getCause() != null)
				msg = e3.getCause().getLocalizedMessage();
			else
				msg = e3.getLocalizedMessage();
			if (msg == null || msg.length() == 0)
				msg = e3.toString();
			change.addError(po.getClass() + "." + methodName + ": " + msg);
			log.log(Level.WARNING, po.getClass() + "." + methodName, e3);
		}
		return change;
	}	//	processCalloutDirect

	/**
	 * 	Whether this tab can be exported by the present role.
	 * 	@param ctx context 
	 *	@return true if role can export
	 */
	public boolean isCanExport(Ctx ctx)
	{
		MRole role = MRole.get(ctx, ctx.getAD_Role_ID(), ctx.getAD_User_ID(), false);
		return role.isCanExport(getAD_Table_ID());
	}	//	isCanExport


	/**
	 * 	Whether this tab can be reported by the present role.
	 * 	@param ctx context 
	 *	@return true if role can export
	 */
	public boolean isCanReport(Ctx ctx)
	{
		MRole role = MRole.get(ctx, ctx.getAD_Role_ID(), ctx.getAD_User_ID(), false);
		return role.isCanReport(getAD_Table_ID());
	}	//	isCanReport




	/**
	 * 	String Representation
	 *	@return info
	 */
	public String toString()
	{
		StringBuffer sb = new StringBuffer ("UITab[")
		.append (getAD_Tab_ID())
		.append ("-").append (getName());
		if (p_vos != null)
			sb.append (";#Fields=").append(p_vos.size());
		sb.append ("]");
		return sb.toString();
	}	//	toString


	public void refreshDropDowns(ChangeVO change, int windowNo, CContext newCtx) {
		//now make a new hashamp of the current state
		if(change.changedContext != null)
			newCtx.setContext(windowNo,change.changedContext);
		if(change.changedFields != null)
			newCtx.setContext(windowNo,change.changedFields);
		if(change.rowData != null) {
			log.info("found row data, now merge");
			int i=0;
			for(UIField f: getFields()) {
				newCtx.setContext(windowNo, f.getColumnName(), change.rowData[i]);
				i++;
			}
		}

		//ctx.addWindow(windowNo, context);
		//now requery those dependent dropdowns
		for(UIField f: getFields()) {
			String colName = f.getColumnName();
			//TODO now i get values for all lookups, whereas i only need value for info window
			if(f.isLookup() && f.isDependentValue()) {
				String val = (String)change.changedFields.get(colName);
				if(val != null) {
					if(change.changedDropDowns == null)
						change.changedDropDowns = new HashMap();
					ArrayList arry = (ArrayList) change.changedDropDowns
					.get(colName);
					ArrayList<NamePair> data = f.getLookupData(newCtx, windowNo, false);
					if (arry == null) {
						//if it is a lookup, and depends on others for value, requery
						//arry.add(new ValueNamePair(id, f.getLookupDisplay(m_context, windowNo, id)));
						change.changedDropDowns.put(colName, data);
						//}
					} else {
						log.warning("field" + colName
								+ " already has changedDropDowns, merge it. Before merge:" + arry);
						for(NamePair d: data) {
							if(NamePair.indexOfKey(arry, d.getID()) == -1)
								arry.add(d);
						}
						log.warning("After merge:"+arry);
					}

				}
			}
		}


	}

	public int getRecord_ID(String[] row) {
		int keyColumnPos = getKeyColumnPos();
		if(keyColumnPos == -1)
			return -1;
		return Integer.parseInt(row[keyColumnPos]);

	}
	public int getKeyColumnPos() {
		if (m_keyColumns == null)
			createColumnLists();
		return m_keyColumnPos;
	}
}	//	UITab

