/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2008 Bull S.A.S.
 * 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.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;

import javax.ejb.EJBException;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.ejb.TransactionRequiredLocalException;
import javax.ejb.TransactionRolledbackLocalException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.resource.spi.work.WorkManager;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionRequiredException;
import javax.transaction.TransactionRolledbackException;

import org.objectweb.util.monolog.api.BasicLevel;
import org.ow2.carol.rmi.exception.NoSuchObjectExceptionHelper;
import org.ow2.carol.util.configuration.ConfigurationRepository;
import org.ow2.jonas.Version;
import org.ow2.jonas.jndi.checker.api.IResourceCheckerManager;
import org.ow2.jonas.jndi.checker.api.IResourceCheckerInfo;
import org.ow2.jonas.jndi.checker.api.ResourceCheckpoints;
import org.ow2.jonas.deployment.ejb.BeanDesc;
import org.ow2.jonas.deployment.ejb.MethodDesc;
import org.ow2.jonas.ha.HaService;
import org.ow2.jonas.lib.security.context.SecurityContext;
import org.ow2.jonas.lib.security.context.SecurityCurrent;
import org.ow2.jonas.lib.timer.TraceTimer;
import org.ow2.jonas.lib.util.DirFilter;
import org.ow2.jonas.naming.JComponentContextFactory;
import org.ow2.jonas.naming.JNamingManager;
import org.ow2.jonas.tm.TransactionManager;
import org.ow2.util.event.api.IEventDispatcher;

/**
 * This class is a factory for beans.
 * @author Philippe Durieux
 * @author Florent Benoit (JACC security)
 * @author eyindanga (Life cycle dispatcher)
 */
public abstract class JFactory implements BeanFactory {

    protected JContainer cont;

    protected boolean stopped = false;

    protected JNamingManager naming = null;

    protected TransactionManager tm = null;

    protected WorkManager wm = null;

    protected Context JNDICtx = null; // JNDI Context for this component

    protected String ejbname = null;

    protected BeanDesc dd;

    protected Properties ejb10Env = null;

    protected TimerService myTimerService = null;

    protected boolean txbeanmanaged = false; // always false if entity bean

    protected Class<?> beanclass = null; // class of the bean instance

    protected File passivationDir;

    /**
     * initial value for pool size
     */
    protected int minPoolSize;

    protected int maxCacheSize;

    protected boolean isClusterReplicated;

    /**
     * Need for checking transaction interoperability which is only needs when
     * using iiop
     */
    private boolean iiopProtocolAvailable = false;

    /**
     * Transactional attribute for ejbTimeout method.
     * default is TX_REQUIRES_NEW
     */
    protected int timerTxAttr;

    /**
     * Signature for ejbTimeout.
     */
    protected String ejbTimeoutSignature;

    /**
     * Event dispatcher.
     */
    protected IEventDispatcher dispatcher;

    /**
     * Link to the resource checker manager.
     */
    private IResourceCheckerManager resourceCheckerManager = null;

