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}