/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

// $Id: ServiceImpl.java,v 1.17.4.11 2005/04/22 11:28:32 tdiesler Exp $
package org.jboss.webservice.client;

// $Id: ServiceImpl.java,v 1.17.4.11 2005/04/22 11:28:32 tdiesler Exp $

import org.jboss.axis.EngineConfiguration;
import org.jboss.axis.client.AxisClient;
import org.jboss.axis.wsdl.gen.Parser;
import org.jboss.logging.Logger;
import org.jboss.webservice.EngineConfigurationFinder;
import org.jboss.webservice.deployment.BeanXMLMetaData;
import org.jboss.webservice.deployment.ServiceDescription;
import org.jboss.webservice.deployment.TypeMappingDescription;
import org.jboss.webservice.deployment.WSDDGenerator;
import org.jboss.webservice.encoding.ser.MetaDataBeanDeserializerFactory;
import org.jboss.webservice.encoding.ser.MetaDataBeanSerializerFactory;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.webservice.metadata.jaxrpcmapping.ServiceEndpointInterfaceMapping;

import javax.wsdl.Definition;
import javax.wsdl.Port;
import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.ServiceException;
import javax.xml.rpc.encoding.DeserializerFactory;
import javax.xml.rpc.encoding.SerializerFactory;
import javax.xml.rpc.encoding.TypeMapping;
import javax.xml.rpc.encoding.TypeMappingRegistry;
import javax.xml.rpc.handler.HandlerRegistry;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * Lookup EngineConfiguration configure the the jaxrpc service with it.
 *
 * @author Thomas.Diesler@jboss.org
 * @version $Revision: 1.17.4.11 $
 * @since 27-April-2004
 */
public class ServiceImpl extends org.jboss.axis.client.Service
{
   /** @since 4.0.2 */
   static final long serialVersionUID = -340351073471390974L;
   // provide logging
   private static final Logger log = Logger.getLogger(ServiceImpl.class);

   // The key for the serviceDescMap if it only has one entry
   private static final String DEFAULT_PORT = "DEFAULT_PORT";

   // The target endpoitn address, used by port-component-links
   private String targetEndpointAddress;
   // Maps the port name to the service description generated from the wsdl and jaxrpc-mapping.xml
   private Map serviceDescMap = new HashMap();
   // The jaxrpc-mapping if we have one
   private JavaWsdlMapping javaWsdlMapping;
   // The wsdl definition if we have one
   private Definition wsdlDefinition;
   // The call properties from the client deployment descriptor
   private Properties callProperties;

   /**
    * Constructs a new Service object - this assumes the caller will set
    * the appropriate fields by hand rather than getting them from the
    * WSDL.
    */
   public ServiceImpl()
   {
      super();
   }

   /**
    * Constructs a new Service object - this assumes the caller will set
    * the appropriate fields by hand rather than getting them from the
    * WSDL.
    */
   public ServiceImpl(QName serviceName)
   {
      super(serviceName);
      if (serviceName == null)
         throw new IllegalStateException("service name cannot be null");
   }

   /**
    * Constructs a new Service object for the service in the WSDL document
    * pointed to by the wsdlDoc URL and serviceName parameters.
    *
    * @param wsdlDoc     URL of the WSDL document
    * @param serviceName Qualified name of the desired service
    * @throws ServiceException If there's an error finding or parsing the WSDL
    */
   public ServiceImpl(URL wsdlDoc, QName serviceName) throws ServiceException
   {
      super(wsdlDoc, serviceName);
      if (serviceName == null)
         throw new IllegalStateException("service name cannot be null");
   }

   /**
    * Add a service description for the client service
    *
    * @param serviceDesc The Service description to add
    * @param portName    The optional port name
    */
   public void initService(ServiceDescription serviceDesc, String portName) throws ServiceException
   {
      log.debug("initService: port=" + portName);

      if (portName == null)
         portName = DEFAULT_PORT;

      if (wsdlDefinition != null && wsdlDefinition != serviceDesc.getWsdlDefinition())
         throw new IllegalArgumentException("Cannot redefine the wsdl definition for this service");

      if (javaWsdlMapping != null && javaWsdlMapping != serviceDesc.getJavaWsdlMapping())
         throw new IllegalArgumentException("Cannot redefine the jaxrpc-mapping definition for this service");

      if (serviceDescMap.get(portName) != null)
         throw new IllegalArgumentException("A service decription for this tport is already registered");

      wsdlDefinition = serviceDesc.getWsdlDefinition();
      javaWsdlMapping = serviceDesc.getJavaWsdlMapping();

      serviceDescMap.put(portName, serviceDesc);

      if (log.isTraceEnabled())
      {
         WSDDGenerator wsddGenerator = new WSDDGenerator(serviceDesc);

         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
         wsddGenerator.appendOperations(pw);
         wsddGenerator.appendTypeMappings(pw);

         log.trace("Service configuration:\n" + sw);
      }

      setupTypeMapping(serviceDesc);
   }

