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