content: /*
   * Copyright (C) 2013 4th Line GmbH, Switzerland
   *
   * The contents of this file are subject to the terms of either the GNU
   * Lesser General Public License Version 2 or later ("LGPL") or the
   * Common Development and Distribution License Version 1 or later
   * ("CDDL") (collectively, the "License"). You may not use this file
   * except in compliance with the License. See LICENSE.txt for more
   * information.
   *
   * This program 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.
   */
  
  package org.fourthline.cling.transport.impl;
  
  import org.fourthline.cling.model.Constants;
  import org.fourthline.cling.model.XMLUtil;
  import org.fourthline.cling.model.action.ActionArgumentValue;
  import org.fourthline.cling.model.action.ActionException;
  import org.fourthline.cling.model.action.ActionInvocation;
  import org.fourthline.cling.model.message.control.ActionMessage;
  import org.fourthline.cling.model.message.control.ActionRequestMessage;
  import org.fourthline.cling.model.message.control.ActionResponseMessage;
  import org.fourthline.cling.model.meta.ActionArgument;
  import org.fourthline.cling.model.types.ErrorCode;
  import org.fourthline.cling.model.types.InvalidValueException;
  import org.fourthline.cling.transport.spi.SOAPActionProcessor;
  import org.fourthline.cling.model.UnsupportedDataException;
  import org.w3c.dom.Attr;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.w3c.dom.Node;
  import org.w3c.dom.NodeList;
  import org.xml.sax.ErrorHandler;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.SAXParseException;
  
  import javax.xml.parsers.DocumentBuilder;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.parsers.FactoryConfigurationError;
  
  import java.io.StringReader;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
  /**
   * Default implementation based on the <em>W3C DOM</em> XML processing API.
   *
   * @author Christian Bauer
   */
  public class SOAPActionProcessorImpl implements SOAPActionProcessor, ErrorHandler {
  
      private static Logger log = Logger.getLogger(SOAPActionProcessor.class.getName());
      
      protected DocumentBuilderFactory createDocumentBuilderFactory() throws FactoryConfigurationError {
      	return DocumentBuilderFactory.newInstance();
      }
  
      public void writeBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
  
          log.fine("Writing body of " + requestMessage + " for: " + actionInvocation);
  
          try {
  
              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
              factory.setNamespaceAware(true);
              Document d = factory.newDocumentBuilder().newDocument();
              Element body = writeBodyElement(d);
  
              writeBodyRequest(d, body, requestMessage, actionInvocation);
  
              if (log.isLoggable(Level.FINER)) {
                  log.finer("===================================== SOAP BODY BEGIN ============================================");
                  log.finer(requestMessage.getBodyString());
                  log.finer("-===================================== SOAP BODY END ============================================");
              }
  
          } catch (Exception ex) {
              throw new UnsupportedDataException("Can't transform message payload: " + ex, ex);
          }
      }
  
      public void writeBody(ActionResponseMessage responseMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
  
          log.fine("Writing body of " + responseMessage + " for: " + actionInvocation);
  
          try {
  
              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
              factory.setNamespaceAware(true);
              Document d = factory.newDocumentBuilder().newDocument();
              Element body = writeBodyElement(d);
  
              if (actionInvocation.getFailure() != null) {
                  writeBodyFailure(d, body, responseMessage, actionInvocation);
              } else {
                  writeBodyResponse(d, body, responseMessage, actionInvocation);
              }
  
              if (log.isLoggable(Level.FINER)) {
                  log.finer("===================================== SOAP BODY BEGIN ============================================");
                  log.finer(responseMessage.getBodyString());
                  log.finer("-===================================== SOAP BODY END ============================================");
              }
  
          } catch (Exception ex) {
              throw new UnsupportedDataException("Can't transform message payload: " + ex, ex);
          }
      }
  
      public void readBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException {
  
          log.fine("Reading body of " + requestMessage + " for: " + actionInvocation);
          if (log.isLoggable(Level.FINER)) {
              log.finer("===================================== SOAP BODY BEGIN ============================================");
              log.finer(requestMessage.getBodyString());
              log.finer("-===================================== SOAP BODY END ============================================");
          }
  
          String body = getMessageBody(requestMessage);
          try {
  
              DocumentBuilderFactory factory = createDocumentBuilderFactory();
              factory.setNamespaceAware(true);
              DocumentBuilder documentBuilder = factory.newDocumentBuilder();
              documentBuilder.setErrorHandler(this);
  
              Document d = documentBuilder.parse(new InputSource(new StringReader(body)));
  
              Element bodyElement = readBodyElement(d);
  
              readBodyRequest(d, bodyElement, requestMessage, actionInvocation);
  
          } catch (Exception ex) {
              throw new UnsupportedDataException("Can't transform message payload: " + ex, ex, body);
          }
      }
  
      public void readBody(ActionResponseMessage responseMsg, ActionInvocation actionInvocation) throws UnsupportedDataException {
  
          log.fine("Reading body of " + responseMsg + " for: " + actionInvocation);
          if (log.isLoggable(Level.FINER)) {
              log.finer("===================================== SOAP BODY BEGIN ============================================");
              log.finer(responseMsg.getBodyString());
              log.finer("-===================================== SOAP BODY END ============================================");
          }
  
          String body = getMessageBody(responseMsg);
          try {
  
              DocumentBuilderFactory factory = createDocumentBuilderFactory();
              factory.setNamespaceAware(true);
              DocumentBuilder documentBuilder = factory.newDocumentBuilder();
              documentBuilder.setErrorHandler(this);
  
              Document d = documentBuilder.parse(new InputSource(new StringReader(body)));
  
              Element bodyElement = readBodyElement(d);
  
              ActionException failure = readBodyFailure(d, bodyElement);
  
              if (failure == null) {
                  readBodyResponse(d, bodyElement, responseMsg, actionInvocation);
              } else {
                  actionInvocation.setFailure(failure);
              }
  
          } catch (Exception ex) {
      		throw new UnsupportedDataException("Can't transform message payload: " + ex, ex, body);
          }
      }
  
      /* ##################################################################################################### */
  
      protected void writeBodyFailure(Document d,
                                      Element bodyElement,
                                      ActionResponseMessage message,
                                      ActionInvocation actionInvocation) throws Exception {
  
          writeFaultElement(d, bodyElement, actionInvocation);
          message.setBody(toString(d));
      }
  
      protected void writeBodyRequest(Document d,
                                      Element bodyElement,
                                      ActionRequestMessage message,
                                      ActionInvocation actionInvocation) throws Exception {
  
          Element actionRequestElement = writeActionRequestElement(d, bodyElement, message, actionInvocation);
          writeActionInputArguments(d, actionRequestElement, actionInvocation);
          message.setBody(toString(d));
  
      }
  
      protected void writeBodyResponse(Document d,
                                       Element bodyElement,
                                       ActionResponseMessage message,
                                       ActionInvocation actionInvocation) throws Exception {
  
          Element actionResponseElement = writeActionResponseElement(d, bodyElement, message, actionInvocation);
          writeActionOutputArguments(d, actionResponseElement, actionInvocation);
          message.setBody(toString(d));
      }
  
      protected ActionException readBodyFailure(Document d, Element bodyElement) throws Exception {
          return readFaultElement(bodyElement);
      }
  
      protected void readBodyRequest(Document d,
                                     Element bodyElement,
                                     ActionRequestMessage message,
                                     ActionInvocation actionInvocation) throws Exception {
  
          Element actionRequestElement = readActionRequestElement(bodyElement, message, actionInvocation);
          readActionInputArguments(actionRequestElement, actionInvocation);
      }
  
      protected void readBodyResponse(Document d,
                                      Element bodyElement,
                                      ActionResponseMessage message,
                                      ActionInvocation actionInvocation) throws Exception {
  
          Element actionResponse = readActionResponseElement(bodyElement, actionInvocation);
          readActionOutputArguments(actionResponse, actionInvocation);
      }
  
      /* ##################################################################################################### */
  
      protected Element writeBodyElement(Document d) {
  
          Element envelopeElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Envelope");
          Attr encodingStyleAttr = d.createAttributeNS(Constants.SOAP_NS_ENVELOPE, "s:encodingStyle");
          encodingStyleAttr.setValue(Constants.SOAP_URI_ENCODING_STYLE);
          envelopeElement.setAttributeNode(encodingStyleAttr);
          d.appendChild(envelopeElement);
  
          Element bodyElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Body");
          envelopeElement.appendChild(bodyElement);
  
          return bodyElement;
      }
  
      protected Element readBodyElement(Document d) {
  
          Element envelopeElement = d.getDocumentElement();
          
          if (envelopeElement == null || !getUnprefixedNodeName(envelopeElement).equals("Envelope")) {
              throw new RuntimeException("Response root element was not 'Envelope'");
          }
  
          NodeList envelopeElementChildren = envelopeElement.getChildNodes();
          for (int i = 0; i < envelopeElementChildren.getLength(); i++) {
              Node envelopeChild = envelopeElementChildren.item(i);
  
              if (envelopeChild.getNodeType() != Node.ELEMENT_NODE)
                  continue;
  
              if (getUnprefixedNodeName(envelopeChild).equals("Body")) {
                  return (Element) envelopeChild;
              }
          }
  
          throw new RuntimeException("Response envelope did not contain 'Body' child element");
      }
  
      /* ##################################################################################################### */
  
      protected Element writeActionRequestElement(Document d,
                                                  Element bodyElement,
                                                  ActionRequestMessage message,
                                                  ActionInvocation actionInvocation) {
  
          log.fine("Writing action request element: " + actionInvocation.getAction().getName());
  
          Element actionRequestElement = d.createElementNS(
                  message.getActionNamespace(),
                  "u:" + actionInvocation.getAction().getName()
          );
          bodyElement.appendChild(actionRequestElement);
  
          return actionRequestElement;
      }
  
      protected Element readActionRequestElement(Element bodyElement,
                                                 ActionRequestMessage message,
                                                 ActionInvocation actionInvocation) {
          NodeList bodyChildren = bodyElement.getChildNodes();
  
          log.fine("Looking for action request element matching namespace:" + message.getActionNamespace());
  
          for (int i = 0; i < bodyChildren.getLength(); i++) {
              Node bodyChild = bodyChildren.item(i);
  
              if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
                  continue;
  
              String unprefixedName = getUnprefixedNodeName(bodyChild);
              if (unprefixedName.equals(actionInvocation.getAction().getName())) {
                  if (bodyChild.getNamespaceURI() == null
                      || !bodyChild.getNamespaceURI().equals(message.getActionNamespace()))
                      throw new UnsupportedDataException(
                          "Illegal or missing namespace on action request element: " + bodyChild
                      );
                  log.fine("Reading action request element: " + unprefixedName);
                  return (Element) bodyChild;
              }
          }
          throw new UnsupportedDataException(
              "Could not read action request element matching namespace: " + message.getActionNamespace()
          );
      }
  
      /* ##################################################################################################### */
  
      protected Element writeActionResponseElement(Document d,
                                                   Element bodyElement,
                                                   ActionResponseMessage message,
                                                   ActionInvocation actionInvocation) {
  
          log.fine("Writing action response element: " + actionInvocation.getAction().getName());
          Element actionResponseElement = d.createElementNS(
                  message.getActionNamespace(),
                  "u:" + actionInvocation.getAction().getName() + "Response"
          );
          bodyElement.appendChild(actionResponseElement);
  
          return actionResponseElement;
      }
  
      protected Element readActionResponseElement(Element bodyElement, ActionInvocation actionInvocation) {
          NodeList bodyChildren = bodyElement.getChildNodes();
  
          for (int i = 0; i < bodyChildren.getLength(); i++) {
              Node bodyChild = bodyChildren.item(i);
  
              if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
                  continue;
  
              if (getUnprefixedNodeName(bodyChild).equals(actionInvocation.getAction().getName() + "Response")) {
                  log.fine("Reading action response element: " + getUnprefixedNodeName(bodyChild));
                  return (Element) bodyChild;
              }
          }
          log.fine("Could not read action response element");
          return null;
      }
  
      /* ##################################################################################################### */
  
      protected void writeActionInputArguments(Document d,
                                               Element actionRequestElement,
                                               ActionInvocation actionInvocation) {
  
          for (ActionArgument argument : actionInvocation.getAction().getInputArguments()) {
              log.fine("Writing action input argument: " + argument.getName());
              String value = actionInvocation.getInput(argument) != null ? actionInvocation.getInput(argument).toString() : "";
              XMLUtil.appendNewElement(d, actionRequestElement, argument.getName(), value);
          }
      }
  
      public void readActionInputArguments(Element actionRequestElement,
                                           ActionInvocation actionInvocation) throws ActionException {
          actionInvocation.setInput(
                  readArgumentValues(
                          actionRequestElement.getChildNodes(),
                          actionInvocation.getAction().getInputArguments()
                  )
          );
      }
  
      /* ##################################################################################################### */
  
      protected void writeActionOutputArguments(Document d,
                                                Element actionResponseElement,
                                                ActionInvocation actionInvocation) {
  
          for (ActionArgument argument : actionInvocation.getAction().getOutputArguments()) {
              log.fine("Writing action output argument: " + argument.getName());
              String value = actionInvocation.getOutput(argument) != null ? actionInvocation.getOutput(argument).toString() : "";
              XMLUtil.appendNewElement(d, actionResponseElement, argument.getName(), value);
          }
      }
  
      protected void readActionOutputArguments(Element actionResponseElement,
                                               ActionInvocation actionInvocation) throws ActionException {
  
          actionInvocation.setOutput(
                  readArgumentValues(
                          actionResponseElement.getChildNodes(),
                          actionInvocation.getAction().getOutputArguments()
                  )
          );
      }
  
      /* ##################################################################################################### */
  
      protected void writeFaultElement(Document d, Element bodyElement, ActionInvocation actionInvocation) {
  
          Element faultElement = d.createElementNS(Constants.SOAP_NS_ENVELOPE, "s:Fault");
          bodyElement.appendChild(faultElement);
  
          // This stuff is really completely arbitrary nonsense... let's hope they fired the guy who decided this
          XMLUtil.appendNewElement(d, faultElement, "faultcode", "s:Client");
          XMLUtil.appendNewElement(d, faultElement, "faultstring", "UPnPError");
  
          Element detailElement = d.createElement("detail");
          faultElement.appendChild(detailElement);
  
          Element upnpErrorElement = d.createElementNS(Constants.NS_UPNP_CONTROL_10, "UPnPError");
          detailElement.appendChild(upnpErrorElement);
  
          int errorCode = actionInvocation.getFailure().getErrorCode();
          String errorDescription = actionInvocation.getFailure().getMessage();
  
          log.fine("Writing fault element: " + errorCode + " - " + errorDescription);
  
          XMLUtil.appendNewElement(d, upnpErrorElement, "errorCode", Integer.toString(errorCode));
          XMLUtil.appendNewElement(d, upnpErrorElement, "errorDescription", errorDescription);
  
      }
  
      protected ActionException readFaultElement(Element bodyElement) {
  
          boolean receivedFaultElement = false;
          String errorCode = null;
          String errorDescription = null;
  
          NodeList bodyChildren = bodyElement.getChildNodes();
  
          for (int i = 0; i < bodyChildren.getLength(); i++) {
              Node bodyChild = bodyChildren.item(i);
  
              if (bodyChild.getNodeType() != Node.ELEMENT_NODE)
                  continue;
  
              if (getUnprefixedNodeName(bodyChild).equals("Fault")) {
  
                  receivedFaultElement = true;
  
                  NodeList faultChildren = bodyChild.getChildNodes();
  
                  for (int j = 0; j < faultChildren.getLength(); j++) {
                      Node faultChild = faultChildren.item(j);
  
                      if (faultChild.getNodeType() != Node.ELEMENT_NODE)
                          continue;
  
                      if (getUnprefixedNodeName(faultChild).equals("detail")) {
  
                          NodeList detailChildren = faultChild.getChildNodes();
                          for (int x = 0; x < detailChildren.getLength(); x++) {
                              Node detailChild = detailChildren.item(x);
  
                              if (detailChild.getNodeType() != Node.ELEMENT_NODE)
                                  continue;
  
                              if (getUnprefixedNodeName(detailChild).equals("UPnPError")) {
  
                                  NodeList errorChildren = detailChild.getChildNodes();
                                  for (int y = 0; y < errorChildren.getLength(); y++) {
                                      Node errorChild = errorChildren.item(y);
  
                                      if (errorChild.getNodeType() != Node.ELEMENT_NODE)
                                          continue;
  
                                      if (getUnprefixedNodeName(errorChild).equals("errorCode"))
                                          errorCode = XMLUtil.getTextContent(errorChild);
  
                                      if (getUnprefixedNodeName(errorChild).equals("errorDescription"))
                                          errorDescription = XMLUtil.getTextContent(errorChild);
                                  }
                              }
                          }
                      }
                  }
              }
          }
  
          if (errorCode != null) {
              try {
                  int numericCode = Integer.valueOf(errorCode);
                  ErrorCode standardErrorCode = ErrorCode.getByCode(numericCode);
                  if (standardErrorCode != null) {
                      log.fine("Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription);
                      return new ActionException(standardErrorCode, errorDescription, false);
                  } else {
                      log.fine("Reading fault element: " + numericCode + " - " + errorDescription);
                      return new ActionException(numericCode, errorDescription);
                  }
              } catch (NumberFormatException ex) {
                  throw new RuntimeException("Error code was not a number");
              }
          } else if (receivedFaultElement) {
              throw new RuntimeException("Received fault element but no error code");
          }
          return null;
      }
  
  
      /* ##################################################################################################### */
  
      protected String getMessageBody(ActionMessage message) throws UnsupportedDataException {
          if (!message.isBodyNonEmptyString())
              throw new UnsupportedDataException(
                  "Can't transform null or non-string/zero-length body of: " + message
              );
          return message.getBodyString().trim();
      }
  
      protected String toString(Document d) throws Exception {
          // Just to be safe, no newline at the end
          String output = XMLUtil.documentToString(d);
          while (output.endsWith("\n") || output.endsWith("\r")) {
              output = output.substring(0, output.length() - 1);
          }
  
          return output;
      }
  
      protected String getUnprefixedNodeName(Node node) {
          return node.getPrefix() != null
                  ? node.getNodeName().substring(node.getPrefix().length() + 1)
                  : node.getNodeName();
      }
  
      /**
       * The UPnP spec says that action arguments must be in the order as declared
       * by the service. This method however is lenient, the action argument nodes
       * in the XML can be in any order, as long as they are all there everything
       * is OK.
       */
      protected ActionArgumentValue[] readArgumentValues(NodeList nodeList, ActionArgument[] args)
              throws ActionException {
  
          List<Node> nodes = getMatchingNodes(nodeList, args);
  
          ActionArgumentValue[] values = new ActionArgumentValue[args.length];
  
          for (int i = 0; i < args.length; i++) {
          	
              ActionArgument arg = args[i];
              Node node = findActionArgumentNode(nodes, arg);
              if(node == null) {
                  throw new ActionException(
                          ErrorCode.ARGUMENT_VALUE_INVALID,
                          "Could not find argument '" + arg.getName() + "' node");
              }
              log.fine("Reading action argument: " + arg.getName());
              String value = XMLUtil.getTextContent(node);
              values[i] = createValue(arg, value);
          }
          return values;
      }
  
      /**
       * Finds all element nodes in the list that match any argument name or argument
       * alias, throws {@link ActionException} if not all arguments were found.
       */
      protected List<Node> getMatchingNodes(NodeList nodeList, ActionArgument[] args) throws ActionException {
  
          List<String> names = new ArrayList<>();
          for (ActionArgument argument : args) {
              names.add(argument.getName());
              names.addAll(Arrays.asList(argument.getAliases()));
          }
  
          List<Node> matches = new ArrayList<>();
          for (int i = 0; i < nodeList.getLength(); i++) {
              Node child = nodeList.item(i);
  
              if (child.getNodeType() != Node.ELEMENT_NODE)
                  continue;
  
              if (names.contains(getUnprefixedNodeName(child)))
                  matches.add(child);
          }
  
          if (matches.size() < args.length) {
              throw new ActionException(
                      ErrorCode.ARGUMENT_VALUE_INVALID,
                      "Invalid number of input or output arguments in XML message, expected " + args.length + " but found " + matches.size()
              );
          }
          return matches;
      }
  
      /**
       * Creates an instance of {@link ActionArgumentValue} and wraps an
       * {@link InvalidValueException} as an {@link ActionException} with the
       * appropriate {@link ErrorCode}.
       */
      protected ActionArgumentValue createValue(ActionArgument arg, String value) throws ActionException {
          try {
              return new ActionArgumentValue(arg, value);
          } catch (InvalidValueException ex) {
              throw new ActionException(
                      ErrorCode.ARGUMENT_VALUE_INVALID,
                      "Wrong type or invalid value for '" + arg.getName() + "': " + ex.getMessage(),
                      ex
              );
          }
      }
  
      /**
       * Returns the node with the same unprefixed name as the action argument
       * name/alias or <code>null</code>.
       */
      protected Node findActionArgumentNode(List<Node> nodes, ActionArgument arg) {
      	for(Node node : nodes) {
      		if(arg.isNameOrAlias(getUnprefixedNodeName(node))) return node;
      	}
      	return null;
      }
  
      public void warning(SAXParseException e) throws SAXException {
          log.warning(e.toString());
      }
  
      public void error(SAXParseException e) throws SAXException {
          throw e;
      }
  
      public void fatalError(SAXParseException e) throws SAXException {
          throw e;
      }
  }
  