/*
 * 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 Durieux
 * Contributor(s):
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas_ejb.container;

import java.rmi.RemoteException;
import java.util.Hashtable;
import java.util.Properties;
import javax.ejb.EJBException;
import javax.ejb.TransactionRequiredLocalException;
import javax.ejb.TransactionRolledbackLocalException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import org.enhydra.naming.ContainerNaming;
import org.objectweb.jonas_ejb.deployment.api.BeanDesc;
import org.objectweb.jonas_ejb.deployment.api.MessageDrivenDesc;
import org.objectweb.jonas_ejb.deployment.api.MethodDesc;
import org.objectweb.jonas_ejb.lib.BeanNaming;

/**
 * This class is a factory for beans.
 */
abstract public class JFactory implements BeanFactory {

    protected JContainer cont;
    protected ContainerNaming naming = null;
    protected TransactionManager tm = null;
    protected Context JNDICtx = null; // JNDI Context for this component
    protected BeanNaming bn = null;
    protected String ejbname = null;
    protected BeanDesc dd;
    protected Properties ejb10Env = null;
    protected boolean txbeanmanaged = false; // always false if entity bean
    protected Class beanclass = null; // class of the bean instance

    /**
     * constructor (for entity)
     * must be without parameters (required by Jorm)
     */
    public JFactory() {
	TraceEjb.debugIc("JFactory constructor");
    }

    /**
     * constructor (for session)
     */
    public JFactory(BeanDesc dd, JContainer cont) {
	TraceEjb.debugIc("JFactory constructor");
	init(dd, cont);
    }

    /**
     * Init this object 
     * @args dd the deployment descriptor
     * @args cont the Container
     */
    public void init(BeanDesc dd, JContainer cont) {
	this.cont = cont;
	this.dd = dd;
	naming = cont.getContainerNaming();
	tm = cont.getTransactionManager();
	ejbname = dd.getEjbName();
	bn = new BeanNaming(dd);
	ejb10Env = dd.getEjb10Environment();

	// Create a JNDI context for this bean component
	try {
	    JNDICtx = naming.createEnvironmentContext(ejbname);
	    cont.setBeanEnvironment(JNDICtx, dd);
	} catch(NamingException e) {
	    TraceEjb.error("JFactory: Cannot build naming for this component", e);
	    throw new EJBException("Cannot build naming for this component"+ e);
	}

	// Get the class of the bean instance (for ejbCreate, ejbRemove, ...)
        String cn = null;
        try {
            cn = bn.getFullDerivedBeanName();
            beanclass = cont.getClassLoader().loadClass(cn);
	} catch (ClassNotFoundException e) {
	    TraceEjb.error("JFactory: Failed to find class " +cn+ e);
	    throw new EJBException("Container failed to find class " +cn);
	}

    }

    // ---------------------------------------------------------------
    // BeanFactory implementation
    // ---------------------------------------------------------------

    /**
     * get the bean name
     */
    public String getEJBName() {
	return ejbname;
    }

    /**
     * @return the Instance pool size for this Ejb
     */
    abstract public int getPoolSize();

    /**
     * Get the Deployement descriptor of this Ejb
     * @return BeanDesc The bean deployment descriptor
     */
    public BeanDesc getDeploymentDescriptor() {
	return dd;
    }

    /**
     * returns the TransactionManager
     */
    public TransactionManager getTransactionManager() {
	return tm;
    }

    /**
     * returns the JContainer object
     */
    public JContainer getContainer() {
	return cont;
    }

    /**
     * return the JNDI Enviromnment
     */
    public Hashtable getEnv() {
	return naming.getEnv();
    }

    /**
     * Returns the InitialContext
     */
    public InitialContext getInitialContext() {
	return naming.getInitialContext();
    }

    // ---------------------------------------------------------------
    // other public methods
    // ---------------------------------------------------------------

    /**
     * returns an InitialContext to access CosNaming
     */
    private static InitialContext corbaIctx = null;
    public static InitialContext getCorbaInitialContext() throws NamingException {
	if (corbaIctx == null) {
	    Properties properties = new Properties();
	    properties.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
	    String host = System.getProperty("david.CosNaming.default_host", "localhost");
	    String port = System.getProperty("david.CosNaming.default_port", "14727");
	    properties.put("java.naming.provider.url", "iiop://" + host + ":" + port);
	    corbaIctx =  new InitialContext(properties);
	}
	return corbaIctx;
    }

