blob: cf4814160853f88a212ff011be6cfaf4a3306138 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.rest;
import static org.junit.Assert.fail;
import java.text.MessageFormat;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.DifferenceConstants;
import org.custommonkey.xmlunit.DifferenceListener;
import org.custommonkey.xmlunit.NodeDetail;
import org.eclipse.skalli.commons.ComparatorUtils;
import org.w3c.dom.Node;
@SuppressWarnings("nls")
public class ProjectsV1V2Diff implements DifferenceListener {
protected final Pattern ROOT_XPATH_PATTERN = getPattern("");
protected final Pattern EXTENSION_XPATH_PATTERN = getPattern("/extensions\\[1\\]/.+\\[1\\]");
protected final Pattern LINK_XPATH_PATTERN = getPattern("/link\\[\\d+\\]");
protected final Pattern LINK_HREF_XPATH_PATTERN = getPattern("/link\\[\\d+\\]/@href");
protected final Pattern LINK_REL_XPATH_PATTERN = getPattern("/link\\[\\d+\\]/@rel");
protected final Pattern PHASE_XPATH_PATTERN = getPattern("/phase\\[1\\]");
protected final Pattern REGISTERED_XPATH_PATTERN = getPattern("/registered\\[1\\]");
protected final Pattern DESCRIPTION_XPATH_PATTERN = getPattern("/description\\[1\\]");
protected final Pattern DESCRIPTION_FORMAT_XPATH_PATTERN = getPattern("/descriptionFormat\\[1\\]");
protected final Pattern DESCRIPTION_TEXT_XPATH_PATTERN = getPattern("/description\\[1\\]/text\\(\\)\\[1\\]");
protected final Pattern SUBPROJECTS_XPATH_PATTERN = getPattern("/subprojects\\[1\\]");
protected final Pattern EXTENSIONS_XPATH_PATTERN = getPattern("/extensions\\[1\\]");
protected final Pattern MEMBERS_XPATH_PATTERN = getPattern("/members\\[1\\]");
protected final String webLocator;
public ProjectsV1V2Diff(String webLocator) {
this.webLocator = webLocator;
}
@Override
public int differenceFound(Difference difference) {
int result = RETURN_ACCEPT_DIFFERENCE;
NodeDetail expected = difference.getControlNodeDetail();
NodeDetail actual = difference.getTestNodeDetail();
switch (difference.getId()) {
case DifferenceConstants.ELEMENT_NUM_ATTRIBUTES_ID:
// <project> tag has an additional "lastModifiedMillis" attribute in the new API
if (equalsAndMatchesAnyXPath(expected, actual, ROOT_XPATH_PATTERN, EXTENSION_XPATH_PATTERN)
&& (valueToInt(actual) == valueToInt(expected) + 1)) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
break;
case DifferenceConstants.ATTR_NAME_NOT_FOUND_ID:
// <project> tag has an additional "lastModifiedMillis" attribute in the new API
if (equalsAndMatchesAnyXPath(expected, actual, ROOT_XPATH_PATTERN, EXTENSION_XPATH_PATTERN)
&& equalsValueNull(expected)
&& "lastModifiedMillis".equals(actual.getValue())) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
};
break;
case DifferenceConstants.CHILD_NODELIST_LENGTH_ID:
// <project> tag has always an additional <descriptionFormnat> tag in the new API, but never in the old API;
// <project> tag has always an additional <link rel=permalink> in the new API, but never in the old API;
// <project> tag has always a <link rel=subprojects> in the new API,
// but only if it has also a <subprojects> tag in the old API;
// <project> tag has always a <members> tag in the new API (even if empty),
// but only if it was non-empty in the old API;
// <project> tag has always a <subprojects> tag in the new API (even if empty),
// but only if it was non-empty in the old API;
// therefore, we have at least 2 additional tags, but never more than 4
if (equalsAndMatchesAnyXPath(expected, actual, ROOT_XPATH_PATTERN)
&& (valueToInt(actual) >= valueToInt(expected) + 3)
&& (valueToInt(actual) <= valueToInt(expected) + 5)) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
break;
case DifferenceConstants.CHILD_NODELIST_SEQUENCE_ID:
// some tags have changed their position in the new API compared to the old API, e.g.
// all <link> tags are now grouped together, and the <phase>, <registered> and <description>
// tags are now rendered before the links.
if (equalsAndMatchesAnyXPath(expected, actual, LINK_XPATH_PATTERN, PHASE_XPATH_PATTERN,
REGISTERED_XPATH_PATTERN, DESCRIPTION_XPATH_PATTERN, MEMBERS_XPATH_PATTERN,
SUBPROJECTS_XPATH_PATTERN, EXTENSIONS_XPATH_PATTERN)) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
break;
case DifferenceConstants.ATTR_VALUE_ID:
// The <link> tags have different ordering and position within the <project> tag
if (equalsAndMatchesAnyXPath(expected, actual, LINK_HREF_XPATH_PATTERN,LINK_REL_XPATH_PATTERN)) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
break;
case DifferenceConstants.CHILD_NODE_NOT_FOUND_ID:
// in the new API we may have additional <descriptionFormat>, <link>, <subprojects> and <members> tags,
// which may no be there in the old API
if (equalsValueNull(expected) && (
matchesAnyXPath(actual, DESCRIPTION_FORMAT_XPATH_PATTERN) && "descriptionFormat".equals(actual.getValue())
|| matchesAnyXPath(actual, LINK_XPATH_PATTERN) && "link".equals(actual.getValue())
|| matchesAnyXPath(actual, SUBPROJECTS_XPATH_PATTERN) && "subprojects".equals(actual.getValue())
|| matchesAnyXPath(actual, MEMBERS_XPATH_PATTERN) && "members".equals(actual.getValue()))) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
}
break;
case DifferenceConstants.TEXT_VALUE_ID:
// old and new API render different file endings: the new API renders a single \n #xA),
// while the old API preferred \r\n (#xD #xA)
if (equalsAndMatchesAnyXPath(expected, actual, DESCRIPTION_TEXT_XPATH_PATTERN)
&& equalsValueIgnoreLineEndings(expected, actual)) {
result = RETURN_IGNORE_DIFFERENCE_NODES_IDENTICAL;
};
break;
default:
result = RETURN_ACCEPT_DIFFERENCE;
}
return result;
}
@Override
public void skippedComparison(Node expected, Node actual) {
fail(MessageFormat.format(
"comparison skipped because the node types are not comparable: {0} (type: {1}) - {2} (type: {3})",
expected.getNodeName(), expected.getNodeType(), actual.getNodeName(), actual.getNodeType()));
}
protected String getRootPath() {
return "/projects\\[1\\]";
}
protected Pattern getPattern(String relPath) {
return Pattern.compile(MessageFormat.format("^{0}/project\\[\\d+\\]{1}$", getRootPath(), relPath));
}
protected Pattern getExtPattern(String relPath) {
return Pattern.compile(MessageFormat.format("^{0}/project\\[\\d+\\]/extensions\\[1\\]{1}$", getRootPath(), relPath));
}
protected boolean equalsXPath(NodeDetail expected, NodeDetail actual) {
return ComparatorUtils.equals(expected.getXpathLocation(), actual.getXpathLocation());
}
protected boolean matchesAnyXPath(NodeDetail node, Pattern... xPathPatterns) {
for (Pattern xPathPattern: xPathPatterns) {
if (xPathPattern.matcher(node.getXpathLocation()).matches()) {
return true;
}
}
return false;
}
protected boolean equalsAndMatchesAnyXPath(NodeDetail expected, NodeDetail actual, Pattern... xPathPatterns) {
for (Pattern xPathPattern : xPathPatterns) {
if (equalsXPath(expected, actual)
&& matchesAnyXPath(expected, xPathPattern)
&& matchesAnyXPath(actual, xPathPattern)) {
return true;
}
}
return false;
}
protected boolean equalsValueInt(NodeDetail node, int value) {
return isValueInt(node) && valueToInt(node) == value;
}
protected boolean isValueInt(NodeDetail node) {
return NumberUtils.isNumber(node.getValue());
}
protected int valueToInt(NodeDetail node) {
return NumberUtils.toInt(node.getValue());
}
protected boolean equalsValueNull(NodeDetail node) {
return "null".equals(node.getValue());
}
protected boolean equalsValueIgnoreLineEndings(NodeDetail expected, NodeDetail actual) {
return normalized(expected).equals(normalized(actual));
}
protected String normalized(NodeDetail node) {
return StringUtils.replace(node.getValue(), "\r\n", "\n");
}
protected boolean hasEmptyChildNode(NodeDetail nodeDetails, String name) {
Node node = nodeDetails.getNode().getFirstChild();
while (node != null) {
if (name.equals(node.getNodeName())) {
return !node.hasChildNodes();
}
node = node.getNextSibling();
}
return false;
}
protected boolean hasBooleanChildNode(NodeDetail nodeDetails, String name) {
Node node = nodeDetails.getNode().getFirstChild();
while (node != null) {
if (name.equals(node.getNodeName())) {
return "true".equals(node.getTextContent()) || "false".equals(node.getTextContent());
}
node = node.getNextSibling();
}
return false;
}
protected boolean hasAnyChildNode(NodeDetail nodeDetails, Set<String> names) {
Node node = nodeDetails.getNode().getFirstChild();
while (node != null) {
if (names.contains(node.getNodeName())) {
return true;
}
node = node.getNextSibling();
}
return false;
}
}