/*
 * 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.
 */
package org.jboss.ws.core.jaxws.spi;

// $Id: ServiceDelegateImpl.java 12933 2010-09-08 11:31:56Z darran.lofthouse@jboss.com $

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.jws.WebService;
import javax.xml.bind.JAXBContext;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.EndpointReference;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.WebServiceFeature;
import javax.xml.ws.Service.Mode;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.spi.ServiceDelegate;

import org.jboss.logging.Logger;
import org.jboss.util.NotImplementedException;
import org.jboss.ws.core.StubExt;
import org.jboss.ws.core.jaxws.client.ClientImpl;
import org.jboss.ws.core.jaxws.client.ClientProxy;
import org.jboss.ws.core.jaxws.client.DispatchImpl;
import org.jboss.ws.core.jaxws.client.ServiceObjectFactoryJAXWS;
import org.jboss.ws.core.jaxws.handler.HandlerResolverImpl;
import org.jboss.ws.metadata.builder.jaxws.JAXWSClientMetaDataBuilder;
import org.jboss.ws.metadata.builder.jaxws.JAXWSMetaDataBuilder;
import org.jboss.ws.metadata.umdm.ClientEndpointMetaData;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.HandlerMetaDataJAXWS;
import org.jboss.ws.metadata.umdm.ServiceMetaData;
import org.jboss.ws.metadata.umdm.UnifiedMetaData;
import org.jboss.ws.metadata.umdm.EndpointMetaData.Type;
import org.jboss.ws.metadata.wsdl.WSDLUtils;
import org.jboss.wsf.common.ResourceLoaderAdapter;
import org.jboss.wsf.spi.deployment.UnifiedVirtualFile;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerChainMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerChainsMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedPortComponentRefMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedServiceRefMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedStubPropertyMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData.HandlerType;

/**
 * Service delegates are used internally by Service objects to allow pluggability of JAX-WS implementations.
 *
 * Every Service object has its own delegate, created using the javax.xml.ws.Provider#createServiceDelegate method.
 * A Service object delegates all of its instance methods to its delegate.
 *
 * @author Thomas.Diesler@jboss.com
 * @since 03-May-2006
 */
public class ServiceDelegateImpl extends ServiceDelegate
{
   // provide logging
   private final Logger log = Logger.getLogger(ServiceDelegateImpl.class);

   // Lock to ensure only one thread can initialise the defaultExecutor.
   private static final Object DEFAULT_EXECUTOR_LOCK = new Object();
   // The executor service
   private static ExecutorService defaultExecutor = null;
         
   // The service meta data that is associated with this JAXWS Service
   private ServiceMetaData serviceMetaData;
   // The ServiceRefMetaData supplied by the ServiceObjectFactory 
   private UnifiedServiceRefMetaData usRef;
   // The handler resolver
   private HandlerResolver handlerResolver;
   // The executor service
   private ExecutorService executor;

   // A list of annotated ports
   private List<QName> annotatedPorts = new ArrayList<QName>();

