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

import java.util.*;

import org.compiere.common.*;
import org.compiere.controller.*;
import org.compiere.model.*;
import org.compiere.process.*;
import org.compiere.util.*;

/**
 * GWT Server Implementation. You maintain one instance per User
 * 
 * @author Jorg Janke
 * @version $Id: GwtServer.java,v 1.1 2008/01/16 07:40:54 jrmt Exp $
 */
public class GwtServer {
	/**
	 * Gwt Server
	 */
	public GwtServer() {
		if (m_context == null)
			m_context = new CContext();
	} // GwtServer

	/** Context */
	private CContext m_context = null;
	/** Login */
	private Login m_login = null;
	/** Locale */
	private Locale m_loc = null;

	/** Window Cache */
	private CCache<Integer, UIWindow> m_windows = new CCache<Integer, UIWindow>(
			"AD_Window", 20, 0);
	/** Tab Cache */
	private CCache<Integer, UITab> m_tabs = new CCache<Integer, UITab>(
			"AD_Tab", 20, 0);
	/** Field Cache */
	private CCache<Integer, UIField> m_fields = new CCache<Integer, UIField>(
			"AD_Field", 200, 0);
	/** Tab Results */
	private HashMap<Integer, ArrayList<String[]>> m_results = new HashMap<Integer, ArrayList<String[]>>();

	/** Logger */
	private static CLogger log = CLogger.getCLogger(GwtServer.class);

	/**
	 * Get Login
	 * 
	 * @return login
	 */
	public Login getLogin() {
		if (m_login == null)
			m_login = new Login(m_context);
		return m_login;
	} // getLogin

	/**
	 * Returns the context associated with this GwtServer
	 * 
	 * @return context
	 */
	public CContext getContext() {
		return m_context;
	}

	/**
	 * Set Locale
	 * 
	 * @param loc
	 *            locale (from login)
	 */
	public void setLocale(Locale loc) {
		m_loc = loc;
	} // setLocale

	/**
	 * Get Locale
	 * 
	 * @return Locale
	 */
	public Locale getLocale() {
		if (m_loc == null)
			return Locale.US;
		return m_loc;
	} // getLocale

	/**
	 * Get Menu
	 * 
	 * @return menu as array list
	 */
	public ArrayList<CTreeNode> getMenuTree() {
		int AD_Tree_ID = getTreeID();
		log.fine("AD_Tree_ID=" + AD_Tree_ID + " - "
				+ Env.getAD_Language(m_context));
		return getMenuTree(AD_Tree_ID, false);
	} // getMenuTree

	/**
	 * Get Tree ID for role
	 * 
	 * @return AD_Tree_ID as int
	 */
	private int getTreeID() {
		int AD_Role_ID = m_context.getAD_Role_ID();

		// Load Menu Structure ----------------------
		int AD_Tree_ID = DB
		.getSQLValue(
				null,
				"SELECT COALESCE(r.AD_Tree_Menu_ID, ci.AD_Tree_Menu_ID)"
				+ "FROM AD_ClientInfo ci"
				+ " INNER JOIN AD_Role r ON (ci.AD_Client_ID=r.AD_Client_ID) "
				+ "WHERE AD_Role_ID=?", AD_Role_ID);
		if (AD_Tree_ID <= 0)
			AD_Tree_ID = 10; // Menu

		return AD_Tree_ID;
	} // getTreeID

	/**
	 * Get Menu favorites for a user
	 * 
	 * @return menu as array list
	 */
	public ArrayList<CTreeNode> getMenuFavorites() {
		MUser user = MUser.get(getContext());
		int AD_Tree_ID = user.getAD_Tree_MenuFavorite_ID();
		if (AD_Tree_ID == 0) // favorites has not yet been created
			return new ArrayList<CTreeNode>();
		return getMenuTree(AD_Tree_ID, false);
	}// get favorites menu

