/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 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.Iterator;
import java.util.List;
import java.util.Properties;

import javax.ejb.EJBContext;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.EnterpriseBean;
import javax.ejb.TimerService;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.resource.spi.work.WorkManager;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.ow2.jonas.deployment.common.SecurityRoleRefDesc;
import org.ow2.jonas.tm.TransactionManager;


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

/**
 * This class implements javax.ejb.EJBContext class.
 * It may be extended in JSessionContext or JEntityContext
 * @author Philippe Coq, Philippe Durieux, Jeff Mesnil (security)
 * @author Florent Benoit (JACC security)
 */
public abstract class JContext implements EJBContext {

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

    protected EnterpriseBean myinstance;
    protected final JFactory bf;
    protected final TransactionManager tm;
    private final JHome home;
    private final JLocalHome localhome;
    private final JContainer cont;

    /**
     * Permission manager of this container. JACC
     */
    private PermissionManager permissionManager = null;


    /**
     * State of this Context
     * This state is used to know if some operation is allowed or not in this context.
     * May be used also for debugging.
     */
    public static final int CTX_STATE_INITIAL = 0;
    public static final int CTX_STATE_PASSIVE = 1;
    public static final int CTX_STATE_ACTIVE = 2;
    public static final int CTX_STATE_COMMITTING = 3;
    public static final int CTX_STATE_FINDING = 4;

    int instanceState = CTX_STATE_INITIAL;

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

    /**
     * Constructs a JContext
     * @param bf -  the BeanFactory
     * @param i -  the bean instance
     */
    protected JContext(JFactory bf, EnterpriseBean i) {
        this.bf = bf;
        myinstance = i;
        if (i == null) {
            TraceEjb.logger.log(BasicLevel.ERROR, "null EnterpriseBean!");
        }
        this.home = bf.getHome();
        this.localhome = bf.getLocalHome();
        this.cont = bf.getContainer();
        this.tm = bf.getTransactionManager();
        this.permissionManager = cont.getPermissionManager();

    }

