/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.enterprise.webservice;

import java.lang.UnsupportedOperationException;

import java.net.URL;

import java.util.HashSet;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;

import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.HandlerRegistry;
import javax.xml.rpc.Stub;
import javax.xml.rpc.Call;

import com.sun.enterprise.deployment.ServiceReferenceDescriptor;
import com.sun.enterprise.deployment.ServiceRefPortInfo;
import com.sun.enterprise.deployment.NameValuePairDescriptor;
import com.sun.enterprise.deployment.runtime.common.MessageSecurityBindingDescriptor;

import com.sun.enterprise.security.jauth.ClientAuthConfig;
import com.sun.enterprise.webservice.MessageLayerClientHandler;

/**
 * InvocationHandler used to intercept calls to concrete JAXRPC 
 * Service implementation.  
 *
 * NOTE : This class makes no distinction between "partial" WSDL and
 * "full" WSDL.  If a service-ref's packaged WSDL is "partial", the
 * deployer is required to specify a wsdl-override in the runtime info
 * that points to a final WSDL.  In such a case, the behavior for each
 * method listed in the table in section 4.2.2.7 of the spec is the
 * same as Full WSDL.
 *
 * @author Kenneth Saks
 */
public class ServiceInvocationHandler implements InvocationHandler {

    private ServiceReferenceDescriptor serviceRef;

    // real service instance
    private Service serviceDelegate;

    // used in full wsdl case for DII methods. Lazily instantiated.
    private Service configuredServiceDelegate;

    private ClassLoader classLoader;

    private Method getClientManagedPortMethod;

    // location of full wsdl associated with service-ref
    private URL wsdlLocation;

    private boolean fullWsdl = false;
    private boolean noWsdl = false;

    private WsUtil wsUtil = new WsUtil();

    // Service method types
    private static final int CREATE_CALL_NO_ARGS = 1;
    private static final int CREATE_CALL_PORT = 2;
    private static final int CREATE_CALL_OPERATION_QNAME = 3;
    private static final int CREATE_CALL_OPERATION_STRING = 4;
    private static final int GET_CALLS = 5;
    private static final int GET_HANDLER_REGISTRY = 6;
    private static final int GET_PORT_CONTAINER_MANAGED = 7;
    private static final int GET_PORT_CLIENT_MANAGED = 8;
    private static final int GET_PORTS = 9;
    private static final int GET_SERVICE_NAME = 10;
    private static final int GET_TYPE_MAPPING_REGISTRY = 11;
    private static final int GET_WSDL_LOCATION = 12;
    private static final int GENERATED_SERVICE_METHOD = 13;

    private static Map serviceMethodTypes;
    private static Set fullWsdlIllegalMethods;
    private static Set noWsdlIllegalMethods;

    static {
        Init();
    }

