/*
* Copyright 2006-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.consol.citrus.validation.xml;
import com.consol.citrus.CitrusConstants;
import com.consol.citrus.context.TestContext;
import com.consol.citrus.exceptions.*;
import com.consol.citrus.message.*;
import com.consol.citrus.util.XMLUtils;
import com.consol.citrus.validation.*;
import com.consol.citrus.validation.context.ValidationContext;
import com.consol.citrus.validation.matcher.ValidationMatcherUtils;
import com.consol.citrus.xml.XsdSchemaRepository;
import com.consol.citrus.xml.namespace.NamespaceContextBuilder;
import com.consol.citrus.xml.schema.MultiResourceXsdSchema;
import com.consol.citrus.xml.schema.WsdlXsdSchema;
import com.consol.citrus.xml.xpath.XPathExpressionResult;
import com.consol.citrus.xml.xpath.XPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.util.*;
import org.springframework.xml.validation.XmlValidator;
import org.springframework.xml.validation.XmlValidatorFactory;
import org.springframework.xml.xsd.XsdSchema;
import org.w3c.dom.*;
import org.w3c.dom.ls.LSException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
/**
* Default message validator implementation. Working on XML messages
* providing message payload, header and namespace validation.
*
* @author Christoph Deppisch
* @since 2007
*/
public class DomXmlMessageValidator extends AbstractMessageValidator<XmlMessageValidationContext> implements ApplicationContextAware {
/**
* Logger
*/
private static Logger log = LoggerFactory.getLogger(DomXmlMessageValidator.class);
@Autowired(required = false)
private List<XsdSchemaRepository> schemaRepositories = new ArrayList<XsdSchemaRepository>();
@Autowired(required = false)
private NamespaceContextBuilder namespaceContextBuilder = new NamespaceContextBuilder();
/** Root application context this validator is defined in */
private ApplicationContext applicationContext;
/** Transformer factory */
private TransformerFactory transformerFactory = TransformerFactory.newInstance();
/**
* Validates the message with test context and xml validation context.
* @param receivedMessage the message to validate
* @param context the current test context
* @param validationContext the validation context
* @throws ValidationException if validation fails
*/
public void validateMessage(Message receivedMessage, TestContext context,
XmlMessageValidationContext validationContext) throws ValidationException {
log.info("Start XML message validation");
try {
if (validationContext.isSchemaValidationEnabled()) {
validateXMLSchema(receivedMessage, validationContext);
validateDTD(validationContext.getDTDResource(), receivedMessage);
}
validateNamespaces(validationContext.getControlNamespaces(), receivedMessage);
validateMessagePayload(receivedMessage, validationContext, context);
validateMessageElements(receivedMessage, validationContext, context);
Message controlMessage = validationContext.getControlMessage(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);
}
validateMessageHeader(controlMessage.copyHeaders(), receivedMessage.copyHeaders(), 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().toString()));
throw new ValidationException("Validation failed:", e);
} catch (ValidationException ex) {
log.error("Failed to validate:\n" + XMLUtils.prettyPrint(receivedMessage.getPayload().toString()));
throw ex;
}
}
/**
* Validates the message header comparing a control set of header
* elements with the actual message header.
*
* @param controlHeaders
* @param receivedHeaders
* @param context
*/
protected void validateMessageHeader(Map<String, Object> controlHeaders,
Map<String, Object> receivedHeaders,
TestContext context) {
ControlMessageValidator validatorDelegate = new ControlMessageValidator();
validatorDelegate.validateMessageHeader(controlHeaders, receivedHeaders, context);
}
/**
* Validate message payload XML elements.
*
* @param receivedMessage
* @param validationContext
* @param context
*/
protected void validateMessageElements(Message receivedMessage,
XmlMessageValidationContext validationContext, TestContext context) {
if (CollectionUtils.isEmpty(validationContext.getPathValidationExpressions())) { return; }
assertPayloadExists(receivedMessage);
log.info("Start XML elements validation");
Document received = XMLUtils.parseMessagePayload(receivedMessage.getPayload().toString());
NamespaceContext namespaceContext = namespaceContextBuilder.buildContext(
receivedMessage, validationContext.getNamespaces());
for (Entry<String, String> entry : validationContext.getPathValidationExpressions().entrySet()) {
String elementPathExpression = entry.getKey();
String expectedValue = entry.getValue();
String actualValue;
elementPathExpression = context.replaceDynamicContentInString(elementPathExpression);
if (XPathUtils.isXPathExpression(elementPathExpression)) {
XPathExpressionResult resultType = XPathExpressionResult.fromString(
elementPathExpression, XPathExpressionResult.NODE);
elementPathExpression = XPathExpressionResult.cutOffPrefix(elementPathExpression);
//Give ignore elements the chance to prevent the validation in case result type is node
if (resultType.equals(XPathExpressionResult.NODE) &&
isNodeIgnored(XPathUtils.evaluateAsNode(received,
elementPathExpression,
namespaceContext),
validationContext,
namespaceContext)) {
continue;
}
actualValue = XPathUtils.evaluate(received,
elementPathExpression,
namespaceContext,
resultType);
} else {
Node node = XMLUtils.findNodeByName(received, elementPathExpression);
if (node == null) {
throw new UnknownElementException(
"Element ' " + elementPathExpression + "' could not be found in DOM tree");
}
if (isNodeIgnored(node, validationContext, namespaceContext)) {
continue;
}
actualValue = getNodeValue(node);
}
//check if expected value is variable or function (and resolve it, if yes)
expectedValue = context.replaceDynamicContentInString(expectedValue);
//do the validation of actual and expected value for element
validateExpectedActualElements(actualValue, expectedValue, elementPathExpression, context);
if (log.isDebugEnabled()) {
log.debug("Validating element: " + elementPathExpression + "='" + expectedValue + "': OK.");
}
}
log.info("Validation of XML elements finished successfully: All elements OK");
}
/**
* Validate message with a DTD.
*
* @param dtdResource
* @param receivedMessage
*/
protected void validateDTD(Resource dtdResource, Message receivedMessage) {
//TODO implement this
}
/**
* Validate message with a XML schema.
*
* @param receivedMessage
* @param validationContext
*/
protected void validateXMLSchema(Message receivedMessage, XmlMessageValidationContext validationContext) {
if (receivedMessage.getPayload() == null || !StringUtils.hasText(receivedMessage.getPayload().toString())) {
return;
}
try {
Document doc = XMLUtils.parseMessagePayload(receivedMessage.getPayload().toString());
if (!StringUtils.hasText(doc.getFirstChild().getNamespaceURI())) {
return;
}
log.info("Starting XML schema validation ...");
XmlValidator validator = null;
XsdSchemaRepository schemaRepository = null;
if (validationContext.getSchema() != null) {
validator = applicationContext.getBean(validationContext.getSchema(), XsdSchema.class).createValidator();
} else if (validationContext.getSchemaRepository() != null) {
schemaRepository = applicationContext.getBean(validationContext.getSchemaRepository(), XsdSchemaRepository.class);
} else if (schemaRepositories.size() == 1) {
schemaRepository = schemaRepositories.get(0);
} else if (schemaRepositories.size() > 0) {
for (XsdSchemaRepository repository : schemaRepositories) {
if (repository.canValidate(doc)) {
schemaRepository = repository;
}
}
if (schemaRepository == null) {
throw new CitrusRuntimeException(String.format("Failed to find proper schema repository in Spring bean context for validating element '%s(%s)'",
doc.getFirstChild().getLocalName(), doc.getFirstChild().getNamespaceURI()));
}
} else {
log.warn("Neither schema instance nor schema repository defined - skipping XML schema validation");
return;
}
if (schemaRepository != null) {
if (!schemaRepository.canValidate(doc)) {
throw new CitrusRuntimeException(String.format("Unable to find proper XML schema definition for element '%s(%s)' in schema repository '%s'",
doc.getFirstChild().getLocalName(),
doc.getFirstChild().getNamespaceURI(),
schemaRepository.getName()));
}
List<Resource> schemas = new ArrayList<Resource>();
for (XsdSchema xsdSchema : schemaRepository.getSchemas()) {
if (xsdSchema instanceof MultiResourceXsdSchema) {
for (Resource resource : ((MultiResourceXsdSchema) xsdSchema).getSchemas()) {
schemas.add(resource);
}
} else if (xsdSchema instanceof WsdlXsdSchema) {
for (Resource resource : ((WsdlXsdSchema) xsdSchema).getSchemas()) {
schemas.add(resource);
}
} else {
synchronized (transformerFactory) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
transformerFactory.newTransformer().transform(xsdSchema.getSource(), new StreamResult(bos));
} catch (TransformerException e) {
throw new CitrusRuntimeException("Failed to read schema " + xsdSchema.getTargetNamespace(), e);
}
schemas.add(new ByteArrayResource(bos.toByteArray()));
}
}
}
validator = XmlValidatorFactory.createValidator(schemas.toArray(new Resource[schemas.size()]), WsdlXsdSchema.W3C_XML_SCHEMA_NS_URI);
}
SAXParseException[] results = validator.validate(new DOMSource(doc));
if (results.length == 0) {
log.info("Schema of received XML validated OK");
} else {
log.error("Schema validation failed for message:\n" +
XMLUtils.prettyPrint(receivedMessage.getPayload().toString()));
// Report all parsing errors
log.debug("Found " + results.length + " schema validation errors");
StringBuilder errors = new StringBuilder();
for(SAXParseException e : results) {
errors.append(e.toString());
errors.append("\n");
}
log.debug(errors.toString());
throw new ValidationException("Schema validation failed:", results[0]);
}
} catch (IOException e) {
throw new CitrusRuntimeException(e);
} catch (SAXException e) {
throw new CitrusRuntimeException(e);
}
}
/**
* Validate namespaces in message. The method compares namespace declarations in the root
* element of the received message to expected namespaces. Prefixes are important too, so
* differing namespace prefixes will fail the validation.
*
* @param expectedNamespaces
* @param receivedMessage
*/
protected void validateNamespaces(Map<String, String> expectedNamespaces, Message receivedMessage) {
if (CollectionUtils.isEmpty(expectedNamespaces)) { return; }
if (receivedMessage.getPayload() == null || !StringUtils.hasText(receivedMessage.getPayload().toString())) {
throw new ValidationException("Unable to validate message namespaces - receive message payload was empty");
}
log.info("Start XML namespace validation");
Document received = XMLUtils.parseMessagePayload(receivedMessage.getPayload().toString());
Map<String, String> foundNamespaces = XMLUtils.lookupNamespaces(receivedMessage.getPayload().toString());
if (foundNamespaces.size() != expectedNamespaces.size()) {
throw new ValidationException("Number of namespace declarations not equal for node " +
XMLUtils.getNodesPathName(received.getFirstChild()) + " found " +
foundNamespaces.size() + " expected " + expectedNamespaces.size());
}
for (Entry<String, String> entry : expectedNamespaces.entrySet()) {
String namespace = entry.getKey();
String url = entry.getValue();
if (foundNamespaces.containsKey(namespace)) {
if (!foundNamespaces.get(namespace).equals(url)) {
throw new ValidationException("Namespace '" + namespace +
"' values not equal: found '" + foundNamespaces.get(namespace) +
"' expected '" + url + "' in reference node " +
XMLUtils.getNodesPathName(received.getFirstChild()));
} else {
log.info("Validating namespace " + namespace + " value as expected " + url + " - value OK");
}
} else {
throw new ValidationException("Missing namespace " + namespace + "(" + url + ") in node " +
XMLUtils.getNodesPathName(received.getFirstChild()));
}
}
log.info("XML namespace validation finished successfully: All values OK");
}
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()));
}
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));
}
}
/**
* Validate message payloads by comparing to a control message.
*
* @param receivedMessage
* @param validationContext
* @param context
*/
protected void validateMessagePayload(Message receivedMessage, XmlMessageValidationContext validationContext,
TestContext context) {
Message controlMessage = validationContext.getControlMessage(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().toString();
if (receivedMessage.getPayload() == null || !StringUtils.hasText(receivedMessage.getPayload().toString())) {
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().toString());
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);
}
/**
* Validates XML header fragment data.
* @param receivedHeaderData
* @param controlHeaderData
* @param validationContext
* @param context
*/
private void validateXmlHeaderFragment(String receivedHeaderData, String controlHeaderData,
XmlMessageValidationContext validationContext, TestContext context) {
log.info("Start XML header data validation ...");
Document received = XMLUtils.parseMessagePayload(receivedHeaderData);
Document source = XMLUtils.parseMessagePayload(controlHeaderData);
XMLUtils.stripWhitespaceNodes(received);
XMLUtils.stripWhitespaceNodes(source);
if (log.isDebugEnabled()) {
log.debug("Received header data:\n" + XMLUtils.serialize(received));
log.debug("Control header data:\n" + XMLUtils.serialize(source));
}
validateXmlTree(received, source, validationContext,
namespaceContextBuilder.buildContext(new DefaultMessage(receivedHeaderData), validationContext.getNamespaces()),
context);
}
/**
* Walk the XML tree and validate all nodes.
*
* @param received
* @param source
* @param validationContext
*/
private void validateXmlTree(Node received, Node source,
XmlMessageValidationContext validationContext, NamespaceContext namespaceContext, TestContext context) {
switch(received.getNodeType()) {
case Node.DOCUMENT_TYPE_NODE:
doDocumentTypeDefinition(received, source, validationContext, namespaceContext, context);
break;
case Node.DOCUMENT_NODE:
validateXmlTree(received.getFirstChild(), source.getFirstChild(),
validationContext, namespaceContext, context);
break;
case Node.ELEMENT_NODE:
doElement(received, source, validationContext, namespaceContext, context);
break;
case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE:
doText(received, source);
break;
case Node.ATTRIBUTE_NODE:
throw new IllegalStateException();
case Node.COMMENT_NODE:
doComment(received);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
doPI(received);
break;
}
}
/**
* 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);
}
/**
* 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(isElementNodeIgnored(source, received, validationContext, 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 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");
}
}
/**
* 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 ((StringUtils.hasText(sourceAttribute.getNodeValue()) && sourceAttribute.getNodeValue().trim().equals(CitrusConstants.IGNORE_PLACEHOLDER))
|| isAttributeIgnored(receivedElement, receivedAttribute, validationContext, namespaceContext)) {
if (log.isDebugEnabled()) {
log.debug("Attribute '" + receivedAttributeName + "' is on ignore list - skipped value validation");
}
return;
} else if (isValidationMatcherExpression(sourceAttribute)) {
ValidationMatcherUtils.resolveValidationMatcher(sourceAttribute.getNodeName(),
receivedAttribute.getNodeValue().trim(),
sourceAttribute.getNodeValue().trim(),
context);
if (log.isDebugEnabled()) {
log.debug("Attribute '" + receivedAttributeName + "'='" + sourceAttribute.getNodeValue() + "': OK");
}
return;
}
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 '" + receivedAttributeName + "' describes namespace qualified attribute value," +
" control value '" + sourceValue + "' does not");
}
}
}
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 comment node during validation.
*
* @param received
*/
private void doComment(Node received) {
log.info("Ignored comment node (" + received.getNodeValue() + ")");
}
/**
* Handle processing instruction during validation.
*
* @param received
*/
private void doPI(Node received) {
log.info("Ignored processing instruction (" + received.getLocalName() + "=" + received.getNodeValue() + ")");
}
/**
* Counts the attributenode for an element (xmlns attributes ignored)
* @param attributesR attributesMap
* @return number of attributes
*/
private int countAttributes(NamedNodeMap attributesR) {
int cntAttributes = 0;
for (int i = 0; i < attributesR.getLength(); i++) {
if (!attributesR.item(i).getNodeName().startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
cntAttributes++;
}
}
return cntAttributes;
}
/**
* Checks whether the current attribute is ignored.
* @param elementNode
* @param attributeNode
* @param validationContext
* @return
*/
private boolean isAttributeIgnored(Node elementNode, Node attributeNode,
XmlMessageValidationContext validationContext, NamespaceContext namespaceContext) {
Set<String> ignoreMessageElements = validationContext.getIgnoreExpressions();
if (CollectionUtils.isEmpty(ignoreMessageElements)) {
return false;
}
/** This is the faster version, but then the ignoreValue name must be
* the full path name like: Numbers.NumberItem.AreaCode
*/
if (ignoreMessageElements.contains(XMLUtils.getNodesPathName(elementNode) + "." + attributeNode.getNodeName())) {
return true;
}
/** This is the slower version, but here the ignoreValues can be
* the short path name like only: AreaCode
*
* If there are more nodes with the same short name,
* the first one will match, eg. if there are:
* Numbers1.NumberItem.AreaCode
* Numbers2.NumberItem.AreaCode
* And ignoreValues contains just: AreaCode
* the only first Node: Numbers1.NumberItem.AreaCode will be ignored.
*/
for (String expression : ignoreMessageElements) {
Node foundAttributeNode = XMLUtils.findNodeByName(elementNode.getOwnerDocument(), expression);
if (foundAttributeNode != null && attributeNode.isSameNode(foundAttributeNode)) {
return true;
}
}
/** This is the XPath version using XPath expressions in
* ignoreValues to identify nodes to be ignored
*/
for (String expression : ignoreMessageElements) {
if (XPathUtils.isXPathExpression(expression)) {
Node foundAttributeNode = XPathUtils.evaluateAsNode(elementNode.getOwnerDocument(),
expression,
namespaceContext);
if (foundAttributeNode != null && foundAttributeNode.isSameNode(attributeNode)) {
return true;
}
}
}
return false;
}
/**
* Checks if given element node is either on ignore list or
* contains @ignore@ tag inside control message
* @param source
* @param received
* @param validationContext
* @param namespaceContext
* @return
*/
private boolean isElementNodeIgnored(Node source, Node received, XmlMessageValidationContext validationContext,
NamespaceContext namespaceContext) {
if (isNodeIgnored(received, validationContext, namespaceContext)) {
if (log.isDebugEnabled()) {
log.debug("Element: '" + received.getLocalName() + "' is on ignore list - skipped validation");
}
return true;
} else if (source.getFirstChild() != null &&
StringUtils.hasText(source.getFirstChild().getNodeValue()) &&
source.getFirstChild().getNodeValue().trim().equals(CitrusConstants.IGNORE_PLACEHOLDER)) {
if (log.isDebugEnabled()) {
log.debug("Element: '" + received.getLocalName() + "' is ignored by placeholder '" +
CitrusConstants.IGNORE_PLACEHOLDER + "'");
}
return true;
}
return false;
}
/**
* Checks whether the node is ignored by node path expression or xpath expression.
* @param node
* @param validationContext
* @return
*/
private boolean isNodeIgnored(final Node node, XmlMessageValidationContext validationContext,
NamespaceContext namespaceContext) {
Set<String> ignoreMessageElements = validationContext.getIgnoreExpressions();
if (CollectionUtils.isEmpty(ignoreMessageElements)) {
return false;
}
/** This is the faster version, but then the ignoreValue name must be
* the full path name like: Numbers.NumberItem.AreaCode
*/
if (ignoreMessageElements.contains(XMLUtils.getNodesPathName(node))) {
return true;
}
/** This is the slower version, but here the ignoreValues can be
* the short path name like only: AreaCode
*
* If there are more nodes with the same short name,
* the first one will match, eg. if there are:
* Numbers1.NumberItem.AreaCode
* Numbers2.NumberItem.AreaCode
* And ignoreValues contains just: AreaCode
* the only first Node: Numbers1.NumberItem.AreaCode will be ignored.
*/
for (String expression : ignoreMessageElements) {
if (node == XMLUtils.findNodeByName(node.getOwnerDocument(), expression)) {
return true;
}
}
/** This is the XPath version using XPath expressions in
* ignoreValues to identify nodes to be ignored
*/
for (String expression : ignoreMessageElements) {
if (XPathUtils.isXPathExpression(expression)) {
Node foundNode = XPathUtils.evaluateAsNode(node.getOwnerDocument(),
expression,
namespaceContext);
if (foundNode != null && foundNode.isSameNode(node)) {
return true;
}
}
}
return false;
}
@Override
public XmlMessageValidationContext findValidationContext(List<ValidationContext> validationContexts) {
for (ValidationContext validationContext : validationContexts) {
if (validationContext instanceof XmlMessageValidationContext) {
return (XmlMessageValidationContext) validationContext;
}
}
return null;
}
/**
* Asserts that a message contains payload
* @param message the message to check for payload
* @throws ValidationException if message does not contain payload
*/
private void assertPayloadExists(Message message) throws ValidationException {
if (message.getPayload() == null || !StringUtils.hasText(message.getPayload().toString())) {
throw new ValidationException("Unable to validate message elements - receive message payload was empty");
}
}
/**
* Resolves an XML node's value
* @param node
* @return node's string value
*/
private String getNodeValue(Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE && node.getFirstChild() != null) {
return node.getFirstChild().getNodeValue();
} else {
return node.getNodeValue();
}
}
/**
* Validates actual against expected value of element
* @param actualValue
* @param expectedValue
* @param elementPathExpression
* @param context
* @throws ValidationException if validation fails
*/
private void validateExpectedActualElements(String actualValue, String expectedValue, String elementPathExpression, TestContext context)
throws ValidationException {
try {
if (actualValue != null) {
Assert.isTrue(expectedValue != null,
ValidationUtils.buildValueMismatchErrorMessage(
"Values not equal for element '" + elementPathExpression + "'", null, actualValue));
//check if validation matcher on element is specified
if (ValidationMatcherUtils.isValidationMatcherExpression(expectedValue)) {
ValidationMatcherUtils.resolveValidationMatcher(elementPathExpression,
actualValue,
expectedValue,
context);
}
else {
Assert.isTrue(actualValue.equals(expectedValue),
ValidationUtils.buildValueMismatchErrorMessage(
"Values not equal for element '" + elementPathExpression + "'", expectedValue, actualValue));
}
} else {
Assert.isTrue(expectedValue == null || expectedValue.length() == 0,
ValidationUtils.buildValueMismatchErrorMessage(
"Values not equal for element '" + elementPathExpression + "'", expectedValue, null));
}
} catch (IllegalArgumentException e) {
throw new ValidationException("Validation failed:", e);
}
}
/**
* Checks whether the given node contains a validation matcher
* @param node
* @return true if node value contains validation matcher, false if not
*/
private boolean isValidationMatcherExpression(Node node) {
switch (node.getNodeType()) {
case Node.ELEMENT_NODE:
return node.getFirstChild() != null &&
StringUtils.hasText(node.getFirstChild().getNodeValue()) &&
ValidationMatcherUtils.isValidationMatcherExpression(node.getFirstChild().getNodeValue().trim());
case Node.ATTRIBUTE_NODE:
return StringUtils.hasText(node.getNodeValue()) &&
ValidationMatcherUtils.isValidationMatcherExpression(node.getNodeValue().trim());
default: return false; //validation matchers makes no sense
}
}
@Override
public boolean supportsMessageType(String messageType, Message message) {
return messageType.equalsIgnoreCase(MessageType.XML.toString());
}
/**
* Set the schema repository holding all known schema definition files.
* @param schemaRepository the schemaRepository to set
*/
public void addSchemaRepository(XsdSchemaRepository schemaRepository) {
if (schemaRepositories == null) {
schemaRepositories = new ArrayList<XsdSchemaRepository>();
}
schemaRepositories.add(schemaRepository);
}
/**
* {@inheritDoc}
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}