/***********************************************************
Copyright (C) 2024 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library 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
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
***********************************************************/
package com.verisign.epp.pool;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.verisign.epp.interfaces.EPPSession;
import com.verisign.epp.util.EPPEnv;
import com.verisign.epp.util.EPPEnvException;
import com.verisign.epp.util.EnvException;
import com.verisign.epp.util.Environment;

/**
 * <i>Singleton</i> EPP session pool that will handle creating sessions
 * dynamically, expiring session after an absolute timeout, keeping sessions
 * alive based on an idle timeout, and dynamically grows and shrinks the pool
 * based on configurable settings. One of the {@code init} methods must be
 * called before useing the pool. The {@link #init()} method uses configuration
 * settings defined in the {@link com.verisign.epp.util.Environment} class. The
 * {@link com.verisign.epp.util.Environment} settings include the following:<br>
 * <br>
 * <ul>
 * <li>EPP.SessionPool.poolableFactoryClassName - EPPSessionPoolableFactory
 * class used by the pool, with a default of EoT poolable factory of
 * "com.verisign.epp.pool.EPPGenericSessionPoolableFactory".
 * <li>EPP.SessionPool.clientId - (required) Login name
 * <li>EPP.SessionPool.password - (required) password
 * <li>EPP.SessionPool.absoluteTimeout - (optional) Session absolute timeout.
 * Default is 24 hours
 * <li>{@code EPP.SessionPool.minAbsoluteTimeout} - Session minimum absolute
 * timeout. If both {@code minAbsoluteTimeout} and {@code maxAbsoluteTimeout} is
 * set, it will override {@code absoluteTimeout} and randomize the session
 * absolute timeout between the {@code minAbsoluteTimeout} and
 * {@code maxAbsoluteTimeout}.
 * <li>{@code EPP.SessionPool.maxAbsoluteTimeout} - Session maximum absolute
 * timeout. If both {@code minAbsoluteTimeout} and {@code maxAbsoluteTimeout} is
 * set, it will override {@code absoluteTimeout} and randomize the session
 * absolute timeout between the {@code minAbsoluteTimeout} and
 * {@code maxAbsoluteTimeout}.
 * <li>EPP.SessionPool.idleTimeout - (optional) Session idle timeout used to
 * determine when keep alive messages are sent. Default is 10 minutes.
 * <li>EPP.SessionPool.maxIdle - (optional) Maximum number of idle sessions in
 * pool. Default is 10.
 * <li>EPP.SessionPool.maxTotal - (optional) Maximum number of active sessions
 * in pool. Default is 10.
 * <li>EPP.SessionPool.initMaxTotal - (optional) Boolean value indicating if the
 * {@code maxTotal} sessions should be pre-initialized at initialization in the
 * {@link #init()} method. Default is {@code false}.
 * <li>EPP.SessionPool.maxWait - (optional) Maximum time in milliseconds for a
 * client to block waiting for a pooled session. Default is 60 seconds.
 * <li>EPP.SessionPool.minIdle - (optional) Minimum number of idle sessions in
 * the pool. Default is 0.
 * <li>EPP.SessionPool.timeBetweenEvictionRunsMillis - (optional) Frequency in
 * milliseconds of scanning the pool for idle and absolute timeout sessions.
 * Default is 60 seconds.
 * <li>EPP.SessionPool.borrowRetries - (optional) Number of retries to
 * get/create a session when calling {@link #borrowObject()}. Default is
 * {@code 0}.
 * </ul>
 */
public class EPPSessionPool {

	/**
	 * The default session absolute timeout.
	 */
	public static final long DEFAULT_ABSOLUTE_TIMEOUT = 24 * 60 * 60 * 1000; // 24
	                                                                         // hours

	/**
	 * The default session absolute timeout.
	 */
	public static final long DEFAULT_IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes

	/**
	 * The default maximum amount of time (in millis) the {@link #borrowObject}
	 * method should block before throwing an exception.
	 */
	public static final long DEFAULT_MAX_WAIT = 60 * 1000; // 60 second blocking
	                                                       // time

	/**
	 * The default "time between eviction runs" value.
	 */
	public static final long DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 1 * 60 * 1000; // Every
	                                                                                    // 1
	                                                                                    // minute

	/**
	 * The default cap on the number of "sleeping" instances in the pool.
	 */
	public static final int DEFAULT_MAX_IDLE = 10;

	/**
	 * The default cap on the total number of instances for the pool.
	 */
	public static final int DEFAULT_MAX_TOTAL = 10;

	/**
	 * The default minimum number of "sleeping" instances in the pool before
	 * before the evictor thread (if active) spawns new objects.
	 */
	public static final int DEFAULT_MIN_IDLE = 0;

	/**
	 * The timeout value is not set.
	 */
	private static long TIMEOUT_UNSET = -1;

	/**
	 * Default for the {@code initMaxTotal} property, which is {@code false}.
	 */
	private static final boolean DEFAULT_INIT_MAX_TOTAL = false;

