/*
* The Apache Software License, Version 1.1
*
*
* Copyright (c) 2001-2003 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.jboss.axis.providers.java;

import org.jboss.axis.AxisFault;
import org.jboss.axis.Constants;
import org.jboss.axis.Message;
import org.jboss.axis.MessageContext;
import org.jboss.axis.MessagePart;
import org.jboss.axis.description.OperationDesc;
import org.jboss.axis.description.ParameterDesc;
import org.jboss.axis.description.ServiceDesc;
import org.jboss.axis.enums.Style;
import org.jboss.axis.enums.Use;
import org.jboss.axis.handlers.soap.SOAPService;
import org.jboss.axis.message.RPCElement;
import org.jboss.axis.message.RPCHeaderParam;
import org.jboss.axis.message.RPCParam;
import org.jboss.axis.message.RPCParamElementImpl;
import org.jboss.axis.message.SOAPBodyElementAxisImpl;
import org.jboss.axis.message.SOAPElementAxisImpl;
import org.jboss.axis.message.SOAPEnvelopeAxisImpl;
import org.jboss.axis.soap.SOAPConstants;
import org.jboss.axis.utils.JavaUtils;
import org.jboss.axis.utils.Messages;
import org.jboss.logging.Logger;
import org.w3c.dom.Element;

import javax.activation.DataHandler;
import javax.xml.namespace.QName;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.holders.Holder;
import javax.xml.soap.AttachmentPart;
import javax.xml.soap.SOAPHeaderElement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

/**
 * Implement message processing by walking over RPCElements of the
 * envelope body, invoking the appropriate methods on the service object.
 *
 * @author Doug Davis (dug@us.ibm.com)
 */
public class RPCInvocation
{

   private static Logger log = Logger.getLogger(RPCInvocation.class.getName());

   private RPCProvider rpcProvider;
   private MessageContext messageContext;
   private SOAPEnvelopeAxisImpl requestEnvelope;
   private SOAPEnvelopeAxisImpl responseEnvelope;
   private Object targetObject;
   private OperationDesc operation;
   private Object[] argValues;
   private Object returnValue;
   private RPCElement body;
   private List outParams;

   // A flag that indicates thaat the response envelope has already been prepared
   private boolean responsePrepared;

   public RPCInvocation(RPCProvider rpcProvider, MessageContext messageContext, SOAPEnvelopeAxisImpl reqEnv, SOAPEnvelopeAxisImpl resEnv, Object targetObject)
   {
      this.rpcProvider = rpcProvider;
      this.messageContext = messageContext;
      this.requestEnvelope = reqEnv;
      this.responseEnvelope = resEnv;
      this.targetObject = targetObject;
   }

   /**
    * Copy constructor
    */
   public RPCInvocation(RPCInvocation invocation)
   {
      this.rpcProvider = invocation.rpcProvider;
      this.messageContext = invocation.messageContext;
      this.requestEnvelope = invocation.requestEnvelope;
      this.responseEnvelope = invocation.responseEnvelope;
      this.targetObject = invocation.targetObject;
      this.responsePrepared = invocation.responsePrepared;
   }

   public RPCProvider getProvider()
   {
      return rpcProvider;
   }

