/*
 * $Id: SOAPCallingConvention.java,v 1.83 2013/01/18 10:41:47 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.server;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.w3c.dom.Attr;

import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.spec.DataSectionElementSpec;
import org.xins.common.spec.EntityNotFoundException;
import org.xins.common.spec.FunctionSpec;
import org.xins.common.spec.InvalidSpecificationException;
import org.xins.common.spec.ParameterSpec;
import org.xins.common.text.ParseException;
import org.xins.common.types.Type;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xins.common.xml.ElementList;

import org.znerd.xmlenc.XMLOutputter;

/**
 * The SOAP calling convention.
 * The SOAP message parsed by this calling convention is expected to match
 * the WSDL generated by the _WSDL meta-function.
 *
 * @version $Revision: 1.83 $ $Date: 2013/01/18 10:41:47 $
 * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
 * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
 */
public class SOAPCallingConvention extends CallingConvention {

   /**
    * The response encoding format.
    */
   protected static final String RESPONSE_ENCODING = "UTF-8";

   /**
    * The content type of the HTTP response.
    */
   protected static final String RESPONSE_CONTENT_TYPE = "text/xml; charset=" + RESPONSE_ENCODING;

   /**
    * The key used to store the name of the namespace in the request attributes.
    *
    * @since XINS 2.1.
    */
   protected static final String REQUEST_NAMESPACE = "_namespace";

   /**
    * The formatter for XINS Date type.
    */
   private static final SimpleDateFormat XINS_DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");

   /**
    * The formatter for SOAP Date type.
    */
   private static final SimpleDateFormat SOAP_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");

   /**
    * The formatter for XINS Timestamp type.
    */
   private static final SimpleDateFormat XINS_TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyyMMddHHmmss");

   /**
    * The formatter for SOAP dateType type.
    */
   private static final SimpleDateFormat SOAP_TIMESTAMP_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

   /**
    * The API. Never <code>null</code>.
    */
   private final API _api;

   /**
    * Creates a new <code>SOAPCallingConvention</code> instance.
    *
    * @param api
    *    the API, needed for the SOAP messages, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>api == null</code>.
    */
   public SOAPCallingConvention(API api) throws IllegalArgumentException {

      // Check arguments
      MandatoryArgumentChecker.check("api", api);

      // Store the API
      _api = api;
   }

   protected String[] getSupportedMethods() {
      return new String[] { "POST" };
   }

   /**
    * Checks if the specified request can be handled by this calling
    * convention.
    *
    * <p>This method will not throw any exception.
    *
    * @param httpRequest
    *    the HTTP request to investigate, cannot be <code>null</code>.
    *
    * @return
    *    <code>true</code> if this calling convention is <em>possibly</em>
    *    able to handle this request, or <code>false</code> if it
    *    <em>definitely</em> not able to handle this request.
    *
    * @throws Exception
    *    if analysis of the request causes an exception;
    *    <code>false</code> will be assumed.
    */
   protected boolean matches(HttpServletRequest httpRequest)
   throws Exception {

      // Parse the XML in the request (if any)
      Element element = parseXMLRequest(httpRequest);
      String elementName = element.getLocalName();

      // The root element must be <Envelope/>
      if (elementName.equals("Envelope")) {

         // There must be a <Body/> element within the <Envelope/>
         Element bodyElement = new ElementList(element, "Body").get(0);

         // There must be one child element
         ElementList bodyChildren = new ElementList(bodyElement);
         if (bodyChildren.size() == 1) {
            Element functionElement     = (Element) bodyChildren.get(0);
            String  functionElementName = functionElement.getLocalName();

            // The name of the child element must match '<Function>Request'
            return functionElementName.endsWith("Request") &&
                   functionElementName.length() > 7;
         }
      }

      return false;
   }