    public ServiceInvocationHandler(ServiceReferenceDescriptor descriptor,
                                    Service delegate, ClassLoader loader)
        throws Exception {

        serviceRef = descriptor;
        serviceDelegate = delegate;
        classLoader = loader;

        if( serviceRef.hasWsdlFile() ) {
            wsdlLocation = wsUtil.privilegedGetServiceRefWsdl(serviceRef);
            fullWsdl = true;
        } else {
            noWsdl = true;
        }

        getClientManagedPortMethod = javax.xml.rpc.Service.class.getMethod
            ("getPort", new Class[] { QName.class, Class.class } );

	addMessageSecurityHandler(delegate);
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) 
        throws Throwable {

        // NOTE : be careful with "args" parameter.  It is null
        //        if method signature has 0 arguments.

        if( method.getDeclaringClass() == java.lang.Object.class )  {
            return invokeJavaObjectMethod(this, method, args);
        }

        int methodType = getMethodType(method);

        checkUnsupportedMethods(methodType);

        Object returnValue = null;

        try {

            // Initialize method info for invocation based on arguments.
            // Some/All of this might be overridden below.
            Object serviceToInvoke = serviceDelegate;
            Method methodToInvoke  = method;
            int methodTypeToInvoke = methodType;
            Object[] argsForInvoke = args;

            switch(methodType) {

            case GET_PORT_CONTAINER_MANAGED :
                Class serviceEndpointInterfaceClass = (Class) args[0];
                String serviceEndpointInterface = 
                    serviceEndpointInterfaceClass.getName();
                ServiceRefPortInfo portInfo = 
                    serviceRef.getPortInfo(serviceEndpointInterface);
                
                // If we have a port, use it to call getPort(QName, SEI) instead
                if( (portInfo != null) && portInfo.hasWsdlPort() ) {
                    methodToInvoke = getClientManagedPortMethod;
                    methodTypeToInvoke = GET_PORT_CLIENT_MANAGED;
                    argsForInvoke  = new Object[] { portInfo.getWsdlPort(),
                                                    args[0] };
                } else {
                    // This means the deployer did not resolve the port to 
                    // which this SEI is mapped.  Just call getPort(SEI) 
                    // method on delegate. This is not guaranteed to work.
                }
                break;

            case GET_WSDL_LOCATION :
                return wsdlLocation;

            case CREATE_CALL_PORT :
            case CREATE_CALL_OPERATION_QNAME :
            case CREATE_CALL_OPERATION_STRING :
            case GET_CALLS :
            case GET_PORTS :

                serviceToInvoke = getConfiguredServiceDelegate();
                break;

            } // End switch (methodType)

            returnValue = methodToInvoke.invoke(serviceToInvoke, argsForInvoke);
            
            if( returnValue instanceof Stub ) {
                Stub stub = (Stub) returnValue;
                setStubProperties(stub, methodTypeToInvoke, methodToInvoke, 
                                  argsForInvoke);
            } else if( returnValue instanceof Call ) {
                Call[] calls = new Call[1];
                calls[0] = (Call) returnValue;
                setCallProperties(calls, methodTypeToInvoke, argsForInvoke);
            } else if( methodType == GET_CALLS ) {
                Call[] calls = (Call[]) returnValue;
                setCallProperties(calls, methodTypeToInvoke, argsForInvoke);
            }
            
        } catch(InvocationTargetException ite) {
            throw ite.getCause();
        }

	return returnValue;
    }

    public HandlerInfo getMessageSecurityHandlerInfo(QName port) throws Exception 
    {
	HandlerInfo rvalue = null;

	MessageSecurityBindingDescriptor binding = null;
	ServiceRefPortInfo portInfo = serviceRef.getPortInfoByPort(port);
	if (portInfo != null) {
	    binding = portInfo.getMessageSecurityBinding();
	}

	ClientAuthConfig config = ClientAuthConfig.getConfig
	    (com.sun.enterprise.security.jauth.AuthConfig.SOAP,
	     binding, null);

	if (config != null) {

	    // get understood headers from auth module.
	    QName[] headers = config.getMechanisms();
	    
	    Map properties = new HashMap();
	    properties.put(MessageLayerClientHandler.CLIENT_AUTH_CONFIG, config);
            properties.put(javax.xml.ws.handler.MessageContext.WSDL_SERVICE,
                serviceRef.getServiceName());

	    rvalue = new HandlerInfo(MessageLayerClientHandler.class, 
				     properties, headers);
	}

	return rvalue;
    }

    private boolean addMessageSecurityHandler(Service service) throws Exception
    {
	HandlerRegistry registry = service.getHandlerRegistry();
	Iterator ports = null;
	try {
	    ports = service.getPorts();
	} catch (Exception e) {
	    // FIXME: should make sure that the exception was thrown because
	    // the service is not fully defined; but for now just return.
	    ports = null;
	}

        while(ports != null && ports.hasNext()) {

            QName nextPort = (QName) ports.next();

            List handlerChain = registry.getHandlerChain(nextPort);

	    // append security handler to the end of every handler chain
	    // ASSUMPTION 1: that registry.getHandlerChain() never returns null.
	    // ASSUMPTION 2: that handlers from ServiceRef have already been added

	    HandlerInfo handlerInfo = getMessageSecurityHandlerInfo(nextPort);

	    if (handlerInfo != null) {
		handlerChain.add(handlerInfo);
	    }
        }

	return ports == null ? false : true;
    }
	