	/**
	 * Get Menu tree that directly enter "create new" mode
	 * 
	 * @return menu as array list
	 */
	public ArrayList<CTreeNode> getMenuCreateNew() {
		MUser user = MUser.get(getContext());
		int AD_Tree_ID = user.getAD_Tree_MenuNew_ID();
		if (AD_Tree_ID == 0) // create new has not yet been created
			return new ArrayList<CTreeNode>();
		return getMenuTree(AD_Tree_ID, false);
	}// getMenuCreateNew

	/**
	 * Get a menu tree representation based on a AD_Tree_ID
	 * 
	 * @param int
	 *            AD_Tree_ID (A tree based on AD_Menu)
	 * @return menu as array list
	 */
	private ArrayList<CTreeNode> getMenuTree(int AD_Tree_ID, boolean edit) {
		MTree tree = new MTree(m_context, AD_Tree_ID, edit, true, null); // Language
		// set
		// in
		// WLogin
		// Trim tree
		CTreeNode root = tree.getRoot();
		Enumeration<?> en = root.preorderEnumeration();
		while (en.hasMoreElements()) {
			CTreeNode nd = (CTreeNode) en.nextElement();
			if (nd.isTask() || nd.isWorkbench() || nd.isWorkFlow()
					|| nd.getNode_ID() == 383 // Reset Cache - kills the
					// server
			) {
				CTreeNode parent = (CTreeNode) nd.getParent();
				parent.remove(nd);
			}
		}
		tree.trimTree();
		en = root.preorderEnumeration();
		ArrayList<CTreeNode> retValue = new ArrayList<CTreeNode>();
		while (en.hasMoreElements()) {
			CTreeNode nd = (CTreeNode) en.nextElement();
			// Issue #420: removed menu entries for un-implemented forms
			if ((nd.getAD_Form_ID() == 116) 	   // Workflow Editor
					|| (nd.getAD_Form_ID() == 117) // Workflow Activities
					|| (nd.getAD_Form_ID() == 119) // Performance Indicators
					|| (nd.getAD_Form_ID() == 105) // Generate charges
					|| (nd.getAD_Form_ID() == 106) // Payment Print/Export
					|| (nd.getAD_Form_ID() == 102) // Initial Client Setup
					|| (nd.getAD_Window_ID() == 298) // Workflow Activities (all)													
					|| (nd.getAD_Window_ID() == 283) // Accounting Dimensions
					|| (nd.getAD_Window_ID() == 153) // Account Combination
					|| (nd.getAD_Window_ID() == 162) // Accounting Fact Details
					|| (nd.getAD_Window_ID() == 118) // Account Element
					|| (nd.getAD_Window_ID() == 255) // Accounting Fact Balances
					|| (nd.getAD_Workflow_ID() == 106) // Business Partner Setup
					|| (nd.getAD_Workflow_ID() == 104) // Initial Tenant Setup Review
					|| (nd.getAD_Workflow_ID() == 112) // Performance Measurement Setup
					|| (nd.getAD_Workflow_ID() == 113) // Request Setup
					|| (nd.getAD_Workflow_ID() == 110) // Tax Setup
					|| (nd.getAD_Workflow_ID() == 111) // Sales Setup
			) {
			} else
				retValue.add(nd);
		}
		return retValue;
	}

	/**
	 * Make Favorites add/remove persistent ("bar" in swing client)
	 * 
	 * @param add
	 *            true if add - otherwise remove
	 * @param Node_ID
	 *            Node ID
	 * @return true if updated
	 */
	public boolean updateFavorites(boolean add, int Node_ID) {
		/*
		 * Code logic now uses MUser to store favorites. TODO:
		 * VTreePanel.barDBupdate should be similarly updated or deprecated for
		 * Swing client.
		 */
		MUser user = MUser.get(getContext());

		return user.addUserMenuFavorite(Node_ID, 0);

	} // updateFavorites

	/**
	 * Update of user favorites for a user with specified ordering for favorites
	 * 
	 * @param menuIDs
	 *            List<Integer> ordered list of menuIDs to put in the tree
	 * @return true if updated
	 */

