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
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.FileNotFoundException;
035import java.io.IOException;
036import java.io.InputStream;
037import java.nio.file.Path;
038import java.nio.file.Paths;
039import java.util.ArrayList;
040import java.util.List;
041import java.util.Map;
042
043import javax.xml.parsers.DocumentBuilder;
044import javax.xml.parsers.DocumentBuilderFactory;
045
046import org.apache.commons.codec.binary.Base64;
047import org.apache.commons.io.IOUtils;
048import org.fhir.ucum.UcumEssenceService;
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.npm.FilesystemPackageCacheManager;
056import org.hl7.fhir.utilities.settings.FhirSettings;
057import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
058import org.hl7.fhir.utilities.tests.ResourceLoaderTests;
059import org.hl7.fhir.utilities.tests.TestConfig;
060import org.w3c.dom.Document;
061import org.w3c.dom.Element;
062import org.w3c.dom.NamedNodeMap;
063import org.w3c.dom.Node;
064
065import com.google.gson.JsonArray;
066import com.google.gson.JsonElement;
067import com.google.gson.JsonNull;
068import com.google.gson.JsonObject;
069import com.google.gson.JsonPrimitive;
070import com.google.gson.JsonSyntaxException;
071
072public class TestingUtilities {
073  private static final boolean SHOW_DIFF = false;
074
075  static public IWorkerContext fcontext;
076
077  public static IWorkerContext context() {
078    if (fcontext == null) {
079      FilesystemPackageCacheManager pcm;
080      try {
081        pcm = new FilesystemPackageCacheManager(
082            org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager.FilesystemPackageCacheMode.USER);
083        fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1"));
084        fcontext
085            .setUcumService(new UcumEssenceService(TestingUtilities.resourceNameToFile("ucum", "ucum-essence.xml")));
086        fcontext.setExpansionProfile(new Parameters());
087      } catch (Exception e) {
088        throw new Error(e);
089      }
090
091    }
092    return fcontext;
093  }
094
095  static public boolean silent;
096
097  static public String fixedpath;
098  static public String contentpath;
099
100  public static String home() {
101    if (fixedpath != null)
102      return fixedpath;
103    String s = System.getenv("FHIR_HOME");
104    if (!Utilities.noString(s))
105      return s;
106    s = "C:\\work\\org.hl7.fhir\\build";
107    if (new File(s).exists())
108      return s;
109    throw new Error("FHIR Home directory not configured");
110  }
111
112  public static String content() throws IOException {
113    if (contentpath != null)
114      return contentpath;
115    String s = "R:\\fhir\\publish";
116    if (new File(s).exists())
117      return s;
118    return Utilities.path(home(), "publish");
119  }
120
121  // diretory that contains all the US implementation guides
122  public static String us() {
123    if (fixedpath != null)
124      return fixedpath;
125    String s = System.getenv("FHIR_HOME");
126    if (!Utilities.noString(s))
127      return s;
128    s = "C:\\work\\org.hl7.fhir.us";
129    if (new File(s).exists())
130      return s;
131    throw new Error("FHIR US directory not configured");
132  }
133
134  public static String checkXMLIsSame(InputStream f1, InputStream f2) throws Exception {
135    String result = compareXml(f1, f2);
136    return result;
137  }
138
139  public static String checkXMLIsSame(String f1, String f2) throws Exception {
140    String result = compareXml(f1, f2);
141    if (result != null && SHOW_DIFF) {
142      String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
143      List<String> command = new ArrayList<String>();
144      command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\"");
145
146      ProcessBuilder builder = new ProcessBuilder(command);
147      builder.directory(new CSFile(Utilities.path("[tmp]")));
148      builder.start();
149
150    }
151    return result;
152  }
153
154  private static String compareXml(InputStream f1, InputStream f2) throws Exception {
155    return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement());
156  }
157
158  private static String compareXml(String f1, String f2) throws Exception {
159    return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement());
160  }
161
162  private static String compareElements(String path, Element e1, Element e2) {
163    if (!e1.getNamespaceURI().equals(e2.getNamespaceURI()))
164      return "Namespaces differ at " + path + ": " + e1.getNamespaceURI() + "/" + e2.getNamespaceURI();
165    if (!e1.getLocalName().equals(e2.getLocalName()))
166      return "Names differ at " + path + ": " + e1.getLocalName() + "/" + e2.getLocalName();
167    path = path + "/" + e1.getLocalName();
168    String s = compareAttributes(path, e1.getAttributes(), e2.getAttributes());
169    if (!Utilities.noString(s))
170      return s;
171    s = compareAttributes(path, e2.getAttributes(), e1.getAttributes());
172    if (!Utilities.noString(s))
173      return s;
174
175    Node c1 = e1.getFirstChild();
176    Node c2 = e2.getFirstChild();
177    c1 = skipBlankText(c1);
178    c2 = skipBlankText(c2);
179    while (c1 != null && c2 != null) {
180      if (c1.getNodeType() != c2.getNodeType())
181        return "node type mismatch in children of " + path + ": " + Integer.toString(e1.getNodeType()) + "/"
182            + Integer.toString(e2.getNodeType());
183      if (c1.getNodeType() == Node.TEXT_NODE) {
184        if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent())))
185          return "Text differs at " + path + ": " + normalise(c1.getTextContent()) + "/"
186              + normalise(c2.getTextContent());
187      } else if (c1.getNodeType() == Node.ELEMENT_NODE) {
188        s = compareElements(path, (Element) c1, (Element) c2);
189        if (!Utilities.noString(s))
190          return s;
191      }
192
193      c1 = skipBlankText(c1.getNextSibling());
194      c2 = skipBlankText(c2.getNextSibling());
195    }
196    if (c1 != null)
197      return "node mismatch - more nodes in source in children of " + path;
198    if (c2 != null)
199      return "node mismatch - more nodes in target in children of " + path;
200    return null;
201  }
202
203  private static Object normalise(String text) {
204    String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
205    while (result.contains("  "))
206      result = result.replace("  ", " ");
207    return result;
208  }
209
210  private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) {
211    for (int i = 0; i < src.getLength(); i++) {
212
213      Node sa = src.item(i);
214      String sn = sa.getNodeName();
215      if (!(sn.equals("xmlns") || sn.startsWith("xmlns:"))) {
216        Node ta = tgt.getNamedItem(sn);
217        if (ta == null)
218          return "Attributes differ at " + path + ": missing attribute " + sn;
219        if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) {
220          byte[] b1 = unBase64(sa.getTextContent());
221          byte[] b2 = unBase64(ta.getTextContent());
222          if (!sameBytes(b1, b2))
223            return "Attributes differ at " + path + ": value " + normalise(sa.getTextContent()) + "/"
224                + normalise(ta.getTextContent());
225        }
226      }
227    }
228    return null;
229  }
230
231  private static boolean sameBytes(byte[] b1, byte[] b2) {
232    if (b1.length == 0 || b2.length == 0)
233      return false;
234    if (b1.length != b2.length)
235      return false;
236    for (int i = 0; i < b1.length; i++)
237      if (b1[i] != b2[i])
238        return false;
239    return true;
240  }
241
242  private static byte[] unBase64(String text) {
243    return Base64.decodeBase64(text);
244  }
245
246  private static Node skipBlankText(Node node) {
247    while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isAllWhitespace(node.getTextContent()))
248        || (node.getNodeType() == Node.COMMENT_NODE)))
249      node = node.getNextSibling();
250    return node;
251  }
252
253  private static Document loadXml(String fn) throws Exception {
254    return loadXml(new FileInputStream(fn));
255  }
256
257  private static Document loadXml(InputStream fn) throws Exception {
258    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
259    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
260    factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
261    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
262    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
263    factory.setXIncludeAware(false);
264    factory.setExpandEntityReferences(false);
265
266    factory.setNamespaceAware(true);
267    DocumentBuilder builder = factory.newDocumentBuilder();
268    return builder.parse(fn);
269  }
270
271  public static String checkJsonSrcIsSame(String s1, String s2)
272      throws JsonSyntaxException, FileNotFoundException, IOException {
273    return checkJsonSrcIsSame(s1, s2, true);
274  }
275
276  public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff)
277      throws JsonSyntaxException, FileNotFoundException, IOException {
278    String result = compareJsonSrc(s1, s2);
279    if (result != null && SHOW_DIFF && showDiff) {
280      String diff = null;
281      if (System.getProperty("os.name").contains("Linux"))
282        diff = Utilities.path("/", "usr", "bin", "meld");
283      else {
284        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"),
285            "\\WinMergeU.exe", null))
286          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
287        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"),
288            "\\Meld.exe", null))
289          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
290      }
291      if (diff == null || diff.isEmpty())
292        return result;
293
294      List<String> command = new ArrayList<String>();
295      String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json");
296      String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json");
297      TextFile.stringToFile(s1, f1);
298      TextFile.stringToFile(s2, f2);
299      command.add(diff);
300      if (diff.toLowerCase().contains("meld"))
301        command.add("--newtab");
302      command.add(f1);
303      command.add(f2);
304
305      ProcessBuilder builder = new ProcessBuilder(command);
306      builder.directory(new CSFile(Utilities.path("[tmp]")));
307      builder.start();
308
309    }
310    return result;
311  }
312
313  public static String checkJsonIsSame(String f1, String f2)
314      throws JsonSyntaxException, FileNotFoundException, IOException {
315    String result = compareJson(f1, f2);
316    if (result != null && SHOW_DIFF) {
317      String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
318      List<String> command = new ArrayList<String>();
319      command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\"");
320
321      ProcessBuilder builder = new ProcessBuilder(command);
322      builder.directory(new CSFile(Utilities.path("[tmp]")));
323      builder.start();
324
325    }
326    return result;
327  }
328
329  private static String compareJsonSrc(String f1, String f2)
330      throws JsonSyntaxException, FileNotFoundException, IOException {
331    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1);
332    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2);
333    return compareObjects("", o1, o2);
334  }
335
336  private static String compareJson(String f1, String f2)
337      throws JsonSyntaxException, FileNotFoundException, IOException {
338    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1));
339    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2));
340    return compareObjects("", o1, o2);
341  }
342
343  private static String compareObjects(String path, JsonObject o1, JsonObject o2) {
344    for (Map.Entry<String, JsonElement> en : o1.entrySet()) {
345      String n = en.getKey();
346      if (!n.equals("fhir_comments")) {
347        if (o2.has(n)) {
348          String s = compareNodes(path + '.' + n, en.getValue(), o2.get(n));
349          if (!Utilities.noString(s))
350            return s;
351        } else
352          return "properties differ at " + path + ": missing property " + n;
353      }
354    }
355    for (Map.Entry<String, JsonElement> en : o2.entrySet()) {
356      String n = en.getKey();
357      if (!n.equals("fhir_comments")) {
358        if (!o1.has(n))
359          return "properties differ at " + path + ": missing property " + n;
360      }
361    }
362    return null;
363  }
364
365  private static String compareNodes(String path, JsonElement n1, JsonElement n2) {
366    if (n1.getClass() != n2.getClass())
367      return "properties differ at " + path + ": type " + n1.getClass().getName() + "/" + n2.getClass().getName();
368    else if (n1 instanceof JsonPrimitive) {
369      JsonPrimitive p1 = (JsonPrimitive) n1;
370      JsonPrimitive p2 = (JsonPrimitive) n2;
371      if (p1.isBoolean() && p2.isBoolean()) {
372        if (p1.getAsBoolean() != p2.getAsBoolean())
373          return "boolean property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString();
374      } else if (p1.isString() && p2.isString()) {
375        String s1 = p1.getAsString();
376        String s2 = p2.getAsString();
377        if (!(s1.contains("<div") && s2.contains("<div")))
378          if (!s1.equals(s2))
379            if (!sameBytes(unBase64(s1), unBase64(s2)))
380              return "string property values differ at " + path + ": type " + s1 + "/" + s2;
381      } else if (p1.isNumber() && p2.isNumber()) {
382        if (!p1.getAsString().equals(p2.getAsString()))
383          return "number property values differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString();
384      } else
385        return "property types differ at " + path + ": type " + p1.getAsString() + "/" + p2.getAsString();
386    } else if (n1 instanceof JsonObject) {
387      String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2);
388      if (!Utilities.noString(s))
389        return s;
390    } else if (n1 instanceof JsonArray) {
391      JsonArray a1 = (JsonArray) n1;
392      JsonArray a2 = (JsonArray) n2;
393
394      if (a1.size() != a2.size())
395        return "array properties differ at " + path + ": count " + Integer.toString(a1.size()) + "/"
396            + Integer.toString(a2.size());
397      for (int i = 0; i < a1.size(); i++) {
398        String s = compareNodes(path + "[" + Integer.toString(i) + "]", a1.get(i), a2.get(i));
399        if (!Utilities.noString(s))
400          return s;
401      }
402    } else if (n1 instanceof JsonNull) {
403
404    } else
405      return "unhandled property " + n1.getClass().getName();
406    return null;
407  }
408
409  public static String checkTextIsSame(String s1, String s2)
410      throws JsonSyntaxException, FileNotFoundException, IOException {
411    return checkTextIsSame(s1, s2, true);
412  }
413
414  public static String checkTextIsSame(String s1, String s2, boolean showDiff)
415      throws JsonSyntaxException, FileNotFoundException, IOException {
416    String result = compareText(s1, s2);
417    if (result != null && SHOW_DIFF && showDiff) {
418      String diff = null;
419      if (System.getProperty("os.name").contains("Linux"))
420        diff = Utilities.path("/", "usr", "bin", "meld");
421      else {
422        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"),
423            "\\WinMergeU.exe", null))
424          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
425        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"),
426            "\\Meld.exe", null))
427          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
428      }
429      if (diff == null || diff.isEmpty())
430        return result;
431
432      List<String> command = new ArrayList<String>();
433      String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json");
434      String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json");
435      TextFile.stringToFile(s1, f1);
436      TextFile.stringToFile(s2, f2);
437      command.add(diff);
438      if (diff.toLowerCase().contains("meld"))
439        command.add("--newtab");
440      command.add(f1);
441      command.add(f2);
442
443      ProcessBuilder builder = new ProcessBuilder(command);
444      builder.directory(new CSFile(Utilities.path("[tmp]")));
445      builder.start();
446
447    }
448    return result;
449  }
450
451  private static String compareText(String s1, String s2) {
452    for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) {
453      if (s1.charAt(i) != s2.charAt(i))
454        return "Strings differ at character " + Integer.toString(i) + ": '" + s1.charAt(i) + "' vs '" + s2.charAt(i)
455            + "'";
456    }
457    if (s1.length() != s2.length())
458      return "Strings differ in length: " + Integer.toString(s1.length()) + " vs " + Integer.toString(s2.length())
459          + " but match to the end of the shortest";
460    return null;
461  }
462
463  public static String resourceNameToFile(String name) throws IOException {
464    return resourceNameToFile(null, name);
465  }
466
467  private static boolean fileForPathExists(String path) {
468    return new File(path).exists();
469  }
470
471  public static String generateResourcePath(String subFolder, String name) throws IOException {
472    String path = Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name);
473    BaseTestingUtilities.createParentDirIfNotExists(Paths.get(path));
474    return path;
475  }
476
477  public static String resourceNameToFile(String subFolder, String name) throws IOException {
478
479    final String resourcePath = (subFolder != null ? subFolder + "/" : "") + name;
480    final String filePathFromClassLoader = TestingUtilities.class.getClassLoader().getResource(resourcePath).getPath();
481
482    if (fileForPathExists(filePathFromClassLoader)) {
483      return filePathFromClassLoader;
484    } else {
485      final Path newFilePath = (subFolder != null) ? Paths.get("target", subFolder, name) : Paths.get("target", name);
486      copyResourceToNewFile(resourcePath, newFilePath);
487      return newFilePath.toString();
488    }
489  }
490
491  private static void copyResourceToNewFile(String resourcePath, Path newFilePath) throws IOException {
492    BaseTestingUtilities.createParentDirIfNotExists(newFilePath);
493    ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath);
494  }
495
496  public static String loadTestResource(String... paths) throws IOException {
497    /**
498     * This 'if' condition checks to see if the fhir-test-cases project
499     * (https://github.com/FHIR/fhir-test-cases) is installed locally at the same
500     * directory level as the core library project is. If so, the test case data is
501     * read directly from that project, instead of the imported maven dependency
502     * jar. It is important, that if you want to test against the dependency
503     * imported from sonatype nexus, instead of your local copy, you need to either
504     * change the name of the project directory to something other than
505     * 'fhir-test-cases', or move it to another location, not at the same directory
506     * level as the core project.
507     */
508
509    String dir = TestConfig.getInstance().getFhirTestCasesDirectory();
510    if (dir == null && FhirSettings.hasFhirTestCasesPath()) {
511      dir = FhirSettings.getFhirTestCasesPath();
512    }
513    if (dir != null && new CSFile(dir).exists()) {
514      String n = Utilities.path(dir, Utilities.path(paths));
515      // ok, we'll resolve this locally
516      return TextFile.fileToString(new CSFile(n));
517    } else {
518      // resolve from the package
519      String contents;
520      String classpath = ("/org/hl7/fhir/testcases/" + Utilities.pathURL(paths));
521      try (InputStream inputStream = BaseTestingUtilities.class.getResourceAsStream(classpath)) {
522        if (inputStream == null) {
523          throw new IOException("Can't find file on classpath: " + classpath);
524        }
525        contents = IOUtils.toString(inputStream, java.nio.charset.StandardCharsets.UTF_8);
526      }
527      return contents;
528    }
529  }
530
531}