/*
 * Copyright 2001-2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.axis.providers.java;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;

import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Handler;
import org.apache.axis.MessageContext;
import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.handlers.soap.SOAPService;
import org.apache.axis.utils.ClassUtils;
import org.apache.axis.utils.Messages;
import org.apache.commons.logging.Log;

/**
 * A basic EJB Provider
 *
 * @author Carl Woolf (cwoolf@macromedia.com)
 * @author Tom Jordahl (tomj@macromedia.com)
 * @author C?dric Chabanois (cchabanois@ifrance.com)
 */
public class EJBProvider extends RPCProvider
{
    protected static Log log =
        LogFactory.getLog(EJBProvider.class.getName());

    // The enterprise category is for stuff that an enterprise product might
    // want to track, but in a simple environment (like the AXIS build) would
    // be nothing more than a nuisance.
    protected static Log entLog =
        LogFactory.getLog(Constants.ENTERPRISE_LOG_CATEGORY);

    public static final String OPTION_BEANNAME = "beanJndiName";
    public static final String OPTION_HOMEINTERFACENAME = "homeInterfaceName";
    public static final String OPTION_REMOTEINTERFACENAME = "remoteInterfaceName";
    public static final String OPTION_LOCALHOMEINTERFACENAME = "localHomeInterfaceName";
    public static final String OPTION_LOCALINTERFACENAME = "localInterfaceName";
    
    
    public static final String jndiContextClass = "jndiContextClass";
    public static final String jndiURL = "jndiURL";
    public static final String jndiUsername = "jndiUser";
    public static final String jndiPassword = "jndiPassword";
    
    protected static final Class[] empty_class_array = new Class[0];
    protected static final Object[] empty_object_array = new Object[0];
    
    private static InitialContext cached_context = null;

    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////
    /////// Default methods from JavaProvider ancestor, overridden
    ///////   for ejbeans
    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////

    /**
     * Return a object which implements the service.
     * 
     * @param msgContext the message context
     * @param clsName The JNDI name of the EJB home class
     * @return an object that implements the service
     */
    protected Object makeNewServiceObject(MessageContext msgContext,
                                           String clsName)
        throws Exception
    {
        String remoteHomeName = getStrOption(OPTION_HOMEINTERFACENAME, 
                                                msgContext.getService());
        String localHomeName = getStrOption(OPTION_LOCALHOMEINTERFACENAME, 
                                                msgContext.getService());
        String homeName = (remoteHomeName != null ? remoteHomeName:localHomeName);

        if (homeName == null) {
            // cannot find both remote home and local home  
            throw new AxisFault(
                Messages.getMessage("noOption00", 
                                    OPTION_HOMEINTERFACENAME, 
                                    msgContext.getTargetService()));
        }
                                                        
        // Load the Home class name given in the config file
        Class homeClass = ClassUtils.forName(homeName, true, msgContext.getClassLoader());

        // we create either the ejb using either the RemoteHome or LocalHome object
        if (remoteHomeName != null)
            return createRemoteEJB(msgContext, clsName, homeClass);
        else 
            return createLocalEJB(msgContext, clsName, homeClass);
    }

    /**
     * Create an EJB using a remote home object
     * 
     * @param msgContext the message context
     * @param beanJndiName The JNDI name of the EJB remote home class
     * @param homeClass the class of the home interface
     * @return an EJB
     */
    private Object createRemoteEJB(MessageContext msgContext, 
                                    String beanJndiName,
                                    Class homeClass)
        throws Exception
    {
        // Get the EJB Home object from JNDI 
        Object ejbHome = getEJBHome(msgContext.getService(),
                                    msgContext, beanJndiName);
        Object ehome = javax.rmi.PortableRemoteObject.narrow(ejbHome, homeClass);

        // Invoke the create method of the ejbHome class without actually
        // touching any EJB classes (i.e. no cast to EJBHome)
        Method createMethod = homeClass.getMethod("create", empty_class_array);
        Object result = createMethod.invoke(ehome, empty_object_array);
        
        return result;        
    }

    /**
     * Create an EJB using a local home object
     * 
     * @param msgContext the message context
     * @param beanJndiName The JNDI name of the EJB local home class
     * @param homeClass the class of the home interface
     * @return an EJB
     */
    private Object createLocalEJB(MessageContext msgContext, 
                                   String beanJndiName,
                                   Class homeClass)
        throws Exception
    {
        // Get the EJB Home object from JNDI 
        Object ejbHome = getEJBHome(msgContext.getService(),
                                    msgContext, beanJndiName);

        // the home object is a local home object
        Object ehome;
        if (homeClass.isInstance(ejbHome))
          ehome = ejbHome;
        else
          throw new ClassCastException(
                  Messages.getMessage("badEjbHomeType"));
          
        // Invoke the create method of the ejbHome class without actually
        // touching any EJB classes (i.e. no cast to EJBLocalHome)
        Method createMethod = homeClass.getMethod("create", empty_class_array);
        Object result = createMethod.invoke(ehome, empty_object_array);
                               
        return result;
    }

