/******************************************************************************
 * 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.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import javax.sql.RowSet;
import org.adempiere.exceptions.AdempiereException;
import org.adempiere.exceptions.DBException;
import org.adempiere.util.ProcessUtil;
import org.compiere.Adempiere;
import org.compiere.db.AdempiereDatabase;
import org.compiere.db.CConnection;
import org.compiere.db.Database;
import org.compiere.db.ProxyFactory;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MLanguage;
import org.compiere.model.MRole;
import org.compiere.model.MSequence;
import org.compiere.model.MSysConfig;
import org.compiere.model.MSystem;
import org.compiere.model.MTable;
import org.compiere.model.PO;
import org.compiere.model.POResultSet;
import org.compiere.model.SystemIDs;
import org.compiere.process.ProcessInfo;
import org.compiere.process.ProcessInfoParameter;
/**
 *  Static methods for JDBC interface
 *
 *  @author     Jorg Janke
 *  @version    $Id: DB.java,v 1.8 2006/10/09 00:22:29 jjanke Exp $
 *  ---
 *  @author Ashley Ramdass (Posterita)
 *		
Modifications: removed static references to database connection and instead always
 *			get a new connection from database pool manager which manages all connections
 *			set rw/ro properties for the connection accordingly.
 *
 *  @author Teo Sarca, SC ARHIPAC SERVICE SRL
 * 		 BF [ 1647864 ] WAN: delete record error
 * 		 FR [ 1884435 ] Add more DB.getSQLValue helper methods
 * 		 FR [ 1904460 ] DB.executeUpdate should handle Boolean params
 * 		 BF [ 1962568 ] DB.executeUpdate should handle null params
 * 		 FR [ 1984268 ] DB.executeUpdateEx should throw DBException
 * 		 FR [ 1986583 ] Add DB.executeUpdateEx(String, Object[], String)
 * 		 BF [ 2030233 ] Remove duplicate code from DB class
 * 		 FR [ 2107062 ] Add more DB.getKeyNamePairs methods
 *		 FR [ 2448461 ] Introduce DB.getSQLValue*Ex methods
 *		 FR [ 2781053 ] Introduce DB.getValueNamePairs
 *		 FR [ 2818480 ] Introduce DB.createT_Selection helper method
 *			https://sourceforge.net/p/adempiere/feature-requests/757/
 *  @author Teo Sarca, teo.sarca@gmail.com
 * 		 BF [ 2873324 ] DB.TO_NUMBER should be a static method
 * 			https://sourceforge.net/p/adempiere/bugs/2160/
 * 		 FR [ 2873891 ] DB.getKeyNamePairs should use trxName
 * 			https://sourceforge.net/p/adempiere/feature-requests/847/
 *  @author Paul Bowden, phib BF 2900767 Zoom to child tab - inefficient queries
 *  @see https://sourceforge.net/p/adempiere/bugs/2222/
 */