   /**
    * J2EE components should not use the getHandlerRegistry() method.
    * A container provider must throw a java.lang.UnsupportedOperationException from the getHandlerRegistry()
    * method of the Service Interface. Handler support is documented in Chapter 6 Handlers.
    */
   public HandlerRegistry getHandlerRegistry()
   {
      throw new UnsupportedOperationException("Components should not use the getHandlerRegistry() method.");
   }

   /** J2EE components should not use the getTypeMappingRegistry() method.
    * A container provider must throw a java.lang.UnsupportedOperationException from the getTypeMappingRegistry()
    * method of the Service Interface.
    */
   public TypeMappingRegistry getTypeMappingRegistry()
   {
      throw new UnsupportedOperationException("Components should not use the getTypeMappingRegistry() method.");
   }

   /** Returns the dynamic proxy for the given SEI.
    */
   public Remote getPort(Class seiClass) throws ServiceException
   {
      Remote port = (Remote)super.getPort(seiClass);

      // The proxy is required to support the SEI and Stub interfaces
      InvocationHandler handler = new PortProxy(port, seiClass);
      ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
      Class[] ifaces = {seiClass, org.jboss.webservice.client.Stub.class};
      return (Remote)Proxy.newProxyInstance(contextCL, ifaces, handler);
   }

   /** Returns the dynamic proxy for the given SEI.
    */
   public Remote getPort(QName portName, Class seiClass) throws ServiceException
   {
      Remote port = (Remote)super.getPort(portName, seiClass);

      // The proxy is required to support the SEI and Stub interfaces
      InvocationHandler handler = new PortProxy(port, seiClass);
      ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
      Class[] ifaces = {seiClass, org.jboss.webservice.client.Stub.class};
      return (Remote)Proxy.newProxyInstance(contextCL, ifaces, handler);
   }

   protected Remote getGeneratedStub(QName portName, Class proxyInterface) throws ServiceException
   {
      // [TDI] 10-Feb-2005 Prevent use of Axis generated stubs
      // http://jira.jboss.com/jira/browse/JBWS-105
      Remote stub = super.getGeneratedStub(portName, proxyInterface);
      if (stub != null)
         throw new ServiceException("Axis generated stubs not supported in WS4EE clients: " + stub.getClass().getName());

      return null;
   }

   /**
    * Set the default call properties to use with every created call
    *
    * @param callProperties the properties to set
    * @see #createCall()
    */
   public void setCallProperties(Properties callProperties)
   {
      this.callProperties = callProperties;
   }


   public ServiceDescription getServiceDescription(String portName)
   {
      ServiceDescription serviceDesc = null;

      if (serviceDescMap.size() == 1 && DEFAULT_PORT.equals(serviceDescMap.keySet().iterator().next()))
         serviceDesc = (ServiceDescription)serviceDescMap.values().iterator().next();

      else if (portName != null)
         serviceDesc = (ServiceDescription)serviceDescMap.get(portName);

      if (serviceDesc == null)
         log.warn("Cannot get ServiceDescription for: portName=" + portName + " we have " + serviceDescMap.keySet());

      return serviceDesc;
   }

   public Definition getWsdlDefinition()
   {
      return wsdlDefinition;
   }

   public void setWsdlDefinition(Definition wsdlDefinition)
   {
      this.wsdlDefinition = wsdlDefinition;
   }

   public JavaWsdlMapping getJavaWsdlMapping()
   {
      return javaWsdlMapping;
   }

   public void setJavaWsdlMapping(JavaWsdlMapping javaWsdlMapping)
   {
      this.javaWsdlMapping = javaWsdlMapping;
   }