   protected FunctionRequest convertRequestImpl(HttpServletRequest httpRequest)
   throws InvalidRequestException,
          FunctionNotSpecifiedException {

      Element envelopeElem = parseXMLRequest(httpRequest);

      String envelopeName = envelopeElem.getLocalName();
      if (! envelopeName.equals("Envelope")) {
         throw new InvalidRequestException("Root element is not a SOAP envelope but \"" +
               envelopeName + "\".");
      }

      Element functionElem;
      try {
         Element bodyElem = new ElementList(envelopeElem, "Body").getUniqueChildElement();
         functionElem = new ElementList(bodyElem).getUniqueChildElement();
      } catch (ParseException pex) {
         throw new InvalidRequestException("Incorrect SOAP message.", pex);
      }
      String requestName = functionElem.getLocalName();
      if (!requestName.endsWith("Request")) {
         throw new InvalidRequestException("Function names should always end " +
               "\"Request\" for the SOAP calling convention.");
      }
      String functionName = requestName.substring(0, requestName.lastIndexOf("Request"));
      Map<String, Object> backpack = new HashMap<String, Object>();
      backpack.put(REQUEST_NAMESPACE, functionElem.getNamespaceURI());

      Element parametersElem;
      ElementList parametersList = new ElementList(functionElem, "parameters");
      if (parametersList.isEmpty()) {
         parametersElem = functionElem;
      } else {
         parametersElem = parametersList.get(0);
      }

      // Parse the input parameters
      Map<String, String> parameters = readInputParameters(parametersElem, functionName);

      // Parse the input data section
      Element transformedDataSection = readDataSection(parametersElem, functionName);

      return new FunctionRequest(functionName, parameters, transformedDataSection, backpack);
   }

   protected void convertResultImpl(FunctionResult      xinsResult,
                                    HttpServletResponse httpResponse,
                                    Map<String, Object> backpack)
   throws IOException {

      // Send the XML output to the stream and flush
      httpResponse.setContentType(RESPONSE_CONTENT_TYPE);
      PrintWriter out = httpResponse.getWriter();
      if (xinsResult.getErrorCode() != null) {
         httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      } else {
         httpResponse.setStatus(HttpServletResponse.SC_OK);
      }

      // Store the result in a StringWriter before sending it.
      Writer buffer = new StringWriter(1024);

      // Create an XMLOutputter
      XMLOutputter xmlout = new XMLOutputter(buffer, RESPONSE_ENCODING);

      // Output the declaration
      // XXX: Make it configurable whether the declaration is output or not?
      xmlout.declaration();

      // Write the envelope start tag
      xmlout.startTag("soap:Envelope");
      xmlout.attribute("xmlns:soap", "http://schemas.xmlsoap.org/soap/envelope/");

      // Write the body start tag
      xmlout.startTag("soap:Body");

      String functionName = (String) backpack.get(BackpackConstants.FUNCTION_NAME);
      String namespaceURI = (String) backpack.get(REQUEST_NAMESPACE);

      if (xinsResult.getErrorCode() != null) {
         writeFaultSection(functionName, namespaceURI, xinsResult, xmlout);
      } else {

         // Write the response start tag
         xmlout.startTag("ns0:" + functionName + "Response");
         xmlout.attribute("xmlns:ns0", namespaceURI);

         writeOutputParameters(functionName, xinsResult, xmlout);
         writeOutputDataSection(functionName, xinsResult, xmlout);

         xmlout.endTag(); // response
      }

      xmlout.endTag(); // body
      xmlout.endTag(); // envelope

      // Write the result to the servlet response
      out.write(buffer.toString());

      out.close();
   }

   /**
    * Reads the input parameters.
    *
    * @param parametersElem
    *    the XML element which contains the parameters, cannot be <code>null</code>
    *
    * @param functionName
    *    the name of the function called, cannot be <code>null</code>.
    *
    * @return
    *    the parameters for the function, never <code>null</code>.
    */
   protected Map<String, String> readInputParameters(Element parametersElem, String functionName) {
      Map<String, String> parameters = new HashMap<String, String>();
      for (Element parameterElem : new ElementList(parametersElem)) {
         String parameterName = parameterElem.getLocalName();
         String parameterValue = parameterElem.getTextContent();
         try {
            FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
            Type parameterType = functionSpec.getInputParameter(parameterName).getType();
            parameterValue = soapInputValueTransformation(parameterType, parameterValue);
         } catch (InvalidSpecificationException ise) {

            // keep the old value
         } catch (EntityNotFoundException enfe) {

            // keep the old value
         }
         parameters.put(parameterName, parameterValue);
      }
      return parameters;
   }

