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

// $Id: CallImpl.java 13515 2010-12-16 20:04:23Z bmaxwell $

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.ServiceException;
import javax.xml.rpc.Stub;
import javax.xml.rpc.encoding.SerializerFactory;
import javax.xml.rpc.soap.SOAPFaultException;
import javax.xml.soap.SOAPException;

import org.jboss.logging.Logger;
import org.jboss.ws.Constants;
import org.jboss.ws.core.CommonBindingProvider;
import org.jboss.ws.core.CommonClient;
import org.jboss.ws.core.CommonMessageContext;
import org.jboss.ws.core.EndpointInvocation;
import org.jboss.ws.core.RoleSource;
import org.jboss.ws.core.WSTimeoutException;
import org.jboss.ws.core.binding.TypeMappingImpl;
import org.jboss.ws.core.jaxrpc.SchemaGenerator;
import org.jboss.ws.core.jaxrpc.UnqualifiedCallParameter;
import org.jboss.ws.core.jaxrpc.binding.JBossXBDeserializerFactory;
import org.jboss.ws.core.jaxrpc.binding.JBossXBSerializerFactory;
import org.jboss.ws.core.jaxrpc.handler.HandlerChainBaseImpl;
import org.jboss.ws.core.jaxrpc.handler.MessageContextJAXRPC;
import org.jboss.ws.core.jaxrpc.handler.SOAPMessageContextJAXRPC;
import org.jboss.ws.core.soap.MessageContextAssociation;
import org.jboss.ws.extensions.xop.XOPContext;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.ws.metadata.umdm.ParameterMetaData;
import org.jboss.ws.metadata.umdm.ServiceMetaData;
import org.jboss.ws.metadata.umdm.TypesMetaData;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSModel;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData.HandlerType;
import org.jboss.wsf.common.JavaUtils;

import com.sun.corba.se.spi.orb.Operation;

/** Provides support for the dynamic invocation of a service endpoint.
 * The javax.xml.rpc.Service interface acts as a factory for the creation of Call instances.
 *
 * Once a Call instance is created, various setter and getter methods may be used to configure this Call instance.
 *
 * @author Thomas.Diesler@jboss.org
 * @since 10-Oct-2004
 */
public class CallImpl extends CommonClient implements Call, RoleSource
{
   // provide logging
   private static Logger log = Logger.getLogger(CallImpl.class);

   // The service that created this call
   private ServiceImpl jaxrpcService;
   // The port type name
   private QName portType;
   // A Map<String,Object> of Call properties
   private Map<String, Object> properties = new HashMap<String, Object>();
   // The EndpointInvocation for the call.
   private EndpointInvocation epInv;

   // The set of supported properties
   private static final Set<String> standardProperties = new HashSet<String>();
   static
   {
      standardProperties.add(Call.ENCODINGSTYLE_URI_PROPERTY);
      standardProperties.add(Call.OPERATION_STYLE_PROPERTY);
      standardProperties.add(Call.SESSION_MAINTAIN_PROPERTY);
      standardProperties.add(Call.SOAPACTION_URI_PROPERTY);
      standardProperties.add(Call.SOAPACTION_USE_PROPERTY);
      standardProperties.add(Call.USERNAME_PROPERTY);
      standardProperties.add(Call.PASSWORD_PROPERTY);

      standardProperties.add(Stub.ENDPOINT_ADDRESS_PROPERTY);
      standardProperties.add(Stub.SESSION_MAINTAIN_PROPERTY);
      standardProperties.add(Stub.USERNAME_PROPERTY);
      standardProperties.add(Stub.PASSWORD_PROPERTY);
   }

   /** Create a call that needs to be configured manually
    */
   CallImpl(ServiceImpl service)
   {
      super(service.getServiceMetaData());
      this.jaxrpcService = service;

      if (epMetaData != null)
         setTargetEndpointAddress(epMetaData.getEndpointAddress());
   }

