/***********************************************************
Copyright (C) 2004 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 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.transport.EPPConException;
import com.verisign.epp.transport.client.EPPSSLConfig;
import com.verisign.epp.transport.client.EPPSSLContext;
import com.verisign.epp.transport.client.EPPSSLImpl;

import com.verisign.epp.util.Environment;

/**
 * EPP session pool for a specific system name 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
 * based on the system name defined by &lt;system&gt;:<br>
 * <br>
 * <ul>
 * <li>{@code EPP.SessionPool.&lt;system&gt;.poolableClassName} - (required)
 * EPPSessionPoolableFactory class used by the pool.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.clientId} - (required) Login name
 * <li>{@code EPP.SessionPool.&lt;system&gt;.password} - (required) password
 * <li>{@code EPP.SessionPool.&lt;system&gt;.serverName} - Name or IP address of
 * TCP server or URL of HTTP server
 * <li>{@code EPP.SessionPool.&lt;system&gt;.serverPort} - (optional) TCP server
 * port number. This is not used for a HTTP server connection
 * <li>{@code EPP.SessionPool.&lt;system&gt;.absoluteTimeout} - (optional)
 * Session absolute timeout. Default is 24 hours.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.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.&lt;system&gt;.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>{@code EPP.SessionPool.&lt;system&gt;.idleTimeout} - (optional) Session
 * idle timeout used to determine when keep alive messages are sent. Default is
 * 10 minutes.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.maxIdle} - (optional) Maximum
 * number of idle sessions in pool. Default is 10.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.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>{@code EPP.SessionPool.&lt;system&gt;.maxTotal} - (optional) Maximum
 * number of active sessions in pool. Default is 10.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.maxWait} - (optional) Maximum time
 * in milliseconds for a client to block waiting for a pooled session. Default
 * is 60 seconds.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.minIdle} - (optional) Minimum
 * number of idle sessions in the pool. Default is 0.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.timeBetweenEvictionRunsMillis} -
 * (optional) Frequency in milliseconds of scanning the pool for idle and
 * absolute timeout sessions. Default is 60 seconds.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.borrowRetries} - (optional) Number
 * of retries to get/create a session when calling {@link #borrowObject()}.
 * Default is {@code 0}.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLProtocol} - (optional) SSL
 * protocol to use. If defined the pool will have its own SSL configuration. The
 * required SSL properties include {@code SSLKeyStore}, {@code SSLKeyFileName},
 * and {@code SSLKeyPassPhrase}
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLKeyStore} - (optional) Type of
 * identity KeyStore. Required if {@code SSLProtocol} is defined for pool.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLKeyFileName} - (optional) Name
 * of the identity KeyStore file. Required if {@code SSLProtocol} is defined for
 * pool.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLPassPhrase} - (optional) The
 * passphrase/password to access the identity KeyStore file defined by
 * {@code SSLKeyFileName}. Required if {@code SSLProtocol} is defined for pool.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLKeyPassPhrase} - (optional) the
 * passphrase/password for the private key stored in the identity KeyStore.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLTrustStore} - (optional)
 * KeyStore type of the Trust Store
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLTrustStoreFileName} - (optional)
 * The name of the Trust Store file. If not defined for the pool, the default
 * JDK Trust Store will be used that is located at the path
 * {@code $JAVA_HOME/lib/security/cacerts}.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLTrustStorePassPhrase} -
 * (optional) The passphrase/password to access the Trust Store file defined by
 * the pool {@code SSLTrustStoreFileName} property.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLEnabledProtocols} - (optional)
 * The space delimited list of enabled SSL protocols.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLEnabledCipherSuites} -
 * (optional) The space delimited list of SSL cipher suites.
 * <li>{@code EPP.SessionPool.&lt;system&gt;.SSLDebug} - (optional) Defines the
 * SSL debug Java system property {@code javax.net.debug} value.
 * </ul>
 */
public class EPPSystemSessionPool {

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

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

	/**
	 * 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 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(EPPSystemSessionPool.class);

	/**
	 * SSL context information
	 */
	private EPPSSLContext sslContext = null;

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

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

	/**
	 * The factory associated with the pool
	 */
	private EPPSessionPoolableFactory factory = new EPPSessionPoolableFactory();

	/**
	 * 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 of system associated with this pool.
	 */
	private String system;

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

