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);
  }
  public void hydrateActionArgument(MutableActionArgument actionArgument, Node actionArgumentNode) {

    NodeList argumentNodeChildren = actionArgumentNode.getChildNodes();
    for (int i = 0; i < argumentNodeChildren.getLength(); i++) {
      Node argumentNodeChild = argumentNodeChildren.item(i);

      if (argumentNodeChild.getNodeType() != Node.ELEMENT_NODE) continue;

      if (ELEMENT.name.equals(argumentNodeChild)) {
        actionArgument.name = XMLUtil.getTextContent(argumentNodeChild);
      } else if (ELEMENT.direction.equals(argumentNodeChild)) {
        String directionString = XMLUtil.getTextContent(argumentNodeChild);
        try {
          actionArgument.direction =
              ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ENGLISH));
        } catch (IllegalArgumentException ex) {
          // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT
          log.warning(
              "UPnP specification violation: Invalid action argument direction, assuming 'IN': "
                  + directionString);
          actionArgument.direction = ActionArgument.Direction.IN;
        }
      } else if (ELEMENT.relatedStateVariable.equals(argumentNodeChild)) {
        actionArgument.relatedStateVariable = XMLUtil.getTextContent(argumentNodeChild);
      } else if (ELEMENT.retval.equals(argumentNodeChild)) {
        actionArgument.retval = true;
      }
    }
  }
  public void hydrateAction(MutableAction action, Node actionNode) {

    NodeList actionNodeChildren = actionNode.getChildNodes();
    for (int i = 0; i < actionNodeChildren.getLength(); i++) {
      Node actionNodeChild = actionNodeChildren.item(i);

      if (actionNodeChild.getNodeType() != Node.ELEMENT_NODE) continue;

      if (ELEMENT.name.equals(actionNodeChild)) {
        action.name = XMLUtil.getTextContent(actionNodeChild);
      } else if (ELEMENT.argumentList.equals(actionNodeChild)) {

        NodeList argumentChildren = actionNodeChild.getChildNodes();
        for (int j = 0; j < argumentChildren.getLength(); j++) {
          Node argumentChild = argumentChildren.item(j);

          if (argumentChild.getNodeType() != Node.ELEMENT_NODE) continue;

          MutableActionArgument actionArgument = new MutableActionArgument();
          hydrateActionArgument(actionArgument, argumentChild);
          action.arguments.add(actionArgument);
        }
      }
    }
  }
  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;
  }
  public String generate(Service service) throws DescriptorBindingException {
    try {
      log.fine("Generating XML descriptor from service model: " + service);

      return XMLUtil.documentToString(buildDOM(service));

    } catch (Exception ex) {
      throw new DescriptorBindingException("Could not build DOM: " + ex.getMessage(), ex);
    }
  }
  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);
    }
  }
  /**
   * 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;
  }
  public void hydrateStateVariable(
      MutableStateVariable stateVariable, Element stateVariableElement) {

    stateVariable.eventDetails =
        new StateVariableEventDetails(
            stateVariableElement.getAttribute("sendEvents") != null
                && stateVariableElement
                    .getAttribute(ATTRIBUTE.sendEvents.toString())
                    .toUpperCase(Locale.ENGLISH)
                    .equals("YES"));

    NodeList stateVariableChildren = stateVariableElement.getChildNodes();
    for (int i = 0; i < stateVariableChildren.getLength(); i++) {
      Node stateVariableChild = stateVariableChildren.item(i);

      if (stateVariableChild.getNodeType() != Node.ELEMENT_NODE) continue;

      if (ELEMENT.name.equals(stateVariableChild)) {
        stateVariable.name = XMLUtil.getTextContent(stateVariableChild);
      } else if (ELEMENT.dataType.equals(stateVariableChild)) {
        String dtName = XMLUtil.getTextContent(stateVariableChild);
        Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(dtName);
        stateVariable.dataType =
            builtin != null ? builtin.getDatatype() : new CustomDatatype(dtName);
      } else if (ELEMENT.defaultValue.equals(stateVariableChild)) {
        stateVariable.defaultValue = XMLUtil.getTextContent(stateVariableChild);
      } else if (ELEMENT.allowedValueList.equals(stateVariableChild)) {

        List<String> allowedValues = new ArrayList();

        NodeList allowedValueListChildren = stateVariableChild.getChildNodes();
        for (int j = 0; j < allowedValueListChildren.getLength(); j++) {
          Node allowedValueListChild = allowedValueListChildren.item(j);

          if (allowedValueListChild.getNodeType() != Node.ELEMENT_NODE) continue;

          if (ELEMENT.allowedValue.equals(allowedValueListChild))
            allowedValues.add(XMLUtil.getTextContent(allowedValueListChild));
        }

        stateVariable.allowedValues = allowedValues;

      } else if (ELEMENT.allowedValueRange.equals(stateVariableChild)) {

        MutableAllowedValueRange range = new MutableAllowedValueRange();

        NodeList allowedValueRangeChildren = stateVariableChild.getChildNodes();
        for (int j = 0; j < allowedValueRangeChildren.getLength(); j++) {
          Node allowedValueRangeChild = allowedValueRangeChildren.item(j);

          if (allowedValueRangeChild.getNodeType() != Node.ELEMENT_NODE) continue;

          if (ELEMENT.minimum.equals(allowedValueRangeChild)) {
            try {
              range.minimum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
            } catch (Exception ex) {
            }
          } else if (ELEMENT.maximum.equals(allowedValueRangeChild)) {
            try {
              range.maximum = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
            } catch (Exception ex) {
            }
          } else if (ELEMENT.step.equals(allowedValueRangeChild)) {
            try {
              range.step = Long.valueOf(XMLUtil.getTextContent(allowedValueRangeChild));
            } catch (Exception ex) {
            }
          }
        }

        stateVariable.allowedValueRange = range;
      }
    }
  }
  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;
  }