   /** Create a call for a known WSDL endpoint.
    *
    * @param epMetaData A WSDLEndpoint
    */
   CallImpl(ServiceImpl service, EndpointMetaData epMetaData)
   {
      super(epMetaData);
      this.jaxrpcService = service;

      setTargetEndpointAddress(epMetaData.getEndpointAddress());
   }

   /** Create a call for a known WSDL endpoint.
    *
    * @param portName Qualified name for the target service endpoint
    * @throws ServiceException
    */
   CallImpl(ServiceImpl service, QName portName, QName opName) throws ServiceException
   {
      super(service.getServiceMetaData(), portName, opName);
      this.jaxrpcService = service;

      if (epMetaData != null)
         setTargetEndpointAddress(epMetaData.getEndpointAddress());
   }

   public ServiceImpl getServiceImpl()
   {
      return jaxrpcService;
   }

   @Override
   protected Map<String, Object> getRequestContext()
   {
      return properties;
   }
   
   @Override
   protected EndpointInvocation createEndpointInvocation(OperationMetaData opMetaData)
   {
      epInv = super.createEndpointInvocation(opMetaData);

      return epInv;
   }

   /** Gets the address of a target service endpoint.
    */
   public String getTargetEndpointAddress()
   {
      return (String)properties.get(Stub.ENDPOINT_ADDRESS_PROPERTY);
   }

   /** Sets the address of the target service endpoint. This address must correspond to the transport
    * specified in the binding for this Call instance.
    *
    * @param address Address of the target service endpoint; specified as an URI
    */
   public void setTargetEndpointAddress(String address)
   {
      this.properties.put(Stub.ENDPOINT_ADDRESS_PROPERTY, address);
   }

   /** Adds a parameter type and mode for a specific operation.
    */
   public void addParameter(String paramName, QName xmlType, ParameterMode parameterMode)
   {
      TypeMappingImpl typeMapping = getEndpointMetaData().getServiceMetaData().getTypeMapping();
      Class javaType = typeMapping.getJavaType(xmlType);

      // CTS com/sun/ts/tests/jaxrpc/api/javax_xml_rpc/Call/AddGetRemoveAllParametersTest1
      // tests addParameter/getParameter without giving the javaType for a custom parameter
      // IMHO, this flavour of addParameter should only be used for standard types, where
      // the javaType can be derived from the xmlType
      if (javaType == null)
      {
         log.warn("Register unqualified call parameter for: " + xmlType);
         javaType = new UnqualifiedCallParameter(xmlType).getClass();
         typeMapping.register(javaType, xmlType, null, null);
      }

      addParameter(paramName, xmlType, javaType, parameterMode);
   }

   /** Adds a parameter type and mode for a specific operation.
    */
   public void addParameter(String paramName, QName xmlType, Class javaType, ParameterMode mode)
   {
      QName xmlName = new QName(paramName);
      addParameter(xmlName, xmlType, javaType, mode, false);
   }

   /** Add a parameter to the current operation description.
    * This is a propriatary extension that gives full control over the parameter configuration.
    */
   public void addParameter(QName xmlName, QName xmlType, Class javaType, ParameterMode mode, boolean inHeader)
   {
      if (xmlType == null || javaType == null)
         throw new IllegalArgumentException("Invalid null parameter");

      OperationMetaData opMetaData = getOperationMetaData();
      ParameterMetaData paramMetaData = new ParameterMetaData(opMetaData, xmlName, xmlType, javaType.getName());
      paramMetaData.setMode(mode);
      paramMetaData.setInHeader(inHeader);
      paramMetaData.setIndex(opMetaData.getParameters().size());
      opMetaData.addParameter(paramMetaData);

      registerParameterType(xmlType, javaType);
   }