    /**
     * Tells if the ejb that will be used to handle this service is a remote
     * one
     */
    private boolean isRemoteEjb(SOAPService service)
    {
        return getStrOption(OPTION_HOMEINTERFACENAME,service) != null; 
    }

    /**
     * Tells if the ejb that will be used to handle this service is a local
     * one
     */
    private boolean isLocalEjb(SOAPService service)
    {
        return (!isRemoteEjb(service)) && 
          (getStrOption(OPTION_LOCALHOMEINTERFACENAME,service) != null);
    }


    /**
     * Return the option in the configuration that contains the service class
     * name.  In the EJB case, it is the JNDI name of the bean.
     */
    protected String getServiceClassNameOptionName()
    {
        return OPTION_BEANNAME;
    }

    /**
     * Get a String option by looking first in the service options,
     * and then at the Handler's options.  This allows defaults to be
     * specified at the provider level, and then overriden for particular
     * services.
     *
     * @param optionName the option to retrieve
     * @return String the value of the option or null if not found in
     *                either scope
     */
    protected String getStrOption(String optionName, Handler service)
    {
        String value = null;
        if (service != null)
            value = (String)service.getOption(optionName);
        if (value == null)
            value = (String)getOption(optionName);
        return value;
    }

    /**
     * Get the remote interface of an ejb from its home class.
     * This function can only be used for remote ejbs
     * 
     * @param beanJndiName the jndi name of the ejb
     * @param service the soap service
     * @param msgContext the message context (can be null)
     */
    private Class getRemoteInterfaceClassFromHome(String beanJndiName,
                                                   SOAPService service,
                                                   MessageContext msgContext)
        throws Exception                                       
    {
        // Get the EJB Home object from JNDI
        Object ejbHome = getEJBHome(service, msgContext, beanJndiName);

        String homeName = getStrOption(OPTION_HOMEINTERFACENAME,
                                       service);
        if (homeName == null)
            throw new AxisFault(
                    Messages.getMessage("noOption00",
                                        OPTION_HOMEINTERFACENAME,
                                        service.getName()));

        // Load the Home class name given in the config file
        ClassLoader cl = (msgContext != null) ?
                msgContext.getClassLoader() :
                Thread.currentThread().getContextClassLoader();
        Class homeClass = ClassUtils.forName(homeName, true, cl);


        // Make sure the object we got back from JNDI is the same type
        // as the what is specified in the config file
        Object ehome = javax.rmi.PortableRemoteObject.narrow(ejbHome, homeClass);

        // This code requires the use of ejb.jar, so we do the stuff below
        //   EJBHome ejbHome = (EJBHome) ehome;
        //   EJBMetaData meta = ejbHome.getEJBMetaData();
        //   Class interfaceClass = meta.getRemoteInterfaceClass();

        // Invoke the getEJBMetaData method of the ejbHome class without
        // actually touching any EJB classes (i.e. no cast to EJBHome)
        Method getEJBMetaData =
                homeClass.getMethod("getEJBMetaData", empty_class_array);
        Object metaData = getEJBMetaData.invoke(ehome, empty_object_array);
        Method getRemoteInterfaceClass =
                metaData.getClass().getMethod("getRemoteInterfaceClass",
                                                  empty_class_array);
        return (Class) getRemoteInterfaceClass.invoke(metaData,
                                                       empty_object_array);
    }                                      

    
    /**
     * Get the class description for the EJB Remote or Local Interface, 
     * which is what we are interested in exposing to the world (i.e. in WSDL).
     * 
     * @param msgContext the message context (can be null)
     * @param beanJndiName the JNDI name of the EJB
     * @return the class info of the EJB remote or local interface
     */ 
    protected Class getServiceClass(String beanJndiName,
                                    SOAPService service,
                                    MessageContext msgContext)
        throws AxisFault
    {
        Class interfaceClass = null;
        
        try {
            // First try to get the interface class from the configuation
            // Note that we don't verify that remote remoteInterfaceName is used for
            // remote ejb and localInterfaceName for local ejb. Should we ?
            String remoteInterfaceName = 
                    getStrOption(OPTION_REMOTEINTERFACENAME, service);
            String localInterfaceName = 
                    getStrOption(OPTION_LOCALINTERFACENAME, service);
            String interfaceName = (remoteInterfaceName != null ? remoteInterfaceName : localInterfaceName);

            if(interfaceName != null){
                ClassLoader cl = (msgContext != null) ?
                        msgContext.getClassLoader() :
                        Thread.currentThread().getContextClassLoader();
                interfaceClass = ClassUtils.forName(interfaceName,
                                                    true,
                                                    cl);
            } 
            else
            {
                // cannot get the interface name from the configuration, we get
                // it from the EJB Home (if remote)
                if (isRemoteEjb(service)) {
                    interfaceClass = getRemoteInterfaceClassFromHome(beanJndiName,
                                                                     service,
                                                                     msgContext);
                }
                else 
                if (isLocalEjb(service)) {
                    // we cannot get the local interface from the local ejb home
                    // localInterfaceName is mandatory for local ejbs
                    throw new AxisFault(
                            Messages.getMessage("noOption00", 
                                                OPTION_LOCALINTERFACENAME, 
                                                service.getName()));
                }
                else
                {
                    // neither a local ejb or a remote one ...
                    throw new AxisFault(Messages.getMessage("noOption00", 
                                                OPTION_HOMEINTERFACENAME,
                                                service.getName()));
                }
            }
        } catch (Exception e) {
            throw AxisFault.makeFault(e);
        }

        // got it, return it
       return interfaceClass;
    }