   /**
    * Prepare the invocation object from the given prameters
    */
   public void prepareFromRequestEnvelope()
   {

      log.debug("Enter: prepareFromRequestEnvelope\n" + requestEnvelope);

      SOAPService service = messageContext.getService();
      ServiceDesc serviceDesc = service.getServiceDescription();
      operation = messageContext.getOperation();

      try
      {
         Vector bodies = requestEnvelope.getBodyElements();
         if (log.isDebugEnabled())
         {
            log.debug(Messages.getMessage("bodyElems00", "" + bodies.size()));
            log.debug(Messages.getMessage("bodyIs00", "" + bodies.get(0)));
         }

         // Find the first "root" body element, which is the RPC call.
         for (int bNum = 0; body == null && bNum < bodies.size(); bNum++)
         {
            // If this is a regular old SOAPBodyElement, and it's a root,
            // we're probably a non-wrapped doc/lit service.  In this case,
            // we deserialize the element, and create an RPCElement "wrapper"
            // around it which points to the correct method.
            // FIXME : There should be a cleaner way to do this...
            if (!(bodies.get(bNum) instanceof RPCElement))
            {
               SOAPBodyElementAxisImpl bodyEl = (SOAPBodyElementAxisImpl)bodies.get(bNum);
               // igors: better check if bodyEl.getID() != null
               // to make sure this loop does not step on SOAP-ENC objects
               // that follow the parameters! FIXME?
               if (bodyEl.isRoot() && operation != null && bodyEl.getID() == null)
               {
                  ParameterDesc param = operation.getParameter(bNum);
                  // at least do not step on non-existent parameters!
                  if (param != null)
                  {
                     Object val = bodyEl.getValueAsType(param.getTypeQName());
                     body = new RPCElement("",
                             operation.getName(),
                             new Object[]{val});
                  }
               }
            }
            else
            {
               body = (RPCElement)bodies.get(bNum);
            }
         }

         // special case code for a document style operation with no
         // arguments (which is a strange thing to have, but whatever)
         if (body == null)
         {
            // throw an error if this isn't a document style service
            if (!(serviceDesc.getStyle().equals(Style.DOCUMENT)))
            {
               throw new JAXRPCException(Messages.getMessage("noBody00"));
            }

            // look for a method in the service that has no arguments,
            // use the first one we find.
            ArrayList ops = serviceDesc.getOperations();
            for (Iterator iterator = ops.iterator(); iterator.hasNext();)
            {
               OperationDesc desc = (OperationDesc)iterator.next();
               if (desc.getNumInParams() == 0)
               {
                  // found one with no parameters, use it
                  messageContext.setOperation(desc);
                  // create an empty element
                  body = new RPCElement(desc.getName());
                  // stop looking
                  break;
               }
            }

            // If we still didn't find anything, report no body error.
            if (body == null)
            {
               throw new JAXRPCException(Messages.getMessage("noBody00"));
            }
         }

         String methodName = body.getMethodName();
         Vector args = body.getParams();
         int numArgs = args.size();

         // This may have changed, so get it again...
         // FIXME (there should be a cleaner way to do this)
         operation = messageContext.getOperation();

         if (operation == null)
         {
            QName qname = new QName(body.getNamespaceURI(),
                    body.getName());
            operation = getOperation(serviceDesc, qname);
         }

         if (operation == null)
         {
            // BP R2725
            // As specified by the SOAP processing model, (a) a "VersionMismatch" faultcode must be generated if the
            // namespace of the "Envelope" element is incorrect, (b) a "MustUnderstand" fault must be generated if the
            // instance does not understand a SOAP header block with a value of "1" for the soap:mustUnderstand attribute.
            // In all other cases where a message is inconsistent with its WSDL description, a fault with a "Client"
            // faultcode should be generated.
            // [TDI] 17-Aug-2004 com/sun/ts/tests/jaxrpc/wsi/j2w/rpc/literal/R2725
            Vector headers = requestEnvelope.getHeaders();
            for (int i = 0; i < headers.size(); i++)
            {
               SOAPHeaderElement header = (SOAPHeaderElement)headers.elementAt(i);
               if (header.getMustUnderstand() == true)
               {
                  AxisFault fault = new AxisFault(Messages.getMessage("noSuchOperation", methodName));
                  fault.setFaultCode(Constants.FAULT_MUSTUNDERSTAND);

                  // This hack wrapps the checked AxisException in a RuntimeException
                  // so we don't loose the fault code, etc
                  // [TDI] 17-Aug-2004
                  throw new JAXRPCException(fault);
               }
            }

            throw new JAXRPCException(Messages.getMessage("noSuchOperation", methodName));
         }

         // Create the array we'll use to hold the actual parameter
         // values.  We know how big to make it from the metadata.
         argValues = new Object[operation.getNumParams()];

         // A place to keep track of the out params (INOUTs and OUTs)
         outParams = new ArrayList();

         // Check if we have and endpoint of type doStuff(org.w3c.dom.Element)
         // In that case we pass the entire SOAPBodyElement to the endpoint
         boolean isGenericDocumentEndpoint = false;
         Style style = serviceDesc.getStyle();
         Use use = serviceDesc.getUse();
         if (style.equals(Style.DOCUMENT) && use.equals(Use.LITERAL) && operation.getInParams().size() == 1)
         {
            ParameterDesc firstInParam = (ParameterDesc)operation.getInParams().get(0);
            if (Element.class.isAssignableFrom(firstInParam.getJavaType()))
            {
               isGenericDocumentEndpoint = true;
               argValues[0] = body;
            }
         }

         // Put the values contained in the RPCParams into an array
         // suitable for passing to java.lang.reflect.Method.invoke()
         // Make sure we respect parameter ordering if we know about it
         // from metadata, and handle whatever conversions are necessary
         // (values -> Holders, etc)
         if (isGenericDocumentEndpoint == false)
         {
            for (int i = 0; i < numArgs; i++)
            {
               RPCParam rpcParam = (RPCParam)args.get(i);
               Object value = rpcParam.getValue();

               // first check the type on the paramter
               ParameterDesc paramDesc = rpcParam.getParamDesc();

               // if we found some type info try to make sure the value type is
               // correct.  For instance, if we deserialized a xsd:dateTime in
               // to a Calendar and the service takes a Date, we need to convert
               if (paramDesc != null && paramDesc.getJavaType() != null)
               {

                  // Get the type in the signature (java type or its holder)
                  Class sigType = paramDesc.getJavaType();

                  // Get the content of an attachement part
                  if (value instanceof AttachmentPart)
                  {
                     if (sigType.equals(DataHandler.class))
                        value = ((AttachmentPart)value).getDataHandler();
                     else
                        value = ((AttachmentPart)value).getContent();
                  }

                  // Convert the value into the expected type in the signature
                  value = JavaUtils.convert(value, sigType);

                  rpcParam.setValue(value);
                  if (paramDesc.getMode() == ParameterDesc.INOUT)
                  {
                     outParams.add(rpcParam);
                  }
               }

               // Put the value (possibly converted) in the argument array
               // make sure to use the parameter order if we have it
               if (paramDesc == null || paramDesc.getOrder() == -1)
               {
                  argValues[i] = value;
               }
               else
               {
                  argValues[paramDesc.getOrder()] = value;
               }

               if (log.isDebugEnabled())
               {
                  log.debug("  " + Messages.getMessage("value00", "" + argValues[i]));
               }
            }
         }

         // See if any subclasses want a crack at faulting on a bad operation
         // FIXME : Does this make sense here???
         String allowedMethods = (String)service.getOption("allowedMethods");
         rpcProvider.checkMethodName(messageContext, allowedMethods, operation.getName());

         // Now create any out holders we need to pass in
         int count = numArgs;
         for (int i = 0; i < argValues.length; i++)
         {

            // We are interested only in OUT/INOUT
            ParameterDesc param = operation.getParameter(i);
            if (param.getMode() == ParameterDesc.IN)
               continue;

            Class holderClass = param.getJavaType();
            if (holderClass != null &&
                    Holder.class.isAssignableFrom(holderClass))
            {
               int index = count;
               if (param.getMode() == ParameterDesc.OUT)
               {
                  // OUT params don't have param order, just stick them at the end.
                  count++;
               }
               else if (param.getMode() == ParameterDesc.INOUT)
               {
                  // Use the parameter order if specified or just stick them to the end.
                  if (param.getOrder() != -1)
                  {
                     index = param.getOrder();
                  }
                  else
                  {
                     count++;
                  }
                  // If it's already filled, don't muck with it
                  if (argValues[index] != null)
                  {
                     continue;
                  }
               }
               argValues[index] = holderClass.newInstance();
               // Store an RPCParam in the outs collection so we
               // have an easy and consistent way to write these
               // back to the client below
               RPCParam p = new RPCParam(param.getQName(),
                       argValues[index]);
               p.setParamDesc(param);
               outParams.add(p);
            }
            else
            {
               String message = Messages.getMessage("badOutParameter00", "" + param.getQName(), operation.getName());
               log.error(message);
               throw new JAXRPCException(message);
            }
         }

         log.debug("Exit: prepareFromRequestEnvelope");
      }
      catch (JAXRPCException e)
      {
         log.error(e.toString(), e);
         throw e;
      }
      catch (Exception e)
      {
         log.error(e.toString(), e);
         throw new JAXRPCException(e);
      }
   }