   public ServiceDelegateImpl(URL wsdlURL, QName serviceName, Class serviceClass)
   {
      // If this Service was constructed through the ServiceObjectFactory
      // this thread local association should be available
      usRef = ServiceObjectFactoryJAXWS.getServiceRefAssociation();
      UnifiedVirtualFile vfsRoot = (usRef != null ? vfsRoot = usRef.getVfsRoot() : new ResourceLoaderAdapter());

      // Verify wsdl access if this is not a generic Service
      if (wsdlURL != null && serviceClass != Service.class)
      {
         try
         {
            InputStream is = wsdlURL.openStream();
            is.close();
         }
         catch (IOException e)
         {
            log.warn("Cannot access wsdlURL: " + wsdlURL);
            wsdlURL = null;
         }
      }
      
      if (wsdlURL != null)
      {
         JAXWSClientMetaDataBuilder builder = new JAXWSClientMetaDataBuilder();
         serviceMetaData = builder.buildMetaData(serviceName, wsdlURL, vfsRoot);
      }
      else
      {
         UnifiedMetaData wsMetaData = new UnifiedMetaData(vfsRoot);
         serviceMetaData = new ServiceMetaData(wsMetaData, serviceName);
         wsMetaData.addService(serviceMetaData);
      }

      handlerResolver = new HandlerResolverImpl();

      if (usRef != null)
      {
         serviceMetaData.setServiceRefName(usRef.getServiceRefName());

         // Setup the service handlers
         if (usRef.getHandlerChain() != null)
         {
            String filename = usRef.getHandlerChain();
            UnifiedHandlerChainsMetaData handlerChainsMetaData = JAXWSMetaDataBuilder.getHandlerChainsMetaData(serviceClass, filename);
            for (UnifiedHandlerChainMetaData UnifiedHandlerChainMetaData : handlerChainsMetaData.getHandlerChains())
            {
               for (UnifiedHandlerMetaData uhmd : UnifiedHandlerChainMetaData.getHandlers())
               {
                  HandlerMetaDataJAXWS hmd = HandlerMetaDataJAXWS.newInstance(uhmd, HandlerType.ENDPOINT);
                  serviceMetaData.addHandler(hmd);
               }
            }
            ((HandlerResolverImpl)handlerResolver).initServiceHandlerChain(serviceMetaData);
         }
      }
   }

   /**
    * The getPort method returns a stub. A service client uses this stub to invoke operations on the target service endpoint.
    * The serviceEndpointInterface specifies the service endpoint interface that is supported by the created dynamic proxy or stub instance.
    */
   @Override
   public <T> T getPort(QName portName, Class<T> seiClass)
   {
      assertSEIConstraints(seiClass);

      if (serviceMetaData == null)
         throw new WebServiceException("Service meta data not available");

      // com/sun/ts/tests/jaxws/api/javax_xml_ws/Service#GetPort1NegTest1WithWsdl
      EndpointMetaData epMetaData = serviceMetaData.getEndpoint(portName);
      if (epMetaData == null && serviceMetaData.getEndpoints().size() > 0)
         throw new WebServiceException("Cannot get port meta data for: " + portName);

      // This is the case when the service could not be created from wsdl
      if (serviceMetaData.getEndpoints().size() == 0)
      {
         log.warn("Cannot get port meta data for: " + portName);

         QName portType = getPortTypeName(seiClass);
         epMetaData = new ClientEndpointMetaData(serviceMetaData, portName, portType, Type.JAXWS);
      }
      
      String seiClassName = seiClass.getName();
      epMetaData.setServiceEndpointInterfaceName(seiClassName);

      return getPortInternal(epMetaData, seiClass);
   }

   /**
    * The getPort method returns a stub. A service client uses this stub to invoke operations on the target service endpoint.
    * The serviceEndpointInterface specifies the service endpoint interface that is supported by the created dynamic proxy or stub instance.
    */
   @Override
   public <T> T getPort(Class<T> seiClass)
   {
      assertSEIConstraints(seiClass);

      if (serviceMetaData == null)
         throw new WebServiceException("Service meta data not available");

      String seiClassName = seiClass.getName();
      EndpointMetaData epMetaData = serviceMetaData.getEndpointByServiceEndpointInterface(seiClassName);

      if (epMetaData == null && serviceMetaData.getEndpoints().size() == 1)
      {
         epMetaData = serviceMetaData.getEndpoints().get(0);
         epMetaData.setServiceEndpointInterfaceName(seiClassName);
      }
      else
      {
         QName portTypeName = getPortTypeName(seiClass);
         for (EndpointMetaData epmd : serviceMetaData.getEndpoints())
         {
            if (portTypeName.equals(epmd.getPortTypeName()))
            {
               epmd.setServiceEndpointInterfaceName(seiClass.getName());
               epMetaData = epmd;
               break;
            }
         }
      }

      if (epMetaData == null)
         throw new WebServiceException("Cannot get port meta data for: " + seiClassName);

      return getPortInternal(epMetaData, seiClass);
   }

