/**
 * 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: JEntitySwitch.java 19262 2010-02-18 09:23:36Z durieuxp $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;

import javax.ejb.EJBException;
import javax.ejb.EntityBean;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.ObjectNotFoundException;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.ejb.TransactionRolledbackLocalException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.objectweb.util.monolog.api.BasicLevel;
import org.ow2.jonas.deployment.ejb.EntityDesc;
import org.ow2.jonas.ha.HaService;
import org.ow2.jonas.lib.timer.TraceTimer;

/**
 * JEntitySwitch is used internally to synchronize accesses to the entity
 * context and thus to the entity bean instance. All parts common to EJBObject
 * and EJBLocalObject should be here. Different policies can be applied to
 * manage context/instance pairs: - only 1 pair (container manages the
 * transaction isolation) - 1 pair for each transaction (transaction isolation
 * managed by DataBase) - 2 pairs (1 for transactional accesses, 1 for non
 * transaction accesses)
 * @author Philippe Durieux
 * @author Philippe Coq
 */
public abstract class JEntitySwitch {

    /**
     * The Factory for this bean
     */
    protected JEntityFactory bf;

    /**
     * The Primary Key for this bean instance.
     */
    protected Object pk = null;

    /**
     * The EJBLocalObject, or null if bean has no local interface.
     */
    protected JEntityLocal local = null;

    /**
     * The EJBObject, or null if bean has no remote interface.
     */
    protected JEntityRemote remote = null;

    /**
     * time in millisec. to keep objects in memory when not used.
     * After this time, objects are freed, if instances have been passivated
     * before.
     */
    protected long inactivityTimeout; // millisec.

    /**
     * time in millisec. to keep objects in memory without rereading them
     * (for RO policy only)
     */
    protected long readTimeout; // millisec.

    /**
     * shared=true if the bean can be modify outside this container.
     */
    protected boolean shared;

    /**
     * Lock policy used for this entity bean. Possible values are :
     * <dl>
     * <li>0 = LOCK_CONTAINER_READ_UNCOMMITTED (1 instance) </li>
     * <li>1 = LOCK_CONTAINER_SERIALIZED (1 instance) </li>
     * <li>2 = LOCK_CONTAINER_READ_COMMITTED (2 instances) </li>
     * <li>3 = LOCK_DATABASE (n instances) </li>
     * <li>4 = LOCK_READ_ONLY (1 instance) </li>
     * <li>5 = LOCK_CONTAINER_READ_WRITE (1 instance) </li>
     * <li>6 = LOCK_CONTAINER_SERIALIZED_TRANSACTED (1 instance) </li>
     * </dl>
     */
    protected int lockpolicy;

    /**
     * True if a transaction is mandatory for all modifying methods. In this
     * case, all methods outside tranction is considered as read-only.
     * Only CS policy has txUpdates=false.
     */
    protected boolean txUpdates = true;

    /**
     * The Timer Service
     */
    protected TimerService myTimerService = null;

    /**
     * nb of non transacted requests running
     */
    protected int countIH = 0;

    /**
     * nb of transacted requests running
     */
    protected int countIT = 0;

    /**
     * nb of threads waiting (synchronization)
     */
    protected int waiters = 0;

    /**
     * True if this instance may have been modified outside transactions. Avoids
     * to put it twice in dirty list.
     * Only used for CS policy (non transacted modifying methods)
     */
    protected boolean inDirtyList = false;

    /**
     * True if instance has been modified by a Transaction. This means that
     * reading outside transaction should reload it before use.
     * Only used for DB and CRC policies (non transacted instance)
     */
    protected boolean mustReload = false;

    /**
     * True if a TX need this instance currently used outside tx When the last
     * release is done, we must store this instance.
     * Only used when txUpdate is false (i.e. CS policy)
     */
    protected boolean mustStore = false;

    /**
     * True when context/instance has been discarded, to avoids that passivate
     * store its state on storage.
     */
    protected boolean todiscard = false;

    /**
     * transaction on which a Context has been registered.
     * Registration may be differed at first write in case of lazyregister=true.
     * This ref is reset to null when the transaction is completed.
     */
    protected Transaction runningtx = null;