	/**
	 * Default constructor as part of the <i>Singleton Design Pattern</i>.
	 */
	EPPSystemSessionPool(String aSystem) {
		this.system = aSystem;
	}

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

	}

	/**
	 * Initialize the 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
	 */
	public void init() throws EPPSessionPoolException {

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

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

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

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

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

			// minAbsoluteTimout
			theValue = this.getProperty("maxAbsoluteTimeout");
			if (theValue != null) {
				this.maxAbsoluteTimeout = Long.parseLong(theValue);
				log.info("init(): system = " + this.system + ": max absolute timeout = " + this.maxAbsoluteTimeout + " ms");
			}
			else {
				log.info("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": idle timeout = " + this.idleTimeout + " ms");

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

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

			// serverName
			this.serverName = this.getProperty("serverName");
			if (this.serverName == null) {
				log.error("EPPSystemSessionPool.init(): system = " + this.system + ": serverName not defined");
				throw new EPPSessionPoolException("serverName not defined");
			}
			log.info("init(): system = " + this.system + ": serverName = " + this.serverName);

			// serverPort
			theValue = this.getProperty("serverPort");
			if (theValue != null) {
				this.serverPort = Integer.valueOf(theValue);
			}
			log.info("init(): system = " + this.system + ": serverPort = " + this.serverPort);

			// clientHost
			this.clientHost = this.getProperty("clientHost");
			log.info("init(): system = " + this.system + ": clientHost = " + this.clientHost);

			// 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": time between eviction runs = "
			      + this.config.getTimeBetweenEvictionRunsMillis() + " ms");

			// blockWhenExhausted
			this.config.setBlockWhenExhausted(true);
		}
		catch (Exception ex) {
			log.error("EPPSystemSessionPool.init(): system = " + this.system
			      + ": Exception referencing Environment property: " + ex);
			throw new EPPSessionPoolException("Exception referencing Environment property: " + 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);

		// Does pool have its own SSL configuration?
		if (this.getProperty("SSLProtocol") != null) {

			// Create EPPSSLConfig with required properties
			EPPSSLConfig theConfig = new EPPSSLConfig(this.getProperty("SSLProtocol"), this.getProperty("SSLKeyStore"),
			      this.getProperty("SSLKeyFileName"), this.getProperty("SSLPassPhrase"));

			// Set optional EPPSSLConfig properties
			theConfig.setIdentityKeyPassPhrase(this.getProperty("SSLKeyPassPhrase"));
			theConfig.setSslDebug(this.getProperty("SSLDebug"));

			theConfig.setTrustStore(this.getProperty("SSLTrustStore"), this.getProperty("SSLTrustStoreFileName"),
			      this.getProperty("SSLTrustStorePassPhrase"));

			theConfig.setSSLEnabledProtocols(this.getProperty("SSLEnabledProtocols"));
			theConfig.setSSLEnabledCipherSuites(this.getProperty("SSLEnabledCipherSuites"));

			try {
				this.sslContext = EPPSSLImpl.initialize(theConfig);
			}
			catch (EPPConException ex) {
				log.error("EPPSystemSessionPool.init(): system = " + this.system
				      + ": Exception initializing EPPSSLContext: " + ex);
				throw new EPPSessionPoolException("EPPSystemSessionPool.init(): system = " + this.system
				      + ": Exception initializing EPPSSLContext: " + ex);
			}

			this.factory.setSSLContext(this.sslContext);
		}

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

		// Pre-initialize maxTotal sessions in pool?
		if (this.initMaxTotal && this.config.getMaxTotal() > 0) {
			log.info("init(): system = " + this.system + ": 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("init(): system = " + this.system + ": Pre-initialized session #" + (i + 1));
				}
				catch (EPPSessionPoolException ex) {
					log.error(
					      "init(): system = " + this.system + ": 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;
				}
			}

		}

	}

	/**
	 * Closes the session pool contained in {@code EPPSystemSessionPool} cleanly.
	 * Cleanly closing the pool means clearing the pool 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("EPPSystemSessionPool.close(): Exception closing pool <" + this.pool + ">: " + ex);
			}
		}
		log.info("close(): pool closed");
	}

	/**
	 * 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("EPPSystemSessionPool: 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 = this.pool.borrowObject();

				log.debug("borrowObject(): Session = " + theSession + ", Active = " + 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("EPPSystemSessionPool: Exception " + ex);
				}
				else {
					// Continue retrying
					log.debug("borrowObject(): Exception on borrow session after " + retries + " retries: " + 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 instanceof EPPPooledGenericSession)) {
			log.error(
			      "invalidateObject(" + aSession + "): session not of type " + EPPPooledGenericSession.class.getName());
			throw new EPPSessionPoolException(
			      "EPPSessionPool: session not of type " + EPPPooledGenericSession.class.getName());
		}

		try {
			this.pool.invalidateObject((EPPPooledGenericSession) aSession);
			log.debug("invalidateObject(" + aSession + "): Active = " + this.pool.getNumActive() + ", Idle = "
			      + this.pool.getNumIdle());
		}
		catch (Exception ex) {
			log.error("invalidateObject(" + 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 instanceof EPPPooledGenericSession)) {
			log.error(
			      "invalidateObject(" + aSession + "): session not of type " + EPPPooledGenericSession.class.getName());
			throw new EPPSessionPoolException(
			      "EPPSessionPool: session not of type " + EPPPooledGenericSession.class.getName());
		}

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

	}

	/**
	 * Gets the contained {@code GenericObjectPool<EPPPooledGenericSession>}.
	 *
	 * @return Contained {@code GenericObjectPool<EPPPooledGenericSession>}
	 */
	public GenericObjectPool<EPPPooledGenericSession> getGenericObjectPool() {
		return this.pool;
	}

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

	/**
	 * 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<EPPPooledGenericSession>}.
	 *
	 * @return Returns the config.
	 */
	public GenericObjectPoolConfig<EPPPooledGenericSession> 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 system name associated with this pool.
	 *
	 * @return Pool system name
	 */
	public String getSystem() {
		return this.system;
	}

	/**
	 * 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;
	}

	/**
	 * 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) {
		return Environment.getProperty(PROP_PREFIX + "." + this.system + "." + aProperty);
	}

}