	/**
	 * Default number of retries when attempting to get/create a session when
	 * calling {@link #borrowObject()}.
	 */
	private static final int DEFAULT_BORROW_RETRIES = 0;

	/**
	 * Session pool property prefix
	 */
	private final static String PROP_PREFIX = "EPP.SessionPool";

	/** Category for logging */
	private static Logger log = LoggerFactory.getLogger(EPPSessionPool.class);

	/**
	 * Real pool being used.
	 */
	private GenericObjectPool<EPPPooledSession> pool = null;

	/**
	 * System pools, where the system name is the key, and the system
	 * {@code GenericObjectPool} is the value.
	 */
	private Map systemPools = new HashMap();

	/**
	 * <i>Singleton</i> instance
	 */
	protected static EPPSessionPool instance = new EPPSessionPool();

	/**
	 * Config used to configure the pool
	 */
	private GenericObjectPoolConfig<EPPPooledSession> config = new GenericObjectPoolConfig<>();

	/**
	 * The factory associated with the pool
	 */
	private EPPSessionPoolableFactory factory;

	/**
	 * Pre-initialize the pool to the {@code maxTotal} setting? This will cause
	 * {@code maxTotal} sessions to be created and added back to the pool. The
	 * default value is {@link DEFAULT_INIT_MAX_TOTAL}.
	 */
	private boolean initMaxTotal = DEFAULT_INIT_MAX_TOTAL;

	/**
	 * Number of retries when attempting to get/create a session when calling
	 * {@link #borrowObject()}. {@link #borrowObject()} will retry
	 * {@code borrowRetries} times for successfully borrowing a session after the
	 * first failure, so a value of {@code 0} will not implement any retries.
	 */
	private int borrowRetries = DEFAULT_BORROW_RETRIES;

	/**
	 * The client identifier used to establish a session
	 */
	private String clientId;

	/**
	 * The password used to establish a session
	 */
	private String password;

	/**
	 * Idle timeout in milliseconds for session. Session will be closed if no
	 * transactions are sent with the session within {@code idleTimeout}
	 * milliseconds.
	 */
	private long idleTimeout;

	/**
	 * Session absolute timeout in milliseconds for all sessions. If both
	 * {@code minAbsoluteTimeout} and {@code maxAbsoluteTimemout} are set, they
	 * will override the setting of the {@code absoluteTimeout}.
	 */
	private long absoluteTimeout = TIMEOUT_UNSET;

	/**
	 * Minimum absolute timeout in milliseconds. The actual absolute timeout will
	 * be randomized between the {@code minAbsoluteTimeout} and
	 * {@code maxAbsoluteTimeout}. If both {@code minAbsoluteTimeout} and
	 * {@code maxAbsoluteTimemout} are set, they will override the setting of the
	 * {@code absoluteTimeout}.
	 */
	private long minAbsoluteTimeout = TIMEOUT_UNSET;

	/**
	 * Maximum absolute timeout in milliseconds. The actual absolute timeout will
	 * be randomized between the {@code minAbsoluteTimeout} and
	 * {@code maxAbsoluteTimeout}. If both {@code minAbsoluteTimeout} and
	 * {@code maxAbsoluteTimemout} are set, they will override the setting of the
	 * {@code absoluteTimeout}.
	 */
	private long maxAbsoluteTimeout = TIMEOUT_UNSET;

	/**
	 * Name or IP address of TCP server or URL of HTTP server.
	 */
	private String serverName;

	/**
	 * Port number of TCP server. This attribute should be {@code null} when
	 * connecting to a HTTP server.
	 */
	private Integer serverPort;

	/**
	 * Name or IP address to connect from. When {@code null} the host will be set
	 * to the loop back.
	 */
	private String clientHost = null;

	/**
	 * Initialize the session with the call to
	 * {@code EPPSessionPoolableFactory#makeObject()} with a default value of
	 * {@code true}. This attribute also impacts the automatic call to end the
	 * session with the call to
	 * {@link EPPSessionPoolableFactory#destroyObject(PooledObject)}.
	 */
	private boolean initSessionOnMake = true;

	/**
	 * Default constructor as part of the <i>Singleton Design Pattern</i>.
	 */
	protected EPPSessionPool() {
	}

	/**
	 * Gets the <i>Singleton Design Pattern</i> instance. Ensure the
	 * {@link #init()} is called at least once.
	 *
	 * @return Singleton instance of {@code EPPSessionPool}.
	 */
	public static EPPSessionPool getInstance() {
		return instance;
	}

	/**
	 * Initialize the pool with a specific {@code EPPSessionPoolableFactory} and
	 * {@code GenericObjectPoolConfig} setting.
	 *
	 * @param aFactory
	 *           EPP session poolable object factory
	 * @param aConfig
	 *           Configuration attributes for pool
	 */
	public void init(EPPSessionPoolableFactory aFactory, GenericObjectPoolConfig<EPPPooledSession> aConfig) {
		this.pool = new GenericObjectPool<EPPPooledSession>(aFactory, aConfig);
	}

