Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.hl7.fhir.r5.test.utils.CompareUtilities Maven / Gradle / Ivy
package org.hl7.fhir.r5.test.utils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Constants;
import org.hl7.fhir.utilities.*;
import org.hl7.fhir.utilities.filesystem.CSFile;
import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
import org.hl7.fhir.utilities.json.JsonUtilities;
import org.hl7.fhir.utilities.json.model.JsonArray;
import org.hl7.fhir.utilities.json.model.JsonElement;
import org.hl7.fhir.utilities.json.model.JsonNull;
import org.hl7.fhir.utilities.json.model.JsonObject;
import org.hl7.fhir.utilities.json.model.JsonPrimitive;
import org.hl7.fhir.utilities.json.model.JsonProperty;
import org.hl7.fhir.utilities.json.parser.JsonParser;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CompareUtilities extends BaseTestingUtilities {
private static final boolean SHOW_DIFF = false;
private JsonObject externals;
private Map variables;
private Set modes;
private boolean patternMode;
public CompareUtilities() {
super();
this.variables = new HashMap();
}
public CompareUtilities(Set modes) {
super();
this.modes = modes;
this.variables = new HashMap();
}
public CompareUtilities(Set modes, JsonObject externals) {
super();
this.modes = modes;
this.externals = externals;
this.variables = new HashMap();
}
public CompareUtilities(Set modes, JsonObject externals, Map variables) {
super();
this.externals = externals;
this.variables = variables;
}
/**
* in pattern mode, the comparison is only looking to find the expected properties. anything else is ignored
* @return
*/
public boolean isPatternMode() {
return patternMode;
}
public CompareUtilities setPatternMode(boolean patternMode) {
this.patternMode = patternMode;
return this;
}
public String createNotEqualMessage(String id, final String message, final String expected, final String actual) {
if (patternMode) {
return new StringBuilder()
.append(message).append(". ")
.append("Expected:").append(presentExpected(expected)).append(" for "+id).append(". ")
.append("Actual :").append("\""+actual+"\"").toString();
} else {
return new StringBuilder()
.append(message).append('\n')
.append("Expected:").append(presentExpected(expected)).append(" for "+id).append('\n')
.append("Actual :").append("\""+actual+"\"").toString();
}
}
private String presentExpected(String expected) {
if (expected == null) {
return "null";
} else if (expected.startsWith("$") && expected.endsWith("$")) {
if (expected.startsWith("$choice:")) {
return "Contains one of "+readChoices(expected.substring(8, expected.length()-1)).toString();
} else if (expected.startsWith("$fragments:")) {
List fragments = readChoices(expected.substring(11, expected.length()-1));
return "Contains all of "+fragments.toString();
} else if (expected.startsWith("$external:")) {
String[] cmd = expected.substring(1, expected.length() - 1).split(":");
if (externals != null) {
String s = externals.asString(cmd[1]);
return "\""+s+"\" (Ext)";
} else {
List fragments = readChoices(cmd[2]);
return "Contains all of "+fragments.toString()+" (because no external string provided for "+cmd[1]+")";
}
} else {
switch (expected) {
case "$$" : return "$$";
case "$instant$": return "\"An Instant\"";
case "$date$": return "\"A date\"";
case "$uuid$": return "\"A Uuid\"";
case "$string$": return "\"A string\"";
case "$id$": return "\"An Id\"";
case "$url$": return "\"A URL\"";
case "$token$": return "\"A Token\"";
case "$version$": return variables.containsKey("version") ? variables.get("version") : "(anything)";
case "$semver$": return "A semver";
default: return "Unhandled template: "+expected;
}
}
} else {
return "\""+expected+"\"";
}
}
public String checkXMLIsSame(String id, InputStream expected, InputStream actual) throws Exception {
String result = compareXml(id, expected, actual);
return result;
}
public String checkXMLIsSame(String id, String expected, String actual) throws Exception {
String result = compareXml(id, expected, actual);
if (result != null && SHOW_DIFF) {
String diff = getDiffTool();
if (diff != null && ManagedFileAccess.file(diff).exists() || Utilities.isToken(diff)) {
Runtime.getRuntime().exec(new String[]{diff, expected, actual});
}
}
return result;
}
private String getDiffTool() throws IOException {
if (FhirSettings.hasDiffToolPath()) {
return FhirSettings.getDiffToolPath();
} else if (System.getenv("ProgramFiles") != null) {
return Utilities.path(System.getenv("ProgramFiles"), "WinMerge", "WinMergeU.exe");
} else {
return null;
}
}
private String compareXml(String id, InputStream expected, InputStream actual) throws Exception {
return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement());
}
private String compareXml(String id, String expected, String actual) throws Exception {
return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement());
}
private String compareElements(String id, String path, Element expectedElement, Element actualElement) {
if (!namespacesMatch(expectedElement.getNamespaceURI(), actualElement.getNamespaceURI()))
return createNotEqualMessage(id, "Namespaces differ at " + path, expectedElement.getNamespaceURI(), actualElement.getNamespaceURI());
if (!expectedElement.getLocalName().equals(actualElement.getLocalName()))
return createNotEqualMessage(id, "Names differ at " + path , expectedElement.getLocalName(), actualElement.getLocalName());
path = path + "/" + expectedElement.getLocalName();
String s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes());
if (!Utilities.noString(s))
return s;
s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes());
if (!Utilities.noString(s))
return s;
Node expectedChild = expectedElement.getFirstChild();
Node actualChild = actualElement.getFirstChild();
expectedChild = skipBlankText(expectedChild);
actualChild = skipBlankText(actualChild);
while (expectedChild != null && actualChild != null) {
if (expectedChild.getNodeType() != actualChild.getNodeType())
return createNotEqualMessage(id, "node type mismatch in children of " + path, Short.toString(expectedElement.getNodeType()), Short.toString(actualElement.getNodeType()));
if (expectedChild.getNodeType() == Node.TEXT_NODE) {
if (!normalise(expectedChild.getTextContent()).equals(normalise(actualChild.getTextContent())))
return createNotEqualMessage(id, "Text differs at " + path, normalise(expectedChild.getTextContent()).toString(), normalise(actualChild.getTextContent()).toString());
} else if (expectedChild.getNodeType() == Node.ELEMENT_NODE) {
s = compareElements(id, path, (Element) expectedChild, (Element) actualChild);
if (!Utilities.noString(s))
return s;
}
expectedChild = skipBlankText(expectedChild.getNextSibling());
actualChild = skipBlankText(actualChild.getNextSibling());
}
if (expectedChild != null)
return "node mismatch - more nodes in actual in children of " + path;
if (actualChild != null)
return "node mismatch - more nodes in expected in children of " + path;
return null;
}
private boolean namespacesMatch(String ns1, String ns2) {
return ns1 == null ? ns2 == null : ns1.equals(ns2);
}
private String normalise(String text) {
String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
while (result.contains(" "))
result = result.replace(" ", " ");
return result;
}
private String compareAttributes(String id, String path, NamedNodeMap expected, NamedNodeMap actual) {
for (int i = 0; i < expected.getLength(); i++) {
Node expectedNode = expected.item(i);
String expectedNodeName = expectedNode.getNodeName();
if (!(expectedNodeName.equals("xmlns") || expectedNodeName.startsWith("xmlns:"))) {
Node actualNode = actual.getNamedItem(expectedNodeName);
if (actualNode == null)
return "Attributes differ at " + path + ": missing attribute " + expectedNodeName;
if (!normalise(expectedNode.getTextContent()).equals(normalise(actualNode.getTextContent()))) {
byte[] b1 = unBase64(expectedNode.getTextContent());
byte[] b2 = unBase64(actualNode.getTextContent());
if (!sameBytes(b1, b2))
return createNotEqualMessage(id, "Attributes differ at " + path, normalise(expectedNode.getTextContent()).toString(), normalise(actualNode.getTextContent()).toString()) ;
}
}
}
return null;
}
private boolean sameBytes(byte[] b1, byte[] b2) {
if (b1.length == 0 || b2.length == 0)
return false;
if (b1.length != b2.length)
return false;
for (int i = 0; i < b1.length; i++)
if (b1[i] != b2[i])
return false;
return true;
}
private byte[] unBase64(String text) {
return Base64.decodeBase64(text);
}
private Node skipBlankText(Node node) {
while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && StringUtils.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE)))
node = node.getNextSibling();
return node;
}
private Document loadXml(String fn) throws Exception {
return loadXml(ManagedFileAccess.inStream(fn));
}
private Document loadXml(InputStream fn) throws Exception {
DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(fn);
}
public String checkJsonSrcIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException {
return checkJsonSrcIsSame(id, expected, actual, true);
}
public String checkJsonSrcIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException {
String result = compareJsonSrc(id, expectedString, actualString);
if (result != null && SHOW_DIFF && showDiff) {
String diff = null;
if (System.getProperty("os.name").contains("Linux"))
diff = Utilities.path("/", "usr", "bin", "meld");
else if (System.getenv("ProgramFiles(X86)") != null) {
if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
else if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
}
if (diff == null || diff.isEmpty())
return result;
List command = new ArrayList();
String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json");
String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json");
FileUtilities.stringToFile(expectedString, expected);
FileUtilities.stringToFile(actualString, actual);
command.add(diff);
if (diff.toLowerCase().contains("meld"))
command.add("--newtab");
command.add(expected);
command.add(actual);
ProcessBuilder builder = new ProcessBuilder(command);
builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]")));
builder.start();
}
return result;
}
public String checkJsonIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException {
String result = compareJson(id, expected, actual);
if (result != null && SHOW_DIFF) {
String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
List command = new ArrayList();
command.add("\"" + diff + "\" \"" + expected + "\" \"" + actual + "\"");
ProcessBuilder builder = new ProcessBuilder(command);
builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]")));
builder.start();
}
return result;
}
private String compareJsonSrc(String id, String expected, String actual) throws FileNotFoundException, IOException {
JsonObject actualJsonObject = JsonParser.parseObject(actual);
JsonObject expectedJsonObject = JsonParser.parseObject(expected);
return compareObjects(id, "", expectedJsonObject, actualJsonObject);
}
private String compareJson(String id, String expected, String actual) throws FileNotFoundException, IOException {
JsonObject actualJsonObject = JsonParser.parseObject(FileUtilities.fileToString(actual));
JsonObject expectedJsonObject = JsonParser.parseObject(FileUtilities.fileToString(expected));
return compareObjects(id, "", expectedJsonObject, actualJsonObject);
}
private String compareObjects(String id, String path, JsonObject expectedJsonObject, JsonObject actualJsonObject) {
List optionals = listOptionals(expectedJsonObject);
List countOnlys = listCountOnlys(expectedJsonObject);
for (JsonProperty en : actualJsonObject.getProperties()) {
String n = en.getName();
if (!n.equals("fhir_comments")) {
if (expectedJsonObject.has(n)) {
String s = compareNodes(id, path + '.' + n, expectedJsonObject.get(n), en.getValue(), countOnlys.contains(n), n, actualJsonObject);
if (!Utilities.noString(s))
return s;
} else if (!patternMode) {
return "properties differ at " + path + ": unexpected property " + n;
}
}
}
for (JsonProperty en : expectedJsonObject.getProperties()) {
String n = en.getName();
if (!n.equals("fhir_comments") && !isOptional(n, optionals)) {
if (!actualJsonObject.has(n) && !allOptional(en.getValue()))
return "properties differ at " + path + ": missing property " + n;
}
}
return null;
}
private boolean isOptional(String n, List optionals) {
return n.equals("$optional$") || optionals.contains("*") || optionals.contains(n);
}
private boolean allOptional(JsonElement value) {
if (value.isJsonArray()) {
JsonArray a = value.asJsonArray();
for (JsonElement e : a) {
if (e.isJsonObject()) {
JsonObject o = e.asJsonObject();
if (!o.has("$optional$")) {
return false;
}
} else {
// nothing
}
}
return true;
} else {
return false;
}
}
private List listOptionals(JsonObject expectedJsonObject) {
List res = new ArrayList<>();
if (expectedJsonObject.has("$optional-properties$")) {
res.add("$optional-properties$");
res.add("$count-arrays$");
for (String s : expectedJsonObject.getStrings("$optional-properties$")) {
res.add(s);
}
}
return res;
}
private List listCountOnlys(JsonObject expectedJsonObject) {
List res = new ArrayList<>();
if (expectedJsonObject.has("$count-arrays$")) {
for (String s : expectedJsonObject.getStrings("$count-arrays$")) {
res.add(s);
}
}
return res;
}
private String compareNodes(String id, String path, JsonElement expectedJsonElement, JsonElement actualJsonElement, boolean countOnly, String name, JsonObject parent) {
if (!(expectedJsonElement instanceof JsonPrimitive && actualJsonElement instanceof JsonPrimitive)) {
if (actualJsonElement.getClass() != expectedJsonElement.getClass()) {
return createNotEqualMessage(id, "properties differ at " + path, expectedJsonElement.getClass().getName(), actualJsonElement.getClass().getName());
}
}
if (actualJsonElement instanceof JsonPrimitive) {
JsonPrimitive actualJsonPrimitive = (JsonPrimitive) actualJsonElement;
JsonPrimitive expectedJsonPrimitive = (JsonPrimitive) expectedJsonElement;
if (actualJsonPrimitive.isJsonBoolean() && expectedJsonPrimitive.isJsonBoolean()) {
if (actualJsonPrimitive.asBoolean() != expectedJsonPrimitive.asBoolean())
return createNotEqualMessage(id, "boolean property values differ at " + path , expectedJsonPrimitive.asString(), actualJsonPrimitive.asString());
} else if (actualJsonPrimitive.isJsonString() && expectedJsonPrimitive.isJsonString()) {
String actualJsonString = actualJsonPrimitive.asString();
String expectedJsonString = expectedJsonPrimitive.asString();
if (!(actualJsonString.contains(" es || as < expectedMin) {
return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as));
}
int c = 0;
for (int i = 0; i < es; i++) {
if (c >= as) {
if (i >= es - oc && isOptional(expectedArray.get(i), name, parent)) {
return null; // this is OK
} else {
return "One or more array items did not match at "+path+" starting at index "+i;
}
}
String s = compareNodes(id, path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(c), false, null, null);
if (!Utilities.noString(s) && !isOptional(expectedArray.get(i), name, parent)) {
return s;
}
if (Utilities.noString(s)) {
c++;
}
}
if (c < as) {
return "Unexpected Node found in array at '"+path+"' at index "+c;
}
}
}
} else
return "unhandled property " + actualJsonElement.getClass().getName();
return null;
}
private int optionalCount(JsonArray arr, String name, JsonObject parent) {
int c = 0;
for (JsonElement e : arr) {
if (e.isJsonObject()) {
JsonObject j = e.asJsonObject();
if (j.isJsonString("$optional$") && passesOptionalFilter(j.asString("$optional$"))) {
c++;
}
if (j.isJsonBoolean("$optional$") && j.asBoolean("$optional$")) {
c++;
}
}
}
return c;
}
private boolean isOptional(JsonElement e, String name, JsonObject parent) {
if (e.isJsonObject()) {
JsonObject j = e.asJsonObject();
if (j.isJsonString("$optional$") && passesOptionalFilter(j.asString("$optional$"))) {
return true;
} else if (j.isJsonBoolean("$optional$") && j.asBoolean("$optional$")) {
return true;
} else {
return false;
}
} else {
return false;
}
}
private boolean passesOptionalFilter(String token) {
if (token.startsWith("!")) {
return modes == null || !modes.contains(token.substring(1));
} else {
return modes != null && modes.contains(token);
}
}
private int countExpectedMin(JsonArray array, String name, JsonObject parent) {
int count = array.size();
for (JsonElement e : array) {
if (isOptional(e, name, parent)) {
count--;
}
}
return count;
}
private boolean matches(String actualJsonString, String expectedJsonString) {
if (expectedJsonString.startsWith("$") && expectedJsonString.endsWith("$")) {
if (expectedJsonString.startsWith("$choice:")) {
return Utilities.existsInList(actualJsonString, readChoices(expectedJsonString.substring(8, expectedJsonString.length()-1)));
} else if (expectedJsonString.startsWith("$fragments:")) {
List
fragments = readChoices(expectedJsonString.substring(11, expectedJsonString.length()-1));
for (String f : fragments) {
if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) {
return false;
}
}
return true;
} else if (expectedJsonString.startsWith("$external:")) {
String[] cmd = expectedJsonString.substring(1, expectedJsonString.length() - 1).split("\\:");
if (externals != null) {
String s = externals.asString(cmd[1]);
return actualJsonString.equals(s);
} else if (cmd.length <= 2) {
return true;
} else {
List fragments = readChoices(cmd[2]);
for (String f : fragments) {
if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) {
return false;
}
}
return true;
}
} else {
switch (expectedJsonString) {
case "$$" : return true;
case "$instant$": return actualJsonString.matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))");
case "$date$": return actualJsonString.matches("([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]{1,9})?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?");
case "$uuid$": return actualJsonString.matches("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
case "$string$": return actualJsonString.equals(actualJsonString.trim());
case "$id$": return actualJsonString.matches("[A-Za-z0-9\\-\\.]{1,64}");
case "$url$": return actualJsonString.matches("(https?://|www\\.)[-a-zA-Z0-9+&@#/%?=~_|!:.;]*[-a-zA-Z0-9+&@#/%=~_|]");
case "$token$": return actualJsonString.matches("[0-9a-zA-Z_][0-9a-zA-Z_\\.\\-]*");
case "$semver$": return actualJsonString.matches("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
case "$version$": return matchesVariable(actualJsonString, "version");
default:
throw new Error("Unhandled template: "+expectedJsonString);
}
}
} else {
return actualJsonString.equals(expectedJsonString);
}
}
private boolean matchesVariable(String value, String name) {
if (variables.containsKey(name)) {
return value.equals(variables.get(name));
} else {
return true;
}
}
private List readChoices(String s) {
List list = new ArrayList<>();
for (String p : s.split("\\|")) {
list.add(p);
}
return list;
}
public String checkTextIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException {
return checkTextIsSame(id, expected, actual, true);
}
public String checkTextIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException {
String result = compareText(id, expectedString, actualString);
if (result != null && SHOW_DIFF && showDiff) {
String diff = null;
if (System.getProperty("os.name").contains("Linux"))
diff = Utilities.path("/", "usr", "bin", "meld");
else {
if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
else if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
}
if (diff == null || diff.isEmpty())
return result;
List command = new ArrayList();
String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json");
String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json");
FileUtilities.stringToFile(expectedString, expected);
FileUtilities.stringToFile(actualString, actual);
command.add(diff);
if (diff.toLowerCase().contains("meld"))
command.add("--newtab");
command.add(expected);
command.add(actual);
ProcessBuilder builder = new ProcessBuilder(command);
builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]")));
builder.start();
}
return result;
}
private String compareText(String id, String expectedString, String actualString) {
for (int i = 0; i < Integer.min(expectedString.length(), actualString.length()); i++) {
if (expectedString.charAt(i) != actualString.charAt(i))
return createNotEqualMessage(id, "Strings differ at character " + Integer.toString(i), charWithContext(expectedString, i), charWithContext(actualString, i));
}
if (expectedString.length() != actualString.length())
return createNotEqualMessage(id, "Strings differ in length but match to the end of the shortest.", Integer.toString(expectedString.length()), Integer.toString(actualString.length()));
return null;
}
private String charWithContext(String s, int i) {
String result = s.substring(i, i+1);
if (i > 7) {
i = i - 7;
}
int e = i + 20;
if (e > s.length()) {
e = s.length();
}
if (e > i+1) {
result = result + " with context '"+s.substring(i, e)+"'";
}
return result;
}
}