   /**
    * Overwrite to provide a Call object that is ws4ee aware.
    *
    * @return Call            Used for invoking the Web Service
    * @throws javax.xml.rpc.ServiceException If there's an error
    */
   public Call createCall() throws ServiceException
   {
      CallImpl call = new CallImpl(this);
      if (callProperties != null)
      {
         // Set the default call properties
         Iterator keys = callProperties.keySet().iterator();
         while (keys.hasNext())
         {
            String key = (String)keys.next();
            String value = callProperties.getProperty(key);
            call.setProperty(key, value);
         }
      }
      return call;
   }

   /**
    * Returns an <code>Iterator</code> for the list of
    * <code>QName</code>s of service endpoints grouped by this
    * service
    * <p/>
    * For a partial wsdl see ws-1.1 spec section
    * 4.2.2.5 Service method use with partial WSDL
    */
   public Iterator getPorts() throws ServiceException
   {
      // This assumes that the client deploys a partial wsdl, but obtains
      // a full wsdl using the <wsdl-override> element
      if (wsdlService == null)
         throw new UnsupportedOperationException("wsdl service is not available");

      return wsdlService.getPorts().keySet().iterator();
   }

   /**
    * Get the service name for this service.
    */
   public QName getServiceName()
   {
      if (serviceName != null)
         return serviceName;

      // This assumes that the client deploys a partial wsdl, but obtains
      // a full wsdl using the <wsdl-override> element
      if (wsdlService == null)
         throw new UnsupportedOperationException("wsdl service is not available");

      return wsdlService.getQName();
   }

   /**
    * Get the AxisClient engine.
    */
   protected AxisClient getAxisClient()
   {
      if (engine == null)
      {
         engine = new ClientEngine(getEngineConfiguration());
      }
      return (AxisClient)engine;
   }

   /**
    * Use the {@link org.jboss.webservice.EngineConfigurationFinder#getClientEngineConfiguration()}
    * to discover the client engine configuration.
    */
   protected EngineConfiguration getEngineConfiguration()
   {
      if (config == null)
      {
         config = EngineConfigurationFinder.getClientEngineConfiguration();
         if (config == null)
            throw new IllegalStateException("Cannot obtain client config");
      }

      return config;
   }

   /**
    * Turn off automatic wrapped style
    */
   protected Parser getParser()
   {
      Parser parser = super.getParser();
      parser.setNowrap(true);
      return parser;
   }

   /**
    * The service may provide the target endpoint address, in case the client wsdl port
    * does not contain a valid address. This maybe the case for ws4ee clients that use a
    * port-component-link element.
    */
   protected String getTargetEnpointAddress()
   {
      return targetEndpointAddress;
   }

   public void setTargetEndpointAddress(String targetEndpointAddress)
   {
      this.targetEndpointAddress = targetEndpointAddress;
   }

   /**
    * Get the WSDL Port for a given service portComponentLinkEndpoint interface
    * <p/>
    * This looks up the port from the jaxrpc-mapping
    * before defaulting to the Axis functionality.
    * <p/>
    * Note, this method is not part of standard Axis.
    *
    * @param seiClass The service portComponentLinkEndpoint interface class
    * @return A wsdl Port
    * @throws javax.xml.rpc.ServiceException If there's an error
    */
   protected Port getWSDLPort(Class seiClass) throws ServiceException
   {
      // A partial wsdl may not have a service, nor a port
      if (wsdlService == null)
         return null;

      Map wsdlPorts = wsdlService.getPorts();
      if (wsdlPorts == null || wsdlPorts.size() == 0)
         throw new ServiceException("Cannot obtain wsdl wsdlPorts for service: " + wsdlService.getQName());

      Port wsdlPort = null;

      // there is only one, so return it
      if (wsdlPorts.values().size() == 1)
      {
         wsdlPort = (Port)wsdlPorts.values().iterator().next();
         return wsdlPort;
      }

      if (javaWsdlMapping != null)
      {
         log.debug("Trying to get jaxrpc port mapping for: " + seiClass.getName());
         ServiceEndpointInterfaceMapping[] seiMappings = javaWsdlMapping.getServiceEndpointInterfaceMappings();
         for (int i = 0; wsdlPort == null && i < seiMappings.length; i++)
         {
            ServiceEndpointInterfaceMapping seiMapping = seiMappings[i];
            if (seiClass.getName().equals(seiMapping.getServiceEndpointInterface()))
            {
               QName bindingName = seiMapping.getWsdlBinding();
               Iterator it = wsdlPorts.values().iterator();
               while (wsdlPort == null && it.hasNext())
               {
                  Port auxPort = (Port)it.next();
                  if (auxPort.getBinding().getQName().equals(bindingName))
                     wsdlPort = auxPort;
               }
            }
         }
      }

      if (wsdlPort == null)
      {
         log.warn("Cannot obtain jaxrpc port mapping for: " + seiClass.getName());
         wsdlPort = super.getWSDLPort(seiClass);
      }

      return wsdlPort;
   }

