/*
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@objectweb.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
 *
 * Initial developer(s): Philippe Coq, Philippe Durieux
 * Contributor(s): ______________________________________.
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas_ejb.container; 

import java.lang.IllegalStateException;
import java.rmi.RemoteException;
import javax.ejb.EntityBean;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.EJBException;
import javax.ejb.RemoveException;
import javax.naming.Context;
import javax.ejb.EntityContext;
import javax.transaction.Transaction;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;

/**
 * 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.
 */

public class JEntityContext extends JContext implements EntityContext, Synchronization {

    Transaction beanCoord = null;	// Transaction related to this synchronization
    private JEntitySwitch bs = null;
    boolean ismarkedremoved;     // true between a remove and the commit 
    boolean isnewinstance = false;	// true if just created in this transaction

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

    /**
     * Constructs an EntityContext
     * the Context has to be initialized after this.
     * @param bf -  the JEntityFactory
     * @param eb  - the Enterprise Bean instance
     */
    public JEntityContext(JEntityFactory bf, EntityBean eb) {
	super(bf, eb);
	// entity beans may have home or localHome
	this.home = bf.getHome();
	this.localhome = bf.getLocalHome();
    }

    // ------------------------------------------------------------------
    // 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. 
     * @exception 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 {
	TraceEjb.debugIc("JEntityContext.getEJBObject");
	if (ismarkedremoved || bs == null) {
	    if (ismarkedremoved)
		TraceEjb.error("EJB is marked removed");
	    else
		TraceEjb.error("EJB has no EntitySwitch");
	    throw new IllegalStateException("EJB is removed");
	}
	EJBObject ejbobject = bs.getRemote();
	if (ejbobject == null) {
	    throw new IllegalStateException("No Remote Interface for this bean");
	}
	return ejbobject;
    }

    /**
     * Obtain a reference to the EJB local object that is 
     * currently associated with the instance.
     * @returns The EJB local object currently associated with the instance.
     * @throws  java.lang.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 {
	TraceEjb.debugIc("JEntityContext.getEJBLocalObject");
	if (ismarkedremoved || bs == null) {
	    if (ismarkedremoved)
		TraceEjb.error("EJB is marked removed");
	    else
		TraceEjb.error("EJB has no EntitySwitch");
	    throw new IllegalStateException("EJB is removed");
	}
	EJBLocalObject ejblocalobject = bs.getLocal();
	if (ejblocalobject == null)
	    throw new IllegalStateException("No Local Interface for this bean");
	return ejblocalobject;
    }

    /**
     * Obtains the primary key of the EJB object that is currently associated 
     *  with this instance. 
     * @return  The EJB object currently associated with the instance.
     * @exception  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 {
	TraceEjb.debugIc("JEntityContext.getPrimaryKey");
	if (ismarkedremoved || bs == null) {
	    if (ismarkedremoved)
		TraceEjb.error("EJB is marked removed");
	    else
		TraceEjb.error("EJB has no EntitySwitch");
	    throw new IllegalStateException("EJB is removed");
	}
	return bs.getPrimaryKey();
    }

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

   /** 
     * This beforeCompletion method is called by the transaction 
     * manager prior to the start of the transaction completion process.
     * This method executes in the transaction context of the calling 
     * thread.
     * The Bean's state must be stored on the persistent storage before
     * the completion of the transaction.
    */
    public void beforeCompletion() {
	TraceEjb.debugIc("JEntityContext beforeCompletion");

	// If isMarkedRemoved, no need to do all this.
	if (ismarkedremoved) {
	    TraceEjb.debugIc("ismarkedremoved -> no ejbStore");
	    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;
	    }
	}

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

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

    /** 
     * The afterCompletion method is called by the transaction 
     * manager after the transaction is committed or rolled back. 
     * This method executes without a transaction context.
     *
     * @param status The status of the transaction completion.
     */
    public void afterCompletion(int status) {

	// Just check that we knew we were in a transaction (debug)
	if (beanCoord == null) {
	    TraceEjb.error("JEntityContext.afterCompletion: no tx");
	}

	boolean committed = (status == Status.STATUS_COMMITTED);

	if (committed) {
	    TraceEjb.debugIc("JEntityContext afterCompletion-committed");
	} else {
	    TraceEjb.verbose("JEntityContext afterCompletion-rolledback");
	}

	// Let the EntitySwitch now 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() {
	TraceEjb.debugIc("JEntityContext razEntityContext");
	bs = null;
	beanCoord = null;
	ismarkedremoved = false;
	isnewinstance = false;
    }
   
