/*-----------------------------------------------------------------------------
 *
 * Enhydra JdbcS
 * The Initial Developer of the Original Code is Lutris Technologies Inc.
 * Portions created by Lutris are Copyright 1997-2000 Lutris Technologies Inc.
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Enhydra Public License
 * Version 1.1 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * www.enhydra.org/license/epl.html
 *
 * Software distributed under the License is distributed on an "ASIS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 *
 * -----------------------------------------------------------------------------
 * $Id$
 * -----------------------------------------------------------------------------
 */
package org.enhydra.jdbc.pool;

import java.util.Hashtable;
import java.util.Enumeration;
import java.io.PrintWriter;
import org.enhydra.jdbc.core.JdbcThreadFactory;

/**
 * GenericPool is the main class of the Pool. It works with any kind
 * of object that's implement PoolHelper (must provide specific
 * operation on the specific object)
 * Objects stored in hashtables are GenerationObject object. These objects
 * allow to store multiples things in them, in particular, the
 * generation number to identify the generation of an object.
 */
public class GenericPool {

    private long        lifeTime;               // lifetime of an object in the pool
    private Hashtable   locked, unlocked;       // two hashtables, to stock objects in use and free objects
    private PrintWriter logWriter;              // trace log writer
    private JdbcThreadFactory threadFactory;    // thread factory
    private int         minSize;                // minimum size of the pool, set to 0
    private int         maxSize;                // maximum size of the pool, if set to 0 : unlimited
    private PoolHelper  poolHelper;             // object type
    private int         count;                  // count the number of object in the pool
    private boolean     gc;                     // gc system call
    private boolean     debug;                  // debug flag
    private long        deadLockMaxWait;           // time to wait before deadlock (return exception)
    private long        deadLockRetryWait;               // time to wait before 2 try of loop

    /**
     * checking level object
     * 0 = no special checking
     * 1 = just a check on an object
     * 2 = test the object
     * 3 = just a check on an object (for all the objects)
     * 4 = test the object (for all the objects)
     */
    private int         checkLevelObject;

    protected Thread    keeper;                 // thread to clean up dead objects
    protected PoolKeeper poolKeeper;
    private long        sleepTime;              // sleeptime for the pool keeper

    /**
     * Generation number.  When an error occurs, all objects of the
     * same generation or earlier are dropped.
     */
    protected int generation = 1;

    // Default values
    public static final long    DEFAULT_EXPIRATION = 600000;    // 10 minutes
    public static final long    DEFAULT_SLEEPTIME = 300000;     // 5 minutes
    public static final int     DEFAULT_MINSIZE =  2;           // 2 objects
    public static final int     DEFAULT_MAXSIZE = 50;           // 50 objects
    public static final int     DEFAULT_DEADLOCKMAXWAIT = 300000;  // 5 minutes
    public static final int     DEFAULT_DEADLOCKRETRYWAIT = 10000; // 10 seconds

    /**
     * Creates an GenericPool with the default params.
     */
    public GenericPool(PoolHelper helper) {
        this(helper, DEFAULT_MINSIZE, DEFAULT_MAXSIZE, DEFAULT_EXPIRATION, DEFAULT_SLEEPTIME);
    } // constructor

    public GenericPool(PoolHelper helper, int initSize) {
        this(helper, DEFAULT_MINSIZE, initSize, DEFAULT_EXPIRATION, DEFAULT_SLEEPTIME);
    } // constructor

    /**
     * Constructor, set the two hashtables and set by default the other values
     */
    public GenericPool (PoolHelper helper,
                        int minSize,
                        int maxSize,
                        long lifeTime,
                        long sleepTime) {
	this.threadFactory = null;
        this.lifeTime = lifeTime;
        this.minSize = minSize;
        this.maxSize = maxSize;
        this.poolHelper = helper;               // helper is the type of the object (interface)
        this.sleepTime = sleepTime;
        this.logWriter = null;
        this.checkLevelObject = 0;
        this.deadLockMaxWait = DEFAULT_DEADLOCKMAXWAIT;
        this.deadLockRetryWait = DEFAULT_DEADLOCKRETRYWAIT;
        log("GenericPool is created");
    } // constructor

