/******************************************************************************
 * Product: Adempiere ERP & CRM Smart Business Solution                       *
 * Copyright (C) 1999-2006 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., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA        *
 * or via info@compiere.org or http://www.compiere.org/license.html           *
 *****************************************************************************/
package org.compiere.model;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.logging.Level;

import org.compiere.util.CCache;
import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;

/**
 *  Persistent Object Info.
 *  Provides structural information.
 *
 *  @author Jorg Janke
 *  @version $Id: POInfo.java,v 1.2 2006/07/30 00:58:37 jjanke Exp $
 *  @author Victor Perez, e-Evolution SC
 *			<li>[ 2195894 ] Improve performance in PO engine
 *			<li>https://sourceforge.net/p/adempiere/feature-requests/555/
 */
public class POInfo implements Serializable
{
	/**
	 * generated serial id
	 */
	private static final long serialVersionUID = -6346988499971159874L;

	/**
	 *  POInfo Factory Method
	 *  @param ctx context
	 *  @param AD_Table_ID AD_Table_ID
	 *  @return POInfo
	 */
	public static POInfo getPOInfo (Properties ctx, int AD_Table_ID)
	{
		return getPOInfo(ctx, AD_Table_ID, null);
	}
	
	/**
	 *  POInfo Factory Method
	 *  @param ctx context
	 *  @param AD_Table_ID AD_Table_ID
	 *  @param trxName Transaction name
	 *  @return POInfo instance
	 */
	public static synchronized POInfo getPOInfo (Properties ctx, int AD_Table_ID, String trxName)
	{
		Integer key = Integer.valueOf(AD_Table_ID);
		POInfo retValue = (POInfo)s_cache.get(key);
		if (retValue == null)
		{
			retValue = new POInfo(ctx, AD_Table_ID, false, trxName);
			if (retValue.getColumnCount() == 0)
				//	May be run before Language verification
				retValue = new POInfo(ctx, AD_Table_ID, true, trxName);
			else
				s_cache.put(key, retValue);
		}
		return retValue;
	}   //  getPOInfo

	/** Cache of POInfo     */
	private static CCache<Integer,POInfo>  s_cache = new CCache<Integer,POInfo>(I_AD_Table.Table_Name, "POInfo", 200, 0, false, 0);
	
	/**
	 *  Create Persistent Info
	 *  @param ctx context
	 *  @param AD_Table_ID AD_ Table_ID
	 * 	@param baseLanguageOnly get in base language
	 */
	private POInfo (Properties ctx, int AD_Table_ID, boolean baseLanguageOnly)
	{
		this(ctx, AD_Table_ID, baseLanguageOnly, null);
	}
	
	/**
	 *  Create Persistent Info
	 *  @param ctx context
	 *  @param AD_Table_ID AD_ Table_ID
	 * 	@param baseLanguageOnly get in base language
	 *  @param trxName transaction name
	 */
	private POInfo (Properties ctx, int AD_Table_ID, boolean baseLanguageOnly, String trxName)
	{
		m_ctx = ctx;
		m_AD_Table_ID = AD_Table_ID;
		boolean baseLanguage = baseLanguageOnly ? true : Env.isBaseLanguage(m_ctx, "AD_Table");
		loadInfo (baseLanguage, trxName);
	}   //  PInfo

	/** Context             	*/
	private transient Properties  m_ctx = null;
	/** Table_ID            	*/
	private int         m_AD_Table_ID = 0;
	/** Table Name          	*/
	private String      m_TableName = null;
	/** Access Level			*/
	private String		m_AccessLevel = MTable.ACCESSLEVEL_Organization;
	/** Columns             	*/
	private POInfoColumn[]    m_columns = null;
	/** Table has Key Column	*/ 
	private boolean		m_hasKeyColumn = false;
	/**	Table needs keep log*/
	private boolean 	m_IsChangeLog = false;
	/** column name to index map **/
	private Map<String, Integer> m_columnNameMap;
	/** ad_column_id to index map **/
	private Map<Integer, Integer> m_columnIdMap;
	private Boolean m_IsTranslated = null;