    private Service getConfiguredServiceDelegate() throws Exception {
        synchronized(this) {
            if( configuredServiceDelegate == null ) {
                // We need a ConfiguredService to handle these
                // invocations, since the JAXRPC RI Generated Service impl 
                // does not.  Configured service is potentially 
                // a heavy-weight object so we lazily instantiate it to
                // take advantage of the likelihood that 
                // GeneratedService service-refs won't be used for DII.
                Service configuredService = 
                    wsUtil.createConfiguredService(serviceRef);
                wsUtil.configureHandlerChain(serviceRef, configuredService, 
					     configuredService.getPorts(), classLoader);
                configuredServiceDelegate = configuredService;
		
		addMessageSecurityHandler(configuredService);
            }
        }
        return configuredServiceDelegate;
    }

    private int getMethodType(Method method) {
        Integer methodType = (Integer) serviceMethodTypes.get(method);
        return (methodType != null) ? 
            methodType.intValue() : GENERATED_SERVICE_METHOD;
    }

    /**
     * Convert invocation method to a constant for easier processing.
     */
    private static void Init() {

        serviceMethodTypes     = new HashMap();
        fullWsdlIllegalMethods = new HashSet();
        noWsdlIllegalMethods   = new HashSet();

        try {

            Class noParams[]   = new Class[0];
            String createCall  = "createCall";
            Class serviceClass = javax.xml.rpc.Service.class;

            //
            // Map Service method to method type.
            //

            Method createCallNoArgs = 
                serviceClass.getDeclaredMethod(createCall, noParams);
            serviceMethodTypes.put(createCallNoArgs, 
                                   new Integer(CREATE_CALL_NO_ARGS));

            Method createCallPort =
                serviceClass.getDeclaredMethod(createCall, 
                                               new Class[] { QName.class });
            serviceMethodTypes.put(createCallPort, 
                                   new Integer(CREATE_CALL_PORT));

            Method createCallOperationQName =
                serviceClass.getDeclaredMethod
                (createCall, new Class[] { QName.class, QName.class });
            serviceMethodTypes.put(createCallOperationQName,
                                   new Integer(CREATE_CALL_OPERATION_QNAME));

            Method createCallOperationString =
                serviceClass.getDeclaredMethod
                (createCall, new Class[] { QName.class, String.class });
            serviceMethodTypes.put(createCallOperationString,
                                   new Integer(CREATE_CALL_OPERATION_STRING));
            
            Method getCalls = serviceClass.getDeclaredMethod
                ("getCalls", new Class[] { QName.class });
            serviceMethodTypes.put(getCalls, new Integer(GET_CALLS));

            Method getHandlerRegistry = serviceClass.getDeclaredMethod
                ("getHandlerRegistry", noParams);
            serviceMethodTypes.put(getHandlerRegistry, 
                                   new Integer(GET_HANDLER_REGISTRY));

            Method getPortContainerManaged = serviceClass.getDeclaredMethod
                ("getPort", new Class[] { Class.class });
            serviceMethodTypes.put(getPortContainerManaged, 
                                   new Integer(GET_PORT_CONTAINER_MANAGED));

            Method getPortClientManaged = serviceClass.getDeclaredMethod
                ("getPort", new Class[] { QName.class, Class.class });
            serviceMethodTypes.put(getPortClientManaged, 
                                   new Integer(GET_PORT_CLIENT_MANAGED));
            
            Method getPorts = serviceClass.getDeclaredMethod
                ("getPorts", noParams);
            serviceMethodTypes.put(getPorts, new Integer(GET_PORTS));

            Method getServiceName = serviceClass.getDeclaredMethod
                ("getServiceName", noParams);
            serviceMethodTypes.put(getServiceName, 
                                   new Integer(GET_SERVICE_NAME));

            Method getTypeMappingRegistry = serviceClass.getDeclaredMethod
                ("getTypeMappingRegistry", noParams);
            serviceMethodTypes.put(getTypeMappingRegistry, 
                                   new Integer(GET_TYPE_MAPPING_REGISTRY));

            Method getWsdlLocation = serviceClass.getDeclaredMethod
                ("getWSDLDocumentLocation", noParams);
            serviceMethodTypes.put(getWsdlLocation,
                                   new Integer(GET_WSDL_LOCATION));
        } catch(NoSuchMethodException nsme) {}

        // Implementation of table 4.2.2.7.  All "No WSDL" column cells
        // with value Unspecified throw UnsupportedOperationException

        fullWsdlIllegalMethods.add(new Integer(GET_HANDLER_REGISTRY));
        fullWsdlIllegalMethods.add(new Integer(GET_TYPE_MAPPING_REGISTRY));

        noWsdlIllegalMethods.add(new Integer(CREATE_CALL_PORT));
        noWsdlIllegalMethods.add(new Integer(CREATE_CALL_OPERATION_QNAME));
        noWsdlIllegalMethods.add(new Integer(CREATE_CALL_OPERATION_STRING));
        noWsdlIllegalMethods.add(new Integer(GET_CALLS));
        noWsdlIllegalMethods.add(new Integer(GET_HANDLER_REGISTRY));
        noWsdlIllegalMethods.add(new Integer(GET_PORT_CONTAINER_MANAGED));
        noWsdlIllegalMethods.add(new Integer(GET_PORT_CLIENT_MANAGED));
        noWsdlIllegalMethods.add(new Integer(GET_PORTS));
        noWsdlIllegalMethods.add(new Integer(GET_SERVICE_NAME));
        noWsdlIllegalMethods.add(new Integer(GET_TYPE_MAPPING_REGISTRY));
        noWsdlIllegalMethods.add(new Integer(GET_WSDL_LOCATION));

        // This case shouldn't happen since if service-ref has generated
        // service and no WSDL it won't get past deployment, but it's here
        // for completeness.
        noWsdlIllegalMethods.add(new Integer(GENERATED_SERVICE_METHOD));
    }

