/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2001-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Axis" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.jboss.axis.providers.java;

import org.jboss.axis.AxisEngine;
import org.jboss.axis.AxisFault;
import org.jboss.axis.Constants;
import org.jboss.axis.Handler;
import org.jboss.axis.Message;
import org.jboss.axis.MessageContext;
import org.jboss.axis.description.ServiceDesc;
import org.jboss.axis.encoding.TypeMapping;
import org.jboss.axis.enums.Scope;
import org.jboss.axis.handlers.soap.SOAPService;
import org.jboss.axis.message.SOAPEnvelopeAxisImpl;
import org.jboss.axis.providers.BasicProvider;
import org.jboss.axis.session.Session;
import org.jboss.axis.utils.ClassUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.axis.utils.cache.ClassCache;
import org.jboss.axis.utils.cache.JavaClass;
import org.jboss.axis.wsdl.fromJava.Emitter;
import org.jboss.logging.Logger;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.holders.IntHolder;
import javax.xml.rpc.server.ServiceLifecycle;
import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * Base class for Java dispatching.  Fetches various fields out of envelope,
 * looks up service object (possibly using session state), and delegates
 * envelope body processing to subclass via abstract processMessage method.
 *
 * @author Doug Davis (dug@us.ibm.com)
 * @author Carl Woolf (cwoolf@macromedia.com)
 */
public abstract class JavaProvider extends BasicProvider
{
   private static Logger log = Logger.getLogger(JavaProvider.class.getName());

   public static final String OPTION_CLASSNAME = "className";
   public static final String OPTION_ALLOWEDMETHODS = "allowedMethods";
   public static final String OPTION_IS_STATIC = "isStatic";
   public static final String OPTION_CLASSPATH = "classPath";
   public static final String OPTION_WSDL_PORTTYPE = "wsdlPortType";
   public static final String OPTION_WSDL_SERVICEELEMENT = "wsdlServiceElement";
   public static final String OPTION_WSDL_SERVICEPORT = "wsdlServicePort";
   public static final String OPTION_WSDL_TARGETNAMESPACE = "wsdlTargetNamespace";
   public static final String OPTION_WSDL_INPUTSCHEMA = "wsdlInputSchema";

   public static final String OPTION_SCOPE = "scope";

   /**
    * Get the service object whose method actually provides the service.
    * May look up in session table.
    */
   public Object getServiceObject(MessageContext msgContext,
                                  Handler service,
                                  String clsName,
                                  IntHolder scopeHolder)
           throws Exception
   {
      String serviceName = msgContext.getService().getName();

      // scope can be "Request", "Session", "Application"
      // (as with Apache SOAP)
      Scope scope = Scope.getScope((String)service.getOption(OPTION_SCOPE), Scope.DEFAULT);

      scopeHolder.value = scope.getValue();

      if (scope == Scope.REQUEST)
      {
         // make a one-off
         return getNewServiceObject(msgContext, clsName);
      }
      else if (scope == Scope.SESSION)
      {
         // What do we do if serviceName is null at this point???
         if (serviceName == null)
            serviceName = msgContext.getService().toString();

         // look in incoming session
         Session session = msgContext.getSession();
         if (session != null)
         {
            return getSessionServiceObject(session, serviceName,
                    msgContext, clsName);
         }
         else
         {
            // was no incoming session, sigh, treat as request scope
            scopeHolder.value = Scope.DEFAULT.getValue();
            return getNewServiceObject(msgContext, clsName);
         }
      }
      else if (scope == Scope.APPLICATION)
      {
         // MUST be AxisEngine here!
         AxisEngine engine = msgContext.getAxisEngine();
         Session appSession = engine.getApplicationSession();
         if (appSession != null)
         {
            return getSessionServiceObject(appSession, serviceName,
                    msgContext, clsName);
         }
         else
         {
            // was no application session, sigh, treat as request scope
            // FIXME : Should we bomb in this case?
            scopeHolder.value = Scope.DEFAULT.getValue();
            return getNewServiceObject(msgContext, clsName);
         }
      }
      else
      {
         // NOTREACHED
         return null;
      }
   }

   /**
    * Simple utility class for dealing with synchronization issues.
    */
   class LockObject
   {
      private boolean completed = false;