	public void init() throws EPPSessionPoolException {
		try {
			String theValue = this.getProperty("systemPools");

			if (theValue == null) {
				log.info("Initializing a single session pool");
				this.initSinglePool();
			}
			else {
				log.info("Initializing system session pools with systems = " + theValue);
				this.initSystemPools(theValue);
			}
		}
		catch (EnvException ex) {
			throw new EPPSessionPoolException("init(): EnvException: " + ex);
		}

	}

	/**
	 * Closes the session pool(s) contained in {@code EPPSessionPool} cleanly.
	 * Cleanly closing the session pools means clearing the pools that will
	 * execute an EPP logout for each of the idle sessions and close the pool.
	 */
	public void close() {
		log.info("close(): closing pool");

		// The default pool exists?
		if (this.pool != null) {
			// Clear and close the current pool
			this.pool.clear();
			try {
				this.pool.close();
			}
			catch (Exception ex) {
				log.error("EPPSessionPool.close(): Exception closing default pool <" + this.pool + ">: " + ex);
			}
		}

		// The system pools exist?
		if (this.systemPools != null) {
			// Close each of the system pools

			Set theSystemKeys = this.systemPools.keySet();
			Iterator theSystemKeysIter = theSystemKeys.iterator();
			while (theSystemKeysIter.hasNext()) {
				String theCurrKey = (String) theSystemKeysIter.next();
				EPPSystemSessionPool theCurrPool = (EPPSystemSessionPool) this.systemPools.get(theCurrKey);
				theCurrPool.close();
			} // end while (theSystemKeysIter.hasNext())

		} // end if (this.systemPools != null)

		log.info("close(): pool closed");
	}

	/**
	 * Name of the default system session pool, which uses the
	 * {@code borrowObject()}, {@code returnObject(EPPSession)}, and
	 * {@code invalidateObject(EPPSession)}.
	 */
	public static final String DEFAULT = "default";

	/**
	 * Initializes the system session pools given the value of the system pools
	 * property, which is a comma seperated list of system names.
	 *
	 * @param aPoolsProp
	 *           Comma seperated list of system names
	 *
	 * @throws EPPSessionPoolException
	 *            Error initializing pools
	 */
	private void initSystemPools(String aPoolsProp) throws EPPSessionPoolException {
		log.debug("initSystemPools: enter, aPoolProp = " + aPoolsProp);
		StringTokenizer thePools = new StringTokenizer(aPoolsProp, ",");

		// For each system name
		while (thePools.hasMoreTokens()) {
			// Create system session pool and add to systemPools attribute
			String theSystem = thePools.nextToken();
			if (theSystem.equals(DEFAULT)) {
				log.info("initSystemPools: Initializing the default pool");
				this.initSinglePool();
			}
			else {
				log.info("initSystemPools: Initializing " + theSystem + " system pool");
				EPPSystemSessionPool theSessionPool = new EPPSystemSessionPool(theSystem);
				theSessionPool.init();
				this.systemPools.put(theSystem, theSessionPool);
			}
		}
		log.debug("initSystemPools: exit");
	}