    /**
     * Start method, to initialize independant values of the pool
     */
    public synchronized void start() {
        locked = new Hashtable();       // create locked objects pool
        unlocked = new Hashtable();     // create unlocked objects pool
        count = 0;                      // 0 element to start
        gc = false;                     // do not actions concerning garbage collector
        long now = System.currentTimeMillis();  // current time

        // to obtain to the beginning minSize objects in the pool
        Object o;
        for  (int i = 0; i < minSize; i++) {    // count have to be equal to minSize
            try {
                GenerationObject genObject = poolHelper.create();
                unlocked.put(genObject, new Long(now)); // put it in the unlocked pool
            } catch (Exception e) {
                log("Error Exception in GenericPool:start");
            }
            ++count;                            // there is one more element in the pool
        }

        //keeper removes dead or useless objects
	if (threadFactory != null) {
	    try {
                this.poolKeeper = new PoolKeeper(sleepTime, this);
		this.keeper = threadFactory.getThread(poolKeeper);
	    } catch (Exception e) {
		throw new IllegalStateException(e.getMessage());
	    }
	} else {
	    this.keeper = new Thread(new PoolKeeper(sleepTime, this));
	}
        keeper.start();                 // start the thread to verify element in the pool(unlocked)
        log("GenericPool:start pool started");
    } // method start

    public synchronized boolean checkOwner(GenerationObject genObject, String user, String password) {
        return equals(user,genObject.getUser()) && equals(password,genObject.getPassword());
    } // method checkOwner

    JdbcThreadFactory getThreadFactory() {
	return threadFactory;
    }

    void setThreadFactory(JdbcThreadFactory tf) {
	threadFactory = tf;
    }

    private boolean equals(String a, String b) {
        if (a==null) return (b==null);
        if (b==null) return (a==null);
        return a.equals(b);
    }

    /**
     * return pooled object
     */
    public synchronized Object checkOut(String user, String password) throws Exception {
        log("GenericPool:checkOut");
        long now = System.currentTimeMillis();          // current time to compare
        GenerationObject o;
        Enumeration e;
        Object realObject;

        if (getUnlockedObjectCount() > 0) {             // if there are objects in the unlocked pool
            if ((checkLevelObject == 3) || (checkLevelObject == 4)) { //need to verify all the objects
                e = unlocked.keys();
                while (e.hasMoreElements()) {
                    o = (GenerationObject)e.nextElement();
                    realObject = o.getObj();            // take the current object

                    if (((checkLevelObject == 3) && !poolHelper.checkThisObject(realObject)) ||
                        ((checkLevelObject == 4) && !poolHelper.testThisObject(realObject))) {
                        log("GenericPool:checkOut remove object checkLevelObject="+checkLevelObject);
                        removeUnlockedObject(o);
                        minimumObject();// build object in the pool if it is lesser than minSize
                    }
                }
            }

            // now, we have to return an object to the user
            e = unlocked.keys();                // then take them
            while (e.hasMoreElements()) {               // for each objects ...
                o = (GenerationObject)e.nextElement();
		realObject = o.getObj();

		// first, verify if the object is not dead (lifetime)
		if ((now - ((Long)unlocked.get(o)).longValue() ) > lifeTime) {// object has expired
		    removeUnlockedObject(o);
		    minimumObject();        // build object in the pool if it is lesser than minSize
		}
		else {
		    if (checkOwner(o, user, password)) {
			// second, verification of the object if needed
			if ((checkLevelObject == 0) ||
			    ((checkLevelObject == 1) && poolHelper.checkThisObject(realObject)) ||
			    ((checkLevelObject == 2) && poolHelper.testThisObject(realObject))) {

			    unlocked.remove(o);         // remove it from the unlocked pool
			    locked.put(o, new Long(now));       // and put it in the locked pool
			    notifyAll();
			    log("GenericPool:checkOut return an object (after verification if needed)");
			    return(o.getObj());         // return this element
			}
			else {                          // object failed validation
			    log("GenericPool:checkOut kill an object from the pool");
			    removeUnlockedObject(o);
			    minimumObject(user, password);// build object in the pool if it is lesser than minSize
			}

		    }
		}
	    }

	} // if getUnlockedObjectCount() > 0

        int currentWait = 0;
        while ((count >= maxSize) && (currentWait < getDeadLockMaxWait())) {
            log("GenericPool.checkOut: waiting for an object");
            try {
                synchronized(this) {
                    wait(getDeadLockRetryWait());
                }
            } catch(InterruptedException excp) {
                log("ERROR: Failed while waiting for an object: "+excp);
            }
            currentWait += getDeadLockRetryWait();
        }

        if (count >= maxSize)
            throw new Exception ("ERROR : impossible to obtain a new object from the pool");

        // if no objects available, create a new one
        if (count < maxSize) {                  // if number of pooled object is < max size of the pool
            log("GenericPool:checkOut no objects available, create a new one");
            try {
                Object tmpObj;
                GenerationObject genObject = poolHelper.create(user,password);

                locked.put(genObject, new Long(now));   // put it in the locked pool
                ++count;                        // there is one more element in the pool
                notifyAll();
                return(genObject.getObj());                     // and return this element
            } catch (Exception excp) {
                log("Error Exception in GenericPool:checkOut");
                // cney: rethrow exception thrown by create
                throw excp;
            }
        }

        return null;

    } // method checkOut