    /**
     * Returns the EJB 1.0 style environment associated with the Bean
     */
    public Properties getEjb10Environment() {
	return ejb10Env;
    }

    /**
     * returns true if transactions are managed inside the bean
     * returns false if transactions are managed by the container
     */
    public boolean isTxBeanManaged() {
	return txbeanmanaged;
    }

    /**
     * set the Component Context for JNDI environment
     * @returns previous Context
     */
    public Context setComponentContext() {
	return naming.setComponentContext(JNDICtx);
    }

    /**
     * reset old Component Context for JNDI environment
     */
    public void resetComponentContext(Context oldctx) {
	naming.setComponentContext(oldctx);
    }

    /**
     * Common preInvoke
     */
    public RequestCtx preInvoke(int txa, String secu) {

	TraceEjb.debugIc("JFactory.preInvoke");
	RequestCtx rctx = null;

	try {
	    // security controls (except for Message Driven Beans)
	    if (secu.length() > 0) {
		cont.checkSecurity(secu);
	    }

	    // Build a RequestCtx to save information about this request
	    // until the postInvoke time.
	    rctx = new RequestCtx(txa);

	    // Set bean context for JNDI and save the previous one in the
	    // RequestCtx to restore it at the end of the Request.
	    rctx.jndiCtx = setComponentContext();

	    // Check Transaction conformance to bean settings
	    checkTransaction(rctx);

	} catch (RuntimeException e) {
	    // Log RuntimeException before rethrowing it
	    // -> Print Stack Trace to see where the problem is.
	    TraceEjb.error("Unexpected Runtime Exception: "+e);
	    e.printStackTrace();
	    throw e;
	} catch (SystemException e) {
	    TraceEjb.error("Transaction Exception: "+e);
	    e.printStackTrace();
	    throw new EJBException("Transaction SystemException");
	}
	return rctx;
    }

    /**
     * Common postInvoke
     */
    public void postInvoke(RequestCtx rctx) {

	TraceEjb.debugIc("JFactory.postInvoke");

	try {
	    // Commit the transaction started in preInvoke
	    if (rctx.mustCommit) {
		if (rctx.sysExc != null) {
		    TraceEjb.error("System Exception raised by request:", rctx.sysExc);
		    // rollback the transaction
		    try {
			tm.rollback();
		    } catch (Exception e) {
			TraceEjb.error("Exception during rollback:", e);
		    }
		} else {
		    try {
			// Check status to avoid returning RollbackException to the client
			// instead of the application exception
			switch (tm.getStatus()) {
			case Status.STATUS_ACTIVE:
			    TraceEjb.debugIc("Committing transaction");
			    tm.commit();
			    break;
			case Status.STATUS_MARKED_ROLLBACK:
			    TraceEjb.debugIc("Rolling back transaction");
			    tm.rollback();
			    break;
			default:
			    TraceEjb.error("postInvoke: Unexpected transaction status "+tm.getStatus());
			    break;
			}
		    } catch (Exception e) {
			TraceEjb.error("postInvoke: ", e);
			throw new EJBException("Container exception", e);
		    }
		}
	    }

	    // We got a system Exception in business method:
	    // - log exception
	    // - discard instance
	    // - set client transaction rollback only
	    // - throw EJBException
	    if (rctx.sysExc != null) {
		// Log system exception
		TraceEjb.error("System Exception in business method:", rctx.sysExc);
		
		// If client transaction: set it rollback only and throw TransactionRolledBackLocalException
		Transaction currentTx = null;
		try {
		    currentTx = tm.getTransaction();
		    if (currentTx != null) {
			currentTx.setRollbackOnly();
			throw new TransactionRolledbackLocalException("System exception in business method");
		    }
		} catch (SystemException e) {
		    TraceEjb.error("postInvoke: cannot set rollback only current tx:", e);
		}
		return;
	    }
	    
	    // Reset bean context for JNDI
	    naming.setComponentContext(rctx.jndiCtx);
	    
	    // Resume client transaction suspended in preInvoke
	    Transaction tx = rctx.clientTx;
	    if (tx != null) {
		try {
		    tm.resume(tx);
		} catch (Exception e) {
		    TraceEjb.error("postInvoke: Cannot resume transaction");
		}
	    }
	    
	} catch (RuntimeException e) {
	    // Log RuntimeException before rethrowing it
	    // -> Print Stack Trace to see where the problem is.
	    TraceEjb.error("Unexpected Runtime Exception: "+e);
	    e.printStackTrace();
	    throw e;
	}
    }

