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