   public OperationDesc getOperation(ServiceDesc serviceDesc, QName qname)
   {
      return serviceDesc.getOperationByElementQName(qname);
   }

   /**
    * Prepare the response envelope from the given parameters
    */
   public void prepareResponseEnvelope(Object resObject)
   {

      returnValue = resObject;
      log.debug("Enter: prepareResponseEnvelope: [resObject=" + resObject + "]");


      // There is no way we can get a new resObj from the response SOAPEnvelope.
      // Sucker is the response handler that modifies the response value.
      // The RPCInvocation will ignore another call to prepareResponseEnvelope
      // because this could potentially overwrite what the handlers did.
      if (responsePrepared)
      {
         log.debug("Ignoring request to prepare response envelope");
         log.debug("Exit: prepareResponseEnvelope\n" + responseEnvelope);
         return;
      }

      SOAPService service = messageContext.getService();
      ServiceDesc serviceDesc = service.getServiceDescription();

      ArrayList immutableElements = new ArrayList();

      try
      {
         // Set the encoding style - in contradiction to BP-1.0/R1005
         // s1as interop tests expect this to be set in the <env:Body>
         // [TDI 11-Sep-2004]
         String encodingStyle = messageContext.getEncodingStyle();
         String bodyEncStyle = responseEnvelope.getBody().getEncodingStyle();
         if (bodyEncStyle.equals(encodingStyle) == false)
         {
            if (bodyEncStyle.equals(""))
               responseEnvelope.getBody().setEncodingStyle(encodingStyle);
            else
               log.warn("Mixed encoding styles are not supported: " + bodyEncStyle + "!=" + encodingStyle);
         }

         RPCElement resBody = null;

         // Check if we have and endpoint of type org.w3c.dom.Element doStuff(...)
         // In that case the resObject is the SOAPBodyElement
         boolean isGenericDocumentReturn = false;
         Style style = serviceDesc.getStyle();
         Use use = serviceDesc.getUse();
         if (style.equals(Style.DOCUMENT) && use.equals(Use.LITERAL) && resObject != null && Element.class.isAssignableFrom(resObject.getClass()))
         {
            Element resElement = (Element)resObject;
            resBody = new RPCElement(resElement.getLocalName());
            resBody.setPrefix(resElement.getPrefix());
            resBody.setNamespaceURI(resElement.getNamespaceURI());
            immutableElements.add(resBody);
            isGenericDocumentReturn = true;
         }

         /* Now put the result in the result SOAPEnvelope */
         /*************************************************/
         if (isGenericDocumentReturn == false)
         {
            String methodName = operation.getMethod().getName();
            resBody = new RPCElement(methodName + "Response");
            resBody.setPrefix(body.getPrefix());
            resBody.setNamespaceURI(body.getNamespaceURI());
            immutableElements.add(resBody);
         }

         // Return first
         if (operation.getMethod().getReturnType() != Void.TYPE)
         {
            QName returnQName = operation.getReturnQName();
            if (returnQName == null)
            {
               String methodName = operation.getMethod().getName();
               returnQName = new QName(body.getNamespaceURI(), methodName + "Response");
            }

            ParameterDesc retParamDesc = operation.getReturnParamDesc();
            QName retTypeQName = retParamDesc.getTypeQName();
            if (resObject != null && retTypeQName != null)
            {
               Class retType = messageContext.getTypeMapping().getClassForQName(retTypeQName);
               if (retType != null && JavaUtils.isConvertable(resObject, retType))
                  resObject = JavaUtils.convert(resObject, retType);
            }

            RPCParam param = new RPCParam(returnQName, resObject);
            param.setParamDesc(retParamDesc);
            param.setRPCCall(resBody);

            // Added to include the user type in the respone message
            // Otherwise the client does not use the BeanDeserializer
            // com/sun/ts/tests/webservices/wsdlImport/file/nested1#invokeNestedImportWsdl
            if (operation.getUse().equals(Use.LITERAL))
               param.setXSITypeGeneration(Boolean.TRUE);

            // Added to build the soap tree such that it contains SOAPElements for the parameters
            // Revisit to do this properly for the different encoding styles, arrays, value types, etc.
            // TDI 06-June-2006
            RPCParamElementImpl paramElement = new RPCParamElementImpl(param);
            resBody.addChildElement(paramElement);
            immutableElements.add(paramElement);

            if (!operation.isReturnHeader())
            {
               // For SOAP 1.2 rpc style, add a result
               if (messageContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS &&
                       (serviceDesc.getStyle().equals(Style.RPC)))
               {
                  RPCParam resultParam = new RPCParam(Constants.QNAME_RPC_RESULT, returnQName);
                  resultParam.setXSITypeGeneration(Boolean.FALSE);
                  resBody.addParam(resultParam);
               }
               resBody.addParam(param);
            }
            else
            {
               responseEnvelope.addHeader(new RPCHeaderParam(param));
            }

         }

         // Then any other out params
         if (!outParams.isEmpty())
         {
            for (Iterator i = outParams.iterator(); i.hasNext();)
            {
               // We know this has a holder, so just unwrap the value
               RPCParam param = (RPCParam)i.next();
               Object value = param.getValue();
               if (value instanceof Holder)
               {
                  Holder holder = (Holder)value;
                  value = JavaUtils.getHolderValue(holder);
               }
               ParameterDesc paramDesc = param.getParamDesc();

               param.setValue(value);
               if (paramDesc != null && paramDesc.isOutHeader())
               {
                  responseEnvelope.addHeader(new RPCHeaderParam(param));
               }
               else
               {
                  resBody.addParam(param);
               }
            }
         }

         responseEnvelope.addBodyElement(resBody);
         responsePrepared = true;

         // Make the added elements immutable
         Iterator it = immutableElements.iterator();
         while (it.hasNext())
         {
            SOAPElementAxisImpl soapElement = (SOAPElementAxisImpl)it.next();
            soapElement.setImmutable(true);
         }

         // Put the response envelope in the SOAPPart
         messageContext.setPastPivot(true);
         Message respMessage = (Message)messageContext.getMessage();
         ((MessagePart)respMessage.getSOAPPart()).setSOAPEnvelope(responseEnvelope);

         log.debug("Exit: prepareResponseEnvelope\n" + responseEnvelope);
      }
      catch (JAXRPCException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new JAXRPCException(e);
      }
   }

   public Object[] getArgValues()
   {
      return argValues;
   }

   public RPCElement getBody()
   {
      return body;
   }

   public MessageContext getMessageContext()
   {
      return messageContext;
   }

   public OperationDesc getOperation()
   {
      return operation;
   }

   public List getOutParams()
   {
      return outParams;
   }

   public SOAPEnvelopeAxisImpl getRequestEnvelope()
   {
      return requestEnvelope;
   }

   public SOAPEnvelopeAxisImpl getResponseEnvelope()
   {
      return responseEnvelope;
   }

   public Object getTargetObject()
   {
      return targetObject;
   }

   public String toString()
   {
      StringBuffer ret = new StringBuffer("\nRPCInvocation:\n");
      ret.append("TargetObject=" + targetObject + "\n");
      ret.append("Operation=" + operation);
      ret.append("ArgValues=" + (argValues != null ? Arrays.asList(argValues) : null) + "\n");
      ret.append("ReturnValue=" + returnValue + "\n");
      return ret.toString();
   }
}