/**
   * Handle text node during validation.
   *
   * @param received
   * @param source
   */
  private void doText(Node received, Node source) {
    if (log.isDebugEnabled()) {
      log.debug("Validating node value for element: " + received.getParentNode());
    }

    if (received.getNodeValue() != null) {
      Assert.isTrue(
          source.getNodeValue() != null,
          ValidationUtils.buildValueMismatchErrorMessage(
              "Node value not equal for element '" + received.getParentNode().getLocalName() + "'",
              null,
              received.getNodeValue().trim()));

      Assert.isTrue(
          received.getNodeValue().trim().equals(source.getNodeValue().trim()),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Node value not equal for element '" + received.getParentNode().getLocalName() + "'",
              source.getNodeValue().trim(),
              received.getNodeValue().trim()));
    } else {
      Assert.isTrue(
          source.getNodeValue() == null,
          ValidationUtils.buildValueMismatchErrorMessage(
              "Node value not equal for element '" + received.getParentNode().getLocalName() + "'",
              source.getNodeValue().trim(),
              null));
    }

    if (log.isDebugEnabled()) {
      log.debug("Node value '" + received.getNodeValue().trim() + "': OK");
    }
  }
  private void doElementNamespaceValidation(Node received, Node source) {
    // validate element namespace
    if (log.isDebugEnabled()) {
      log.debug("Validating namespace for element: " + received.getLocalName());
    }

    if (received.getNamespaceURI() != null) {
      Assert.isTrue(
          source.getNamespaceURI() != null,
          ValidationUtils.buildValueMismatchErrorMessage(
              "Element namespace not equal for element '" + received.getLocalName() + "'",
              null,
              received.getNamespaceURI()));

      Assert.isTrue(
          received.getNamespaceURI().equals(source.getNamespaceURI()),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Element namespace not equal for element '" + received.getLocalName() + "'",
              source.getNamespaceURI(),
              received.getNamespaceURI()));
    } else {
      Assert.isTrue(
          source.getNamespaceURI() == null,
          ValidationUtils.buildValueMismatchErrorMessage(
              "Element namespace not equal for element '" + received.getLocalName() + "'",
              source.getNamespaceURI(),
              null));
    }
  }
  /**
   * Perform validation on namespace qualified attribute values if present. This includes the
   * validation of namespace presence and equality.
   *
   * @param receivedElement
   * @param receivedAttribute
   * @param sourceElement
   * @param sourceAttribute
   */
  private void doNamespaceQualifiedAttributeValidation(
      Node receivedElement, Node receivedAttribute, Node sourceElement, Node sourceAttribute) {
    String receivedValue = receivedAttribute.getNodeValue();
    String sourceValue = sourceAttribute.getNodeValue();

    if (receivedValue.contains(":") && sourceValue.contains(":")) {
      // value has namespace prefix set, do special QName validation
      String receivedPrefix = receivedValue.substring(0, receivedValue.indexOf(':'));
      String sourcePrefix = sourceValue.substring(0, sourceValue.indexOf(':'));

      Map<String, String> receivedNamespaces =
          XMLUtils.lookupNamespaces(receivedAttribute.getOwnerDocument());
      receivedNamespaces.putAll(XMLUtils.lookupNamespaces(receivedElement));

      if (receivedNamespaces.containsKey(receivedPrefix)) {
        Map<String, String> sourceNamespaces =
            XMLUtils.lookupNamespaces(sourceAttribute.getOwnerDocument());
        sourceNamespaces.putAll(XMLUtils.lookupNamespaces(sourceElement));

        if (sourceNamespaces.containsKey(sourcePrefix)) {
          Assert.isTrue(
              sourceNamespaces.get(sourcePrefix).equals(receivedNamespaces.get(receivedPrefix)),
              ValidationUtils.buildValueMismatchErrorMessage(
                  "Values not equal for attribute value namespace '" + receivedValue + "'",
                  sourceNamespaces.get(sourcePrefix),
                  receivedNamespaces.get(receivedPrefix)));

          // remove namespace prefixes as they must not form equality
          receivedValue = receivedValue.substring((receivedPrefix + ":").length());
          sourceValue = sourceValue.substring((sourcePrefix + ":").length());
        } else {
          throw new ValidationException(
              "Received attribute value '"
                  + receivedAttribute.getLocalName()
                  + "' describes namespace qualified attribute value,"
                  + " control value '"
                  + sourceValue
                  + "' does not");
        }
      }
    }

    Assert.isTrue(
        receivedValue.equals(sourceValue),
        ValidationUtils.buildValueMismatchErrorMessage(
            "Values not equal for attribute '" + receivedAttribute.getLocalName() + "'",
            sourceValue,
            receivedValue));
  }
  @Override
  public void validateMessagePayload(
      Message receivedMessage,
      Message controlMessage,
      XmlMessageValidationContext validationContext,
      TestContext context)
      throws ValidationException {
    log.info("Start XML message validation");

    try {
      if (validationContext.isSchemaValidationEnabled()) {
        validateXMLSchema(receivedMessage, validationContext);
        validateDTD(validationContext.getDTDResource(), receivedMessage);
      }

      validateNamespaces(validationContext.getControlNamespaces(), receivedMessage);
      validateMessageContent(receivedMessage, controlMessage, validationContext, context);

      if (controlMessage != null) {
        Assert.isTrue(
            controlMessage.getHeaderData().size() <= receivedMessage.getHeaderData().size(),
            "Failed to validate header data XML fragments - found "
                + receivedMessage.getHeaderData().size()
                + " header fragments, expected "
                + controlMessage.getHeaderData().size());

        for (int i = 0; i < controlMessage.getHeaderData().size(); i++) {
          validateXmlHeaderFragment(
              receivedMessage.getHeaderData().get(i),
              controlMessage.getHeaderData().get(i),
              validationContext,
              context);
        }
      }

      log.info("XML message validation successful: All values OK");
    } catch (ClassCastException e) {
      throw new CitrusRuntimeException(e);
    } catch (DOMException e) {
      throw new CitrusRuntimeException(e);
    } catch (LSException e) {
      throw new CitrusRuntimeException(e);
    } catch (IllegalArgumentException e) {
      log.error(
          "Failed to validate:\n" + XMLUtils.prettyPrint(receivedMessage.getPayload(String.class)));
      throw new ValidationException("Validation failed:", e);
    } catch (ValidationException ex) {
      log.error(
          "Failed to validate:\n" + XMLUtils.prettyPrint(receivedMessage.getPayload(String.class)));
      throw ex;
    }
  }
  /**
   * Validate message payloads by comparing to a control message.
   *
   * @param receivedMessage
   * @param validationContext
   * @param context
   */
  protected void validateMessageContent(
      Message receivedMessage,
      Message controlMessage,
      XmlMessageValidationContext validationContext,
      TestContext context) {
    if (controlMessage == null || controlMessage.getPayload() == null) {
      log.info("Skip message payload validation as no control message was defined");
      return;
    }

    if (!(controlMessage.getPayload() instanceof String)) {
      throw new IllegalArgumentException(
          "DomXmlMessageValidator does only support message payload of type String, "
              + "but was "
              + controlMessage.getPayload().getClass());
    }

    String controlMessagePayload = controlMessage.getPayload(String.class);

    if (receivedMessage.getPayload() == null
        || !StringUtils.hasText(receivedMessage.getPayload(String.class))) {
      Assert.isTrue(
          !StringUtils.hasText(controlMessagePayload),
          "Unable to validate message payload - received message payload was empty, control message payload is not");
      return;
    } else if (!StringUtils.hasText(controlMessagePayload)) {
      return;
    }

    log.info("Start XML tree validation ...");

    Document received = XMLUtils.parseMessagePayload(receivedMessage.getPayload(String.class));
    Document source = XMLUtils.parseMessagePayload(controlMessagePayload);

    XMLUtils.stripWhitespaceNodes(received);
    XMLUtils.stripWhitespaceNodes(source);

    if (log.isDebugEnabled()) {
      log.debug("Received message:\n" + XMLUtils.serialize(received));
      log.debug("Control message:\n" + XMLUtils.serialize(source));
    }

    validateXmlTree(
        received,
        source,
        validationContext,
        namespaceContextBuilder.buildContext(receivedMessage, validationContext.getNamespaces()),
        context);
  }
  private void doElementNameValidation(Node received, Node source) {
    // validate element name
    if (log.isDebugEnabled()) {
      log.debug(
          "Validating element: "
              + received.getLocalName()
              + " ("
              + received.getNamespaceURI()
              + ")");
    }

    Assert.isTrue(
        received.getLocalName().equals(source.getLocalName()),
        ValidationUtils.buildValueMismatchErrorMessage(
            "Element names not equal", source.getLocalName(), received.getLocalName()));
  }
  /**
   * Handle attribute node during validation.
   *
   * @param receivedElement
   * @param receivedAttribute
   * @param sourceElement
   * @param validationContext
   */
  private void doAttribute(
      Node receivedElement,
      Node receivedAttribute,
      Node sourceElement,
      XmlMessageValidationContext validationContext,
      NamespaceContext namespaceContext,
      TestContext context) {
    if (receivedAttribute.getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
      return;
    }

    String receivedAttributeName = receivedAttribute.getLocalName();

    if (log.isDebugEnabled()) {
      log.debug(
          "Validating attribute: "
              + receivedAttributeName
              + " ("
              + receivedAttribute.getNamespaceURI()
              + ")");
    }

    NamedNodeMap sourceAttributes = sourceElement.getAttributes();
    Node sourceAttribute =
        sourceAttributes.getNamedItemNS(receivedAttribute.getNamespaceURI(), receivedAttributeName);

    Assert.isTrue(
        sourceAttribute != null,
        "Attribute validation failed for element '"
            + receivedElement.getLocalName()
            + "', unknown attribute "
            + receivedAttributeName
            + " ("
            + receivedAttribute.getNamespaceURI()
            + ")");

    if (XmlValidationUtils.isAttributeIgnored(
        receivedElement,
        receivedAttribute,
        sourceAttribute,
        validationContext.getIgnoreExpressions(),
        namespaceContext)) {
      return;
    }

    String receivedValue = receivedAttribute.getNodeValue();
    String sourceValue = sourceAttribute.getNodeValue();
    if (isValidationMatcherExpression(sourceAttribute)) {
      ValidationMatcherUtils.resolveValidationMatcher(
          sourceAttribute.getNodeName(),
          receivedAttribute.getNodeValue().trim(),
          sourceAttribute.getNodeValue().trim(),
          context);
    } else if (receivedValue.contains(":") && sourceValue.contains(":")) {
      doNamespaceQualifiedAttributeValidation(
          receivedElement, receivedAttribute, sourceElement, sourceAttribute);
    } else {
      Assert.isTrue(
          receivedValue.equals(sourceValue),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Values not equal for attribute '" + receivedAttributeName + "'",
              sourceValue,
              receivedValue));
    }

    if (log.isDebugEnabled()) {
      log.debug("Attribute '" + receivedAttributeName + "'='" + receivedValue + "': OK");
    }
  }
  /**
   * Handle element node.
   *
   * @param received
   * @param source
   * @param validationContext
   */
  private void doElement(
      Node received,
      Node source,
      XmlMessageValidationContext validationContext,
      NamespaceContext namespaceContext,
      TestContext context) {

    doElementNameValidation(received, source);

    doElementNamespaceValidation(received, source);

    // check if element is ignored either by xpath or by ignore placeholder in source message
    if (XmlValidationUtils.isElementIgnored(
        source, received, validationContext.getIgnoreExpressions(), namespaceContext)) {
      return;
    }

    // work on attributes
    if (log.isDebugEnabled()) {
      log.debug("Validating attributes for element: " + received.getLocalName());
    }
    NamedNodeMap receivedAttr = received.getAttributes();
    NamedNodeMap sourceAttr = source.getAttributes();

    Assert.isTrue(
        countAttributes(receivedAttr) == countAttributes(sourceAttr),
        ValidationUtils.buildValueMismatchErrorMessage(
            "Number of attributes not equal for element '" + received.getLocalName() + "'",
            countAttributes(sourceAttr),
            countAttributes(receivedAttr)));

    for (int i = 0; i < receivedAttr.getLength(); i++) {
      doAttribute(
          received, receivedAttr.item(i), source, validationContext, namespaceContext, context);
    }

    // check if validation matcher on element is specified
    if (isValidationMatcherExpression(source)) {
      ValidationMatcherUtils.resolveValidationMatcher(
          source.getNodeName(),
          received.getFirstChild().getNodeValue().trim(),
          source.getFirstChild().getNodeValue().trim(),
          context);
      return;
    }

    // work on child nodes
    NodeList receivedChilds = received.getChildNodes();
    NodeList sourceChilds = source.getChildNodes();

    Assert.isTrue(
        receivedChilds.getLength() == sourceChilds.getLength(),
        ValidationUtils.buildValueMismatchErrorMessage(
            "Number of child elements not equal for element '" + received.getLocalName() + "'",
            sourceChilds.getLength(),
            receivedChilds.getLength()));

    for (int i = 0; i < receivedChilds.getLength(); i++) {
      this.validateXmlTree(
          receivedChilds.item(i),
          sourceChilds.item(i),
          validationContext,
          namespaceContext,
          context);
    }

    if (log.isDebugEnabled()) {
      log.debug(
          "Validation successful for element: "
              + received.getLocalName()
              + " ("
              + received.getNamespaceURI()
              + ")");
    }
  }
  /**
   * Handle document type definition with validation of publicId and systemId.
   *
   * @param received
   * @param source
   * @param validationContext
   * @param namespaceContext
   */
  private void doDocumentTypeDefinition(
      Node received,
      Node source,
      XmlMessageValidationContext validationContext,
      NamespaceContext namespaceContext,
      TestContext context) {

    Assert.isTrue(
        source instanceof DocumentType,
        "Missing document type definition in expected xml fragment");

    DocumentType receivedDTD = (DocumentType) received;
    DocumentType sourceDTD = (DocumentType) source;

    if (log.isDebugEnabled()) {
      log.debug(
          "Validating document type definition: "
              + receivedDTD.getPublicId()
              + " ("
              + receivedDTD.getSystemId()
              + ")");
    }

    if (!StringUtils.hasText(sourceDTD.getPublicId())) {
      Assert.isNull(
          receivedDTD.getPublicId(),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Document type public id not equal",
              sourceDTD.getPublicId(),
              receivedDTD.getPublicId()));
    } else if (sourceDTD.getPublicId().trim().equals(CitrusConstants.IGNORE_PLACEHOLDER)) {
      if (log.isDebugEnabled()) {
        log.debug(
            "Document type public id: '"
                + receivedDTD.getPublicId()
                + "' is ignored by placeholder '"
                + CitrusConstants.IGNORE_PLACEHOLDER
                + "'");
      }
    } else {
      Assert.isTrue(
          StringUtils.hasText(receivedDTD.getPublicId())
              && receivedDTD.getPublicId().equals(sourceDTD.getPublicId()),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Document type public id not equal",
              sourceDTD.getPublicId(),
              receivedDTD.getPublicId()));
    }

    if (!StringUtils.hasText(sourceDTD.getSystemId())) {
      Assert.isNull(
          receivedDTD.getSystemId(),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Document type system id not equal",
              sourceDTD.getSystemId(),
              receivedDTD.getSystemId()));
    } else if (sourceDTD.getSystemId().trim().equals(CitrusConstants.IGNORE_PLACEHOLDER)) {
      if (log.isDebugEnabled()) {
        log.debug(
            "Document type system id: '"
                + receivedDTD.getSystemId()
                + "' is ignored by placeholder '"
                + CitrusConstants.IGNORE_PLACEHOLDER
                + "'");
      }
    } else {
      Assert.isTrue(
          StringUtils.hasText(receivedDTD.getSystemId())
              && receivedDTD.getSystemId().equals(sourceDTD.getSystemId()),
          ValidationUtils.buildValueMismatchErrorMessage(
              "Document type system id not equal",
              sourceDTD.getSystemId(),
              receivedDTD.getSystemId()));
    }

    validateXmlTree(
        received.getNextSibling(),
        source.getNextSibling(),
        validationContext,
        namespaceContext,
        context);
  }