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