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