	/**
	 * Initialize a single pool using configuration values defined by
	 * {@link com.verisign.epp.util.Environment} class. The
	 * {@link com.verisign.epp.util.Environment} class and logging must be
	 * initialized before calling this method.
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	private void initSinglePool() throws EPPSessionPoolException {
		log.debug("initSinglePool: enter");

		// Get configuration settings for pool
		try {
			String theValue;

			// poolableFactoryClassName
			theValue = this.getProperty("poolableFactoryClassName");
			if (theValue == null) {
				theValue = "com.verisign.epp.pool.EPPGenericSessionPoolableFactory";
			}
			log.info("initSinglePool(): session poolable factory = " + theValue);
			Class thePoolableFactoryClass = Class.forName(theValue);
			this.factory = (EPPSessionPoolableFactory) thePoolableFactoryClass.getDeclaredConstructor().newInstance();

			// clientId
			this.clientId = this.getProperty("clientId");
			if (this.clientId == null) {
				log.error("initSinglePool(): clientId not defined");
				throw new EPPSessionPoolException("clientId not defined");
			}

			// password
			this.password = this.getProperty("password");
			if (this.password == null) {
				log.error("initSinglePool(): password not defined");
				throw new EPPSessionPoolException("password not defined");
			}

			// absoluteTimeout
			theValue = this.getProperty("absoluteTimeout");
			if (theValue != null) {
				this.absoluteTimeout = Long.parseLong(theValue);
				log.info("initSinglePool(): absolute timeout = " + this.absoluteTimeout + " ms");
			}
			else {
				this.absoluteTimeout = DEFAULT_ABSOLUTE_TIMEOUT;
				log.info("initSinglePool(): default absolute timeout = " + this.absoluteTimeout + " ms");
			}

			// minAbsoluteTimout
			theValue = this.getProperty("minAbsoluteTimeout");
			if (theValue != null) {
				this.minAbsoluteTimeout = Long.parseLong(theValue);
				log.info("initSinglePool(): min absolute timeout = " + this.minAbsoluteTimeout + " ms");
			}
			else {
				log.info("initSinglePool(): min absolute timeout not set");
			}

			// minAbsoluteTimout
			theValue = this.getProperty("maxAbsoluteTimeout");
			if (theValue != null) {
				this.maxAbsoluteTimeout = Long.parseLong(theValue);
				log.info("initSinglePool(): max absolute timeout = " + this.maxAbsoluteTimeout + " ms");
			}
			else {
				log.info("initSinglePool(): max absolute timeout not set");
			}

			// idleTimeout
			theValue = this.getProperty("idleTimeout");
			if (theValue != null) {
				this.idleTimeout = Long.parseLong(theValue);
			}
			else {
				this.idleTimeout = DEFAULT_IDLE_TIMEOUT;
			}
			log.info("initSinglePool(): idle timeout = " + this.idleTimeout + " ms");

			// clientTransIdGenerator
			theValue = this.getProperty("clientTransIdGenerator");
			log.info("initSinglePool(): client trans id generator = " + theValue);

			if (theValue != null) {
				try {
					this.factory.setClientTransIdGenerator(
					      (EPPClientTransIdGenerator) Class.forName(theValue).getDeclaredConstructor().newInstance());
				}
				catch (Exception ex) {
					log.error("initSinglePool(): Exception creating instance of class " + theValue + ": " + ex);
					throw new EPPSessionPoolException("Exception creating instance of class " + theValue + ": " + ex);
				}
			}

			// Ensure minEvictableIdleTimeMillis is disabled
			this.config.setMinEvictableIdleTimeMillis(0);

			// maxIdle
			theValue = this.getProperty("maxIdle");
			if (theValue != null) {
				this.config.setMaxIdle(Integer.parseInt(theValue));
			}
			else {
				this.config.setMaxIdle(DEFAULT_MAX_IDLE);
			}
			log.info("initSinglePool(): max idle = " + this.config.getMaxIdle());

			// maxTotal
			theValue = this.getProperty("maxTotal");
			if (theValue == null) {
				// Included for backward compatibility with old configurations.
				theValue = this.getProperty("maxActive");
			}
			if (theValue != null) {
				this.config.setMaxTotal(Integer.parseInt(theValue));
			}
			else {
				this.config.setMaxTotal(DEFAULT_MAX_TOTAL);
			}
			log.info("initSinglePool(): max Total = " + this.config.getMaxTotal());

			// initMaxTotal
			theValue = this.getProperty("initMaxTotal");
			if (theValue == null) {
				// Included for backward compatibility with old configurations.
				theValue = this.getProperty("initMaxActive");
			}
			if (theValue != null) {
				this.initMaxTotal = (Boolean.valueOf(theValue)).booleanValue();
			}
			else {
				this.initMaxTotal = DEFAULT_INIT_MAX_TOTAL;
			}
			log.info("initSinglePool(): init max Total = " + this.initMaxTotal);

			// borrowRetries
			theValue = this.getProperty("borrowRetries");
			if (theValue != null) {
				this.borrowRetries = Integer.parseInt(theValue);
			}
			else {
				this.borrowRetries = DEFAULT_BORROW_RETRIES;
			}
			log.info("initSinglePool(): borrow retries = " + this.borrowRetries);

			// maxWait
			theValue = this.getProperty("maxWait");
			if (theValue != null) {
				this.config.setMaxWaitMillis(Integer.parseInt(theValue));
			}
			else {
				this.config.setMaxWaitMillis(DEFAULT_MAX_WAIT);
			}
			log.info("initSinglePool(): max wait = " + this.config.getMaxWaitMillis());

			// minIdle
			theValue = this.getProperty("minIdle");
			if (theValue != null) {
				this.config.setMinIdle(Integer.parseInt(theValue));
			}
			else {
				this.config.setMinIdle(DEFAULT_MIN_IDLE);
			}
			log.info("initSinglePool(): min idle = " + this.config.getMinIdle());

			// numTestsPerEvictionRun
			this.config.setNumTestsPerEvictionRun(-1); // This will cause all
			                                           // sessions to be tested

			// testOnBorrow
			this.config.setTestOnBorrow(false);

			// testOnReturn
			this.config.setTestOnReturn(false);

			// testWhileIdle
			this.config.setTestWhileIdle(true);

			// timeBetweenEvictionRunsMillis
			theValue = this.getProperty("timeBetweenEvictionRunsMillis");
			if (theValue != null) {
				this.config.setTimeBetweenEvictionRunsMillis(Long.parseLong(theValue));
			}
			else {
				this.config.setTimeBetweenEvictionRunsMillis(DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
			}
			log.info("initSinglePool(): time between eviction runs = " + this.config.getTimeBetweenEvictionRunsMillis()
			      + " ms");

			// blockWhenExhausted
			this.config.setBlockWhenExhausted(true);

			// initSessionOnMake
			theValue = this.getProperty("initSessionOnMake");
			if (theValue != null) {
				this.initSessionOnMake = Boolean.parseBoolean(theValue);
			}
			log.info("initSinglePool(): initSessionOnMake = " + this.initSessionOnMake);

			// serverName
			this.serverName = EPPEnv.getServerName();
			log.info("initSinglePool(): serverName = " + this.serverName);

			// serverPort
			theValue = Environment.getOption("EPP.ServerPort");
			if (theValue != null  && !theValue.isEmpty()) {
				this.serverPort = Integer.valueOf(theValue);
			}
			log.info("initSinglePool(): serverPort = " + this.serverPort);

			// clientHost
			this.clientHost = EPPEnv.getClientHost();
			log.info("initSinglePool(): clientHost = " + this.clientHost);

		}
		catch (Exception ex) {
			ex.printStackTrace();
			log.error("initSinglePool(): Exception with initializing the single session pool: " + ex);
			throw new EPPSessionPoolException("Exception with initializing the single session pool: " + ex);
		}

		// Set factory required attributes
		this.factory.setAbsoluteTimeout(this.absoluteTimeout);
		this.factory.setMinAbsoluteTimeout(this.minAbsoluteTimeout);
		this.factory.setMaxAbsoluteTimeout(this.maxAbsoluteTimeout);
		this.factory.setIdleTimeout(this.idleTimeout);
		this.factory.setClientId(this.clientId);
		this.factory.setPassword(this.password);
		this.factory.setServerName(this.serverName);
		this.factory.setServerPort(this.serverPort);
		this.factory.setClientHost(this.clientHost);

		this.init(this.factory, this.config);

		// Pre-initialize maxTotal sessions in pool?
		if (this.initMaxTotal && this.config.getMaxTotal() > 0) {
			log.info("initSinglePool(): Pre-initialize maxTotal (" + this.config.getMaxTotal() + ") sessions");

			EPPSession theSessions[] = new EPPSession[this.config.getMaxTotal()];

			// Borrow maxTotal sessions from pool
			for (int i = 0; i < this.config.getMaxTotal(); i++) {

				try {
					theSessions[i] = this.borrowObject();
					log.info("initSinglePool(): Pre-initialized session #" + (i + 1));
				}
				catch (EPPSessionPoolException ex) {
					log.error("initSinglePool(): Failure to pre-initialize session #" + (i + 1) + ": " + ex);
				}
			}

			// Return maxTotal sessions back to pool
			for (int i = 0; i < this.config.getMaxTotal(); i++) {
				if (theSessions[i] != null) {
					this.returnObject(theSessions[i]);
					theSessions[i] = null;
				}
			}

		}

		log.debug("initSinglePool: exit");
	}

	/**
	 * Borrows a session from the pool. The session must be returned by either
	 * calling {@link #invalidateObject(com.verisign.epp.interfaces.EPPSession)}
	 * or {@link #returnObject(com.verisign.epp.interfaces.EPPSession)}. This
	 * method will block if there are no idle sessions in the pool for
	 * {@code maxWait} time.
	 *
	 * @return Borrowed {@code EPPSession} instance.
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public EPPSession borrowObject() throws EPPSessionPoolException {
		if (this.pool == null) {
			log.error("borrowObject(): pool is null");
			throw new EPPSessionPoolException("EPPSessionPool: pool is null");
		}

		EPPSession theSession = null;

		// Attempt to borrow session until successful or retries have exceeded.
		for (int retries = 0; theSession == null && retries <= this.borrowRetries; retries++) {
			try {
				theSession = (EPPSession) this.pool.borrowObject();

				log.debug("borrowObject(): Session = " + theSession + ", Total = " + this.pool.getNumActive() + ", Idle = "
				      + this.pool.getNumIdle());
			}
			catch (Exception ex) {

				// Number of retries exceeded?
				if (retries >= this.borrowRetries) {

					// Throw exception to indicate borrow failure
					log.error("borrowObject(): Final exception on borrow session after " + retries + " retries: " + ex);
					throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
				}
				else {
					// Continue retrying
					log.debug("borrowObject(): Exception on borrow session after " + retries + " retries: " + ex);
				}

			}

		}

		return theSession;
	}

	/**
	 * Borrows a session from the pool. The session must be returned by either
	 * calling {@link #invalidateObject(com.verisign.epp.interfaces.EPPSession)}
	 * or {@link #returnObject(com.verisign.epp.interfaces.EPPSession)}. This
	 * method will block if there are no idle sessions in the pool for
	 * {@code maxWait} time.
	 *
	 * @param aSystem
	 *           the system name
	 *
	 * @return Borrowed {@code EPPSession} instance.
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public EPPSession borrowObject(String aSystem) throws EPPSessionPoolException {
		// Default pool?
		if (aSystem != null && aSystem.equals(DEFAULT)) {
			return this.borrowObject();
		}

		EPPSystemSessionPool thePool = this.getSystemSessionPool(aSystem);

		EPPSession theSession = null;
		try {
			theSession = thePool.borrowObject();
			log.debug("borrowObject(" + aSystem + "): Session = " + theSession + ", Total = "
			      + thePool.getGenericObjectPool().getNumActive() + ", Idle = "
			      + thePool.getGenericObjectPool().getNumIdle());
		}
		catch (Exception ex) {
			log.error("borrowObject(" + aSystem + "): Caught Exception: " + ex);
			throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
		}

		return theSession;
	}

	/**
	 * Remove a borrowed session from the pool based on a known issue with it.
	 * The should be done if an unexpected exception occurs with the session
	 * which might be due to the server being down or the session being expired.
	 *
	 * @param aSession
	 *           Session that is invalid
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public void invalidateObject(EPPSession aSession) throws EPPSessionPoolException {
		if (aSession == null) {
			log.error("invalidateObject(" + aSession + "): session is null");
			throw new EPPSessionPoolException("EPPSessionPool: session is null");
		}
		if (!(aSession instanceof EPPPooledSession)) {
			log.error("invalidateObject(" + aSession + "): session not of type " + EPPPooledSession.class.getName());
			throw new EPPSessionPoolException("EPPSessionPool: session not of type " + EPPPooledSession.class.getName());
		}

		if (this.pool == null) {
			log.error("invalidateObject(" + aSession + "): pool is null");
			throw new EPPSessionPoolException("EPPSessionPool: pool is null");
		}

		try {
			this.pool.invalidateObject((EPPPooledSession) aSession);
			log.debug("invalidateObject(" + aSession + "): Total = " + this.pool.getNumActive() + ", Idle = "
			      + this.pool.getNumIdle());
		}
		catch (Exception ex) {
			log.error("invalidateObject(" + aSession + "): Caught Exception: " + ex);
			throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
		}
	}

	/**
	 * Remove a borrowed session from the system session pool based on a known
	 * issue with it. The should be done if an unexpected exception occurs with
	 * the session which might be due to the server being down or the session
	 * being expired.
	 *
	 * @param aSystem
	 *           the system name
	 * @param aSession
	 *           Session that is invalid
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public void invalidateObject(String aSystem, EPPSession aSession) throws EPPSessionPoolException {
		if (aSession == null) {
			log.error("invalidateObject(" + aSystem + ", " + aSession + "): session is null");
			throw new EPPSessionPoolException("EPPSessionPool: session is null");
		}
		if (!(aSession instanceof EPPPooledSession)) {
			log.error("invalidateObject(" + aSystem + ", " + aSession + "): session not of type "
			      + EPPPooledSession.class.getName());
			throw new EPPSessionPoolException("EPPSessionPool: session not of type " + EPPPooledSession.class.getName());
		}

		// Default pool?
		if (aSystem != null && aSystem.equals(DEFAULT)) {
			this.invalidateObject(aSession);
			return;
		}

		EPPSystemSessionPool thePool = this.getSystemSessionPool(aSystem);

		try {
			thePool.invalidateObject(aSession);
			log.debug("invalidateObject(" + aSystem + ", " + aSession + "): Total = "
			      + thePool.getGenericObjectPool().getNumActive() + ", Idle = "
			      + thePool.getGenericObjectPool().getNumIdle());
		}
		catch (Exception ex) {
			log.error("invalidateObject(" + aSystem + ", " + aSession + "): Caught Exception: " + ex);
			throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
		}
	}

	/**
	 * Returned a borrowed session to the pool. This session must have been
	 * returned from a call to {@link #borrowObject()}.
	 *
	 * @param aSession
	 *           Session to return
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public void returnObject(EPPSession aSession) throws EPPSessionPoolException {
		if (aSession == null) {
			log.error("returnObject(" + aSession + "): session is null");
			throw new EPPSessionPoolException("EPPSessionPool: session is null");
		}
		if (!(aSession instanceof EPPPooledSession)) {
			log.error("returnObject(" + aSession + "): session not of type " + EPPPooledSession.class.getName());
			throw new EPPSessionPoolException("EPPSessionPool: session not of type " + EPPPooledSession.class.getName());
		}

		if (this.pool == null) {
			log.error("returnObject(" + aSession + "): pool is null");
			throw new EPPSessionPoolException("EPPSessionPool: pool is null");
		}

		// Touch on return
		this.touchSession(aSession);

		try {
			this.pool.returnObject((EPPPooledSession) aSession);
			log.debug("returnObject(" + aSession + "): Total = " + this.pool.getNumActive() + ", Idle = "
			      + this.pool.getNumIdle());
		}
		catch (Exception ex) {
			log.error("returnObject(" + aSession + "): Caught Exception: " + ex);
			throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
		}

	}

	/**
	 * Returned a borrowed session to a system session pool. This session must
	 * have been returned from a call to {@link #borrowObject(String)}.
	 *
	 * @param aSystem
	 *           the system name
	 * @param aSession
	 *           Session to return
	 *
	 * @throws EPPSessionPoolException
	 *            On error
	 */
	public void returnObject(String aSystem, EPPSession aSession) throws EPPSessionPoolException {
		if (aSession == null) {
			log.error("returnObject(" + aSystem + ", " + aSession + "): session is null");
			throw new EPPSessionPoolException("EPPSessionPool: session is null");
		}
		if (!(aSession instanceof EPPPooledSession)) {
			log.error("returnObject(" + aSystem + ", " + aSession + "): session not of type "
			      + EPPPooledSession.class.getName());
			throw new EPPSessionPoolException("EPPSessionPool: session not of type " + EPPPooledSession.class.getName());
		}

		// Default pool?
		if (aSystem != null && aSystem.equals(DEFAULT)) {
			this.returnObject(aSession);
			return;
		}

		EPPSystemSessionPool thePool = this.getSystemSessionPool(aSystem);

		// Touch on return
		this.touchSession(aSession);

		try {
			thePool.returnObject(aSession);
			log.debug("returnObject(" + aSystem + ", " + aSession + "): Total = "
			      + thePool.getGenericObjectPool().getNumActive() + ", Idle = "
			      + thePool.getGenericObjectPool().getNumIdle());
		}
		catch (Exception ex) {
			log.error("returnObject(" + aSystem + ", " + aSession + "): Caught Exception: " + ex);
			throw new EPPSessionPoolException("EPPSessionPool: Exception " + ex);
		}

	}