	public boolean updateFavorites(List<Integer> menuIDs) {
		MUser user = MUser.get(getContext());

		MTree menuTree = null;
		if ((menuTree = user.getUserFavoriteTree()) == null)
			return false;
		return updateUserTree(menuIDs, menuTree);
	}// updateFavorites

	/**
	 * Make create new add/remove persistent ("bar" in swing client)
	 * 
	 * @param add
	 *            true if add - otherwise remove
	 * @param Node_ID
	 *            Node ID
	 * @return true if updated
	 */
	public boolean updateCreateNew(boolean add, int Node_ID) {
		/*
		 * Code logic now uses MUser to store favorites. TODO:
		 * VTreePanel.barDBupdate should be similarly updated or deprecated for
		 * Swing client.
		 */
		MUser user = MUser.get(getContext());

		return user.addUserMenuNewFavorite(Node_ID, 0);

	} // updateCreateNew

	/*
	 * Update of user favorites for a user with specified ordering for favorites
	 * @param menuIDs List<Integer> ordered list of menuIDs to put in the tree
	 * @return true if updated
	 */

	public boolean updateCreateNew(List<Integer> menuIDs) {
		MUser user = MUser.get(getContext());

		MTree menuTree = null;
		if ((menuTree = user.getUserNewFavoriteTree()) == null)
			return false;

		return updateUserTree(menuIDs, menuTree);
	}// updateCreateNew

	/*
	 * Update of user tree for ordered menu nodes (favorites, create new list)
	 * favorites @param menuIDs List<Integer> ordered list of menuIDs to put in
	 * the tree @param menuTree MTree the tree to be reordered @return true if
	 * updated
	 */
	private boolean updateUserTree(List<Integer> menuIDs, MTree menuTree) {

		CTreeNode root = menuTree.getRoot();
		if (root != null) {
			Enumeration<?> nodes = root.preorderEnumeration();
			while (nodes.hasMoreElements()) {
				CTreeNode nd = (CTreeNode) nodes.nextElement();
				if (!menuIDs.contains(nd.getNode_ID())) {
					MTreeNodeMM node = null;
					if ((node = MTreeNodeMM.get(menuTree, nd.getNode_ID())) != null) {

						if (!node.delete(true))
							return false;
					}
				}
			}
		}

		int seq = 0;
		for (int id : menuIDs) {
			MTreeNodeMM node = null;
			if (((node = MTreeNodeMM.get(menuTree, id)) == null)) {
				node = new MTreeNodeMM(menuTree, id);
			}
			node.setSeqNo(++seq);
			if (!node.save())
				return false;
		}
		return true;
	}

	/**
	 * Get Number of open Requests
	 * 
	 * @return number of requests
	 */
	public int getRequests() {
		return GwtServerUtil.getRequests(m_context);
	} // getRequests

	/**
	 * Get number of open Notes
	 * 
	 * @return Number of notes
	 */
	public int getNotes() {
		return GwtServerUtil.getNotes(m_context);
	} // getNotes

	/***************************************************************************
	 * Get Window in default context based on Role
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Window_ID
	 *            window
	 * @param AD_Menu_ID
	 *            menu
	 * @return WindowVO or null
	 */
	public UIWindow getWindow(int windowNo, int AD_Window_ID, int AD_Menu_ID) 
	{
		UIWindowVOFactory winFactory = new UIWindowVOFactory();
		UIWindowVO winVO = winFactory.get(m_context, AD_Window_ID, AD_Menu_ID);
		if (winVO == null) 
		{
			log.config("No Window - AD_Window_ID=" + AD_Window_ID
					+ ",AD_Menu_ID=" + AD_Menu_ID);
			return null;
		}
		UIWindow win = new UIWindow(winVO, m_context);
		AD_Window_ID = win.getAD_Window_ID();
		m_windows.put(new Integer(AD_Window_ID), win);
		int AD_UserDef_Win_ID = win.getAD_UserDef_Win_ID();
		//
		UIFieldVOFactory fieldFactory = new UIFieldVOFactory();
		win.setFields(fieldFactory.getAll(m_context, AD_Window_ID,
				AD_UserDef_Win_ID));
		//
		UITabVOFactory tabFactory = new UITabVOFactory();
		win.setTabVOs(tabFactory.getAll(m_context, AD_Window_ID,
				AD_UserDef_Win_ID), windowNo);
		//
		log.fine(win.toString());
		return win;
	}	// getWindowVO