public final class DB
{
	/** Connection Descriptor           */
	private static CConnection      s_cc = null;
	/**	Logger							*/
	private static CLogger			log = CLogger.getCLogger (DB.class);
	/** Lock object for mutual access to {@link #s_cc} */
	private static Object			s_ccLock = new Object();
	/** SQL Statement Separator "; "	*/
	public static final String SQLSTATEMENT_SEPARATOR = "; ";
	/**
	 * 	Check need for post Upgrade
	 * 	@param ctx context
	 *	@return true if post upgrade ran - false if there was no need
	 */
	@Deprecated(forRemoval = true, since = "11")
	public static boolean afterMigration (Properties ctx)
	{
		//	UPDATE AD_System SET IsJustMigrated='Y'
		MSystem system = MSystem.get(ctx);
		if (!system.isJustMigrated())
			return false;
		//	Role update
		log.info("Role");
		String sql = "SELECT * FROM AD_Role";
		PreparedStatement pstmt = null;
        ResultSet rs = null;
		try
		{
			pstmt = DB.prepareStatement (sql, null);
			rs = pstmt.executeQuery ();
			while (rs.next ())
			{
				MRole role = new MRole (ctx, rs, null);
				role.updateAccessRecords();
			}
		}
		catch (Exception e)
		{
			log.log(Level.SEVERE, "(1)", e);
		}
        finally
        {
            close(rs);
            close(pstmt);
            rs= null;
            pstmt = null;
        }
		//	Release Specif stuff & Print Format
		try
		{
			Class> clazz = Class.forName("org.compiere.MigrateData");
			clazz.getDeclaredConstructor().newInstance();
		}
		catch (Exception e)
		{
			log.log (Level.SEVERE, "Data", e);
		}
		//	Language check
		log.info("Language");
		MLanguage.maintain(ctx);
		//	Sequence check
		log.info("Sequence");
		ProcessInfo processInfo = new ProcessInfo("Sequence Check", 0);
		processInfo.setClassName("org.compiere.process.SequenceCheck");
		processInfo.setParameter(new ProcessInfoParameter[0]);
		ProcessUtil.startJavaProcess(ctx, processInfo, null);
		//	Costing Setup
		log.info("Costing");
		MAcctSchema[] ass = MAcctSchema.getClientAcctSchema(ctx, 0);
		for (int i = 0; i < ass.length; i++)
		{
			ass[i].checkCosting();
			ass[i].saveEx();
		}
		//	Reset Flag
		system.setIsJustMigrated(false);
		return system.save();
	}	//	afterMigration
	/**
	 * 	Update Mail Settings for System Client and System User (idempiereEnv.properties)
	 */
	public static void updateMail()
	{
		//	Get Property File
		String envName = Ini.getAdempiereHome();
		if (envName == null)
			return;
		envName += File.separator + "idempiereEnv.properties";
		File envFile = new File(envName);
		if (!envFile.exists())
			return;
		Properties env = new Properties();
		try
		{
			FileInputStream in = new FileInputStream(envFile);
			env.load(in);
			in.close();
		}
		catch (Exception e)
		{
			return;
		}
		String updated = env.getProperty("ADEMPIERE_MAIL_UPDATED");
		if (updated != null && updated.equals("Y"))
			return;
		//	See org.compiere.install.ConfigurationData
		String server = env.getProperty("ADEMPIERE_MAIL_SERVER");
		if (server == null || server.length() == 0)
			return;
		String adminEMail = env.getProperty("ADEMPIERE_ADMIN_EMAIL");
		if (adminEMail == null || adminEMail.length() == 0)
			return;
		String mailUser = env.getProperty("ADEMPIERE_MAIL_USER");
		if (mailUser == null || mailUser.length() == 0)
			return;
		String mailPassword;
		if (!env.containsKey("ADEMPIERE_MAIL_PASSWORD") && MSystem.isSecureProps())
			mailPassword = Ini.getVar("ADEMPIERE_MAIL_PASSWORD");
		else
			mailPassword = env.getProperty("ADEMPIERE_MAIL_PASSWORD");
		//
		StringBuilder sql = new StringBuilder("UPDATE AD_Client SET")
			.append(" SMTPHost=").append(DB.TO_STRING(server))
			.append(", RequestEMail=").append(DB.TO_STRING(adminEMail))
			.append(", RequestUser=").append(DB.TO_STRING(mailUser))
			.append(", RequestUserPW=").append(DB.TO_STRING(mailPassword))
			.append(", IsSMTPAuthorization='Y' WHERE AD_Client_ID=0");
		int no = DB.executeUpdate(sql.toString(), null);
		if (log.isLoggable(Level.FINE)) log.fine("Client #"+no);
		//
		sql = new StringBuilder("UPDATE AD_User SET ")
			.append(" EMail=").append(DB.TO_STRING(adminEMail))
			.append(", EMailUser=").append(DB.TO_STRING(mailUser))
			.append(", EMailUserPW=").append(DB.TO_STRING(mailPassword))
			.append(" WHERE AD_User_ID IN (?,?,?)");
		no = DB.executeUpdate(sql.toString(), new Object[]{SystemIDs.USER_SYSTEM_DEPRECATED, SystemIDs.USER_SYSTEM, SystemIDs.USER_SUPERUSER}, false, null);
		if (log.isLoggable(Level.FINE)) log.fine("User #"+no);
		//
		try (FileOutputStream out = new FileOutputStream(envFile))
		{
			env.setProperty("ADEMPIERE_MAIL_UPDATED", "Y");
			env.store(out, "");
			out.flush();
		}
		catch (Exception e)
		{
		}
	}	//	updateMail
	/**
	 *  Set active connection profile
	 *  @param cc connection profile
	 */
	public synchronized static void setDBTarget (CConnection cc)
	{
		if (cc == null)
			throw new IllegalArgumentException("Connection is NULL");
		if (s_cc != null && s_cc.equals(cc))
			return;
		DB.closeTarget();
		//
		synchronized(s_ccLock)
		{
			s_cc = cc;
		}
		s_cc.setDataSource();
		if (log.isLoggable(Level.CONFIG)) log.config(s_cc + " - DS=" + s_cc.isDataSource());
	}   //  setDBTarget
	/**
	 * Connect to database and initialise all connections.
	 * @return True if success, false otherwise
	 */
	@Deprecated
	public static boolean connect() {
		//direct connection
		boolean success =false;
		try
		{
            Connection conn = getConnection();
            if (conn != null)
            {
                s_cc.readInfo(conn);
                conn.close();
            }
            success = (conn != null);
		}
        catch (Exception e)
		{
        	//logging here could cause infinite loop
            //log.log(Level.SEVERE, "Could not connect to DB", e);
        	System.err.println("Could not connect to DB - " + e.getLocalizedMessage());
        	e.printStackTrace();
            success = false;
		}
		return success;
	}
	/**
	 * Is connected to DB.
	 * @return true, if connected to database
	 */
	public static boolean isConnected()
	{
		//bug [1637432]
		if (s_cc == null) return false;
		//get connection
		boolean success = false;
		try
		{
            Connection conn = getConnection();   //  try to get a connection
            if (conn != null)
            {
                conn.close();
                success = true;
            }
            else success = false;
		}
		catch (Exception e)
		{
			success = false;
		}
		return success;
	}
	/**
	 *  Replace by {@link #isConnected()}
	 * 
	 *  Is there a connection to the database ?
	 *  @param createNew ignore
	 *  @return true, if connected to database
	 *  @deprecated
	 */
	@Deprecated (since="10", forRemoval=true)
	public static boolean isConnected(boolean createNew)
	{
		return isConnected();
	}   //  isConnected
	/**
	 * Get auto commit connection from connection pool.
	 * @return {@link Connection}
	 */
	public static Connection getConnection() 
	{
		return getConnection(true);
	}
	
