| /******************************************************************************* |
| * 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; |
| } |
| } |