    synchronized public void minimumObject() {
	minimumObject(null, null);
    } // method minimumObject

    synchronized public void minimumObject(String user, String password) {
        log("GenericPool:minimumObject create object if there are less than minSize objects in the pool count ="+count);
        if ((count < minSize) && (unlocked != null)) {  // if pool has less than minSize elements
            long now = System.currentTimeMillis();      // current time

            Object o;
            for  (int i = count; i < minSize; i++) {    //count have to be equal to minSize
                try {
		    GenerationObject genObject;
		    if ((user != null) && (password != null))
			genObject = poolHelper.create();
		    else
			genObject = poolHelper.create(user, password);
                    unlocked.put(genObject, new Long(now)); // put it in the unlocked pool
                } catch (Exception e) {
                    log("Error Exception in GenericPool:minimumObject");
                }
            }
            count = minSize;                            // pool has now minSize element
        }
    } // method minimumObject

    /**
     * remove object from locked pool
     */
    public synchronized void checkIn(Object o) {
        log("GenericPool:checkIn return an object to the pool");

        for (Enumeration enum=locked.keys(); enum.hasMoreElements(); ) {        // for each object of
            GenerationObject obj = (GenerationObject)enum.nextElement();        // the locked pool

            if (obj.getObj().equals(o)) {
                locked.remove(obj);                             // remove the object from the locked pool

                // we have to verify if the generation of the object is still valid
                int genObj = obj.getGeneration();               // get the generation number

                // if the generation number of the object is not valid, test the object
                if (generation > genObj) {
                    if (! poolHelper.checkThisObject(obj.getObj()))     // if the object is not valid
                        removeUnlockedObject(obj);

                } else {
                    // we can put the object in the unlocked pool for future use
                    unlocked.put(obj, new Long(System.currentTimeMillis()));
                    notifyAll();
                }
            }
        }

        if (count > maxSize) {                          // if we have more than maxSize object in the pool
            log("GenericPool:checkIn more than maxSize object in the pool");
            Enumeration enum=unlocked.keys();           // get the unlocked pool
            for (int i = maxSize ; i < count ; i++) {   // try to remove object from this pool
                if (getUnlockedObjectCount() > 0) {
                    GenerationObject obj = (GenerationObject)enum.nextElement();
                    removeUnlockedObject(obj);
                }
            }
            // now, the size of the pool is changed
            // NOTE : count number ca be greater than maxSize here, in case of there is no
            // available object to delete in the unlocked pool
            count = getUnlockedObjectCount() + getLockedObjectCount();
            if (count > maxSize)
                log("Be careful, the maximum size of the pool does not correspond" +
                    " to your data. When objects will be check in, the pool " +
                    "will decrease");
        }

    } // method checkIn

    synchronized private void removeUnlockedObject(GenerationObject obj) {
        --count;
        unlocked.remove(obj);
        poolHelper.expire(obj.getObj());                // and try to "kill" it
        obj.killObject();
        obj = null;
    } // method removeUnlockedObject

    /**
     * Set/Get methods
     */
    public void setLogWriter(PrintWriter logWriter) {
        this.logWriter = logWriter;
    } // method setLogWriter

    public void setDebug(boolean debug) {
        this.debug = debug;
    } // method setDebug

    public boolean isDebug() {
        return debug;
    } // method isDebug

    public synchronized void setMinSize(int min) throws Exception {
        if (min < 0)
            throw new Exception("Minimum size of the pool can't be lesser than 0");
        else if (min > maxSize)
            throw new Exception("Minimum size of the pool can't be greater than the maxSize ("+maxSize+")");
        else {
            this.minSize = min;
            minimumObject();            // build object in the pool if it is lesser than minSize
        }
    } // method setMinSize

