/**
 * 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: JMessageDrivenBean.java 15428 2008-10-07 11:20:29Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.security.Identity;
import java.security.Principal;
import java.util.Properties;

import javax.ejb.EJBException;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.MessageDrivenBean;
import javax.ejb.MessageDrivenContext;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ServerSession;
import javax.jms.Session;
import javax.jms.XASession;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.resource.spi.work.Work;
import javax.resource.spi.work.WorkException;
import javax.resource.spi.work.WorkManager;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import javax.transaction.xa.XAResource;

import org.ow2.jonas.tm.TransactionManager;



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

/**
 * Generic interposed class for Message Driven Beans This class presents these
 * interfaces, depending on object reached: ServerSession interface to the
 * ServerSessionPool MessageDrivenContext interface to the bean instance
 * MessageListener interface to the JMS Session Runnable interface to the
 * ThreadPool
 * @author Philippe Coq, Philippe Durieux
 * @author Christophe Ney (Easier Enhydra integration)
 */
public class JMessageDrivenBean implements MessageListener, ServerSession, Work, MessageDrivenContext {

    /**
     * java:comp/env prefix.
     */
    private static final String JAVA_COMP_ENV = "java:comp/env/";

    protected Session sess = null;

    protected JMdbFactory bf = null;

    protected MessageDrivenBean mdb = null;

    /**
     * Transactional attribute for onMessage method.
     * TX_NOT_SUPPORTED, TX_REQUIRED or TX_NOT_SET (= bean managed)
     */
    protected int txattr;

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

    protected TransactionManager tm = null;

    protected WorkManager wm = null;

    /**
     * constructor
     * @param bf The MDB Factory
     * @param sess The JMS Session
     * @param mdb The Message Driven Bean
     * @param wm The Work Manager
     */
    public JMessageDrivenBean(JMdbFactory bf, Session sess, MessageDrivenBean mdb, WorkManager wm) {
        this.bf = bf;
        this.sess = sess;
        this.mdb = mdb;
        this.wm = wm;
        // keep these locally for efficiency.
        txattr = bf.getTransactionAttribute();
        timerTxAttr = bf.getTimerTxAttribute();
        tm = bf.getTransactionManager();
    }