   /** Removes all specified parameters from this Call instance. Note that this method removes only the parameters and
    * not the return type. The setReturnType(null) is used to remove the return type.
    *
    * @throws javax.xml.rpc.JAXRPCException This exception may be thrown If this method is called when the method isParameterAndReturnSpecRequired returns false for this Call's operation.
    */
   public void removeAllParameters()
   {
      OperationMetaData opMetaData = getOperationMetaData();
      opMetaData.removeAllParameters();
   }

   /** Sets the return type for a specific operation. Invoking setReturnType(null) removes the return type for this Call object.
    */
   public void setReturnType(QName xmlType)
   {
      Class javaType = getEndpointMetaData().getServiceMetaData().getTypeMapping().getJavaType(xmlType);
      setReturnType(xmlType, javaType);
   }

   /** Sets the return type for a specific operation.
    */
   public void setReturnType(QName xmlType, Class javaType)
   {
      if (xmlType == null || javaType == null)
         throw new IllegalArgumentException("Invalid null parameter");

      OperationMetaData opMetaData = getOperationMetaData();
      QName xmlName = new QName(Constants.DEFAULT_RPC_RETURN_NAME);
      String javaTypeName = javaType.getName();
      ParameterMetaData retMetaData = new ParameterMetaData(opMetaData, xmlName, xmlType, javaTypeName);
      opMetaData.setReturnParameter(retMetaData);

      registerParameterType(xmlType, javaType);
   }

   /** Invokes a remote method using the one-way interaction mode.
    */
   public void invokeOneWay(Object[] inputParams)
   {
      try
      {
         invokeInternal(operationName, inputParams, true);
      }
      catch (RemoteException ex)
      {
         throw new JAXRPCException(ex);
      }
   }

   /** Invokes a specific operation using a synchronous request-response interaction mode.
    */
   public Object invoke(Object[] inputParams) throws RemoteException
   {
      return invokeInternal(operationName, inputParams, false);
   }

   /** Invokes a specific operation using a synchronous request-response interaction mode.
    */
   public Object invoke(QName operationName, Object[] inputParams) throws RemoteException
   {
      return invokeInternal(operationName, inputParams, false);
   }

   protected CommonMessageContext processPivot(CommonMessageContext requestContext)
   {
      return MessageContextJAXRPC.processPivot(requestContext);
   }

   /** Returns a List values for the output parameters of the last invoked operation.
    *
    * @return java.util.List Values for the output parameters. An empty List is returned if there are no output values.
    * @throws JAXRPCException If this method is invoked for a one-way operation or is invoked before any invoke method has been called.
    */
   public List getOutputValues()
   {
      if (epInv == null)
         throw new JAXRPCException("Output params not available");

      try
      {
         OperationMetaData opMetaData = getOperationMetaData();

         List<Object> objPayload = new ArrayList<Object>();
         for (QName xmlName : epInv.getResponseParamNames())
         {
            Object paramValue = epInv.getResponseParamValue(xmlName);
            if (opMetaData.isDocumentWrapped())
            {
               objPayload = Arrays.asList((Object[])paramValue);
               break;
            }
            else
            {
               objPayload.add(paramValue);
            }
         }
         return objPayload;
      }
      catch (SOAPException ex)
      {
         throw new JAXRPCException("Cannot obtain response payload", ex);
      }
   }

   /** Returns a Map of {name, value} for the output parameters of the last invoked operation.
    *  The parameter names in the returned Map are of type java.lang.String.
    *
    * @return Map Output parameters for the last Call.invoke(). Empty Map is returned if there are no output parameters.
    * @throws JAXRPCException If this method is invoked for a one-way operation or is invoked before any invoke method has been called.
    */
   public Map getOutputParams()
   {
      if (epInv == null)
         throw new JAXRPCException("Output params not available");

      try
      {
         Map<String, Object> outMap = new LinkedHashMap<String, Object>();
         for (QName xmlName : epInv.getResponseParamNames())
         {
            Object value = epInv.getResponseParamValue(xmlName);
            outMap.put(xmlName.getLocalPart(), value);
         }
         return outMap;
      }
      catch (SOAPException ex)
      {
         throw new JAXRPCException("Cannot obtain response payload", ex);
      }
   }