   /**
    * Setup the type mapping for this service
    */
   private void setupTypeMapping(ServiceDescription serviceDesc) throws ServiceException
   {
      TypeMappingRegistry tmRegistry = super.getTypeMappingRegistry();
      if (tmRegistry == null)
         throw new IllegalStateException("Cannot obtain TypeMappingRegistry");

      // Use the context classloader to load the user defined type
      ClassLoader cl = Thread.currentThread().getContextClassLoader();

      Iterator it = serviceDesc.getTypMappings();
      while (it.hasNext())
      {
         TypeMappingDescription typeMapping = (TypeMappingDescription)it.next();
         QName typeQName = typeMapping.getTypeQName();
         String javaType = typeMapping.getJavaType();
         String encodingURI = typeMapping.getEncodingURI();
         BeanXMLMetaData metaData = typeMapping.getMetaData();

         TypeMapping tm = tmRegistry.getTypeMapping(encodingURI);

         SerializerFactory serFactory = null;
         DeserializerFactory desFactory = null;
         try
         {
            // Note, the serializer/derserializer factory might not be known until we load the class
            Class typeClass = typeMapping.loadJavaType(cl);

            if (typeClass != null)
            {
               String serFactoryName = typeMapping.getSerializerFactoryName();
               String desFactoryName = typeMapping.getDeserializerFactoryName();

               if (tm.isRegistered(typeClass, typeQName) == false || typeMapping.isUserDefined())
               {
                  if (serFactoryName != null)
                  {
                     Class serFactoryClass = cl.loadClass(serFactoryName);
                     if (hasQualifiedConstructor(serFactoryClass))
                     {
                        Constructor ctor = serFactoryClass.getConstructor(new Class[]{Class.class, QName.class});
                        serFactory = (SerializerFactory)ctor.newInstance(new Object[]{typeClass, typeQName});

                        // Set the TypeMappingDescription.metaData
                        if (serFactory instanceof MetaDataBeanSerializerFactory)
                           ((MetaDataBeanSerializerFactory)serFactory).setMetaData(metaData);
                     }
                     else
                     {
                        serFactory = (SerializerFactory)serFactoryClass.newInstance();
                     }
                  }

                  if (desFactoryName != null)
                  {
                     Class desFactoryClass = cl.loadClass(desFactoryName);
                     if (hasQualifiedConstructor(desFactoryClass))
                     {
                        Constructor ctor = desFactoryClass.getConstructor(new Class[]{Class.class, QName.class});
                        desFactory = (DeserializerFactory)ctor.newInstance(new Object[]{typeClass, typeQName});

                        // Set the TypeMappingDescription.metaData
                        if (desFactory instanceof MetaDataBeanDeserializerFactory)
                           ((MetaDataBeanDeserializerFactory)desFactory).setMetaData(metaData);
                     }
                     else
                     {
                        desFactory = (DeserializerFactory)desFactoryClass.newInstance();
                     }
                  }

                  if (serFactory != null && desFactory != null)
                  {
                     log.debug("Register type mapping [qname=" + typeQName + ",class=" + javaType + "," + serFactoryName + "," + desFactoryName + "]");
                     tm.register(typeClass, typeQName, serFactory, desFactory);
                  }
               }
               else
               {
                  log.debug("Ignore type mapping [qname=" + typeQName + ",class=" + javaType + "," + serFactoryName + "," + desFactoryName + "]");
               }
            }
         }
         catch (InvocationTargetException e)
         {
            log.error("Cannot setup type mapping", e.getTargetException());
            throw new ServiceException(e.getTargetException());
         }
         catch (Exception e)
         {
            log.error("Cannot setup type mapping", e);
            throw new ServiceException(e);
         }

      }
   }

   private boolean hasQualifiedConstructor(Class factoryClass)
   {
      try
      {
         Constructor ctor = factoryClass.getConstructor(new Class[]{Class.class, QName.class});
         return ctor != null;
      }
      catch (Exception ignore)
      {
      }

      return false;
   }
}