/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.webservice.client;

// $Id: PortProxy.java,v 1.4.2.11 2005/07/27 12:54:40 tdiesler Exp $

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;

import org.jboss.axis.Constants;
import org.jboss.axis.client.AxisClientProxy;
import org.jboss.axis.description.OperationDesc;
import org.jboss.axis.description.ParameterDesc;
import org.jboss.axis.description.ParameterWrapping;
import org.jboss.axis.enums.Style;
import org.jboss.axis.enums.Use;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.logging.Logger;
import org.jboss.webservice.deployment.BeanXMLMetaData;
import org.jboss.webservice.deployment.ServiceDescription;
import org.jboss.webservice.deployment.TypeMappingDescription;

/**
 * This is the proxy that implements the service endpoint interface.
 * <p/>
 * Additionally it handles some ws4ee functionality that is not part of jaxrpc behaviour.
 *
 * @author Thomas.Diesler@jboss.org
 * @since 15-Jun-2004
 */
public class PortProxy implements InvocationHandler
{
   // provide logging
   private static final Logger log = Logger.getLogger(PortProxy.class);

   // The underlying jaxrpc call
   private Remote port;
   private CallImpl call;

   // The methods from the SEI
   private List seiMethods = new ArrayList();
   // The methods from java.lang.Object
   private List objectMethods = new ArrayList();
   // The methods from javax.xml.rpc.Stub
   private List stubMethods = new ArrayList();
   private static List transportProps = new ArrayList();
   static
   {
      transportProps.add(Stub.PROPERTY_KEY_STORE);
      transportProps.add(Stub.PROPERTY_KEY_STORE_PASSWORD);
      transportProps.add(Stub.PROPERTY_KEY_STORE_TYPE);
      transportProps.add(Stub.PROPERTY_TRUST_STORE);
      transportProps.add(Stub.PROPERTY_TRUST_STORE_PASSWORD);
      transportProps.add(Stub.PROPERTY_TRUST_STORE_TYPE);
   }

   // The stub property methods
   private Method getPropertyMethod;
   private Method setPropertyMethod;

   /**
    * Construct a client side call proxy.
    * <p/>
    * This proxy implements the (generated) service interface and the service endpoint interface
    * for each port component ref.
    *
    * @param port     The underlying proxy to the service endpoint
    * @param seiClass The service endpoint interface
    */
   public PortProxy(Remote port, Class seiClass)
   {
      this.port = port;

      // Get the underlying call object
      AxisClientProxy axisClientProxy = (AxisClientProxy)Proxy.getInvocationHandler(port);
      this.call = (CallImpl)axisClientProxy.getCall();

      // initialize java.lang.Object methods
      objectMethods.addAll(Arrays.asList(Object.class.getMethods()));

      // initialize SEI methods
      seiMethods.addAll(Arrays.asList(seiClass.getMethods()));

      // initialize javax.xml.rpc.Stub methods
      stubMethods.addAll(Arrays.asList(org.jboss.webservice.client.Stub.class.getMethods()));

      // initialize javax.xml.rpc.Stub property methods
      try
      {
         getPropertyMethod = Stub.class.getMethod("_getProperty", new Class[] { String.class });
         setPropertyMethod = Stub.class.getMethod("_setProperty", new Class[] { String.class, Object.class });
      }
      catch (NoSuchMethodException ignore)
      {
      }
   }

