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.util.ArrayList; 039import java.util.List; 040import java.util.Map; 041 042import javax.xml.parsers.DocumentBuilder; 043import javax.xml.parsers.DocumentBuilderFactory; 044 045import org.apache.commons.codec.binary.Base64; 046import org.fhir.ucum.UcumEssenceService; 047import org.fhir.ucum.UcumException; 048import org.hl7.fhir.exceptions.FHIRException; 049import org.hl7.fhir.r4.context.IWorkerContext; 050import org.hl7.fhir.r4.context.SimpleWorkerContext; 051import org.hl7.fhir.r4.model.Parameters; 052import org.hl7.fhir.utilities.CSFile; 053import org.hl7.fhir.utilities.TextFile; 054import org.hl7.fhir.utilities.Utilities; 055import org.hl7.fhir.utilities.cache.PackageCacheManager; 056import org.hl7.fhir.utilities.cache.ToolsVersion; 057import org.w3c.dom.Document; 058import org.w3c.dom.Element; 059import org.w3c.dom.NamedNodeMap; 060import org.w3c.dom.Node; 061 062import com.google.gson.JsonArray; 063import com.google.gson.JsonElement; 064import com.google.gson.JsonNull; 065import com.google.gson.JsonObject; 066import com.google.gson.JsonPrimitive; 067import com.google.gson.JsonSyntaxException; 068 069public class TestingUtilities { 070 private static final boolean SHOW_DIFF = true; 071 072 static public IWorkerContext fcontext; 073 074 public static IWorkerContext context() { 075 if (fcontext == null) { 076 PackageCacheManager pcm; 077 try { 078 pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION); 079 fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.core", "4.0.0")); 080 fcontext.setUcumService(new UcumEssenceService(TestingUtilities.resourceNameToFile("ucum", "ucum-essence.xml"))); 081 fcontext.setExpansionProfile(new Parameters()); 082 } catch (Exception e) { 083 throw new Error(e); 084 } 085 086 } 087 return fcontext; 088 } 089 static public boolean silent; 090 091 static public String fixedpath; 092 static public String contentpath; 093 094 public static String home() { 095 if (fixedpath != null) 096 return fixedpath; 097 String s = System.getenv("FHIR_HOME"); 098 if (!Utilities.noString(s)) 099 return s; 100 s = "C:\\work\\org.hl7.fhir\\build"; 101 if (new File(s).exists()) 102 return s; 103 throw new Error("FHIR Home directory not configured"); 104 } 105 106 public static String content() throws IOException { 107 if (contentpath != null) 108 return contentpath; 109 String s = "R:\\fhir\\publish"; 110 if (new File(s).exists()) 111 return s; 112 return Utilities.path(home(), "publish"); 113 } 114 115 // diretory that contains all the US implementation guides 116 public static String us() { 117 if (fixedpath != null) 118 return fixedpath; 119 String s = System.getenv("FHIR_HOME"); 120 if (!Utilities.noString(s)) 121 return s; 122 s = "C:\\work\\org.hl7.fhir.us"; 123 if (new File(s).exists()) 124 return s; 125 throw new Error("FHIR US directory not configured"); 126 } 127 128 public static String checkXMLIsSame(InputStream f1, InputStream f2) throws Exception { 129 String result = compareXml(f1, f2); 130 return result; 131 } 132 133 public static String checkXMLIsSame(String f1, String f2) throws Exception { 134 String result = compareXml(f1, f2); 135 if (result != null && SHOW_DIFF) { 136 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 137 List<String> command = new ArrayList<String>(); 138 command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\""); 139 140 ProcessBuilder builder = new ProcessBuilder(command); 141 builder.directory(new CSFile("c:\\temp")); 142 builder.start(); 143 144 } 145 return result; 146 } 147 148 private static String compareXml(InputStream f1, InputStream f2) throws Exception { 149 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 150 } 151 152 private static String compareXml(String f1, String f2) throws Exception { 153 return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement()); 154 } 155 156 private static String compareElements(String path, Element e1, Element e2) { 157 if (!e1.getNamespaceURI().equals(e2.getNamespaceURI())) 158 return "Namespaces differ at "+path+": "+e1.getNamespaceURI()+"/"+e2.getNamespaceURI(); 159 if (!e1.getLocalName().equals(e2.getLocalName())) 160 return "Names differ at "+path+": "+e1.getLocalName()+"/"+e2.getLocalName(); 161 path = path + "/"+e1.getLocalName(); 162 String s = compareAttributes(path, e1.getAttributes(), e2.getAttributes()); 163 if (!Utilities.noString(s)) 164 return s; 165 s = compareAttributes(path, e2.getAttributes(), e1.getAttributes()); 166 if (!Utilities.noString(s)) 167 return s; 168 169 Node c1 = e1.getFirstChild(); 170 Node c2 = e2.getFirstChild(); 171 c1 = skipBlankText(c1); 172 c2 = skipBlankText(c2); 173 while (c1 != null && c2 != null) { 174 if (c1.getNodeType() != c2.getNodeType()) 175 return "node type mismatch in children of "+path+": "+Integer.toString(e1.getNodeType())+"/"+Integer.toString(e2.getNodeType()); 176 if (c1.getNodeType() == Node.TEXT_NODE) { 177 if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent()))) 178 return "Text differs at "+path+": "+normalise(c1.getTextContent()) +"/"+ normalise(c2.getTextContent()); 179 } 180 else if (c1.getNodeType() == Node.ELEMENT_NODE) { 181 s = compareElements(path, (Element) c1, (Element) c2); 182 if (!Utilities.noString(s)) 183 return s; 184 } 185 186 c1 = skipBlankText(c1.getNextSibling()); 187 c2 = skipBlankText(c2.getNextSibling()); 188 } 189 if (c1 != null) 190 return "node mismatch - more nodes in source in children of "+path; 191 if (c2 != null) 192 return "node mismatch - more nodes in target in children of "+path; 193 return null; 194 } 195 196 private static Object normalise(String text) { 197 String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' '); 198 while (result.contains(" ")) 199 result = result.replace(" ", " "); 200 return result; 201 } 202 203 private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) { 204 for (int i = 0; i < src.getLength(); i++) { 205 206 Node sa = src.item(i); 207 String sn = sa.getNodeName(); 208 if (! (sn.equals("xmlns") || sn.startsWith("xmlns:"))) { 209 Node ta = tgt.getNamedItem(sn); 210 if (ta == null) 211 return "Attributes differ at "+path+": missing attribute "+sn; 212 if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) { 213 byte[] b1 = unBase64(sa.getTextContent()); 214 byte[] b2 = unBase64(ta.getTextContent()); 215 if (!sameBytes(b1, b2)) 216 return "Attributes differ at "+path+": value "+normalise(sa.getTextContent()) +"/"+ normalise(ta.getTextContent()); 217 } 218 } 219 } 220 return null; 221 } 222 223 private static boolean sameBytes(byte[] b1, byte[] b2) { 224 if (b1.length == 0 || b2.length == 0) 225 return false; 226 if (b1.length != b2.length) 227 return false; 228 for (int i = 0; i < b1.length; i++) 229 if (b1[i] != b2[i]) 230 return false; 231 return true; 232 } 233 234 private static byte[] unBase64(String text) { 235 return Base64.decodeBase64(text); 236 } 237 238 private static Node skipBlankText(Node node) { 239 while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 240 node = node.getNextSibling(); 241 return node; 242 } 243 244 private static Document loadXml(String fn) throws Exception { 245 return loadXml(new FileInputStream(fn)); 246 } 247 248 private static Document loadXml(InputStream fn) throws Exception { 249 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 250 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 251 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 252 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 253 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 254 factory.setXIncludeAware(false); 255 factory.setExpandEntityReferences(false); 256 257 factory.setNamespaceAware(true); 258 DocumentBuilder builder = factory.newDocumentBuilder(); 259 return builder.parse(fn); 260 } 261 262 public static String checkJsonSrcIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 263 return checkJsonSrcIsSame(s1,s2,true); 264 } 265 266 public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 267 String result = compareJsonSrc(s1, s2); 268 if (result != null && SHOW_DIFF && showDiff) { 269 String diff = null; 270 if (System.getProperty("os.name").contains("Linux")) 271 diff = Utilities.path("/", "usr", "bin", "meld"); 272 else { 273 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 274 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 275 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 276 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 277 } 278 if (diff == null || diff.isEmpty()) 279 return result; 280 281 List<String> command = new ArrayList<String>(); 282 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 283 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 284 TextFile.stringToFile(s1, f1); 285 TextFile.stringToFile(s2, f2); 286 command.add(diff); 287 if (diff.toLowerCase().contains("meld")) 288 command.add("--newtab"); 289 command.add(f1); 290 command.add(f2); 291 292 ProcessBuilder builder = new ProcessBuilder(command); 293 builder.directory(new CSFile(Utilities.path("[tmp]"))); 294 builder.start(); 295 296 } 297 return result; 298 } 299 public static String checkJsonIsSame(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 300 String result = compareJson(f1, f2); 301 if (result != null && SHOW_DIFF) { 302 String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 303 List<String> command = new ArrayList<String>(); 304 command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\""); 305 306 ProcessBuilder builder = new ProcessBuilder(command); 307 builder.directory(new CSFile("c:\\temp")); 308 builder.start(); 309 310 } 311 return result; 312 } 313 314 private static String compareJsonSrc(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 315 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1); 316 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2); 317 return compareObjects("", o1, o2); 318 } 319 320 private static String compareJson(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException { 321 JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1)); 322 JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2)); 323 return compareObjects("", o1, o2); 324 } 325 326 private static String compareObjects(String path, JsonObject o1, JsonObject o2) { 327 for (Map.Entry<String, JsonElement> en : o1.entrySet()) { 328 String n = en.getKey(); 329 if (!n.equals("fhir_comments")) { 330 if (o2.has(n)) { 331 String s = compareNodes(path+'.'+n, en.getValue(), o2.get(n)); 332 if (!Utilities.noString(s)) 333 return s; 334 } 335 else 336 return "properties differ at "+path+": missing property "+n; 337 } 338 } 339 for (Map.Entry<String, JsonElement> en : o2.entrySet()) { 340 String n = en.getKey(); 341 if (!n.equals("fhir_comments")) { 342 if (!o1.has(n)) 343 return "properties differ at "+path+": missing property "+n; 344 } 345 } 346 return null; 347 } 348 349 private static String compareNodes(String path, JsonElement n1, JsonElement n2) { 350 if (n1.getClass() != n2.getClass()) 351 return "properties differ at "+path+": type "+n1.getClass().getName()+"/"+n2.getClass().getName(); 352 else if (n1 instanceof JsonPrimitive) { 353 JsonPrimitive p1 = (JsonPrimitive) n1; 354 JsonPrimitive p2 = (JsonPrimitive) n2; 355 if (p1.isBoolean() && p2.isBoolean()) { 356 if (p1.getAsBoolean() != p2.getAsBoolean()) 357 return "boolean property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 358 } else if (p1.isString() && p2.isString()) { 359 String s1 = p1.getAsString(); 360 String s2 = p2.getAsString(); 361 if (!(s1.contains("<div") && s2.contains("<div"))) 362 if (!s1.equals(s2)) 363 if (!sameBytes(unBase64(s1), unBase64(s2))) 364 return "string property values differ at "+path+": type "+s1+"/"+s2; 365 } else if (p1.isNumber() && p2.isNumber()) { 366 if (!p1.getAsString().equals(p2.getAsString())) 367 return "number property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 368 } else 369 return "property types differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString(); 370 } 371 else if (n1 instanceof JsonObject) { 372 String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2); 373 if (!Utilities.noString(s)) 374 return s; 375 } else if (n1 instanceof JsonArray) { 376 JsonArray a1 = (JsonArray) n1; 377 JsonArray a2 = (JsonArray) n2; 378 379 if (a1.size() != a2.size()) 380 return "array properties differ at "+path+": count "+Integer.toString(a1.size())+"/"+Integer.toString(a2.size()); 381 for (int i = 0; i < a1.size(); i++) { 382 String s = compareNodes(path+"["+Integer.toString(i)+"]", a1.get(i), a2.get(i)); 383 if (!Utilities.noString(s)) 384 return s; 385 } 386 } 387 else if (n1 instanceof JsonNull) { 388 389 } else 390 return "unhandled property "+n1.getClass().getName(); 391 return null; 392 } 393 394 public static String temp() { 395 if (new File("c:\\temp").exists()) 396 return "c:\\temp"; 397 return System.getProperty("java.io.tmpdir"); 398 } 399 400 public static String checkTextIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException { 401 return checkTextIsSame(s1,s2,true); 402 } 403 404 public static String checkTextIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException { 405 String result = compareText(s1, s2); 406 if (result != null && SHOW_DIFF && showDiff) { 407 String diff = null; 408 if (System.getProperty("os.name").contains("Linux")) 409 diff = Utilities.path("/", "usr", "bin", "meld"); 410 else { 411 if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null)) 412 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe"); 413 else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null)) 414 diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe"); 415 } 416 if (diff == null || diff.isEmpty()) 417 return result; 418 419 List<String> command = new ArrayList<String>(); 420 String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json"); 421 String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json"); 422 TextFile.stringToFile(s1, f1); 423 TextFile.stringToFile(s2, f2); 424 command.add(diff); 425 if (diff.toLowerCase().contains("meld")) 426 command.add("--newtab"); 427 command.add(f1); 428 command.add(f2); 429 430 ProcessBuilder builder = new ProcessBuilder(command); 431 builder.directory(new CSFile(Utilities.path("[tmp]"))); 432 builder.start(); 433 434 } 435 return result; 436 } 437 438 439 private static String compareText(String s1, String s2) { 440 for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) { 441 if (s1.charAt(i) != s2.charAt(i)) 442 return "Strings differ at character "+Integer.toString(i)+": '"+s1.charAt(i) +"' vs '"+s2.charAt(i)+"'"; 443 } 444 if (s1.length() != s2.length()) 445 return "Strings differ in length: "+Integer.toString(s1.length())+" vs "+Integer.toString(s2.length())+" but match to the end of the shortest"; 446 return null; 447 } 448 449 450 public static String resourceNameToFile(String name) throws IOException { 451 return Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", name); 452 } 453 454 455 public static String resourceNameToFile(String subFolder, String name) throws IOException { 456 return Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name); 457 } 458 459}