/*
  Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FLOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program is free software; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; version 2
  of the License.

  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., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA
 
 */
package com.mysql.jdbc;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * An implementation of java.sql.Connection that load balances requests across a
 * series of MySQL JDBC connections, where the balancing takes place at
 * transaction commit.
 * 
 * Therefore, for this to work (at all), you must use transactions, even if only
 * reading data.
 * 
 * This implementation will invalidate connections that it detects have had
 * communication errors when processing a request. Problematic hosts will be
 * added to a global blacklist for loadBalanceBlacklistTimeout ms, after which
 * they will be removed from the blacklist and made eligible once again to be
 * selected for new connections.
 * 
 * This implementation is thread-safe, but it's questionable whether sharing a
 * connection instance amongst threads is a good idea, given that transactions
 * are scoped to connections in JDBC.
 * 
 * @version $Id: $
 * 
 */
public class LoadBalancingConnectionProxy implements InvocationHandler,
		PingTarget {

	private static Method getLocalTimeMethod;
	
	private long totalPhysicalConnections = 0;
	private long activePhysicalConnections = 0;
	private String hostToRemove = null;
	private long lastUsed = 0;
	private long transactionCount = 0;
	private ConnectionGroup connectionGroup = null;
	private String closedReason = null;

	public static final String BLACKLIST_TIMEOUT_PROPERTY_KEY = "loadBalanceBlacklistTimeout";

	static {
		try {
			getLocalTimeMethod = System.class.getMethod("nanoTime",
					new Class[0]);
		} catch (SecurityException e) {
			// ignore
		} catch (NoSuchMethodException e) {
			// ignore
		}
	}

	// Lifted from C/J 5.1's JDBC-2.0 connection pool classes, let's merge this
	// if/when this gets into 5.1
	protected class ConnectionErrorFiringInvocationHandler implements
			InvocationHandler {
		Object invokeOn = null;

		public ConnectionErrorFiringInvocationHandler(Object toInvokeOn) {
			invokeOn = toInvokeOn;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			Object result = null;

			try {
				result = method.invoke(invokeOn, args);

				if (result != null) {
					result = proxyIfInterfaceIsJdbc(result, result.getClass());
				}
			} catch (InvocationTargetException e) {
				dealWithInvocationException(e);
			}

			return result;
		}
	}

	protected MySQLConnection currentConn;

	protected List<String> hostList;

	protected Map<String, ConnectionImpl> liveConnections;

	private Map<ConnectionImpl, String> connectionsToHostsMap;

	private long[] responseTimes;

	private Map<String, Integer> hostsToListIndexMap;

	private boolean inTransaction = false;

	private long transactionStartTime = 0;

	private Properties localProps;

	private boolean isClosed = false;

	private BalanceStrategy balancer;

	private int retriesAllDown;

	private static Map<String, Long> globalBlacklist = new HashMap<String, Long>();

	private int globalBlacklistTimeout = 0;
	
	private long connectionGroupProxyID = 0;

	private LoadBalanceExceptionChecker exceptionChecker;

	private Map<Class<?>, Boolean> jdbcInterfacesForProxyCache = new HashMap<Class<?>, Boolean>();
	
	private MySQLConnection thisAsConnection = null;

	private int autoCommitSwapThreshold = 0;
	
	private static Constructor<?> JDBC_4_LB_CONNECTION_CTOR;
	
	static {
		if(Util.isJdbc4()){
			try {
				JDBC_4_LB_CONNECTION_CTOR =  Class.forName(
						"com.mysql.jdbc.JDBC4LoadBalancedMySQLConnection").getConstructor(
						new Class[] { LoadBalancingConnectionProxy.class});
			} catch (SecurityException e) {
				throw new RuntimeException(e);
			} catch (NoSuchMethodException e) {
				throw new RuntimeException(e);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	
	




	/**
	 * Creates a proxy for java.sql.Connection that routes requests between the
	 * given list of host:port and uses the given properties when creating
	 * connections.
	 * 
	 * @param hosts
	 * @param props
	 * @throws SQLException
	 */
	LoadBalancingConnectionProxy(List<String> hosts, Properties props)
			throws SQLException {
		String group = props.getProperty("loadBalanceConnectionGroup",
				null);
		boolean enableJMX = false;
		String enableJMXAsString = props.getProperty("loadBalanceEnableJMX",
				"false");
		try{
			enableJMX = Boolean.parseBoolean(enableJMXAsString);
		} catch (Exception e){
			throw SQLError.createSQLException(Messages.getString(
					"LoadBalancingConnectionProxy.badValueForLoadBalanceEnableJMX",
					new Object[] { enableJMXAsString }),
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);			
		}
		
		if(group != null){
			this.connectionGroup = ConnectionGroupManager.getConnectionGroupInstance(group);
			if(enableJMX){
				ConnectionGroupManager.registerJmx();
			}
			this.connectionGroupProxyID = this.connectionGroup.registerConnectionProxy(this, hosts);
			hosts = new ArrayList<String>(this.connectionGroup.getInitialHosts());
		}
		
		this.hostList = hosts;

		int numHosts = this.hostList.size();

		this.liveConnections = new HashMap<String, ConnectionImpl>(numHosts);
		this.connectionsToHostsMap = new HashMap<ConnectionImpl, String>(numHosts);
		this.responseTimes = new long[numHosts];
		this.hostsToListIndexMap = new HashMap<String, Integer>(numHosts);

		this.localProps = (Properties) props.clone();
		this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY);
		this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY);

		for (int i = 0; i < numHosts; i++) {
			this.hostsToListIndexMap.put(this.hostList.get(i), Integer.valueOf(i));
			this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY + "."
					+ (i + 1));
			this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY + "."
					+ (i + 1));
		}

		this.localProps.remove(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY);
		this.localProps.setProperty("useLocalSessionState", "true");

		String strategy = this.localProps.getProperty("loadBalanceStrategy",
				"random");
		
		String lbExceptionChecker = this.localProps.getProperty("loadBalanceExceptionChecker",
		"com.mysql.jdbc.StandardLoadBalanceExceptionChecker");


		String retriesAllDownAsString = this.localProps.getProperty(
				"retriesAllDown", "120");

		try {
			this.retriesAllDown = Integer.parseInt(retriesAllDownAsString);
		} catch (NumberFormatException nfe) {
			throw SQLError.createSQLException(Messages.getString(
					"LoadBalancingConnectionProxy.badValueForRetriesAllDown",
					new Object[] { retriesAllDownAsString }),
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
		}
		String blacklistTimeoutAsString = this.localProps.getProperty(
				BLACKLIST_TIMEOUT_PROPERTY_KEY, "0");

		try {
			this.globalBlacklistTimeout = Integer
					.parseInt(blacklistTimeoutAsString);
		} catch (NumberFormatException nfe) {
			throw SQLError
					.createSQLException(
							Messages
									.getString(
											"LoadBalancingConnectionProxy.badValueForLoadBalanceBlacklistTimeout",
											new Object[] { retriesAllDownAsString }),
							SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
		}

		if ("random".equals(strategy)) {
			this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
					"com.mysql.jdbc.RandomBalanceStrategy",
					"InvalidLoadBalanceStrategy", null).get(0);
		} else if ("bestResponseTime".equals(strategy)) {
			this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
					"com.mysql.jdbc.BestResponseTimeBalanceStrategy",
					"InvalidLoadBalanceStrategy", null).get(0);
		} else {
			this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
					strategy, "InvalidLoadBalanceStrategy", null).get(0);
		}

		String autoCommitSwapThresholdAsString = props.getProperty("loadBalanceAutoCommitStatementThreshold",
		"0");
		try {
			this.autoCommitSwapThreshold  = Integer.parseInt(autoCommitSwapThresholdAsString);
		} catch (NumberFormatException nfe) {
			throw SQLError.createSQLException(Messages.getString(
					"LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementThreshold",
					new Object[] { autoCommitSwapThresholdAsString }),
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
		}
		
		String autoCommitSwapRegex = props.getProperty("loadBalanceAutoCommitStatementRegex","");
		if(!("".equals(autoCommitSwapRegex))){
			try{
				"".matches(autoCommitSwapRegex);
			} catch (Exception e){
				throw SQLError.createSQLException(Messages.getString(
						"LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementRegex",
						new Object[] { autoCommitSwapRegex }),
						SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
			}
		}
		
		if(this.autoCommitSwapThreshold > 0){
			String statementInterceptors = this.localProps.getProperty("statementInterceptors");
			if(statementInterceptors == null){
				this.localProps.setProperty("statementInterceptors", "com.mysql.jdbc.LoadBalancedAutoCommitInterceptor");
			} else if(statementInterceptors.length() > 0){
				this.localProps.setProperty("statementInterceptors", statementInterceptors + ",com.mysql.jdbc.LoadBalancedAutoCommitInterceptor");
			}
			props.setProperty("statementInterceptors", this.localProps.getProperty("statementInterceptors"));
			
		}
		this.balancer.init(null, props);
		

		this.exceptionChecker = (LoadBalanceExceptionChecker) Util.loadExtensions(null, props,
				lbExceptionChecker, "InvalidLoadBalanceExceptionChecker", null).get(0);
		this.exceptionChecker.init(null, props);

		if(Util.isJdbc4()  || JDBC_4_LB_CONNECTION_CTOR != null){
			thisAsConnection =  (MySQLConnection) Util.handleNewInstance(JDBC_4_LB_CONNECTION_CTOR,
					new Object[] {this}, null);
		}else{
			thisAsConnection = new LoadBalancedMySQLConnection(this);
		}
		pickNewConnection();
		
		
	}

	/**
	 * Creates a new physical connection for the given host:port and updates
	 * required internal mappings and statistics for that connection.
	 * 
	 * @param hostPortSpec
	 * @return
	 * @throws SQLException
	 */
	public synchronized ConnectionImpl createConnectionForHost(
			String hostPortSpec) throws SQLException {
		Properties connProps = (Properties) this.localProps.clone();

		String[] hostPortPair = NonRegisteringDriver
				.parseHostPortPair(hostPortSpec);
		String hostName = hostPortPair[NonRegisteringDriver.HOST_NAME_INDEX];
		String portNumber = hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX];
		String dbName = connProps
				.getProperty(NonRegisteringDriver.DBNAME_PROPERTY_KEY);

		if (hostName == null) {
			throw new SQLException(
					"Could not find a hostname to start a connection to");
		}
		if (portNumber == null) {
			portNumber = "3306";// use default
		}

		connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY, hostName);
		connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY,
				portNumber);
		connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY + ".1",
				hostName);
		connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY + ".1",
				portNumber);
		connProps.setProperty(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY, "1");
		connProps.setProperty("roundRobinLoadBalance", "false"); // make sure we
																	// don't
																	// pickup
																	// the
																	// default
																	// value

		ConnectionImpl conn = (ConnectionImpl) ConnectionImpl.getInstance(
				hostName, Integer.parseInt(portNumber), connProps, dbName,
				"jdbc:mysql://" + hostName + ":" + portNumber + "/");
		
		

		this.liveConnections.put(hostPortSpec, conn);
		this.connectionsToHostsMap.put(conn, hostPortSpec);
		

		this.activePhysicalConnections++;
		this.totalPhysicalConnections++;

		conn.setProxy(this.thisAsConnection);

		return conn;
	}

	/**
	 * @param e
	 * @throws SQLException
	 * @throws Throwable
	 * @throws InvocationTargetException
	 */
	void dealWithInvocationException(InvocationTargetException e)
			throws SQLException, Throwable, InvocationTargetException {
		Throwable t = e.getTargetException();

		if (t != null) {
			if (t instanceof SQLException && shouldExceptionTriggerFailover((SQLException) t )) {
				invalidateCurrentConnection();
				pickNewConnection();
			}

			throw t;
		}

		throw e;
	}

	/**
	 * Closes current connection and removes it from required mappings.
	 * 
	 * @throws SQLException
	 */
	synchronized void invalidateCurrentConnection() throws SQLException {
		invalidateConnection(this.currentConn);
	}
	
	
	/**
	 * Closes specified connection and removes it from required mappings.
	 * @param conn
	 * @throws SQLException
	 */
	synchronized void invalidateConnection(MySQLConnection conn) throws SQLException {
		try {
			if (!conn.isClosed()) {
				conn.close();
			}
		} catch (SQLException e) { 
			// we don't really want to throw this Exception
		} finally {
			// add host to the global blacklist, if enabled
			if (this.isGlobalBlacklistEnabled()) {
				this.addToGlobalBlacklist(this.connectionsToHostsMap
						.get(conn));

			}
			// remove from liveConnections
			this.liveConnections.remove(this.connectionsToHostsMap
					.get(conn));
			Object mappedHost = this.connectionsToHostsMap
					.remove(conn);
			if (mappedHost != null
					&& this.hostsToListIndexMap.containsKey(mappedHost)) {
				int hostIndex = (this.hostsToListIndexMap
						.get(mappedHost)).intValue();
				// reset the statistics for the host
				synchronized (this.responseTimes) {
					this.responseTimes[hostIndex] = 0;
				}
			}
		}		
	}
	

	private void closeAllConnections() {
		synchronized (this) {
			// close all underlying connections
			Iterator<ConnectionImpl> allConnections = this.liveConnections.values().iterator();

			while (allConnections.hasNext()) {
				try {
					this.activePhysicalConnections--;
					allConnections.next().close();
				} catch (SQLException e) {
				}
			}

			if (!this.isClosed) {
				this.balancer.destroy();
				if(this.connectionGroup != null){
					this.connectionGroup.closeConnectionProxy(this);
				}
			}

			this.liveConnections.clear();
			this.connectionsToHostsMap.clear();
		}

	}
	
		
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		return this.invoke(proxy, method, args, true);
	}

	/**
	 * Proxies method invocation on the java.sql.Connection interface, trapping
	 * "close", "isClosed" and "commit/rollback" (to switch connections for load
	 * balancing).
	 * 
	 * @param proxy
	 * @param method
	 * @param args
	 * @param swapAtTransactionBoundary
	 * @return
	 * @throws Throwable
	 */
	public synchronized Object invoke(Object proxy, Method method, Object[] args, boolean swapAtTransactionBoundary)
			throws Throwable {

		String methodName = method.getName();
		
		if("getLoadBalanceSafeProxy".equals(methodName)){
			return this.currentConn;
		}

		if ("equals".equals(methodName) && args.length == 1) {
			if (args[0] instanceof Proxy) {
				return Boolean.valueOf((((Proxy) args[0]).equals(this)));
			}
			return Boolean.valueOf(this.equals(args[0]));
		}

		if ("hashCode".equals(methodName)) {
			return Integer.valueOf(this.hashCode());
		}

		if ("close".equals(methodName)) {
			closeAllConnections();

			this.isClosed = true;
			this.closedReason = "Connection explicitly closed.";

			return null;
		}

		if ("isClosed".equals(methodName)) {
			return Boolean.valueOf(this.isClosed);
		}

		if (this.isClosed) {
			String reason = "No operations allowed after connection closed.";
			if(this.closedReason != null){
				reason += ("  " + this.closedReason);
			}
			throw SQLError.createSQLException(
					reason,
					SQLError.SQL_STATE_CONNECTION_NOT_OPEN, null /*
																 * no access to
																 * a interceptor
																 * here...
																 */);
		}

		if (!inTransaction) {
			this.inTransaction = true;
			this.transactionStartTime = getLocalTimeBestResolution();
			this.transactionCount++;
		}

		Object result = null;

		try {
			this.lastUsed = System.currentTimeMillis();
			result = method.invoke(thisAsConnection, args);

			if (result != null) {
				if (result instanceof com.mysql.jdbc.Statement) {
					((com.mysql.jdbc.Statement) result).setPingTarget(this);
				}

				result = proxyIfInterfaceIsJdbc(result, result.getClass());
			}
		} catch (InvocationTargetException e) {
			dealWithInvocationException(e);
		} finally {
			if (swapAtTransactionBoundary && ("commit".equals(methodName) || "rollback".equals(methodName))) {
				this.inTransaction = false;

				// Update stats
				String host = this.connectionsToHostsMap.get(this.currentConn);
				// avoid NPE if the connection has already been removed from
				// connectionsToHostsMap
				// in invalidateCurrenctConnection()
				if (host != null) {
					synchronized (this.responseTimes) {
						int hostIndex = (this.hostsToListIndexMap
								.get(host)).intValue();

						if(hostIndex < this.responseTimes.length){
							this.responseTimes[hostIndex] = getLocalTimeBestResolution()
									- this.transactionStartTime;
						}
					}
				}
				pickNewConnection();
			}
		}

		return result;
	}

	/**
	 * Picks the "best" connection to use for the next transaction based on the
	 * BalanceStrategy in use.
	 * 
	 * @throws SQLException
	 */
	protected synchronized void pickNewConnection() throws SQLException {
		if (this.currentConn == null) { // startup
			this.currentConn = this.balancer.pickConnection(this, Collections
					.unmodifiableList(this.hostList), Collections
					.unmodifiableMap(this.liveConnections),
					this.responseTimes.clone(), this.retriesAllDown);
			return;
		}
		
		if(this.currentConn.isClosed()){
			invalidateCurrentConnection();
		}

		int pingTimeout = this.currentConn.getLoadBalancePingTimeout();
		boolean pingBeforeReturn = this.currentConn.getLoadBalanceValidateConnectionOnSwapServer();
		
		for(int hostsTried = 0, hostsToTry = this.hostList.size(); hostsTried <= hostsToTry; hostsTried++){
			ConnectionImpl newConn = null;
			try{
				newConn = this.balancer.pickConnection(

					this, Collections.unmodifiableList(this.hostList), Collections
							.unmodifiableMap(this.liveConnections),
					this.responseTimes.clone(), this.retriesAllDown);
		
				if (this.currentConn != null) {
					if(pingBeforeReturn){
						if(pingTimeout == 0){
							newConn.ping();
						} else {
							newConn.pingInternal(true, pingTimeout);
						}
					}
					
					syncSessionState(this.currentConn, newConn);
				}
				
				this.currentConn = newConn;
				return;
			} catch (SQLException e){
				
				if (shouldExceptionTriggerFailover(e) && newConn != null) {
					// connection error, close up shop on current
					// connection
					invalidateConnection(newConn);
				}
			}

		}
		// no hosts available to swap connection to, close up.
		this.isClosed = true;
		this.closedReason = "Connection closed after inability to pick valid new connection during fail-over.";

	}

	/**
	 * Recursively checks for interfaces on the given object to determine if it
	 * implements a java.sql interface, and if so, proxies the instance so that
	 * we can catch and fire SQL errors.
	 * 
	 * @param toProxy
	 * @param clazz
	 * @return
	 */
	Object proxyIfInterfaceIsJdbc(Object toProxy, Class<?> clazz) {
		
		if(isInterfaceJdbc(clazz)){
			
		Class<?>[] interfacesToProxy = getAllInterfacesToProxy(clazz);
		
		return Proxy.newProxyInstance(toProxy.getClass()
				.getClassLoader(), interfacesToProxy,
				createConnectionProxy(toProxy));
		}

		return toProxy;
	}

	private Map<Class<?>, Class<?>[]> allInterfacesToProxy = new HashMap<Class<?>, Class<?>[]>();
	
	private Class<?>[] getAllInterfacesToProxy(Class<?> clazz) {
		Class<?>[] interfacesToProxy = this.allInterfacesToProxy.get(clazz);
		
		if (interfacesToProxy != null) {
			return interfacesToProxy;
		}
		
		List<Class<?>> interfaces = new LinkedList<Class<?>>();
		
		Class<?> superClass = clazz;
		
		while (!(superClass.equals(Object.class))) {
			Class<?>[] declared = superClass.getInterfaces();
			
			for (int i = 0; i < declared.length; i++) {
				interfaces.add(declared[i]);
			}
			
			superClass = superClass.getSuperclass();
		}
		
		interfacesToProxy = new Class[interfaces.size()];
		interfaces.toArray(interfacesToProxy);
		
		this.allInterfacesToProxy.put(clazz, interfacesToProxy);
		
		return interfacesToProxy;
	}
	
	
	private boolean isInterfaceJdbc(Class<?> clazz){
		if(this.jdbcInterfacesForProxyCache.containsKey(clazz)){
			return (this.jdbcInterfacesForProxyCache.get(clazz)).booleanValue();
		}
		
		Class<?>[] interfaces = clazz.getInterfaces();

		for (int i = 0; i < interfaces.length; i++) {
			String packageName = interfaces[i].getPackage().getName();

			if ("java.sql".equals(packageName)
					|| "javax.sql".equals(packageName)
					|| "com.mysql.jdbc".equals(packageName)) {
				this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true));
				
				return true;
			}

			if(isInterfaceJdbc(interfaces[i])){
				this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true));
				
				return true;
			}
		}

		this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(false));
		return false;
		
	}

	protected ConnectionErrorFiringInvocationHandler createConnectionProxy(
			Object toProxy) {
		return new ConnectionErrorFiringInvocationHandler(toProxy);
	}

	/**
	 * Returns best-resolution representation of local time, using nanoTime() if
	 * available, otherwise defaulting to currentTimeMillis().
	 */
	private static long getLocalTimeBestResolution() {
		if (getLocalTimeMethod != null) {
			try {
				return ((Long) getLocalTimeMethod.invoke(null, (Object[])null))
						.longValue();
			} catch (IllegalArgumentException e) {
				// ignore - we fall through to currentTimeMillis()
			} catch (IllegalAccessException e) {
				// ignore - we fall through to currentTimeMillis()
			} catch (InvocationTargetException e) {
				// ignore - we fall through to currentTimeMillis()
			}
		}

		return System.currentTimeMillis();
	}

	public synchronized void doPing() throws SQLException {
		SQLException se = null;
		boolean foundHost = false;
		int pingTimeout = this.currentConn.getLoadBalancePingTimeout();
		synchronized (this) {
			for (Iterator<String> i = this.hostList.iterator(); i.hasNext();) {
				String host = i.next();
				ConnectionImpl conn = this.liveConnections.get(host);
				if (conn == null) {
					continue;
				}
				try {
					if(pingTimeout == 0){
						conn.ping();
					} else {
						conn.pingInternal(true, pingTimeout);
					}
					foundHost = true;
				} catch (SQLException e) {
					this.activePhysicalConnections--;
					// give up if it is the current connection, otherwise NPE
					// faking resultset later.
					if (host.equals(this.connectionsToHostsMap
							.get(this.currentConn))) {
						// clean up underlying connections, since connection
						// pool won't do it
						closeAllConnections();
						this.isClosed = true;
						this.closedReason = "Connection closed because ping of current connection failed.";
						throw e;
					}

					// if the Exception is caused by ping connection lifetime
					// checks, don't add to blacklist
					if (e
							.getMessage()
							.equals(
									Messages
											.getString("Connection.exceededConnectionLifetime"))) {
						// only set the return Exception if it's null
						if (se == null) {
							se = e;
						}
					} else {
						// overwrite the return Exception no matter what
						se = e;
						if (this.isGlobalBlacklistEnabled()) {
							this.addToGlobalBlacklist(host);
						}
					}
					// take the connection out of the liveConnections Map
					this.liveConnections.remove(this.connectionsToHostsMap
							.get(conn));
				}
			}
		}
		// if there were no successful pings
		if (!foundHost) {
			closeAllConnections();
			this.isClosed = true;
			this.closedReason = "Connection closed due to inability to ping any active connections.";
			// throw the stored Exception, if exists
			if (se != null) {
				throw se;
			}
			// or create a new SQLException and throw it, must be no
			// liveConnections
			((ConnectionImpl) this.currentConn)
					.throwConnectionClosedException();
		}
	}

	public void addToGlobalBlacklist(String host, long timeout) {
		if (this.isGlobalBlacklistEnabled()) {
			synchronized (globalBlacklist) {
				globalBlacklist.put(host, Long.valueOf(timeout));
			}
		}
	}
	
	public void addToGlobalBlacklist(String host){
		addToGlobalBlacklist(host, System.currentTimeMillis()
						+ this.globalBlacklistTimeout);
		
	}

	public boolean isGlobalBlacklistEnabled() {
		return (this.globalBlacklistTimeout > 0);
	}

	public synchronized Map<String, Long> getGlobalBlacklist() {
		if (!this.isGlobalBlacklistEnabled()) {
			String localHostToRemove = this.hostToRemove;
			
			if(hostToRemove != null){
				HashMap<String, Long> fakedBlacklist = new HashMap<String, Long>();
				fakedBlacklist.put(localHostToRemove, Long.valueOf(System.currentTimeMillis() + 5000));
				return fakedBlacklist;
			}
			
			return new HashMap<String, Long>(1);
		}

		// Make a local copy of the blacklist
		Map<String, Long> blacklistClone = new HashMap<String, Long>(globalBlacklist.size());
		// Copy everything from synchronized global blacklist to local copy for
		// manipulation
		synchronized (globalBlacklist) {
			blacklistClone.putAll(globalBlacklist);
		}
		Set<String> keys = blacklistClone.keySet();

		// we're only interested in blacklisted hosts that are in the hostList
		keys.retainAll(this.hostList);

		// Don't need to synchronize here as we using a local copy
		for (Iterator<String> i = keys.iterator(); i.hasNext();) {
			String host = i.next();
			// OK if null is returned because another thread already purged Map
			// entry.
			Long timeout = globalBlacklist.get(host);
			if (timeout != null
					&& timeout.longValue() < System.currentTimeMillis()) {
				// Timeout has expired, remove from blacklist
				synchronized (globalBlacklist) {
					globalBlacklist.remove(host);
				}
				i.remove();
			}

		}
		if (keys.size() == this.hostList.size()) {
			// return an empty blacklist, let the BalanceStrategy
			// implementations try to connect to everything
			// since it appears that all hosts are unavailable - we don't want
			// to wait for
			// loadBalanceBlacklistTimeout to expire.
			return new HashMap<String, Long>(1);
		}
		
		return blacklistClone;
	}

	public boolean shouldExceptionTriggerFailover(SQLException ex){
		return this.exceptionChecker.shouldExceptionTriggerFailover(ex);
		
	}
	
	public void removeHostWhenNotInUse(String host)
			throws SQLException {
		int timeBetweenChecks = 1000;
		long timeBeforeHardFail = 15000;
		
		synchronized (this) {
			addToGlobalBlacklist(host, timeBeforeHardFail + 1000);
			
			long cur = System.currentTimeMillis();
			
			while (System.currentTimeMillis() - timeBeforeHardFail < cur) {
	
				this.hostToRemove = host;
				
				if (!host.equals(this.currentConn.getHost())) {
					removeHost(host);
					return;
				}
			}
		}
		
		try {
			Thread.sleep(timeBetweenChecks);
		} catch (InterruptedException e) {
			// better to swallow this and retry.
		}

		removeHost(host);
	}
	
	public synchronized void removeHost(String host) throws SQLException {

		if (this.connectionGroup != null) {
			if (this.connectionGroup.getInitialHosts().size() == 1
					&& this.connectionGroup.getInitialHosts().contains(host)) {
				throw SQLError.createSQLException(
						"Cannot remove only configured host.", null);
			}

			this.hostToRemove = host;
			
			if (host.equals(this.currentConn.getHost())) {
				closeAllConnections();
			} else {
				this.connectionsToHostsMap.remove(this.liveConnections
						.remove(host));
				Integer idx = this.hostsToListIndexMap.remove(host);
				long[] newResponseTimes = new long[this.responseTimes.length - 1];
				int newIdx = 0;
				for (Iterator<String> i = this.hostList.iterator(); i.hasNext(); newIdx++) {
					String copyHost = i.next();
					if (idx != null
							&& idx.intValue() < this.responseTimes.length) {
						newResponseTimes[newIdx] = this.responseTimes[idx
								.intValue()];
						this.hostsToListIndexMap.put(copyHost,
								Integer.valueOf(newIdx));
					}
				}
				this.responseTimes = newResponseTimes;
			}
		}

	}

	public synchronized boolean addHost(String host) {

		if (this.hostsToListIndexMap.containsKey(host)) {
			return false;
		}
		
		long[] newResponseTimes = new long[this.responseTimes.length + 1];
		
		for (int i = 0; i < this.responseTimes.length; i++) {
			newResponseTimes[i] = this.responseTimes[i];
		}
		
		this.responseTimes = newResponseTimes;
		this.hostList.add(host);
		this.hostsToListIndexMap.put(host,
				Integer.valueOf(this.responseTimes.length - 1));
		
		return true;
	}
	
	public synchronized long getLastUsed(){
		return this.lastUsed;
	}
	
	public synchronized boolean inTransaction(){
		return this.inTransaction;
	}
	
	public synchronized long getTransactionCount(){
		return this.transactionCount;
	}
	
	public synchronized long getActivePhysicalConnectionCount(){
		return this.activePhysicalConnections;
	}
	
	public synchronized long getTotalPhysicalConnectionCount(){
		return this.totalPhysicalConnections;
	}
	
	public synchronized long getConnectionGroupProxyID(){
		return this.connectionGroupProxyID;
	}
	
	public synchronized String getCurrentActiveHost() {
		MySQLConnection c = this.currentConn;
		if(c != null){
			Object o = this.connectionsToHostsMap.get(c);
			if(o != null){
				return o.toString();
			}
		}
		return null;
	}
	
	public synchronized long getCurrentTransactionDuration(){

		if (this.inTransaction && this.transactionStartTime > 0) {
			return getLocalTimeBestResolution() - this.transactionStartTime;
		}
		
		return 0;
	}
	
	protected void syncSessionState(Connection initial, Connection target)
			throws SQLException {
		if (initial == null || target == null) {
			return;
		}
		target.setAutoCommit(initial.getAutoCommit());
		target.setCatalog(initial.getCatalog());
		target.setTransactionIsolation(initial.getTransactionIsolation());
		target.setReadOnly(initial.isReadOnly());

	}

}