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