   /**
    * Reads the input parameters.
    *
    * @param parametersElem
    *    the XML element which contains the parameters and data section,
    *    cannot be <code>null</code>
    *
    * @param functionName
    *    the name of the function called, cannot be <code>null</code>.
    *
    * @return
    *    the data section for the function, can be <code>null</code>.
    *
    * @throws InvalidRequestException
    *    if the SOAP request is invalid.
    */
   protected Element readDataSection(Element parametersElem, String functionName) throws InvalidRequestException {
      Element transformedDataSection = null;
      ElementList dataSectionList = new ElementList(parametersElem, "data");
      if (dataSectionList.size() == 1) {
         Element dataSection = (Element) dataSectionList.get(0);

         try {
            FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
            Map dataSectionSpec = functionSpec.getInputDataSectionElements();
            transformedDataSection = soapElementTransformation(dataSectionSpec, true, dataSection, true);
         } catch (InvalidSpecificationException ise) {

            // keep the old value
            transformedDataSection = dataSection;
         } catch (EntityNotFoundException enfe) {

            // keep the old value
            transformedDataSection = dataSection;
         }
      } else if (dataSectionList.size() > 1) {
         throw new InvalidRequestException("Only one data section is allowed.");
      }
      return transformedDataSection;
   }

   /**
    * Writes the fault section to the SOAP XML when an error code is returned
    * from the function call.
    *
    * @param functionName
    *    the name of the function called.
    *
    * @param namespaceURI
    *    the namespace URI to use for the parameters.
    *
    * @param xinsResult
    *    the result of the call to the function.
    *
    * @param xmlout
    *    the XML outputter to write the parameters in.
    *
    * @throws IOException
    *    if the data cannot be written to the XML outputter for any reason.
    */
   protected void writeFaultSection(String functionName, String namespaceURI, FunctionResult xinsResult, XMLOutputter xmlout)
   throws IOException {

      // Write the fault start tag
      xmlout.startTag("soap:Fault");
      xmlout.startTag("faultcode");
      if (xinsResult.getErrorCode().equals(DefaultResultCodes._INVALID_REQUEST.getName())) {
         xmlout.pcdata("soap:Client");
      } else {
         xmlout.pcdata("soap:Server");
      }
      xmlout.endTag(); // faultcode
      xmlout.startTag("faultstring");
      xmlout.pcdata(xinsResult.getErrorCode());
      xmlout.endTag(); // faultstring
      if (!xinsResult.getParameters().isEmpty() || xinsResult.getDataElement() != null) {
         xmlout.startTag("detail");
         xmlout.startTag("ns0:" + xinsResult.getErrorCode() + "Fault");
         xmlout.attribute("xmlns:ns0", namespaceURI);
         writeOutputParameters(functionName, xinsResult, xmlout);
         writeOutputDataSection(functionName, xinsResult, xmlout);
         xmlout.endTag(); // ns0:<errorcode>Fault
         xmlout.endTag(); // detail
      }
      xmlout.endTag(); // fault
   }

   /**
    * Writes the output parameters to the SOAP XML.
    *
    * @param functionName
    *    the name of the function called.
    *
    * @param xinsResult
    *    the result of the call to the function.
    *
    * @param xmlout
    *    the XML outputter to write the parameters in.
    *
    * @throws IOException
    *    if the data cannot be written to the XML outputter for any reason.
    */
   protected void writeOutputParameters(String functionName, FunctionResult xinsResult, XMLOutputter xmlout)
   throws IOException {
      Map<String, String> parameters = xinsResult.getParameters();
      for (Map.Entry<String, String> outputParameter : parameters.entrySet()) {
         String parameterName = outputParameter.getKey();
         String parameterValue = outputParameter.getValue();
         if (xinsResult.getErrorCode() == null) {
            try {
               FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
               Type parameterType = functionSpec.getOutputParameter(parameterName).getType();
               parameterValue = soapOutputValueTransformation(parameterType, parameterValue);
            } catch (InvalidSpecificationException ise) {

               // keep the old value
            } catch (EntityNotFoundException enfe) {

               // keep the old value
            }
         }
         xmlout.startTag(parameterName);
         xmlout.pcdata(parameterValue);
         xmlout.endTag();
      }
   }