    private void checkUnsupportedMethods(int methodType) 
        throws UnsupportedOperationException {

        Set illegalMethods = fullWsdl ?
            fullWsdlIllegalMethods : noWsdlIllegalMethods;

        if( illegalMethods.contains(new Integer(methodType)) ) {
            throw new UnsupportedOperationException();
        }

        return;
    }

    private void setStubProperties(Stub stub, int methodType, Method method, 
                                   Object[] args) {

        // Port info lookup will be based on SEI or port.
        QName port = null;
        String serviceEndpointInterface = null;

        switch(methodType) {
        case GET_PORT_CONTAINER_MANAGED :

            serviceEndpointInterface = ((Class) args[0]).getName();
            break;

        case GET_PORT_CLIENT_MANAGED :

            port = (QName) args[0];
            serviceEndpointInterface = ((Class) args[1]).getName();
            break;

        case GENERATED_SERVICE_METHOD :

            // java.rmi.Remote get<Name_of_wsdl:port>()
            String portLocalPart = method.getName().startsWith("get") ?
                method.getName().substring(3) : null;
            if( portLocalPart != null ) {
                QName serviceName = serviceRef.getServiceName();
                port = new QName(serviceName.getNamespaceURI(), portLocalPart);
            }
            serviceEndpointInterface = method.getReturnType().getName();

            break;

        default :
            return;
        }

        ServiceRefPortInfo portInfo = null;

        // If port is known, it takes precedence in lookup.
        if( port != null ) {
            portInfo = serviceRef.getPortInfoByPort(port);
        }
        if( portInfo == null ) {
            portInfo = serviceRef.getPortInfoBySEI(serviceEndpointInterface);
        }
            
        if( portInfo != null ) {
            Set properties = portInfo.getStubProperties();            

            for(Iterator iter = properties.iterator(); iter.hasNext();) {
                NameValuePairDescriptor next = (NameValuePairDescriptor) 
                    iter.next();
                if( next.getName().equals
                    (WsUtil.CLIENT_TRANSPORT_LOG_PROPERTY) ) {
                    // value is a URL
                    wsUtil.setClientTransportLog(serviceRef, stub, 
                                                 next.getValue());
                } else if(next.getName().equals(ServiceEngineUtil.JBI_ENABLED)){
                    setJBIProperties(stub, portInfo);
                } else {
                    stub._setProperty(next.getName(), next.getValue());
                }
            }

            // If this port has a resolved target endpoint address due to a
            // port-component-link, set it on stub.  However, if the runtime
            // info has an entry for target endpoint address, that takes 
            // precedence.
            if( portInfo.hasTargetEndpointAddress() ) {
                if(!portInfo.hasStubProperty(Stub.ENDPOINT_ADDRESS_PROPERTY)) {
                    stub._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY,
                                      portInfo.getTargetEndpointAddress());
                }
            }
        }
    }

    private void setCallProperties(Call[] calls, int methodType, Object[] args){

        Set callProperties = getPropertiesForCall(methodType, args);

        if( callProperties != null ) {
            for(int callIndex = 0; callIndex < calls.length; callIndex++) {
                setCallProperties(calls[callIndex], callProperties);
            }
        }
    }
        
    private Set getPropertiesForCall(int methodType, Object args[]) {

        Set callProperties = null;
        switch(methodType) {

        case CREATE_CALL_PORT :
        case CREATE_CALL_OPERATION_QNAME :
        case CREATE_CALL_OPERATION_STRING :
        case GET_CALLS :

                // Each of these methods has port as first argument.
                QName port = (QName) args[0];
                
                // Check if call properties are set at the port level.
                ServiceRefPortInfo portInfo = 
                    serviceRef.getPortInfoByPort(port);
                if( portInfo != null ) {
                    callProperties = portInfo.getCallProperties();
                }

                break;

        case CREATE_CALL_NO_ARGS :

            callProperties = serviceRef.getCallProperties();
            break;

        }
        
        return callProperties;
    }

    private void setCallProperties(Call call, Set callProperties) {
        for(Iterator iter = callProperties.iterator(); iter.hasNext();) {
            NameValuePairDescriptor next = (NameValuePairDescriptor) 
                iter.next();
            call.setProperty(next.getName(), next.getValue());
        }
    }

    private Object invokeJavaObjectMethod(InvocationHandler handler, 
                                          Method method, Object[] args) 
        throws Throwable {

        Object returnValue = null;

        // Can only be one of : 
        //     boolean java.lang.Object.equals(Object)
        //     int     java.lang.Object.hashCode()
        //     String  java.lang.Object.toString()
        //
        // Optimize by comparing as few characters as possible.

        switch( method.getName().charAt(0) ) {
            case 'e' :
                Object other = Proxy.isProxyClass(args[0].getClass()) ?
                    Proxy.getInvocationHandler(args[0]) : args[0];
                returnValue = new Boolean(handler.equals(other));
                break;
            case 'h' :
                returnValue = new Integer(handler.hashCode());
                break;
            case 't' :
                returnValue = handler.toString();
                break;
            default :
                throw new Throwable("Object method " + method.getName() + 
                                    "not found");
        }

        return returnValue;
    }

    private void setJBIProperties(Object stubOrCall, ServiceRefPortInfo portInfo) {
        // Check if the target service is a JBI service, and get its QName
        QName svcQName = serviceRef.getServiceName();
        if ( svcQName == null )
            return;
                                                                                
        if ( stubOrCall instanceof Stub ) {
            com.sun.xml.rpc.spi.runtime.StubBase stub =
                    (com.sun.xml.rpc.spi.runtime.StubBase)stubOrCall;
                                                                                
            try {
                // This statement is getting executed only
                //because jbi-enabled property on the stub is set to true
                ServiceEngineUtil.setJBITransportFactory(portInfo, stub, true);
            } catch(Throwable e) {
                // Do nothing
                //logger.severe("Exception raised while setting transport " +
                //      "factory to NMR : " + e.getMessage());
            }
            return;
        }
    }
}
