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