/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * 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 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import javax.ejb.EJBException;
import javax.ejb.EntityBean;
import javax.ejb.FinderException;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.ow2.jonas.deployment.ejb.EntityDesc;
import org.ow2.jonas.deployment.ejb.EntityJdbcCmp1Desc;
import org.ow2.jonas.deployment.ejb.EntityJdbcCmp2Desc;


import org.objectweb.util.monolog.api.BasicLevel;

/**
 * This class is a factory for an Entity Bean. It is responsible for
 * - managing Home and LocalHome.
 * - managing a pool of instances/contexts
 * - keeping the list of PKs and the associated JEntitySwitch's
 * - keeping the JNDI context for this component (java:comp/env)
 * @author Philippe Coq, Philippe Durieux
 */
public class JEntityFactory extends JFactory implements TimerService {

    /**
     * optional home
     */
    protected JEntityHome home = null;

    /**
     * @return the home if it exists
     */
    public JHome getHome() {
        return home;
    }

    /**
     * optional local home
     */
    protected JEntityLocalHome localhome = null;

    /**
     * @return the local home if it exists
     */
    public JLocalHome getLocalHome() {
        return localhome;
    }

    /**
     * lock policy
     */
    protected int lockPolicy;

    /**
     * @return lockPolicy
     */
    public int getLockPolicy() {
        return lockPolicy;
    }

    /**
     * enable the prefetch for CMP2 bean
     */
    protected boolean prefetch = false;

    /**
     * @return true if prefetch enable
     */
    public boolean isPrefetch() {
        return prefetch;
    }

    /**
     * True if CMP2 container
     */
    protected boolean cmp2 = true;

    /**
     * @return true if CMP2 container
     */
    public boolean isCMP2() {
        return cmp2;
    }

    /**
     * shared if the EJB container is not the only one to modify
     * the bean state on the database, or if a cluster of container
     * access the bean concurrently.
     */
    protected boolean shared = false;

    /**
     * @return true if shared
     */
    public boolean isShared() {
        return shared;
    }

    /**
     * reentrant if instance can be used concurrently
     */
    protected boolean reentrant;

    /**
     * @return true if reentrant
     */
    public boolean isReentrant() {
        return reentrant;
    }

    /**
     * @return true if the bean is shared
     */
    public boolean getSelectForUpdate() {
        return shared && lockPolicy == EntityDesc.LOCK_CONTAINER_SERIALIZED_TRANSACTED;
        // In case of database lock-policy, setting the selectForUpdate option
        // leads sometimes to deadlocks, at least on postgresql.
        //return shared;
    }

    /**
     * Number of seconds before reading again instances for read-only.
     */
    protected int readTimeout; // sec.

    /**
     * @return read timeout in sec.
     */
    public int getReadTimeout() {
        return readTimeout;
    }

    /**
     * Number of seconds before releasing objects when not used.
     */
    private int inactivityTimeout; // sec.

    /**
     * @return inactivity timeout in sec.
     */
    public int getInactivityTimeout() {
        return inactivityTimeout;
    }

    /**
     * Set the inactivity timeout (jonas admin)
     * Not used today
     */
    public void setInactivityTimeout(int i) {
        inactivityTimeout = i;
    }

    /**
     * Number of seconds before loocking for deadlock when stuck
     */
    private int deadlockTimeout; // sec.

    /**
     * @return deadlock timeout in sec.
     */
    public int getDeadlockTimeout() {
        return deadlockTimeout;
    }

    /**
     * Set the deadlock timeout (jonas admin)
     * Not used today
     */
    public void setDeadlockTimeout(int i) {
        deadlockTimeout = i;
    }

    /**
     * Number of seconds before storing objects (CS policy only)
     */
    private int passivationTimeout; // sec.

    /**
     * @return passivation timeout in sec.
     */
    public int getPassivationTimeout() {
        return passivationTimeout;
    }

    /**
     * Set the passivation timeout (jonas admin)
     * Not used today
     */
    public void setPassivationTimeout(int i) {
        TraceEjb.logger.log(BasicLevel.WARN, ejbname);
        passivationTimeout = i;
    }

    /**
     * Datasource in case of CMP
     */
    protected Object datasource = null;

    /**
     * datasource JNDI name
     */
    String dsname = null;