   /**
    * Gets the qualified name of the port type.
    *
    * @return Qualified name of the port type
    */
   public QName getPortTypeName()
   {
      if (portType != null)
      {
         return portType;
      }

      /* This code could be used to derive the portType from the endpoint meta data.
       * However, it breaks CTS com/sun/ts/tests/jaxrpc/api/javax_xml_rpc/Call/Client.java#SetGetPortTypeNameTest2
       if (epMetaData != null)
       {
       ServiceMetaData serviceMetaData = epMetaData.getServiceMetaData();
       WSDLDefinitions wsdlDefinitions = serviceMetaData.getWsdlDefinitions();
       WSDLService wsdlService = wsdlDefinitions.getService(new NCName(serviceMetaData.getName().getLocalPart()));
       WSDLEndpoint wsdlEndpoint = wsdlService.getEndpoint(epMetaData.getName());
       WSDLInterface wsdlInterface = wsdlEndpoint.getInterface();
       return wsdlInterface.getQName();
       }
       */

      // CTS com/sun/ts/tests/jaxrpc/api/javax_xml_rpc/Call/Client.java#SetGetPortTypeNameTest2
      return new QName("");
   }

   /**
    * Gets the return type for a specific operation
    *
    * @return Returns the XML type for the return value
    */
   public QName getReturnType()
   {
      QName retType = null;
      if (operationName != null)
      {
         OperationMetaData opDesc = getOperationMetaData();
         ParameterMetaData retMetaData = opDesc.getReturnParameter();
         if (retMetaData != null)
            retType = retMetaData.getXmlType();
      }
      return retType;
   }

   /**
    * Sets the qualified name of the interface.
    *
    * @param portType - Qualified name of the port type
    */
   public void setPortTypeName(QName portType)
   {
      this.portType = portType;
   }

   /**
    * Indicates whether addParameter and setReturnType methods are to be invoked to specify the parameter and return
    * type specification for a specific operation.
    *
    * @param opName Qualified name of the operation
    * @return Returns true if the Call implementation class requires addParameter and setReturnType to be invoked in the client code for the specified operation. This method returns false otherwise.
    * @throws IllegalArgumentException If invalid operation name is specified
    */
   public boolean isParameterAndReturnSpecRequired(QName opName)
   {
      setOperationName(opName);
      OperationMetaData opMetaData = getOperationMetaData();
      return opMetaData.getParameters().size() == 0 && opMetaData.getReturnParameter() == null;
   }

   /** Gets the names of configurable properties supported by this Call object.
    * @return Iterator for the property names
    */
   public Iterator getPropertyNames()
   {
      return standardProperties.iterator();
   }

   /** Gets the value of a named property.
    */
   public Object getProperty(String name)
   {
      if (null == name)
         throw new JAXRPCException("Unsupported property: " + name);
      // CTS: com/sun/ts/tests/jaxrpc/api/javax_xml_rpc/Call/Client.java#SetGetPropertyTest2
      if (name.startsWith("javax.xml.rpc") && standardProperties.contains(name) == false)
         throw new JAXRPCException("Unsupported property: " + name);

      return properties.get(name);
   }

   /** Sets the value for a named property.
    */
   public void setProperty(String name, Object value)
   {
      if (null == name)
         throw new JAXRPCException("Unsupported property: " + name);

      // CTS: com/sun/ts/tests/jaxrpc/api/javax_xml_rpc/Call/Client.java#SetGetPropertyTest2
      if (name.startsWith("javax.xml.rpc") && standardProperties.contains(name) == false)
         throw new JAXRPCException("Unsupported property: " + name);

      if (log.isDebugEnabled())
         log.debug("setProperty: [name=" + name + ",value=" + value + "]");
      properties.put(name, value);
   }