   /**
    * Writes the output data section to the SOAP XML.
    *
    * @param functionName
    *    the name of the function called.
    *
    * @param xinsResult
    *    the result of the call to the function.
    *
    * @param xmlout
    *    the XML outputter to write the data section in.
    *
    * @throws IOException
    *    if the data cannot be written to the XML outputter for any reason.
    */
   protected void writeOutputDataSection(String functionName, FunctionResult xinsResult, XMLOutputter xmlout)
   throws IOException {
      Element dataElement = xinsResult.getDataElement();
      if (dataElement != null) {

         Element transformedDataElement = null;
         if (xinsResult.getErrorCode() == null) {
            try {
               FunctionSpec functionSpec = _api.getAPISpecification().getFunction(functionName);
               Map dataSectionSpec = functionSpec.getOutputDataSectionElements();
               transformedDataElement = soapElementTransformation(dataSectionSpec, false, dataElement, true);
            } catch (InvalidSpecificationException ise) {

               // keep the old value
            } catch (EntityNotFoundException enfe) {

               // keep the old value
            }
         }

         /*ElementSerializer serializer = new ElementSerializer();
         if (transformedDataElement == null) {
            serializer.output(xmlout, dataElement);
         } else {
            serializer.output(xmlout, transformedDataElement);
         }*/
      }
   }

   /**
    * Transforms the value of a input SOAP parameter to the XINS equivalent.
    *
    * @param parameterType
    *    the type of the parameter, cannot be <code>null</code>.
    *
    * @param value
    *    the value of the SOAP parameter, cannot be <code>null</code>.
    *
    * @return
    *    the XINS value, never <code>null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   protected String soapInputValueTransformation(Type parameterType, String value) throws InvalidSpecificationException {
      if (parameterType instanceof org.xins.common.types.standard.Boolean) {
         if (value.equals("1")) {
            return "true";
         } else if (value.equals("0")) {
            return "false";
         }
      }
      if (parameterType instanceof org.xins.common.types.standard.Date) {
         try {
            synchronized (SOAP_DATE_FORMATTER) {
               Date date = SOAP_DATE_FORMATTER.parse(value);
               return XINS_DATE_FORMATTER.format(date);
            }
         } catch (java.text.ParseException pe) {
            Utils.logProgrammingError(pe);
         }
      }
      if (parameterType instanceof org.xins.common.types.standard.Timestamp) {
         try {
            synchronized (SOAP_TIMESTAMP_FORMATTER) {
               Date date = SOAP_TIMESTAMP_FORMATTER.parse(value);
               return XINS_TIMESTAMP_FORMATTER.format(date);
            }
         } catch (java.text.ParseException pe) {
            Utils.logProgrammingError(pe);
         }
      }
      return value;
   }

   /**
    * Transforms the value of a output XINS parameter to the SOAP equivalent.
    *
    * @param parameterType
    *    the type of the parameter, cannot be <code>null</code>.
    *
    * @param value
    *    the value returned by the XINS function, cannot be <code>null</code>.
    *
    * @return
    *    the SOAP value, never <code>null</code>.
    *
    * @throws InvalidSpecificationException
    *    if the specification is incorrect.
    */
   protected String soapOutputValueTransformation(Type parameterType, String value) throws InvalidSpecificationException {
      if (parameterType instanceof org.xins.common.types.standard.Date) {
         try {
            synchronized (SOAP_DATE_FORMATTER) {
               Date date = XINS_DATE_FORMATTER.parse(value);
               return SOAP_DATE_FORMATTER.format(date);
            }
         } catch (java.text.ParseException pe) {
            Utils.logProgrammingError(pe);
         }
      }
      if (parameterType instanceof org.xins.common.types.standard.Timestamp) {
         try {
            synchronized (SOAP_TIMESTAMP_FORMATTER) {
               Date date = XINS_TIMESTAMP_FORMATTER.parse(value);
               return SOAP_TIMESTAMP_FORMATTER.format(date);
            }
         } catch (java.text.ParseException pe) {
            Utils.logProgrammingError(pe);
         }
      }
      if (parameterType instanceof org.xins.common.types.standard.Hex) {
         return value.toUpperCase();
      }
      return value;
   }