    /**
     * transaction that has modified an instance
     */
    protected Transaction writingtx = null;

    // max time to wait when in a transaction, before checking for
    // a deadlock.
    protected long deadlockTimeout; // millisec.

    /**
     * List of all transactions currently blocked by runningtx
     */
    protected ArrayList<Transaction> blockedtx = new ArrayList<Transaction>();

    /**
     * True if this object will be garbaged and must not be used any longer.
     */
    protected boolean isremoved = false;

    /**
     * True if this object is no longer referenced by its factory, i.e.
     * it is no longer in the PK list.
     */
    protected boolean isdetached = false;

    protected boolean infindbypk = false;

    // ident is used mainly for debugging
    protected static int counter = 0;

    protected String ident="0000 ";

    /**
     * timestamp used to free objects when not used for a specified time.
     */
    protected long estimestamp = System.currentTimeMillis();

    protected final static long FEW_SECONDS = 2000;

    private static final int MAX_NB_RETRY = 2;

    /**
     * true if we can differ the registration at first write.
     * Cannot be true if shared or prefetch.
     */
    protected boolean lazyregister = false;

    /**
     * reentrant=true if a bean instance can be accessed concurrently
     * in the same transaction, or outside transaction.
     */
    protected boolean reentrant;

    abstract JEntityContext getContext4Tx(Transaction tx);

    abstract void setContext4Tx(Transaction tx, JEntityContext ctx);

    abstract void removeContext4Tx(Transaction tx);

    abstract protected void initpolicy(JEntityFactory bf);

    /**
     * @param store True if want to store instance first (CS policy)
     * @param passivate True if we want to passivate instance
     * @return result of operation: ALL_DONE, STORED, or NOT_DONE
     */
    abstract public int passivateIH(boolean store, boolean passivate);

    /**
     * Called only for CS policy, after passivateIH
     */
    abstract public void endIH();

    // Results from passivateIH
    static final int ALL_DONE = 0;
    static final int STORED   = 1;
    static final int NOT_DONE = 2;

    /**
     * empty constructor. Object is initialized via init() because it is
     * implemented differently according to jorm mappers.
     */
    public JEntitySwitch() {
    }

    /**
     * @return true if updates must be done inside tx
     */
    public boolean isWriteInsideTx() {
        return txUpdates;
    }

    /**
     * constructor. A new object is build when a new PK is known in the
     * container, either when a new bean is created, or when a find occurs. For
     * create(), PK is not known yet when this object is build.
     * @param bf The Entity Factory
     * @param pk The Primary Key
     */
    public void init(final JEntityFactory bf, final Object pk) {
        this.bf = bf;
        this.pk = pk;
        isremoved = false;
        if (pk == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "Init Entity Switch with a null PK!");
            throw new EJBException("Init Entity Switch with a null PK!");
        }

        shared = bf.isShared();
        reentrant = bf.isReentrant();
        inactivityTimeout = bf.getInactivityTimeout() * 1000;
        deadlockTimeout = bf.getDeadlockTimeout() * 1000;
        readTimeout = bf.getReadTimeout() * 1000;
        initpolicy(bf);
        if (TraceEjb.isDebugSynchro() || TraceEjb.isDebugTx()) {
            // generate new ident (debug)
            String pks = pk.toString();
            ident = "<" + counter + ":" + pks + "> ";
        }
        counter++;

        countIH = 0;
        countIT = 0;
        waiters = 0;

        // Create EJBObject if bean has a Remote Interface
        if (bf.getHome() != null) {
            try {
                remote = ((JEntityHome) bf.getHome()).createRemoteObject();
                remote.setEntitySwitch(this);
            } catch (RemoteException e) {
                throw new EJBException("cannot create Remote Object", e);
            }
        }

