/**
 * 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.rmi.RemoteException;

import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;
import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.RemoveException;
import javax.ejb.TimerService;
import javax.transaction.Status;
import javax.transaction.Synchronization;
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;

/**
 * This class implements javax.ejb.EntityContext interface. An Entitycontext is
 * bound to a bean instance. To be used, it must be associated to a
 * JEntitySwitch, and possibly to a Transaction. In case the Context is used
 * inside a Transaction, we use the Synchronization interface to be aware of
 * transaction demarcations.
 * @author Philippe Coq, Philippe Durieux
 */
public class JEntityContext extends JContext implements EntityContext, Synchronization {

    /**
     * this instance has been modified
     */
    private boolean dirty = false;

    /**
     * This Context has been initialized, i.e. the EntitySwitch is
     * relevant (initEntityContext has been called)
     */
    private boolean initialized = false;

    /**
     * Transaction related to this synchronization
     */
    Transaction beanCoord = null;

    /**
     * @return the transaction associated to this context.
     */
    public Transaction getMyTx() {
        return beanCoord;
    }

    private boolean mustnotifywriting = false;

    private JEntitySwitch bs = null;

    /**
     * true between a remove and the commit
     */
    boolean ismarkedremoved;

    /**
     * true if just created in this transaction
     */
    boolean isnewinstance = false;

    // ------------------------------------------------------------------
    // constructors
    // ------------------------------------------------------------------

    /**
     * Constructs an EntityContext the Context has to be initialized after this.
     * @param bf - the JEntityFactory
     * @param eb - the Enterprise Bean instance
     */
    public JEntityContext(final JEntityFactory bf, final EntityBean eb) {
        super(bf, eb);
    }

    // ------------------------------------------------------------------
    // EJBContext implementation
    // ------------------------------------------------------------------

