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