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