	/**
	 * Get Window direct (internal)
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Window_ID
	 *            window
	 * @param AD_Menu_ID
	 *            menu
	 * @return WindowVO or null
	 */
	protected UIWindow getWindow(int windowNo, int AD_Window_ID) {
		UIWindow win = m_windows.get(new Integer(AD_Window_ID));
		if (win == null)
			win = getWindow(windowNo, AD_Window_ID, 0);
		return win;
	} // getWindowVO

	/**
	 * Get Tab with ID
	 * 
	 * @param AD_Tab_ID
	 * @return tab or null
	 */
	public UITab getTab(int AD_Tab_ID) {
		Integer tabKey = new Integer(AD_Tab_ID);
		UITab tab = m_tabs.get(tabKey);
		if (tab == null) {
			fillTabsFields();
			tab = m_tabs.get(tabKey);
		} // find in window
		return tab;
	} // getTab

	/**
	 * Get Field
	 * 
	 * @param AD_Field_ID
	 *            id
	 * @param windowNo
	 *            relative windowNo
	 * @return field or null
	 */
	public UIField getField(int AD_Field_ID, int windowNo) {
		Integer key = new Integer(AD_Field_ID);
		UIField field = m_fields.get(key);
		if (field == null) {
			fillTabsFields();
			field = m_fields.get(key);
			if (field == null) {
				UIFieldVOFactory fieldFactory = new UIFieldVOFactory();
				UIFieldVO vo = fieldFactory.get(m_context, AD_Field_ID);
				// m_context.setSOTrx(windowNo, isSOTrx);
				if (vo != null) {
					field = new UIField(vo);
					field.initialize(m_context, windowNo);
					log.warning("Loaded directly: " + field); // SOTrx may not
					// be correct
					m_fields.put(key, field); // save in cache
				}
			} // create new
		} // find in window
		return field;
	} // getField

	/**
	 * Fill Tab and Field arrays
	 */
	private void fillTabsFields() {
		Iterator<UIWindow> it = m_windows.values().iterator();
		while (it.hasNext()) {
			UIWindow win = it.next();
			ArrayList<UITab> tabs = win.getTabs();
			for (int j = 0; j < tabs.size(); j++) {
				UITab winTab = tabs.get(j);
				Integer tabKey = new Integer(winTab.getAD_Tab_ID());
				m_tabs.put(tabKey, winTab);
				//
				ArrayList<UIField> fields = winTab.getFields();
				for (int k = 0; k < fields.size(); k++) {
					UIField field = fields.get(k);
					Integer fieldKey = new Integer(field.getAD_Field_ID());
					m_fields.put(fieldKey, field);

				}
			}
		}
	} // fillTabsFields