    /**
     * @return the Datasource used for CMP
     */
    public Object getDataSource() {
        if (datasource == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + ": datasource not defined");
        }
        return datasource;
    }

    /**
     * Dummy method that defines the FinderException in the throws clause
     * to can catch this exception in any case in the JentityHome.vm
     * @param dummy if true do nothing, else throw the FinderException
     * @throws FinderException if dummy is false
     */
    public void dummyFinderException(boolean dummy) throws FinderException {
        if (!dummy) {
            throw new FinderException("dummy exception !!!");
        }
    }

    // -----------------------------------------------------------------
    // Start - Stop
    // -----------------------------------------------------------------

    /**
     * constructor must be without parameters (required by Jorm)
     */
    public JEntityFactory() {
        TraceEjb.interp.log(BasicLevel.DEBUG, "");
    }

    /**
     * Get the DataSource registered in JNDI
     * Must use the container classloader.
     * TODO move this in Container
     */
    private void setDataSource() {
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(cont.getClassLoader());
            InitialContext ictx = naming.getInitialContext();
            datasource = ictx.lookup(dsname);
            TraceEjb.logger.log(BasicLevel.DEBUG, "DataSource is " + datasource);
        } catch (NamingException e) {
            String err = dsname + " is not known by the EJB server.";
            TraceEjb.logger.log(BasicLevel.ERROR, err, e);
            throw new EJBException(err, e);
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    /**
     * Init this object
     * @param dd the deployment descriptor
     * @param cont the Container
     */
    public void init(EntityDesc dd, JContainer cont) {
        super.init(dd, cont);
        reentrant = dd.isReentrant();
        shared = dd.isShared();
        lockPolicy = dd.getLockPolicy();
        prefetch = dd.isPrefetch();
        hardLimit = dd.isHardLimit();

        // Check lock-policy compatible with shared and prefetch flags
        switch (lockPolicy) {
            case EntityDesc.LOCK_CONTAINER_READ_UNCOMMITTED:
                if (prefetch) {
                    prefetch = false;
                    TraceEjb.logger.log(BasicLevel.WARN, "prefetch flag forced to false, with CRU lock policy");
                }
                TraceEjb.logger.log(BasicLevel.WARN, "This policy (CRU) is deprecated. Use CRW instead!");
                break;
            case EntityDesc.LOCK_CONTAINER_READ_WRITE:
                if (prefetch) {
                    prefetch = false;
                    TraceEjb.logger.log(BasicLevel.WARN, "prefetch flag forced to false with CRW lock policy");
                }
                if (shared) {
                    shared = false;
                    TraceEjb.logger.log(BasicLevel.WARN, "shared flag forced to false with CRW lock policy");
                }
                break;
            case EntityDesc.LOCK_CONTAINER_SERIALIZED:
                if (shared) {
                    TraceEjb.logger.log(BasicLevel.WARN, "shared flag should be set to false with CS policy");
                    TraceEjb.logger.log(BasicLevel.WARN, "Consider switching to the new CST policy");
                }
                break;
            case EntityDesc.LOCK_DATABASE:
                if (!shared) {
                    shared = true;
                    TraceEjb.logger.log(BasicLevel.WARN, "shared flag forced to true with DB policy");
                }
                break;
        }
        inactivityTimeout = dd.getInactivityTimeout();
        passivationTimeout = dd.getPassivationTimeout();
        deadlockTimeout = dd.getDeadlockTimeout();
        readTimeout = dd.getReadTimeout();
        setMaxWaitTime(dd.getMaxWaitTime());

        // Finds the DataSource associated to the bean
        // Useful for Entity with Container Managed Persistence.
        if (dd instanceof EntityJdbcCmp1Desc) {
            cmp2 = false;
            dsname = ((EntityJdbcCmp1Desc) dd).getDatasourceJndiName();
            if (dsname != null) {
                setDataSource();
            }
            if (lockPolicy == EntityDesc.LOCK_CONTAINER_READ_WRITE) {
                throw new EJBException("Cannot use CONTAINER_READ_WRITE with a CMP1 bean");
            }
        }

        if (dd instanceof EntityJdbcCmp2Desc) {
            cmp2 = true;
            dsname = ((EntityJdbcCmp2Desc) dd).getDatasourceJndiName();
            if (dsname != null) {
                setDataSource();
            }
        }

        // Create the Home if defined in DD
        Class homeclass = null;
        String clname = dd.getFullWrpHomeName();
        if (clname != null) {
            try {
                homeclass = cont.getClassLoader().loadClass(clname);
            } catch (ClassNotFoundException e) {
                throw new EJBException(ejbname + "Cannot load " + clname, e);
            }
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, ejbname + ": " + clname + " loaded");
            }
            try {
                // new JEntityHome(dd, this)
                int nbp = 2;
                Class[] ptype = new Class[nbp];
                Object[] pobj = new Object[nbp];
                ptype[0] = org.ow2.jonas.deployment.ejb.EntityDesc.class;
                pobj[0] = (Object) dd;
                ptype[1] = org.ow2.jonas.lib.ejb21.JEntityFactory.class;
                pobj[1] = (Object) this;
                home = (JEntityHome) homeclass.getConstructor(ptype).newInstance(pobj);
            } catch (Exception e) {
               throw new EJBException(ejbname + " Cannot create home ", e);
            }

            // register it in JNDI
            try {
                home.register();
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot register home ", e);
            }
        }

        // Create the LocalHome if defined in DD
        clname = dd.getFullWrpLocalHomeName();
        if (clname != null) {
            try {
                homeclass = cont.getClassLoader().loadClass(clname);
            } catch (ClassNotFoundException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot load " + clname);
                throw new EJBException("Cannot load " + clname, e);
            }
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, ejbname + ": " + clname + " loaded");
            }
            try {
                // new JEntityLocalHome(dd, this)
                int nbp = 2;
                Class[] ptype = new Class[nbp];
                Object[] pobj = new Object[nbp];
                ptype[0] = org.ow2.jonas.deployment.ejb.EntityDesc.class;
                pobj[0] = (Object) dd;
                ptype[1] = org.ow2.jonas.lib.ejb21.JEntityFactory.class;
                pobj[1] = (Object) this;
                localhome = (JEntityLocalHome) homeclass.getConstructor(ptype).newInstance(pobj);
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot create localhome ", e);
            }
            // register it in JNDI
            try {
                localhome.register();
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot register localhome ", e);
            }
        }
    }

    /**
     * stop this EJB.
     * Mainly unregister it in JNDI.
     */
    public void stop() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        try {
            if (home != null) {
                home.unregister();
            }
            if (localhome != null) {
                localhome.unregister();
            }
        } catch (NamingException e) {
        }
        stopContainer();
    }

    // --------------------------------------------------------------
    // Instance Pool Management
    // --------------------------------------------------------------

    /**
     * freelist of JEntityContext's
     */
    protected List bctxlist = new ArrayList();

    /**
     * @return the Instance pool size for this Ejb
     */
    public int getPoolSize() {
        return bctxlist.size();
    }

    /**
     * Current number of instances in memory
     */
    protected int instanceCount = 0;

    /**
     * @return current cache size
     */
    public int getCacheSize() {
        return instanceCount;
    }

    /**
     * @return true if max-cache-size has been reached
     */
    public boolean tooManyInstances() {
        return (maxCacheSize > 0 && instanceCount >= maxCacheSize);
    }

    /**
     * True if cannot overtake max-cache-size
     */
    protected boolean hardLimit = false;

    /**
     * @return true if hard limit for max-cache-size
     */
    public boolean isHardLimit() {
        return hardLimit;
    }

    /**
     * nb of threads waiting for an instance
     */
    private int currentWaiters = 0;

    /**
     * @return current number of instance waiters
     */
    public int getCurrentWaiters() {
        return currentWaiters;
    }

    /**
     * max nb of milliseconds to wait for an instance when pool is empty
     */
    private long waiterTimeout = 10000L;

    /**
     * @return waiter timeout in seconds
     */
    public int getMaxWaitTime() {
        return (int) (waiterTimeout / 1000L);
    }

    /**
     * @param sec max time to wait for a connection, in seconds
     */
    public void setMaxWaitTime(int sec) {
        waiterTimeout = sec * 1000L;
    }

    /**
     * Init the pool of instances
     */
    public void initInstancePool() {
        // pre-allocate a set of JEntityContext (bean instances)
        Context bnctx = setComponentContext(); // for createNewInstance
        for (int i = 0; i < minPoolSize; i++) {
            JEntityContext ctx = null;
            try {
                ctx = createNewInstance(null);
                synchronized(bctxlist) {
                    bctxlist.add(ctx);
                    instanceCount++;
                }
            } catch (Exception e) {
                TraceEjb.logger.log(BasicLevel.WARN, ejbname + " cannot create new instance", e);
                break;
            }
        }
        resetComponentContext(bnctx);
    }

    /**
     * Get a Context from the pool, or create a new one if no more
     * available in the pool.
     * This JContext must be initialized then by the caller.
     * @return a JEntityContext, not initialized.
     */
    public JEntityContext getJContext(JEntitySwitch es) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        JEntityContext bctx = null;

        // Prevent a max-cache-size reached exception, if possible.
        int limit = maxCacheSize - maxCacheSize / 15;
        if (instanceCount >= limit && limit > minPoolSize) {
            // ask swapper to call reduceCache()
            // if maxcachesize will be reached soon, or is reached, or is highly
            // overtaken.
            if (instanceCount > maxCacheSize + limit ||
                    instanceCount == limit ||
                    instanceCount == maxCacheSize) {
                if (TraceEjb.isDebugSwapper()) {
                    TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname +
                            ": Too many instances :" + instanceCount + ", max=" + maxCacheSize);
                }
                cont.registerBF(this);
            }
        }

        // try to find a free context in the pool
        long timetowait = waiterTimeout;
        long starttime = 0;
        synchronized (bctxlist) {
            while (bctx == null) {
                if (bctxlist.isEmpty()) {
                    // pool is empty
                    if (!hardLimit || !tooManyInstances()) {
                        // If can create more instances, go ahead.
                        instanceCount++;
                        break;
                    }
                    // If a timeout has been specified, wait
                    if (timetowait > 0) {
                        currentWaiters++;
                        if (starttime == 0) {
                            starttime = System.currentTimeMillis();
                            if (TraceEjb.isDebugSwapper()) {
                                TraceEjb.swapper.log(BasicLevel.DEBUG, "WAITING " + ejbname);
                            }
                        }
                        try {
                            bctxlist.wait(timetowait);
                        } catch (InterruptedException e) {
                            TraceEjb.swapper.log(BasicLevel.WARN, "INTERRUPTED " + ejbname);
                        } finally {
                            currentWaiters--;
                        }
                        long stoptime = System.currentTimeMillis();
                        long stillwaited = stoptime - starttime;
                        timetowait =  waiterTimeout - stillwaited;
                    } else {
                        // max-wait-time elapsed
                        TraceEjb.swapper.log(BasicLevel.ERROR, "max-cache-size reached on " + ejbname);
                        throw new EJBException("max-cache-size reached on " + ejbname);
                    }
                } else {
                    // we got an instance from the pool.
                    bctx = (JEntityContext) bctxlist.remove(0);
                }
            }
        }

        if (bctx == null) {
            // create a new one if pool empty
            try {
                bctx = createNewInstance(es);
            } catch (Exception e) {
                instanceCount--;
                throw new EJBException("Cannot create a new instance ", e);
            }
        }
        return bctx;
    }

    /**
     * Release a Context
     * @param ctx - The JContext to release.
     * @param poolaction 0=never pool, 1=pool if < MinPoolSize, 2=pool if < MaxCacheSize
     */
    public void releaseJContext(JContext ctx, int poolaction) {
        TraceEjb.context.log(BasicLevel.DEBUG, ejbname);
        JEntityContext bctx = (JEntityContext) ctx;
        bctx.razEntityContext();
        boolean poolit = true;
        synchronized (bctxlist) {
            switch (poolaction) {
                case 0:
                    poolit = false;
                    break;
                case 1:
                    poolit = (bctxlist.size() < minPoolSize);
                    break;
                default:
                    poolit = !tooManyInstances();
                    break;
            }
            if (poolit) {
                bctxlist.add(bctx);
            } else {
                instanceCount--;
            }
            // Someone may be waiting for an instance.
            if (currentWaiters > 0) {
                bctxlist.notify();
            }
        }
    }

    /**
     * Release a Context [deprecated]
     * @param ctx - The JContext to release.
     */
    public void releaseJContext(JContext ctx) {
        releaseJContext(ctx, 2);
    }

    protected JEntityContext createNewContext(EntityBean bean) {
        return new JEntityContext(this, bean);
    }

    /**
     * Create a new instance of the bean and its EntityContext
     * In case of CMP, the bean class is derived to manage entity persistence.
     * @return JEntityContext
     * @throws Exception cannot instantiate bean
     */
    protected JEntityContext createNewInstance(JEntitySwitch es) throws Exception {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }

        // create the bean instance
        EntityBean bean = null;
        try {
            bean = (EntityBean) beanclass.newInstance();
        } catch (Exception e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot instantiate bean " + instanceCount);
            throw e;
        }

        // Set classloader for getting the right component context
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(myClassLoader());

            // create a new EntityContext and bind it to the instance
            JEntityContext ec = createNewContext(bean);

            // Must set the EntitySwitch now because setEntityContext may
            // need it (getEjbObject for example, in TCK.)
            if (es != null) {
                ec.setEntitySwitch(es);
            } else {
                // case of remove or find
                TraceEjb.logger.log(BasicLevel.DEBUG, "possible error in setEntityContext: No e.s. defined yet");
            }
            bean.setEntityContext(ec);
            ec.setPassive();

            return ec;
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    /**
     * Synchronize all dirty instances
     * Does nothing if not CS policy.
     * @param alwaysStore True if we want to store modify instances
     * even if passivation timeout is not reached.
     */
    public void syncDirty(boolean alwaysStore) {

        if (!mustSyncDirtyEntities) {
            // nothing to do
            return;
        }

        if (TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname + " cacheSize = " + getCacheSize());
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname + " dirtyList size = " + dirtyList.size());
        }

        // Set the JNDI component context (for ejbStore)
        Context bnctx = setComponentContext();

        // try to passivate all instances and build a new list.
        LinkedList wsdone = new LinkedList();
        synchronized (this) {
            for (ListIterator i = dirtyList.listIterator(0); i.hasNext();) {
                JEntitySwitch es = (JEntitySwitch) i.next();
                switch (es.passivateIH(alwaysStore, false)) {
                    case JEntitySwitch.ALL_DONE:
                        i.remove();
                        break;
                    case JEntitySwitch.STORED:
                        i.remove();
                        wsdone.addLast(es);
                        break;
                    case JEntitySwitch.NOT_DONE:
                        if (TraceEjb.isDebugSwapper()) {
                            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname + " busy");
                        }
                        break;
                }
            }
        }

        // for each instance passivated, notify it's OK to continue.
        // must be done after all instances are passivated, in case of relations.
        mustSyncDirtyEntities = false; // do this before endIH
        for (ListIterator i = wsdone.listIterator(0); i.hasNext();) {
            JEntitySwitch es = (JEntitySwitch) i.next();
            es.endIH();
        }

        // Reset old value for component context
        resetComponentContext(bnctx);
    }

    /**
     * Reduce number of instances in memory
     * passivate all instances that are not busy and not used very recently
     */
    public void reduceCache() {
        TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);

        // Get the list of PK.
        HashMap clone = null;
        synchronized (this) {
            clone = (HashMap) pklist.clone();
        }
        if (clone.size() == 0) {
            return;
        }

        TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);

        // Set classloader for getting the right component context
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(myClassLoader());

            // Set the JNDI component context (for ejbStore)
            Context bnctx = setComponentContext();

            JEntitySwitch bs = null;
            for (Iterator i = clone.values().iterator(); i.hasNext();) {
                bs = (JEntitySwitch) i.next();
                if (! bs.isInFindByPK()) {
                    bs.passivateIH(true, true);
                }
            }
            // Reset old value for component context
            resetComponentContext(bnctx);
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    // --------------------------------------------------------------
    // PK list - JEntitySwitch cache Management
    // --------------------------------------------------------------

    /**
     * List of JEntitySwitch objects
     * At each PK is associated a JEntitySwitch object
     */
    private HashMap pklist = new HashMap();

    /**
     * dirty list of JEntitySwitch
     * Only used for CS policy (non transacted modifying methods)
     */
    private LinkedList dirtyList = new LinkedList();

    private boolean mustSyncDirtyEntities = false;

    /**
     * @return true if dirty list is not empty
     * Only used for CS policy (non transacted modifying methods)
     */
    public boolean dirtyInstances() {
        return mustSyncDirtyEntities;
    }

    /**
     * This field is used to generate an uid for unique automatic pk
     */
    private static final long DIZINIT = 1132241379300L;
    private int ucount = (int) (System.currentTimeMillis() - DIZINIT) / 100;

    /**
     * Calculate a new uid for automatic pk creation Used by JEntityCmpJdbc.vm
     * @return int (unique pk)
     */
    public int calculateAutomaticPk() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        int uid;
        synchronized (this) {
            uid = ucount++;
        }
        return uid;
    }

    /**
     * Encode PK. This does nothing, except in case of CMP2
     * @return String representation of the PK
     */
    public Serializable encodePK(Serializable pk) {
        return pk;
    }

    /**
     * Decode PK. This does nothing, except in case of CMP2
     * @return PK matching the String
     */
    public Serializable decodePK(Serializable strpk) {
        return strpk;
    }

    /**
     * get EJB by its PK
     * Creates if not exist yet.
     * @param pk The Primary Key Object
     * @return The JEntitySwitch matching the PK.
     */
    public synchronized JEntitySwitch getEJB(Object pk) {

        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }

        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return null;
        }
        JEntitySwitch bs = (JEntitySwitch) pklist.get(pk);
        // Must wait while findByPK is in progress
        while (bs != null && bs.isInFindByPK()) {
            try {
                wait(6000); // wait no more than few seconds
            } catch (InterruptedException e) {
                TraceEjb.logger.log(BasicLevel.WARN, "finder too long");
                break;
            }
            // read again, since it may have been removed (if not existing on storage)
            bs = (JEntitySwitch) pklist.get(pk);
        }
        if (bs == null) {
            bs = getJEntitySwitch();
            bs.init(this, pk);
            pklist.put(pk, bs);
        }
        return bs;
    }

    /**
     * get EJB by its PK. If not exist yet, map the EntitySwitch to the PK.
     * @param pk The Primary Key Object
     * @param bs the Entityswitch
     * @param findbypk true if a ejbFindByPK has to be done just after.
     * @return The JEntitySwitch matching the PK, or null if none exist.
     */
    public synchronized JEntitySwitch existEJB(Object pk, JEntitySwitch bs, boolean findbypk) {
        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return null;
        }
        JEntitySwitch ret = (JEntitySwitch) pklist.get(pk);
        // Must wait while findByPK is in progress
        while (ret != null && ret.isInFindByPK()) {
            try {
                wait(6000); // wait no more than few seconds
            } catch (InterruptedException e) {
                TraceEjb.logger.log(BasicLevel.WARN, "finder too long");
                break;
            }
            // read again, since it may have been removed (if not existing on storage)
            ret = (JEntitySwitch) pklist.get(pk);
        }
        if (ret == null && bs != null) {
            // Bind new EntitySwitch while we got the lock.
            bs.init(this, pk);
            pklist.put(pk, bs);
            if (findbypk) {
                // This ES must not be used before ejbFindByPK is OK!
                bs.setInFindByPK(true);
            }
        }
        return ret;
    }

    /**
     * Notify that the ejbFindByPrimarKey has been successful
     */
    public synchronized void startEJB(Object pk) {
        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return;
        }
        JEntitySwitch bs = (JEntitySwitch) pklist.get(pk);
        bs.setInFindByPK(false);
        notifyAll();
    }

    /**
     * rebind a PK with a JEntitySwitch (called by create methods)
     * @param tx current Transaction
     * @param bctx The EntityContext
     * @param pk The Primary Key Object
     * @return true if bs has been added to the PK list.
     */
    public boolean rebindEJB(Transaction tx, JEntityContext bctx, Object pk) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }

        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return false;
        }
        JEntitySwitch bs = bctx.getEntitySwitch();
        JEntitySwitch old = null;
        synchronized (this) {
            old = (JEntitySwitch) pklist.get(pk);
        }
        boolean ret = true;
        if (old != null) {
            // different cases:
            // - a create after a remove, in the same tx (or no tx)
            // - in CMP2: a "DuplicateKey".
            // - Bean removed by another server (bean shared)

            // Release completely the old EntitySwitch before binding the new one.
            // must be done outside the lock, to avoid a deadlock.
            ret = old.terminate(tx);
        }
        synchronized (this) {
            bs.init(this, pk);
            pklist.put(pk, bs);
            notifyAll();
        }
        return ret;
    }

    /**
     * Bind a PK with a JEntitySwitch
     * @param pk The Primary Key Object
     * @param bs The JEntitySwitch
     */
    public synchronized void bindEJB(Object pk, JEntitySwitch bs) {
        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return;
        }
        if (TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);
        }
        bs.init(this, pk);
        pklist.put(pk, bs);
        notifyAll();
    }

    /**
     * This method allocates a new JEntitySwitch. But no association has been
     * done between the primary key and the new JEntitySwitch. Therefore the
     * initialisation is not done.
     * @return The JEntitySwitch.
     */
    public JEntitySwitch getJEntitySwitch() {
        switch (lockPolicy) {
            case EntityDesc.LOCK_CONTAINER_READ_UNCOMMITTED:
                return new JEntitySwitchCRU();
            case EntityDesc.LOCK_CONTAINER_READ_COMMITTED:
                return new JEntitySwitchCRC();
            case EntityDesc.LOCK_DATABASE:
                return new JEntitySwitchDB();
            case EntityDesc.LOCK_READ_ONLY:
                return new JEntitySwitchRO();
            case EntityDesc.LOCK_CONTAINER_READ_WRITE:
                return new JEntitySwitchCRW();
            case EntityDesc.LOCK_CONTAINER_SERIALIZED_TRANSACTED:
                return new JEntitySwitchCST();
            case EntityDesc.LOCK_CONTAINER_SERIALIZED:
                return new JEntitySwitchCS();
            default:
                TraceEjb.logger.log(BasicLevel.WARN, "lockPolicy not initilized");
                return new JEntitySwitchCS();
        }
    }

    /**
     * remove an EJB by its PK
     * @param pk The Primary Key Object
     */
    public synchronized void removeEJB(Object pk) {
        if (TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);
        }
        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "pk null ???");
            return;
        }
        pklist.remove(pk);
        notifyAll();
    }

    /**
     * Register an EntitySwitch in the dirty list.
     * should be called each time a new instance is modified outside transaction
     * @param ejb The Entity Switch to be registered
     */
    public synchronized void registerEJB(JEntitySwitch ejb) {
        if (TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);
        }
        if (dirtyList.contains(ejb)) {
            TraceEjb.swapper.log(BasicLevel.ERROR, ejbname + " Elt already in the dirty list");
            return;
        }
        dirtyList.addLast(ejb);
        mustSyncDirtyEntities = true;
    }


    /**
     * Ask swapper to synchronize all dirty EntitySwitch
     * Only used for container-serialized policy
     */
    public void synchronizeEntities() {
        if (TraceEjb.isDebugSwapper()) {
            TraceEjb.swapper.log(BasicLevel.DEBUG, ejbname);
        }
        cont.registerBFS(this);
    }

    /**
     * Search if this transaction is blocked in a deadlock
     * @param suspect Transaction suspected to block everybody
     * @return True if a deadlock has been detected.
     */
    public boolean isDeadLocked(Transaction suspect) {
        // Get a clone of the complete list of PKs
        HashMap clone = null;
        synchronized (this) {
            clone = (HashMap) pklist.clone();
        }
        JEntitySwitch es = null;
        // Build a list of all transactions that block "suspect" tx.
        ArrayList blktx = new ArrayList();
        Transaction testedtx = suspect;
        // Loop until a result is found (true or false)
        while (true) {
            for (Iterator i = clone.values().iterator(); i.hasNext();) {
                es = (JEntitySwitch) i.next();
                Transaction tx = es.getBlockingTx(testedtx);
                if (tx != null) {
                    if (tx.equals(suspect)) {
                        TraceEjb.synchro.log(BasicLevel.WARN, "Found a deadlock on :" + tx);
                        // found a deadlock
                        return true;
                    }
                    blktx.add(tx);
                }
            }
            if (blktx.size() == 0) {
                // no deadlock found.
                TraceEjb.synchro.log(BasicLevel.DEBUG, "No deadlock found");
                return false;
            }
            testedtx = (Transaction) blktx.remove(0);
        }
    }

   /**
     * Take a dump of current entity counters and return them
     * @return EntityCounters
     */
    public synchronized EntityCounters getEntityCounters() {
        EntityCounters ec = new EntityCounters();
        Collection coll = pklist.values();
        ec.pk = pklist.size();
        for (Iterator i = coll.iterator(); i.hasNext();) {
            JEntitySwitch es = (JEntitySwitch) i.next();
            switch (es.getState()) {
                case 0:
                    ec.inTx++;
                    break;
                case 1:
                    ec.outTx++;
                    break;
                case 2:
                    ec.idle++;
                    break;
                case 3:
                    ec.passive++;
                    break;
                case 4:
                    ec.removed++;
                    break;
            }
        }
        return ec;
    }


    // --------------------------------------------------------------
    // transaction association management
    // --------------------------------------------------------------

    /**
     * List of Transaction Listeners.
     * At each Transaction is associated a TxListener object
     */
    protected HashMap txlist = new HashMap();

   /**
     * synchronize data modified in this transaction.
     * this is necessary in case of finder method, because
     * ejb-ql looks for on disk.
     * @param tx the Transaction
     */
    public void syncForFind(Transaction tx) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        if (tx == null) {
            if (lockPolicy == EntityDesc.LOCK_CONTAINER_SERIALIZED) {
                syncDirty(true);
            }
        } else {
            this.getContainer().storeAll(tx);
        }
    }

    /**
      * synchronize data modified in the current transaction.
      * this is necessary in case of select method, because
      * ejb-ql looks for on disk.
      */
    public void syncForSelect() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        // First of all, get the current transaction
        Transaction currtx = null;
        try {
            currtx = tm.getTransaction();
            if (TraceEjb.isDebugTx()) {
                TraceEjb.tx.log(BasicLevel.DEBUG, "currtx=" + currtx);
            }
        } catch (SystemException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "system exception while getting transaction:", e);
            return;
        }
        // Sync dirty instances for the current transaction
        syncForFind(currtx);
    }

    /**
     * For storing entities modified in tx
     * @param tx current transaction
     * @see org.ow2.jonas.lib.ejb21.BeanFactory#storeInstances(javax.transaction.Transaction)
     */
    public void storeInstances(Transaction tx) {
        TxListener txl = (TxListener) txlist.get(tx);
        if (txl != null) {
            txl.storeInstances();
        }

    }


    /**
     * Remove a Transaction Listener from the list.
     * @param tx the Transaction to remove
     */
    public void removeTxListener(Transaction tx) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        txlist.remove(tx);
    }

    /**
     * Unregister a Context/Instance in the transaction.
     * @param tx current Transaction
     * @param ec JEntityContext to be registered
     */
    public void unregisterContext(Transaction tx, JEntityContext ec) throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
        // Get the TXListener associated to this tx.
        TxListener txl = (TxListener) txlist.get(tx);
        if (txl == null) {
            return;
        }
        txl.removeInstance(ec);
    }

    /**
     * Register a Context/Instance in the transaction.
     * @param tx current Transaction
     * @param ec JEntityContext to be registered
     * @return true if instance has been registered.
     */
    public boolean registerContext(Transaction tx, JEntityContext ec) throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }

        // Get the TXListener associated to this tx, or create it.
        TxListener txl = (TxListener) txlist.get(tx);
        if (txl == null) {
            txl = new TxListener(this, tx);
            // register it as Synchronization.
            try {
                tx.registerSynchronization(txl);
            } catch (RollbackException e) {
                if (TraceEjb.isVerbose()) {
                    TraceEjb.logger.log(BasicLevel.WARN, ejbname + " transaction has been marked rollbackOnly", e);
                }
                return false;
            } catch (SystemException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, ejbname + " cannot register synchro ", e);
                // forget txl.
                throw new IllegalStateException("cannot register synchro");
            }
            txlist.put(tx, txl);
        }

        // Add the JEntityContext
        txl.addInstance(ec);
        return true;
    }

    /**
     * Check Transaction before calling a method on a bean. For Entity beans,
     * the only possible case is "Container Managed Tx"
     * @param rctx The Request Context
     */
    public void checkTransaction(RequestCtx rctx) {
        checkTransactionContainer(rctx);
    }





    // -----------------------------------------------------------------------
    // Timer Service Implementation
    // ----------------------------------------------------------------------

   /**
     * Obtains the TimerService associated for this Bean
     * @return a JTimerService instance.
     */
    public TimerService getTimerService() {
        return this;
    }

    /**
     * Illegal operation: timers are associated with a PK
     * @see javax.ejb.TimerService#createTimer(long, java.io.Serializable)
     */
    public Timer createTimer(long arg0, Serializable arg1) throws IllegalArgumentException, IllegalStateException, EJBException {
        throw new IllegalStateException("timers are associated with a PK");
    }

    /**
     * Illegal operation: timers are associated with a PK
    /* (non-Javadoc)
     * @see javax.ejb.TimerService#createTimer(long, long, java.io.Serializable)
     */
    public Timer createTimer(long arg0, long arg1, Serializable arg2) throws IllegalArgumentException, IllegalStateException, EJBException {
        throw new IllegalStateException("timers are associated with a PK");
    }

    /**
     * Illegal operation: timers are associated with a PK
     * @see javax.ejb.TimerService#createTimer(java.util.Date, java.io.Serializable)
     */
    public Timer createTimer(Date arg0, Serializable arg1) throws IllegalArgumentException, IllegalStateException, EJBException {
        throw new IllegalStateException("timers are associated with a PK");
    }

    /**
     * Illegal operation: timers are associated with a PK
     * @see javax.ejb.TimerService#createTimer(java.util.Date, long, java.io.Serializable)
     */
    public Timer createTimer(Date arg0, long arg1, Serializable arg2) throws IllegalArgumentException, IllegalStateException, EJBException {
        throw new IllegalStateException("timers are associated with a PK");
    }

    /**
     * Illegal operation: timers are associated with a PK
     * @see javax.ejb.TimerService#getTimers()
     */
    public Collection getTimers() throws IllegalStateException, EJBException {
        throw new IllegalStateException("timers are associated with a PK");
    }


}
