/**
 * 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 javax.ejb.EJBException;
import javax.ejb.TransactionRolledbackLocalException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

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


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

/**
 * Container Serialized (CS) lock-policy. Transaction Isolation managed by the
 * container. Non transactional methods of the bean can modify the bean.
 * @author Philippe Durieux
 */
public class JEntitySwitchCS extends JEntitySwitchCST {

    /**
     * time in millisec. to keep instance state in memory without storing it
     * After this time, instance is stored on database. before.
     */
    protected long passivationTimeout; // millisec.

    /**
     * timestamp uses for passivationTimeout
     */
    protected long ptimestamp;

    /**
     * empty constructor. Object is initialized via init() because it is
     * implemented differently according to jorm mappers.
     */
    public JEntitySwitchCS() {
        lockpolicy = EntityDesc.LOCK_CONTAINER_SERIALIZED;
        txUpdates = false;
    }

    protected void initpolicy(JEntityFactory bf) {
        lazyregister = false;
        passivationTimeout = bf.getPassivationTimeout() * 1000;
        ptimestamp = System.currentTimeMillis();
    }

    public void waitmyturn(Transaction tx) {
        // Synchronization.
        if (tx == null) {
            // Must wait in case of TX
            while (runningtx != null) {
                if (TraceEjb.isDebugSynchro()) {
                    TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IH: WAIT end IT");
                }
                waiters++;
                try {
                    wait(10000L);
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IH: NOTIFIED");
                } catch (InterruptedException e) {
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IH: INTERRUPTED");
                } catch (Exception e) {
                    throw new EJBException("JEntitySwitch synchronization pb", e);
                } finally {
                    waiters--;
                }
            }
        } else {
            int waitcount = 0;
            Transaction lastrunning = null;

            // Must wait in case of TX or if instance has been modified outside
            // transaction.
            // Don't wait transactions if 1 instance per transaction.
            while (inDirtyList || (runningtx != null && !tx.equals(runningtx))) {
                if (inDirtyList) {
                    // instance is dirty : must write it before working on it
                    // again.
                    // this is mandatory to be able to retrieve a good state in
                    // case
                    // of rollback.
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IT: WAIT end IH");
                    // Cannot passivate here, because must be done outside tx
                    // context
                    bf.synchronizeEntities();
                } else {
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IT: WAIT end IT");
                    // deadlock detection
                    blockedtx.add(tx);
                    if (waitcount > 0 && runningtx.equals(lastrunning) && bf.isDeadLocked(runningtx)) {
                        blockedtx.remove(tx);
                        try {
                            tx.setRollbackOnly();
                        } catch (SystemException e) {
                            TraceEjb.logger.log(BasicLevel.ERROR, ident
                                    + "getICtx IT: unexpected exception setting rollbackonly");
                        }
                        TraceEjb.logger.log(BasicLevel.WARN, ident + "getICtx IT: transaction rolled back");
                        throw new TransactionRolledbackLocalException("possible deadlock");
                    }
                    lastrunning = runningtx;
                }
                waitcount++;
                waiters++;
                try {
                    wait(deadlockTimeout);
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IT: NOTIFIED");
                } catch (InterruptedException e) {
                    if (TraceEjb.isDebugSynchro())
                        TraceEjb.synchro.log(BasicLevel.DEBUG, ident + "mapICtx IT: INTERRUPTED");
                } catch (Exception e) {
                    throw new EJBException("JEntitySwitch synchronization pb", e);
                } finally {
                    waiters--;
                    if (lastrunning != null) {
                        blockedtx.remove(tx);
                    }
                }
                // If transaction has been rolledback or set rollback only, give
                // up.
                int status = Status.STATUS_ROLLEDBACK;
                try {
                    status = tx.getStatus();
                } catch (SystemException e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, ident
                            + "getICtx IT: unexpected exception getting transaction status");
                }
                switch (status) {
                    case Status.STATUS_MARKED_ROLLBACK:
                    case Status.STATUS_ROLLEDBACK:
                    case Status.STATUS_ROLLING_BACK:
                        TraceEjb.logger.log(BasicLevel.WARN, ident + "getICtx IT: transaction rolled back");
                        throw new TransactionRolledbackLocalException("rollback occured while waiting");
                }
            }
        }

    }

    /**
     * try to passivate instances
     * @param store If we want to store the bean state first.
     * @param passivate If we want to pasivate the bean also
     * @return int result of operation: ALL_DONE = just remove from dirty list.
     *         STORED = remove from dirty list and call endIH NOT_DONE = keep it
     *         in the list.
     */
    public synchronized int passivateIH(boolean store, boolean passivate) {

        JEntityContext jec = getContext4Tx(null);

        // Check if we should store the bean state first.
        // This is CS policy specific.
        boolean toStore = store;
        if (!store && inDirtyList) {
            toStore = System.currentTimeMillis() - ptimestamp > passivationTimeout;
        }

        // If instance busy, do nothing now.
        if (runningtx != null || countIT > 0) {
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " used in TX");
            }
            return NOT_DONE;
        }
        if (countIH > 0) {
            // will be stored later, when released by last thread (countIH = 0)
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " used off TX");
            }
            mustStore = toStore;
            return toStore ? ALL_DONE : STORED;
        }

        int ret = toStore ? STORED : NOT_DONE;
        if (jec != null) {
            // If marked removed, don't need to store.
            if (jec.isMarkedRemoved()) {
                discardContext(null, true, true);
                return STORED;
            }
            // Store the bean state
            if (toStore) {
                if (TraceEjb.isDebugContext()) {
                    TraceEjb.context.log(BasicLevel.DEBUG, ident + " store object");
                }
                try {
                    jec.storeIfModified();
                } catch (Exception e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, ident, " error while storing bean state:", e);
                }
                mustStore = false;
                // restart timer to compute next time to store.
                ptimestamp = System.currentTimeMillis();
                // don't set dirty false here, will be done in endIH.
                // this prevent duplicates in dirty list.
            }
            // passivation has not been required: just return now.
            if (!passivate) {
                return ret;
            }
            // Don't passivate too recent instances to avoid problems at create
            if (System.currentTimeMillis() < estimestamp) {
                TraceEjb.context.log(BasicLevel.DEBUG, "too recent ");
                return ret;
            }

            // Passivate the instance.
            if (TraceEjb.isDebugContext()) {
                TraceEjb.context.log(BasicLevel.DEBUG, "passivate: " + jec);
            }
            if (!jec.passivate()) {
                return ret;
            }
            if (jec.getMyTx() != null) {
                TraceEjb.context.log(BasicLevel.WARN, "Will forget Tx???");
            }
            // Will be pooled only if min-pool-size not reached in free list.
            bf.releaseJContext(jec, 1);
            removeContext4Tx(null);
            // notify waiters for new instances
            if (waiters > 0) {
                notifyAll();
            }
        }

        // Instance is passivated, look if we can destroy the objects.
        if (inactivityTimeout > 0 && System.currentTimeMillis() - estimestamp > inactivityTimeout) {
            detachPk();
            estimestamp = System.currentTimeMillis();
            ret = ALL_DONE;
        }
        return ret;
    }

    /**
     * Instance is ready to use for new transaction.
     */
    public synchronized void endIH() {
        inDirtyList = false;
        if (getContext4Tx(null) == null) {
            if (TraceEjb.isDebugSynchro()) TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " discarded!");
            return;
        }
        if (TraceEjb.isDebugSynchro()) {
            if (countIH == 0) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " ready again");
            } else {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " busy!");
            }
        }
        if (waiters > 0) {
            if (TraceEjb.isDebugSynchro()) {
                TraceEjb.synchro.log(BasicLevel.DEBUG, ident + " notify");
            }
            notifyAll();
        }
    }

}
