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}