	/**
	 * Gets the contained {@code GenericObjectPool}.
	 *
	 * @return Contained {@code GenericObjectPool} if defined; {@code null}
	 *         otherwise.
	 */
	public GenericObjectPool<EPPPooledSession> getGenericObjectPool() {
		return this.pool;
	}

	/**
	 * Does the system session pool exist?
	 *
	 * @param aSystem
	 *           System session pool name to find
	 *
	 * @return {@code true} if the system session pool exists; {@code false}
	 *         otherwise.
	 */
	public boolean hasSystemSessionPool(String aSystem) {
		return (this.systemPools.get(aSystem) != null ? true : false);
	}

	/**
	 * Gets the contained {@code EPPSystemSessionPool} for a system.
	 *
	 * @param aSystem
	 *           System name for pool
	 *
	 * @return Contained {@code EPPSystemSessionPool}.
	 *
	 * @exception EPPSessionPoolException
	 *               When system pool can not be found
	 */
	public EPPSystemSessionPool getSystemSessionPool(String aSystem) throws EPPSessionPoolException {
		EPPSystemSessionPool theSystemPool = (EPPSystemSessionPool) this.systemPools.get(aSystem);

		if (theSystemPool == null) {
			log.error("getGenericObjectPool(): Could not find system pool " + aSystem);
			throw new EPPSessionPoolException("Could not find system pool " + aSystem);
		}

		return theSystemPool;
	}