    /**
     * constructor (for entity) must be without parameters (required by Jorm).
     */
    public JFactory() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, ejbname);
        }
    }

    public void stopContainer() {
        stopped = true;
    }

    public boolean isStopped() {
        return stopped;
    }

    /**
     * Get the directory where to store stateful state and timers
     */
    public File getPassivationDir() {
        return passivationDir;
    }

    /**
     * constructor (for session)
     * @param dd The bean deployment descriptor
     * @param cont the container for this bean
     */
    public JFactory(final BeanDesc dd, final JContainer cont) {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        init(dd, cont);
    }

    /**
     * Init this object
     * @param dd the deployment descriptor
     * @param cont the Container
     */
    public void init(final BeanDesc dd, final JContainer cont) {
        this.cont = cont;
        this.dd = dd;
        naming = cont.getContainerNaming();
        tm = cont.getTransactionManager();
        wm = cont.getWorkManager();
        ejbname = dd.getEjbName();
        ejb10Env = dd.getEjb10Environment();
        maxCacheSize = dd.getCacheMax();
        minPoolSize = dd.getPoolMin();
        isClusterReplicated = dd.isClusterReplicated();

        // Check the JOnAS version used for deployment in case of Session or
        // Entity
        String cln = dd.getFullWrpHomeName() != null ? dd.getFullWrpHomeName() : dd.getFullWrpLocalHomeName();
        if (cln != null) {
            checkJonasVersion(cln);
        }

        // Create a JNDI context for this bean component
        try {
            JComponentContextFactory ccf = cont.getComponentContextFactory();
            if (ccf == null) {
                throw new EJBException("No Component Context Factory");
            }
            JNDICtx = ccf.createComponentContext(ejbname);
            cont.setBeanEnvironment(JNDICtx, dd);
        } catch (NamingException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "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 = dd.getFullDerivedBeanName();
            beanclass = cont.getClassLoader().loadClass(cn);
            /*
             * This is currently being commented out. The spec doesn't exclude
             * having the same class multiple times, just that the first class
             * will be the one that is loaded. With this code included, this
             * precludes a user from using the same bean classes but with
             * different transaction attributes for example within the same
             * ear file.
             */
        } catch (ClassNotFoundException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "failed to find class " + cn + e);
            throw new EJBException("Container failed to find class " + cn, e);
        }

        timerTxAttr = dd.getTimerTxAttribute();
        ejbTimeoutSignature = dd.getEjbTimeoutSignature();

        // check TX interop if using iiop.
        String protocol = ConfigurationRepository.getCurrentConfiguration().getProtocol().getName();
        if (protocol.equals("iiop")) {
            iiopProtocolAvailable = true;
        }

        // mkdir a passivation directory if not exist yet.
        String pds = cont.getTmpDirName() + File.separator + dd.getIdentifier();
        passivationDir = new File(pds);
        if (! passivationDir.isDirectory()) {
            passivationDir.mkdir();
            TraceEjb.factory.log(BasicLevel.DEBUG, "created: " + pds);
        }
    }

    /**
     * @return Returns the WorkManager.
     */
    public WorkManager getWorkManager() {
        return wm;
    }

    /**
     * Init the pool of instances. Not used for stateful beans
     */
    public abstract void initInstancePool();

    /**
     * Restart Timers
     */
    public void restartTimers() {
        // Look for passivated Timers to reactivate.
        String[] timerList = getFileList(passivationDir, null, ".tim");
        for (int i = 0; i < timerList.length; i++) {
            File file = new File(passivationDir, timerList[i]);
            Timer timer = null;
            JObjectInputStream ois = null;
            try {
                TraceTimer.logger.log(BasicLevel.DEBUG, "restarting: " + timerList[i]);
                ois = new JObjectInputStream(new FileInputStream(file), cont.getClassLoader());
                JTimerHandle th = (JTimerHandle) ois.readObject();
                timer = th.restartTimer();
                file.delete();
                ois.close();
            } catch (Exception e) {
                // A Timer that cannot be restarted should not prevent the
                // Container
                // to be started. So, don't throw exception here.
                TraceTimer.logger.log(BasicLevel.ERROR, "Cannot restart timer: " + timerList[i]);
                TraceTimer.logger.log(BasicLevel.ERROR, "For bean: " + passivationDir);
                TraceTimer.logger.log(BasicLevel.ERROR, "Exception: " + e);
                if (timer != null) {
                    try {
                        timer.cancel();
                    } catch (Exception ign) {
                        TraceTimer.logger.log(BasicLevel.ERROR, "Cancel timer: " + e);
                    }
                }
            } finally {
                if (ois != null) {
                    try {
                        ois.close();
                    } catch (Exception ig) {
                        TraceTimer.logger.log(BasicLevel.WARN, "Cannot close ObjectInputStream: " + ig);
                    }
                }
            }
        }
    }

    /**
     * Assess availability of a class in a given class loader
     * @param className the name of the class without the .class extension
     * @param unique flag indicating if the class should be found only once in
     *        the classloader
     * @return True if class is available in the current class loader.
     */
    protected boolean isClassAvailable(String className, final boolean unique) {
        className = className.replace('.', '/') + ".class";
        Enumeration<URL> e = null;
        try {
            e = ((URLClassLoader) cont.getClassLoader()).findResources(className);
        } catch (IOException e1) {
            return false;
        }
        if (e.hasMoreElements()) {
            e.nextElement();
            return unique ? !e.hasMoreElements() : true;
        } else {
            return false;
        }
    }

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

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

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

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

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

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

    /**
     * @return A Hashtable containing the JNDI Environment
     */
    public Hashtable getEnv() {
        return naming.getEnv();
    }

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

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

    /**
     * @return the TimerService for this bean.
     * This works only for stateless or message driven beans.
     */
    public abstract TimerService getTimerService();

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

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

    /**
     * set the Component Context for JNDI environment
     * @return previous Context
     */
    public Context setComponentContext() {
        Context oldctx = naming.setComponentContext(JNDICtx);
        if (TraceEjb.isDebugLoaderLog() && oldctx == null) {
            TraceEjb.loaderlog.log(BasicLevel.DEBUG, "previous ctx was null");
        }
        return oldctx;
    }

    /**
     * reset old Component Context for JNDI environment
     * @param oldctx previous Component Context to restore.
     */
    public void resetComponentContext(final Context oldctx) {
        if (TraceEjb.isDebugLoaderLog() && oldctx == null) {
            TraceEjb.loaderlog.log(BasicLevel.DEBUG, "to null");
        }
        naming.resetComponentContext(oldctx);
    }

    /**
     * @return the transaction attribute for ejbTimeout method
     */
    public int getTimerTxAttribute() {
        return timerTxAttr;
    }

    /**
     * @return the security signature for ejbTimeout method.
     */
    public String getEjbTimeoutSignature() {
        return ejbTimeoutSignature;
    }

    /**
     * @return min pool size for Jmx
     */
    public int getMinPoolSize() {
        return minPoolSize;
    }

    /**
     * @return max cache size for Jmx
     */
    public int getMaxCacheSize() {
        return maxCacheSize;
    }

    /**
     * @return max cache size for Jmx
     */
    public int getCacheSize() {
            throw new IllegalStateException();
    }

    /**
    * Check if the access to the bean is authorized
    * @param ejbInv object containing security signature of the method, args of
    *        method, etc
    */
    public void checkSecurity(final EJBInvocation ejbInv) {

        String runAsRoleDD = null;

        try {
            // security controls (except for Message Driven Beans)
            if (ejbInv != null && ejbInv.methodPermissionSignature != null) {
                // Check the security with the identities of the callers
                // Establishing a run-as identity for an enterprise bean does
                // not affect the
                // identities of its callers, which are the identities tested
                // for the permission
                // to access the methods of the enterprise bean.
                // The run-as identity establishes the identity the enterprise
                // bean will
                // use when it makes calls
                // see 21.3.4.1 EJB 2.0 (Security Management / Run-as)
                if (ejbInv.methodPermissionSignature.length() != 0) {
                    cont.checkSecurity(ejbname, ejbInv, (dd.getRunAsRole() != null));
                }
            }

            runAsRoleDD = dd.getRunAsRole();
            // And now, push the run-as role if any
            // This role will be used for the calls of the bean
            if (runAsRoleDD != null) {
                SecurityCurrent current = SecurityCurrent.getCurrent();
                if (current != null) {
                    SecurityContext sctx = current.getSecurityContext();
                    if (sctx == null) {
                        if (TraceEjb.isDebugSecurity()) {
                            TraceEjb.security.log(BasicLevel.DEBUG, "runas : Security context is null, create a new one"
                                    + " in ejb " + ejbname);
                        }
                        sctx = new SecurityContext();
                        current.setSecurityContext(sctx);
                    }
                    String principalName = dd.getRunAsPrincipalName();
                    String[] runAsRoles = dd.getDeploymentDesc().getRolesForRunAsPrincipal(principalName);
                    if (runAsRoles == null) {
                        runAsRoles = new String[] {runAsRoleDD};
                    }
                    if (TraceEjb.isDebugSecurity()) {
                        TraceEjb.security.log(BasicLevel.DEBUG, "runAs roles are ");
                        for (int r = 0; r < runAsRoles.length; r++) {
                            TraceEjb.security.log(BasicLevel.DEBUG, "Role[" + r + "] = " + runAsRoles[r]);
                        }
                        TraceEjb.security.log(BasicLevel.DEBUG, "RunAs principal name = " + principalName);
                    }
                    sctx.pushRunAs(runAsRoleDD, principalName, runAsRoles);
                } else {
                    TraceEjb.security.log(BasicLevel.ERROR, "Can't push runas role as security current is null"
                            + " in ejb " + ejbname);
                }
            }
        } catch (RuntimeException re) {
            // pop run-as role
            if (runAsRoleDD != null) {
                SecurityCurrent current = SecurityCurrent.getCurrent();
                if (current != null) {
                    SecurityContext sctx = current.getSecurityContext();
                    if (sctx == null) {
                        if (TraceEjb.isDebugSecurity()) {
                            TraceEjb.security.log(BasicLevel.DEBUG, "runas : Security context is  null " + " in ejb "
                                + ejbname);
                        }
                    } else {
                        sctx.popRunAs();
                    }
                } else {
                    if (TraceEjb.isDebugSecurity()) {
                        TraceEjb.security.log(BasicLevel.DEBUG, "Can't pop runas role as security current is null"
                            + " in ejb " + ejbname);
                    }
                }
            }
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.logger.log(BasicLevel.DEBUG, "Security Runtime Exception", re);
            }
            throw re;
        }


    }

    /**
     * Common preInvoke
     * @param txa Transaction Attribute (Supports, Required, ...)
     * @return A RequestCtx object
     * @throws EJBException
     */
    public RequestCtx preInvoke(final int txa) {

        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        RequestCtx rctx = null;


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

            // Set classLoader to the correct value and save the previous one
            // in the RequestCtx to restore it at the end of the Request.
            rctx.cloader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(myClassLoader());

            // New stack
            if (resourceCheckerManager != null) {
                resourceCheckerManager.push();
            }

            // 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.logger.log(BasicLevel.ERROR, "unexpected Runtime Exception", e);
            throw e;
        }
        return rctx;
    }

    /**
     * Common postInvoke
     * @param rctx The RequestCtx that was returned at preInvoke()
     * @throws EJBException
     */
    public void postInvoke(final RequestCtx rctx) {

        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        // WARNING: All exceptions raised here will overload those raised
        // in the caller. (postInvoke is usually in a "finally" block)
        try {
            String runAsRoleDD = dd.getRunAsRole();
            // Pop the run-as role if any
            if (runAsRoleDD != null) {
                // Pop run-as role
                SecurityCurrent current = SecurityCurrent.getCurrent();
                if (current != null) {
                    SecurityContext sctx = current.getSecurityContext();
                    if (sctx == null) {
                        TraceEjb.security.log(BasicLevel.ERROR, "runas: Security context is null " + " in ejb "
                                + ejbname);
                    } else {
                        sctx.popRunAs();
                    }
                } else {
                    TraceEjb.security.log(BasicLevel.ERROR, "Can't pop runas role as security current is null"
                            + " in ejb " + ejbname);
                }
            }

            // Commit the transaction started in preInvoke
            if (rctx.mustCommit) {
                // Sanity check : transaction should be the same
                try {
                    Transaction t = tm.getTransaction();
                    if (t == null) {
                        TraceEjb.logger.log(BasicLevel.ERROR, "Transaction disappeared: " + rctx.currTx);
                        Thread.dumpStack();
                        throw new EJBException("null transaction");
                    }
                    if (rctx.currTx != t) {
                        TraceEjb.logger.log(BasicLevel.ERROR, "Transaction changed: " + rctx.currTx);
                        Thread.dumpStack();
                        throw new EJBException("bad transaction :" + t);
                    }
                } catch (Exception e) {
                    throw new EJBException("Cannot get current transaction:", e);
                }
                if (rctx.sysExc != null) {
                    TraceEjb.logger.log(BasicLevel.ERROR, "system exception raised by request:", rctx.sysExc);
                    // rollback the transaction
                    try {
                        tm.rollback();
                    } catch (Exception e) {
                        TraceEjb.logger.log(BasicLevel.ERROR, "exception during rollback:", e);
                    }
                } else {
                    HaService haService = cont.getHaService();

                    try {
                        // Check status to avoid returning RollbackException to
                        // the client
                        // instead of the application exception
                        switch (tm.getStatus()) {
                            case Status.STATUS_ACTIVE:

                                if(isClusterReplicated && haService != null && haService.isStarted()) {
                                    /*** REPLICATION CODE ***/
                                    haService.replicate();
                                    /*** REPLICATION CODE ***/
                                }

                                if (TraceEjb.isDebugTx()) {
                                    TraceEjb.tx.log(BasicLevel.DEBUG, "committing transaction: " + rctx.currTx);
                                }
                                tm.commit();

                                if(isClusterReplicated && haService != null && haService.isStarted()) {
                                    /*** REPLICATION CODE ***/
                                    haService.replicateCommit(true);
                                    /*** REPLICATION CODE ***/
                                }

                                break;
                            case Status.STATUS_MARKED_ROLLBACK:
                                if (TraceEjb.isDebugTx()) {
                                    TraceEjb.tx.log(BasicLevel.DEBUG, "rolling back transaction: " + rctx.currTx);
                                }
                                tm.rollback();

                                if(isClusterReplicated && haService != null && haService.isStarted()) {
                                    /*** REPLICATION CODE ***/
                                    haService.replicateCommit(false);
                                    /*** REPLICATION CODE ***/
                                }
                                break;
                            default:
                                TraceEjb.logger.log(BasicLevel.ERROR, "unexpected transaction status " + tm.getStatus());
                                TraceEjb.logger.log(BasicLevel.ERROR, "transaction: " + rctx.currTx);
                                Thread.dumpStack();

                                if(isClusterReplicated && haService != null && haService.isStarted()) {
                                    /*** REPLICATION CODE ***/
                                    haService.replicateCommit(false);
                                    /*** REPLICATION CODE ***/
                                }

                                throw new EJBException("unexpected transaction status :" + tm.getStatus());
                        }
                    } catch (RollbackException e) {
                        TraceEjb.logger.log(BasicLevel.WARN, "Could not commit transaction (rolled back)");

                        if(isClusterReplicated && haService != null && haService.isStarted()) {
                            /*** REPLICATION CODE ***/
                            haService.replicateCommit(false);
                            /*** REPLICATION CODE ***/
                        }

                        throw new TransactionRolledbackLocalException("Could not commit transaction", e);
                    } catch (Exception e) {
                        TraceEjb.logger.log(BasicLevel.WARN, "Could not commit transaction:" + e);

                        if(isClusterReplicated && haService != null && haService.isStarted()) {
                            /*** REPLICATION CODE ***/
                            haService.replicateCommit(false);
                            /*** REPLICATION CODE ***/
                        }

                        throw new EJBException("Container exception", e);
                    }
                }
            }

            // Perform check for stateless/MDB
            if ((resourceCheckerManager != null) &&(!(this instanceof JStatefulFactory))) {
                resourceCheckerManager.detect(new EJBResourceCheckerInfo(this, ResourceCheckpoints.EJB_POST_INVOKE));
            }

            // pop the stack
            // For stateful, needs to store the current list of resources
            if (resourceCheckerManager != null) {
                resourceCheckerManager.pop();
            }

            // Reset class loader to the original value
            Thread.currentThread().setContextClassLoader(rctx.cloader);

            // Resume client transaction suspended in preInvoke
            Transaction tx = rctx.clientTx;
            if (tx != null) {
                try {
                    if (TraceEjb.isDebugTx()) {
                        TraceEjb.tx.log(BasicLevel.DEBUG, "resuming transaction");
                    }
                    tm.resume(tx);
                } catch (Exception e) {
                    TraceEjb.logger.log(BasicLevel.ERROR, "cannot resume transaction", e);
                }
            }

            // Reset bean context for JNDI
            resetComponentContext(rctx.jndiCtx);

            // 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.logger.log(BasicLevel.ERROR, "system exception in business method:", rctx.sysExc);

                // The transaction is already rolled back, causing the exception
                // don't try to rollback it again.
                // The bean method has not been called.
                if (rctx.sysExc instanceof TransactionRolledbackLocalException) {
                    TraceEjb.logger.log(BasicLevel.DEBUG, "The transaction is already rolled back");
                    throw new TransactionRolledbackLocalException(rctx.sysExc.getMessage());
                }

                // If client transaction: set it rollback only and throw
                // TransactionRolledBackLocalException
                Transaction currentTx = null;
                try {
                    currentTx = tm.getTransaction();
                    // Must not rollback a suspended transaction just resumed (bug #306840)
                    if (currentTx != null && rctx.clientTx == null) {
                        TraceEjb.logger.log(BasicLevel.WARN, "Client transaction will rollback");
                        currentTx.setRollbackOnly();
                        // See spec EJB 18.3.1 + 18.4.2.3
                        if (rctx.bmcalled) {
                            throw new TransactionRolledbackLocalException(rctx.sysExc.getMessage());
                        }
                    }
                } catch (SystemException e) {
                    TraceEjb.logger.log(BasicLevel.DEBUG, "cannot set rollback only current tx:", e);
                }
                return;
            }


        } catch (TransactionRolledbackLocalException e) {
            throw e;
        } catch (EJBException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "ejbexception: ", e);
            throw e;
        } catch (RuntimeException e) {
            // Log RuntimeException before rethrowing it
            // -> Print Stack Trace to see where the problem is.
            TraceEjb.logger.log(BasicLevel.ERROR, "unexpected runtime exception: ", e);
            throw e;
        }
    }

    /**
     * preInvoke for Remote access
     * @param txa Transaction Attribute (Supports, Required, ...)
     * @return A RequestCtx object
     * @throws java.rmi.TransactionRequiredException
     * @throws java.rmi.TransactionRolledbackException
     * @throws java.rmi.NoSuchObjectException
     * @throws RemoteException preinvoke raised an EJBException
     */
    public RequestCtx preInvokeRemote(final int txa) throws RemoteException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        try {
            return preInvoke(txa);
        } catch (javax.ejb.TransactionRequiredLocalException e) {
            TransactionRequiredException tr = new TransactionRequiredException(e.getMessage());
            tr.detail = e;
            throw tr;
        } catch (javax.ejb.TransactionRolledbackLocalException e) {
            TransactionRolledbackException tr = new TransactionRolledbackException(e.getMessage());
            tr.detail = e;
            throw tr;
        } catch (javax.ejb.NoSuchObjectLocalException e) {
            throw NoSuchObjectExceptionHelper.create(e);
        } catch (javax.ejb.EJBException e) {
            RemoteException re = new RemoteException(e.getMessage());
            re.detail = e;
            throw re;
        }
    }

    /**
     * postInvoke for Remote access
     * @param rctx The RequestCtx that was returne t preInvoke()
     * @throws TransactionRequiredException
     * @throws TransactionRolledbackException
     * @throws NoSuchObjectException
     * @throws RemoteException postinvoke failed
     */
    public void postInvokeRemote(final RequestCtx rctx) throws RemoteException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        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(), e);
        }
    }

    /**
     * check Transaction attribute
     * @param rctx the Request Context
     */
    abstract void checkTransaction(RequestCtx rctx);

    // ---------------------------------------------------------------
    // private methods
    // ---------------------------------------------------------------
    /**
     * Check Transaction Interoperability requirements if the Tx comes
     * from another vendor.
     * For the moment JOnAS doesn't support tx interop.
     * See EJB 2.1 ?19.6.2.2.2
     *
     * @param txa transaction attribute
     *
     */
    private void checkTransactionInteroperability(final int txa) {
        if (tm.nonJotmTransactionContext()) {
            switch(txa) {
                // in the cases below, an exception is returned to the client side in order
                // to rollback the tx
                case MethodDesc.TX_MANDATORY:
                    TraceEjb.logger.log(BasicLevel.WARN, "mandatory and tx from another vendor");
                    throw new EJBException("Doesn't support transaction interoperability");

                case MethodDesc.TX_REQUIRED:
                    TraceEjb.logger.log(BasicLevel.WARN, "required and tx from another vendor");
                    throw new EJBException("Doesn't support transaction interoperability");

                case MethodDesc.TX_SUPPORTS:
                    TraceEjb.logger.log(BasicLevel.WARN, "supports and tx from another vendor");
                    throw new EJBException("Doesn't support transaction interoperability");
                default:
                    // nothing to do in particular for interop requirement
                    break;
            }
        }
    }

    /**
     * Process Transaction Attribute before calling a business method
     * @param rctx the Request Context
     * @throws EJBException
     * @throws TransactionRequiredLocalException
     */
    protected void checkTransactionContainer(final RequestCtx rctx) {

        int txa = rctx.txAttr;

        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.logger.log(BasicLevel.ERROR, "system exception while getting transaction:", e);
        }

        // Check requirements for transaction interoperability (iiop)
        if (iiopProtocolAvailable) {
            checkTransactionInteroperability(txa);
        }

        // Raises exception if "Never" and we are in a transaction
        if (txa == MethodDesc.TX_NEVER && currtx != null) {
            TraceEjb.logger.log(BasicLevel.WARN, "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.logger.log(BasicLevel.WARN, "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();
                if (cltx != null && TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, "Suspending client tx:" + cltx);
                }
                currtx = null;
            } catch (SystemException e) {
                TraceEjb.logger.log(BasicLevel.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();
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, "Start tx: " + currtx);
                }
            } catch (NotSupportedException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "cannot start a transaction: NotSupportedException");
                throw new EJBException("Nested Transactions Not Supported", e);
            } catch (SystemException e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "cannot start a transaction:\n", e);
                throw new EJBException("Cannot start a transaction: SystemException", e);
            }
        }

        // update RequestCtx
        rctx.currTx = currtx;
        rctx.clientTx = cltx;
    }

    /**
     * Check if the given class have been generated by GenIC tool with a correct
     * version. Trace an error message, if not.
     * @param clName class name
     */
    protected void checkJonasVersion(final String clName) {
        /*
         * Check if the static 'JONAS_VERSION' variable defined in the given
         * class (ie GenIC's Version used to deploy the bean), has the same
         * value as the JOnAS Version.NUMBER variable.
         */
        String fdName = "JONAS_VERSION";
        String gVersion = null;
        try {
            // The name of the resource ends with .class and the '.' are replace
            // with '/'
            String resourceName = clName.replace('.', '/') + ".class";

            // get the list of the resources for this class
            Enumeration<URL> e = null;
            try {
                e = cont.getClassLoader().getResources(resourceName);
            } catch (IOException ioe) {
                TraceEjb.logger.log(BasicLevel.ERROR, "failed to find class " + clName + ioe);
                throw new EJBException("Container failed to find class " + clName, ioe);
            }

            // Count the resources
            int nbCls = 0;
            String urls = "";
            while (e.hasMoreElements()) {
                nbCls++;
                urls += e.nextElement() + "\n";
            }

            // More than one resource.
            if (nbCls > 1) {
                TraceEjb.logger
                        .log(
                                BasicLevel.WARN,
                                "there are "
                                        + nbCls
                                        + " resources for the class "
                                        + clName
                                        + ". Some problems can occur because it's the first resource which will be loaded. The list of resources is : \n"
                                        + urls);
            }

            // Get the class and get the value of the "JONAS_VERSION" static
            // field
            Class<?> cl = cont.getClassLoader().loadClass(clName);
            Field fd = cl.getDeclaredField("JONAS_VERSION");
            gVersion = (String) fd.get(null);
        } catch (ClassNotFoundException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "failed to find class " + clName, e);
            return;
        } catch (NoSuchFieldException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "failed to find field " + fdName + " of class " + clName, e);
            return;
        } catch (IllegalAccessException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "failed to get the value of the field " + fdName + " of class "
                    + clName, e);
            return;
        }
        // Compare the current JOnAS version to the GenIC's Version used to
        // deploy the bean
        if (!Version.getNumber().equals(gVersion)) {
            TraceEjb.logger.log(BasicLevel.WARN, ejbname + "(Ver. " + gVersion
                    + ") bean not deployed with the same JOnAS version(" + Version.getNumber() + "). You have to redeploy "
                    + cont.getFileName() + ".");
        }
    }

    /**
     * @return the classloader for this container.
     */
    public ClassLoader myClassLoader() {
        return cont.getClassLoader();
    }

    /**
     * @return a list of files with this suffix in directory dstr.
     */
    public String[] getFileList(final File dir, final String prefix, final String suffix) {
        return dir.list(new DirFilter(prefix, suffix));
    }

    /**
     * @return the dispatcher
     */
    public IEventDispatcher getDispatcher() {
        return dispatcher;
    }

    /**
     * @param dispatcher the dispatcher to set
     */
    public void setDispatcher(final IEventDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    /**
     * @return the resource checker manager
     */
    public IResourceCheckerManager getResourceCheckerManager() {
        return resourceCheckerManager;
    }

    /**
     * Sets the resource checker manager.
     * @param resourceCheckerManager the given instance
     */
    public void setResourceCheckerManager(IResourceCheckerManager resourceCheckerManager) {
        this.resourceCheckerManager = resourceCheckerManager;
    }
}

/**
 * Checker info for EJB.
 * @author Florent Benoit
 */
class EJBResourceCheckerInfo implements IResourceCheckerInfo {

    /**
     * Caller info.
     */
    private String callerInfo = null;

    /**
     * Check point.
     */
    private ResourceCheckpoints checkPoint = null;

    /**
     * Build a checker info object based on the given request.
     * @param factory the given EJB factory
     */
    public EJBResourceCheckerInfo(final JFactory factory, final ResourceCheckpoints checkPoint) {
        this.callerInfo = factory.getContainer().getFileName() + ", beanName = " + factory.getEJBName();
        this.checkPoint = checkPoint;
    }

    /**
     * @return checkpoint of the caller.
     */
    public ResourceCheckpoints getCheckPoint() {
            return checkPoint;
        }

    /**
     * @return data for identifying the current caller (EJB Name, Servlet Name, etc)
     */
    public String getCallerInfo() {
           return callerInfo;
    }

}