    /**
     * preInvoke for Remote access
     */
    public RequestCtx preInvokeRemote(int txa, String secu) throws RemoteException {
	TraceEjb.debugIc("JFactory.preInvokeRemote");
	try {
	    return preInvoke(txa, secu);
	} catch(javax.ejb.TransactionRequiredLocalException e) {
	    throw new javax.transaction.TransactionRequiredException(e.getMessage());
	} catch(javax.ejb.TransactionRolledbackLocalException e) {
	    throw new javax.transaction.TransactionRolledbackException(e.getMessage());
	} catch(javax.ejb.NoSuchObjectLocalException e) {
	    throw new java.rmi.NoSuchObjectException(e.getMessage());
	} catch (javax.ejb.EJBException e) {
	    throw new java.rmi.RemoteException(e.getMessage());
	}
    }

    /**
     * postInvoke for Remote access
     */
    public void postInvokeRemote(RequestCtx rctx) throws RemoteException {
	TraceEjb.debugIc("JFactory.postInvokeRemote");
	try {
	    postInvoke(rctx);
	} catch(javax.ejb.TransactionRequiredLocalException e) {
	    throw new javax.transaction.TransactionRequiredException(e.getMessage());
	} catch(javax.ejb.TransactionRolledbackLocalException e) {
	    throw new javax.transaction.TransactionRolledbackException(e.getMessage());
	} catch(javax.ejb.NoSuchObjectLocalException e) {
	    throw new java.rmi.NoSuchObjectException(e.getMessage());
	} catch (javax.ejb.EJBException e) {
	    throw new java.rmi.RemoteException(e.getMessage());
	}
    }

    abstract void checkTransaction(RequestCtx rctx) throws SystemException;
    
    // ---------------------------------------------------------------
    // private methods
    // ---------------------------------------------------------------

    /**
     * Process Transaction Attribute before calling a business method
     * @throw EJBException
     * @throw TransactionRequiredLocalException
     */
    protected void checkTransactionContainer(RequestCtx rctx) {
	
	int txa = rctx.txAttr;
	TraceEjb.debugTx("JFactory.checkTransactionContainer: Transaction Attribute = ", MethodDesc.getTxAttributeName(txa));

	if (txa == MethodDesc.TX_NOT_SET) {
	    // No check to do (for example: session home)
	    return;
	}

	rctx.mustCommit = false;
	Transaction cltx = null;

	// First of all, get the current transaction
	Transaction currtx = null;
	try {
	    currtx = tm.getTransaction();
	} catch (SystemException e) {
	    TraceEjb.error("SystemException while getting transaction:", e);
	}

	// Raises exception if "Never" and we are in a transaction
	if (txa == MethodDesc.TX_NEVER && currtx != null) {
	    TraceEjb.verbose("Never and transaction not null");
	    throw new EJBException("Never attribute = caller must not be in a transaction");
	}

	// Raises exception if "Mandatory" and we are not in a transaction
	if (txa == MethodDesc.TX_MANDATORY && currtx == null) {
	    TraceEjb.verbose("Mandatory and not in transaction");
	    throw new TransactionRequiredLocalException("Mandatory attribute = caller must be in a transaction");
	}
	
	// Suspend transaction if "NotSupported" or "RequiresNew"
	if (currtx != null &&
	    (txa == MethodDesc.TX_REQUIRES_NEW || txa == MethodDesc.TX_NOT_SUPPORTED)) {
	    try {
		cltx = tm.suspend();
		currtx = null;
	    } catch (SystemException e) {
		TraceEjb.error("Cannot suspend transaction:\n", e);
		throw new EJBException("Cannot suspend transaction", e);
	    }
	}

	// Start a new transaction in 2 cases:
	// - "RequiresNew" attribute
	// - "Required" and no current transaction
	// this transaction will be closed at postInvoke.
	if (txa == MethodDesc.TX_REQUIRES_NEW || 
	    (txa == MethodDesc.TX_REQUIRED && currtx == null)) {
	    try {
		tm.begin();
		rctx.mustCommit = true;
		currtx = tm.getTransaction();
	    } catch (NotSupportedException e) {
		TraceEjb.error("Cannot start a transaction: NotSupportedException");
		throw new EJBException("Nested Transactions Not Supported");
	    } catch (SystemException e) {
		TraceEjb.error("Cannot start a transaction:\n", e);
		throw new EJBException("Cannot start a transaction: SystemException");
	    }
	}

	// update RequestCtx
	rctx.currTx = currtx;
    }


}
