/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
// $Id: ServiceObjectFactoryJAXRPC.java 4023 2007-07-28 07:14:06Z thomas.diesler@jboss.com $
package org.jboss.ws.core.jaxrpc.client;

// $Id: ServiceObjectFactoryJAXRPC.java 4023 2007-07-28 07:14:06Z thomas.diesler@jboss.com $

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
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.URL;
import java.net.URLEncoder;
import java.rmi.Remote;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.Service;

import org.jboss.logging.Logger;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.client.ServiceObjectFactory;
import org.jboss.ws.core.server.PortComponentResolver;
import org.jboss.ws.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.ws.metadata.jaxrpcmapping.JavaWsdlMappingFactory;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.ServerEndpointMetaData;
import org.jboss.ws.metadata.umdm.ServiceMetaData;
import org.jboss.ws.metadata.wsse.WSSecurityConfiguration;
import org.jboss.wsf.spi.deployment.Endpoint;
import org.jboss.wsf.spi.management.EndpointRegistry;
import org.jboss.wsf.spi.management.EndpointRegistryFactory;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedCallPropertyMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedPortComponentRefMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedServiceRefMetaData;
import org.jboss.wsf.spi.SPIProviderResolver;
import org.jboss.wsf.spi.SPIProvider;

/**
 * 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 ServiceObjectFactoryJAXRPC extends ServiceObjectFactory
{
   // provide logging
   private static final Logger log = Logger.getLogger(ServiceObjectFactoryJAXRPC.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 javax.naming.spi.NamingManager#getObjectInstance
    * @see javax.naming.spi.NamingManager#getURLContext
    */
   public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception
   {
      try
      {
         Reference ref = (Reference)obj;

         // Unmarshall the ServiceRefMetaData
         UnifiedServiceRefMetaData serviceRef = null;
         RefAddr metaRefAddr = ref.get(ServiceReferenceable.SERVICE_REF_META_DATA);
         ByteArrayInputStream bais = new ByteArrayInputStream((byte[])metaRefAddr.getContent());
         try
         {
            ObjectInputStream ois = new ObjectInputStream(bais);
            serviceRef = (UnifiedServiceRefMetaData)ois.readObject();
            ois.close();
         }
         catch (IOException ex)
         {
            NamingException ne = new NamingException("Cannot unmarshall service ref meta data");
            ne.setRootCause(ex);
            throw ne;
         }

         // Unmarshall the WSSecurityConfiguration
         WSSecurityConfiguration securityConfig = null;
         RefAddr wsseRefAddr = ref.get(ServiceReferenceable.SECURITY_CONFIG);
         if (wsseRefAddr != null)
         {
            bais = new ByteArrayInputStream((byte[])wsseRefAddr.getContent());
            try
            {
               ObjectInputStream ois = new ObjectInputStream(bais);
               securityConfig = (WSSecurityConfiguration)ois.readObject();
               ois.close();
            }
            catch (IOException e)
            {
               throw new NamingException("Cannot unmarshall security config, cause: " + e.toString());
            }
         }

         ServiceImpl jaxrpcService = null;
         URL wsdlLocation = serviceRef.getWsdlLocation();
         if (wsdlLocation != null)
         {
            if (log.isDebugEnabled())
               log.debug("Create jaxrpc service from wsdl");

            // Create the actual service object
            QName serviceName = serviceRef.getServiceQName();
            JavaWsdlMapping javaWsdlMapping = getJavaWsdlMapping(serviceRef);
            jaxrpcService = new ServiceImpl(serviceName, wsdlLocation, javaWsdlMapping, securityConfig, serviceRef);
         }
         else
         {
            if (log.isDebugEnabled())
               log.debug("Create jaxrpc service with no wsdl");
            jaxrpcService = new ServiceImpl(new QName(Constants.NS_JBOSSWS_URI, "AnonymousService"));
         }

         ServiceMetaData serviceMetaData = jaxrpcService.getServiceMetaData();

         // Set any service level properties
         if (serviceRef.getCallProperties().size() > 0)
         {
            Properties callProps = new Properties();
            serviceMetaData.setProperties(callProps);
            for (UnifiedCallPropertyMetaData prop : serviceRef.getCallProperties())
               callProps.setProperty(prop.getPropName(), prop.getPropValue());
         }

         // 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 pcLink = (String)pcLinkRef.getContent();
            log.debug("Resolving port-component-link: " + pcLink);

            // First try to obtain the endpoint address loacally
            String endpointAddress = null;
            try
            {
               SPIProvider spiProvider = SPIProviderResolver.getInstance().getProvider();
               EndpointRegistry epRegistry = spiProvider.getSPI(EndpointRegistryFactory.class).getEndpointRegistry();
               Endpoint endpoint = epRegistry.resolve( new PortComponentResolver(pcLink) );
               if (endpoint == null)
                  throw new WSException("Cannot resolve port-component-link: " + pcLink);

               ServerEndpointMetaData sepMetaData = endpoint.getAttachment(ServerEndpointMetaData.class);
               endpointAddress = sepMetaData.getEndpointAddress();
            }
            catch (Throwable ex)
            {
               // ignore, we are probably a remote client
            }

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

            if (log.isDebugEnabled())
               log.debug("Resolved to: " + endpointAddress);
            if (serviceMetaData.getEndpoints().size() == 1)
            {
               EndpointMetaData epMetaData = serviceMetaData.getEndpoints().get(0);
               epMetaData.setEndpointAddress(endpointAddress);
            }
            else
            {
               log.warn("Cannot set endpoint address for port-component-link, unsuported number of endpoints");
            }
         }

         narrowPortSelection(serviceRef, serviceMetaData);

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

         // load the service interface class
         ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
         Class siClass = contextCL.loadClass(serviceRef.getServiceInterface());
         if (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
         for (UnifiedPortComponentRefMetaData pcr : serviceRef.getPortComponentRefs())
         {
            Class seiClass = contextCL.loadClass(pcr.getServiceEndpointInterface());
            if (Remote.class.isAssignableFrom(seiClass) == false)
               throw new IllegalArgumentException("The SEI does not implement java.rmi.Remote: " + seiClass.getName());
         }

         // Setup the handler chain
         setupHandlerChain(jaxrpcService);

         InvocationHandler handler = new ServiceProxy(jaxrpcService, siClass);
         return Proxy.newProxyInstance(contextCL, new Class[] { siClass, ServiceExt.class }, handler);
      }
      catch (Exception ex)
      {
         log.error("Cannot create service", ex);
         throw ex;
      }
   }

   /**
    * Setup the handler chain(s) for this service
    */
   private void setupHandlerChain(ServiceImpl jaxrpcService) throws Exception
   {
      List<EndpointMetaData> endpoints = jaxrpcService.getServiceMetaData().getEndpoints();
      for (EndpointMetaData epMetaData : endpoints)
      {
         jaxrpcService.setupHandlerChain(epMetaData);
      }
   }

   private JavaWsdlMapping getJavaWsdlMapping(UnifiedServiceRefMetaData serviceRef)
   {
      JavaWsdlMapping javaWsdlMapping = null;
      if (serviceRef.getMappingFile() != null)
      {
         String mappingFile = serviceRef.getMappingFile();
         try
         {
            JavaWsdlMappingFactory mappingFactory = JavaWsdlMappingFactory.newInstance();
            URL mappingURL = serviceRef.getVfsRoot().findChild(mappingFile).toURL();
            javaWsdlMapping = mappingFactory.parse(mappingURL);
         }
         catch (Exception e)
         {
            throw new WSException("Cannot unmarshal jaxrpc-mapping-file: " + mappingFile, e);
         }
      }
      return javaWsdlMapping;
   }
}
