001package org.hl7.fhir.r5.test.utils; 002 003import java.io.File; 004import java.io.FileInputStream; 005import java.io.FileNotFoundException; 006import java.io.IOException; 007import java.io.InputStream; 008import java.util.ArrayList; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Map; 012 013import javax.xml.parsers.DocumentBuilder; 014import javax.xml.parsers.DocumentBuilderFactory; 015 016import org.apache.commons.codec.binary.Base64; 017import org.fhir.ucum.UcumEssenceService; 018import org.hl7.fhir.r5.context.IWorkerContext; 019import org.hl7.fhir.r5.context.SimpleWorkerContext; 020import org.hl7.fhir.r5.context.TerminologyCache; 021import org.hl7.fhir.r5.model.Parameters; 022import org.hl7.fhir.utilities.CSFile; 023import org.hl7.fhir.utilities.TextFile; 024import org.hl7.fhir.utilities.ToolGlobalSettings; 025import org.hl7.fhir.utilities.Utilities; 026import org.hl7.fhir.utilities.VersionUtilities; 027import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; 028import org.hl7.fhir.utilities.npm.NpmPackage; 029import org.hl7.fhir.utilities.npm.ToolsVersion; 030import org.hl7.fhir.utilities.tests.BaseTestingUtilities; 031import org.w3c.dom.Document; 032import org.w3c.dom.Element; 033import org.w3c.dom.NamedNodeMap; 034import org.w3c.dom.Node; 035 036/* 037 Copyright (c) 2011+, HL7, Inc. 038 All rights reserved. 039 040 Redistribution and use in source and binary forms, with or without modification, 041 are permitted provided that the following conditions are met: 042 043 * Redistributions of source code must retain the above copyright notice, this 044 list of conditions and the following disclaimer. 045 * Redistributions in binary form must reproduce the above copyright notice, 046 this list of conditions and the following disclaimer in the documentation 047 and/or other materials provided with the distribution. 048 * Neither the name of HL7 nor the names of its contributors may be used to 049 endorse or promote products derived from this software without specific 050 prior written permission. 051 052 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 053 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 054 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 055 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 056 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 057 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 058 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 059 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 060 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 061 POSSIBILITY OF SUCH DAMAGE. 062 063 */ 064 065 066import com.google.gson.JsonArray; 067import com.google.gson.JsonElement; 068import com.google.gson.JsonNull; 069import com.google.gson.JsonObject; 070import com.google.gson.JsonPrimitive; 071import com.google.gson.JsonSyntaxException; 072 073public class TestingUtilities extends BaseTestingUtilities { 074 private static final boolean SHOW_DIFF = true; 075 076 static public Map<String, IWorkerContext> fcontexts; 077 078 public static IWorkerContext context() { 079 return context("4.0.1"); 080 } 081 082 public static IWorkerContext context(String version) { 083 if ("4.5.0".equals(version)) { 084 version = "4.4.0"; // temporary work around 085 } 086 087 String v = VersionUtilities.getMajMin(version); 088 if (fcontexts == null) { 089 fcontexts = new HashMap<>(); 090 } 091 if (!fcontexts.containsKey(v)) { 092 FilesystemPackageCacheManager pcm; 093 try { 094 pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); 095 IWorkerContext fcontext = getWorkerContext(pcm.loadPackage(VersionUtilities.packageForVersion(version), version)); 096 fcontext.setUcumService(new UcumEssenceService(TestingUtilities.loadTestResourceStream("ucum", "ucum-essence.xml"))); 097 fcontext.setExpansionProfile(new Parameters()); 098// ((SimpleWorkerContext) fcontext).connectToTSServer(new TerminologyClientR5("http://tx.fhir.org/r4"), null); 099 fcontexts.put(v, fcontext); 100 } catch (Exception e) { 101 e.printStackTrace(); 102 throw new Error(e); 103 } 104 } 105 return fcontexts.get(v); 106 } 107 108 public static SimpleWorkerContext getWorkerContext(NpmPackage npmPackage) throws Exception { 109 SimpleWorkerContext swc = new SimpleWorkerContext.SimpleWorkerContextBuilder().withUserAgent(TestConstants.USER_AGENT).withTerminologyCachePath(TestConstants.TX_CACHE).fromPackage(npmPackage); 110 TerminologyCache.setCacheErrors(true); 111 swc.setAllowLoadingDuplicates(true); 112 return swc; 113 } 114 115 public static SimpleWorkerContext getWorkerContext(NpmPackage npmPackage, IWorkerContext.IContextResourceLoader loader) throws Exception { 116 SimpleWorkerContext swc = new SimpleWorkerContext.SimpleWorkerContextBuilder().withUserAgent(TestConstants.USER_AGENT).withTerminologyCachePath(TestConstants.TX_CACHE).fromPackage(npmPackage, loader); 117 TerminologyCache.setCacheErrors(true); 118 swc.setAllowLoadingDuplicates(true); 119 return swc; 120 } 121 122 static public String fixedpath; 123 static public String contentpath; 124 125 public static String home() { 126 if (fixedpath != null) 127 return fixedpath; 128 String s = System.getenv("FHIR_HOME"); 129 if (!Utilities.noString(s)) 130 return s; 131 s = "C:\\work\\org.hl7.fhir\\build"; 132 // FIXME: change this back 133 s = "/Users/jamesagnew/git/fhir"; 134 if (new File(s).exists()) 135 return s; 136 throw new Error("FHIR Home directory not configured"); 137 } 138 139 140 public static String content() throws IOException { 141 if (contentpath != null) 142 return contentpath; 143 String s = "R:\\fhir\\publish"; 144 if (new File(s).exists()) 145 return s; 146 return Utilities.path(home(), "publish"); 147 } 148 149 // diretory that contains all the US implementation guides 150 public static String us() { 151 if (fixedpath != null) 152 return fixedpath; 153 String s = System.getenv("FHIR_HOME"); 154 if (!Utilities.noString(s)) 155 return s; 156 s = "C:\\work\\org.hl7.fhir.us"; 157 if (new File(s).exists()) 158 return s; 159 throw new Error("FHIR US directory not configured"); 160 } 161 162 public static String checkXMLIsSame(InputStream f1, InputStream f2) throws Exception { 163 String result = compareXml(f1, f2); 164 return result; 165 } 166 167 public static String checkXMLIsSame(String f1, String f2) throws Exception { 168 String result = compareXml(f1, f2); 169 if (result != null && SHOW_DIFF) { 170 String diff = ToolGlobalSettings.hasComparePath() ? ToolGlobalSettings.getComparePath() : Utilities.path(System.getenv("ProgramFiles"), "WinMerge", "WinMergeU.exe"); 171 if (new File(diff).exists() || Utilities.isToken(diff)) { 172 List<String> command = new ArrayList<String>(); 173 Process p = Runtime.getRuntime().exec(new String[]{diff, f1, f2}); 174 } 175 } 176 return result; 177 } 178 179 private static String compareXml(InputStream f1, InputStream f2) throws Exception { 180 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 181 } 182 183 private static String compareXml(String f1, String f2) throws Exception { 184 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 185 } 186 187 private static String compareElements(String path, Element e1, Element e2) { 188 if (!namespacesMatch(e1.getNamespaceURI(), e2.getNamespaceURI())) 189 return "Namespaces differ at " + path + ": " + e1.getNamespaceURI() + "/" + e2.getNamespaceURI(); 190 if (!e1.getLocalName().equals(e2.getLocalName())) 191 return "Names differ at " + path + ": " + e1.getLocalName() + "/" + e2.getLocalName(); 192 path = path + "/" + e1.getLocalName(); 193 String s = compareAttributes(path, e1.getAttributes(), e2.getAttributes()); 194 if (!Utilities.noString(s)) 195 return s; 196 s = compareAttributes(path, e2.getAttributes(), e1.getAttributes()); 197 if (!Utilities.noString(s)) 198 return s; 199 200 Node c1 = e1.getFirstChild(); 201 Node c2 = e2.getFirstChild(); 202 c1 = skipBlankText(c1); 203 c2 = skipBlankText(c2); 204 while (c1 != null && c2 != null) { 205 if (c1.getNodeType() != c2.getNodeType()) 206 return "node type mismatch in children of " + path + ": " + Integer.toString(e1.getNodeType()) + "/" + Integer.toString(e2.getNodeType()); 207 if (c1.getNodeType() == Node.TEXT_NODE) { 208 if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent()))) 209 return "Text differs at " + path + ": " + normalise(c1.getTextContent()) + "/" + normalise(c2.getTextContent()); 210 } else if (c1.getNodeType() == Node.ELEMENT_NODE) { 211 s = compareElements(path, (Element) c1, (Element) c2); 212 if (!Utilities.noString(s)) 213 return s; 214 } 215 216 c1 = skipBlankText(c1.getNextSibling()); 217 c2 = skipBlankText(c2.getNextSibling()); 218 } 219 if (c1 != null) 220 return "node mismatch - more nodes in source in children of " + path; 221 if (c2 != null) 222 return "node mismatch - more nodes in target in children of " + path; 223 return null; 224 } 225 226 private static boolean namespacesMatch(String ns1, String ns2) { 227 return ns1 == null ? ns2 == null : ns1.equals(ns2); 228 } 229 230 private static Object normalise(String text) { 231 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 232 while (result.contains(" ")) 233 result = result.replace(" ", " "); 234 return result; 235 } 236 237 private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) { 238 for (int i = 0; i < src.getLength(); i++) { 239 240 Node sa = src.item(i); 241 String sn = sa.getNodeName(); 242 if (!(sn.equals("xmlns") || sn.startsWith("xmlns:"))) { 243 Node ta = tgt.getNamedItem(sn); 244 if (ta == null) 245 return "Attributes differ at " + path + ": missing attribute " + sn; 246 if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) { 247 byte[] b1 = unBase64(sa.getTextContent()); 248 byte[] b2 = unBase64(ta.getTextContent()); 249 if (!sameBytes(b1, b2)) 250 return "Attributes differ at " + path + ": value " + normalise(sa.getTextContent()) + "/" + normalise(ta.getTextContent()); 251 } 252 } 253 } 254 return null; 255 } 256 257 private static boolean sameBytes(byte[] b1, byte[] b2) { 258 if (b1.length == 0 || b2.length == 0) 259 return false; 260 if (b1.length != b2.length) 261 return false; 262 for (int i = 0; i < b1.length; i++) 263 if (b1[i] != b2[i]) 264 return false; 265 return true; 266 } 267 268 private static byte[] unBase64(String text) { 269 return Base64.decodeBase64(text); 270 } 271 272 private static Node skipBlankText(Node node) { 273 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 274 node = node.getNextSibling(); 275 return node; 276 } 277 278 private static Document loadXml(String fn) throws Exception { 279 return loadXml(new FileInputStream(fn)); 280 } 281 282 private static Document loadXml(InputStream fn) throws Exception { 283 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 284 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 285 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 286 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 287 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 288 factory.setXIncludeAware(false); 289 factory.setExpandEntityReferences(false); 290 291 factory.setNamespaceAware(true); 292 DocumentBuilder builder = factory.newDocumentBuilder(); 293 return builder.parse(fn); 294 } 295 296 public static String checkJsonSrcIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 297 return checkJsonSrcIsSame(s1, s2, true); 298 } 299 300 public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 301 String result = compareJsonSrc(s1, s2); 302 if (result != null && SHOW_DIFF && showDiff) { 303 String diff = null; 304 if (System.getProperty("os.name").contains("Linux")) 305 diff = Utilities.path("/", "usr", "bin", "meld"); 306 else { 307 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 308 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 309 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 310 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 311 } 312 if (diff == null || diff.isEmpty()) 313 return result; 314 315 List<String> command = new ArrayList<String>(); 316 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 317 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 318 TextFile.stringToFile(s1, f1); 319 TextFile.stringToFile(s2, f2); 320 command.add(diff); 321 if (diff.toLowerCase().contains("meld")) 322 command.add("--newtab"); 323 command.add(f1); 324 command.add(f2); 325 326 ProcessBuilder builder = new ProcessBuilder(command); 327 builder.directory(new CSFile(Utilities.path("[tmp]"))); 328 builder.start(); 329 330 } 331 return result; 332 } 333 334 public static String checkJsonIsSame(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 335 String result = compareJson(f1, f2); 336 if (result != null && SHOW_DIFF) { 337 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 338 List<String> command = new ArrayList<String>(); 339 command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\""); 340 341 ProcessBuilder builder = new ProcessBuilder(command); 342 builder.directory(new CSFile("c:\\temp")); 343 builder.start(); 344 345 } 346 return result; 347 } 348 349 private static String compareJsonSrc(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 350 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1); 351 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2); 352 return compareObjects("", o1, o2); 353 } 354 355 private static String compareJson(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 356 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1)); 357 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2)); 358 return compareObjects("", o1, o2); 359 } 360 361 private static String compareObjects(String path, JsonObject o1, JsonObject o2) { 362 for (Map.Entry<String, JsonElement> en : o1.entrySet()) { 363 String n = en.getKey(); 364 if (!n.equals("fhir_comments")) { 365 if (o2.has(n)) { 366 String s = compareNodes(path + '.' + n, en.getValue(), o2.get(n)); 367 if (!Utilities.noString(s)) 368 return s; 369 } else 370 return "properties differ at " + path + ": missing property " + n; 371 } 372 } 373 for (Map.Entry<String, JsonElement> en : o2.entrySet()) { 374 String n = en.getKey(); 375 if (!n.equals("fhir_comments")) { 376 if (!o1.has(n)) 377 return "properties differ at " + path + ": missing property " + n; 378 } 379 } 380 return null; 381 } 382 383 private static String compareNodes(String path, JsonElement n1, JsonElement n2) { 384 if (n1.getClass() != n2.getClass()) 385 return "properties differ at " + path + ": type " + n1.getClass().getName() + "/" + n2.getClass().getName(); 386 else if (n1 instanceof JsonPrimitive) { 387 JsonPrimitive p1 = (JsonPrimitive) n1; 388 JsonPrimitive p2 = (JsonPrimitive) n2; 389 if (p1.isBoolean() && p2.isBoolean()) { 390 if (p1.getAsBoolean() != p2.getAsBoolean()) 391 return "boolean property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 392 } else if (p1.isString() && p2.isString()) { 393 String s1 = p1.getAsString(); 394 String s2 = p2.getAsString(); 395 if (!(s1.contains("<div") && s2.contains("<div"))) 396 if (!s1.equals(s2)) 397 if (!sameBytes(unBase64(s1), unBase64(s2))) 398 return "string property values differ at " + path + ": type " + s1 + "/" + s2; 399 } else if (p1.isNumber() && p2.isNumber()) { 400 if (!p1.getAsString().equals(p2.getAsString())) 401 return "number property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 402 } else 403 return "property types differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString(); 404 } else if (n1 instanceof JsonObject) { 405 String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2); 406 if (!Utilities.noString(s)) 407 return s; 408 } else if (n1 instanceof JsonArray) { 409 JsonArray a1 = (JsonArray) n1; 410 JsonArray a2 = (JsonArray) n2; 411 412 if (a1.size() != a2.size()) 413 return "array properties differ at " + path + ": count " + Integer.toString(a1.size()) + "/" + Integer.toString(a2.size()); 414 for (int i = 0; i < a1.size(); i++) { 415 String s = compareNodes(path + "[" + Integer.toString(i) + "]", a1.get(i), a2.get(i)); 416 if (!Utilities.noString(s)) 417 return s; 418 } 419 } else if (n1 instanceof JsonNull) { 420 421 } else 422 return "unhandled property " + n1.getClass().getName(); 423 return null; 424 } 425 426 public static String temp() { 427 if (new File("c:\\temp").exists()) 428 return "c:\\temp"; 429 return System.getProperty("java.io.tmpdir"); 430 } 431 432 public static String checkTextIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 433 return checkTextIsSame(s1, s2, true); 434 } 435 436 public static String checkTextIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 437 String result = compareText(s1, s2); 438 if (result != null && SHOW_DIFF && showDiff) { 439 String diff = null; 440 if (System.getProperty("os.name").contains("Linux")) 441 diff = Utilities.path("/", "usr", "bin", "meld"); 442 else { 443 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 444 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 445 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 446 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 447 } 448 if (diff == null || diff.isEmpty()) 449 return result; 450 451 List<String> command = new ArrayList<String>(); 452 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 453 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 454 TextFile.stringToFile(s1, f1); 455 TextFile.stringToFile(s2, f2); 456 command.add(diff); 457 if (diff.toLowerCase().contains("meld")) 458 command.add("--newtab"); 459 command.add(f1); 460 command.add(f2); 461 462 ProcessBuilder builder = new ProcessBuilder(command); 463 builder.directory(new CSFile(Utilities.path("[tmp]"))); 464 builder.start(); 465 466 } 467 return result; 468 } 469 470 471 private static String compareText(String s1, String s2) { 472 for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) { 473 if (s1.charAt(i) != s2.charAt(i)) 474 return "Strings differ at character " + Integer.toString(i) + ": '" + s1.charAt(i) + "' vs '" + s2.charAt(i) + "'"; 475 } 476 if (s1.length() != s2.length()) 477 return "Strings differ in length: " + Integer.toString(s1.length()) + " vs " + Integer.toString(s2.length()) + " but match to the end of the shortest"; 478 return null; 479 } 480 481 public static String tempFile(String folder, String name) throws IOException { 482 String tmp = tempFolder(folder); 483 return Utilities.path(tmp, name); 484 } 485 486 public static String tempFolder(String name) throws IOException { 487 File tmp = new File("C:\\temp"); 488 if (tmp.exists() && tmp.isDirectory()) { 489 String path = Utilities.path("C:\\temp", name); 490 Utilities.createDirectory(path); 491 return path; 492 } else if (ToolGlobalSettings.hasTempPath()) { 493 return ToolGlobalSettings.getTempPath(); 494 } else if (new File("/tmp").exists()) { 495 String path = Utilities.path("/tmp", name); 496 Utilities.createDirectory(path); 497 return path; 498 } else { 499 String path = Utilities.path(System.getProperty("java.io.tmpdir"), name); 500 Utilities.createDirectory(path); 501 return path; 502 } 503 } 504}