   /**
    * Processes a method invocation on a proxy instance and returns
    * the result.
    */
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
   {
      String methodName = method.getName();

      try
      {
         Object retObj = null;
         if (seiMethods.contains(method))
         {
            log.debug("Invoke on service endpoint interface: " + methodName);

            AxisClientProxy axisProxy = (AxisClientProxy)Proxy.getInvocationHandler(port);
            axisProxy.setupCallOperation(method);

            OperationDesc opDesc = call.getOperation();
            Style style = opDesc.getStyle();
            Use use = opDesc.getUse();

            // check if need to wrap a doc/lit request structure
            // [TDI] 30-Jun-2005
            if (style == Style.DOCUMENT && use == Use.LITERAL && opDesc.getNumInParams() == 1)
            {
               ParameterDesc paramDesc = (ParameterDesc)opDesc.getInParams().get(0);
               QName typeQName = paramDesc.getTypeQName();
               Class javaType = paramDesc.getJavaType();
               Object firstArg = (args != null ? args[0] : null);
               if (!Constants.isSchemaXSD(typeQName.getNamespaceURI())
                     && (firstArg == null || !JavaUtils.isConvertable(firstArg, javaType, false)))
               {
                  if (paramDesc.getWrappedVariables() == null)
                  {
                     ServiceDescription serviceDescription = call.getServiceDescription();
                     TypeMappingDescription typeMapping = serviceDescription.getTypeMapping(typeQName);
                     if (typeMapping == null)
                        throw new IllegalStateException("Cannot obtain type mapping for: " + typeQName);

                     BeanXMLMetaData tmMetaData = typeMapping.getMetaData();
                     if (tmMetaData == null)
                        throw new IllegalStateException("Cannot obtain type mapping meta data for: " + typeQName);

                     opDesc.setWrapParameters(true);
                     paramDesc.setWrappedVariables(tmMetaData.getElementOrder());
                  }
                  ParameterDesc retParamDesc = (ParameterDesc)opDesc.getReturnParamDesc();
                  QName retTypeQName = retParamDesc.getTypeQName();
                  if (retParamDesc.getWrappedVariables() == null)
                  {
                     ServiceDescription serviceDescription = call.getServiceDescription();
                     TypeMappingDescription typeMapping = serviceDescription.getTypeMapping(retTypeQName);
                     BeanXMLMetaData tmMetaData = typeMapping.getMetaData();
                     if (tmMetaData == null)
                        throw new IllegalStateException("Cannot obtain type mapping meta data for: " + retTypeQName);

                     opDesc.setWrapParameters(true);
                     retParamDesc.setWrappedVariables(tmMetaData.getElementOrder());
                  }

                  args = new Object[] { ParameterWrapping.wrapRequestParameters(opDesc, args) };
               }
            }

            retObj = axisProxy.invokeSEIMethod(method, args);

            Class returnType = method.getReturnType();
            if (retObj != null && !returnType.isAssignableFrom(retObj.getClass()))
            {
               if (JavaUtils.isConvertable(retObj, returnType, false))
                  retObj = JavaUtils.convert(retObj, returnType);

               else if (opDesc.isWrapParameters())
                  retObj = ParameterWrapping.unwrapResponseParameter(opDesc, retObj);
            }

            return retObj;
         }

         if (stubMethods.contains(method))
         {
            log.debug("Invoke on stub interface: " + methodName);

            // Handle standard properties
            if ("getUsername".equals(methodName))
               retObj = getPropertyMethod.invoke(port, new Object[] { Stub.USERNAME_PROPERTY });
            else if ("setUsername".equals(methodName))
               retObj = setPropertyMethod.invoke(port, new Object[] { Stub.USERNAME_PROPERTY, args[0] });
            else if ("getPassword".equals(methodName))
               retObj = getPropertyMethod.invoke(port, new Object[] { Stub.PASSWORD_PROPERTY });
            else if ("setPassword".equals(methodName))
               retObj = setPropertyMethod.invoke(port, new Object[] { Stub.PASSWORD_PROPERTY, args[0] });
            else if ("getEndpointAddress".equals(methodName))
               retObj = getPropertyMethod.invoke(port, new Object[] { Stub.ENDPOINT_ADDRESS_PROPERTY });
            else if ("setEndpointAddress".equals(methodName))
               retObj = setPropertyMethod.invoke(port, new Object[] { Stub.ENDPOINT_ADDRESS_PROPERTY, args[0] });
            else if ("getSessionMaintain".equals(methodName))
               retObj = getPropertyMethod.invoke(port, new Object[] { Stub.SESSION_MAINTAIN_PROPERTY });
            else if ("setSessionMaintain".equals(methodName))
               retObj = setPropertyMethod.invoke(port, new Object[] { Stub.SESSION_MAINTAIN_PROPERTY, args[0] });

            // Handle non standard timeout
            else if ("getTimeout".equals(methodName))
               retObj = call.getTimeout();
            else if ("setTimeout".equals(methodName))
               call.setTimeout((Integer)args[0]);
            // for backward compatibility
            else if (getPropertyMethod.equals(method) && Stub.CLIENT_TIMEOUT_PROPERTY.equals(args[0]))
               retObj = call.getTimeout();
            else if (setPropertyMethod.equals(method) && Stub.CLIENT_TIMEOUT_PROPERTY.equals(args[0]))
               call.setTimeout((Integer)args[1]);
            else if (getPropertyMethod.equals(method) && Stub.PROPERTY_CLIENT_TIMEOUT.equals(args[0]))
               retObj = call.getTimeout();
            else if (setPropertyMethod.equals(method) && Stub.PROPERTY_CLIENT_TIMEOUT.equals(args[0]))
               call.setTimeout((Integer)args[1]);
            else if ("getTransportOption".equals(methodName))
               retObj = call.getTransportOption((String)args[0]);
            else if ("setTransportOption".equals(methodName))
               call.setTransportOption((String)args[0], args[1]);
            else if (getPropertyMethod.equals(method) && transportProps.contains(args[0]))
               retObj = call.getTransportOption((String)args[0]);
            else if (setPropertyMethod.equals(method) && transportProps.contains(args[0]))
               call.setTransportOption((String)args[0], args[1]);

            // Handle non standard attachments
            else if ("addAttachment".equals(methodName))
               call.addAttachment((String)args[0], args[1]);
            else if ("removeAttachment".equals(methodName))
               call.removeAttachment((String)args[0]);
            else if ("getAttachments".equals(methodName))
               retObj = call.getAttachmentIdentifiers();
            else if ("getAttachment".equals(methodName))
               retObj = call.getAttachment((String)args[0]);

            // It's a standard Stub method
            else
               retObj = method.invoke(port, args);

            return retObj;
         }
         if (objectMethods.contains(method))
         {
            log.debug("Invoke on object: " + methodName);

            retObj = method.invoke(port, args);
            return retObj;
         }

         throw new JAXRPCException("Don't know how to invoke: " + method);
      }
      catch (Exception e)
      {
         Throwable th = processException(e);

         // On the client side, the JAXRPCException or runtime exception is propagated to the
         // client code as a RemoteException or its subtype.
         if (seiMethods.contains(method))
         {
            if (th instanceof JAXRPCException || th instanceof RuntimeException)
            {
               RemoteException re = new RemoteException(th.getMessage(), th);
               th = re;
            }
         }

         throw th;
      }
   }

   /**
    * Log the client side exception
    */
   private Throwable processException(Exception ex)
   {
      Throwable th = ex;
      if (ex instanceof InvocationTargetException)
         th = ((InvocationTargetException)ex).getTargetException();

      log.error("Port error", th);
      return th;
   }
}