   /** Removes a named property.
    */
   public void removeProperty(String name)
   {
      properties.remove(name);
   }

   /** Gets the XML type of a parameter by name.
    */
   public QName getParameterTypeByName(String paramName)
   {
      OperationMetaData opMetaData = getOperationMetaData();
      ParameterMetaData paramMetaData = opMetaData.getParameter(new QName(paramName));
      if (paramMetaData != null)
         return paramMetaData.getXmlType();
      else return null;
   }

   protected CommonBindingProvider getCommonBindingProvider()
   {
      if (bindingProvider == null)
      {
         bindingProvider = new CommonBindingProvider(getEndpointMetaData());
      }
      return bindingProvider;
   }

   @Override
   protected void setInboundContextProperties()
   {
   }

   @Override
   protected void setOutboundContextProperties()
   {
   }

   private Object invokeInternal(QName opName, Object[] inputParams, boolean forceOneway) throws RemoteException
   {
      if (opName.equals(operationName) == false)
         setOperationName(opName);

      OperationMetaData opMetaData = getOperationMetaData();

      // Check or generate the the schema if this call is unconfigured
      generateOrUpdateSchemas(opMetaData);

      // Associate a message context with the current thread
      SOAPMessageContextJAXRPC msgContext = new SOAPMessageContextJAXRPC();
      MessageContextAssociation.pushMessageContext(msgContext);
      Object retObj = null;
      try
      {
         retObj = super.invoke(opName, inputParams, forceOneway);
         return retObj;
      }
      catch (SOAPFaultException ex)
      {
         log.error("Call invocation failed", ex);
         String faultCode = ex.getFaultCode().getLocalPart();
         throw new RemoteException("Call invocation failed with code [" + faultCode + "] because of: " + ex.getFaultString(), ex);
      }
      catch (RemoteException rex)
      {
         throw rex;
      }
      catch (WSTimeoutException toex)
      {
         throw toex;
      }
      catch (Exception ex)
      {
         throw new RemoteException("Call invocation failed", ex);
      }
      finally
      {
         // Reset the message context association
         MessageContextAssociation.popMessageContext();
      }
   }

   @Override
   protected boolean callRequestHandlerChain(QName portName, HandlerType type)
   {
      SOAPMessageContextJAXRPC msgContext = (SOAPMessageContextJAXRPC)MessageContextAssociation.peekMessageContext();
      HandlerChainBaseImpl handlerChain = (HandlerChainBaseImpl)jaxrpcService.getHandlerChain(portName);
      return (handlerChain != null ? handlerChain.handleRequest(msgContext, type) : true);
   }

   @Override
   protected boolean callResponseHandlerChain(QName portName, HandlerType type)
   {
      SOAPMessageContextJAXRPC msgContext = (SOAPMessageContextJAXRPC)MessageContextAssociation.peekMessageContext();
      HandlerChainBaseImpl handlerChain = (HandlerChainBaseImpl)jaxrpcService.getHandlerChain(portName);

      boolean status = true;
      if (handlerChain != null)
      {
         status = handlerChain.handleResponse(msgContext, type);
      }
      return status;
   }

   @Override
   protected boolean callFaultHandlerChain(QName portName, HandlerType type, Exception ex)
   {
      SOAPMessageContextJAXRPC msgContext = (SOAPMessageContextJAXRPC)MessageContextAssociation.peekMessageContext();
      HandlerChainBaseImpl handlerChain = (HandlerChainBaseImpl)jaxrpcService.getHandlerChain(portName);

      boolean status = true;
      if (handlerChain != null)
      {
         status = handlerChain.handleFault(msgContext, type);
      }
      return status;
   }

   @Override
   protected void closeHandlerChain(QName portName, HandlerType type)
   {
      // nothing to do for JAXRPC
   }

