001package org.hl7.fhir.r4.test.utils; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032import java.io.File; 033import java.io.FileInputStream; 034import java.io.FileNotFoundException; 035import java.io.IOException; 036import java.io.InputStream; 037import java.nio.file.Path; 038import java.nio.file.Paths; 039import java.util.ArrayList; 040import java.util.List; 041import java.util.Map; 042 043import javax.xml.parsers.DocumentBuilder; 044import javax.xml.parsers.DocumentBuilderFactory; 045 046import org.apache.commons.codec.binary.Base64; 047import org.apache.commons.io.IOUtils; 048import org.fhir.ucum.UcumEssenceService; 049import org.hl7.fhir.r4.context.IWorkerContext; 050import org.hl7.fhir.r4.context.SimpleWorkerContext; 051import org.hl7.fhir.r4.model.Parameters; 052import org.hl7.fhir.utilities.CSFile; 053import org.hl7.fhir.utilities.TextFile; 054import org.hl7.fhir.utilities.Utilities; 055import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; 056import org.hl7.fhir.utilities.settings.FhirSettings; 057import org.hl7.fhir.utilities.tests.BaseTestingUtilities; 058import org.hl7.fhir.utilities.tests.ResourceLoaderTests; 059import org.hl7.fhir.utilities.tests.TestConfig; 060import org.w3c.dom.Document; 061import org.w3c.dom.Element; 062import org.w3c.dom.NamedNodeMap; 063import org.w3c.dom.Node; 064 065import com.google.gson.JsonArray; 066import com.google.gson.JsonElement; 067import com.google.gson.JsonNull; 068import com.google.gson.JsonObject; 069import com.google.gson.JsonPrimitive; 070import com.google.gson.JsonSyntaxException; 071 072public class TestingUtilities { 073 private static final boolean SHOW_DIFF = false; 074 075 static public IWorkerContext fcontext; 076 077 public static IWorkerContext context() { 078 if (fcontext == null) { 079 FilesystemPackageCacheManager pcm; 080 try { 081 pcm = new FilesystemPackageCacheManager( 082 org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager.FilesystemPackageCacheMode.USER); 083 fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")); 084 fcontext 085 .setUcumService(new UcumEssenceService(TestingUtilities.resourceNameToFile("ucum", "ucum-essence.xml"))); 086 fcontext.setExpansionProfile(new Parameters()); 087 } catch (Exception e) { 088 throw new Error(e); 089 } 090 091 } 092 return fcontext; 093 } 094 095 static public boolean silent; 096 097 static public String fixedpath; 098 static public String contentpath; 099 100 public static String home() { 101 if (fixedpath != null) 102 return fixedpath; 103 String s = System.getenv("FHIR_HOME"); 104 if (!Utilities.noString(s)) 105 return s; 106 s = "C:\\work\\org.hl7.fhir\\build"; 107 if (new File(s).exists()) 108 return s; 109 throw new Error("FHIR Home directory not configured"); 110 } 111 112 public static String content() throws IOException { 113 if (contentpath != null) 114 return contentpath; 115 String s = "R:\\fhir\\publish"; 116 if (new File(s).exists()) 117 return s; 118 return Utilities.path(home(), "publish"); 119 } 120 121 // diretory that contains all the US implementation guides 122 public static String us() { 123 if (fixedpath != null) 124 return fixedpath; 125 String s = System.getenv("FHIR_HOME"); 126 if (!Utilities.noString(s)) 127 return s; 128 s = "C:\\work\\org.hl7.fhir.us"; 129 if (new File(s).exists()) 130 return s; 131 throw new Error("FHIR US directory not configured"); 132 } 133 134 public static String checkXMLIsSame(InputStream f1, InputStream f2) throws Exception { 135 String result = compareXml(f1, f2); 136 return result; 137 } 138 139 public static String checkXMLIsSame(String f1, String f2) throws Exception { 140 String result = compareXml(f1, f2); 141 if (result != null && SHOW_DIFF) { 142 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 143 List<String> command = new ArrayList<String>(); 144 command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\""); 145 146 ProcessBuilder builder = new ProcessBuilder(command); 147 builder.directory(new CSFile(Utilities.path("[tmp]"))); 148 builder.start(); 149 150 } 151 return result; 152 } 153 154 private static String compareXml(InputStream f1, InputStream f2) throws Exception { 155 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 156 } 157 158 private static String compareXml(String f1, String f2) throws Exception { 159 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 160 } 161 162 private static String compareElements(String path, Element e1, Element e2) { 163 if (!e1.getNamespaceURI().equals(e2.getNamespaceURI())) 164 return "Namespaces differ at " + path + ": " + e1.getNamespaceURI() + "/" + e2.getNamespaceURI(); 165 if (!e1.getLocalName().equals(e2.getLocalName())) 166 return "Names differ at " + path + ": " + e1.getLocalName() + "/" + e2.getLocalName(); 167 path = path + "/" + e1.getLocalName(); 168 String s = compareAttributes(path, e1.getAttributes(), e2.getAttributes()); 169 if (!Utilities.noString(s)) 170 return s; 171 s = compareAttributes(path, e2.getAttributes(), e1.getAttributes()); 172 if (!Utilities.noString(s)) 173 return s; 174 175 Node c1 = e1.getFirstChild(); 176 Node c2 = e2.getFirstChild(); 177 c1 = skipBlankText(c1); 178 c2 = skipBlankText(c2); 179 while (c1 != null && c2 != null) { 180 if (c1.getNodeType() != c2.getNodeType()) 181 return "node type mismatch in children of " + path + ": " + Integer.toString(e1.getNodeType()) + "/" 182 + Integer.toString(e2.getNodeType()); 183 if (c1.getNodeType() == Node.TEXT_NODE) { 184 if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent()))) 185 return "Text differs at " + path + ": " + normalise(c1.getTextContent()) + "/" 186 + normalise(c2.getTextContent()); 187 } else if (c1.getNodeType() == Node.ELEMENT_NODE) { 188 s = compareElements(path, (Element) c1, (Element) c2); 189 if (!Utilities.noString(s)) 190 return s; 191 } 192 193 c1 = skipBlankText(c1.getNextSibling()); 194 c2 = skipBlankText(c2.getNextSibling()); 195 } 196 if (c1 != null) 197 return "node mismatch - more nodes in source in children of " + path; 198 if (c2 != null) 199 return "node mismatch - more nodes in target in children of " + path; 200 return null; 201 } 202 203 private static Object normalise(String text) { 204 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 205 while (result.contains(" ")) 206 result = result.replace(" ", " "); 207 return result; 208 } 209 210 private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) { 211 for (int i = 0; i < src.getLength(); i++) { 212 213 Node sa = src.item(i); 214 String sn = sa.getNodeName(); 215 if (!(sn.equals("xmlns") || sn.startsWith("xmlns:"))) { 216 Node ta = tgt.getNamedItem(sn); 217 if (ta == null) 218 return "Attributes differ at " + path + ": missing attribute " + sn; 219 if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) { 220 byte[] b1 = unBase64(sa.getTextContent()); 221 byte[] b2 = unBase64(ta.getTextContent()); 222 if (!sameBytes(b1, b2)) 223 return "Attributes differ at " + path + ": value " + normalise(sa.getTextContent()) + "/" 224 + normalise(ta.getTextContent()); 225 } 226 } 227 } 228 return null; 229 } 230 231 private static boolean sameBytes(byte[] b1, byte[] b2) { 232 if (b1.length == 0 || b2.length == 0) 233 return false; 234 if (b1.length != b2.length) 235 return false; 236 for (int i = 0; i < b1.length; i++) 237 if (b1[i] != b2[i]) 238 return false; 239 return true; 240 } 241 242 private static byte[] unBase64(String text) { 243 return Base64.decodeBase64(text); 244 } 245 246 private static Node skipBlankText(Node node) { 247 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isAllWhitespace(node.getTextContent())) 248 || (node.getNodeType() == Node.COMMENT_NODE))) 249 node = node.getNextSibling(); 250 return node; 251 } 252 253 private static Document loadXml(String fn) throws Exception { 254 return loadXml(new FileInputStream(fn)); 255 } 256 257 private static Document loadXml(InputStream fn) throws Exception { 258 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 259 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 260 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 261 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 262 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 263 factory.setXIncludeAware(false); 264 factory.setExpandEntityReferences(false); 265 266 factory.setNamespaceAware(true); 267 DocumentBuilder builder = factory.newDocumentBuilder(); 268 return builder.parse(fn); 269 } 270 271 public static String checkJsonSrcIsSame(String s1, String s2) 272 throws JsonSyntaxException, FileNotFoundException, IOException { 273 return checkJsonSrcIsSame(s1, s2, true); 274 } 275 276 public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) 277 throws JsonSyntaxException, FileNotFoundException, IOException { 278 String result = compareJsonSrc(s1, s2); 279 if (result != null && SHOW_DIFF && showDiff) { 280 String diff = null; 281 if (System.getProperty("os.name").contains("Linux")) 282 diff = Utilities.path("/", "usr", "bin", "meld"); 283 else { 284 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), 285 "\\WinMergeU.exe", null)) 286 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 287 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), 288 "\\Meld.exe", null)) 289 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 290 } 291 if (diff == null || diff.isEmpty()) 292 return result; 293 294 List<String> command = new ArrayList<String>(); 295 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 296 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 297 TextFile.stringToFile(s1, f1); 298 TextFile.stringToFile(s2, f2); 299 command.add(diff); 300 if (diff.toLowerCase().contains("meld")) 301 command.add("--newtab"); 302 command.add(f1); 303 command.add(f2); 304 305 ProcessBuilder builder = new ProcessBuilder(command); 306 builder.directory(new CSFile(Utilities.path("[tmp]"))); 307 builder.start(); 308 309 } 310 return result; 311 } 312 313 public static String checkJsonIsSame(String f1, String f2) 314 throws JsonSyntaxException, FileNotFoundException, IOException { 315 String result = compareJson(f1, f2); 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 + "\" \"" + f1 + "\" \"" + f2 + "\""); 320 321 ProcessBuilder builder = new ProcessBuilder(command); 322 builder.directory(new CSFile(Utilities.path("[tmp]"))); 323 builder.start(); 324 325 } 326 return result; 327 } 328 329 private static String compareJsonSrc(String f1, String f2) 330 throws JsonSyntaxException, FileNotFoundException, IOException { 331 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1); 332 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2); 333 return compareObjects("", o1, o2); 334 } 335 336 private static String compareJson(String f1, String f2) 337 throws JsonSyntaxException, FileNotFoundException, IOException { 338 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1)); 339 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2)); 340 return compareObjects("", o1, o2); 341 } 342 343 private static String compareObjects(String path, JsonObject o1, JsonObject o2) { 344 for (Map.Entry<String, JsonElement> en : o1.entrySet()) { 345 String n = en.getKey(); 346 if (!n.equals("fhir_comments")) { 347 if (o2.has(n)) { 348 String s = compareNodes(path + '.' + n, en.getValue(), o2.get(n)); 349 if (!Utilities.noString(s)) 350 return s; 351 } else 352 return "properties differ at " + path + ": missing property " + n; 353 } 354 } 355 for (Map.Entry<String, JsonElement> en : o2.entrySet()) { 356 String n = en.getKey(); 357 if (!n.equals("fhir_comments")) { 358 if (!o1.has(n)) 359 return "properties differ at " + path + ": missing property " + n; 360 } 361 } 362 return null; 363 } 364 365 private static String compareNodes(String path, JsonElement n1, JsonElement n2) { 366 if (n1.getClass() != n2.getClass()) 367 return "properties differ at " + path + ": type " + n1.getClass().getName() + "/" + n2.getClass().getName(); 368 else if (n1 instanceof JsonPrimitive) { 369 JsonPrimitive p1 = (JsonPrimitive) n1; 370 JsonPrimitive p2 = (JsonPrimitive) n2; 371 if (p1.isBoolean() && p2.isBoolean()) { 372 if (p1.getAsBoolean() != p2.getAsBoolean()) 373 return "boolean property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 374 } else if (p1.isString() && p2.isString()) { 375 String s1 = p1.getAsString(); 376 String s2 = p2.getAsString(); 377 if (!(s1.contains("<div") && s2.contains("<div"))) 378 if (!s1.equals(s2)) 379 if (!sameBytes(unBase64(s1), unBase64(s2))) 380 return "string property values differ at " + path + ": type " + s1 + "/" + s2; 381 } else if (p1.isNumber() && p2.isNumber()) { 382 if (!p1.getAsString().equals(p2.getAsString())) 383 return "number property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 384 } else 385 return "property types differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 386 } else if (n1 instanceof JsonObject) { 387 String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2); 388 if (!Utilities.noString(s)) 389 return s; 390 } else if (n1 instanceof JsonArray) { 391 JsonArray a1 = (JsonArray) n1; 392 JsonArray a2 = (JsonArray) n2; 393 394 if (a1.size() != a2.size()) 395 return "array properties differ at " + path + ": count " + Integer.toString(a1.size()) + "/" 396 + Integer.toString(a2.size()); 397 for (int i = 0; i < a1.size(); i++) { 398 String s = compareNodes(path + "[" + Integer.toString(i) + "]", a1.get(i), a2.get(i)); 399 if (!Utilities.noString(s)) 400 return s; 401 } 402 } else if (n1 instanceof JsonNull) { 403 404 } else 405 return "unhandled property " + n1.getClass().getName(); 406 return null; 407 } 408 409 public static String checkTextIsSame(String s1, String s2) 410 throws JsonSyntaxException, FileNotFoundException, IOException { 411 return checkTextIsSame(s1, s2, true); 412 } 413 414 public static String checkTextIsSame(String s1, String s2, boolean showDiff) 415 throws JsonSyntaxException, FileNotFoundException, IOException { 416 String result = compareText(s1, s2); 417 if (result != null && SHOW_DIFF && showDiff) { 418 String diff = null; 419 if (System.getProperty("os.name").contains("Linux")) 420 diff = Utilities.path("/", "usr", "bin", "meld"); 421 else { 422 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), 423 "\\WinMergeU.exe", null)) 424 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 425 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), 426 "\\Meld.exe", null)) 427 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 428 } 429 if (diff == null || diff.isEmpty()) 430 return result; 431 432 List<String> command = new ArrayList<String>(); 433 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 434 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 435 TextFile.stringToFile(s1, f1); 436 TextFile.stringToFile(s2, f2); 437 command.add(diff); 438 if (diff.toLowerCase().contains("meld")) 439 command.add("--newtab"); 440 command.add(f1); 441 command.add(f2); 442 443 ProcessBuilder builder = new ProcessBuilder(command); 444 builder.directory(new CSFile(Utilities.path("[tmp]"))); 445 builder.start(); 446 447 } 448 return result; 449 } 450 451 private static String compareText(String s1, String s2) { 452 for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) { 453 if (s1.charAt(i) != s2.charAt(i)) 454 return "Strings differ at character " + Integer.toString(i) + ": '" + s1.charAt(i) + "' vs '" + s2.charAt(i) 455 + "'"; 456 } 457 if (s1.length() != s2.length()) 458 return "Strings differ in length: " + Integer.toString(s1.length()) + " vs " + Integer.toString(s2.length()) 459 + " but match to the end of the shortest"; 460 return null; 461 } 462 463 public static String resourceNameToFile(String name) throws IOException { 464 return resourceNameToFile(null, name); 465 } 466 467 private static boolean fileForPathExists(String path) { 468 return new File(path).exists(); 469 } 470 471 public static String generateResourcePath(String subFolder, String name) throws IOException { 472 String path = Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name); 473 BaseTestingUtilities.createParentDirIfNotExists(Paths.get(path)); 474 return path; 475 } 476 477 public static String resourceNameToFile(String subFolder, String name) throws IOException { 478 479 final String resourcePath = (subFolder != null ? subFolder + "/" : "") + name; 480 final String filePathFromClassLoader = TestingUtilities.class.getClassLoader().getResource(resourcePath).getPath(); 481 482 if (fileForPathExists(filePathFromClassLoader)) { 483 return filePathFromClassLoader; 484 } else { 485 final Path newFilePath = (subFolder != null) ? Paths.get("target", subFolder, name) : Paths.get("target", name); 486 copyResourceToNewFile(resourcePath, newFilePath); 487 return newFilePath.toString(); 488 } 489 } 490 491 private static void copyResourceToNewFile(String resourcePath, Path newFilePath) throws IOException { 492 BaseTestingUtilities.createParentDirIfNotExists(newFilePath); 493 ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath); 494 } 495 496 public static String loadTestResource(String... paths) throws IOException { 497 /** 498 * This 'if' condition checks to see if the fhir-test-cases project 499 * (https://github.com/FHIR/fhir-test-cases) is installed locally at the same 500 * directory level as the core library project is. If so, the test case data is 501 * read directly from that project, instead of the imported maven dependency 502 * jar. It is important, that if you want to test against the dependency 503 * imported from sonatype nexus, instead of your local copy, you need to either 504 * change the name of the project directory to something other than 505 * 'fhir-test-cases', or move it to another location, not at the same directory 506 * level as the core project. 507 */ 508 509 String dir = TestConfig.getInstance().getFhirTestCasesDirectory(); 510 if (dir == null && FhirSettings.hasFhirTestCasesPath()) { 511 dir = FhirSettings.getFhirTestCasesPath(); 512 } 513 if (dir != null && new CSFile(dir).exists()) { 514 String n = Utilities.path(dir, Utilities.path(paths)); 515 // ok, we'll resolve this locally 516 return TextFile.fileToString(new CSFile(n)); 517 } else { 518 // resolve from the package 519 String contents; 520 String classpath = ("/org/hl7/fhir/testcases/" + Utilities.pathURL(paths)); 521 try (InputStream inputStream = BaseTestingUtilities.class.getResourceAsStream(classpath)) { 522 if (inputStream == null) { 523 throw new IOException("Can't find file on classpath: " + classpath); 524 } 525 contents = IOUtils.toString(inputStream, java.nio.charset.StandardCharsets.UTF_8); 526 } 527 return contents; 528 } 529 } 530 531}