   /**
    * Convert the values of element to the required format.
    *
    * @param dataSection
    *    the specification of the elements, cannot be <code>null</code>.
    *
    * @param input
    *    <code>true</code> if it's the input parameter that should be transform,
    *    <code>false</code> if it's the output parameter.
    *
    * @param element
    *    the element node to process, cannot be <code>null</code>.
    *
    * @param top
    *    <code>true</code> if it's the top element, <code>false</code> otherwise.
    *
    * @return
    *    the converted value, never <code>null</code>.
    */
   protected Element soapElementTransformation(Map dataSection, boolean input, Element element, boolean top) {
      String elementName = element.getLocalName();
      if (elementName == null) {
         elementName = element.getTagName();
      }
      String elementNameSpacePrefix = element.getPrefix();
      String elementNameSpaceURI = element.getNamespaceURI();
      NamedNodeMap elementAttributes = element.getAttributes();
      String elementText = element.getTextContent();
      ElementList elementChildren = new ElementList(element);
      Map childrenSpec = dataSection;

      String elementQualifiedName = (elementNameSpacePrefix == null) ? elementName : elementNameSpacePrefix + ":" + elementName;
      Element builder = element.getOwnerDocument().createElementNS(elementNameSpaceURI, elementQualifiedName);

      if (!top) {
         builder.setTextContent(elementText);

         // Find the DataSectionElement for this element.
         DataSectionElementSpec elementSpec = (DataSectionElementSpec) dataSection.get(elementName);
         childrenSpec = elementSpec.getSubElements();

         // Go through the attributes
         for (int i = 0; i < elementAttributes.getLength(); i++) {
            Attr attribute = (Attr) elementAttributes.item(i);
            String attributeName  = attribute.getName();
            String attributeValue = attribute.getValue();
            try {

               // Convert the value if needed
               ParameterSpec attributeSpec = elementSpec.getAttribute(attributeName);
               Type attributeType = attributeSpec.getType();
               if (input) {
                  attributeValue = soapInputValueTransformation(attributeType, attributeValue);
               } else {
                  attributeValue = soapOutputValueTransformation(attributeType, attributeValue);
               }
            } catch (InvalidSpecificationException ise) {

               // Keep the old value
            } catch (EntityNotFoundException enfe) {

               // Keep the old value
            }

            setDataElementAttribute(builder, attributeName, attributeValue, elementNameSpacePrefix);
         }
      }

      // Add the children of this element
      for (Element nextChild : elementChildren) {
         Element transformedChild = soapElementTransformation(childrenSpec, input, nextChild, false);
         builder.appendChild(transformedChild);
      }

      return builder;
   }

   /**
    * Writes the attribute a output data element for the returned SOAP element.
    *
    * @param builder
    *    the builder used to create the SOAP Element, cannot be <code>null</code>.
    *
    * @param attributeName
    *    the name of the attribute, cannot be <code>null</code>.
    *
    * @param attributeValue
    *    the value of the attribute, cannot be <code>null</code>.
    *
    * @param elementNameSpacePrefix
    *    the namespace prefix of the parent element, can be <code>null</code>.
    *
    * @since XINS 2.1.
    */
   protected void setDataElementAttribute(Element builder, String attributeName,
         String attributeValue, String elementNameSpacePrefix) {
      builder.setAttribute(attributeName, attributeValue);
   }
}