        // Create EJBLocalObject if bean has a Local Interface
        if (bf.getLocalHome() != null) {
            local = ((JEntityLocalHome) bf.getLocalHome()).createLocalObject();
            local.setEntitySwitch(this);
        }
        // Set the timestamp to a big value because instance will be used.
        // This solve a problem in case of findAll() for example :
        // Avoids to discard instance just before it will be used.
        estimestamp = System.currentTimeMillis() + FEW_SECONDS;
    }

    /**
     * @return true if lazy registering enabled.
     */
    public boolean lazyRegistering() {
        return lazyregister;
    }

    /**
     * @return the underlaying EJBLocalObject
     */
    public JEntityLocal getLocal() {
        return local;
    }

    /**
     * @return the underlaying EJBObject
     */
    public JEntityRemote getRemote() {
        return remote;
    }

    /**
     * Obtains the TimerService associated for this Entity Bean (one / pk)
     * @return a JTimerService instance.
     */
    public TimerService getEntityTimerService() {
        // In case this object is used for a finder method,
        // pk should be null, and TimerService must not be retrieved.
        if (pk == null) {
            throw new java.lang.IllegalStateException();
        }
        if (myTimerService == null) {
            // TODO : Check that instance implements TimedObject ?
            myTimerService = new JTimerService(this);
        }
        return myTimerService;
    }

    /**
     * This transaction has just modified this instance. (CMP2 only)
     * Called only if lazyRegister is set and not RO policy.
     * @param tx transaction
     */
    public synchronized void notifyWriting(final Transaction tx, final JEntityContext bctx) {
        TraceEjb.synchro.log(BasicLevel.DEBUG, ident);
        if (writingtx != null) {
            // If same tx, we have been called for nothing because
            // we are already registered (dirty=true)
            if (!tx.equals(writingtx)) {
                TraceEjb.logger.log(BasicLevel.WARN, "Conflict with " + writingtx);
                TraceEjb.logger.log(BasicLevel.WARN, "Current Tx is " + tx);
                try {
                    tx.setRollbackOnly();
                } catch (SystemException e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, "cannot set current transaction RollbackOnly");
                }
                // Don't rollback writingtx, because no change has been done yet by tx.
                // This works only if setDirty is always called before modifications.
                throw new EJBException("Conflict writing entity bean");
            }
        } else {
            if (lazyregister) {
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, ident + " Register Ctx on " + tx);
                }
                if (!registerCtx(tx, bctx)) {
                    TraceEjb.context.log(BasicLevel.WARN, "Could not register Context");
                }
            }
            if (TraceEjb.isDebugTx()) {
                TraceEjb.tx.log(BasicLevel.DEBUG, ident + " Set writingtx");
            }
            writingtx = tx;
        }

    }

    /**
     * Notify a timeout for this bean and this Pk
     * @param timer timer whose expiration caused this notification.
     */
    public void notifyTimeout(final Timer timer) {
        if (bf.isStopped()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Container stopped");
            return;
        }
        if (isremoved) {
            TraceTimer.logger.log(BasicLevel.DEBUG, "Bean is removed");
            return;
        }
        if (TraceTimer.isDebug()) {
            TraceTimer.logger.log(BasicLevel.DEBUG, ident);
        }

        boolean committed = false;
        for (int nbretry = 0; ! committed && nbretry < MAX_NB_RETRY; nbretry++) {
            RequestCtx rctx = bf.preInvoke(bf.getTimerTxAttribute());
            try {
                JEntityContext bctx = getICtx(rctx.currTx, false);
                EntityBean eb = bctx.getInstance();
                bf.checkSecurity(null);
                if (eb instanceof TimedObject) {
                    TimedObject instance = (TimedObject) eb;
                    instance.ejbTimeout(timer);
                } else {
                    throw new EJBException("The bean does not implement the `TimedObject` interface");
                }
                committed = (rctx.currTx == null) ||
                (rctx.currTx.getStatus() != Status.STATUS_MARKED_ROLLBACK);
            } catch (EJBException e) {
                rctx.sysExc = e;
                throw e;
            } catch (RuntimeException e) {
                rctx.sysExc = e;
                throw new EJBException("RuntimeException thrown by an enterprise Bean", e);
            } catch (Error e) {
                rctx.sysExc = e;
                throw new EJBException("Error thrown by an enterprise Bean" + e);
            } catch (RemoteException e) {
                rctx.sysExc = e;
                throw new EJBException("Remote Exception raised:", e);
            } catch (SystemException e) {
                rctx.sysExc = e;
                throw new EJBException("Cannot get transaction status:", e);
            } finally {
                try {
                    bf.postInvoke(rctx);
                } finally {
                    releaseICtx(rctx.currTx, rctx.sysExc != null);
                }
            }
        }
    }

    /**
     * @return the Primary Key Object for this instance.
     */
    public Object getPrimaryKey() {
        if (pk == null) {
            throw new java.lang.IllegalStateException();
        }
        return pk;
    }

    /**
     * bind a JEntityContext for a create method.
     * @param tx - the Transaction object
     * @param bctx - the JEntityContext to bind
     */
    public void bindICtx(final Transaction tx, final JEntityContext bctx) {
        if (TraceEjb.isDebugSynchro()) {
            TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " tx=" + tx);
        }
        mapICtx(tx, bctx, true, false, false);
    }

    /**
     * Try to bind a JEntityContext if none already bound. Called by finder
     * methods. This is actually kind of optimization.
     * Can be bypassed if problems: just return false.
     * @param tx - the Transaction object
     * @param bctx The Entity Context
     * @param simple True if simple finder method
     * @return true if context has been bound to this EntitySwitch.
     */
    public synchronized boolean tryBindICtx(final Transaction tx, final JEntityContext bctx, final boolean simple) throws ObjectNotFoundException {

        if (TraceEjb.isDebugSynchro()) {
            TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " tx=" + tx);
        }

        // Don't bind if already a context.
        if (getContext4Tx(tx) != null) {
            if (TraceEjb.isDebugContext()) {
                TraceEjb.context.log(BasicLevel.DEBUG, ident + "context already mapped!");
            }
            if (getContext4Tx(tx).isMarkedRemoved()) {
                if (TraceEjb.isDebugContext()) {
                    TraceEjb.context.log(BasicLevel.DEBUG, ident + " currently being removed");
                }
                if (simple) {
                    throw new ObjectNotFoundException("Instance is currently being removed");
                } else {
                    // Don't throw exception in case of finder multiple.
                    return false;
                }
            }
            // bctx will be released by caller. (See JEntityHome.vm)
            return false;
        }

        // No synchronization, since this is used for readonly methods (finder);
        boolean isdirty = false;
        try {
            isdirty = bctx.initEntityContext(this);
            bctx.activate(true);
        } catch (Exception e) {
            TraceEjb.synchro.log(BasicLevel.WARN, ident + "Cannot bind Ctx: " + e);
            return false;
        }
        setContext4Tx(tx, bctx); // after activate (CRU => full parallellism)
        if (!lazyregister || isdirty) {
            if (tx == null) {
                if (TraceEjb.isDebugSynchro()) {
                    TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "IH find");
                }
            } else {
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, ident + "IT find: registerCtx");
                }
                if (!registerCtx(tx, bctx)) {
                    TraceEjb.context.log(BasicLevel.WARN, "Could not register context");
                    return false;
                }
            }
        }
        return true;
    }

    public void setInFindByPK(final boolean b) {
        infindbypk = b;
    }

    public boolean isInFindByPK() {
        return infindbypk;
    }

    /**
     * bind a JEntityContext for a remove method. called in case of remove(pk)
     * or remove(handle)
     * @param tx - the Transaction object
     * @param newctx - the JEntityContext to bind
     * @return the BeanContext
     */
    public JEntityContext getICtx(final Transaction tx, final JEntityContext newctx) {
        return mapICtx(tx, newctx, false, false, false);
    }

    /**
     * Get a context/instance associated with this transaction Called at each
     * request on the bean (including remove)
     * @param tx - the Transaction object
     * @param checkr - true if we must check non-reentrance.
     * @return the BeanContext
     */
    public JEntityContext getICtx(final Transaction tx, final boolean checkr) {
        return mapICtx(tx, null, false, true, checkr);
    }

    /**
     * Release completely this object, since another one will be used.
     * this occurs in case of create, when another EntitySwitch exist
     * already.
     * @param tx - the Transaction object
     */
    public synchronized boolean terminate(final Transaction tx) {
        TraceEjb.synchro.log(BasicLevel.DEBUG, ident);
        waitmyturn(tx);
        JEntityContext jec = getContext4Tx(tx);
        if (jec != null) {
            if (todiscard || jec.isMarkedRemoved()) {
                TraceEjb.logger.log(BasicLevel.DEBUG, "will discardContext");
                discardContext(tx, true, true);
            } else {
                try {
                    jec.storeIfModified();
                } catch (Exception e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, ident, "error while storing bean state:", e);
                }
                jec.passivate();
                discardContext(tx, false, true);
            }
            if (waiters > 0) {
                if (TraceEjb.isDebugSynchro()) {
                    TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " notify");
                }
                notifyAll();
            }
        }
        return true;
    }

    abstract void waitmyturn(Transaction tx);

    /**
     * Map a context and its instance.
     * @param tx - the Transaction object
     * @param bctx - the JEntityContext to bind if not null
     * @param forced - force to take this context. (case of create)
     * @param holdit - increment count to hold it, a release will be called
     *        later.
     * @param checkreentrance - true if we must check non-reentrance.
     * @return JEntityContext actually mapped
     */
    public synchronized JEntityContext mapICtx(final Transaction tx, final JEntityContext bctx, final boolean forced, final boolean holdit, final boolean checkreentrance) {

        try {
            // DEBUG only
            if (bf == null) {
                TraceEjb.synchro.log(BasicLevel.ERROR, "JEntitySwitch not initialized!");
                throw new EJBException("JEntitySwitch not initialized");
            }
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " tx=" + tx);
            }

            // Check non-reentrance (See spec EJB 2.1 section 12.1.13)
            if (!reentrant && checkreentrance) {
                if (runningtx != null && countIT > 0 && tx != null && tx.equals(runningtx)) {
                    throw new EJBException("non-reentrant bean accessed twice in same transaction");
                }
                if (tx == null && countIH > 0) {
                    throw new EJBException("non-reentrant bean accessed twice outside transaction");
                }
            }

            // Check that tx is not rollback and throw exception if so.
            // See bug #312334
            // Maybe this is not mandatory, since an exception should be
            // thrown later anyway, when trying to register the Context,
            // but it seems better to stop ASAP.
            if (tx != null) {
                int status = Status.STATUS_ROLLEDBACK;
                try {
                    status = tx.getStatus();
                } catch (SystemException e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, ident+ "cannot get transaction status");
                }
                switch (status) {
                    // Don't throw exception if setRollbackOnly (pb TCK)
                    // must go ahead and eventually throw IllegalStateException
                    // Case of fail setting with cascade delete beans.
                case Status.STATUS_ROLLEDBACK:
                case Status.STATUS_ROLLING_BACK:
                    if (TraceEjb.isVerbose()) {
                        TraceEjb.logger.log(BasicLevel.WARN, ident + "getICtx: transaction rolled back");
                    }
                    throw new TransactionRolledbackLocalException("Stop working on bean: Transaction Rolled back");
                }
            }

            // synchro (lock-policy dependant)
            waitmyturn(tx);

            // Set the timestamp to a big value because instance will be used.
            estimestamp = System.currentTimeMillis() + FEW_SECONDS;

            // Check the case where the object is detached
            if (isdetached) {
                JEntityFactory fact = bf;
                JEntitySwitch old = fact.existEJB(getPrimaryKey(), this, false);
                if (old != null) {
                    throw new NoSuchObjectLocalException("Inactivity timeout expired");
                }
                isdetached = false;
            }

            // Choose the context to use.
            boolean newtrans = false;
            boolean isdirty = false;
            JEntityContext jec = getContext4Tx(tx);
            if (forced) {
                // If the new context is enforced, we must first release the older
                if (jec != null) {
                    if (TraceEjb.isDebugContext()) {
                        TraceEjb.context.log(BasicLevel.DEBUG, ident + "new context is enforced!");
                    }
                    discardContext(tx, false, true);
                }
                jec = bctx;
                setContext4Tx(tx, jec);
                isdirty = jec.initEntityContext(this);
                newtrans = true;
                isremoved = false;  // in case of create following a remove

                if (bf.isClusterReplicated) {
                    HaService haService = bf.cont.getHaService();

                    if(haService != null && haService.isStarted()) {
                        haService.addEntityBean(jec);
                    }

                }
            } else {
                // First check if bean still exists
                if (isremoved) {
                    TraceEjb.logger.log(BasicLevel.WARN, ident + " has been removed.");
                    throw new NoSuchObjectLocalException("Try to access a bean previously removed");
                }
                if (jec != null) {
                    if (todiscard) {
                        TraceEjb.logger.log(BasicLevel.WARN, ident + " has been discarded.");
                        throw new NoSuchObjectLocalException("Try to access a bean previously discarded");
                    }
                    // Reuse the Context for this transaction.
                    // If a context was supplied, release it first.
                    if (bctx != null) {
                        if (TraceEjb.isDebugContext()) {
                            TraceEjb.context.log(BasicLevel.DEBUG, ident + " a context was supplied");
                        }
                        if (bctx.getMyTx() != null) {
                            TraceEjb.context.log(BasicLevel.WARN, "Will forget Tx!");
                        }
                        bf.releaseJContext(bctx, 2);
                    }
                    // In case the same instance is used for all tx, must check if
                    // new one. For LOCK_DATABASE policy, it cannot be a new tx.
                    if (runningtx == null && lockpolicy != EntityDesc.LOCK_DATABASE) {
                        newtrans = true;
                    }
                    jec.reuseEntityContext(newtrans);
                } else {
                    if (bctx != null) {
                        jec = bctx;
                    } else {
                        // no Context available : get one from the pool.
                        jec = bf.getJContext(this);
                    }
                    isdirty = jec.initEntityContext(this);
                    jec.activate(true);
                    setContext4Tx(tx, jec); // after activate
                    newtrans = true;
                }
            }

            if (tx != null) {
                // Register Context now, except if no new transaction
                if (newtrans && (!lazyregister|| isdirty)) {
                    try {
                        if (registerCtx(tx, jec)) {
                            if (TraceEjb.isDebugSynchro()) {
                                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IT: registerSynchronization");
                            }
                        } else {
                            // must not continue, since Tx is probably rolled back
                            TraceEjb.context.log(BasicLevel.WARN, ident + "Context not registered");
                            throw new IllegalStateException("Transaction probably rollback");
                        }
                    } catch (IllegalStateException e) {
                        TraceEjb.synchro.log(BasicLevel.WARN, ident + "mapICtx IT: not registered!", e);
                        // must not continue, since Tx is probably rolled back
                        // don't risk to modify database without registering the Ctx
                        throw e;
                    }
                }
            } else {
                if (holdit) {
                    if (TraceEjb.isDebugSynchro()) {
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IH count=" + countIH);
                    }
                    if ((shared || mustReload) && countIH == 0) {
                        // reload state that could have been modified by
                        // transactions.
                        jec.activate(false);
                        mustReload = false;
                    }
                }
                if (!inDirtyList && !txUpdates) {
                    inDirtyList = true;
                    bf.registerEJB(this);
                }
            }

            return jec;
        } finally {
            if (holdit) {
                if (tx == null) {
                    countIH++;
                } else {
                    countIT++;
                }
            }
        }
    }

    /**
     * Look if the specified transaction is blocked.
     * @param testedtx Transaction we look for
     * @return The blocking transaction
     */
    public Transaction getBlockingTx(final Transaction testedtx) {
        Transaction ret = null;
        if (runningtx != null && blockedtx.size() > 0) {
            try {
                for (Iterator<Transaction> i = blockedtx.iterator(); i.hasNext();) {
                    Transaction tx = i.next();
                    if (tx.equals(testedtx)) {
                        ret = runningtx;
                        break;
                    }
                }
            } catch (ConcurrentModificationException e) {
                TraceEjb.synchro.log(BasicLevel.WARN, "Concurrent access. Will retry later.");
            }
        }
        return ret;
    }

    /**
     * Release a context/instance at end of request.
     * @param tx - transaction associated to this context
     * @param discard - instance must be discarded
     */
    public synchronized void releaseICtx(final Transaction tx, final boolean discard) {

        if (tx == null) {
            // ------------- IH end -------------
            countIH--;
            if (TraceEjb.isDebugSynchro()) {
                    TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " countIH=" + countIH);
            }
            if (countIH == 0) {
                JEntityContext jec = getContext4Tx(tx);
                if (jec == null) {
                    TraceEjb.context.log(BasicLevel.ERROR, ident + " No context!");
                    Thread.dumpStack();
                    return;
                }
                if (jec.isMarkedRemoved()) {
                    discardContext(tx, true, true);
                } else if (todiscard || discard) {
                    discardContext(tx, false, false);
                } else {
                    if (mustStore) {
                        // This is for CS policy only
                        try {
                            jec.storeIfModified();
                        } catch (EJBException e) {
                            if (TraceEjb.isVerbose()) {
                                TraceEjb.logger.log(BasicLevel.WARN, ident + " ejbexception", e);
                            }
                        }
                        mustStore = false;
                    }
                    if (bf.tooManyInstances()) {
                        // Passivate if max-cache-size has been reached.
                        if (TraceEjb.isDebugContext()) {
                            TraceEjb.context.log(BasicLevel.DEBUG, jec + " passivated!");
                        }
                        jec.passivate();
                        if (jec.getMyTx() != null) {
                            TraceEjb.context.log(BasicLevel.WARN, "Will forget Tx!");
                        }
                        bf.releaseJContext(jec, 0);
                        removeContext4Tx(tx);
                    }
                }
                if (waiters > 0) {
                    if (TraceEjb.isDebugSynchro()) {
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " notify");
                    }
                    notifyAll();
                }
            } else {
                if (discard) {
                    todiscard = true;
                }
            }
        } else {
            // ------------- IT end -------------
            // nothing to do now. Wait for tx completed.
            countIT--;
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " countIT=" + countIT);
            }
            if (runningtx != null && discard) {
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, ident + " will be disarded when tx is over: " + runningtx);
                }
                todiscard = true;
            }
        }
    }

    public synchronized void forceDiscardICtx(final Transaction tx) {
        discardContext(tx, true, true);
    }

    /**
     * This transaction is now over. We can dispose of the instance for another
     * transaction or discard it.
     * A special implementation exists for RO policy
     * @param tx the transaction object
     * @param committed true if transaction was committed.
     */
    public synchronized void txCompleted(final Transaction tx, final boolean committed) {
        JEntityContext jec = getContext4Tx(tx);
        if (jec == null) {
            TraceEjb.context.log(BasicLevel.ERROR, ident + " No context for this tx");
            return;
        }
        runningtx = null;
        if (writingtx != null && tx.equals(writingtx)) {
            writingtx = null;
        }

        if (TraceEjb.isDebugSynchro()) {
            TraceEjb.synchro.log(BasicLevel.DEBUG, ident + (committed ? " commit" : " rollback"));
        }

        // The create was eventually rolled back or instance has been discarded.
        // We must remove PK entry.
        if (todiscard || (jec.isNewInstance() && !committed)) {
            if (TraceEjb.isDebugTx()) {
                TraceEjb.tx.log(BasicLevel.DEBUG, ident + " will be discarded");
            }
            if (writingtx != null) {
                TraceEjb.tx.log(BasicLevel.WARN, ident + " has a running tx:" + writingtx);
            }
            discardContext(tx, !todiscard, false);
            return;
        }

        if (jec.isMarkedRemoved()) {
            if (TraceEjb.isDebugContext()) {
                TraceEjb.context.log(BasicLevel.DEBUG, ident + " removed!");
            }
            discardContext(tx, committed, true);
        } else {
            if (shared || !committed || bf.tooManyInstances()) {
                // Passivate when shared, or if max-cache-size has been reached.
                if (TraceEjb.isDebugContext()) {
                    TraceEjb.context.log(BasicLevel.DEBUG, jec + " passivated!");
                }
                jec.passivate();
                // context is discarded if transaction rolled back
                if (!committed && writingtx != null) {
                    TraceEjb.tx.log(BasicLevel.WARN, ident + " !!! Discard but has a running tx:" + writingtx);
                }
                bf.releaseJContext(jec, committed ? 2 : 0);
                removeContext4Tx(tx);
            } else {
                // do this before reallocating the Context for another tx
                // (inside this lock)
                jec.detachTx();
            }
            // If special instance for no transactional accesses, it must be
            // reloaded
            if (lockpolicy == EntityDesc.LOCK_CONTAINER_READ_COMMITTED ||
                    lockpolicy == EntityDesc.LOCK_DATABASE) {
                mustReload = true;
            }
        }
        if (waiters > 0) {
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " notify");
            }
            notifyAll();
        }
    }

    /**
     * register a Context on the transaction, as a Synchronization. this will be
     * used later to store instance state when needed : before a finder, or at
     * beforeCompletion, and to release instance at commit.
     * @param tx Transaction object
     * @param bctx The Context to be registered
     * @return boolean true if registered
     */
    protected boolean registerCtx(final Transaction tx, final JEntityContext bctx) {
        // DEBUG only
        if (bf == null) {
            TraceEjb.synchro.log(BasicLevel.ERROR, "JEntitySwitch not initialized!");
            throw new EJBException("JEntitySwitch not initialized");
        }
        // register this context to the transaction, as a Synchronization.
        try {
            // Do this before, to test first if context still valid.
            if (! bctx.setRunningTx(tx)) {
                TraceEjb.context.log(BasicLevel.DEBUG, bctx + "already registered!");
                if (runningtx == null) {
                    TraceEjb.context.log(BasicLevel.ERROR, "runningtx = null");
                }
                return true;
            }
            if (bf.registerContext(tx, bctx)) {
                if (TraceEjb.isDebugContext()) {
                    TraceEjb.context.log(BasicLevel.DEBUG, bctx + " registered!");
                }
                runningtx = tx;
            } else {
                TraceEjb.context.log(BasicLevel.WARN, bctx + " could not be registered!");
                bctx.setRunningTx(null);
                return false;
            }
        } catch (IllegalStateException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, ident + "Transaction is in an illegal state");
            throw e;
        }
        return true;
    }

    /**
     * Detach entity switch from PK list.
     * This should trigger cleaning memory from all these objects by garbage collector.
     */
    protected void detachPk() {
        TraceEjb.swapper.log(BasicLevel.DEBUG, ident + " discarded on timeout");

        if (remote != null) {
            try {
                remote.unexportObject();
            } catch (NoSuchObjectException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, ident + " unexport entity failed: ", e);
            }
        }
        bf.removeEJB(getPrimaryKey());

        // this Entity switch can still be reached from another bean (CMR)
        // In this case, we'll have to reconnect it to the PK list.
        isdetached = true;
    }

    /**
     * Discard instance/Context and free all objects.
     * @param tx - the Transaction object
     * @param forgetpk - true if remove pk from the list
     * @param pool - true if instance can be pooled.
     */
    protected void discardContext(final Transaction tx, final boolean forgetpk, final boolean pool) {
        if (TraceEjb.isDebugTx()) {
            TraceEjb.tx.log(BasicLevel.DEBUG, "tx=" + tx);
        }
        JEntityContext jec = getContext4Tx(tx);
        if (jec != null) {
            if (runningtx != null && tx != null && tx.equals(runningtx)) {
                // This Context is registered in the tx
                // Must remove it from the list before releasing it.
                bf.unregisterContext(runningtx, jec);
                runningtx = null;
            }
            bf.releaseJContext(jec, pool ? 2 : 0);
            removeContext4Tx(tx);
        }
        if (forgetpk) {
            if (remote != null) {
                try {
                    remote.unexportObject();
                } catch (NoSuchObjectException e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, ident + " unexport entity failed: ", e);
                }
            }
            bf.removeEJB(getPrimaryKey());

            // Cancel all timers for this bean (= remove the TimerService)
            if (myTimerService != null) {
                ((JTimerService) myTimerService).cancelAllTimers();
                myTimerService = null;
            }
            // this Entity switch must not be used any longer.
            isremoved = true;
        }
        todiscard = false;  // done.
    }

    /**
     * @return lock policy for this bean
     */
    public int getPolicy() {
        return lockpolicy;
    }

    /**
     * @return State of this instance. State values are 0=in-tx, 1=out-tx, 2=idle,
     *         3=passive, 4=removed. we don't synchronize this method to avoid
     *         jadmin blocks
     */
    abstract public int getState();
    /**
     * @return the JFactory
     */
    public JFactory getBeanFactory() {
        return bf;
    }

}