	/**
	 * Gets the contained {@code GenericObjectPool} for a system.
	 *
	 * @param aSystem
	 *           System name for pool
	 *
	 * @return Contained {@code GenericObjectPool}.
	 *
	 * @exception EPPSessionPoolException
	 *               When system pool can not be found
	 */
	public GenericObjectPool<EPPPooledSession> getGenericObjectPool(String aSystem) throws EPPSessionPoolException {

		EPPSystemSessionPool theSystemPool = this.getSystemSessionPool(aSystem);

		if (theSystemPool.getGenericObjectPool() == null) {
			log.error("getGenericObjectPool(): GenericObjectPool is null for system pool " + aSystem);
			throw new EPPSessionPoolException("GenericObjectPool is null for system pool " + aSystem);

		}

		return theSystemPool.getGenericObjectPool();
	}

	/**
	 * Gets the session absolute timeout.
	 *
	 * @return Returns the absolute timeout in milliseconds.
	 */
	public long getAbsoluteTimeout() {
		return this.absoluteTimeout;
	}

	/**
	 * Gets the minimum session absolute timeout in milliseconds. If both
	 * {@code minAbsoluteTimeout} and {@code maxAbsoluteTimemout} are set, they
	 * will override the setting of {@code absoluteTimeout}.
	 *
	 * @return Minimum absolute timeout in milliseconds
	 */
	public long getMinAbsoluteTimeout() {
		return this.minAbsoluteTimeout;
	}