    /**
     * Set the instance State
     */
    public void setState(int newState) {
        instanceState = newState;
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "" + instanceState);
        }
    }

    public void setPassive() {
         instanceState = CTX_STATE_PASSIVE;
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
    }

    public void setActive() {
         instanceState = CTX_STATE_ACTIVE;
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
    }

    public void setCommitting() {
         instanceState = CTX_STATE_COMMITTING;
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
    }

    public void setFinding() {
         instanceState = CTX_STATE_FINDING;
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "");
        }
    }

    /**
     * Get the Instance State
     * @throws Exception
     */
    public int getState() {
        if (TraceEjb.isDebugContext()) {
            TraceEjb.context.log(BasicLevel.DEBUG, "" + instanceState);
        }
        return instanceState;
    }

    // ------------------------------------------------------------------
    // JOnAS specific added functions
    // ------------------------------------------------------------------

    /**
     * Get the WorkManager
     */
    public WorkManager getWorkManager() {
        return bf.getWorkManager();
    }

    // ------------------------------------------------------------------
    // EJBContext implementation
    // ------------------------------------------------------------------

    /**
     * Obtains the java.security.Identity of the caller.
     * @deprecated
     * @return The Identity object that identifies the caller.
     */
    public Identity getCallerIdentity() {
        throw new RuntimeException ("getCallerIdentity() method deprecated. use instead getCallerPrincipal()");
    }


    /**
     * Obtain the java.security.Principal that identifies the caller.
     * throws a java.lang.IllegalStateException if there is no security context available
     * @return The Principal object that identifies the caller.
     * @throws IllegalStateException no security context exists
     */
    public Principal getCallerPrincipal() throws IllegalStateException {

        if (getState() == CTX_STATE_INITIAL) {
            throw new IllegalStateException("the instance is not allowed to call this method (ctx not initialized)");
        }
        boolean inRunAs = false;
        if (bf.dd.getRunAsRole() != null) {
            inRunAs = true;
        }

        Principal principal = cont.getPrincipalFactory().getCallerPrincipal(inRunAs);
        if (principal == null) {
            throw new IllegalStateException("no security context exists");
        }
        return principal;

    }

    /**
     * the enterprise bean's home interface.
     * @return The enterprise bean's home interface.
     * @throws IllegalStateException - if the enterprise bean does not have a local home interface.
     */
    public EJBHome getEJBHome() throws IllegalStateException {
        return home;
    }

    /**
     * Obtain the enterprise bean's local home interface.
     * @return The enterprise bean's local home interface.
     * @throws IllegalStateException if the enterprise bean does not have a local home interface.
     */
    public EJBLocalHome getEJBLocalHome() throws IllegalStateException {
        if (!bf.dd.hasDefinedLocalInterface()) {
            TraceEjb.logger.log(BasicLevel.ERROR, "No Local Interface declared for this bean");
            throw new IllegalStateException("No Local Interface declared for this bean");
        }
        return localhome;
    }

    /**
     * Obtains the enterprise bean's environment properties for EJB 1.0 style.
     * (Conform to EJB 1.1 specification for a container chooses to provide support
     *  for EJB 1.0 style environment properties. Cf EJB 1.1 Chapter 14.5, pages 216-217)
     *  Note: If the enterprise bean has no environment properties, this method returns an empty
     *        java.util.Properties object. This method never returns null.
     * @deprecated
     * @return The environment properties for the enterprise bean.
     */
    public Properties getEnvironment() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        return bf.getEjb10Environment();
    }

    /**
     * Tests if the transaction has been marked for rollback only.
     * @return true if transaction will rollback
     * @throws IllegalStateException if state is 0
     */
    public boolean getRollbackOnly() throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        if (getState() == CTX_STATE_INITIAL) {
            throw new IllegalStateException("the instance is not allowed to call this method (ctx not initialized)");
        }

        try {
            switch (tm.getStatus()) {
            case Status.STATUS_MARKED_ROLLBACK:
            case Status.STATUS_ROLLING_BACK:
                return true;
            case Status.STATUS_ACTIVE:
            case Status.STATUS_COMMITTING:
            case Status.STATUS_PREPARED:
            case Status.STATUS_PREPARING:
                return false;
            case Status.STATUS_ROLLEDBACK:
                throw new IllegalStateException("Transaction already rolled back");
            case Status.STATUS_COMMITTED:
                throw new IllegalStateException("Transaction already committed");
            case Status.STATUS_NO_TRANSACTION:
            case Status.STATUS_UNKNOWN:
                throw new IllegalStateException("Cannot getRollbackOnly outside transaction");
            }
        } catch (SystemException e) {
            TraceEjb.logger.log(BasicLevel.ERROR, "cannot get transaction status:", e);
            throw new IllegalStateException("Cannot get transaction status");
        }
        return true;
    }

    /**
     * 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 (e.g. if the bean is a stateful session bean)
     */
    public abstract TimerService getTimerService() throws IllegalStateException;

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

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

        if (!bf.isTxBeanManaged()) {
            throw new IllegalStateException("This bean is not allowed to use UserTransaction interface");
        }
        if (getState() == CTX_STATE_INITIAL) {
            throw new IllegalStateException("the instance is not allowed to call this method (ctx not initialized)");
        }

        return (UserTransaction) tm;
    }

    /**
     * @deprecated Use boolean isCallerInRole(String roleName) instead.
     * Tests if the caller has a given role.
     * @param role - The java.security.Identity of the role to be tested.
     * @return True if the caller has the specified role.
     */
    public boolean isCallerInRole(Identity role) {
        throw new RuntimeException ("isCallerInRole(Identity) method deprecated. use instead isCallerInRole(String)");
    }

    /**
     * Test if the caller has a given role.
     *
     * @param roleName The name of the security role.
     * The role must be one of the security-role-ref  that is defined in the
     * deployment descriptor.
     * @return True if the caller has the specified role.
     * @throws IllegalStateException Security service not started
     */
    public boolean isCallerInRole(String roleName) throws IllegalStateException {
        if (TraceEjb.isDebugSecurity()) {
            TraceEjb.security.log(BasicLevel.DEBUG, "");
        }

        if (getState() == CTX_STATE_INITIAL) {
            throw new IllegalStateException("the instance is not allowed to call this method (ctx not initialized)");
        }

        // See EJB 2.1 specification
        // Chapter 21 : 21.2.5.2

        // Check that the roleName is in security-role-ref
        List list = bf.dd.getSecurityRoleRefDescList();

        if (list == null) {
            TraceEjb.logger.log(BasicLevel.WARN, "EJB 2.1 spec, Chapter 21 : 21.2.5.2 : No security-role-ref list. Invalid usage of isCallerInRole without security-role-ref elements.");
            return false;
        }
        boolean foundItem = false;
        Iterator it = bf.dd.getSecurityRoleRefDescList().iterator();
        String tmpRoleName = null;
        SecurityRoleRefDesc sRoleRefDesc = null;
        while (!foundItem && it.hasNext()) {
            sRoleRefDesc = (SecurityRoleRefDesc) it.next();
            tmpRoleName = sRoleRefDesc.getRoleName();
            if (tmpRoleName.equals(roleName)) {
                foundItem = true;
            }
        }

        if (!foundItem) {
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "No security-role-ref with role name '" + roleName
                                + "' was found in the deployment descriptor of bean '"
                                + bf.getEJBName() + ".");
            }
            return false;
        }

        boolean inRunAs = false;
        if (bf.dd.getRunAsRole() != null) {
            inRunAs = true;
        }
        // Check with JACC manager
        boolean inRole = permissionManager.isCallerInRole(bf.getEJBName(), roleName, inRunAs);

        if (TraceEjb.isDebugSecurity()) {
            TraceEjb.security.log(BasicLevel.DEBUG, "isCallerInRole: " + inRole);
        }
        return inRole;

    }

    /**
     * Marks the current transaction for rollback.
     * The transaction will become permanently marked for rollback.
     * @throws IllegalStateException in getRollbackOnly() method
     */
    public void setRollbackOnly() throws IllegalStateException {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        // Check transaction state.
        // Should throw IllegalStateException if not in a correct state.
        // This may be not handled correctly by the TM.
        getRollbackOnly();

        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, "setRollbackOnly unexpected exception:", 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");
            }
        }
    }
}