    public synchronized void setMaxSize(int max) throws Exception {
        if (max < 0)
             throw new Exception("Maximum size of the pool can't be lesser than 0");
        else if (max < minSize)
             throw new Exception("Maximum size of the pool can't be lesser than the minSize ("+minSize+")");
        else {
            this.maxSize = max;
            if (count > max) {                                  // if pool has more than max element
                log("GenericPool:setMaxSize pool has more than max element");
                // we try to remove element from the unlocked pool
                Enumeration enum=unlocked.keys();
                for (int i = max ; i < count ; i++) {
                    if (getUnlockedObjectCount() > 0) {         // if there is element in the unlocked pool
                        GenerationObject o = (GenerationObject)enum.nextElement();
                        removeUnlockedObject(o);
                    }
                }

                // new size is :
                count = getUnlockedObjectCount() + getLockedObjectCount();
                // if there is still more than max element, the objects will be removed
                // when the check in operation will be performed.
                if (count > max)
                    log("GenericPool:setMaxSize Be careful, the maximum size of " +
                        "the pool does not correspond to your data. When objects " +
                        "will be check in, the pool will decrease");
            }
        }
    } // method setMaxSize

    public void setLifeTime(long lifeTime) {
        this.lifeTime = lifeTime;
    } // method setLifeTime

    public void setSleepTime(long sleepTime) {
        this.sleepTime = sleepTime;
    } // method setSleepTime

    public void setGeneration(int generation) {
        this.generation = generation;
        log("GenericPool:setGeneration Be careful, it is very dangerous to change " +
            "the generation number, many objects could be destroyed");
    } // method setGeneration

    public void setGC(boolean gc) {
        this.gc = gc;
    } // method setGC

    /**
     * level are accepted between 0 and 4
     */
    public void setCheckLevelObject(int level) {
        if ((level > 0) && (level <= 4))
            this.checkLevelObject = level;
    } // method setCheckLevelObject

    public void setDeadLockMaxWait(long deadLock) {
        this.deadLockMaxWait = deadLock;
    } // method seDeadLockMaxWait

    public void setDeadLockRetryWait(long deadLockRetryWait) {
        this.deadLockRetryWait = deadLockRetryWait;
    } // method setDeadLockRetryWait

    public PrintWriter getLogWriter() {
        return logWriter;
    } // method getLogWriter

    public int getMinSize() {
        return minSize;
    } // method getMinSize

    public int getMaxSize() {
        return maxSize;
    } // method getMaxSize

    public long getLifeTime() {
        return lifeTime;
    } // method getExpirationTime

    public boolean isGC() {
        return gc;
    } // method isGC

    public int getCount() {
        return count;
    } // method getCount

    public long getSleepTime() {
        return sleepTime;
    } // method getSleepTime

    public int getGeneration() {
        return generation;
    } // method getGeneration

    public int getCheckLevelObject() {
        return checkLevelObject;
    } // method checkLevelObject

    /**
     * switch off the pool
     */
    public void stop() {
        log("GenericPool:stop start to stop the pool");
        if ((getLockedObjectCount() != 0) || (getUnlockedObjectCount() != 0)) {
            expireAll();                // try to kill all the objects in the 2 pools
            poolKeeper.stop();          // release the pool.
            keeper.interrupt();         // and interrupt the pool keeper
            locked.clear();             // clear the locked pool
            unlocked.clear();   // clear the unlocked pool
            locked   = null;
            unlocked = null;
            count    = 0;               // there is no element in the pool
            log("GenericPool:stop pool stopped");
        }
    } // method switchOff

    /**
     * returns the current number of objects that are locked
     */
    public int getLockedObjectCount() {
        if (locked != null)
            return locked.size();
        else
            return 0;
    } // method getLockedObjectCount

    /**
     * returns the current number of objects that are unlocked
     */
    public int getUnlockedObjectCount() {
        if (unlocked != null)
            return unlocked.size();
        else
            return 0;
    } // method getUnlockedObjectCount

    public long getDeadLockMaxWait() {
        return this.deadLockMaxWait;
    } // method getDeadLockMaxWait

    public long getDeadLockRetryWait() {
        return this.deadLockRetryWait;
    } // method getDeadLockRetryWait

    /**
     * returns information from the pool
     */
    public String toString() {
        return "GenericPool:Pool information : " +
            "\n   num of element = "+count +
            "\n   minsize = "+minSize +
            "\n   maxsize = "+maxSize +
            "\n   lifeTime = "+lifeTime +" (ms)"+
            "\n   generation = "+generation +
            "\n   size of locked table = "+getLockedObjectCount() +
            "\n   size of unlocked table = "+getUnlockedObjectCount()+
            "\n   time to wait before deadlock = "+getDeadLockMaxWait()+" (ms)"+
            "\n   number of wait loop = "+getDeadLockRetryWait()+"\n";
    } // method toString