	/**
	 * Gets the maximum session absolute timeout in milliseconds. If both
	 * {@code minAbsoluteTimeout} and {@code maxAbsoluteTimemout} are set, they
	 * will override the setting of {@code absoluteTimeout}.
	 *
	 * @return Maximum absolute timeout in milliseconds
	 */
	public long getMaxAbsoluteTimeout() {
		return this.maxAbsoluteTimeout;
	}

	/**
	 * Returns whether the absolute timeout will be randomized between the
	 * {@code minAbsoluteTimeout} and {@code maxAbsoluteTimemout}.
	 * 
	 * @return {@code true} if the absolute timeout will be randomized;
	 *         {@code false} otherwise.
	 */
	public boolean isRandomAbsoluteTimeout() {
		if ((this.minAbsoluteTimeout != TIMEOUT_UNSET && this.maxAbsoluteTimeout != TIMEOUT_UNSET)
		      && (this.maxAbsoluteTimeout > this.minAbsoluteTimeout)) {
			return true;
		}
		else {
			return false;
		}

	}

	/**
	 * Gets the client identifier used to authenticate.
	 *
	 * @return Returns the client identifier.
	 */
	public String getClientId() {
		return this.clientId;
	}

	/**
	 * Gets the configuration for the {@code GenericObjectPool}.
	 *
	 * @return Returns the config.
	 */
	public GenericObjectPoolConfig<EPPPooledSession> getConfig() {
		return this.config;
	}

