001package org.hl7.fhir.r5.test.utils; 002 003import org.apache.commons.codec.binary.Base64; 004import org.apache.commons.lang3.StringUtils; 005import org.hl7.fhir.exceptions.FHIRException; 006import org.hl7.fhir.utilities.*; 007import org.hl7.fhir.utilities.filesystem.CSFile; 008import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 009import org.hl7.fhir.utilities.json.JsonUtilities; 010import org.hl7.fhir.utilities.json.model.JsonArray; 011import org.hl7.fhir.utilities.json.model.JsonElement; 012import org.hl7.fhir.utilities.json.model.JsonNull; 013import org.hl7.fhir.utilities.json.model.JsonObject; 014import org.hl7.fhir.utilities.json.model.JsonPrimitive; 015import org.hl7.fhir.utilities.json.model.JsonProperty; 016import org.hl7.fhir.utilities.json.parser.JsonParser; 017import org.hl7.fhir.utilities.settings.FhirSettings; 018import org.hl7.fhir.utilities.xml.XMLUtil; 019import org.w3c.dom.Document; 020import org.w3c.dom.Element; 021import org.w3c.dom.NamedNodeMap; 022import org.w3c.dom.Node; 023 024import org.hl7.fhir.utilities.tests.BaseTestingUtilities; 025 026import javax.xml.parsers.DocumentBuilder; 027import javax.xml.parsers.DocumentBuilderFactory; 028import java.io.*; 029import java.util.ArrayList; 030import java.util.List; 031import java.util.Map; 032 033public class CompareUtilities extends BaseTestingUtilities { 034 035 private static final boolean SHOW_DIFF = false; 036 private JsonObject externals; 037 038 public String createNotEqualMessage(String id, final String message, final String expected, final String actual) { 039 return new StringBuilder() 040 .append(message).append('\n') 041 .append("Expected:").append(presentExpected(expected)).append(" for "+id).append('\n') 042 .append("Actual :").append("\""+actual+"\"").toString(); 043 } 044 045 private String presentExpected(String expected) { 046 if (expected == null) { 047 return "null"; 048 } else if (expected.startsWith("$") && expected.endsWith("$")) { 049 if (expected.startsWith("$choice:")) { 050 return "Contains one of "+readChoices(expected.substring(8, expected.length()-1)).toString(); 051 } else if (expected.startsWith("$fragments:")) { 052 List<String> fragments = readChoices(expected.substring(11, expected.length()-1)); 053 return "Contains all of "+fragments.toString(); 054 } else if (expected.startsWith("$external:")) { 055 String[] cmd = expected.substring(1, expected.length() - 1).split(":"); 056 if (externals != null) { 057 String s = externals.asString(cmd[1]); 058 return "\""+s+"\" (Ext)"; 059 } else { 060 List<String> fragments = readChoices(cmd[2]); 061 return "Contains all of "+fragments.toString()+" (because no external string provided for "+cmd[1]+")"; 062 } 063 } else { 064 switch (expected) { 065 case "$$" : return "$$"; 066 case "$instant$": return "\"An Instant\""; 067 case "$uuid$": return "\"A Uuid\""; 068 case "$id$": return "\"An Id\""; 069 default: return "Unhandled template: "+expected; 070 } 071 } 072 } else { 073 return "\""+expected+"\""; 074 } 075 } 076 077 public static String checkXMLIsSame(String id, InputStream expected, InputStream actual) throws Exception { 078 CompareUtilities self = new CompareUtilities(); 079 String result = self.compareXml(id, expected, actual); 080 return result; 081 } 082 083 public static String checkXMLIsSame(String id, String expected, String actual) throws Exception { 084 CompareUtilities self = new CompareUtilities(); 085 String result = self.compareXml(id, expected, actual); 086 if (result != null && SHOW_DIFF) { 087 String diff = getDiffTool(); 088 if (diff != null && ManagedFileAccess.file(diff).exists() || Utilities.isToken(diff)) { 089 Runtime.getRuntime().exec(new String[]{diff, expected, actual}); 090 } 091 } 092 return result; 093 } 094 095 private static String getDiffTool() throws IOException { 096 if (FhirSettings.hasDiffToolPath()) { 097 return FhirSettings.getDiffToolPath(); 098 } else if (System.getenv("ProgramFiles") != null) { 099 return Utilities.path(System.getenv("ProgramFiles"), "WinMerge", "WinMergeU.exe"); 100 } else { 101 return null; 102 } 103 } 104 105 private String compareXml(String id, InputStream expected, InputStream actual) throws Exception { 106 return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement()); 107 } 108 109 private String compareXml(String id, String expected, String actual) throws Exception { 110 return compareElements(id, "", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement()); 111 } 112 113 private String compareElements(String id, String path, Element expectedElement, Element actualElement) { 114 if (!namespacesMatch(expectedElement.getNamespaceURI(), actualElement.getNamespaceURI())) 115 return createNotEqualMessage(id, "Namespaces differ at " + path, expectedElement.getNamespaceURI(), actualElement.getNamespaceURI()); 116 if (!expectedElement.getLocalName().equals(actualElement.getLocalName())) 117 return createNotEqualMessage(id, "Names differ at " + path , expectedElement.getLocalName(), actualElement.getLocalName()); 118 path = path + "/" + expectedElement.getLocalName(); 119 String s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes()); 120 if (!Utilities.noString(s)) 121 return s; 122 s = compareAttributes(id, path, expectedElement.getAttributes(), actualElement.getAttributes()); 123 if (!Utilities.noString(s)) 124 return s; 125 126 Node expectedChild = expectedElement.getFirstChild(); 127 Node actualChild = actualElement.getFirstChild(); 128 expectedChild = skipBlankText(expectedChild); 129 actualChild = skipBlankText(actualChild); 130 while (expectedChild != null && actualChild != null) { 131 if (expectedChild.getNodeType() != actualChild.getNodeType()) 132 return createNotEqualMessage(id, "node type mismatch in children of " + path, Short.toString(expectedElement.getNodeType()), Short.toString(actualElement.getNodeType())); 133 if (expectedChild.getNodeType() == Node.TEXT_NODE) { 134 if (!normalise(expectedChild.getTextContent()).equals(normalise(actualChild.getTextContent()))) 135 return createNotEqualMessage(id, "Text differs at " + path, normalise(expectedChild.getTextContent()).toString(), normalise(actualChild.getTextContent()).toString()); 136 } else if (expectedChild.getNodeType() == Node.ELEMENT_NODE) { 137 s = compareElements(id, path, (Element) expectedChild, (Element) actualChild); 138 if (!Utilities.noString(s)) 139 return s; 140 } 141 142 expectedChild = skipBlankText(expectedChild.getNextSibling()); 143 actualChild = skipBlankText(actualChild.getNextSibling()); 144 } 145 if (expectedChild != null) 146 return "node mismatch - more nodes in actual in children of " + path; 147 if (actualChild != null) 148 return "node mismatch - more nodes in expected in children of " + path; 149 return null; 150 } 151 152 private boolean namespacesMatch(String ns1, String ns2) { 153 return ns1 == null ? ns2 == null : ns1.equals(ns2); 154 } 155 156 private String normalise(String text) { 157 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 158 while (result.contains(" ")) 159 result = result.replace(" ", " "); 160 return result; 161 } 162 163 private String compareAttributes(String id, String path, NamedNodeMap expected, NamedNodeMap actual) { 164 for (int i = 0; i < expected.getLength(); i++) { 165 166 Node expectedNode = expected.item(i); 167 String expectedNodeName = expectedNode.getNodeName(); 168 if (!(expectedNodeName.equals("xmlns") || expectedNodeName.startsWith("xmlns:"))) { 169 Node actualNode = actual.getNamedItem(expectedNodeName); 170 if (actualNode == null) 171 return "Attributes differ at " + path + ": missing attribute " + expectedNodeName; 172 if (!normalise(expectedNode.getTextContent()).equals(normalise(actualNode.getTextContent()))) { 173 byte[] b1 = unBase64(expectedNode.getTextContent()); 174 byte[] b2 = unBase64(actualNode.getTextContent()); 175 if (!sameBytes(b1, b2)) 176 return createNotEqualMessage(id, "Attributes differ at " + path, normalise(expectedNode.getTextContent()).toString(), normalise(actualNode.getTextContent()).toString()) ; 177 } 178 } 179 } 180 return null; 181 } 182 183 private boolean sameBytes(byte[] b1, byte[] b2) { 184 if (b1.length == 0 || b2.length == 0) 185 return false; 186 if (b1.length != b2.length) 187 return false; 188 for (int i = 0; i < b1.length; i++) 189 if (b1[i] != b2[i]) 190 return false; 191 return true; 192 } 193 194 private byte[] unBase64(String text) { 195 return Base64.decodeBase64(text); 196 } 197 198 private Node skipBlankText(Node node) { 199 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && StringUtils.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 200 node = node.getNextSibling(); 201 return node; 202 } 203 204 private Document loadXml(String fn) throws Exception { 205 return loadXml(ManagedFileAccess.inStream(fn)); 206 } 207 208 private Document loadXml(InputStream fn) throws Exception { 209 DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory(); 210 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 211 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 212 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 213 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 214 factory.setXIncludeAware(false); 215 factory.setExpandEntityReferences(false); 216 217 factory.setNamespaceAware(true); 218 DocumentBuilder builder = factory.newDocumentBuilder(); 219 return builder.parse(fn); 220 } 221 222 public static String checkJsonSrcIsSame(String id, String expected, String actual, JsonObject externals) throws FileNotFoundException, IOException { 223 return checkJsonSrcIsSame(id, expected, actual, true, externals); 224 } 225 226 public static String checkJsonSrcIsSame(String id, String expectedString, String actualString, boolean showDiff, JsonObject externals) throws FileNotFoundException, IOException { 227 CompareUtilities self = new CompareUtilities(); 228 self.externals = externals; 229 String result = self.compareJsonSrc(id, expectedString, actualString); 230 if (result != null && SHOW_DIFF && showDiff) { 231 String diff = null; 232 if (System.getProperty("os.name").contains("Linux")) 233 diff = Utilities.path("/", "usr", "bin", "meld"); 234 else if (System.getenv("ProgramFiles(X86)") != null) { 235 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 236 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 237 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 238 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 239 } 240 if (diff == null || diff.isEmpty()) 241 return result; 242 243 List<String> command = new ArrayList<String>(); 244 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 245 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 246 TextFile.stringToFile(expectedString, expected); 247 TextFile.stringToFile(actualString, actual); 248 command.add(diff); 249 if (diff.toLowerCase().contains("meld")) 250 command.add("--newtab"); 251 command.add(expected); 252 command.add(actual); 253 254 ProcessBuilder builder = new ProcessBuilder(command); 255 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 256 builder.start(); 257 258 } 259 return result; 260 } 261 262 public static String checkJsonIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 263 CompareUtilities self = new CompareUtilities(); 264 String result = self.compareJson(id, expected, actual); 265 if (result != null && SHOW_DIFF) { 266 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 267 List<String> command = new ArrayList<String>(); 268 command.add("\"" + diff + "\" \"" + expected + "\" \"" + actual + "\""); 269 270 ProcessBuilder builder = new ProcessBuilder(command); 271 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 272 builder.start(); 273 274 } 275 return result; 276 } 277 278 private String compareJsonSrc(String id, String expected, String actual) throws FileNotFoundException, IOException { 279 JsonObject actualJsonObject = JsonParser.parseObject(actual); 280 JsonObject expectedJsonObject = JsonParser.parseObject(expected); 281 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 282 } 283 284 private String compareJson(String id, String expected, String actual) throws FileNotFoundException, IOException { 285 JsonObject actualJsonObject = JsonParser.parseObject(TextFile.fileToString(actual)); 286 JsonObject expectedJsonObject = JsonParser.parseObject(TextFile.fileToString(expected)); 287 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 288 } 289 290 private String compareObjects(String id, String path, JsonObject expectedJsonObject, JsonObject actualJsonObject) { 291 List<String> optionals = listOptionals(expectedJsonObject); 292 List<String> countOnlys = listCountOnlys(expectedJsonObject); 293 for (JsonProperty en : actualJsonObject.getProperties()) { 294 String n = en.getName(); 295 if (!n.equals("fhir_comments")) { 296 if (expectedJsonObject.has(n)) { 297 String s = compareNodes(id, path + '.' + n, expectedJsonObject.get(n), en.getValue(), countOnlys.contains(n)); 298 if (!Utilities.noString(s)) 299 return s; 300 } else 301 return "properties differ at " + path + ": missing property " + n; 302 } 303 } 304 for (JsonProperty en : expectedJsonObject.getProperties()) { 305 String n = en.getName(); 306 if (!n.equals("fhir_comments") && !n.equals("$optional$") && !optionals.contains(n)) { 307 if (!actualJsonObject.has(n) && !allOptional(en.getValue())) 308 return "properties differ at " + path + ": missing property " + n; 309 } 310 } 311 return null; 312 } 313 314 private boolean allOptional(JsonElement value) { 315 if (value.isJsonArray()) { 316 JsonArray a = value.asJsonArray(); 317 for (JsonElement e : a) { 318 if (e.isJsonObject()) { 319 JsonObject o = e.asJsonObject(); 320 if (!o.has("$optional$")) { 321 return false; 322 } 323 } else { 324 // nothing 325 } 326 } 327 return true; 328 } else { 329 return false; 330 } 331 } 332 333 private List<String> listOptionals(JsonObject expectedJsonObject) { 334 List<String> res = new ArrayList<>(); 335 if (expectedJsonObject.has("$optional-properties$")) { 336 res.add("$optional-properties$"); 337 res.add("$count-arrays$"); 338 for (String s : expectedJsonObject.getStrings("$optional-properties$")) { 339 res.add(s); 340 } 341 } 342 return res; 343 } 344 345 private List<String> listCountOnlys(JsonObject expectedJsonObject) { 346 List<String> res = new ArrayList<>(); 347 if (expectedJsonObject.has("$count-arrays$")) { 348 for (String s : expectedJsonObject.getStrings("$count-arrays$")) { 349 res.add(s); 350 } 351 } 352 return res; 353 } 354 355 private String compareNodes(String id, String path, JsonElement expectedJsonElement, JsonElement actualJsonElement, boolean countOnly) { 356 if (!(expectedJsonElement instanceof JsonPrimitive && actualJsonElement instanceof JsonPrimitive)) { 357 if (actualJsonElement.getClass() != expectedJsonElement.getClass()) { 358 return createNotEqualMessage(id, "properties differ at " + path, expectedJsonElement.getClass().getName(), actualJsonElement.getClass().getName()); 359 } 360 } 361 if (actualJsonElement instanceof JsonPrimitive) { 362 JsonPrimitive actualJsonPrimitive = (JsonPrimitive) actualJsonElement; 363 JsonPrimitive expectedJsonPrimitive = (JsonPrimitive) expectedJsonElement; 364 if (actualJsonPrimitive.isJsonBoolean() && expectedJsonPrimitive.isJsonBoolean()) { 365 if (actualJsonPrimitive.asBoolean() != expectedJsonPrimitive.asBoolean()) 366 return createNotEqualMessage(id, "boolean property values differ at " + path , expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 367 } else if (actualJsonPrimitive.isJsonString() && expectedJsonPrimitive.isJsonString()) { 368 String actualJsonString = actualJsonPrimitive.asString(); 369 String expectedJsonString = expectedJsonPrimitive.asString(); 370 if (!(actualJsonString.contains("<div") && expectedJsonString.contains("<div"))) 371 if (!matches(actualJsonString, expectedJsonString)) 372 if (!sameBytes(unBase64(actualJsonString), unBase64(expectedJsonString))) 373 return createNotEqualMessage(id, "string property values differ at " + path, expectedJsonString, actualJsonString); 374 } else if (actualJsonPrimitive.isJsonNumber() && expectedJsonPrimitive.isJsonNumber()) { 375 if (!actualJsonPrimitive.asString().equals(expectedJsonPrimitive.asString())) 376 return createNotEqualMessage(id, "number property values differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 377 } else if (expectedJsonElement instanceof JsonNull) { 378 return actualJsonPrimitive instanceof JsonNull ? null : createNotEqualMessage(id, "null Properties differ at " + path, "null", actualJsonPrimitive.asString()); 379 } else { 380 return createNotEqualMessage(id, "property types differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 381 } 382 } else if (actualJsonElement instanceof JsonObject) { 383 String s = compareObjects(id, path, (JsonObject) expectedJsonElement, (JsonObject) actualJsonElement); 384 if (!Utilities.noString(s)) 385 return s; 386 } else if (actualJsonElement instanceof JsonArray) { 387 JsonArray actualArray = (JsonArray) actualJsonElement; 388 JsonArray expectedArray = (JsonArray) expectedJsonElement; 389 390 int as = actualArray.size(); 391 int es = expectedArray.size(); 392 if (countOnly) { 393 if (as != es) { 394 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 395 } 396 } else { 397 int expectedMin = countExpectedMin(expectedArray); 398 int oc = optionalCount(expectedArray); 399 400 if (as > es || as < expectedMin) 401 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 402 int c = 0; 403 for (int i = 0; i < es; i++) { 404 if (c >= as) { 405 if (i >= es - oc && isOptional(expectedArray.get(i))) { 406 return null; // this is OK 407 } else { 408 return "One or more array items did not match at "+path+" starting at index "+i; 409 } 410 } 411 String s = compareNodes(id, path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(c), false); 412 if (!Utilities.noString(s) && !isOptional(expectedArray.get(i))) { 413 return s; 414 } 415 if (Utilities.noString(s)) { 416 c++; 417 } 418 } 419 if (c < as) { 420 return "Unexpected Node found in array at '"+path+"' at index "+c; 421 } 422 } 423 } else 424 return "unhandled property " + actualJsonElement.getClass().getName(); 425 return null; 426 } 427 428 private int optionalCount(JsonArray arr) { 429 int c = 0; 430 for (JsonElement e : arr) { 431 if (e.isJsonObject()) { 432 JsonObject j = e.asJsonObject(); 433 if (j.has("$optional$") && j.asBoolean("$optional$")) { 434 c++; 435 } 436 } 437 } 438 return c; 439 } 440 441 private boolean isOptional(JsonElement e) { 442 return e.isJsonObject() && e.asJsonObject().has("$optional$"); 443 } 444 445 private int countExpectedMin(JsonArray array) { 446 int count = array.size(); 447 for (JsonElement e : array) { 448 if (isOptional(e)) { 449 count--; 450 } 451 } 452 return count; 453 } 454 455 private boolean matches(String actualJsonString, String expectedJsonString) { 456 if (expectedJsonString.startsWith("$") && expectedJsonString.endsWith("$")) { 457 if (expectedJsonString.startsWith("$choice:")) { 458 return Utilities.existsInList(actualJsonString, readChoices(expectedJsonString.substring(8, expectedJsonString.length()-1))); 459 460 } else if (expectedJsonString.startsWith("$fragments:")) { 461 List<String> fragments = readChoices(expectedJsonString.substring(11, expectedJsonString.length()-1)); 462 for (String f : fragments) { 463 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 464 return false; 465 } 466 } 467 return true; 468 } else if (expectedJsonString.startsWith("$external:")) { 469 String[] cmd = expectedJsonString.substring(1, expectedJsonString.length() - 1).split("\\:"); 470 if (externals != null) { 471 String s = externals.asString(cmd[1]); 472 return actualJsonString.equals(s); 473 } else if (cmd.length <= 2) { 474 return true; 475 } else { 476 List<String> fragments = readChoices(cmd[2]); 477 for (String f : fragments) { 478 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 479 return false; 480 } 481 } 482 return true; 483 } 484 } else { 485 switch (expectedJsonString) { 486 case "$$" : return true; 487 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))"); 488 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}"); 489 case "$id$": return actualJsonString.matches("[A-Za-z0-9\\-\\.]{1,64}"); 490 default: 491 throw new Error("Unhandled template: "+expectedJsonString); 492 } 493 } 494 } else { 495 return actualJsonString.equals(expectedJsonString); 496 } 497 } 498 499 private List<String> readChoices(String s) { 500 List<String> list = new ArrayList<>(); 501 for (String p : s.split("\\|")) { 502 list.add(p); 503 } 504 return list; 505 } 506 507 public static String checkTextIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 508 return checkTextIsSame(id, expected, actual, true); 509 } 510 511 public static String checkTextIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException { 512 CompareUtilities self = new CompareUtilities(); 513 String result = self.compareText(id, expectedString, actualString); 514 if (result != null && SHOW_DIFF && showDiff) { 515 String diff = null; 516 if (System.getProperty("os.name").contains("Linux")) 517 diff = Utilities.path("/", "usr", "bin", "meld"); 518 else { 519 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 520 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 521 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 522 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 523 } 524 if (diff == null || diff.isEmpty()) 525 return result; 526 527 List<String> command = new ArrayList<String>(); 528 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 529 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 530 TextFile.stringToFile(expectedString, expected); 531 TextFile.stringToFile(actualString, actual); 532 command.add(diff); 533 if (diff.toLowerCase().contains("meld")) 534 command.add("--newtab"); 535 command.add(expected); 536 command.add(actual); 537 538 ProcessBuilder builder = new ProcessBuilder(command); 539 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 540 builder.start(); 541 542 } 543 return result; 544 } 545 546 547 private String compareText(String id, String expectedString, String actualString) { 548 for (int i = 0; i < Integer.min(expectedString.length(), actualString.length()); i++) { 549 if (expectedString.charAt(i) != actualString.charAt(i)) 550 return createNotEqualMessage(id, "Strings differ at character " + Integer.toString(i), charWithContext(expectedString, i), charWithContext(actualString, i)); 551 } 552 if (expectedString.length() != actualString.length()) 553 return createNotEqualMessage(id, "Strings differ in length but match to the end of the shortest.", Integer.toString(expectedString.length()), Integer.toString(actualString.length())); 554 return null; 555 } 556 557 private String charWithContext(String s, int i) { 558 String result = s.substring(i, i+1); 559 if (i > 7) { 560 i = i - 7; 561 } 562 int e = i + 20; 563 if (e > s.length()) { 564 e = s.length(); 565 } 566 if (e > i+1) { 567 result = result + " with context '"+s.substring(i, e)+"'"; 568 } 569 return result; 570 } 571 572}