   /** Generate or update the XSD schema for all parameters and the return.
    *  This should only be done when the Call is unconfigured, hence there is no WSDL
    */
   private void generateOrUpdateSchemas(OperationMetaData opMetaData)
   {
      ServiceMetaData serviceMetaData = opMetaData.getEndpointMetaData().getServiceMetaData();
      if (serviceMetaData.getWsdlLocation() == null)
      {
         TypesMetaData typesMetaData = serviceMetaData.getTypesMetaData();
         for (ParameterMetaData paramMetaData : opMetaData.getParameters())
         {
            generateOrUpdateParameterSchema(typesMetaData, paramMetaData);
         }

         ParameterMetaData retMetaData = opMetaData.getReturnParameter();
         if (retMetaData != null)
         {
            generateOrUpdateParameterSchema(typesMetaData, retMetaData);
         }
      }
   }

   /** Generate or update the XSD schema for a given parameter
    *  This should only be done if the parameter is not an attachment
    */
   private void generateOrUpdateParameterSchema(TypesMetaData typesMetaData, ParameterMetaData paramMetaData)
   {
      if (paramMetaData.isSwA() == false)
      {
         QName xmlType = paramMetaData.getXmlType();
         Class javaType = paramMetaData.getJavaType();

         ServiceMetaData serviceMetaData = getEndpointMetaData().getServiceMetaData();
         TypeMappingImpl typeMapping = serviceMetaData.getTypeMapping();
         SerializerFactory serFactory = typeMapping.getSerializer(javaType, xmlType);
         if (serFactory instanceof JBossXBSerializerFactory)
         {
            SchemaGenerator xsdGenerator = new SchemaGenerator();
            JBossXSModel model = xsdGenerator.generateXSDSchema(xmlType, javaType);
            typesMetaData.addSchemaModel(model);
         }
      }
   }

   private void registerParameterType(QName xmlType, Class javaType)
   {
      ServiceMetaData serviceMetaData = getEndpointMetaData().getServiceMetaData();

      String nsURI = xmlType.getNamespaceURI();
      if (Constants.NS_ATTACHMENT_MIME_TYPE.equals(nsURI) == false)
      {
         TypeMappingImpl typeMapping = serviceMetaData.getTypeMapping();
         Class regJavaType = typeMapping.getJavaType(xmlType);
         if (regJavaType == null)
         {
            typeMapping.register(javaType, xmlType, new JBossXBSerializerFactory(), new JBossXBDeserializerFactory());
         }
         else if (regJavaType != null && JavaUtils.isAssignableFrom(regJavaType, javaType) == false)
         {
            throw new IllegalArgumentException("Different java type already registered: " + regJavaType.getName());
         }
      }
   }

   @Override
   public void setConfigName(String configName, String configFile)
   {
      EndpointMetaData epMetaData = getEndpointMetaData();
      epMetaData.setConfigName(configName, configFile);

      // Reinitialize the client handler chain
      jaxrpcService.setupHandlerChain(epMetaData);
   }

   public Set<QName> getHeaders()
   {
      HandlerChainBaseImpl handlerChain = (HandlerChainBaseImpl)jaxrpcService.getHandlerChain(epMetaData.getPortName());

      return (handlerChain != null) ? handlerChain.getHeaders() : new HashSet<QName>();
   }

   public Set<String> getRoles()
   {
      HandlerChainBaseImpl handlerChain = (HandlerChainBaseImpl)jaxrpcService.getHandlerChain(epMetaData.getPortName());

      Set<String> set = new HashSet<String>();
      String[] roles = handlerChain.getRoles();
      if (roles != null)
         Collections.addAll(set, roles);

      return set;
   }

   @Override
   protected boolean shouldMaintainSession()
   {
      Object bool = getRequestContext().get(Stub.SESSION_MAINTAIN_PROPERTY);
      return Boolean.TRUE.equals(bool);
   }
}