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