    // ------------------------------------------------------------------
    // 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
     */
    public TimerService getTimerService() throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        return bf.getTimerService();
    }

    // ----------------------------------------------------------------------
    // javax.jms.MessageListener implementation
    // ----------------------------------------------------------------------

    /**
     * A message has been received by the Session. Basically, we have to do:
     * preInvoke + onMessage + postInvoke. No exception should be returned to
     * the caller.
     * @param message The received message to handle.
     */
    public synchronized void onMessage(Message message) {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }

        RequestCtx rctx = null;
        try {

            if (tm.getTransaction() != null) {
                // This should not occur (DEBUG)
                TraceEjb.logger.log(BasicLevel.ERROR, "Transaction already OPENED");
                TraceEjb.logger.log(BasicLevel.ERROR, "Transaction = " + tm.getTransaction());
                Thread.dumpStack();
                return;
            }

            rctx = bf.preInvoke(txattr);
            bf.checkSecurity(null);
            if (rctx.mustCommit) {
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, "enlistResource");
                }
                rctx.currTx.enlistResource(((XASession) sess).getXAResource());
            }
        } catch (Exception e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "preInvoke failed: ", e);
            return;
        }
        try {
            if (TraceEjb.isDebugJms()) {
                TraceEjb.mdb.log(BasicLevel.DEBUG, "Call MDB");
            }
            ((MessageListener) mdb).onMessage(message);
            if (TraceEjb.isDebugJms()) {
                TraceEjb.mdb.log(BasicLevel.DEBUG, "Return from MDB");
            }
        } catch (RuntimeException e) {
            rctx.sysExc = e;
            TraceEjb.logger.log(BasicLevel.ERROR, "runtime exception thrown by an enterprise Bean", e);
        } catch (Error e) {
            rctx.sysExc = e;
            TraceEjb.logger.log(BasicLevel.ERROR, "error thrown by an enterprise Bean", e);
        } finally {
            try {
                if (rctx.mustCommit) {
                    if (TraceEjb.isDebugTx()) {
                        TraceEjb.tx.log(BasicLevel.DEBUG, "delistResource");
                    }
                    rctx.currTx.delistResource(((XASession) sess).getXAResource(), XAResource.TMSUCCESS);
                }
                bf.postInvoke(rctx);
            } catch (Exception e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "exception on postInvoke: ", e);
            }
        }
    }

    // ----------------------------------------------------------------------
    // javax.jms.ServerSession implementation
    // ----------------------------------------------------------------------

    /**
     * Return the ServerSession's Session. This must be a Session created by the
     * same Connection which will be dispatching messages to it. The provider
     * will assign one or more messages to the Session and then call start on
     * the ServerSession.
     * @return the server session's session.
     * @exception JMSException - if a JMS fails to get associated session for
     *            this serverSession due to some internal error.
     */
    public Session getSession() throws JMSException {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }
        return sess;
    }

    /**
     * Cause the session's run method to be called to process messages that were
     * just assigned to it.
     * @exception JMSException - if a JMS fails to start the server session to
     *            process messages.
     */
    public void start() throws JMSException {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }
        try {
            wm.scheduleWork(this);
        } catch (WorkException e) {
            JMSException jmsE = new JMSException("Cannot schedule work");
            jmsE.initCause(e);
            throw jmsE;
        }
    }

    // ----------------------------------------------------------------------
    // Work implementation
    // ----------------------------------------------------------------------

    /**
     * Process messages by calling run method on Session. When finished, return
     * the object in the pool.
     */
    public void run() {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }

        // this thread must have the classloader of this container to
        // be able to retrieve beans called from a MDB.
        Thread.currentThread().setContextClassLoader(bf.myClassLoader());

        sess.run();
        bf.releaseServerSession(this);
    }

    public void release() {
        TraceEjb.mdb.log(BasicLevel.WARN, "Ignored");
    }

    // ----------------------------------------------------------------------
    // javax.ejb.MessageDrivenContext implementation
    // ----------------------------------------------------------------------

    private static final String DISALLOWED_MSG = " is disallowed in a message driven bean";

    /**
     * Obtains the java.security.Identity of the caller. disallowed in
     * messagedriven bean method because there is no security context
     * @deprecated @exception java.lang.IllegalStateException always
     */
    public Identity getCallerIdentity() {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("getCallerIdentity()" + DISALLOWED_MSG);
    }

    /**
     * Obtain the java.security.Principal that identifies the caller. throws a
     * java.lang.IllegalStateException for message driven bean because there is
     * no security context available (EJB v2.0, chapter 14.5.1)
     * @exception java.lang.IllegalStateException always
     */
    public Principal getCallerPrincipal() {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("getCallerPrincipal()" + DISALLOWED_MSG);
    }

    /**
     * Test if the caller has a given role.
     * @deprecated @throws java.lang.IllegalStateException for message driven
     *             bean because there is no security context available
     */
    public boolean isCallerInRole(Identity role) {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("isCallerInRole()" + DISALLOWED_MSG);
    }

    /**
     * Test if the caller has a given role.
     * @throws java.lang.IllegalStateException for message driven bean because
     *         there is no security context available
     */
    public boolean isCallerInRole(java.lang.String roleLink) {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("isCallerInRole()" + DISALLOWED_MSG);
    }

    /**
     * Marks the current transaction for rollback. Should be used only if the
     * instance is associated with a transaction
     * @throws java.lang.IllegalStateException if the instance is not associated
     *         with a transaction
     */
    public void setRollbackOnly() {

        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }
        try {
            tm.setRollbackOnly();
        } catch (IllegalStateException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "current thread not associated with transaction");
            throw e;
        } catch (SystemException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "unexpected exception:", e);
        }
    }

    /**
     * Tests if the transaction has been marked for rollback only.
     * @return True if transaction has been marked for rollback.
     */
    public boolean getRollbackOnly() {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }

        try {
            if (tm.getTransaction() != null) {
                switch (tm.getStatus()) {
                case Status.STATUS_MARKED_ROLLBACK:
                case Status.STATUS_ROLLEDBACK:
                case Status.STATUS_ROLLING_BACK:
                    return true;
                case Status.STATUS_NO_TRANSACTION:
                    throw new IllegalStateException("No transaction");
                default:
                    return false;
                }
            } else {
                TraceEjb.logger.log(BasicLevel.ERROR, "the bean is not associated in a transaction");
                throw new IllegalStateException("the message driven bean is not associated in a transaction");
            }
        } catch (SystemException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "cannot get status:", e);
            return false;
        }
    }

    /**
     * Is disallowed. There is no home for message driven bean.
     * @throws IllegalStateException Always.
     */
    public EJBHome getEJBHome() {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("getEJBHome()" + DISALLOWED_MSG);
    }

    /**
     * Is disallowed. There is no local home for message driven bean.
     * @throws IllegalStateException Always.
     */
    public EJBLocalHome getEJBLocalHome() {
        TraceEjb.logger.log(BasicLevel.ERROR, DISALLOWED_MSG);
        throw new IllegalStateException("getEJBLocalHome()" + DISALLOWED_MSG);
    }

    /**
     * @deprecated Use the JNDI naming context java:comp/env instead.
     * @return properties for the bean.
     */
    public Properties getEnvironment() {
        TraceEjb.logger.log(BasicLevel.ERROR, "deprecated use : Use the JNDI naming context java:comp/env");
        return new java.util.Properties();
    }

    /**
     * Obtains the transaction demarcation interface.
     * @return The UserTransaction interface that the enterprise bean instance
     *         can use for transaction demarcation.
     * @exception IllegalStateException Thrown if the instance container does
     *            not make the UserTransaction interface available to the
     *            instance.
     */
    public UserTransaction getUserTransaction() throws IllegalStateException {

        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }

        if (!bf.isTxBeanManaged()) {
            throw new IllegalStateException("This bean is not allowed to use UserTransaction interface");
        }
        return (UserTransaction) tm;
    }

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

    /**
     * Deliver a timeout to the bean
     * @param timer timer whose expiration caused this notification.
     */
    public void deliverTimeout(Timer timer) {
        if (TraceEjb.isDebugJms()) {
            TraceEjb.mdb.log(BasicLevel.DEBUG, "");
        }

        RequestCtx rctx = null;
        try {
            rctx = bf.preInvoke(timerTxAttr);
        } catch (Exception e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "preInvoke failed: ", e);
            return;
        }
        try {
            bf.checkSecurity(null);
            if (mdb instanceof TimedObject) {
                ((TimedObject) mdb).ejbTimeout(timer);
            } else {
                throw new EJBException("The bean does not implement the `TimedObject` interface");
            }
        } catch (EJBException e) {
            rctx.sysExc = e;
            TraceEjb.logger.log(BasicLevel.ERROR, "EJB exception thrown by an enterprise Bean", e);
        } catch (RuntimeException e) {
            rctx.sysExc = e;
            TraceEjb.logger.log(BasicLevel.ERROR, "runtime exception thrown by an enterprise Bean", e);
        } catch (Error e) {
            rctx.sysExc = e;
            TraceEjb.logger.log(BasicLevel.ERROR, "error thrown by an enterprise Bean", e);
        } finally {
            try {
                bf.postInvoke(rctx);
            } catch (Exception e) {
                TraceEjb.logger.log(BasicLevel.ERROR, "exception on postInvoke: ", e);
            }
        }
    }


    /**
     * Lookup object with given name.
     * @param name given name
     * @return result of the lookup
     */
    public Object lookup(final String name) {
        // Search in java:comp/env first
        try {
            return new InitialContext().lookup(JAVA_COMP_ENV + name);
        } catch (NamingException ne) {
            // try in registry
            try {
                return new InitialContext().lookup(name);
            } catch (NamingException e) {
                throw new IllegalArgumentException("Lookup on '" + name + "' was not found");
            }
        }
    }

}