      synchronized void waitUntilComplete() throws InterruptedException
      {
         while (!completed)
         {
            wait();
         }
      }

      synchronized void complete()
      {
         completed = true;
         notifyAll();
      }
   }

   /**
    * Get a service object from a session.  Handles threading / locking
    * issues when multiple threads might be accessing the same session
    * object, and ensures only one thread gets to create the service
    * object if there isn't one already.
    */
   private Object getSessionServiceObject(Session session,
                                          String serviceName,
                                          MessageContext msgContext,
                                          String clsName) throws Exception
   {
      Object obj = null;
      boolean makeNewObject = false;

      // This is a little tricky.
      synchronized (session.getLockObject())
      {
         // store service objects in session, indexed by class name
         obj = session.get(serviceName);

         // If nothing there, put in a placeholder object so
         // other threads wait for us to create the real
         // service object.
         if (obj == null)
         {
            obj = new LockObject();
            makeNewObject = true;
            session.set(serviceName, obj);
         }
      }

      // OK, we DEFINITELY have something in obj at this point.  Either
      // it's the service object or it's a LockObject (ours or someone
      // else's).

      if (LockObject.class == obj.getClass())
      {
         LockObject lock = (LockObject)obj;

         // If we were the lucky thread who got to install the
         // placeholder, create a new service object and install it
         // instead, then notify anyone waiting on the LockObject.
         if (makeNewObject)
         {
            try
            {
               obj = getNewServiceObject(msgContext, clsName);
               session.set(serviceName, obj);
            }
            finally
            {
               lock.complete();
            }
         }
         else
         {
            // It's someone else's LockObject, so wait around until
            // it's completed.
            lock.waitUntilComplete();

            // Now we are guaranteed there is a service object in the
            // session, so this next part doesn't need syncing
            obj = session.get(serviceName);
         }
      }

      return obj;
   }

   /**
    * Return a new service object which, if it implements the ServiceLifecycle
    * interface, has been init()ed.
    *
    * @param msgContext the MessageContext
    * @param clsName    the name of the class to instantiate
    * @return an initialized service object
    */
   private Object getNewServiceObject(MessageContext msgContext,
                                      String clsName) throws Exception
   {
      Object serviceObject = makeNewServiceObject(msgContext, clsName);
      if (serviceObject != null &&
              serviceObject instanceof ServiceLifecycle)
      {
         ((ServiceLifecycle)serviceObject).init(msgContext.getProperty(Constants.MC_SERVLET_ENDPOINT_CONTEXT));
      }
      return serviceObject;
   }

   /**
    * Process the current message.  Side-effect resEnv to create return value.
    *
    * @param msgContext self-explanatory
    * @param reqEnv     the request envelope
    * @param resEnv     the response envelope
    * @param obj        the service object itself
    */
   public abstract void processMessage(MessageContext msgContext,
                                       SOAPEnvelopeAxisImpl reqEnv,
                                       SOAPEnvelopeAxisImpl resEnv,
                                       Object obj)
           throws Exception;