    /**
     * Remove unusable objects from the pool, called by PoolKeeper
     * Check the unlocked objects for expired members.
     */
    protected synchronized void cleanUp() {
        // During shutdown, unlocked may be null.
        if (unlocked == null) return;
        long now = System.currentTimeMillis();                                  // current time
        for (Enumeration enum=unlocked.keys(); enum.hasMoreElements(); ) {      // for each object of the
            GenerationObject o = (GenerationObject)enum.nextElement();          // unlocked pool
            long lasttouch = ((Long) unlocked.get(o)).longValue();              // birth day of the pool

            if ((now - lasttouch) > lifeTime) {         // if the object has expired
                log("GenericPool:cleanUp clean up the pool");
                removeUnlockedObject(o);
            }
        }

        if (isGC())             // if the pool is GCeable
            System.gc();        // launch system call to clean up unused objects

        if (count < minSize) {  // if there is less than minSize objects in the pool
            log("GenericPool:cleanUp less than minSize objects in the pool");
            GenerationObject o;
            for  (int i = count; i < minSize; i++) {            //count have to be equal to minSize
                try {
		    GenerationObject genObject = poolHelper.create();
		    unlocked.put(genObject, new Long(now)); // put it in the unlocked pool
                } catch (Exception e) {
                    log("Error Exception in GenericPool:cleanUp");
                }
                ++count;                                // there is one more element in the pool
                notifyAll();
            }
        }
    } // method cleanUp

    /**
     * close all object in the unlocked and locked structures
     */
    void expireAll() {
        log("GenericPool:expireAll close all object in the unlocked and locked structures");
        for (Enumeration enum=unlocked.keys(); enum.hasMoreElements(); ) {      // for each object of
            GenerationObject o = (GenerationObject)enum.nextElement();          // the unlocked pool
            poolHelper.expire(o.getObj());                                      // try to "kill" the object
            o.killObject();
            o = null;
        }
        for (Enumeration enum=locked.keys(); enum.hasMoreElements(); ) {        // for each object of
            GenerationObject o = (GenerationObject)enum.nextElement();          // the locked pool
            poolHelper.expire(o.getObj());                                      // try to "kill" the object
            o.killObject();
            o = null;
        }
    } // method expireAll

    /**
     * Allows to verify if objects from the pool - for the o generation -
     * are valid or not. (only for the unlocked pool, to avoid to allocate
     * non-valid object
     */
    public void nextGeneration(Object obj) {
        log("GenericPool:nextGeneration");
        int genObj = 0;
        for (Enumeration enum=locked.keys(); enum.hasMoreElements(); ) {        // for each object of
            GenerationObject o = (GenerationObject)enum.nextElement();          // the locked pool

            if (o.getObj().equals(obj))
                genObj = o.getGeneration();             // get the generation number
        }

        for (Enumeration enum=unlocked.keys(); enum.hasMoreElements(); ) {      // for each object of
            GenerationObject o = (GenerationObject)enum.nextElement();          // the unlocked pool

            if (o.getGeneration() <= genObj) {                  // all objects of the same generation
                                                                // or earlier are dropped
                if (! poolHelper.checkThisObject(o.getObj()))   // if the object is not valid
                    removeUnlockedObject(o);

            }
        }
        ++this.generation;              // now, we work with the next generation of object

    }// method nextGeneration

    /**
     * removes an object for the locked pool, when an error has occurred
     */
    synchronized public void removeLockedObject(Object obj) {
        log("GenericPool:removeObject remove an object");
        int genObj;
        for (Enumeration enum=locked.keys(); enum.hasMoreElements(); ) {        // for each object of
            GenerationObject o = (GenerationObject)enum.nextElement();          // the locked pool
            if (o.getObj().equals(obj)) {
                locked.remove(o);                               // remove the object from the locked pool
                --count;
                o.killObject();
                o = null;
            }
        }
    } // method removeObject

    /**
     * Outputs a log message to the log writer.
     */
    public void log (String msg) {
        if (isDebug()) {
            if (logWriter == null) return;      // check for no logging
            logWriter.println(msg);             // output the message
        }
    } // method log

} // class GenericPool