   private <T> QName getPortTypeName(Class<T> seiClass)
   {
      if (!seiClass.isAnnotationPresent(WebService.class))
         throw new IllegalArgumentException("Cannot find @WebService on: " + seiClass.getName());

      WebService anWebService = seiClass.getAnnotation(WebService.class);
      String localPart = anWebService.name();
      if (localPart.length() == 0)
         localPart = WSDLUtils.getJustClassName(seiClass);

      String nsURI = anWebService.targetNamespace();
      if (nsURI.length() == 0)
         nsURI = WSDLUtils.getTypeNamespace(seiClass);

      QName portType = new QName(nsURI, localPart);
      return portType;
   }

   private <T> T getPortInternal(EndpointMetaData epMetaData, Class<T> seiClass)
   {
      QName portName = epMetaData.getPortName();

      // Adjust the endpoint meta data according to the annotations
      if (annotatedPorts.contains(portName) == false)
      {
         synchronized (epMetaData)
         {
            if (annotatedPorts.contains(portName) == false)
            {
               JAXWSClientMetaDataBuilder metaDataBuilder = new JAXWSClientMetaDataBuilder();
               metaDataBuilder.rebuildEndpointMetaData(epMetaData, seiClass);
               annotatedPorts.add(portName);
            }
         }
      }

      return (T)createProxy(seiClass, epMetaData);
   }

   private void assertSEIConstraints(Class seiClass)
   {
      if (seiClass == null)
         throw new IllegalArgumentException("Service endpoint interface cannot be null");

      if (!seiClass.isAnnotationPresent(WebService.class))
         throw new WebServiceException("SEI is missing @WebService annotation: " + seiClass);
   }

   @Override
   /**
    * Creates a new port for the service.
    * Ports created in this way contain no WSDL port type information
    * and can only be used for creating Dispatchinstances.
    */
   public void addPort(QName portName, String bindingId, String epAddress)
   {
      EndpointMetaData epMetaData = serviceMetaData.getEndpoint(portName);
      if (epMetaData == null)
      {
         epMetaData = new ClientEndpointMetaData(serviceMetaData, portName, null, Type.JAXWS);
         serviceMetaData.addEndpoint(epMetaData);
      }
      epMetaData.setBindingId(bindingId);
      epMetaData.setEndpointAddress(epAddress);
   }

   @Override
   public <T> Dispatch<T> createDispatch(QName portName, Class<T> type, Mode mode)
   {
      ExecutorService executor = (ExecutorService)getExecutor();
      EndpointMetaData epMetaData = getEndpointMetaData(portName);
      DispatchImpl dispatch = new DispatchImpl(executor, epMetaData, type, mode);
      return dispatch;
   }

   @Override
   public Dispatch<Object> createDispatch(QName portName, JAXBContext jbc, Mode mode)
   {
      ExecutorService executor = (ExecutorService)getExecutor();
      EndpointMetaData epMetaData = getEndpointMetaData(portName);
      DispatchImpl dispatch = new DispatchImpl(executor, epMetaData, jbc, mode);
      return dispatch;
   }

   private EndpointMetaData getEndpointMetaData(QName portName)
   {
      EndpointMetaData epMetaData = serviceMetaData.getEndpoint(portName);
      if (epMetaData == null)
         throw new WebServiceException("Cannot find port: " + portName);

      return epMetaData;
   }

   /** Gets the name of this service. */
   @Override
   public QName getServiceName()
   {
      return serviceMetaData.getServiceName();
   }

   /** Returns an Iterator for the list of QNames of service endpoints grouped by this service */
   @Override
   public Iterator<QName> getPorts()
   {
      ArrayList<QName> portNames = new ArrayList<QName>();
      for (EndpointMetaData epMetaData : serviceMetaData.getEndpoints())
      {
         portNames.add(epMetaData.getPortName());
      }
      return portNames.iterator();
   }

   @Override
   public URL getWSDLDocumentLocation()
   {
      return serviceMetaData.getWsdlLocation();
   }

   @Override
   public HandlerResolver getHandlerResolver()
   {
      return handlerResolver;
   }

   @Override
   public void setHandlerResolver(HandlerResolver handlerResolver)
   {
      this.handlerResolver = handlerResolver;
   }

