/**
 * 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.security.Identity;
import java.security.Principal;
import java.util.Map;
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.naming.InitialContext;
import javax.naming.NamingException;
import javax.resource.spi.endpoint.MessageEndpoint;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import javax.transaction.xa.XAResource;

import org.ow2.jonas.deployment.ejb.MethodDesc;
import org.ow2.jonas.lib.security.context.SecurityContext;
import org.ow2.jonas.lib.security.context.SecurityCurrent;
import org.ow2.jonas.tm.TransactionManager;


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

/**
 * Generic interposed class for Message Endpoints This class presents these
 * interfaces, depending on object reached: MessageDrivenContext interface to
 * the bean instance
 * @author Philippe Coq, Philippe Durieux
 * @author Christophe Ney (Easier Enhydra integration)
 */
public class JMessageEndpoint implements MessageDrivenContext {

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

    protected JMdbEndpointFactory bf = null;

    protected MessageDrivenBean mdb = null;

    protected MessageEndpoint mep = null;

    protected int txattr; // TX_NOT_SUPPORTED, TX_REQUIRED or TX_NOT_SET (= bean
                          // managed)

    protected TransactionManager tm = null;

    protected XAResource xar = null;

    protected boolean released = false;

    private static final int MAX_NB_RETRY = 2;

    /**
     * constructor
     * @param bf The MDB Endpoint Factory
     * @param mdb The Message Driven Bean
     */
    public JMessageEndpoint(JMdbEndpointFactory bf, MessageDrivenBean mdb) {
        this.bf = bf;
        this.mdb = mdb;
        // keep these locally for efficiency.
        txattr = bf.getTransactionAttribute();
        tm = bf.getTransactionManager();
    }

    public void setProxy(MessageEndpoint mep) {
        this.mep = mep;
    }

    public XAResource getXAResource() {
        return xar;
    }

    public void setXAResource(XAResource xa) {
        xar = xa;
        // Check state of transaction to determine what to do
    }

    public boolean getReleasedState() {
        return released;
    }

    public void setReleasedState(boolean state) {
        released = state;
    }

    // ------------------------------------------------------------------
    // 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.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);
    }

    /**
     * @return the java.security.Principal that identifies the caller.
     * @throws IllegalStateException if there is no principal
     */
    public Principal getCallerPrincipal() {
        boolean inRunAs = false;
        if (bf.dd.getRunAsRole() != null) {
            inRunAs = true;
        }
        // Set a security context if there was no one set.
        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");
                }
                sctx = new SecurityContext();
                current.setSecurityContext(sctx);
            }
        }

        Principal principal =  bf.getContainer().getPrincipalFactory().getCallerPrincipal(inRunAs);
        if (principal == null) {
            throw new IllegalStateException("No principal exists in security context");
        }
        return principal;
    }

    /**
     * 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, "");
      }

    if(bf.isTxBeanManaged())
      throw new IllegalStateException("Bean-managed transaction, not use this method.");
    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, "");
      }
    if(bf.isTxBeanManaged())
      throw new IllegalStateException("Bean-managed transaction, not use this method.");
    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, "");
        }

        boolean committed = false;
        for (int nbretry = 0; ! committed && nbretry < MAX_NB_RETRY; nbretry++) {
            RequestCtx rctx = null;
            try {
                // For MDB, transaction attribute must be required or NotSupported.
                rctx = bf.preInvoke(MethodDesc.TX_REQUIRED);
            } 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");
                }
                committed = ! getRollbackOnly();
            } 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");
            }
        }
    }

    @Override
    public Map<String, Object> getContextData() {
        throw new UnsupportedOperationException("EJBs 2.1 do not support this operation.");
    }
}
