/*
 * 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.client;

// $Id:ClientProxy.java 1054 2006-09-26 10:33:43Z thomas.diesler@jboss.com $

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPException;
import javax.xml.ws.AsyncHandler;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Response;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.soap.SOAPFaultException;

import org.jboss.logging.Logger;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.StubExt;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.wsf.common.JavaUtils;

/**
 * The dynamic proxy that delegates to the underlying client implementation
 *
 * @author Thomas.Diesler@jboss.org
 * @since 04-Jul-2006
 */
public class ClientProxy implements InvocationHandler
{
   // provide logging
   private static final Logger log = Logger.getLogger(ClientProxy.class);

   // The underlying Call
   private ClientImpl client;
   // List<Method> of the Stub methods
   private List stubMethods;
   // List<Method> of the Object methods
   private List objectMethods;

   // The service configured executor
   private ExecutorService executor;

   // The set of standard properties
   private static final Set<String> standardProperties = new HashSet<String>();
   static
   {
      standardProperties.add(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
      standardProperties.add(BindingProvider.SESSION_MAINTAIN_PROPERTY);
      standardProperties.add(BindingProvider.USERNAME_PROPERTY);
      standardProperties.add(BindingProvider.PASSWORD_PROPERTY);
      standardProperties.add(BindingProvider.SOAPACTION_USE_PROPERTY);
      standardProperties.add(BindingProvider.SOAPACTION_URI_PROPERTY);
   }

   public ClientProxy(ExecutorService executor, ClientImpl client)
   {
      this.client = client;
      this.executor = executor;
      this.stubMethods = new ArrayList(Arrays.asList(BindingProvider.class.getMethods()));
      this.stubMethods.addAll(Arrays.asList(StubExt.class.getMethods()));
      this.objectMethods = Arrays.asList(Object.class.getMethods());
   }

   /** Processes a method invocation on a proxy instance and returns the result.
    */
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
   {
      // An invocation on the Stub interface
      String methodName = method.getName();
      if (stubMethods.contains(method))
      {
         Method stubMethod = ClientImpl.class.getMethod(methodName, method.getParameterTypes());
         return stubMethod.invoke(client, args);
      }

      // An invocation on proxy's Object class
      else if (objectMethods.contains(method))
      {
         Method objMethod = ClientImpl.class.getMethod(methodName, method.getParameterTypes());
         return objMethod.invoke(client, args);
      }

      // An invocation on the service endpoint interface
      else
      {
         EndpointMetaData epMetaData = client.getEndpointMetaData();
         OperationMetaData opMetaData = epMetaData.getOperation(method);
         if (opMetaData == null)
            throw new WSException("Cannot obtain operation meta data for: " + methodName);

         QName opName = opMetaData.getQName();

         if (log.isTraceEnabled())
            log.trace("Invoke method: " + method + opMetaData);

         try
         {
            Object retObj;
            Class retType = method.getReturnType();
            boolean isAsync = methodName.endsWith(Constants.ASYNC_METHOD_SUFFIX);

            // Invoke asynchronously
            if (isAsync && JavaUtils.isAssignableFrom(Response.class, retType))
            {
               retObj = invokeAsync(opName, args, retType);
            }
            // Invoke asynchronously with handler
            else if (isAsync && JavaUtils.isAssignableFrom(Future.class, retType) && args.length > 1)
            {
               Object handler = args[args.length - 1];
               retObj = invokeAsync(opName, args, retType, (AsyncHandler)handler);
            }
            // Invoke synchronously
            else
            {
               Map<String, Object> resContext = client.getBindingProvider().getResponseContext();
               retObj = invoke(opName, args, retType, resContext);
            }
            return retObj;
         }
         catch (Exception ex)
         {
            handleException(ex);
            return null;
         }
      }
   }

   private Object invoke(QName opName, Object[] args, Class retType, Map<String, Object> resContext) throws RemoteException
   {
      Object retObj = client.invoke(opName, args, resContext);
      if (retObj != null)
      {
         if (retType == null)
            throw new WSException("Return value not supported by: " + opName);

         if (JavaUtils.isPrimitive(retType))
            retObj = JavaUtils.getPrimitiveValueArray(retObj);
      }
      return retObj;
   }

   private Response invokeAsync(QName opName, Object[] args, Class retType)
   {
      ResponseImpl response = new ResponseImpl();
      Runnable task = new AsyncRunnable(response, null, opName, args, retType);

      if (log.isDebugEnabled())
         log.debug("Schedule task " + ((AsyncRunnable)task).getTaskID().toString());

      Future future = executor.submit(task);
      response.setFuture(future);
      return response;
   }

   private Future invokeAsync(QName opName, Object[] args, Class retType, AsyncHandler handler)
   {
      ResponseImpl response = new ResponseImpl();
      Runnable task = new AsyncRunnable(response, handler, opName, args, retType);
      Future future = executor.submit(task);
      response.setFuture(future);
      return response;
   }

   /**
    * 4.2.4  Conformance (Remote Exceptions): If an error occurs during a remote operation invocation, an implemention
    * MUST throw a service specific exception if possible. If the error cannot be mapped to a service
    * specific exception, an implementation MUST throw a ProtocolException or one of its subclasses, as
    * appropriate for the binding in use. See section 6.4.1 for more details.
    */
   private void handleException(Exception ex) throws Throwable
   {
      if (ex instanceof SOAPFaultException)
      {
         // Unwrap the cause if it is an Application Exception, otherwise use a protocol exception
         Throwable cause = ex.getCause();
         if (cause instanceof Exception)
         {
            // Throw unwrapped WebServiceException
            if (cause instanceof WebServiceException)
               throw (WebServiceException)cause;

            // Throw wrapped SOAPException
            if (cause instanceof SOAPException)
               throw (SOAPFaultException)ex;

            // Throw wrapped RuntimeException
            if (cause instanceof RuntimeException)
               throw (SOAPFaultException)ex;

            // Throw all other causes
            throw (Exception)cause;
         }
      }
      throw ex;
   }

   class AsyncRunnable implements Runnable
   {
      private ResponseImpl response;
      private AsyncHandler handler;
      private QName opName;
      private Object[] args;
      private Class retType;
      private UUID uuid;

      public AsyncRunnable(ResponseImpl response, AsyncHandler handler, QName opName, Object[] args, Class retType)
      {
         this.response = response;
         this.handler = handler;
         this.opName = opName;
         this.args = args;
         this.retType = retType;
         this.uuid = UUID.randomUUID();
      }

      public void run()
      {
         try
         {
            Map<String, Object> resContext = response.getContext();
            Object result = invoke(opName, args, retType, resContext);

            if (log.isDebugEnabled())
               log.debug("Finished task " + getTaskID().toString() + ": " + result);

            response.set(result);
         }
         catch (Exception ex)
         {
            handleAsynInvokeException(ex);
         }
         
         // Call the handler if available
         if (handler != null)
            handler.handleResponse(response);
      }

      // 2.3.4.5 Conformance (Asychronous fault cause): An ExecutionException that is thrown by the get method
      // of Response as a result of a WSDL fault MUST have as its cause the service specific exception mapped
      // from the WSDL fault, if there is one, otherwise the ProtocolException mapped from the WSDL fault.      
      private void handleAsynInvokeException(Exception ex)
      {
         Exception toBeWrapped = ex;
         if (ex instanceof SOAPFaultException)
         {
            // Unwrap the cause if it is an Application Exception, otherwise use a protocol exception
            Throwable cause = ex.getCause();
            if (cause instanceof Exception)
            {
               // Use unwrapped WebServiceException
               if (cause instanceof WebServiceException)
                  ex = (WebServiceException)cause;

               // Use application exception if possible.
               if (((cause instanceof SOAPException) == false) && ((cause instanceof RuntimeException) == false))
               {
                  toBeWrapped = (Exception)cause;
               }
            }
         }

         response.setException(toBeWrapped);
      }

      public UUID getTaskID()
      {
         return uuid;
      }
   }
}
