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

// $Id: ServiceObjectFactory.java,v 1.28.2.2 2005/02/09 09:21:13 tdiesler Exp $
package org.jboss.webservice.client;

// $Id: ServiceObjectFactory.java,v 1.28.2.2 2005/02/09 09:21:13 tdiesler Exp $

import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.system.server.ServerConfig;
import org.jboss.webservice.AxisServiceMBean;
import org.jboss.webservice.PortComponentInfo;
import org.jboss.webservice.deployment.ServiceDescription;
import org.jboss.webservice.metadata.HandlerMetaData;
import org.jboss.webservice.metadata.InitParamMetaData;
import org.jboss.webservice.metadata.PortComponentRefMetaData;
import org.jboss.webservice.metadata.ServiceRefMetaData;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.webservice.util.WSDLUtilities;

import javax.management.MBeanServer;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.NamingManager;
import javax.naming.spi.ObjectFactory;
import javax.wsdl.Definition;
import javax.wsdl.Port;
import javax.wsdl.Service;
import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.HandlerInfo;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

/**
 * This ServiceObjectFactory reconstructs a javax.xml.rpc.Service
 * for a given WSDL when the webservice client does a JNDI lookup
 * <p/>
 * It uses the information provided by the service-ref element in application-client.xml
 *
 * @author Thomas.Diesler@jboss.org
 * @since 15-April-2004
 */
public class ServiceObjectFactory implements ObjectFactory
{
   // provide logging
   private static final Logger log = Logger.getLogger(ServiceObjectFactory.class);