   /**
    * Invoke the message by obtaining various common fields, looking up
    * the service object (via getServiceObject), and actually processing
    * the message (via processMessage).
    */
   public void invoke(MessageContext msgContext) throws AxisFault
   {
      if (log.isDebugEnabled())
         log.debug("Enter: JavaProvider::invoke (" + this + ")");

      /* Find the service we're invoking so we can grab it's options */
      /***************************************************************/
      String serviceName = msgContext.getTargetService();
      Handler service = msgContext.getService();

      /* Now get the service (RPC) specific info  */
      /********************************************/
      String clsName = getServiceClassName(service);

      if ((clsName == null) || clsName.equals(""))
      {
         throw new AxisFault("Server.NoClassForService",
                 Messages.getMessage("noOption00", getServiceClassNameOptionName(), serviceName),
                 null, null);
      }

      IntHolder scope = new IntHolder();
      Object serviceObject = null;

      try
      {
         serviceObject = getServiceObject(msgContext, service, clsName, scope);

         Message resMsg = msgContext.getResponseMessage();
         SOAPEnvelopeAxisImpl resEnv;

         // If we didn't have a response message, make sure we set one up
         // with the appropriate versions of SOAP and Schema
         if (resMsg == null)
         {
            resEnv = new SOAPEnvelopeAxisImpl(msgContext.getSOAPConstants(),
                    msgContext.getSchemaVersion());

            resMsg = new Message(resEnv);
            msgContext.setResponseMessage(resMsg);
         }
         else
         {
            resEnv = resMsg.getSOAPEnvelope();
         }

         Message reqMsg = msgContext.getRequestMessage();
         SOAPEnvelopeAxisImpl reqEnv = reqMsg.getSOAPEnvelope();

         processMessage(msgContext, reqEnv, resEnv, serviceObject);
      }
      catch (Exception ex)
      {
         processException(ex);
         log.error("Detect unprocessed exception", ex);
         throw AxisFault.makeFault(ex);
      }
      finally
      {
         // If this is a request scoped service object which implements
         // ServiceLifecycle, let it know that it's being destroyed now.
         if (serviceObject != null &&
                 scope.value == Scope.REQUEST.getValue() &&
                 serviceObject instanceof ServiceLifecycle)
         {
            ((ServiceLifecycle)serviceObject).destroy();
         }
      }

      if (log.isDebugEnabled())
         log.debug("Exit: JavaProvider::invoke (" + this + ")");
   }

   /**
    * Externalize exception handling
    * @param ex Exception during message processing
    * @throws AxisFault The generated Axis fault
    */
   protected void processException(Exception ex) throws AxisFault
   {
      if (ex instanceof AxisFault)
      {
         throw (AxisFault)ex;
      }

      // This hack unwrapps the checked AxisException from a RuntimeException
      // so we don't loose the fault code, etc
      // [TDI] 17-Aug-2004
      if (ex instanceof JAXRPCException)
      {
         JAXRPCException rte = (JAXRPCException)ex;
         if (rte.getLinkedCause() instanceof AxisFault)
            throw (AxisFault)rte.getLinkedCause();

         AxisFault fault = AxisFault.makeFault(ex);
         fault.addFaultDetail(Constants.QNAME_FAULTDETAIL_RUNTIMEEXCEPTION, "true");
         throw fault;
      }

      if (ex instanceof SAXException)
      {
         SAXException exp = (SAXException)ex;
         Exception real = exp.getException();
         if (real != null)
            throw AxisFault.makeFault(real);
      }

      AxisFault fault = AxisFault.makeFault(ex);
      throw fault;
   }