   private ExecutorService getDefaultExecutor()
   {
      if (defaultExecutor == null)
      {
         synchronized (DEFAULT_EXECUTOR_LOCK)
         {
            if (defaultExecutor == null)
            {
               defaultExecutor = Executors.newCachedThreadPool();
               if (log.isTraceEnabled())
               {
                  log.trace("Created new defaultExecutor", new Throwable("Call Trace"));
               }
            }
         }
      }

      return defaultExecutor;
   }
   
   @Override
   public Executor getExecutor()
   {
      if (executor == null)
      {
         executor = getDefaultExecutor();
      }
      return executor;
   }

   @Override
   public void setExecutor(Executor executor)
   {
      if ((executor instanceof ExecutorService) == false)
         throw new IllegalArgumentException("Supported executors must implement " + ExecutorService.class.getName());

      this.executor = (ExecutorService)executor;
   }

   private <T> T createProxy(Class<T> seiClass, EndpointMetaData epMetaData) throws WebServiceException
   {
      if (seiClass == null)
         throw new IllegalArgumentException("SEI class cannot be null");
      
      try
      {
         ExecutorService executor = (ExecutorService)getExecutor();
         ClientProxy handler = new ClientProxy(executor, new ClientImpl(epMetaData, handlerResolver));
         ClassLoader cl = epMetaData.getClassLoader();
         
         T proxy;
         try 
         {
            proxy = (T)Proxy.newProxyInstance(cl, new Class[] { seiClass, BindingProvider.class, StubExt.class }, handler);
         }
         catch (RuntimeException rte)
         {
            URL codeLocation = seiClass.getProtectionDomain().getCodeSource().getLocation();
            log.error("Cannot create proxy for SEI " + seiClass.getName() + " from: " + codeLocation);
            throw rte;
         }

         // Configure the stub
         configureStub((StubExt)proxy);

         return proxy;
      }
      catch (WebServiceException ex)
      {
         throw ex;
      }
      catch (Exception ex)
      {
         throw new WebServiceException("Cannot create proxy", ex);
      }
   }

   private void configureStub(StubExt stub)
   {
      EndpointMetaData epMetaData = stub.getEndpointMetaData();
      String seiName = epMetaData.getServiceEndpointInterfaceName();
      QName portName = epMetaData.getPortName();

      if (usRef == null)
      {
         log.debug("No port configuration for: " + portName);
         return;
      }

      String configFile = usRef.getConfigFile();
      String configName = usRef.getConfigName();

      UnifiedPortComponentRefMetaData pcref = usRef.getPortComponentRef(seiName, portName);
      if (pcref != null)
      {
         if (pcref.getConfigFile() != null)
            configFile = pcref.getConfigFile();
         if (pcref.getConfigName() != null)
            configName = pcref.getConfigName();

         BindingProvider bp = (BindingProvider)stub;
         Map<String, Object> reqCtx = bp.getRequestContext();
         for (UnifiedStubPropertyMetaData prop : pcref.getStubProperties())
         {
            log.debug("Set stub property: " + prop);
            reqCtx.put(prop.getPropName(), prop.getPropValue());
         }
      }

      if (configName != null || configFile != null)
      {
         log.debug("Configure Stub: [configName=" + configName + ",configFile=" + configFile + "]");
         stub.setConfigName(configName, configFile);
      }
   }

   @Override
   public <T> Dispatch<T> createDispatch(QName portName, Class<T> type, Mode mode, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public <T> Dispatch<T> createDispatch(EndpointReference endpointReference, Class<T> type, Mode mode, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public Dispatch<Object> createDispatch(QName portName, JAXBContext context, Mode mode, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public Dispatch<Object> createDispatch(EndpointReference endpointReference, JAXBContext context, Mode mode, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public <T> T getPort(QName portName, Class<T> serviceEndpointInterface, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public <T> T getPort(EndpointReference endpointReference, Class<T> serviceEndpointInterface, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }

   @Override
   public <T> T getPort(Class<T> serviceEndpointInterface, WebServiceFeature... features)
   {
      throw new NotImplementedException();
   }
}