	/**
	 *  Load Table and Column Info
	 * 	@param baseLanguage true to load data in base language
	 *  @param trxName
	 */
	private void loadInfo (boolean baseLanguage, String trxName)
	{
		m_columnNameMap = new HashMap<String, Integer>();
		m_columnIdMap = new HashMap<Integer, Integer>();
		ArrayList<POInfoColumn> list = new ArrayList<POInfoColumn>(15);
		StringBuilder sql = new StringBuilder();
		sql.append("SELECT t.TableName, c.ColumnName,c.AD_Reference_ID,"    //  1..3
			+ "c.IsMandatory,c.IsUpdateable,c.DefaultValue,"                //  4..6
			+ "e.Name,e.Description, c.AD_Column_ID, "						//  7..9
			+ "c.IsKey,c.IsParent, "										//	10..11
			+ "c.AD_Reference_Value_ID, vr.Code, "							//	12..13
			+ "c.FieldLength, c.ValueMin, c.ValueMax, c.IsTranslated, "		//	14..17
			+ "t.AccessLevel, c.ColumnSQL, c.IsEncrypted, "					// 18..20
			+ "c.IsAllowLogging,c.IsAllowCopy,t.IsChangeLog ");											// 21..23
		sql.append("FROM AD_Table t"
			+ " INNER JOIN AD_Column c ON (t.AD_Table_ID=c.AD_Table_ID)"
			+ " LEFT OUTER JOIN AD_Val_Rule vr ON (c.AD_Val_Rule_ID=vr.AD_Val_Rule_ID)"
			+ " INNER JOIN AD_Element");
		if (!baseLanguage)
			sql.append("_Trl");
		sql.append(" e "
			+ " ON (c.AD_Element_ID=e.AD_Element_ID) "
			+ "WHERE t.AD_Table_ID=?"
			+ " AND c.IsActive='Y'");
		if (!baseLanguage)
			sql.append(" AND e.AD_Language='").append(Env.getAD_Language(m_ctx)).append("'");
		sql.append(" ORDER BY c.AD_Column_ID");
		//
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement(sql.toString(), trxName);
			pstmt.setInt(1, m_AD_Table_ID);
			rs = pstmt.executeQuery();
			while (rs.next())
			{
				if (m_TableName == null)
					m_TableName = rs.getString(1);
				String ColumnName = rs.getString(2);
				int AD_Reference_ID = rs.getInt(3);
				boolean IsMandatory = "Y".equals(rs.getString(4));
				boolean IsUpdateable = "Y".equals(rs.getString(5));
				String DefaultLogic = rs.getString(6);
				String Name = rs.getString(7);
				String Description = rs.getString(8);
				int AD_Column_ID = rs.getInt(9);
				boolean IsKey = "Y".equals(rs.getString(10));
				if (IsKey)
					m_hasKeyColumn = true;
				boolean IsParent = "Y".equals(rs.getString(11));
				int AD_Reference_Value_ID = rs.getInt(12);
				String ValidationCode = rs.getString(13);
				int FieldLength = rs.getInt(14);
				String ValueMin = rs.getString(15);
				String ValueMax = rs.getString(16);
				boolean IsTranslated = "Y".equals(rs.getString(17));
				//
				m_AccessLevel = rs.getString(18);
				String ColumnSQL = rs.getString(19);
				if (ColumnSQL != null && ColumnSQL.length() > 0 && (ColumnSQL.startsWith(MColumn.VIRTUAL_UI_COLUMN_PREFIX) || ColumnSQL.startsWith(MColumn.VIRTUAL_SEARCH_COLUMN_PREFIX)))
					ColumnSQL = "NULL";
				if (ColumnSQL != null && ColumnSQL.contains("@"))
					ColumnSQL = Env.parseContext(Env.getCtx(), -1, ColumnSQL, false, true);
				boolean IsEncrypted = "Y".equals(rs.getString(20));
				boolean IsAllowLogging = "Y".equals(rs.getString(21));
				boolean IsAllowCopy = "Y".equals(rs.getString(22));
				m_IsChangeLog="Y".equals(rs.getString(23));

				POInfoColumn col = new POInfoColumn (
					AD_Column_ID, ColumnName, ColumnSQL, AD_Reference_ID,
					IsMandatory, IsUpdateable,
					DefaultLogic, Name, Description,
					IsKey, IsParent,
					AD_Reference_Value_ID, ValidationCode,
					FieldLength, ValueMin, ValueMax,
					IsTranslated, IsEncrypted,
					IsAllowLogging, IsAllowCopy);
				list.add(col);
				
				m_columnNameMap.put(ColumnName.toUpperCase(), list.size() - 1);
				m_columnIdMap.put(AD_Column_ID, list.size() - 1);
			}
		}
		catch (SQLException e)
		{
			CLogger.get().log(Level.SEVERE, sql.toString(), e);
		}
		finally {
			DB.close(rs, pstmt);
			rs = null; pstmt = null;
		}
		//  convert to array
		m_columns = new POInfoColumn[list.size()];
		list.toArray(m_columns);
	}   //  loadInfo

	/**
	 *  String representation
	 *  @return String Representation
	 */
	@Override
	public String toString()
	{
		return "POInfo[" + getTableName() + ",AD_Table_ID=" + getAD_Table_ID() + "]";
	}   //  toString

	/**
	 *  String representation for column
	 * 	@param index column index
	 *  @return String Representation
	 */
	public String toString (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return "POInfo[" + getTableName() + "-(InvalidColumnIndex=" + index + ")]";
		return "POInfo[" + getTableName() + "-" + m_columns[index].toString() + "]";
	}   //  toString

	/**
	 *  Get Table Name
	 *  @return Table Name
	 */
	public String getTableName()
	{
		return m_TableName;
	}   //  getTableName

	/**
	 *  Get AD_Table_ID
	 *  @return AD_Table_ID
	 */
	public int getAD_Table_ID()
	{
		return m_AD_Table_ID;
	}   //  getAD_Table_ID

	/**
	 * 	Table has a Key Column
	 *	@return true if table has a key column
	 */
	public boolean hasKeyColumn()
	{
		return m_hasKeyColumn;
	}	//	hasKeyColumn

	/**
	 * 	Get Table Access Level
	 *	@return Table.ACCESS..
	 */
	public String getAccessLevel()
	{
		return m_AccessLevel;
	}	//	getAccessLevel
	
	/**
	 *  Get ColumnCount
	 *  @return column count
	 */
	public int getColumnCount()
	{
		return m_columns.length;
	}   //  getColumnCount

	/**
	 *  Get Column Index
	 *  @param ColumnName column name
	 *  @return index of column with ColumnName or -1 if not found
	 */
	public int getColumnIndex (String ColumnName)
	{
		Integer i = m_columnNameMap.get(ColumnName.toUpperCase());
		if (i != null)
			return i.intValue();
		
		return -1;
	}   //  getColumnIndex

	/**
	 *  Get Column Index
	 *  @param AD_Column_ID column
	 *  @return index of column with AD_Column_ID or -1 if not found
	 */
	public int getColumnIndex (int AD_Column_ID)
	{
		Integer i = m_columnIdMap.get(AD_Column_ID);
		if (i != null)
			return i.intValue();
		
		return -1;
	}   //  getColumnIndex
	
	/**
	 * Get AD_Column_ID
	 * @param columnName
	 * @return AD_Column_ID if found, -1 if not found
	 */
	public int getAD_Column_ID(String columnName) 
	{
		for (int i = 0; i < m_columns.length; i++)
		{
			if (columnName.equalsIgnoreCase(m_columns[i].ColumnName)) // globalqss : modified to compare ignoring case [ 1619179 ]
				return m_columns[i].AD_Column_ID;
		}
		return -1;
	}

	/**
	 *  Get Column Info
	 *  @param index column index
	 *  @return column info
	 */
	protected POInfoColumn getColumn (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index];
	}   //  getColumn

	/**
	 *  Get Column Name
	 *  @param index column index
	 *  @return column name
	 */
	public String getColumnName (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index].ColumnName;
	}   //  getColumnName

	/**
	 *  Get Column SQL or Column Name
	 *  @param index column index
	 *  @return column sql or column name
	 */
	public String getColumnSQL (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		if (m_columns[index].ColumnSQL != null && m_columns[index].ColumnSQL.length() > 0) {
			if (m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_UI_COLUMN_PREFIX) || m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_SEARCH_COLUMN_PREFIX))
				return "NULL AS " + m_columns[index].ColumnName;
			return m_columns[index].ColumnSQL + " AS " + m_columns[index].ColumnName;
		}
		return m_columns[index].ColumnName;
	}   //  getColumnSQL

	/**
	 *  Is Column Virtual?
	 *  @param index column index
	 *  @return true if column is virtual
	 */
	public boolean isVirtualColumn (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return true;
		return m_columns[index].ColumnSQL != null 
			&& m_columns[index].ColumnSQL.length() > 0;
	}   //  isVirtualColumn

	/**
	 *  Is Column Virtual DB?
	 *  @param index column index
	 *  @return true if column is virtual DB
	 */
	public boolean isVirtualDBColumn (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return true;
		return m_columns[index].ColumnSQL != null 
			&& m_columns[index].ColumnSQL.length() > 0
			&& !m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_UI_COLUMN_PREFIX)
			&& !m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_SEARCH_COLUMN_PREFIX);
	}   //  isVirtualDBColumn

	/**
	 *  Is Column Virtual UI?
	 *  @param index index
	 *  @return true if column is virtual UI
	 */
	public boolean isVirtualUIColumn (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return true;
		return m_columns[index].ColumnSQL != null 
			&& m_columns[index].ColumnSQL.length() > 0
			&& m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_UI_COLUMN_PREFIX);
	}   //  isVirtualUIColumn
	
	/**
	 *  Is Column Virtual Search?
	 *  @param index index
	 *  @return true if column is virtual search
	 */
	public boolean isVirtualSearchColumn (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return true;
		return m_columns[index].ColumnSQL != null 
			&& m_columns[index].ColumnSQL.length() > 0
			&& m_columns[index].ColumnSQL.startsWith(MColumn.VIRTUAL_SEARCH_COLUMN_PREFIX);
	}   //  isVirtualSearchColumn

	/**
	 *  Get Column Label
	 *  @param index column index
	 *  @return column label
	 */
	public String getColumnLabel (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index].ColumnLabel;
	}   //  getColumnLabel

	/**
	 *  Get Column Description
	 *  @param index column index
	 *  @return column description
	 */
	public String getColumnDescription (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index].ColumnDescription;
	}   //  getColumnDescription

	/**
	 *  Get Column Class
	 *  @param index column index
	 *  @return Class
	 */
	public Class<?> getColumnClass (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index].ColumnClass;
	}   //  getColumnClass

	/**
	 *  Get Column Display Type
	 *  @param index column index
	 *  @return DisplayType
	 */
	public int getColumnDisplayType (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return DisplayType.String;
		return m_columns[index].DisplayType;
	}   //  getColumnDisplayType

	/**
	 *  Get Column Default Logic
	 *  @param index column index
	 *  @return Default Logic
	 */
	public String getDefaultLogic (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return null;
		return m_columns[index].DefaultLogic;
	}   //  getDefaultLogic

	/**
	 *  Is Column Mandatory
	 *  @param index column index
	 *  @return true if column is mandatory
	 */
	public boolean isColumnMandatory (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsMandatory;
	}   //  isMandatory

	/**
	 *  Is Column Updateable
	 *  @param index column index
	 *  @return true if column is updateable
	 */
	public boolean isColumnUpdateable (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsUpdateable;
	}   //  isUpdateable

	/**
	 *  Set Column Updateable
	 *  @param index column index
	 *  @param updateable
	 */
	public void setColumnUpdateable (int index, boolean updateable)
	{
		if (index < 0 || index >= m_columns.length)
			return;
		m_columns[index].IsUpdateable = updateable;
	}	//	setColumnUpdateable

	/**
	 * 	Set all columns updateable
	 * 	@param updateable
	 */
	public void setUpdateable (boolean updateable)
	{
		for (int i = 0; i < m_columns.length; i++)
			m_columns[i].IsUpdateable = updateable;
	}	//	setUpdateable

	/**
	 *  Is Lookup Column
	 *  @param index column index
	 *  @return true if it is a lookup column
	 */
	public boolean isColumnLookup (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return DisplayType.isLookup(m_columns[index].DisplayType);
	}   //  isColumnLookup

	/**
	 *  Get Lookup
	 *  @param index column index
	 *  @return Lookup or null
	 */
	public Lookup getColumnLookup (int index)
	{
		if (!isColumnLookup(index))
			return null;
		//
		int WindowNo = 0;
		//  List, Table, TableDir
		Lookup lookup = null;
		try
		{
			lookup = MLookupFactory.get (m_ctx, WindowNo,
				m_columns[index].AD_Column_ID, m_columns[index].DisplayType,
				Env.getLanguage(m_ctx), m_columns[index].ColumnName,
				m_columns[index].AD_Reference_Value_ID,
				m_columns[index].IsParent, m_columns[index].ValidationCode);
		}
		catch (Exception e)
		{
			CLogger.get().log(Level.WARNING, "Cannot create Lookup for " + m_columns[index].ColumnName + "[" + m_columns[index].AD_Column_ID + "]", e);
			lookup = null;          //  cannot create Lookup
		}
		return lookup;
		/** @todo other lookup types */
	}   //  getColumnLookup

	/**
	 *  Is Column Key
	 *  @param index column index
	 *  @return true if column is a key column
	 */
	public boolean isKey (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsKey;
	}   //  isKey

	/**
	 *  Is Column Parent
	 *  @param index column index
	 *  @return true if column is a Parent column
	 */
	public boolean isColumnParent (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsParent;
	}   //  isColumnParent

	/**
	 *  Is Column Translated
	 *  @param index column index
	 *  @return true if column is translated
	 */
	public boolean isColumnTranslated (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsTranslated;
	}   //  isColumnTranslated

	/**
	 *  Is Table Translated
	 *  @return true if table is translated
	 */
	public synchronized boolean isTranslated ()
	{
		if (m_IsTranslated  == null) {
			m_IsTranslated = Boolean.FALSE;
			for (int i = 0; i < m_columns.length; i++)
			{
				if (m_columns[i].IsTranslated) {
					m_IsTranslated = Boolean.TRUE;
					break;
				}
			}
		}
		return m_IsTranslated.booleanValue();
	}   //  isTranslated

	/**
	 *  Is Column (data) Encrypted
	 *  @param index column index
	 *  @return true if column is encrypted
	 */
	public boolean isEncrypted (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsEncrypted;
	}   //  isEncrypted

	/**
	 * Is column secure
	 * @param index column index
	 * @return true if column is secure
	 */
	public boolean isSecure(int index)
	{
		if (index < 0 || index >= m_columns.length)
			return false;
		return MColumn.get(m_columns[index].AD_Column_ID).isSecure();
	}
	
	/**
	 * Is allowed logging on this column
	 * 
	 * @param index column index
	 * @return true if column is allowed to be logged
	 */
	public boolean isAllowLogging(int index) {
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsAllowLogging;
	} // isAllowLogging

	/**
	 * Is allowed copying this column
	 * 
	 * @param index column index
	 * @return true if column is allowed to be copied
	 */
	public boolean isAllowCopy(int index) {
		if (index < 0 || index >= m_columns.length)
			return false;
		return m_columns[index].IsAllowCopy;
	} // isAllowCopy

	/**
	 *  Get Column FieldLength
	 *  @param index column index
	 *  @return field length or 0
	 */
	public int getFieldLength (int index)
	{
		if (index < 0 || index >= m_columns.length)
			return 0;
		return m_columns[index].FieldLength;
	}   //  getFieldLength

	/**
	 *  Get Column FieldLength
	 *  @param columnName Column Name
	 *  @return field length or 0
	 */
	public int getFieldLength (String columnName)
	{
		int index = getColumnIndex( columnName );
		if (index >= 0) {
			return getFieldLength( index );
		}
		return 0;
	}
	
	/**
	 *  Validate value 
	 *  @param index column index
	 * 	@param value value to validate
	 *  @return null if valid, otherwise error message
	 */
	public String validate (int index, Object value)
	{
		if (index < 0 || index >= m_columns.length)
			return "RangeError";
		//	Mandatory (i.e. not null
		if (m_columns[index].IsMandatory && value == null)
		{
			return "FillMandatory";
		}
		if (value == null)
			return null;
		
		//	Length ignored

		//
		if (m_columns[index].ValueMin != null)
		{
			BigDecimal value_BD = null;
			try
			{
				if (m_columns[index].ValueMin_BD != null)
					value_BD = new BigDecimal(value.toString());
			}
			catch (Exception ex){}
			//	Both are Numeric
			if (m_columns[index].ValueMin_BD != null && value_BD != null)
			{	//	error: 1 - 0 => 1  -  OK: 1 - 1 => 0 & 1 - 10 => -1
				int comp = m_columns[index].ValueMin_BD.compareTo(value_BD);
				if (comp > 0)
				{
					return "LessThanMinValue"+";"+m_columns[index].ValueMin_BD.toPlainString();
				}
			}
			else if (value instanceof Timestamp && m_columns[index].ValueMin_TS != null)    // Date
			{
				if (((Timestamp) value).before(m_columns[index].ValueMin_TS))
				{
					return "LessThanMinValue"+";"+m_columns[index].ValueMin;
				}
			}
			else	//	String
			{
				int comp = m_columns[index].ValueMin.compareTo(value.toString());
				if (comp > 0)
				{
					return "LessThanMinValue"+";"+m_columns[index].ValueMin;
				}
			}
		}
		if (m_columns[index].ValueMax != null)
		{
			BigDecimal value_BD = null;
			try
			{
				if (m_columns[index].ValueMax_BD != null)
					value_BD = new BigDecimal(value.toString());
			}
			catch (Exception ex){}
			//	Both are Numeric
			if (m_columns[index].ValueMax_BD != null && value_BD != null)
			{	//	error 12 - 20 => -1  -  OK: 12 - 12 => 0 & 12 - 10 => 1
				int comp = m_columns[index].ValueMax_BD.compareTo(value_BD);
				if (comp < 0)
				{
					return "MoreThanMaxValue"+";"+m_columns[index].ValueMax_BD.toPlainString();
				}
			}
			else if (value instanceof Timestamp && m_columns[index].ValueMax_TS != null)    // Date
			{
				if (((Timestamp) value).after(m_columns[index].ValueMax_TS))
				{
					return "MoreThanMaxValue"+";"+m_columns[index].ValueMax;
				}
			}
			else	//	String
			{
				int comp = m_columns[index].ValueMax.compareTo(value.toString());
				if (comp < 0)
				{
					return "MoreThanMaxValue"+";"+m_columns[index].ValueMax;
				}
			}
		}
		return null;
	}   //  validate

	/**
	 * Build SQL SELECT statement.
	 * @return {@link StringBuilder} instance with the SQL statement.
	 */
	public StringBuilder buildSelect()
	{
		return buildSelect(false, false);
	}

	/**
	 * Build SQL SELECT statement.
	 * @param fullyQualified prefix column names with the table name
	 * @param noVirtualColumn Include (<code>false</code> value) all declared virtual columns at once 
	 *        or use lazy loading (<code>true</code> value).
	 * @return {@link StringBuilder} instance with the SQL statement.
	 */
	public StringBuilder buildSelect(boolean fullyQualified, boolean noVirtualColumn) {
		return buildSelect(fullyQualified, noVirtualColumn ? new String[] {} : null);
	}

	/**
	 * Build SQL SELECT statement.
	 * @param fullyQualified prefix column names with the table name
	 * @param virtualColumns names of virtual columns to include along with the regular table columns.<br/> 
	 *        - if virtualColumns is null then all declared virtual columns will be included.<br/>
	 *        - if virtualColumns is an empty string array (new String[] {}), no declared virtual columns will be included.
	 * @return {@link StringBuilder} instance with the SQL statement.
	 */
	public StringBuilder buildSelect(boolean fullyQualified, String ... virtualColumns)
	{
		StringBuilder sql = new StringBuilder("SELECT ");
		int size = getColumnCount();
		int count = 0;
		for (int i = 0; i < size; i++)
		{
			boolean virtual = isVirtualColumn(i);
			if (virtual && virtualColumns != null)
			{
				boolean found = false;
				for(String virtualColumn : virtualColumns)
				{
					if(m_columns[i].ColumnName.equalsIgnoreCase(virtualColumn))
					{
						found = true;
						break;
					}
				}
				if(!found)
					continue;
			}

			count++;
			if (count > 1)
				sql.append(",");
			String columnSQL = getColumnSQL(i);
			if (!virtual)
				columnSQL = DB.getDatabase().quoteColumnName(columnSQL);
			if (fullyQualified && !virtual)
				sql.append(getTableName()).append(".");
			sql.append(columnSQL);	//	Normal and Virtual Column
			if (fullyQualified && !virtual)
				sql.append(" AS ").append(m_columns[i].ColumnName);
		}
		sql.append(" FROM ").append(getTableName());
		return sql;
	}

	/**
	 * Is column should always be loaded for partial loading of PO
	 * @param columnIndex
	 * @return true if column should always be loaded for partial loading of PO
	 */
	protected boolean isColumnAlwaysLoadedForPartialPO(int columnIndex)
	{
		String columnName = getColumnName(columnIndex);
		boolean isKey = isKey(columnIndex);
		boolean isUUID = columnName.equals(PO.getUUIDColumnName(m_TableName));
		// Always load key, uuid and standard columns
		if (isKey || isUUID || columnName.equalsIgnoreCase("ad_client_id") || columnName.equalsIgnoreCase("ad_org_id")
				|| columnName.equalsIgnoreCase("isactive") || columnName.equalsIgnoreCase("created") || columnName.equalsIgnoreCase("createdby")
				|| columnName.equalsIgnoreCase("updated") || columnName.equalsIgnoreCase("updatedby"))
			return true;
		else
			return false;
	}
	
	/**
	 * Build SQL SELECT statement for columns.
	 * @param fullyQualified prefix column names with the table name
	 * @return {@link StringBuilder} instance with the SQL statement.
	 */
	public StringBuilder buildSelectForColumns(boolean fullyQualified, String[] columns)
	{		
		StringBuilder sql = new StringBuilder("SELECT ");
		int size = getColumnCount();
		int count = 0;
		for (int i = 0; i < size; i++)
		{
			String columnName = getColumnName(i);
			boolean virtual = isVirtualColumn(i);			
			if (!isColumnAlwaysLoadedForPartialPO(i))
			{
				Optional<String> optional = Arrays.stream(columns).filter(e -> e.equalsIgnoreCase(columnName)).findFirst();
				if (!optional.isPresent())
					continue;
			}

			count++;
			if (count > 1)
				sql.append(",");
			String columnSQL = getColumnSQL(i);
			if (!virtual)
				columnSQL = DB.getDatabase().quoteColumnName(columnSQL);
			if (fullyQualified && !virtual)
				sql.append(getTableName()).append(".");
			sql.append(columnSQL);	//	Normal and Virtual Column
			if (fullyQualified && !virtual)
				sql.append(" AS ").append(m_columns[i].ColumnName);
		}
		sql.append(" FROM ").append(getTableName());
		return sql;
	}
	
	/**
	 * Is save changes to change log table
	 * @return if table save change log
	 */
	public boolean isChangeLog()
	{
		return m_IsChangeLog;
	}
	
	/**
	 * Read object from ois (for serialization)
	 * @param ois
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	private void readObject(ObjectInputStream ois)
			throws ClassNotFoundException, IOException {
	    // default deserialization
	    ois.defaultReadObject();
	    m_ctx = Env.getCtx();
	}
}   //  POInfo