   /**
    * Generate the WSDL for this service.
    * <p/>
    * Put in the "WSDL" property of the message context
    * as a org.w3c.dom.Document
    */
   public void generateWSDL(MessageContext msgContext) throws AxisFault
   {
      if (log.isDebugEnabled())
         log.debug("Enter: JavaProvider::generateWSDL (" + this + ")");

      /* Find the service we're invoking so we can grab it's options */
      /***************************************************************/
      SOAPService service = msgContext.getService();
      ServiceDesc serviceDesc = service.getInitializedServiceDesc(msgContext);

      // Calculate the appropriate namespaces for the WSDL we're going
      // to put out.
      //
      // If we've been explicitly told which namespaces to use, respect
      // that.  If not:
      //
      // The "interface namespace" should be either:
      // 1) The namespace of the ServiceDesc
      // 2) The transport URL (if there's no ServiceDesc ns)

      try
      {
         // Location URL is whatever is explicitly set in the MC
         String locationUrl =
                 msgContext.getStrProp(MessageContext.WSDLGEN_SERV_LOC_URL);

         if (locationUrl == null)
         {
            // If nothing, try what's explicitly set in the ServiceDesc
            locationUrl = serviceDesc.getEndpointURL();
         }

         if (locationUrl == null)
         {
            // If nothing, use the actual transport URL
            locationUrl = msgContext.getStrProp(MessageContext.TRANS_URL);
         }

         // Interface namespace is whatever is explicitly set
         String interfaceNamespace =
                 msgContext.getStrProp(MessageContext.WSDLGEN_INTFNAMESPACE);

         if (interfaceNamespace == null)
         {
            // If nothing, use the default namespace of the ServiceDesc
            interfaceNamespace = serviceDesc.getDefaultNamespace();
         }

         if (interfaceNamespace == null)
         {
            // If nothing still, use the location URL determined above
            interfaceNamespace = locationUrl;
         }

//  Do we want to do this?
//
//            if (locationUrl == null) {
//                locationUrl = url;
//            } else {
//                try {
//                    URL urlURL = new URL(url);
//                    URL locationURL = new URL(locationUrl);
//                    URL urlTemp = new URL(urlURL.getProtocol(),
//                            locationURL.getHost(),
//                            locationURL.getPort(),
//                            urlURL.getFile());
//                    interfaceNamespace += urlURL.getFile();
//                    locationUrl = urlTemp.toString();
//                } catch (Exception e) {
//                    locationUrl = url;
//                    interfaceNamespace = url;
//                }
//            }

         Emitter emitter = new Emitter();

         // This seems like a good idea, but in fact isn't because the
         // emitter will figure out a reasonable name (<classname>Service)
         // for the WSDL service element name.  We provide the 'alias'
         // setting to explicitly set this name. See bug 13262 for more info.
         //emitter.setServiceElementName(serviceDesc.getName());

         // service alias may be provided if exact naming is required,
         // otherwise Axis will name it according to the implementing class name
         String alias = (String)service.getOption("alias");
         if (alias != null) emitter.setServiceElementName(alias);

         // Set style/use
         emitter.setStyle(serviceDesc.getStyle());
         emitter.setUse(serviceDesc.getUse());

         emitter.setClsSmart(serviceDesc.getImplClass(), locationUrl);

         // If a wsdl target namespace was provided, use the targetNamespace.
         // Otherwise use the interfaceNamespace constructed above.
         String targetNamespace = (String)service.getOption(OPTION_WSDL_TARGETNAMESPACE);
         if (targetNamespace == null ||
                 targetNamespace.length() == 0)
         {
            targetNamespace = interfaceNamespace;
         }
         emitter.setIntfNamespace(targetNamespace);

         emitter.setLocationUrl(locationUrl);
         emitter.setServiceDesc(serviceDesc);
         emitter.setTypeMapping((TypeMapping)msgContext.getTypeMappingRegistry()
                 .getTypeMapping(serviceDesc.getUse().getEncoding()));
         emitter.setDefaultTypeMapping((TypeMapping)msgContext.getTypeMappingRegistry().
                 getDefaultTypeMapping());

         String wsdlPortType = (String)service.getOption(OPTION_WSDL_PORTTYPE);
         String wsdlServiceElement = (String)service.getOption(OPTION_WSDL_SERVICEELEMENT);
         String wsdlServicePort = (String)service.getOption(OPTION_WSDL_SERVICEPORT);

         if (wsdlPortType != null && wsdlPortType.length() > 0)
         {
            emitter.setPortTypeName(wsdlPortType);
         }
         if (wsdlServiceElement != null && wsdlServiceElement.length() > 0)
         {
            emitter.setServiceElementName(wsdlServiceElement);
         }
         if (wsdlServicePort != null && wsdlServicePort.length() > 0)
         {
            emitter.setServicePortName(wsdlServicePort);
         }

         String wsdlInputSchema = (String)
                 service.getOption(OPTION_WSDL_INPUTSCHEMA);
         if (null != wsdlInputSchema && wsdlInputSchema.length() > 0)
         {
            emitter.setInputSchema(wsdlInputSchema);
         }

         Document doc = emitter.emit(Emitter.MODE_ALL);

         msgContext.setProperty("WSDL", doc);
      }
      catch (NoClassDefFoundError e)
      {
         log.info(Messages.getMessage("toAxisFault00"), e);
         throw new AxisFault(e.toString(), e);
      }
      catch (Exception e)
      {
         log.info(Messages.getMessage("toAxisFault00"), e);
         throw AxisFault.makeFault(e);
      }

      if (log.isDebugEnabled())
         log.debug("Exit: JavaProvider::generateWSDL (" + this + ")");
   }

   private String getAllowedMethods(Handler service)
   {
      String val = (String)service.getOption(OPTION_ALLOWEDMETHODS);
      if (val == null || val.length() == 0)
      {
         // Try the old option for backwards-compatibility
         val = (String)service.getOption("methodName");
      }
      return val;
   }