	/**
	 * Gets the factory associated with the pool.
	 *
	 * @return Returns the factory.
	 */
	public EPPSessionPoolableFactory getFactory() {
		return this.factory;
	}

	/**
	 * Gets the session idle timeout.
	 *
	 * @return Returns the idle timeout in milliseconds.
	 */
	public long getIdleTimeout() {
		return this.idleTimeout;
	}

	/**
	 * Gets the password used for authentication.
	 *
	 * @return Returns the password.
	 */
	public String getPassword() {
		return this.password;
	}

	/**
	 * Gets the TCP server IP address or host name, or the URL of the HTTP
	 * server.
	 *
	 * @return Server host name, IP address, or URL
	 */
	public String getServerName() {
		return this.serverName;
	}

	/**
	 * Sets the TCP server IP address or host name or the URL of the HTTP server.
	 *
	 * @param aServerName
	 *           Server host name, IP address, or URL
	 */
	public void setServerName(String aServerName) {
		this.serverName = aServerName;
	}

	/**
	 * Gets the TCP server port number. This will be {@code null} if connecting
	 * to a HTTP server.
	 *
	 * @return TCP server port number if defined; {@code null} otherwise.
	 */
	public Integer getServerPort() {
		return this.serverPort;
	}

	/**
	 * Sets the TCP server port number.
	 *
	 * @param aServerPort
	 *           TCP server port number
	 */
	public void setServerPort(Integer aServerPort) {
		this.serverPort = aServerPort;
	}

	/**
	 * Gets the TCP server IP address or host name to connect from. A
	 * {@code null} value will use the loop back.
	 *
	 * @return Client host name or IP address if defined;{@code null} otherwise.
	 */
	public String getClientHost() {
		return this.clientHost;
	}

	/**
	 * Sets the TCP server IP address or host name to connect from. A
	 * {@code null} value will use the loop back.
	 *
	 * @param aClientHost
	 *           Client host name or IP address
	 */
	public void setClientHost(String aClientHost) {
		this.clientHost = aClientHost;
	}

	/**
	 * Initialize the session via an EPP login on the call to
	 * {@link EPPSessionPoolableFactory#makeObject()}? The default value is
	 * {@code true}. This also impacts executing end session via the EPP logout
	 * on the call to
	 * {@link EPPSessionPoolableFactory#destroyObject(PooledObject)}.
	 * 
	 * @return {@code true} the session will be initialized via an EPP login on
	 *         the call to {@link EPPSessionPoolableFactory#makeObject()};
	 *         {@code false} otherwise
	 */
	public boolean isInitSessionOnMake() {
		return initSessionOnMake;
	}

	/**
	 * Set whether to initialize the session via an EPP login on the call to
	 * {@link EPPSessionPoolableFactory#makeObject()}. The default value is
	 * {@code true}. This also impacts executing end session via the EPP logout
	 * on the call to
	 * {@link EPPSessionPoolableFactory#destroyObject(PooledObject)}.
	 * 
	 * @param aInitSessionOnMake
	 *           {@code true} the session will be initialized via an EPP login on
	 *           the call to {@link EPPSessionPoolableFactory#makeObject()};
	 *           {@code false} otherwise
	 */
	public void setInitSessionOnMake(boolean aInitSessionOnMake) {
		this.initSessionOnMake = aInitSessionOnMake;
	}

	/**
	 * Gets an environment property associated with the system.
	 *
	 * @param aProperty
	 *           The property name without the EPP.SessionPool.&lt;session&gt;.
	 *           prefix.
	 *
	 * @return Property value if defined; {@code null} otherwise.
	 */
	private String getProperty(String aProperty) throws EnvException {
		return Environment.getProperty(PROP_PREFIX + "." + aProperty);
	}

	/**
	 * Touches the session to ensure that an idle timeout does not occur.
	 *
	 * @param aSession
	 *           Session to touch
	 * @throws EPPSessionPoolException
	 *            Invalid session passed
	 */
	private void touchSession(EPPSession aSession) throws EPPSessionPoolException {
		if (!(aSession instanceof EPPPooledSession)) {
			log.error("touchSession(" + aSession + "): Session class " + aSession.getClass().getName()
			      + " does not implement EPPPooledSession");
			throw new EPPSessionPoolException("EPPSessionPool: Session class " + aSession.getClass().getName()
			      + " does not implement EPPPooledSession");
		}

		// Touch the session
		((EPPPooledSession) aSession).touch();
	}

}