/*
* The Apache Software License, Version 1.1
*
*
* Copyright (c) 2001 The Apache Software Foundation.  All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in
*    the documentation and/or other materials provided with the
*    distribution.
*
* 3. The end-user documentation included with the redistribution,
*    if any, must include the following acknowledgment:
*       "This product includes software developed by the
*        Apache Software Foundation (http://www.apache.org/)."
*    Alternately, this acknowledgment may appear in the software itself,
*    if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Axis" and "Apache Software Foundation" must
*    not be used to endorse or promote products derived from this
*    software without prior written permission. For written
*    permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
*    nor may "Apache" appear in their name, without prior written
*    permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation.  For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/

package org.apache.axis.client;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Vector;

import javax.xml.namespace.QName;
import javax.xml.rpc.holders.Holder;

import org.apache.axis.description.OperationDesc;
import org.apache.axis.description.ParameterDesc;
import org.apache.axis.utils.BeanPropertyDescriptor;
import org.apache.axis.utils.BeanUtils;
import org.apache.axis.utils.JavaUtils;

/**
 * Very simple dynamic proxy InvocationHandler class.  This class is
 * constructed with a Call object, and then each time a method is invoked
 * on a dynamic proxy using this invocation handler, we simply turn it into
 * a SOAP request.
 *
 * @author Glen Daniels (gdaniels@macromedia.com)
 * @author Cédric Chabanois (cchabanois@ifrance.com)
 */
public class AxisClientProxy implements InvocationHandler {

    private Call call;
    private QName portName;

    /**
     * Constructor - package access only (should only really get used
     * in Service.getPort(endpoint, proxyClass).
     */
    AxisClientProxy(Call call, QName portName)
    {
        this.call = call;
        this.portName = portName; // can be null
    }