   ///////////////////////////////////////////////////////////////
   ///////////////////////////////////////////////////////////////
   /////// Default methods for java classes. Override, eg, for
   ///////   ejbeans
   ///////////////////////////////////////////////////////////////
   ///////////////////////////////////////////////////////////////

   /**
    * Default java service object comes from simply instantiating the
    * class wrapped in jc
    */
   protected Object makeNewServiceObject(MessageContext msgContext, String clsName)
           throws Exception
   {

      Object serviceObject = null;

      ClassLoader cl = msgContext.getClassLoader();

      ClassCache cache = msgContext.getAxisEngine().getClassCache();
      if (cache != null)
      {
         log.debug("makeNewServiceObject from cache: " + clsName);
         JavaClass jc = cache.lookup(clsName, cl);
         serviceObject = jc.getJavaClass().newInstance();
      }
      else
      {
         log.debug("makeNewServiceObject for name: " + clsName);
         Class serviceClass = ClassUtils.forName(clsName, true, cl);
         serviceObject = serviceClass.newInstance();
      }

      return serviceObject;
   }

   /**
    * Return the class name of the service
    */
   protected String getServiceClassName(Handler service)
   {
      return (String)service.getOption(getServiceClassNameOptionName());
   }

   /**
    * Return the option in the configuration that contains the service class
    * name
    */
   protected String getServiceClassNameOptionName()
   {
      return OPTION_CLASSNAME;
   }

   /**
    * Returns the Class info about the service class.
    */
   protected Class getServiceClass(String clsName, SOAPService service, MessageContext msgContext)
           throws AxisFault
   {

      ClassLoader cl = null;
      Class serviceClass = null;
      AxisEngine engine = service.getEngine();

      // If we have a message context, use that to get classloader
      // otherwise get the current threads classloader
      if (msgContext != null)
      {
         cl = msgContext.getClassLoader();
      }
      else
      {
         cl = Thread.currentThread().getContextClassLoader();
      }

      // If we have an engine, use its class cache
      if (engine != null && engine.getClassCache() != null)
      {
         try
         {
            JavaClass jc = engine.getClassCache().lookup(clsName, cl);
            serviceClass = jc.getJavaClass();
         }
         catch (ClassNotFoundException e)
         {
            log.error(Messages.getMessage("exception00"), e);
            throw new AxisFault(Messages.getMessage("noClassForService00", clsName), e);
         }
      }

      // use Class.forName instead.
      if (serviceClass == null)
      {
         try
         {
            serviceClass = ClassUtils.forName(clsName, true, cl);
         }
         catch (ClassNotFoundException e)
         {
            log.error(Messages.getMessage("exception00"), e);
            throw new AxisFault(Messages.getMessage("noClassForService00", clsName), e);
         }
      }

      return serviceClass;
   }

   /**
    * Fill in a service description with the correct impl class
    * and typemapping set.  This uses methods that can be overridden by
    * other providers (like the EJBProvider) to get the class from the
    * right place.
    */
   public void initServiceDesc(SOAPService service, MessageContext msgContext)
           throws AxisFault
   {
      // Set up the Implementation class for the service

      String clsName = getServiceClassName(service);
      if (clsName == null)
      {
         throw new AxisFault(Messages.getMessage("noServiceClass"));
      }
      Class cls = getServiceClass(clsName, service, msgContext);
      ServiceDesc serviceDescription = service.getServiceDescription();

      // And the allowed methods, if necessary
      if (serviceDescription.getAllowedMethods() == null && service != null)
      {
         String allowedMethods = getAllowedMethods(service);
         if (allowedMethods != null && !"*".equals(allowedMethods))
         {
            ArrayList methodList = new ArrayList();
            StringTokenizer tokenizer = new StringTokenizer(allowedMethods, " ,");
            while (tokenizer.hasMoreTokens())
            {
               methodList.add(tokenizer.nextToken());
            }
            serviceDescription.setAllowedMethods(methodList);
         }
      }

      serviceDescription.loadServiceDescByIntrospection(cls);
   }

}