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