    /**
     * Get access to the EJB Timer Service.
     * @return the EJB Timer Service
     * @throws IllegalStateException Thrown if the instance is not allowed to
     *         use this method
     */
    @Override
    public TimerService getTimerService() throws IllegalStateException {
        int mystate = getState();
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "" + mystate);
        }
        switch (mystate) {
            case CTX_STATE_INITIAL:
                TraceEjb.logger.log(BasicLevel.ERROR, "not allowed here (ctx not initialized)");
                throw new IllegalStateException("getTimerService not allowed here (ctx not initialized)");
            case CTX_STATE_COMMITTING:
                TraceEjb.logger.log(BasicLevel.ERROR, "not allowed here (ctx committing)");
                throw new IllegalStateException("getTimerService not allowed here");
            case CTX_STATE_FINDING:
                TraceEjb.logger.log(BasicLevel.ERROR, "not allowed here (ctx finding)");
                throw new IllegalStateException("getTimerService not allowed here");
            case CTX_STATE_PASSIVE:
                // should work when called from ejbCreate,
                // but timer operations should not -> return a dummy
                // TimerService.
                return bf.getTimerService();
            default:
                return bs == null ? bf.getTimerService() : bs.getEntityTimerService();
        }
    }

    // ------------------------------------------------------------------
    // EntityContext implementation
    // ------------------------------------------------------------------

    /**
     * Obtains a reference to the EJB object that is currently associated with
     * the instance.
     * @return The EJB object currently associated with the instance.
     * @throws IllegalStateException Thrown if the instance invokes this method
     *         while the instance is in a state that does not allow the instance
     *         to invoke this method.
     */
    public EJBObject getEJBObject() throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        if (ismarkedremoved) {
            TraceEjb.logger.log(BasicLevel.ERROR, "marked removed");
            throw new IllegalStateException("EJB is removed");
        }
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "no EntitySwitch for " + this);
            throw new IllegalStateException("no EntitySwitch");
        }
        EJBObject ejbobject = bs.getRemote();
        if (ejbobject == null) {
            throw new IllegalStateException("No Remote Interface for " + this);
        }
        return ejbobject;
    }

    /**
     * Obtain a reference to the EJB local object that is currently associated
     * with the instance.
     * @param check if the local interface has been defined
     * @return The EJB local object currently associated with the instance.
     * @throws IllegalStateException - if the instance invokes this method while
     *         the instance is in a state that does not allow the instance to
     *         invoke this method, or if the instance does not have a local
     *         interface.
     */
    private EJBLocalObject getEJBLocalObject(final boolean check) throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        if (check) {
            if (!bf.dd.hasDefinedLocalInterface()) {
                TraceEjb.logger.log(BasicLevel.ERROR, "No Local Interface declared for this bean");
                throw new IllegalStateException("No Local Interface declared for this bean");
            }
        }
        if (ismarkedremoved) {
            TraceEjb.logger.log(BasicLevel.ERROR, "marked removed: " + this);
            throw new IllegalStateException("EJB is removed");
        }
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "no EntitySwitch for " + this);
            throw new IllegalStateException("no EntitySwitch");
        }
        EJBLocalObject ejblocalobject = bs.getLocal();
        if (ejblocalobject == null) {
            throw new IllegalStateException("No Local Object for " + this);
        }
        return ejblocalobject;
    }

    /**
     * Obtain a reference to the EJB local object that is currently associated
     * with the instance.
     * @return The EJB local object currently associated with the instance.
     * @throws IllegalStateException - if the instance invokes this method while
     *         the instance is in a state that does not allow the instance to
     *         invoke this method, or if the instance does not have a local
     *         interface.
     */
    public EJBLocalObject getEJBLocalObject() throws IllegalStateException {
        return getEJBLocalObject(true);
    }

    /**
     * Obtain a reference to the EJB local object that is currently associated
     * with the instance.
     * (internal use of getEJBLocalObject with no check).
     * @return The EJB local object currently associated with the instance.
     * @throws IllegalStateException - if the instance invokes this method while
     *         the instance is in a state that does not allow the instance to
     *         invoke this method, or if the instance does not have a local
     *         interface.
     */
    public EJBLocalObject get2EJBLocalObject() throws IllegalStateException {
         return getEJBLocalObject(false);
    }

    /**
     * Obtains the primary key of the EJB object that is currently associated
     * with this instance.
     * @return The EJB object currently associated with the instance.
     * @throws IllegalStateException Thrown if the instance invokes this method
     *         while the instance is in a state that does not allow the instance
     *         to invoke this method.
     */
    public Object getPrimaryKey() throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        if (ismarkedremoved) {
            TraceEjb.logger.log(BasicLevel.ERROR, "marked removed : " + this);
            throw new IllegalStateException("EJB is removed");
        }
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "no EntitySwitch : " + this);
            throw new IllegalStateException("no EntitySwitch");
        }
        return bs.getPrimaryKey();
    }

    // -------------------------------------------------------------------
    // Synchronization implementation
    // -------------------------------------------------------------------

    /**
     * This method is typically called at beforeCompletion
     */
    public void beforeCompletion() {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }

        // If isMarkedRemoved, no need to do all this.
        if (ismarkedremoved) {
            if (TraceEjb.isDebugContext()) {
                TraceEjb.context.log(BasicLevel.DEBUG, "ismarkedremoved -> no ejbStore");
            }
            return;
        }

        if (beanCoord == null) {
            // should not go here!
            TraceEjb.logger.log(BasicLevel.ERROR, "no tx for " + bs.getPrimaryKey());
            return;
        }

        // First check that the primary key is not null.
        // We must avoid accessing the DataBase when ejbCreate has failed.
        if (isnewinstance) {
            try {
                if (getPrimaryKey() == null) {
                    // We do not force abort creation in order
                    // to be able to catch the CreateException
                    // otherwise CreateException will be overloaded
                    // in postInvoke by "cannot commit exception"
                    return;
                }
            } catch (IllegalStateException e) {
                return;
            }
        }

        try {
            storeIfModified();
        } catch (EJBException e) {
            // Could not store bean data: set transaction rollback only
            abortTransaction();
        }
    }

    /**
     * This method is typically called after the transaction is committed.
     * @param status The status of the transaction completion.
     */
    public void afterCompletion(final int status) {

        boolean committed = (status == Status.STATUS_COMMITTED);
        if (TraceEjb.isDebugContext()) {
            if (committed) {
                TraceEjb.context.log(BasicLevel.DEBUG, "committed:" + bs.getPrimaryKey());
            } else {
                TraceEjb.context.log(BasicLevel.DEBUG, "rolledback:" + bs.getPrimaryKey());
            }
        }

        // Just check that we knew we were in a transaction (debug)
        if (beanCoord == null) {
            // should not go here!
            TraceEjb.logger.log(BasicLevel.ERROR, "no tx for " + bs.getPrimaryKey());
            return;
        }
        // Check also that the bs is not null (debug)
        if (bs == null) {
            // should not go here!
            TraceEjb.logger.log(BasicLevel.ERROR, "Context without EntitySwitch reference: " + this);
            throw new EJBException("Context with no Entity Switch");
        }

        // Let the EntitySwitch know that transaction is over and that it can
        // now dispose of this Context for another transaction.
        // no need to be in the correct component context ?
        bs.txCompleted(beanCoord, committed);
    }

    // -------------------------------------------------------------------
    // Other public methods
    // -------------------------------------------------------------------

    /**
     * Raz Context before freeing it. This is mainly for garbage collector.
     */
    public void razEntityContext() {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
        bs = null;
        beanCoord = null;
        ismarkedremoved = false;
        isnewinstance = false;
        initialized = false;
    }

    /**
     * Detach this Context from tx
     */
    public void detachTx() {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
        beanCoord = null;
        ismarkedremoved = false;
        isnewinstance = false;
    }

    /**
     * Reinit Context for reuse
     * @param bs - The Bean Switch this Context belongs to.
     */
    public boolean initEntityContext(final JEntitySwitch bs) {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
        // setEntitySwitch must not set initialized (See create method)
        setEntitySwitch(bs);
        initialized = true;
        // must consider the case where dirty was set too early (create case)
        return dirty;
     }

    /**
     * Associate transaction to ths Context
     * @param tx
     * @return true if correctly associated, false if was already done.
     */
    public boolean setRunningTx(final Transaction tx) {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "tx:" + tx);
        }
        if (tx != null && beanCoord != null) {
            TraceEjb.context.log(BasicLevel.DEBUG, "Already associated");
            return false;
        }
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "no Entity Switch for " + this);
            throw new EJBException("No Entity Switch for this EJBContext");
        }
        beanCoord = tx;
        return true;
    }

    /**
     * reuse EntityContext for another transaction. optimization: don't pass it
     * by the pool.
     * @param newtrans true if new transaction
     */
    public void reuseEntityContext(final boolean newtrans) {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, ":" + newtrans);
        }

        // This prevents reusing an instance removed in the same transaction.
        if (ismarkedremoved) {
            TraceEjb.context.log(BasicLevel.WARN, "Try to access a deleted object");
            throw new NoSuchObjectLocalException("Instance has been removed");
        }
        if (newtrans) {
            isnewinstance = false;
        }
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "no Entity Switch for " + this);
            throw new EJBException("Internal Error");
        }
    }

    /**
     * Set new instance flag
     */
    public void setNewInstance() {
        isnewinstance = true;
    }

    /**
     * Mark this context as removed. Complete removing will be achieved at the
     * end of the transaction.
     * @throws RemoteException ejbRemove failed
     * @throws RemoveException ejbRemove failed
     */
    public void setRemoved() throws RemoteException, RemoveException {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
        EntityBean eb = (EntityBean) myinstance;

        if (myinstance == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "null instance!");
            return;
        }
        // Set dirty to isolate transactions ???
        setDirty(true);

        // call ejbRemove() on instance
        eb.ejbRemove();
        // getPrimaryKey and getEJBObject should work in ejbRemove.
        // => we do this only after ejbRemove call.
        ismarkedremoved = true;
    }

    /**
     * Check if context has been marked removed
     * @return true when instance is marked removed.
     */
    public boolean isMarkedRemoved() {
        return ismarkedremoved;
    }

    /**
     * Check if context is a newly created instance
     * @return true when instance has been created in the current transaction.
     */
    public boolean isNewInstance() {
        return isnewinstance;
    }

    /**
     * Returns the bean instance of this context Used in the generated classes
     * to retrieve the instance
     * @return the bean instance
     * @throws RemoteException if no instance.
     */
    // RemoteException is throwned to allow simpler genic templates
    public EntityBean getInstance() throws RemoteException {
        if (myinstance == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "null!");
            throw new RemoteException("No instance available");
        }
        return (EntityBean) myinstance;
    }

    /**
     * JEntityFactory accessor
     * @return the JEntityFactory
     */
    public JEntityFactory getEntityFactory() {
        return (JEntityFactory) bf;
    }

    /**
     * JEntitySwitch accessor
     * @return the JEntitySwitch
     */
    public JEntitySwitch getEntitySwitch() {
        if (bs == null) {
            TraceEjb.logger.log(BasicLevel.WARN, "no ES for ctx:" + this);
        }
        return bs;
    }

    /**
     * @return True if Entity Switch has been already associated
     */
    public boolean isInitialized() {
        return bs != null;
    }

    /**
     * set the EntitySwitch
     * @param bs - the EntitySwitch
     */
    public void setEntitySwitch(final JEntitySwitch bs) {
        this.bs = bs;
        TraceEjb.context.log(BasicLevel.DEBUG, "");
        if (ismarkedremoved) {
            TraceEjb.logger.log(BasicLevel.ERROR, "ismarkedremoved WAS SET");
            Thread.dumpStack();
            ismarkedremoved = false;
        }
        // RO = never write. For other policies, must notify writing if lazyRegister
        // has been set. This is only possible in CMP2.
        mustnotifywriting = (bs.getPolicy() != EntityDesc.LOCK_READ_ONLY) && bs.lazyRegistering();
    }

    /**
     * @return true if instance has been modified
     */
    public boolean isDirty() {
        return dirty;
    }

    /**
     * Set the dirty flag: true = instance modified.
     */
    public void setDirty(final boolean d) {
        if (d) {
            // Add the Bean to the list of modified beans in HA
            if (!dirty && (bs != null) && (bs.bf != null) && (bs.bf.dd != null) && bs.bf.dd.isClusterReplicated()) {
                HaService haService = bs.bf.cont.getHaService();

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

            TraceEjb.context.log(BasicLevel.DEBUG, "true");
            // Notify the EntitySwitch at 1st writing
            if (mustnotifywriting && initialized) {
                // retrieve the current transaction.
                Transaction tx = null;
                try {
                    tx = tm.getTransaction();
                } catch (SystemException e) {
                    TraceEjb.context.log(BasicLevel.ERROR, "getTransaction failed", e);
                }
                if (tx == null) {
                    TraceEjb.logger.log(BasicLevel.WARN, "You should not modify the bean without a transactional context");
                } else {
                    bs.notifyWriting(tx, this);
                }
            } else {
                if (!initialized) {
                    TraceEjb.context.log(BasicLevel.DEBUG, "not initialized");
                } else {
                    if (bs.isWriteInsideTx()) {
                        // Check that we do not modify outside a transaction
                        Transaction tx = null;
                        try {
                            tx = tm.getTransaction();
                        } catch (SystemException e) {
                            TraceEjb.context.log(BasicLevel.ERROR, "getTransaction failed", e);
                        }
                        if (tx == null) {
                            TraceEjb.logger.log(BasicLevel.WARN, "Modifying " + bf.getEJBName() + " with no transactional context");
                            TraceEjb.logger.log(BasicLevel.WARN, "You should either set the bean transaction attribute to Required...");
                            TraceEjb.logger.log(BasicLevel.WARN, "... or change the lock policy to container-serialized (deprecated)");
                        }
                    }
                }
            }
        }
        // Must be called after notifyWriting
        dirty = d;
    }

    /**
     * Persistence: write data on storage
     */
    public void storeIfModified() {

        EntityBean eb = (EntityBean) myinstance;

        if (ismarkedremoved) {
            // TODO something smart to do here ?
            if (TraceEjb.isDebugContext()) {
                TraceEjb.context.log(BasicLevel.DEBUG, "marked removed");
            }
            return;
        }
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }

        try {
            // this method can fail if database serializes transactions
            // This may do nothing if bean has not been modified (isModified
            // optimization)
            eb.ejbStore();
        } catch (RemoteException e) {
            throw new EJBException("Exception while storing data", e);
        } catch (EJBException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "raised EJBException ", e);
            throw e;
        } catch (RuntimeException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "runtime exception: ", e);
            throw new EJBException("Exception while storing data", e);
        } catch (Error e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "error: ", e);
            throw new EJBException("Error while storing data");
        }
    }

    /**
     * passivate this instance
     * @return true if passivated.
     */
    public boolean passivate() {

        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }

        EntityBean eb = (EntityBean) myinstance;
        setPassive();

        try {
            eb.ejbPassivate();
        } catch (Exception e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "ejbPassivate failed", e);
        } catch (Error e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "ejbPassivate error", e);
        }
        return true;
    }

    /**
     * Activate instance.
     * @param doactivate True if ejbActivate() is called before ejbLoad()
     */
    public void activate(final boolean doactivate) {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }

        EntityBean eb = (EntityBean) myinstance;

        try {
            if (doactivate) {
                setPassive();
                // Call ejbActivate
                eb.ejbActivate();
            }
            setActive();
            // Load bean state from database
            eb.ejbLoad();
        } catch (RemoteException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "remote exception: ", e);
            throw new EJBException("Cannot activate bean", e);
        } catch (RuntimeException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "runtime exception: ", e);
            throw new EJBException("Cannot activate bean", e);
        } catch (Error e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "error: ", e);
            throw new EJBException("Cannot activate bean");
        }
    }

    // ----------------------------------------------------------------
    // private methods
    // ----------------------------------------------------------------

    /**
     * abort current transaction if an exception occurred while accessing the
     * bean state (load, store, ...)
     */
    private void abortTransaction() {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }

        // If we are inside a transaction, it must be rolled back
        if (beanCoord != null) {
            try {
                beanCoord.setRollbackOnly();
            } catch (SystemException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "cannot setRollbackOnly", e);
            }
        }
    }

}