	/**
	 * Evaluate Query for Window/Tab
	 * 
	 * @param AD_Tab_ID
	 *            tab
	 * @param queryVO
	 *            optional query
	 * @param context
	 *            record context for link columns and other variables
	 * @return number of records or -1 if error
	 */
	public int evaluateQuery(int AD_Tab_ID, QueryVO queryVO,
			HashMap<String, String> context) {
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return -1;
		}
		return tab.evaluateQuery(queryVO, context, m_context);
	} // setQuery

	/**
	 * Execute Query for Tab
	 * 
	 * @param AD_Tab_ID
	 *            tab
	 * @param queryVO
	 *            optional query
	 * @param context
	 *            record context for link columns and other variables
	 * @param queryResultID
	 *            stored query identifier provided by client
	 * @return number of records or -1 if error
	 */
	public int executeQuery(int AD_Tab_ID, QueryVO queryVO,
			HashMap<String, String> context, int queryResultID) {
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return -1;
		}
		ArrayList<String[]> result = tab.executeQueryString(queryVO, context,
				m_context);
		if (result == null) {
			log.config("Not Result for AD_Tab_ID=" + AD_Tab_ID);
			return -1;
		}

		MRole role = MRole.get(m_context, m_context.getAD_Role_ID(), m_context.getAD_User_ID(), false);
		// return -1 to indicate query exceeds
		if (role.isQueryMax(result.size())) {
			m_results.put(queryResultID, new ArrayList<String[]>());
			return -1;
		}

		m_results.put(queryResultID, result);
		return result.size();
	} // executeQuery

	/**
	 * Retrieve results for Tab. If the from/to range does not exist, it returns
	 * existing rows
	 * 
	 * @param queryResultID
	 *            stored query identifier provided by client
	 * @param fromRow
	 *            from row first is 0
	 * @param toRow
	 *            to row including
	 * @return array of rows of array of field values or null if error. You get
	 *         the columnNames via String[] columns = uiTab.getColumnNames();
	 */
	public String[][] getResults(int queryResultID, int fromRow, int noRows) {
		if (noRows < 0) {
			log.config("Invalid: fromRow=" + fromRow + ",noRows" + noRows);
			return null;
		} else if (noRows == 0)
			return new String[][] {};
		//
		ArrayList<String[]> resultAll = m_results.get(queryResultID);
		if (resultAll == null) {
			log.config("No Results for queryResultID=" + queryResultID);
			return null;
		}
		if (resultAll.size() < fromRow) {
			log.config("Insufficient Results for queryResultID="
					+ queryResultID + ", Length=" + resultAll.size()
					+ ", fromRow=" + fromRow);
			return null;
		}
		// copy
		if (resultAll.size() < noRows) {
			log.config("Insufficient Rows for queryResultID=" + queryResultID
					+ ", Length=" + resultAll.size() + ", fromRow=" + fromRow
					+ ", noRows=" + noRows);
			noRows = resultAll.size();
		}
		String[][] result = new String[noRows][];
		for (int i = 0; i < noRows; i++) {
			int index = i + fromRow;
			if (index >= resultAll.size())
				break;
			
			
			result[i] = resultAll.get(index);
		}
		return result;
	} // getResult

	/**
	 * Execute Query for Tab. If the from/to range does not exist, it returns
	 * existing rows
	 * 
	 * @param queryResultID
	 *            stored query identifier provided by client
	 * @param row
	 *            row number first is 0
	 * @return array of rows of array of field values or null if error. You get
	 *         the columnNames via String[] columns = uiTab.getColumnNames();
	 */
	public String[] requery(int queryResultID, int row) {
		// TODO requery
		String[][] results = getResults(queryResultID, row, 1);
		return results[0];
	} // requery

	/**
	 * Sort Results
	 * 
	 * @param queryResultID
	 *            query
	 * @param relColumnNo
	 *            relative column (Starting with 0)
	 * @param ascending
	 *            sort ascending to descending
	 * @return error message
	 */
	public String sort(int queryResultID, int relColumnNo, boolean ascending) {
		return "Not implemented";
	} // sort

	/**
	 * Release All Results
	 */
	public void releaseResults() {
		m_results = new HashMap<Integer, ArrayList<String[]>>();
	} // releaseResults

	/**
	 * Release Results
	 * 
	 * @param queryResultID
	 *            stored query identifier provided by client
	 */
	public void releaseResults(ArrayList<Integer> resultIDs) {
		System.out.println("before cached id:" + m_results.keySet());
		for (int queryResultID : resultIDs)
			m_results.remove(queryResultID);
		System.out.println("after cached id:" + m_results.keySet());
	} // releaseResults

	/**
	 * Create new Row with Default values. The new Row is not saved in Results
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Tab_ID
	 *            tab
	 * @param context
	 *            record context for parent columns and other variables
	 * @return array of field values or null if error. You get the columnNames
	 *         via String[] columns = uiTab.getColumnNames();
	 */
	public ChangeVO newRow(int windowNo, int AD_Tab_ID,
			Map<String, String> context) {
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return null;
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		ctx.setIsSOTrx(windowNo, tab.isSOTrx());
		ChangeVO change = tab.newRow(ctx, windowNo);

		/**
		 * Very likely not needed if (change.changedDropDowns == null)
		 * change.changedDropDowns = new HashMap<String,ArrayList<NamePair>>();
		 * 
		 * for(UIField f:tab.getFields()) { if (f.isDependentValue())
		 * change.changedDropDowns.put(f.getColumnName(),
		 * getLookupValues(windowNo, f.getAD_Field_ID(), change.changedFields)); }
		 */

		return change;
	} // newRow

	/**
	 * Refresh current row of Tab
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Tab_ID
	 *            tab
	 * @param relRowNo
	 *            relative row number in results
	 * @param context
	 *            current (relevant) context of new row
	 * @return error message or null
	 */
	public ChangeVO refreshRow(int windowNo, int AD_Tab_ID, int queryResultID,
			int relRowNo, Map<String, String> context) {
		if (context == null || context.size() == 0)
			return new ChangeVO(true, "No Context");
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return new ChangeVO(true, "@NotFound@ @AD_Tab_ID@=" + AD_Tab_ID);
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		ChangeVO retValue = tab.refreshRow(ctx, windowNo);
		if (retValue.hasError())
			return retValue;

		// Update Results
		ArrayList<String[]> data = m_results.get(queryResultID);
		if (data == null)
			retValue.addError("Data Not Found");
		else {
			String[] dataRow = retValue.rowData.clone();
			data.set(relRowNo, dataRow);
			
			//now change rowData to remove password
			int j=0;
			for(UIField field: tab.getFields()) {
				//return an empty string for passwords etc
				if(field.isEncryptedField() || field.isEncryptedColumn()|| "Password".equals(field.getColumnName()))
					retValue.rowData[j] = "";
				j++;
			}
		}
		return retValue;
	} // refreshRow

	
	public ChangeVO updateRow(int windowNo, int AD_Tab_ID, int queryResultID,
			int relRowNo, Map<String, String> context, boolean force) {
		if (context == null || context.size() == 0)
			return new ChangeVO(true, "No Context");

		ArrayList<String[]> data = m_results.get(queryResultID);
		if (data == null)
			return new ChangeVO(true, "Cached Data Not Found");

		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return new ChangeVO(true, Msg.translate(m_context,
					"@NotFound@ @AD_Tab_ID@=" + AD_Tab_ID));
		}

		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		ChangeVO retValue;
		if(force)
			retValue = tab.saveRow(ctx, windowNo, false, null);
		else
		retValue = tab.saveRow(ctx, windowNo, false, data.get(relRowNo));
		if (retValue.hasError())
			return retValue;

		// Update Results
		String[] dataRow = retValue.rowData.clone();
		data.set(relRowNo, dataRow);

		
		//now change rowData to remove password
		int j=0;
		for(UIField field: tab.getFields()) {
			//return an empty string for passwords etc
			if(field.isEncryptedField() || field.isEncryptedColumn()|| "Password".equals(field.getColumnName()))
				retValue.rowData[j] = "";
			j++;
		}

		return retValue;
		
	}
	/**
	 * Save (Update existing) Row of Tab
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Tab_ID
	 *            tab
	 * @param relRowNo
	 *            relative row number in results
	 * @param context
	 *            current (relevant) context of new row
	 * @return error message or null
	 */
	public ChangeVO updateRow(int windowNo, int AD_Tab_ID, int queryResultID,
			int relRowNo, Map<String, String> context) {
		return updateRow(windowNo, AD_Tab_ID, queryResultID, relRowNo, context, false);
	} // updateRow

	/**
	 * Save (Insert new) Row of Tab
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Tab_ID
	 *            tab
	 * @param curRow
	 *            insert after relative row number in results
	 * @param context
	 *            current (relevant) context of new row
	 * @return error message or null
	 */
	public ChangeVO insertRow(int windowNo, int AD_Tab_ID, int queryResultID,
			int curRow, Map<String, String> context) {
		if (context == null || context.size() == 0)
			return new ChangeVO(true, "No Context");
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return new ChangeVO(true, "@NotFound@ @AD_Tab_ID@=" + AD_Tab_ID);
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		ChangeVO retValue = tab.saveRow(ctx, windowNo, true);
		if (retValue.hasError())
			return retValue;

		// Update Results
		ArrayList<String[]> data = m_results.get(queryResultID);
		if (data == null)
			retValue.addError("Data Not Found");
		else {
			String[] dataRow = retValue.rowData;
			if (curRow >= data.size())
				data.add(dataRow);
			else
				data.add(curRow, dataRow);
		}

		return retValue;
	} // insertRow

	/**
	 * Delete existing Row
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Tab_ID
	 *            tab
	 * @param relRowNo
	 *            relative row number in results
	 * @return error message or null
	 */
	public ChangeVO deleteRow(int windowNo, int AD_Tab_ID, int queryResultID,
			int relRowNo) {
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return new ChangeVO(true, "@NotFound@ @AD_Tab_ID@=" + AD_Tab_ID);
		}
		ArrayList<String[]> data = m_results.get(queryResultID);
		if (data == null)
			return new ChangeVO(true, "Data Not Found");
		String[] rowData = data.get(relRowNo);

		// Copy Data into Context
		Map<String, String> context = new HashMap<String, String>();
		String[] columns = tab.getColumnNames();
		for (int i = 0; i < columns.length; i++) {
			String column = columns[i];
			context.put(column, rowData[i]);
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		ChangeVO retValue = tab.deleteRow(ctx, windowNo);
		if (retValue.hasError())
			return retValue;

		// Update Results
		data.remove(relRowNo);
		return retValue;
	} // deleteRow

	/**
	 * Field Changed
	 * 
	 * @param windowNo
	 *            relative window
	 * @param AD_Field_ID
	 *            field
	 * @param AD_Tab_ID
	 *            tab
	 * @param oldValue
	 *            old field value
	 * @param newValue
	 *            new field value
	 * @param context
	 *            record context
	 * @return Field Change VO
	 */
	public ChangeVO fieldChanged(int windowNo, int AD_Field_ID, int AD_Tab_ID,
			String oldValue, String newValue, Map<String, String> context) {
		// Same Values
		if (oldValue == null || oldValue.equals(Null.NULLString))
			oldValue = "";
		if (newValue == null || newValue.equals(Null.NULLString))
			newValue = "";
		if (oldValue.equals(newValue))
			return null;
		//
		UITab tab = getTab(AD_Tab_ID);
		if (tab == null) {
			log.config("Not found AD_Tab_ID=" + AD_Tab_ID);
			return null;
		}
		UIField field = getField(AD_Field_ID, windowNo);
		if (field == null) {
			log.warning("Cannot find AD_Field_ID=" + AD_Field_ID);
			return null;
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);

		ChangeVO change = tab.fieldChanged(ctx, windowNo, field, oldValue,
				newValue);

		ctx.setContext(windowNo, field.getColumnName(),
				change.newConfirmedFieldValue);
		tab.refreshDropDowns(change, windowNo, ctx);
		// refreshDropDowns(change, windowNo, context, tab);
		return change;
	} // fieldChanged

	/**
	 * Get Field Lookup Value Direct
	 * 
	 * @param windowNo
	 *            Window
	 * @param AD_Field_ID
	 * @param keyValues
	 *            array of id values
	 * @param cache
	 * @return list of display values
	 */
	public ArrayList<NamePair> getLookupValueDirect(int AD_Field_ID,
			ArrayList<String> keyValues, boolean cache) {
		int windowNo = 0; // No Context
		ArrayList<NamePair> displayValues = new ArrayList<NamePair>();
		UIField field = getField(AD_Field_ID, windowNo);
		if (field == null)
			log.warning("Cannot find AD_Field_ID=" + AD_Field_ID);
		//
		for (int i = 0; i < keyValues.size(); i++) {
			String key = keyValues.get(i);
			String value = null;
			if (field != null)
				value = field.getLookupDisplay(m_context, windowNo, key, cache);
			if (value == null) {
				if(key == null)
					value = "";
				else
					value = "<" + key + ">";
			}
			NamePair pp = new ValueNamePair(key, value);
			displayValues.add(pp);
		}
		return displayValues;
	} // getLookupValueDirect

	/**
	 * Get Lookup Data for Field in context
	 * 
	 * @param AD_Field_ID
	 *            field
	 * @param context
	 *            context
	 * @param refresh
	 *            requery
	 * @return lookup pair array
	 */
	public ArrayList<NamePair> getLookupData(int windowNo, int AD_Field_ID,
			Map<String, String> context, boolean refresh) {
		UIField field = getField(AD_Field_ID, windowNo);
		if (field == null) {
			log.warning("Cannot find AD_Field_ID=" + AD_Field_ID);
			return null;
		}
		CContext ctx = new CContext(m_context.entrySet());
		ctx.addWindow(windowNo, context);
		if (field.isLookup())
			return field.getLookupData(ctx, windowNo, refresh);
		else
			log.warning("No Lookup: " + field.getColumnName());
		return null;
	} // getLookupData

	/***************************************************************************
	 * Get All Lookup Data for fields w/o context dependency
	 * 
	 * @param AD_Tab_ID
	 *            tab
	 * @return map if FiledName and lookup pair array
	 * 
	 * public Map<String,NamePair[]> getLookupDataAll (int AD_Tab_ID) { return
	 * null; } // getLookuupDataAll /**
	 **************************************************************************/

	protected HashMap<Integer, ProcessInfo> processes = new HashMap<Integer, ProcessInfo>();

	public void setProcessInfo(int windowNO, ProcessInfo pi) {
		processes.put(new Integer(windowNO), pi);
	}

	public ProcessInfo getProcessInfo(int windowNO) {
		return processes.get(new Integer(windowNO));
	}

	private static int curZoomWindowNO = 0;

	public int getZoomWindowNO() {
		curZoomWindowNO += 100;
		return curZoomWindowNO;
	}

	public Boolean savePreferences(Map ctx) {
		CContext cContext = getContext();
		MUser user = MUser.get(cContext);

		MUserPreference preference = user.getPreference();

		String printerName = (String) ctx.get("PrinterName");
		if (printerName != null && printerName.trim().equalsIgnoreCase("")) {
			cContext.setPrinterName(printerName);
			preference.setPrinterName(printerName);
		}

		String autoCommit = (String) ctx.get("AutoCommit");
		if (autoCommit != null) {
			cContext.setAutoCommit(autoCommit.trim().equalsIgnoreCase("Y"));
			preference.setIsAutoCommit(autoCommit.trim().equalsIgnoreCase("Y"));
		}

		String showAdvanced = (String) ctx.get("#ShowAdvanced");

		if (showAdvanced != null) {
			cContext.setContext("#ShowAdvanced", showAdvanced);
			preference.setIsShowAdvanced(showAdvanced.trim().equalsIgnoreCase(
			"Y"));
		}

		String uiTheme = (String) ctx.get("#UITheme");
		if (uiTheme != null && !uiTheme.trim().equalsIgnoreCase("")) {

			cContext.setContext("#UITheme", uiTheme);
			preference.setUITheme(uiTheme);
		}

		return preference.save();
	}

	public Boolean deleteSavedSearch(int tab_ID, String savedSearchName) {
		CContext cContext = getContext();
		MUserQuery query = MUserQuery.getForUser(cContext, tab_ID, savedSearchName);
		if (query != null)
			if (query.deleteLines()) {
				if (query.delete(true)) {
					return true;
				}
			}
		return false;
	}// deleteSavedsearch

} // GwtServer
