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