   /**
    * Creates an object using the location or reference information specified.
    * <p/>
    *
    * @param obj         The possibly null object containing location or reference
    *                    information that can be used in creating an object.
    * @param name        The name of this object relative to <code>nameCtx</code>,
    *                    or null if no name is specified.
    * @param nameCtx     The context relative to which the <code>name</code>
    *                    parameter is specified, or null if <code>name</code> is
    *                    relative to the default initial context.
    * @param environment The possibly null environment that is used in
    *                    creating the object.
    * @return The object created; null if an object cannot be created.
    * @throws Exception if this object factory encountered an exception
    *                   while attempting to create an object, and no other object factories are
    *                   to be tried.
    * @see NamingManager#getObjectInstance
    * @see NamingManager#getURLContext
    */
   public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment)
           throws Exception
   {
      Reference ref = (Reference)obj;

      ServiceRefMetaData serviceRef = null;

      // unmarshall the ServiceRefMetaData
      RefAddr metaRefAddr = ref.get(ServiceReferenceable.SERVICE_REF_META_DATA);
      ByteArrayInputStream bais = new ByteArrayInputStream((byte[])metaRefAddr.getContent());
      try
      {
         ObjectInputStream ois = new ObjectInputStream(bais);
         serviceRef = (ServiceRefMetaData)ois.readObject();
         ois.close();
      }
      catch (IOException e)
      {
         throw new NamingException("Cannot unmarshall service ref meta data, cause: " + e.toString());
      }

      // reconstruct the resourceCl
      URL url = new URL((String)ref.get(ServiceReferenceable.DEPLOYMENT_URL).getContent());
      ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
      URLClassLoader resourceCl = new URLClassLoader(new URL[]{url}, contextCL);
      serviceRef.setResourceCl(resourceCl);

      // get the wsdl URL if we have one
      URL wsdlURL = serviceRef.getWsdlOverride();
      if (wsdlURL == null && serviceRef.getWsdlFile() != null)
      {
         String wsdlFile = serviceRef.getWsdlFile();
         wsdlURL = resourceCl.findResource(wsdlFile);
         if (wsdlURL == null)
            throw new NamingException("Cannot load wsdl file '" + wsdlFile + "' from: " + url);
      }

      JavaWsdlMapping javaWsdlMapping = serviceRef.getJavaWsdlMapping();

      // Find the optional ws4ee-deployment.xml
      URL ws4eeMetaData = findTypeMappingMetaData(resourceCl);

      ServiceImpl jaxrpcService = null;
      Definition wsdlDefinition = null;
      Service wsdlService = null;

      if (wsdlURL != null)
      {
         log.debug("Create jaxrpc service for wsdl: " + wsdlURL);
         wsdlDefinition = serviceRef.getWsdlDefinition();

         // A partial wsdl may have no service element
         // We take the service name from ther service-ref
         QName serviceName = serviceRef.getServiceQName();

         if (serviceName == null && wsdlDefinition.getServices().values().size() == 1)
            serviceName = (QName)wsdlDefinition.getServices().keySet().iterator().next();

         if (serviceName == null)
            throw new IllegalStateException("Cannot obtain service name, use <service-qname> in your <service-ref>");

         // Create the actual service object
         jaxrpcService = new ServiceImpl(wsdlURL, serviceName);
         jaxrpcService.setWsdlDefinition(wsdlDefinition);
         jaxrpcService.setJavaWsdlMapping(javaWsdlMapping);

         wsdlService = getServiceForName(wsdlDefinition, serviceName);
      }
      else
      {
         log.debug("Create jaxrpc service with no wsdl");
         jaxrpcService = new ServiceImpl();
         jaxrpcService.setJavaWsdlMapping(javaWsdlMapping);
      }

      // If we have a wsdlService we can setup the ServiceDesc and handler chains
      if (wsdlService != null)
      {
         // Create a service description for each port-component-ref
         PortComponentRefMetaData[] pcRefs = serviceRef.getPortComponentRefs();
         if (pcRefs.length > 0)
         {
            for (int i = 0; i < pcRefs.length; i++)
            {
               PortComponentRefMetaData pcRef = pcRefs[i];
               String seiName = pcRef.getServiceEndpointInterface();

               QName portTypeQName = null;
               if (javaWsdlMapping != null)
                  portTypeQName = javaWsdlMapping.getPortTypeQNameForServiceEndpointInterface(seiName);

               List portNames = getPortNameForType(wsdlService, portTypeQName);
               for (Iterator j = portNames.iterator(); j.hasNext();)
               {
                  String portName = (String)j.next();
                  ServiceDescription serviceDesc = new ServiceDescription(wsdlDefinition, javaWsdlMapping, ws4eeMetaData, portName);
                  serviceDesc.setCallProperties(pcRef.getCallProperties());
                  jaxrpcService.initService(serviceDesc, portName);
               }
            }
         }
         else
         {
            // There are zero port-component-ref elements, use the single possible port binding
            ServiceDescription serviceDesc = new ServiceDescription(wsdlDefinition, javaWsdlMapping, ws4eeMetaData, null);
            jaxrpcService.initService(serviceDesc, null);
         }

         // Setup the client side handler chains
         setupHandlerChain(jaxrpcService, serviceRef, wsdlService);
      }

      // Set any default call properties
      Properties callProps = serviceRef.getCallProperties();
      jaxrpcService.setCallProperties(callProps);

      // The web service client using a port-component-link, the contet is the URL to
      // the PortComponentLinkServlet that will return the actual endpoint address
      RefAddr pclinkRef = ref.get(ServiceReferenceable.PORT_COMPONENT_LINK);
      if (pclinkRef != null)
      {
         String serviceID = (String)pclinkRef.getContent();
         log.debug("Resolving port-component-link: " + serviceID);

         // First try to obtain the endpoint address loacally
         String targetEndpointAddress = null;
         try
         {
            MBeanServer server = MBeanServerLocator.locateJBoss();
            PortComponentInfo pcInfo = (PortComponentInfo)server.invoke(AxisServiceMBean.OBJECT_NAME, "getPortComponentInfo",
                    new Object[]{serviceID}, new String[]{String.class.getName()});

            targetEndpointAddress = pcInfo.getServiceEndpointURL();
         }
         catch (Exception ignore)
         {
         }

         // We may be remote in the esoteric case where an appclient uses the port-comonent-link feature
         if (targetEndpointAddress == null)
         {
            String servletPath = (String)ref.get(ServiceReferenceable.PORT_COMPONENT_LINK_SERVLET).getContent();
            servletPath += "?serviceID=" + URLEncoder.encode(serviceID, "UTF-8");
            InputStream is = new URL(servletPath).openStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            targetEndpointAddress = br.readLine();
            is.close();
         }

         log.debug("Resolved to: " + targetEndpointAddress);
         jaxrpcService.setTargetEndpointAddress(targetEndpointAddress);
      }

      /********************************************************
       * Setup the Proxy that implements the service-interface
       ********************************************************/

      // load the service interface class
      Class siClass = serviceRef.getServiceInterfaceClass();
      if (javax.xml.rpc.Service.class.isAssignableFrom(siClass) == false)
         throw new JAXRPCException("The service interface does not implement javax.xml.rpc.Service: " + siClass.getName());

      // load all service endpoint interface classes
      PortComponentRefMetaData[] pcrArray = serviceRef.getPortComponentRefs();
      for (int i = 0; i < pcrArray.length; i++)
      {
         PortComponentRefMetaData pcr = pcrArray[i];
         Class seiClass = pcr.getServiceEndpointInterfaceClass();
         if (java.rmi.Remote.class.isAssignableFrom(seiClass) == false)
            throw new IllegalArgumentException("The SEI does not implement java.rmi.Remote: " + seiClass.getName());

         if (wsdlDefinition != null)
            WSDLUtilities.endorseServiceEndpointInterface(wsdlDefinition, seiClass, javaWsdlMapping);
      }

      InvocationHandler handler = new ServiceProxy(jaxrpcService, siClass);
      return (javax.xml.rpc.Service)Proxy.newProxyInstance(contextCL, new Class[]{siClass}, handler);
   }

   /**
    * @param wsdlService
    * @param portType
    * @return a List of Strings. The list is never empty and never null.
    * @throws IllegalArgumentException when no port names could be obtained.
    */
   private List getPortNameForType(javax.wsdl.Service wsdlService, QName portType)
   {
      List portNames = new ArrayList();

      if (portType != null)
      {
         for (Iterator i = wsdlService.getPorts().values().iterator(); i.hasNext();)
         {
            Port wsdlPort = (Port)i.next();
            if (wsdlPort.getBinding().getPortType().getQName().equals(portType))
               portNames.add(wsdlPort.getName());
         }
      }

      // Fallback strategy when no portType is given
      else if (wsdlService.getPorts().values().size() == 1)
      {
         for (Iterator i = wsdlService.getPorts().values().iterator(); i.hasNext();)
         {
            Port wsdlPort = (Port)i.next();
            portNames.add(wsdlPort.getName());
         }
      }

      if (portNames.isEmpty())
         throw new IllegalArgumentException("Cannot obtatin portName for binding: " + portType);

      return portNames;
   }

   private javax.wsdl.Service getServiceForName(Definition wsdlDefinition, QName serviceName)
   {
      javax.wsdl.Service wsdlService = null;

      if (serviceName != null)
      {
         wsdlService = wsdlDefinition.getService(serviceName);
      }
      else if (wsdlDefinition.getServices().values().size() == 1)
      {
         wsdlService = (javax.wsdl.Service)wsdlDefinition.getServices().values().iterator().next();
      }

      if (wsdlService == null)
         throw new IllegalArgumentException("Cannot obtain WSDL service for name: " + serviceName);

      return wsdlService;
   }

   /**
    * Find optional type mapping meta data.
    *
    * @see {@link org.jboss.webservice.encoding.ser.MetaDataTypeDesc}
    *      <p/>
    *      It looks for "ws4ee-deployment.xml" in the WEB-INF/META-INF dir
    */
   private URL findTypeMappingMetaData(URLClassLoader resourceCL)
   {
      URL resourceURL = null;

      // check to see if wsdd file already exists for this deployment
      String dataDir = System.getProperty(ServerConfig.SERVER_DATA_DIR);
      File resourceFile = new File(dataDir + "/wsdl/ws4ee-deployment.xml");
      if (resourceFile.exists())
      {
         try
         {
            resourceURL = resourceFile.toURL();
         }
         catch (MalformedURLException e)
         {
            log.warn("Could not get url to ws4ee-deployment.xml.", e);
         }
      }

      if (resourceURL == null)
      {
         String[] infDirs = new String[]{"META-INF", "WEB-INF"};
         for (int i = 0; resourceURL == null && i < infDirs.length; i++)
         {
            String resName = infDirs[i] + "/ws4ee-deployment.xml";
            resourceURL = resourceCL.findResource(resName);
         }
      }

      return resourceURL;
   }

   /**
    * Setup the handler chain(s) for this service
    * <p/>
    * This registers a handler chain with the client engine for every port
    * that hat handlers configured in the <service-ref> element
    */
   private void setupHandlerChain(ServiceImpl jaxrpcService, ServiceRefMetaData serviceRef, javax.wsdl.Service wsdlService) throws Exception
   {
      HandlerMetaData[] handlers = serviceRef.getHandlers();

      ClientEngine engine = (ClientEngine)jaxrpcService.getAxisClient();

      // for every port in the wsdl for a given service
      Iterator itPorts = wsdlService.getPorts().values().iterator();
      while (itPorts.hasNext())
      {
         Port wsdlPort = (Port)itPorts.next();
         String portName = wsdlPort.getName();

         ServiceDescription serviceDesc = jaxrpcService.getServiceDescription(portName);

         // no service, no handlers ;-)
         if (serviceDesc != null)
         {
            HashSet handlerRoles = new HashSet();
            ArrayList handlerInfos = new ArrayList();
            for (int i = 0; i < handlers.length; i++)
            {
               HandlerMetaData hMetaData = handlers[i];
               handlerRoles.addAll(Arrays.asList(hMetaData.getSoapRoles()));

               List hPortNames = Arrays.asList(hMetaData.getPortNames());
               if (hPortNames.size() == 0 || hPortNames.contains(portName))
               {
                  ClassLoader cl = Thread.currentThread().getContextClassLoader();
                  Class hClass = cl.loadClass(hMetaData.getHandlerClass());

                  HashMap hConfig = new HashMap();
                  InitParamMetaData[] params = hMetaData.getInitParams();
                  for (int j = 0; j < params.length; j++)
                  {
                     InitParamMetaData param = params[j];
                     hConfig.put(param.getParamName(), param.getParamValue());
                  }

                  QName[] hHeaders = hMetaData.getSoapHeaders();
                  HandlerInfo info = new HandlerInfo(hClass, hConfig, hHeaders);

                  log.debug("Adding client side handler to port '" + portName + "': " + info);
                  handlerInfos.add(info);
               }
            }

            // register the handlers with the client engine
            if (handlerInfos.size() > 0)
               engine.registerHandlerChain(portName, handlerInfos, handlerRoles);
         }
      }
   }
}