	/**
	 * Get auto or not auto commit connection from connection pool.pstmt.setObject(index, param)
	 * @param pstmt
	 * @param index
	 * @param param
	 * @throws SQLException
	 */
	public static void setParameter(PreparedStatement pstmt, int index, Object param)
	throws SQLException
	{
		if (param == null)
			pstmt.setObject(index, null);
		else if (param instanceof String)
			pstmt.setString(index, (String)param);
		else if (param instanceof Integer)
			pstmt.setInt(index, ((Integer)param).intValue());
		else if (param instanceof BigDecimal)
			pstmt.setBigDecimal(index, (BigDecimal)param);
		else if (param instanceof Timestamp)
			pstmt.setTimestamp(index, (Timestamp)param);
		else if (param instanceof Boolean)
			pstmt.setString(index, ((Boolean)param).booleanValue() ? "Y" : "N");
		else if (param instanceof byte[])
			pstmt.setBytes(index, (byte[]) param);
		else if (param instanceof Clob)
			pstmt.setClob(index, (Clob) param);
		else if (param.getClass().getName().equals("oracle.sql.BLOB"))
			pstmt.setObject(index, param);
		else
			throw new DBException("Unknown parameter type "+index+" - "+param);
	}
	/**
	 *	Execute Update.
	 *  saves "DBExecuteError" in Log
	 *  @param sql
	 *  @return number of rows updated or -1 if error
	 *  @deprecated
	 */
	@Deprecated
	public static int executeUpdate (String sql)
	{
		return executeUpdate(sql, null, false, null);
	}	//	executeUpdate
	/**
	 *	Execute Update. params)
    {
		return getSQLValueEx(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get int Value from sql. params)
    {
		return getSQLValue(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get string value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params array of parameters
     * @return first value or null
     * @throws DBException if there is any SQLException
     */
    public static String getSQLValueStringEx (String trxName, String sql, Object... params)
    {
    	String retValue = null;
    	PreparedStatement pstmt = null;
    	ResultSet rs = null;
    	Connection conn = null;
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
    	try
    	{
    		if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
    		
    		if (conn != null)
    			pstmt = prepareStatement(conn, sql);
    		else
    			pstmt = prepareStatement(sql, trxName);
    		setParameters(pstmt, params);
    		rs = pstmt.executeQuery();
    		if (rs.next())
    			retValue = rs.getString(1);
    		else
    			if (log.isLoggable(Level.FINE)) log.fine("No Value " + sql);
    	}
    	catch (SQLException e)
    	{
    		if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
    		throw new DBException(e, sql);
    	}
    	finally
    	{
    		close(rs, pstmt);
    		rs = null; pstmt = null;
    		if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
    	}
    	return retValue;
    }
    /**
     * Get String Value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params collection of parameters
     * @return first value or null
     * @throws DBException if there is any SQLException
     */
    public static String getSQLValueStringEx (String trxName, String sql, List params)
    {
		return getSQLValueStringEx(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get String Value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params array of parameters
     * @return first value or null
     */
    public static String getSQLValueString (String trxName, String sql, Object... params)
    {
    	String retValue = null;
    	try
    	{
    		retValue = getSQLValueStringEx(trxName, sql, params);
    	}
    	catch (Exception e)
    	{
    		log.log(Level.SEVERE, sql, getSQLException(e));
    	}
    	return retValue;
    }
    /**
     * Get string value from sql. params)
    {
		return getSQLValueString(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get BigDecimal value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params array of parameters
     * @return first value or null if not found
     * @throws DBException if there is any SQLException
     */
    public static BigDecimal getSQLValueBDEx (String trxName, String sql, Object... params) throws DBException
    {
    	BigDecimal retValue = null;
    	PreparedStatement pstmt = null;
    	ResultSet rs = null;
    	Connection conn = null;
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
    	try
    	{
    		if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
    		
    		if (conn != null)
    			pstmt = prepareStatement(conn, sql);
    		else
    			pstmt = prepareStatement(sql, trxName);
    		setParameters(pstmt, params);
    		rs = pstmt.executeQuery();
    		if (rs.next())
    			retValue = rs.getBigDecimal(1);
    		else
    			if (log.isLoggable(Level.FINE)) log.fine("No Value " + sql);
    	}
    	catch (SQLException e)
    	{
    		if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
    		throw new DBException(e, sql);
    	}
    	finally
    	{
    		close(rs, pstmt);
    		rs = null; pstmt = null;
    		if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
    	}
    	return retValue;
    }
    /**
     * Get BigDecimal Value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params collection of parameters
     * @return first value or null if not found
     * @throws DBException if there is any SQLException
     */
    public static BigDecimal getSQLValueBDEx (String trxName, String sql, List params) throws DBException
    {
		return getSQLValueBDEx(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get BigDecimal Value from sql. params)
    {
		return getSQLValueBD(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get Timestamp Value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params array of parameters
     * @return first value or null
     * @throws DBException if there is any SQLException
     */
    public static Timestamp getSQLValueTSEx (String trxName, String sql, Object... params)
    {
    	Timestamp retValue = null;
    	PreparedStatement pstmt = null;
    	ResultSet rs = null;
    	Connection conn = null;
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
    	try
    	{
    		if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
    		
    		if (conn != null)
    			pstmt = prepareStatement(conn, sql);
    		else
    			pstmt = prepareStatement(sql, trxName);
    		setParameters(pstmt, params);
    		rs = pstmt.executeQuery();
    		if (rs.next())
    			retValue = rs.getTimestamp(1);
    		else
    			if (log.isLoggable(Level.FINE)) log.fine("No Value " + sql);
    	}
    	catch (SQLException e)
    	{
    		if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
    		throw new DBException(e, sql);
    	}
    	finally
    	{
    		close(rs, pstmt);
    		rs = null; pstmt = null;
    		if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
    	}
    	return retValue;
    }
    /**
     * Get Timestamp Value from sql
     * @param trxName optional transaction name
     * @param sql
     * @param params collection of parameters
     * @return first value or null if not found
     * @throws DBException if there is any SQLException
     */
    public static Timestamp getSQLValueTSEx (String trxName, String sql, List params) throws DBException
    {
		return getSQLValueTSEx(trxName, sql, params.toArray(new Object[params.size()]));
    }
    /**
     * Get Timestamp Value from sql. params)
    {
		Object[] arr = new Object[params.size()];
		params.toArray(arr);
		return getSQLValueTS(trxName, sql, arr);
    }
	/**
	 * Get Array of Key Name Pairs
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @return array of {@link KeyNamePair}
	 * @see #getKeyNamePairs(String, boolean, Object...)
	 */
	public static KeyNamePair[] getKeyNamePairs(String sql, boolean optional)
	{
		return getKeyNamePairs(sql, optional, (Object[])null);
	}
	/**
	 * Get Array of Key Name Pairs
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @return array of {@link KeyNamePair}
	 * @see #getKeyNamePairs(String, boolean, Object...)
	 */
	public static KeyNamePair[] getKeyNamePairsEx(String sql, boolean optional)
	{
		return getKeyNamePairsEx(sql, optional, (Object[])null);
	}
	
	/**
	 * Get Array of Key Name Pairs
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @param params query parameters
	 */
	public static KeyNamePair[] getKeyNamePairs(String sql, boolean optional, Object ... params)
	{
		return getKeyNamePairs(null, sql, optional, params);
	}
	/**
	 * Get Array of Key Name Pairs
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @param params query parameters
	 */
	public static KeyNamePair[] getKeyNamePairsEx(String sql, boolean optional, Object ... params)
	{
		return getKeyNamePairsEx(null, sql, optional, params);
	}
	
	/**
	 * Get Array of Key Name Pairs
	 * @param trxName
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @param params query parameters
	 * @return Array of Key Name Pairs
	 */
	public static KeyNamePair[] getKeyNamePairs(String trxName, String sql, boolean optional, Object ... params)
	{
		try 
		{
			return getKeyNamePairsEx(trxName, sql, optional, params);		
		} 
		catch (Exception e)
        {
            log.log(Level.SEVERE, sql, getSQLException(e));
        }
		return new KeyNamePair[0];
	}
	
	/**
	 * Get Array of Key Name Pairs
	 * @param trxName
	 * @param sql select with id / name as first / second column
	 * @param optional if true (-1,"") is added
	 * @param params query parameters
	 * @return Array of Key Name Pairs
	 */
	public static KeyNamePair[] getKeyNamePairsEx(String trxName, String sql, boolean optional, Object ... params)
	{
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Connection conn = null; 
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
        ArrayList list = new ArrayList();
        if (optional)
        {
            list.add (new KeyNamePair(-1, ""));
        }
        try
        {
        	if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
        	if (conn != null)
        		pstmt = prepareStatement(conn, sql);
        	else
        		pstmt = DB.prepareStatement(sql, trxName);
            setParameters(pstmt, params);
            rs = pstmt.executeQuery();
            while (rs.next())
            {
                list.add(new KeyNamePair(rs.getInt(1), rs.getString(2)));
            }
        }
        catch (SQLException e)
        {
        	if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
            throw new DBException(e.getMessage(), e);
        }
        finally
        {
            close(rs, pstmt);
            rs= null;
            pstmt = null;
            if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
        }
        KeyNamePair[] retValue = new KeyNamePair[list.size()];
        list.toArray(retValue);
        return retValue;
	}	//	getKeyNamePairs
	/**
	 * Get Array of IDs
	 * @param trxName
	 * @param sql select with id as first column
	 * @param params query parameters
     * @throws DBException if there is any SQLException
     */
	public static int[] getIDsEx(String trxName, String sql, Object ... params) throws DBException
	{
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        ArrayList list = new ArrayList();
        try
        {
            pstmt = DB.prepareStatement(sql, trxName);
            setParameters(pstmt, params);
            rs = pstmt.executeQuery();
            while (rs.next())
            {
                list.add(rs.getInt(1));
            }
        }
        catch (SQLException e)
        {
    		throw new DBException(e, sql);
        }
        finally
        {
            close(rs, pstmt);
            rs= null;
            pstmt = null;
        }
		//	Convert to array
		int[] retValue = new int[list.size()];
		for (int i = 0; i < retValue.length; i++)
		{
			retValue[i] = list.get(i);
		}
        return retValue;
	}	//	getIDsEx
	
	/**
	 * 	Is Sales Order Trx.
	 *	    -	include in ' (single quotes)
	 *	    -	replace ' with ''
	 *   
	 *  @param txt  String with text
	 *  @param maxLength    Maximum Length of content or 0 to ignore
	 *  @return escaped string for sql statement (NULL if null)
	 */
	public static String TO_STRING (String txt, int maxLength)
	{
		if (txt == null || txt.length() == 0)
			return "NULL";
		//  Length
		String text = txt;
		if (maxLength != 0 && text.length() > maxLength)
			text = txt.substring(0, maxLength);
		//  copy characters		(we need to look through anyway)
		StringBuilder out = new StringBuilder();
		out.append(QUOTE);		//	'
		for (int i = 0; i < text.length(); i++)
		{
			char c = text.charAt(i);
			if (c == QUOTE)
				out.append("''");
			else
				out.append(c);
		}
		out.append(QUOTE);		//	'
		//
		return out.toString();
	}	//	TO_STRING
	
	/**
	 * 	Return string as JSON object for INSERT statements with correct precision
	 *	@param value
	 *	@return value as json
	 */
	public static String TO_JSON (String value)
	{
		return s_cc.getDatabase().TO_JSON(value);
	}
	
	/**
	 *	@return string with right casting for JSON inserts
	 */
	public static String getJSONCast()
	{
		return s_cc.getDatabase().getJSONCast();
	}
	/**
	 * Convenient method to close result set
	 * @param rs
	 */
	public static void close( ResultSet rs) {
        try {
            if (rs!=null) rs.close();
        } catch (SQLException e) {
            ;
        }
    }
	/**
	 * Convenient method to close statement
	 * @param st
	 */
    public static void close( Statement st) {
        try {
            if (st!=null) st.close();
        } catch (SQLException e) {
            ;
        }
    }
    /**
     * Convenient method to close result set and statement
     * @param rs result set
     * @param st statement
     * @see #close(ResultSet)
     * @see #close(Statement)
     */
    public static void close(ResultSet rs, Statement st) {
    	close(rs);
    	close(st);
    }
    /**
     * Convenient method to close a {@link POResultSet}
     * @param rs result set
     * @see POResultSet#close()
     */
    public static void close(POResultSet> rs) {
    	if (rs != null)
    		rs.close();
    }
	/**
	 * Try to get the SQLException from Exception
	 * @param e Exception
	 * @return SQLException if found or provided exception elsewhere
	 */
    public static Exception getSQLException(Exception e)
    {
    	Throwable e1 = e;
    	while (e1 != null)
    	{
	    	if (e1 instanceof SQLException)
	    		return (SQLException)e1;
	    	e1 = e1.getCause();
    	}
    	return e;
    }
	/** Quote			*/
	private static final char QUOTE = '\'';
    // Following methods are kept for BeanShell compatibility.
	// See BF [ 2030233 ] Remove duplicate code from DB class
    // TODO: remove this when BeanShell will support varargs methods
    public static int getSQLValue (String trxName, String sql)
    {
    	return getSQLValue(trxName, sql, new Object[]{});
    }
    public static int getSQLValue (String trxName, String sql, int int_param1)
    {
    	return getSQLValue(trxName, sql, new Object[]{int_param1});
    }
    public static int getSQLValue (String trxName, String sql, int int_param1, int int_param2)
    {
    	return getSQLValue(trxName, sql, new Object[]{int_param1, int_param2});
    }
    public static int getSQLValue (String trxName, String sql, String str_param1)
    {
    	return getSQLValue(trxName, sql, new Object[]{str_param1});
    }
    public static int getSQLValue (String trxName, String sql, int int_param1, String str_param2)
    {
    	return getSQLValue(trxName, sql, new Object[]{int_param1, str_param2});
    }
    public static String getSQLValueString (String trxName, String sql, int int_param1)
    {
    	return getSQLValueString(trxName, sql, new Object[]{int_param1});
    }
    public static BigDecimal getSQLValueBD (String trxName, String sql, int int_param1)
    {
    	return getSQLValueBD(trxName, sql, new Object[]{int_param1});
    }
    //End BeanShell compatibility.
	/**
	 * Get Array of ValueNamePair items.
	 *  Example:
	 * String sql = "SELECT Name, Description FROM AD_Ref_List WHERE AD_Reference_ID=?";
	 * ValueNamePair[] list = DB.getValueNamePairs(sql, false, params);
	 *  
	 * @param sql SELECT Value_Column, Name_Column FROM ...
	 * @param optional if {@link ValueNamePair#EMPTY} is added
	 * @param params query parameters
	 * @return array of {@link ValueNamePair} or empty array
     * @throws DBException if there is any SQLException
	 */
	public static ValueNamePair[] getValueNamePairs(String sql, boolean optional, List params)
	{
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        ArrayList list = new ArrayList();
        if (optional)
        {
            list.add (ValueNamePair.EMPTY);
        }
        try
        {
            pstmt = DB.prepareStatement(sql, null);
            setParameters(pstmt, params);
            rs = pstmt.executeQuery();
            while (rs.next())
            {
                list.add(new ValueNamePair(rs.getString(1), rs.getString(2)));
            }
        }
        catch (SQLException e)
        {
            throw new DBException(e, sql);
        }
        finally
        {
            close(rs, pstmt);
            rs = null; pstmt = null;
        }
		return list.toArray(new ValueNamePair[list.size()]);
	}
	/**
	 * Get Array of KeyNamePair items.
	 *  Example:
	 * String sql = "SELECT C_City_ID, Name FROM C_City WHERE C_City_ID=?";
	 * KeyNamePair[] list = DB.getKeyNamePairs(sql, false, params);
	 *  
	 * @param sql SELECT ID_Column, Name_Column FROM ...
	 * @param optional if {@link ValueNamePair#EMPTY} is added
	 * @param params query parameters
	 * @return array of {@link KeyNamePair} or empty array
     * @throws DBException if there is any SQLException
	 */
	public static KeyNamePair[] getKeyNamePairs(String sql, boolean optional, List params)
	{
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        ArrayList list = new ArrayList();
        if (optional)
        {
            list.add (KeyNamePair.EMPTY);
        }
        try
        {
            pstmt = DB.prepareStatement(sql, null);
            setParameters(pstmt, params);
            rs = pstmt.executeQuery();
            while (rs.next())
            {
                list.add(new KeyNamePair(rs.getInt(1), rs.getString(2)));
            }
        }
        catch (SQLException e)
        {
            throw new DBException(e, sql);
        }
        finally
        {
            close(rs, pstmt);
            rs = null; pstmt = null;
        }
		return list.toArray(new KeyNamePair[list.size()]);
	}
	/**
	 * Insert selection into T_Selection table. selection, String trxName)
	{
		StringBuilder insert = new StringBuilder();
		insert.append("INSERT INTO T_SELECTION(AD_PINSTANCE_ID, T_SELECTION_ID) ");
		int counter = 0;
		for(Integer selectedId : selection)
		{
			counter++;
			if (counter > 1)
				insert.append(" UNION ");
			insert.append("SELECT ");
			insert.append(AD_PInstance_ID);
			insert.append(", ");
			insert.append(selectedId);
			insert.append(" FROM DUAL ");
			if (counter >= 1000)
			{
				DB.executeUpdateEx(insert.toString(), trxName);
				insert = new StringBuilder();
				insert.append("INSERT INTO T_SELECTION(AD_PINSTANCE_ID, T_SELECTION_ID) ");
				counter = 0;
			}
		}
		if (counter > 0)
		{
			DB.executeUpdateEx(insert.toString(), trxName);
		}
	}
	/**
	 * Insert selection into T_Selection table. saveKeys, String trxName) {
		Collection saveKeysNP = new ArrayList();
		for (NamePair saveKey : saveKeys)
			saveKeysNP.add(saveKey);
		createT_SelectionNewNP(AD_PInstance_ID, saveKeysNP, trxName);
	}
	/**
	 * Insert selection into T_Selection table. saveKeys, String trxName)
	{
		String initialInsert = "INSERT INTO T_SELECTION(AD_PINSTANCE_ID, T_SELECTION_ID, T_SELECTION_UU, ViewID) ";
		StringBuilder insert = new StringBuilder(initialInsert);
		int counter = 0;
		for(NamePair saveKey : saveKeys)
		{
			Object selectedId;
			if (saveKey instanceof KeyNamePair)
				selectedId = ((KeyNamePair)saveKey).getKey();
			else if (saveKey instanceof ValueNamePair)
				selectedId = ((ValueNamePair)saveKey).getValue();
			else
				throw new AdempiereException("NamePair type not allowed in DB.createT_SelectionNewNP, just KeyNamePair or ValueNamePair are allowed");
			counter++;
			if (counter > 1)
				insert.append(" UNION ");
			insert.append("SELECT ");
			insert.append(AD_PInstance_ID);
			insert.append(", ");
			if (selectedId instanceof Integer) {
				insert.append((Integer)selectedId);
				insert.append(", ' '");
			} else {
				insert.append("0, ");
				insert.append(DB.TO_STRING(selectedId.toString()));
			}
			insert.append(", ");
			
			String viewIDValue = saveKey.getName();
			// when no process have viewID or this process have no viewID or value of viewID is null
			if (viewIDValue == null){
				insert.append("NULL");
			}else{
				insert.append(DB.TO_STRING(viewIDValue));
			}
			
			insert.append(" FROM DUAL ");
			if (counter >= 1000)
			{
				DB.executeUpdateEx(insert.toString(), trxName);
				insert.delete(0,  insert.length());
				insert.append(initialInsert);
				counter = 0;
			}
		}
		if (counter > 0)
		{
			DB.executeUpdateEx(insert.toString(), trxName);
		}
	}
	private static boolean m_isUUIDVerified = false;
	private static boolean m_isUUIDSupported = false;
	
	/**
	 * Is DB support generate_uuid function
	 * @return true if current db have working generate_uuid function. generate_uuid doesn't work on 64 bit postgresql
	 * on windows yet.
	 */
	public static boolean isGenerateUUIDSupported() {
		if (! m_isUUIDVerified) {
			String uuidTest = null;
			try {
				uuidTest = getSQLValueStringEx(null, "SELECT Generate_UUID() FROM Dual");
			} catch (Exception e) {}
			m_isUUIDSupported = uuidTest != null && uuidTest.trim().length() == 36;
			m_isUUIDVerified = true;
		}
		return m_isUUIDSupported;
	}
	/**
	 * Throw DBException if trxName doesn't return an existing Trx instance.
	 * @param trxName
	 */
	private static void verifyTrx(String trxName) {
		if (trxName != null && Trx.get(trxName, false) == null) {
			// Using a trx that was previously closed or never opened
			// probably timed out - throw Exception (IDEMPIERE-644)
			String msg = "Transaction closed or never opened ("+trxName+") => (maybe timed out)";
			log.severe(msg); // severe
			throw new DBException(msg);
		}
	}
	/**
	 * Is table or view exists
	 * @param tableName
	 * @return true if table or view with name=tableName exists in db
	 */
	public static boolean isTableOrViewExists(String tableName) {
		Connection conn = getConnection();
		ResultSet rs = null;
		try {
			DatabaseMetaData metadata = conn.getMetaData();
			String tblName;
			if (metadata.storesUpperCaseIdentifiers())
				tblName = tableName.toUpperCase();
			else if (metadata.storesLowerCaseIdentifiers())
				tblName = tableName.toLowerCase();
			else
				tblName = tableName;
			rs = metadata.getTables(null, null, tblName, null);
			if (rs.next()) {
				return true;
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			DB.close(rs);
			try {
				conn.close();
			} catch (SQLException e) {}
		}
		return false;
	}
    /**
     * Get a list of objects from sql (one per each column in the select clause), column indexing starts with 0
     * @param trxName optional transaction name
     * @param sql
     * @param params array of parameters
     * @return null if not found
     * @throws DBException if there is any SQLException
     */
	public static List getSQLValueObjectsEx(String trxName, String sql, Object... params) {
		List retValue = new ArrayList();
    	PreparedStatement pstmt = null;
    	ResultSet rs = null;
    	Connection conn = null;
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
    	try
    	{
    		if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
    		
    		if (conn != null)
    			pstmt = prepareStatement(conn, sql);
    		else
    			pstmt = prepareStatement(sql, trxName);
    		setParameters(pstmt, params);
    		rs = pstmt.executeQuery();
			ResultSetMetaData rsmd = rs.getMetaData();
    		if (rs.next()) {
    			for (int i=1; i<=rsmd.getColumnCount(); i++) {
    				Object obj = rs.getObject(i);
        			if (rs.wasNull())
        				retValue.add(null);
        			else
        				retValue.add(obj);
    			}
    		} else {
    			retValue = null;
    		}
    	}
    	catch (SQLException e)
    	{
    		if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
    		throw new DBException(e, sql);
    	}
    	finally
    	{
    		close(rs, pstmt);
    		rs = null; pstmt = null;
    		if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
    	}
    	return retValue;
	}
    /**
     * Get a list of object list from sql (one object list per each row, and in the object list, one object per each column in the select clause), 
     * column indexing starts with 0.> getSQLArrayObjectsEx(String trxName, String sql, Object... params) {
		List> rowsArray = new ArrayList>();
    	PreparedStatement pstmt = null;
    	ResultSet rs = null;
    	Connection conn = null;
    	if (trxName == null)
    		conn = DB.createConnection(true, Connection.TRANSACTION_READ_COMMITTED);
    	try
    	{
    		if (conn != null)
    		{
    			conn.setAutoCommit(false);
    			conn.setReadOnly(true);
    		}
    		
    		if (conn != null)
    			pstmt = prepareStatement(conn, sql);
    		else
    			pstmt = prepareStatement(sql, trxName);
    		setParameters(pstmt, params);
    		rs = pstmt.executeQuery();
			ResultSetMetaData rsmd = rs.getMetaData();
    		while (rs.next()) {
    			List retValue = new ArrayList();
    			for (int i=1; i<=rsmd.getColumnCount(); i++) {
    				Object obj = rs.getObject(i);
        			if (rs.wasNull())
        				retValue.add(null);
        			else
        				retValue.add(obj);
    			}
    			rowsArray.add(retValue);
    		}
    	}
    	catch (SQLException e)
    	{
    		if (conn != null)
    		{
    			try {
					conn.rollback();
				} catch (SQLException e1) {
					e1.printStackTrace();
				}
    		}
    		throw new DBException(e, sql);
    	}
    	finally
    	{
    		close(rs, pstmt);
    		rs = null; pstmt = null;
    		if (conn != null)
    		{
    			closeAndResetReadonlyConnection(conn);
    		}
    	}
    	if (rowsArray.size() == 0)
    		return null;
    	return rowsArray;
	}
	/**
	 *	Create Read Replica Prepared Statement proxy
	 *  @param sql
	 * 	@param trxName transaction
	 *  @return Prepared Statement (from replica if possible, otherwise normal statement)
	 */
	public static PreparedStatement prepareNormalReadReplicaStatement(String sql, String trxName) {
		return prepareNormalReadReplicaStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, trxName);
	}
	/**
	 *	Create Read Replica Prepared Statement proxy
	 *  @param sql
	 *  @param resultSetType - ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE
	 *  @param resultSetConcurrency - ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE
	 * 	@param trxName transaction name
	 *  @return Prepared Statement (from replica if possible, otherwise normal statement)
	 */
	private static PreparedStatement prepareNormalReadReplicaStatement(String sql, int resultSetType, int resultSetConcurrency, String trxName) {
		if (sql == null || sql.length() == 0)
			throw new IllegalArgumentException("No SQL");
		boolean useReadReplica = MSysConfig.getValue(MSysConfig.DB_READ_REPLICA_URLS) != null;
		if (   trxName == null
			&& useReadReplica
			&& resultSetType == ResultSet.TYPE_FORWARD_ONLY
			&& resultSetConcurrency == ResultSet.CONCUR_READ_ONLY) {
			// this is a candidate for a read replica connection (read-only, forward-only, no-trx), try to obtain one, otherwise fallback to normal
			CPreparedStatement stmt = ProxyFactory.newReadReplicaPreparedStatement(resultSetType, resultSetConcurrency, sql);
			if (stmt != null) {
				return stmt;
			}
		}
		//
		return ProxyFactory.newCPreparedStatement(resultSetType, resultSetConcurrency, sql, trxName);
	}
	
	/**
	 * Create IN clause for csv value
	 * @param columnName
	 * @param csv comma separated value
	 * @return IN clause
	 */
	public static String inClauseForCSV(String columnName, String csv) 
	{
		return inClauseForCSV(columnName, csv, false);
	}
	
	/**
	 * Create IN clause for csv value
	 * @param columnName
	 * @param csv comma separated value
	 * @param isNotClause true to append NOT before IN
	 * @return IN clause
	 */
	public static String inClauseForCSV(String columnName, String csv, boolean isNotClause) 
	{
		StringBuilder builder = new StringBuilder();
		builder.append(columnName);
		
		if(isNotClause)
			builder.append(" NOT ");
		
		builder.append(" IN (");
		String[] values = csv.split("[,]");
		for(int i = 0; i < values.length; i++)
		{
			if (i > 0)
				builder.append(",");
			String key = values[i];
			if (columnName.endsWith("_ID")) 
			{
				builder.append(key);
			}
			else
			{
				if (key.startsWith("\"") && key.endsWith("\"")) 
				{
					key = key.substring(1, key.length()-1);
				}
				builder.append(TO_STRING(key));
			}
		}
		builder.append(")");
		return builder.toString();
	}
	
	/**
	 * Create subset clause for csv value (i.e columnName is a subset of the csv value set)
	 * @param columnName
	 * @param csv
	 * @return subset sql clause
	 */
	public static String subsetClauseForCSV(String columnName, String csv)
	{
		return getDatabase().subsetClauseForCSV(columnName, csv);
	}
	
	/**
	 * Create intersect clause for csv value (i.e columnName is an intersect with the csv value set)
	 * @param columnName
	 * @param csv
	 * @return intersect sql clause
	 */
	public static String intersectClauseForCSV(String columnName, String csv)
	{
		return intersectClauseForCSV(columnName, csv, false);
	}
	
	/**
	 * Create intersect clause for csv value (i.e columnName is an intersect with the csv value set)
	 * @param columnName
	 * @param csv
	 * @param isNotClause true to append NOT before the intersect clause
	 * @return intersect sql clause
	 */
	public static String intersectClauseForCSV(String columnName, String csv, boolean isNotClause)
	{
		return getDatabase().intersectClauseForCSV(columnName, csv, isNotClause);
	}
	
	/**
	 * Is sql a SELECT statement
	 * @param sql
	 * @return true if it is a SELECT statement
	 */
	public static boolean isSelectStatement(String sql) {
		String removeComments = "/\\*(?:.|[\\n\\r])*?\\*/";
		String removeQuotedStrings = "'(?:.|[\\n\\r])*?'";
		String removeLeadingSpaces = "^\\s+";
		String cleanSql = sql.toLowerCase().replaceAll(removeComments, "").replaceAll(removeQuotedStrings, "").replaceFirst(removeLeadingSpaces, "");
		if(cleanSql.matches("^select\\s.*$") && !cleanSql.contains(";"))
			return true;
		else
			return false;
	}
}	//	DB