    /**
     * Reinit Context for reuse
     * @param tx - the Transaction where this instance will be used
     */
    public void initEntityContext(JEntitySwitch bs, Transaction tx) {
	TraceEjb.debugIc("JEntityContext initEntityContext");
	this.bs = bs;
	beanCoord = tx;
	ismarkedremoved = false;
    }

    /**
     * reuse EntityContext for another transaction.
     * optimization: don't pass it by the pool.
     */
    public void reuseEntityContext(Transaction tx) {
	TraceEjb.debugIc("JEntityContext reuseEntityContext");
	beanCoord = tx;
	ismarkedremoved = false;
	isnewinstance = false;
    }

    /**
     * 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.
     */
    public void setRemoved() throws RemoteException, RemoveException {
	TraceEjb.debugIc("JEntityContext setRemoved");
	EntityBean eb = (EntityBean) instance;

	if (instance == null) {
	    TraceEjb.error("JEntityContext.setRemoved: null instance!");
	    return;
	}
	// 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
     */
    public boolean isMarkedRemoved() {
	return ismarkedremoved;
    }

   /**
     * 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 (instance == null) {
	    TraceEjb.error("JEntityContext.getInstance: null!");
	    throw new RemoteException("No instance available");
	}
	return (EntityBean) instance;
    }

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

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

	EntityBean eb = (EntityBean) instance;

	if (ismarkedremoved) {
	    TraceEjb.debugIc("JEntityContext.storeIfModified: marked removed");
	    return;
	}
	TraceEjb.debugIc("JEntityContext.storeIfModified");

	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");
	} catch (EJBException e) {
	    TraceEjb.verbose("JEntityContext.storeIfModified raised EJBException");
	    throw e;
	} catch(RuntimeException e) {
	    TraceEjb.error("JEntityContext.storeIfModified : ", e);
	    throw new EJBException("Exception while storing data", e);
 	} catch(Error e) {
	    TraceEjb.error("JEntityContext.storeIfModified : ", e);
	    throw new EJBException("Error while storing data");
	}
    }

    /**
     * passivate this instance
     */
    public void passivate() {
	TraceEjb.debugIc("JEntityContext.passivate");

	EntityBean eb = (EntityBean) instance;

	try {
	    eb.ejbPassivate();
	} catch(Exception e) {
	    TraceEjb.error("JEntityContext.passivate : ejbPassivate failed", e);
 	} catch(Error e) {
	    TraceEjb.error("JEntityContext.passivate : ejbPassivate error", e);
	}
    }

   /** 
    * Activate instance.
    * @throws RemoteException if a system problem occured.
    */
    public void activate(boolean doactivate) {
	TraceEjb.debugIc("JEntityContext.activate");

	EntityBean eb = (EntityBean) instance;

	try {
	    if (doactivate) {
		// Call ejbActivate
		eb.ejbActivate();
	    }
	    // Load bean state from database
	    eb.ejbLoad();
	} catch(RemoteException e) {
	    TraceEjb.error("JEntityContext.activate: ", e);
	    throw new EJBException("Cannot activate bean");
	} catch(RuntimeException e) {
	    TraceEjb.error("JEntityContext.activate: ", e);
	    throw new EJBException("Cannot activate bean");
	} catch(Error e) {
	    TraceEjb.error("JEntityContext.activate : ", e);
	    throw new EJBException("Cannot activate bean");
	}
    }


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

    /*
     * abort current transaction if an exception occured while
     * accessing the bean state (load, store, ...)
     */
    private void abortTransaction() {
	TraceEjb.debugIc("JEntityContext abortTransaction");

	// If we are inside a transaction, it must be rolled back
	if (beanCoord != null) {
	    try {
		beanCoord.setRollbackOnly();
	    } catch (SystemException e) {
		TraceEjb.error("JEntityContext: Cannot setRollbackOnly");
	    }
	}
    }

}