    /**
     * isBeanCompatible
     * 
     * @param javaType Class
     * Returns true if it appears that this class is a bean
     */
    protected boolean isBeanCompatible(Class javaType) {

        // Must be a non-primitive and non array
        if (javaType.isArray() ||
            javaType.isPrimitive()) {
            return false;
        }

        // Anything in the java or javax package that
        // does not have a defined mapping is excluded.
        if (javaType.getName().startsWith("java.") ||
            javaType.getName().startsWith("javax.")) {
            return false;
        }

        // Return true if appears to be an enum class
        if (JavaUtils.isEnumClass(javaType)) {
            return true;
        }

        // Must have a default public constructor 
        try {
            javaType.getConstructor(new Class[] {});
        } catch (java.lang.NoSuchMethodException e) {
            return false;
        }


        // Make sure superclass is compatible
        Class superClass = javaType.getSuperclass();
        if (superClass != null &&
            superClass != java.lang.Object.class) {
            if (!isBeanCompatible(superClass)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Parameters for invoke method are not the same as parameter for Call 
     * instance : 
     * - Holders must be converted to their mapped java types
     * - only in and inout parameters must be present in call parameters
     */
    private Object[] proxyParams2CallParams(Object[] proxyParams)
        throws JavaUtils.HolderException
    {
        OperationDesc operationDesc = call.getOperation();
        
        if (operationDesc == null)
        {
            // we don't know which parameters are IN, OUT or INOUT
            // let's suppose they are all in
            return proxyParams; 
        }
        
        Vector paramsCall = new Vector();
        for (int i = 0; i < proxyParams.length;i++)
        {
            Object param = proxyParams[i];
            ParameterDesc paramDesc = operationDesc.getParameter(i);            
            
            if (paramDesc.getMode() == paramDesc.INOUT) {
                paramsCall.add(JavaUtils.getHolderValue((Holder)param));
            } 
            else
            if (paramDesc.getMode() == paramDesc.IN) {
                paramsCall.add(param);
            }
        }
        return paramsCall.toArray();
    }

    /**
     * copy in/out and out parameters (Holder parameters) back to proxyParams 
     */
    private void callOutputParams2proxyParams(Object[] proxyParams)
        throws JavaUtils.HolderException
    {
        OperationDesc operationDesc = call.getOperation();
        if (operationDesc == null)
        {
            // we don't know which parameters are IN, OUT or INOUT
            // let's suppose they are all in
            return;
        }
        
        Map outputParams = call.getOutputParams();
       
        for (int i = 0; i < operationDesc.getNumParams();i++)
        {
            Object param = proxyParams[i];
            ParameterDesc paramDesc = operationDesc.getParameter(i);
            if ((paramDesc.getMode() == paramDesc.INOUT) ||
                (paramDesc.getMode() == paramDesc.OUT)) {
                             
                  JavaUtils.setHolderValue((Holder)param,
                      outputParams.get(paramDesc.getQName()));
            }
        }
    }


    public void registerBean(Class beanClass, QName qname)
    {
        call.registerTypeMapping(
            beanClass,
            qname,
            org.apache.axis.encoding.ser.BeanSerializerFactory.class,
            org.apache.axis.encoding.ser.BeanDeserializerFactory.class,
            false);      
        BeanPropertyDescriptor[] propDescs = BeanUtils.getPd(beanClass);
        for (int i = 0;i < propDescs.length;i++)
        {
            BeanPropertyDescriptor propDesc = propDescs[i];
            QName beanPropQName = new QName(qname.getNamespaceURI(),propDesc.getName());
            registerClass(propDesc.getClass(),beanPropQName);
        }        
    }

    public void registerClass(Class javaType, QName qname) {
        if (javaType.isArray()) {
            call.registerTypeMapping(
                javaType,
                qname,
                org.apache.axis.encoding.ser.ArraySerializerFactory.class,
                org.apache.axis.encoding.ser.ArrayDeserializerFactory.class,
                false);
        } else if (JavaUtils.isEnumClass(javaType)) {
            call.registerTypeMapping(
                javaType,
                qname,
                org.apache.axis.encoding.ser.EnumSerializerFactory.class,
                org.apache.axis.encoding.ser.EnumDeserializerFactory.class,
                false);
        } else if (isBeanCompatible(javaType)) {
            call.registerTypeMapping(
                javaType,
                qname,
                org.apache.axis.encoding.ser.BeanSerializerFactory.class,
                org.apache.axis.encoding.ser.BeanDeserializerFactory.class,
                false);
        }
    }

    public void registerSerializers(Class[] proxyParamsClasses)
        throws JavaUtils.HolderException
    {
        OperationDesc operationDesc = call.getOperation();
        
        if (operationDesc == null)
          return;

        for (int i = 0; i < proxyParamsClasses.length;i++)
        {
            ParameterDesc paramDesc = operationDesc.getParameter(i);
            Class paramClass = proxyParamsClasses[i];
            Class heldClass = JavaUtils.getHolderValueType(paramClass);
            if (heldClass != null)
                paramClass = heldClass;
            
            registerClass(paramClass, paramDesc.getTypeQName());
        }        
    }


    /**
     * Handle a method invocation.
     */
    public Object invoke(Object o, Method method, Object[] objects)
            throws Throwable {
        if (method.getName().equals("_setProperty")) {
            call.setProperty((String) objects[0], objects[1]);
            return null;
        }
        else {
          Object outValue;
          Object[] paramsCall;
          
          if ((call.getTargetEndpointAddress() != null) &&
              (call.getPortName() != null)) {
              // call object has been prefilled : targetEndPoint and portname
              // are already set. We complete it with method informations
              call.setOperation(method.getName());
              registerSerializers(method.getParameterTypes());
              paramsCall = proxyParams2CallParams(objects);
              outValue = call.invoke(paramsCall);
          }
          else if (portName != null)
          {
              // we only know the portName. Try to complete this information
              // from wsdl if available
              call.setOperation(portName,method.getName());
              registerSerializers(method.getParameterTypes());
              paramsCall = proxyParams2CallParams(objects);
              outValue = call.invoke(paramsCall);
          }
          else
          {
              // we don't even know the portName (we don't have wsdl)
              paramsCall = objects;
              outValue = call.invoke(method.getName(), paramsCall);
          }
          callOutputParams2proxyParams(objects);
          return outValue;
        }
    }
}
