/*
  $Id: AbstractLdapPool.java 2241 2012-02-07 20:08:51Z dfisher $

  Copyright (C) 2003-2010 Virginia Tech.
  All rights reserved.

  SEE LICENSE FOR MORE INFORMATION

  Author:  Middleware Services
  Email:   middleware@vt.edu
  Version: $Revision: 2241 $
  Updated: $Date: 2012-02-07 21:08:51 +0100 (Tue, 07 Feb 2012) $
*/
package edu.vt.middleware.ldap.pool;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Timer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import edu.vt.middleware.ldap.BaseLdap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <code>AbstractLdapPool</code> contains the basic implementation for pooling
 * ldap objects. The main design objective for the supplied pooling
 * implementations is to provide a pool that does not block on object creation
 * or destruction. This is what accounts for the multiple locks available on
 * this class. The pool is backed by two queues, one for available objects and
 * one for active objects. Objects that are available for {@link #checkOut()}
 * exist in the available queue. Objects that are actively in use exist in the
 * active queue. Note that depending on the implementation an object can exist
 * in both queues at the same time.
 *
 * @param  <T>  type of ldap object
 *
 * @author  Middleware Services
 * @version  $Revision: 2241 $ $Date: 2012-02-07 21:08:51 +0100 (Tue, 07 Feb 2012) $
 */
public abstract class AbstractLdapPool<T extends BaseLdap>
  implements LdapPool<T>
{

  /** Lock for the entire pool. */
  protected final ReentrantLock poolLock = new ReentrantLock();

  /** Condition for notifying threads that an object was returned. */
  protected final Condition poolNotEmpty = poolLock.newCondition();

  /** Lock for check ins. */
  protected final ReentrantLock checkInLock = new ReentrantLock();

  /** Lock for check outs. */
  protected final ReentrantLock checkOutLock = new ReentrantLock();

  /** Log for this class. */
  protected final Log logger = LogFactory.getLog(this.getClass());

  /** List of available ldap objects in the pool. */
  protected Queue<PooledLdap<T>> available = new LinkedList<PooledLdap<T>>();

  /** List of ldap objects in use. */
  protected Queue<PooledLdap<T>> active = new LinkedList<PooledLdap<T>>();

  /** Ldap pool config. */
  protected LdapPoolConfig poolConfig;

  /** Factory to create ldap objects. */
  protected LdapFactory<T> ldapFactory;

  /** Timer for scheduling pool tasks. */
  private Timer poolTimer = new Timer(true);


  /**
   * Creates a new pool with the supplied pool configuration and ldap factory.
   * The pool configuration will be marked as immutable by this pool.
   *
   * @param  lpc  <code>LdapPoolConfig</code>
   * @param  lf  <code>LdapFactory</code>
   */
  public AbstractLdapPool(final LdapPoolConfig lpc, final LdapFactory<T> lf)
  {
    this.poolConfig = lpc;
    this.poolConfig.makeImmutable();
    this.ldapFactory = lf;
  }


  /** {@inheritDoc} */
  public LdapPoolConfig getLdapPoolConfig()
  {
    return this.poolConfig;
  }


  /** {@inheritDoc} */
  public void setPoolTimer(final Timer t)
  {
    this.poolTimer = t;
  }


  /** {@inheritDoc} */
  public void initialize()
  {
    if (this.logger.isDebugEnabled()) {
      this.logger.debug("beginning pool initialization");
    }

    this.poolTimer.scheduleAtFixedRate(
      new PrunePoolTask<T>(this),
      this.poolConfig.getPruneTimerPeriod(),
      this.poolConfig.getPruneTimerPeriod());
    if (this.logger.isDebugEnabled()) {
      this.logger.debug("prune pool task scheduled");
    }

    this.poolTimer.scheduleAtFixedRate(
      new ValidatePoolTask<T>(this),
      this.poolConfig.getValidateTimerPeriod(),
      this.poolConfig.getValidateTimerPeriod());
    if (this.logger.isDebugEnabled()) {
      this.logger.debug("validate pool task scheduled");
    }

    this.initializePool();

    if (this.logger.isDebugEnabled()) {
      this.logger.debug("pool initialized to size " + this.available.size());
    }
  }


  /**
   * Attempts to fill the pool to its minimum size.
   *
   * @throws  IllegalStateException  if the pool does not contain at least one
   * connection and it's minimum size is greater than zero
   */
  private void initializePool()
  {
    if (this.logger.isDebugEnabled()) {
      this.logger.debug(
        "checking ldap pool size >= " + this.poolConfig.getMinPoolSize());
    }

    int count = 0;
    this.poolLock.lock();
    try {
      while (
        this.available.size() < this.poolConfig.getMinPoolSize() &&
          count < this.poolConfig.getMinPoolSize() * 2) {
        final T t = this.createAvailable();
        if (this.poolConfig.isValidateOnCheckIn()) {
          if (this.ldapFactory.validate(t)) {
            if (this.logger.isTraceEnabled()) {
              this.logger.trace(
                "ldap object passed initialize validation: " + t);
            }
          } else {
            if (this.logger.isWarnEnabled()) {
              this.logger.warn(
                "ldap object failed initialize validation: " + t);
            }
            this.removeAvailable(t);
          }
        }
        count++;
      }
      if (this.available.size() == 0 && this.poolConfig.getMinPoolSize() > 0) {
        throw new IllegalStateException("Could not initialize pool");
      }
    } finally {
      this.poolLock.unlock();
    }
  }


  /** {@inheritDoc} */
  public void close()
  {
    this.poolLock.lock();
    try {
      while (this.available.size() > 0) {
        final PooledLdap<T> pl = this.available.remove();
        this.ldapFactory.destroy(pl.getLdap());
      }
      while (this.active.size() > 0) {
        final PooledLdap<T> pl = this.active.remove();
        this.ldapFactory.destroy(pl.getLdap());
      }
      if (this.logger.isDebugEnabled()) {
        this.logger.debug("pool closed");
      }
    } finally {
      this.poolLock.unlock();
    }

    this.poolTimer.cancel();
  }


  /**
   * Create a new ldap object and place it in the available pool.
   *
   * @return  ldap object that was placed in the available pool
   */
  protected T createAvailable()
  {
    final T t = this.ldapFactory.create();
    if (t != null) {
      final PooledLdap<T> pl = new PooledLdap<T>(t);
      this.poolLock.lock();
      try {
        this.available.add(pl);
      } finally {
        this.poolLock.unlock();
      }
    } else {
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("unable to create available ldap object");
      }
    }
    return t;
  }


  /**
   * Create a new ldap object and place it in the active pool.
   *
   * @return  ldap object that was placed in the active pool
   */
  protected T createActive()
  {
    final T t = this.ldapFactory.create();
    if (t != null) {
      final PooledLdap<T> pl = new PooledLdap<T>(t);
      this.poolLock.lock();
      try {
        this.active.add(pl);
      } finally {
        this.poolLock.unlock();
      }
    } else {
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("unable to create active ldap object");
      }
    }
    return t;
  }


  /**
   * Create a new ldap object and place it in both the available and active
   * pools.
   *
   * @return  ldap object that was placed in the available and active pools
   */
  protected T createAvailableAndActive()
  {
    final T t = this.ldapFactory.create();
    if (t != null) {
      final PooledLdap<T> pl = new PooledLdap<T>(t);
      this.poolLock.lock();
      try {
        this.available.add(pl);
        this.active.add(pl);
      } finally {
        this.poolLock.unlock();
      }
    } else {
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("unable to create available and active ldap object");
      }
    }
    return t;
  }


  /**
   * Remove an ldap object from the available pool.
   *
   * @param  t  ldap object that exists in the available pool
   */
  protected void removeAvailable(final T t)
  {
    boolean destroy = false;
    final PooledLdap<T> pl = new PooledLdap<T>(t);
    this.poolLock.lock();
    try {
      if (this.available.remove(pl)) {
        destroy = true;
      } else {
        if (this.logger.isWarnEnabled()) {
          this.logger.warn(
            "attempt to remove unknown available ldap object: " + t);
        }
      }
    } finally {
      this.poolLock.unlock();
    }
    if (destroy) {
      if (this.logger.isTraceEnabled()) {
        this.logger.trace("removing available ldap object: " + t);
      }
      this.ldapFactory.destroy(t);
    }
  }


  /**
   * Remove an ldap object from the active pool.
   *
   * @param  t  ldap object that exists in the active pool
   */
  protected void removeActive(final T t)
  {
    boolean destroy = false;
    final PooledLdap<T> pl = new PooledLdap<T>(t);
    this.poolLock.lock();
    try {
      if (this.active.remove(pl)) {
        destroy = true;
      } else {
        if (this.logger.isWarnEnabled()) {
          this.logger.warn(
            "attempt to remove unknown active ldap object: " + t);
        }
      }
    } finally {
      this.poolLock.unlock();
    }
    if (destroy) {
      if (this.logger.isTraceEnabled()) {
        this.logger.trace("removing active ldap object: " + t);
      }
      this.ldapFactory.destroy(t);
    }
  }


  /**
   * Remove an ldap object from both the available and active pools.
   *
   * @param  t  ldap object that exists in the both the available and active
   * pools
   */
  protected void removeAvailableAndActive(final T t)
  {
    boolean destroy = false;
    final PooledLdap<T> pl = new PooledLdap<T>(t);
    this.poolLock.lock();
    try {
      if (this.available.remove(pl)) {
        destroy = true;
      } else {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug(
            "attempt to remove unknown available ldap object: " + t);
        }
      }
      if (this.active.remove(pl)) {
        destroy = true;
      } else {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug(
            "attempt to remove unknown active ldap object: " + t);
        }
      }
    } finally {
      this.poolLock.unlock();
    }
    if (destroy) {
      if (this.logger.isTraceEnabled()) {
        this.logger.trace("removing active ldap object: " + t);
      }
      this.ldapFactory.destroy(t);
    }
  }


  /**
   * Attempts to activate and validate an ldap object. Performed before an
   * object is returned from {@link LdapPool#checkOut()}.
   *
   * @param  t  ldap object
   *
   * @throws  LdapPoolException  if this method fais
   * @throws  LdapActivationException  if the ldap object cannot be activated
   * @throws  LdapValidateException  if the ldap object cannot be validated
   */
  protected void activateAndValidate(final T t)
    throws LdapPoolException
  {
    if (!this.ldapFactory.activate(t)) {
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("ldap object failed activation: " + t);
      }
      this.removeAvailableAndActive(t);
      throw new LdapActivationException("Activation of ldap object failed");
    }
    if (
      this.poolConfig.isValidateOnCheckOut() &&
        !this.ldapFactory.validate(t)) {
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("ldap object failed check out validation: " + t);
      }
      this.removeAvailableAndActive(t);
      throw new LdapValidationException("Validation of ldap object failed");
    }
  }


  /**
   * Attempts to validate and passivate an ldap object. Performed when an object
   * is given to {@link LdapPool#checkIn}.
   *
   * @param  t  ldap object
   *
   * @return  whether both validate and passivation succeeded
   */
  protected boolean validateAndPassivate(final T t)
  {
    boolean valid = false;
    if (this.poolConfig.isValidateOnCheckIn()) {
      if (!this.ldapFactory.validate(t)) {
        if (this.logger.isWarnEnabled()) {
          this.logger.warn("ldap object failed check in validation: " + t);
        }
      } else {
        valid = true;
      }
    } else {
      valid = true;
    }
    if (valid && !this.ldapFactory.passivate(t)) {
      valid = false;
      if (this.logger.isWarnEnabled()) {
        this.logger.warn("ldap object failed activation: " + t);
      }
    }
    return valid;
  }


  /** {@inheritDoc} */
  public void prune()
  {
    if (this.logger.isTraceEnabled()) {
      this.logger.trace(
        "waiting for pool lock to prune " + this.poolLock.getQueueLength());
    }
    this.poolLock.lock();
    try {
      if (this.active.size() == 0) {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("pruning pool of size " + this.available.size());
        }
        while (this.available.size() > this.poolConfig.getMinPoolSize()) {
          PooledLdap<T> pl = this.available.peek();
          final long time = System.currentTimeMillis() - pl.getCreatedTime();
          if (time > this.poolConfig.getExpirationTime()) {
            pl = this.available.remove();
            if (this.logger.isTraceEnabled()) {
              this.logger.trace(
                "removing " + pl.getLdap() + " in the pool for " + time + "ms");
            }
            this.ldapFactory.destroy(pl.getLdap());
          } else {
            break;
          }
        }
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("pool size pruned to " + this.available.size());
        }
      } else {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("pool is currently active, no objects pruned");
        }
      }
    } finally {
      this.poolLock.unlock();
    }
  }


  /** {@inheritDoc} */
  public void validate()
  {
    this.poolLock.lock();
    try {
      if (this.active.size() == 0) {
        if (this.poolConfig.isValidatePeriodically()) {
          if (this.logger.isDebugEnabled()) {
            this.logger.debug(
              "validate for pool of size " + this.available.size());
          }

          final Queue<PooledLdap<T>> remove = new LinkedList<PooledLdap<T>>();
          for (PooledLdap<T> pl : this.available) {
            if (this.logger.isTraceEnabled()) {
              this.logger.trace("validating " + pl.getLdap());
            }
            if (this.ldapFactory.validate(pl.getLdap())) {
              if (this.logger.isTraceEnabled()) {
                this.logger.trace(
                  "ldap object passed validation: " + pl.getLdap());
              }
            } else {
              if (this.logger.isWarnEnabled()) {
                this.logger.warn(
                  "ldap object failed validation: " + pl.getLdap());
              }
              remove.add(pl);
            }
          }
          for (PooledLdap<T> pl : remove) {
            if (this.logger.isTraceEnabled()) {
              this.logger.trace("removing " + pl.getLdap() + " from the pool");
            }
            this.available.remove(pl);
            this.ldapFactory.destroy(pl.getLdap());
          }
        }
        this.initializePool();
        if (this.logger.isDebugEnabled()) {
          this.logger.debug(
            "pool size after validation is " + this.available.size());
        }
      } else {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug(
            "pool is currently active, " +
            "no validation performed");
        }
      }
    } finally {
      this.poolLock.unlock();
    }
  }


  /** {@inheritDoc} */
  public int availableCount()
  {
    return this.available.size();
  }


  /** {@inheritDoc} */
  public int activeCount()
  {
    return this.active.size();
  }


  /**
   * Called by the garbage collector on an object when garbage collection
   * determines that there are no more references to the object.
   *
   * @throws  Throwable  if an exception is thrown by this method
   */
  protected void finalize()
    throws Throwable
  {
    try {
      this.close();
    } finally {
      super.finalize();
    }
  }


  /**
   * <code>PooledLdap</code> contains an ldap object that is participating in a
   * pool. Used to track how long an ldap object has been in either the
   * available or active queues.
   *
   * @param  <T>  type of ldap object
   */
  static protected class PooledLdap<T extends BaseLdap>
  {

    /** hash code seed. */
    protected static final int HASH_CODE_SEED = 89;

    /** Underlying ldap object. */
    private T ldap;

    /** Time this object was created. */
    private long createdTime;


    /**
     * Creates a new <code>PooledLdap</code> with the supplied ldap object.
     *
     * @param  t  ldap object
     */
    public PooledLdap(final T t)
    {
      this.ldap = t;
      this.createdTime = System.currentTimeMillis();
    }


    /**
     * Returns the ldap object.
     *
     * @return  underlying ldap object
     */
    public T getLdap()
    {
      return this.ldap;
    }


    /**
     * Returns the time this object was created.
     *
     * @return  creation time
     */
    public long getCreatedTime()
    {
      return this.createdTime;
    }


    /**
     * Returns whether the supplied <code>Object</code> contains the same data
     * as this bean.
     *
     * @param  o  <code>Object</code>
     *
     * @return  <code>boolean</code>
     */
    public boolean equals(final Object o)
    {
      if (o == null) {
        return false;
      }
      return
        o == this ||
          (this.getClass() == o.getClass() &&
            o.hashCode() == this.hashCode());
    }


    /**
     * This returns the hash code for this object.
     *
     * @return  <code>int</code>
     */
    public int hashCode()
    {
      int hc = HASH_CODE_SEED;
      if (this.ldap != null) {
        hc += this.ldap.hashCode();
      }
      return hc;
    }
  }
}
