
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 public 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 for (int i = 0; i < actual.getLength(); i++) { 235 Node actualNode = actual.item(i); 236 String actualNodeName = actualNode.getNodeName(); 237 if (!(actualNodeName.equals("xmlns") || actualNodeName.startsWith("xmlns:"))) { 238 Node expectedNode = expected.getNamedItem(actualNodeName); 239 if (expectedNode == null) { 240 return "Attributes differ at " + path + ": unexpected attribute " + actualNodeName; 241 } 242 } 243 } 244 return null; 245 } 246 247 private boolean sameBytes(byte[] b1, byte[] b2) { 248 if (b1.length == 0 || b2.length == 0) 249 return false; 250 if (b1.length != b2.length) 251 return false; 252 for (int i = 0; i < b1.length; i++) 253 if (b1[i] != b2[i]) 254 return false; 255 return true; 256 } 257 258 private byte[] unBase64(String text) { 259 return Base64.decodeBase64(text); 260 } 261 262 private Node skipBlankText(Node node) { 263 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && StringUtils.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 264 node = node.getNextSibling(); 265 return node; 266 } 267 268 private Document loadXml(String fn) throws Exception { 269 return loadXml(ManagedFileAccess.inStream(fn)); 270 } 271 272 private Document loadXml(InputStream fn) throws Exception { 273 DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory(); 274 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 275 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 276 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 277 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 278 factory.setXIncludeAware(false); 279 factory.setExpandEntityReferences(false); 280 281 factory.setNamespaceAware(true); 282 DocumentBuilder builder = factory.newDocumentBuilder(); 283 return builder.parse(fn); 284 } 285 286 public String checkJsonSrcIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 287 return checkJsonSrcIsSame(id, expected, actual, true); 288 } 289 290 public String checkJsonSrcIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException { 291 String result = compareJsonSrc(id, expectedString, actualString); 292 if (result != null && SHOW_DIFF && showDiff) { 293 String diff = null; 294 if (System.getProperty("os.name").contains("Linux")) 295 diff = Utilities.path("/", "usr", "bin", "meld"); 296 else if (System.getenv("ProgramFiles(X86)") != null) { 297 if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 298 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 299 else if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 300 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 301 } 302 if (diff == null || diff.isEmpty()) 303 return result; 304 305 List<String> command = new ArrayList<String>(); 306 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 307 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 308 FileUtilities.stringToFile(expectedString, expected); 309 FileUtilities.stringToFile(actualString, actual); 310 command.add(diff); 311 if (diff.toLowerCase().contains("meld")) 312 command.add("--newtab"); 313 command.add(expected); 314 command.add(actual); 315 316 ProcessBuilder builder = new ProcessBuilder(command); 317 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 318 builder.start(); 319 320 } 321 return result; 322 } 323 324 public String checkJsonIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 325 String result = compareJson(id, expected, actual); 326 if (result != null && SHOW_DIFF) { 327 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 328 List<String> command = new ArrayList<String>(); 329 command.add("\"" + diff + "\" \"" + expected + "\" \"" + actual + "\""); 330 331 ProcessBuilder builder = new ProcessBuilder(command); 332 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 333 builder.start(); 334 335 } 336 return result; 337 } 338 339 private String compareJsonSrc(String id, String expected, String actual) throws FileNotFoundException, IOException { 340 JsonObject actualJsonObject = JsonParser.parseObject(actual); 341 JsonObject expectedJsonObject = JsonParser.parseObject(expected); 342 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 343 } 344 345 private String compareJson(String id, String expected, String actual) throws FileNotFoundException, IOException { 346 JsonObject actualJsonObject = JsonParser.parseObject(FileUtilities.fileToString(actual)); 347 JsonObject expectedJsonObject = JsonParser.parseObject(FileUtilities.fileToString(expected)); 348 return compareObjects(id, "", expectedJsonObject, actualJsonObject); 349 } 350 351 public String compareObjects(String id, String path, JsonObject expectedJsonObject, JsonObject actualJsonObject) { 352 List<String> optionals = listOptionals(expectedJsonObject); 353 List<String> countOnlys = listCountOnlys(expectedJsonObject); 354 for (JsonProperty en : actualJsonObject.getProperties()) { 355 String n = en.getName(); 356 if (!n.equals("fhir_comments")) { 357 if (expectedJsonObject.has(n)) { 358 String s = compareNodes(id, path + '.' + n, expectedJsonObject.get(n), en.getValue(), countOnlys.contains(n), n, actualJsonObject); 359 if (!Utilities.noString(s)) 360 return s; 361 } else if (!patternMode) { 362 return "properties differ at " + path + ": unexpected property " + n; 363 } 364 } 365 } 366 for (JsonProperty en : expectedJsonObject.getProperties()) { 367 String n = en.getName(); 368 if (!n.equals("fhir_comments") && !isOptional(n, optionals)) { 369 if (!actualJsonObject.has(n) && !allOptional(en.getValue())) 370 return "properties differ at " + path + ": missing property " + n; 371 } 372 } 373 return null; 374 } 375 376 private boolean isOptional(String n, List<String> optionals) { 377 return n.equals("$optional$") || optionals.contains("*") || optionals.contains(n); 378 } 379 380 private boolean allOptional(JsonElement value) { 381 if (value.isJsonArray()) { 382 JsonArray a = value.asJsonArray(); 383 for (JsonElement e : a) { 384 if (e.isJsonObject()) { 385 JsonObject o = e.asJsonObject(); 386 if (!o.has("$optional$")) { 387 return false; 388 } 389 } else { 390 // nothing 391 } 392 } 393 return true; 394 } else { 395 return false; 396 } 397 } 398 399 private List<String> listOptionals(JsonObject expectedJsonObject) { 400 List<String> res = new ArrayList<>(); 401 if (expectedJsonObject.has("$optional-properties$")) { 402 res.add("$optional-properties$"); 403 res.add("$count-arrays$"); 404 for (String s : expectedJsonObject.getStrings("$optional-properties$")) { 405 res.add(s); 406 } 407 } 408 return res; 409 } 410 411 private List<String> listCountOnlys(JsonObject expectedJsonObject) { 412 List<String> res = new ArrayList<>(); 413 if (expectedJsonObject.has("$count-arrays$")) { 414 for (String s : expectedJsonObject.getStrings("$count-arrays$")) { 415 res.add(s); 416 } 417 } 418 return res; 419 } 420 421 private String compareNodes(String id, String path, JsonElement expectedJsonElement, JsonElement actualJsonElement, boolean countOnly, String name, JsonObject parent) { 422 if (!(expectedJsonElement instanceof JsonPrimitive && actualJsonElement instanceof JsonPrimitive)) { 423 if (actualJsonElement.getClass() != expectedJsonElement.getClass()) { 424 return createNotEqualMessage(id, "properties differ at " + path, expectedJsonElement.getClass().getName(), actualJsonElement.getClass().getName()); 425 } 426 } 427 if (actualJsonElement instanceof JsonPrimitive) { 428 JsonPrimitive actualJsonPrimitive = (JsonPrimitive) actualJsonElement; 429 JsonPrimitive expectedJsonPrimitive = (JsonPrimitive) expectedJsonElement; 430 if (actualJsonPrimitive.isJsonBoolean() && expectedJsonPrimitive.isJsonBoolean()) { 431 if (actualJsonPrimitive.asBoolean() != expectedJsonPrimitive.asBoolean()) 432 return createNotEqualMessage(id, "boolean property values differ at " + path , expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 433 } else if (actualJsonPrimitive.isJsonString() && expectedJsonPrimitive.isJsonString()) { 434 String actualJsonString = actualJsonPrimitive.asString(); 435 String expectedJsonString = expectedJsonPrimitive.asString(); 436 if (!(actualJsonString.contains("<div") && expectedJsonString.contains("<div"))) 437 if (!matches(actualJsonString, expectedJsonString)) 438 if (!sameBytes(unBase64(actualJsonString), unBase64(expectedJsonString))) 439 return createNotEqualMessage(id, "string property values differ at " + path, expectedJsonString, actualJsonString); 440 } else if (actualJsonPrimitive.isJsonNumber() && expectedJsonPrimitive.isJsonNumber()) { 441 if (!actualJsonPrimitive.asString().equals(expectedJsonPrimitive.asString())) 442 return createNotEqualMessage(id, "number property values differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 443 } else if (expectedJsonElement instanceof JsonNull) { 444 return actualJsonPrimitive instanceof JsonNull ? null : createNotEqualMessage(id, "null Properties differ at " + path, "null", actualJsonPrimitive.asString()); 445 } else { 446 return createNotEqualMessage(id, "property types differ at " + path, expectedJsonPrimitive.asString(), actualJsonPrimitive.asString()); 447 } 448 } else if (actualJsonElement instanceof JsonObject) { 449 String s = compareObjects(id, path, (JsonObject) expectedJsonElement, (JsonObject) actualJsonElement); 450 if (!Utilities.noString(s)) 451 return s; 452 } else if (actualJsonElement instanceof JsonArray) { 453 JsonArray actualArray = (JsonArray) actualJsonElement; 454 JsonArray expectedArray = (JsonArray) expectedJsonElement; 455 456 int as = actualArray.size(); 457 int es = expectedArray.size(); 458 if (countOnly) { 459 if (as != es) { 460 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 461 } 462 } else { 463 int expectedMin = countExpectedMin(expectedArray, name, parent); 464 int oc = optionalCount(expectedArray, name, parent); 465 466 467 if (patternMode) { 468 int c = 0; 469 for (int i = 0; i < expectedArray.size(); i++) { 470 String s = "Doesn't exist"; 471 CommaSeparatedStringBuilder cs = new CommaSeparatedStringBuilder("\r\n"); 472 cs.append(""); 473 while (s != null && c < actualArray.size()) { 474 s = compareNodes(id, path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(c), false, null, null); 475 if (s != null) { 476 cs.append(" "+s); 477 } 478 c++; 479 } 480 if (s != null) { 481 return "The expected item at "+path+" at index "+i+" was not found: "+cs.toString(); 482 } 483 } 484 } else { 485 if (as > es || as < expectedMin) { 486 return createNotEqualMessage(id, "array item count differs at " + path, Integer.toString(es), Integer.toString(as)); 487 } 488 int c = 0; 489 for (int i = 0; i < es; i++) { 490 if (c >= as) { 491 if (i >= es - oc && isOptional(expectedArray.get(i), name, parent)) { 492 return null; // this is OK 493 } else { 494 return "One or more array items did not match at "+path+" starting at index "+i; 495 } 496 } 497 String s = compareNodes(id, path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(c), false, null, null); 498 if (!Utilities.noString(s) && !isOptional(expectedArray.get(i), name, parent)) { 499 return s; 500 } 501 if (Utilities.noString(s)) { 502 c++; 503 } 504 } 505 if (c < as) { 506 return "Unexpected Node found in array at '"+path+"' at index "+c; 507 } 508 } 509 } 510 } else 511 return "unhandled property " + actualJsonElement.getClass().getName(); 512 return null; 513 } 514 515 private int optionalCount(JsonArray arr, String name, JsonObject parent) { 516 int c = 0; 517 for (JsonElement e : arr) { 518 if (e.isJsonObject()) { 519 JsonObject j = e.asJsonObject(); 520 if (j.isJsonString("$optional$") && passesOptionalFilter(j.asString("$optional$"))) { 521 c++; 522 } 523 if (j.isJsonBoolean("$optional$") && j.asBoolean("$optional$")) { 524 c++; 525 } 526 } 527 } 528 return c; 529 } 530 531 private boolean isOptional(JsonElement e, String name, JsonObject parent) { 532 if (e.isJsonObject()) { 533 JsonObject j = e.asJsonObject(); 534 if (j.isJsonString("$optional$") && passesOptionalFilter(j.asString("$optional$"))) { 535 return true; 536 } else if (j.isJsonBoolean("$optional$") && j.asBoolean("$optional$")) { 537 return true; 538 } else { 539 return false; 540 } 541 } else { 542 return false; 543 } 544 } 545 546 private boolean passesOptionalFilter(String token) { 547 if (token.startsWith("!")) { 548 return modes == null || !modes.contains(token.substring(1)); 549 } else { 550 return modes != null && modes.contains(token); 551 } 552 } 553 554 private int countExpectedMin(JsonArray array, String name, JsonObject parent) { 555 int count = array.size(); 556 for (JsonElement e : array) { 557 if (isOptional(e, name, parent)) { 558 count--; 559 } 560 } 561 return count; 562 } 563 564 private boolean matches(String actualJsonString, String expectedJsonString) { 565 if (expectedJsonString.startsWith("$") && expectedJsonString.endsWith("$")) { 566 if (expectedJsonString.startsWith("$choice:")) { 567 return Utilities.existsInList(actualJsonString, readChoices(expectedJsonString.substring(8, expectedJsonString.length()-1))); 568 569 } else if (expectedJsonString.startsWith("$fragments:")) { 570 List<String> fragments = readChoices(expectedJsonString.substring(11, expectedJsonString.length()-1)); 571 for (String f : fragments) { 572 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 573 return false; 574 } 575 } 576 return true; 577 } else if (expectedJsonString.startsWith("$external:")) { 578 String[] cmd = expectedJsonString.substring(1, expectedJsonString.length() - 1).split("\\:"); 579 if (externals != null) { 580 String s = externals.asString(cmd[1]); 581 return actualJsonString.equals(s); 582 } else if (cmd.length <= 2) { 583 return true; 584 } else { 585 List<String> fragments = readChoices(cmd[2]); 586 for (String f : fragments) { 587 if (!actualJsonString.toLowerCase().contains(f.toLowerCase())) { 588 return false; 589 } 590 } 591 return true; 592 } 593 } else { 594 switch (expectedJsonString) { 595 case "$$" : return true; 596 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))"); 597 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)))?"); 598 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}"); 599 case "$string$": return actualJsonString.equals(actualJsonString.trim()); 600 case "$id$": return actualJsonString.matches("[A-Za-z0-9\\-\\.]{1,64}"); 601 case "$url$": return actualJsonString.matches("(https?://|www\\.)[-a-zA-Z0-9+&@#/%?=~_|!:.;]*[-a-zA-Z0-9+&@#/%=~_|]"); 602 case "$token$": return actualJsonString.matches("[0-9a-zA-Z_][0-9a-zA-Z_\\.\\-]*"); 603 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-]+)*))?$"); 604 case "$version$": return matchesVariable(actualJsonString, "version"); 605 default: 606 throw new Error("Unhandled template: "+expectedJsonString); 607 } 608 } 609 } else { 610 return actualJsonString.equals(expectedJsonString); 611 } 612 } 613 614 private boolean matchesVariable(String value, String name) { 615 if (variables.containsKey(name)) { 616 return value.equals(variables.get(name)); 617 } else { 618 return true; 619 } 620 } 621 622 private List<String> readChoices(String s) { 623 List<String> list = new ArrayList<>(); 624 for (String p : s.split("\\|")) { 625 list.add(p); 626 } 627 return list; 628 } 629 630 public String checkTextIsSame(String id, String expected, String actual) throws FileNotFoundException, IOException { 631 return checkTextIsSame(id, expected, actual, true); 632 } 633 634 public String checkTextIsSame(String id, String expectedString, String actualString, boolean showDiff) throws FileNotFoundException, IOException { 635 String result = compareText(id, expectedString, actualString); 636 if (result != null && SHOW_DIFF && showDiff) { 637 String diff = null; 638 if (System.getProperty("os.name").contains("Linux")) 639 diff = Utilities.path("/", "usr", "bin", "meld"); 640 else { 641 if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 642 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 643 else if (FileUtilities.checkFileExists("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 644 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 645 } 646 if (diff == null || diff.isEmpty()) 647 return result; 648 649 List<String> command = new ArrayList<String>(); 650 String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json"); 651 String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json"); 652 FileUtilities.stringToFile(expectedString, expected); 653 FileUtilities.stringToFile(actualString, actual); 654 command.add(diff); 655 if (diff.toLowerCase().contains("meld")) 656 command.add("--newtab"); 657 command.add(expected); 658 command.add(actual); 659 660 ProcessBuilder builder = new ProcessBuilder(command); 661 builder.directory(ManagedFileAccess.csfile(Utilities.path("[tmp]"))); 662 builder.start(); 663 664 } 665 return result; 666 } 667 668 669 private String compareText(String id, String expectedString, String actualString) { 670 for (int i = 0; i < Integer.min(expectedString.length(), actualString.length()); i++) { 671 if (expectedString.charAt(i) != actualString.charAt(i)) 672 return createNotEqualMessage(id, "Strings differ at character " + Integer.toString(i), charWithContext(expectedString, i), charWithContext(actualString, i)); 673 } 674 if (expectedString.length() != actualString.length()) 675 return createNotEqualMessage(id, "Strings differ in length but match to the end of the shortest.", Integer.toString(expectedString.length()), Integer.toString(actualString.length())); 676 return null; 677 } 678 679 private String charWithContext(String s, int i) { 680 String result = s.substring(i, i+1); 681 if (i > 7) { 682 i = i - 7; 683 } 684 int e = i + 20; 685 if (e > s.length()) { 686 e = s.length(); 687 } 688 if (e > i+1) { 689 result = result + " with context '"+s.substring(i, e)+"'"; 690 } 691 return result; 692 } 693 694}