    /**
     * Common routine to do the JNDI lookup on the Home interface object
     * username and password for jndi lookup are got from the configuration or from
     * the messageContext if not found in the configuration
     */ 
    private Object getEJBHome(SOAPService serviceHandler,
                              MessageContext msgContext,
                              String beanJndiName)
        throws AxisFault
    {
        Object ejbHome = null;
        
        // Set up an InitialContext and use it get the beanJndiName from JNDI
        try {
            Properties properties = null;

            // collect all the properties we need to access JNDI:
            // username, password, factoryclass, contextUrl

            // username
            String username = getStrOption(jndiUsername, serviceHandler);
            if ((username == null) && (msgContext != null))
               username = msgContext.getUsername();
            if (username != null) {
                if (properties == null)
                    properties = new Properties();
                properties.setProperty(Context.SECURITY_PRINCIPAL, username);
            }

            // password
            String password = getStrOption(jndiPassword, serviceHandler);
            if ((password == null) && (msgContext != null))
                password = msgContext.getPassword();
            if (password != null) {
                if (properties == null)
                    properties = new Properties();
                properties.setProperty(Context.SECURITY_CREDENTIALS, password);
            }

            // factory class
            String factoryClass = getStrOption(jndiContextClass, serviceHandler);
            if (factoryClass != null) {
                if (properties == null)
                    properties = new Properties();
                properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, factoryClass);
            }

            // contextUrl
            String contextUrl = getStrOption(jndiURL, serviceHandler);
            if (contextUrl != null) {
                if (properties == null)
                    properties = new Properties();
                properties.setProperty(Context.PROVIDER_URL, contextUrl);
            }

            // get context using these properties 
            InitialContext context = getContext(properties);

            // if we didn't get a context, fail
            if (context == null)
                throw new AxisFault( Messages.getMessage("cannotCreateInitialContext00"));
            
            ejbHome = getEJBHome(context, beanJndiName);

            if (ejbHome == null)
                throw new AxisFault( Messages.getMessage("cannotFindJNDIHome00",beanJndiName));
        }
        // Should probably catch javax.naming.NameNotFoundException here 
        catch (Exception exception) {
            entLog.info(Messages.getMessage("toAxisFault00"), exception);
            throw AxisFault.makeFault(exception);
        }

        return ejbHome;
    }
    
    protected InitialContext getCachedContext()
        throws javax.naming.NamingException
    {
        if (cached_context == null)
            cached_context = new InitialContext();
        return cached_context;
    }
        

    protected InitialContext getContext(Properties properties)
        throws AxisFault, javax.naming.NamingException
    {
        // if we got any stuff from the configuration file
        // create a new context using these properties 
        // otherwise, we get a default context and cache it for next time
        return ((properties == null)
                ? getCachedContext()
                : new InitialContext(properties));
    }

    protected Object getEJBHome(InitialContext context, String beanJndiName)
        throws AxisFault, javax.naming.NamingException
    {
        // Do the JNDI lookup
        return context.lookup(beanJndiName);
    }

    /**
     * Override the default implementation such that we can include
     * special handling for {@link java.rmi.ServerException}.
     * <p/>
     * Converts {@link java.rmi.ServerException} exceptions to
     * {@link InvocationTargetException} exceptions with the same cause.
     * This allows the axis framework to create a SOAP fault.
     * </p>
     *
     * @see org.apache.axis.providers.java.RPCProvider#invokeMethod(org.apache.axis.MessageContext, java.lang.reflect.Method, java.lang.Object, java.lang.Object[])
     */
    protected Object invokeMethod(MessageContext msgContext, Method method,
                                  Object obj, Object[] argValues)
            throws Exception {
        try {
            return super.invokeMethod(msgContext, method, obj, argValues);
        } catch (InvocationTargetException ite) {
            Throwable cause = getCause(ite);
            if (cause instanceof java.rmi.ServerException) {
                throw new InvocationTargetException(getCause(cause));
            }
            throw ite;
        }
    }

    /**
     * Get the cause of an exception, using reflection so that
     * it still works under JDK 1.3
     *
     * @param original the original exception
     * @return the cause of the exception, or the given exception if the cause cannot be discovered.
     */
    private Throwable getCause(Throwable original) {
        try {
            Method method = original.getClass().getMethod("getCause", null);
            Throwable cause = (Throwable) method.invoke(original, null);
            if (cause != null) {
                return cause;
            }
        } catch (NoSuchMethodException nsme) {
            // ignore, this occurs under JDK 1.3 
        } catch (Throwable t) {
        }
        return original;
    }
}
