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.CSFile; 055import org.hl7.fhir.utilities.TextFile; 056import org.hl7.fhir.utilities.ToolGlobalSettings; 057import org.hl7.fhir.utilities.Utilities; 058import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; 059import org.hl7.fhir.utilities.npm.ToolsVersion; 060import org.hl7.fhir.utilities.tests.BaseTestingUtilities; 061import org.hl7.fhir.utilities.tests.ResourceLoaderTests; 062import org.hl7.fhir.utilities.tests.TestConfig; 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(true, ToolsVersion.TOOLS_VERSION); 085 fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")); 086 fcontext.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 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())+"/"+Integer.toString(e2.getNodeType()); 182 if (c1.getNodeType() == Node.TEXT_NODE) { 183 if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent()))) 184 return "Text differs at "+path+": "+normalise(c1.getTextContent()) +"/"+ normalise(c2.getTextContent()); 185 } 186 else if (c1.getNodeType() == Node.ELEMENT_NODE) { 187 s = compareElements(path, (Element) c1, (Element) c2); 188 if (!Utilities.noString(s)) 189 return s; 190 } 191 192 c1 = skipBlankText(c1.getNextSibling()); 193 c2 = skipBlankText(c2.getNextSibling()); 194 } 195 if (c1 != null) 196 return "node mismatch - more nodes in source in children of "+path; 197 if (c2 != null) 198 return "node mismatch - more nodes in target in children of "+path; 199 return null; 200 } 201 202 private static Object normalise(String text) { 203 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 204 while (result.contains(" ")) 205 result = result.replace(" ", " "); 206 return result; 207 } 208 209 private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) { 210 for (int i = 0; i < src.getLength(); i++) { 211 212 Node sa = src.item(i); 213 String sn = sa.getNodeName(); 214 if (! (sn.equals("xmlns") || sn.startsWith("xmlns:"))) { 215 Node ta = tgt.getNamedItem(sn); 216 if (ta == null) 217 return "Attributes differ at "+path+": missing attribute "+sn; 218 if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) { 219 byte[] b1 = unBase64(sa.getTextContent()); 220 byte[] b2 = unBase64(ta.getTextContent()); 221 if (!sameBytes(b1, b2)) 222 return "Attributes differ at "+path+": value "+normalise(sa.getTextContent()) +"/"+ normalise(ta.getTextContent()); 223 } 224 } 225 } 226 return null; 227 } 228 229 private static boolean sameBytes(byte[] b1, byte[] b2) { 230 if (b1.length == 0 || b2.length == 0) 231 return false; 232 if (b1.length != b2.length) 233 return false; 234 for (int i = 0; i < b1.length; i++) 235 if (b1[i] != b2[i]) 236 return false; 237 return true; 238 } 239 240 private static byte[] unBase64(String text) { 241 return Base64.decodeBase64(text); 242 } 243 244 private static Node skipBlankText(Node node) { 245 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 246 node = node.getNextSibling(); 247 return node; 248 } 249 250 private static Document loadXml(String fn) throws Exception { 251 return loadXml(new FileInputStream(fn)); 252 } 253 254 private static Document loadXml(InputStream fn) throws Exception { 255 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 256 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 257 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 258 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 259 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 260 factory.setXIncludeAware(false); 261 factory.setExpandEntityReferences(false); 262 263 factory.setNamespaceAware(true); 264 DocumentBuilder builder = factory.newDocumentBuilder(); 265 return builder.parse(fn); 266 } 267 268 public static String checkJsonSrcIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 269 return checkJsonSrcIsSame(s1,s2,true); 270 } 271 272 public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 273 String result = compareJsonSrc(s1, s2); 274 if (result != null && SHOW_DIFF && showDiff) { 275 String diff = null; 276 if (System.getProperty("os.name").contains("Linux")) 277 diff = Utilities.path("/", "usr", "bin", "meld"); 278 else { 279 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 280 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 281 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 282 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 283 } 284 if (diff == null || diff.isEmpty()) 285 return result; 286 287 List<String> command = new ArrayList<String>(); 288 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 289 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 290 TextFile.stringToFile(s1, f1); 291 TextFile.stringToFile(s2, f2); 292 command.add(diff); 293 if (diff.toLowerCase().contains("meld")) 294 command.add("--newtab"); 295 command.add(f1); 296 command.add(f2); 297 298 ProcessBuilder builder = new ProcessBuilder(command); 299 builder.directory(new CSFile(Utilities.path("[tmp]"))); 300 builder.start(); 301 302 } 303 return result; 304 } 305 public static String checkJsonIsSame(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 306 String result = compareJson(f1, f2); 307 if (result != null && SHOW_DIFF) { 308 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 309 List<String> command = new ArrayList<String>(); 310 command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\""); 311 312 ProcessBuilder builder = new ProcessBuilder(command); 313 builder.directory(new CSFile(Utilities.path("[tmp]"))); 314 builder.start(); 315 316 } 317 return result; 318 } 319 320 private static String compareJsonSrc(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 321 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1); 322 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2); 323 return compareObjects("", o1, o2); 324 } 325 326 private static String compareJson(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 327 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1)); 328 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2)); 329 return compareObjects("", o1, o2); 330 } 331 332 private static String compareObjects(String path, JsonObject o1, JsonObject o2) { 333 for (Map.Entry<String, JsonElement> en : o1.entrySet()) { 334 String n = en.getKey(); 335 if (!n.equals("fhir_comments")) { 336 if (o2.has(n)) { 337 String s = compareNodes(path+'.'+n, en.getValue(), o2.get(n)); 338 if (!Utilities.noString(s)) 339 return s; 340 } 341 else 342 return "properties differ at "+path+": missing property "+n; 343 } 344 } 345 for (Map.Entry<String, JsonElement> en : o2.entrySet()) { 346 String n = en.getKey(); 347 if (!n.equals("fhir_comments")) { 348 if (!o1.has(n)) 349 return "properties differ at "+path+": missing property "+n; 350 } 351 } 352 return null; 353 } 354 355 private static String compareNodes(String path, JsonElement n1, JsonElement n2) { 356 if (n1.getClass() != n2.getClass()) 357 return "properties differ at "+path+": type "+n1.getClass().getName()+"/"+n2.getClass().getName(); 358 else if (n1 instanceof JsonPrimitive) { 359 JsonPrimitive p1 = (JsonPrimitive) n1; 360 JsonPrimitive p2 = (JsonPrimitive) n2; 361 if (p1.isBoolean() && p2.isBoolean()) { 362 if (p1.getAsBoolean() != p2.getAsBoolean()) 363 return "boolean property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 364 } else if (p1.isString() && p2.isString()) { 365 String s1 = p1.getAsString(); 366 String s2 = p2.getAsString(); 367 if (!(s1.contains("<div") && s2.contains("<div"))) 368 if (!s1.equals(s2)) 369 if (!sameBytes(unBase64(s1), unBase64(s2))) 370 return "string property values differ at "+path+": type "+s1+"/"+s2; 371 } else if (p1.isNumber() && p2.isNumber()) { 372 if (!p1.getAsString().equals(p2.getAsString())) 373 return "number property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 374 } else 375 return "property types differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 376 } 377 else if (n1 instanceof JsonObject) { 378 String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2); 379 if (!Utilities.noString(s)) 380 return s; 381 } else if (n1 instanceof JsonArray) { 382 JsonArray a1 = (JsonArray) n1; 383 JsonArray a2 = (JsonArray) n2; 384 385 if (a1.size() != a2.size()) 386 return "array properties differ at "+path+": count "+Integer.toString(a1.size())+"/"+Integer.toString(a2.size()); 387 for (int i = 0; i < a1.size(); i++) { 388 String s = compareNodes(path+"["+Integer.toString(i)+"]", a1.get(i), a2.get(i)); 389 if (!Utilities.noString(s)) 390 return s; 391 } 392 } 393 else if (n1 instanceof JsonNull) { 394 395 } else 396 return "unhandled property "+n1.getClass().getName(); 397 return null; 398 } 399 400 401 public static String checkTextIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 402 return checkTextIsSame(s1,s2,true); 403 } 404 405 public static String checkTextIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 406 String result = compareText(s1, s2); 407 if (result != null && SHOW_DIFF && showDiff) { 408 String diff = null; 409 if (System.getProperty("os.name").contains("Linux")) 410 diff = Utilities.path("/", "usr", "bin", "meld"); 411 else { 412 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 413 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 414 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 415 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 416 } 417 if (diff == null || diff.isEmpty()) 418 return result; 419 420 List<String> command = new ArrayList<String>(); 421 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 422 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 423 TextFile.stringToFile(s1, f1); 424 TextFile.stringToFile(s2, f2); 425 command.add(diff); 426 if (diff.toLowerCase().contains("meld")) 427 command.add("--newtab"); 428 command.add(f1); 429 command.add(f2); 430 431 ProcessBuilder builder = new ProcessBuilder(command); 432 builder.directory(new CSFile(Utilities.path("[tmp]"))); 433 builder.start(); 434 435 } 436 return result; 437 } 438 439 440 private static String compareText(String s1, String s2) { 441 for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) { 442 if (s1.charAt(i) != s2.charAt(i)) 443 return "Strings differ at character "+Integer.toString(i)+": '"+s1.charAt(i) +"' vs '"+s2.charAt(i)+"'"; 444 } 445 if (s1.length() != s2.length()) 446 return "Strings differ in length: "+Integer.toString(s1.length())+" vs "+Integer.toString(s2.length())+" but match to the end of the shortest"; 447 return null; 448 } 449 450 public static String resourceNameToFile(String name) throws IOException { 451 return resourceNameToFile(null, name); 452 } 453 454 private static boolean fileForPathExists(String path) { 455 return new File(path).exists(); 456 } 457 458 public static String generateResourcePath(String subFolder, String name) throws IOException { 459 String path = Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name); 460 BaseTestingUtilities.createParentDirIfNotExists(Paths.get(path)); 461 return path; 462 } 463 public static String resourceNameToFile(String subFolder, String name) throws IOException { 464 465 final String resourcePath = (subFolder != null ? subFolder + "/" : "") + name; 466 final String filePathFromClassLoader = TestingUtilities.class.getClassLoader().getResource(resourcePath).getPath(); 467 468 if (fileForPathExists(filePathFromClassLoader)) { 469 return filePathFromClassLoader; 470 } else { 471 final Path newFilePath = (subFolder != null) ? Paths.get("target", subFolder, name) : Paths.get("target", name); 472 copyResourceToNewFile(resourcePath, newFilePath); 473 return newFilePath.toString(); 474 } 475 } 476 477 private static void copyResourceToNewFile(String resourcePath, Path newFilePath) throws IOException { 478 BaseTestingUtilities.createParentDirIfNotExists(newFilePath); 479 ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath); 480 } 481 482 public static String loadTestResource(String... paths) throws IOException { 483 /** 484 * This 'if' condition checks to see if the fhir-test-cases project (https://github.com/FHIR/fhir-test-cases) is 485 * installed locally at the same directory level as the core library project is. If so, the test case data is read 486 * directly from that project, instead of the imported maven dependency jar. It is important, that if you want to 487 * test against the dependency imported from sonatype nexus, instead of your local copy, you need to either change 488 * the name of the project directory to something other than 'fhir-test-cases', or move it to another location, not 489 * at the same directory level as the core project. 490 */ 491 492 String dir = TestConfig.getInstance().getFhirTestCasesDirectory(); 493 if (dir == null && ToolGlobalSettings.hasTestsPath()) { 494 dir = ToolGlobalSettings.getTestsPath(); 495 } 496 if (dir != null && new CSFile(dir).exists()) { 497 String n = Utilities.path(dir, Utilities.path(paths)); 498 // ok, we'll resolve this locally 499 return TextFile.fileToString(new CSFile(n)); 500 } else { 501 // resolve from the package 502 String contents; 503 String classpath = ("/org/hl7/fhir/testcases/" + Utilities.pathURL(paths)); 504 try (InputStream inputStream = BaseTestingUtilities.class.getResourceAsStream(classpath)) { 505 if (inputStream == null) { 506 throw new IOException("Can't find file on classpath: " + classpath); 507 } 508 contents = IOUtils.toString(inputStream, java.nio.charset.StandardCharsets.UTF_8); 509 } 510 return contents; 511 } 512 } 513 514 515 516}