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.CSFile;
055import org.hl7.fhir.utilities.TextFile;
056import org.hl7.fhir.utilities.ToolGlobalSettings;
057import org.hl7.fhir.utilities.Utilities;
058import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
059import org.hl7.fhir.utilities.npm.ToolsVersion;
060import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
061import org.hl7.fhir.utilities.tests.ResourceLoaderTests;
062import org.hl7.fhir.utilities.tests.TestConfig;
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(true, ToolsVersion.TOOLS_VERSION);
085              fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1"));
086              fcontext.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        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())+"/"+Integer.toString(e2.getNodeType());
182                        if (c1.getNodeType() == Node.TEXT_NODE) {    
183                                if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent())))
184                                        return "Text differs at "+path+": "+normalise(c1.getTextContent()) +"/"+ normalise(c2.getTextContent());
185                        }
186                        else if (c1.getNodeType() == Node.ELEMENT_NODE) {
187                                s = compareElements(path, (Element) c1, (Element) c2);
188                                if (!Utilities.noString(s))
189                                        return s;
190                        }
191
192                        c1 = skipBlankText(c1.getNextSibling());
193                        c2 = skipBlankText(c2.getNextSibling());
194                }
195                if (c1 != null)
196                        return "node mismatch - more nodes in source in children of "+path;
197                if (c2 != null)
198                        return "node mismatch - more nodes in target in children of "+path;
199                return null;
200        }
201
202        private static Object normalise(String text) {
203                String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
204                while (result.contains("  ")) 
205                        result = result.replace("  ", " ");
206                return result;
207        }
208
209        private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) {
210          for (int i = 0; i < src.getLength(); i++) {
211          
212            Node sa = src.item(i);
213            String sn = sa.getNodeName();
214            if (! (sn.equals("xmlns") || sn.startsWith("xmlns:"))) {
215              Node ta = tgt.getNamedItem(sn);
216              if (ta == null) 
217                return "Attributes differ at "+path+": missing attribute "+sn;
218              if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) {
219                byte[] b1 = unBase64(sa.getTextContent());
220                byte[] b2 = unBase64(ta.getTextContent());
221                if (!sameBytes(b1, b2))
222                  return "Attributes differ at "+path+": value "+normalise(sa.getTextContent()) +"/"+ normalise(ta.getTextContent());
223              }
224            }
225          }
226          return null;
227        }
228
229        private static boolean sameBytes(byte[] b1, byte[] b2) {
230                if (b1.length == 0 || b2.length == 0)
231                        return false;
232                if (b1.length != b2.length)
233                        return false;
234                for (int i = 0; i < b1.length; i++)
235                        if (b1[i] != b2[i])
236                                return false;
237                return true;
238        }
239
240        private static byte[] unBase64(String text) {
241                return Base64.decodeBase64(text);
242        }
243
244        private static Node skipBlankText(Node node) {
245          while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 
246            node = node.getNextSibling();
247          return node;
248        }
249
250  private static Document loadXml(String fn) throws Exception {
251    return loadXml(new FileInputStream(fn));
252  }
253
254  private static Document loadXml(InputStream fn) throws Exception {
255    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
256      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
257      factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
258      factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
259      factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
260      factory.setXIncludeAware(false);
261      factory.setExpandEntityReferences(false);
262        
263    factory.setNamespaceAware(true);
264      DocumentBuilder builder = factory.newDocumentBuilder();
265      return builder.parse(fn);
266  }
267
268  public static String checkJsonSrcIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException {
269    return checkJsonSrcIsSame(s1,s2,true);
270  }
271
272  public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException {
273    String result = compareJsonSrc(s1, s2);
274    if (result != null && SHOW_DIFF && showDiff) {
275      String diff = null; 
276      if (System.getProperty("os.name").contains("Linux"))
277        diff = Utilities.path("/", "usr", "bin", "meld");
278      else {
279        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
280                diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
281        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
282                diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
283      }
284      if (diff == null || diff.isEmpty())
285          return result;
286      
287      List<String> command = new ArrayList<String>();
288      String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json");
289      String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json");
290      TextFile.stringToFile(s1, f1);
291      TextFile.stringToFile(s2, f2);
292      command.add(diff);
293      if (diff.toLowerCase().contains("meld"))
294          command.add("--newtab");
295      command.add(f1);
296      command.add(f2);
297
298      ProcessBuilder builder = new ProcessBuilder(command);
299      builder.directory(new CSFile(Utilities.path("[tmp]")));
300      builder.start();
301      
302    }
303    return result;
304  }
305  public static String checkJsonIsSame(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
306                String result = compareJson(f1, f2);
307                if (result != null && SHOW_DIFF) {
308            String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
309            List<String> command = new ArrayList<String>();
310            command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\"");
311
312            ProcessBuilder builder = new ProcessBuilder(command);
313            builder.directory(new CSFile(Utilities.path("[tmp]")));
314            builder.start();
315                        
316                }
317                return result;
318        }
319
320  private static String compareJsonSrc(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
321    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1);
322    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2);
323    return compareObjects("", o1, o2);
324  }
325
326  private static String compareJson(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
327    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1));
328    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2));
329    return compareObjects("", o1, o2);
330  }
331
332        private static String compareObjects(String path, JsonObject o1, JsonObject o2) {
333          for (Map.Entry<String, JsonElement> en : o1.entrySet()) {
334                String n = en.getKey();
335            if (!n.equals("fhir_comments")) {
336              if (o2.has(n)) {
337                String s = compareNodes(path+'.'+n, en.getValue(), o2.get(n));
338                        if (!Utilities.noString(s))
339                                return s;
340              }
341              else
342                return "properties differ at "+path+": missing property "+n;
343            }
344          }
345          for (Map.Entry<String, JsonElement> en : o2.entrySet()) {
346                String n = en.getKey();
347            if (!n.equals("fhir_comments")) {
348              if (!o1.has(n)) 
349                return "properties differ at "+path+": missing property "+n;
350            }
351          }
352          return null;
353        }
354
355        private static String compareNodes(String path, JsonElement n1, JsonElement n2) {
356                if (n1.getClass() != n2.getClass())
357                        return "properties differ at "+path+": type "+n1.getClass().getName()+"/"+n2.getClass().getName();
358                else if (n1 instanceof JsonPrimitive) {
359                        JsonPrimitive p1 = (JsonPrimitive) n1;
360                        JsonPrimitive p2 = (JsonPrimitive) n2;
361                        if (p1.isBoolean() && p2.isBoolean()) {
362                                if (p1.getAsBoolean() != p2.getAsBoolean())
363                                        return "boolean property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
364                        }       else if (p1.isString() && p2.isString()) {
365                                String s1 = p1.getAsString();
366                                String s2 = p2.getAsString();
367                                if (!(s1.contains("<div") && s2.contains("<div")))
368                                        if (!s1.equals(s2))
369                                                if (!sameBytes(unBase64(s1), unBase64(s2)))
370                                                        return "string property values differ at "+path+": type "+s1+"/"+s2;
371                        } else if (p1.isNumber() && p2.isNumber()) {
372            if (!p1.getAsString().equals(p2.getAsString()))
373                                return "number property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
374                        } else
375                                return "property types differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
376          }
377          else if (n1 instanceof JsonObject) {
378            String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2);
379                        if (!Utilities.noString(s))
380                                return s;
381          } else if (n1 instanceof JsonArray) {
382                JsonArray a1 = (JsonArray) n1;
383                JsonArray a2 = (JsonArray) n2;
384          
385            if (a1.size() != a2.size()) 
386              return "array properties differ at "+path+": count "+Integer.toString(a1.size())+"/"+Integer.toString(a2.size());
387            for (int i = 0; i < a1.size(); i++) {
388                String s = compareNodes(path+"["+Integer.toString(i)+"]", a1.get(i), a2.get(i));
389                                if (!Utilities.noString(s))
390                                        return s;
391            }
392          }
393          else if (n1 instanceof JsonNull) {
394                
395          } else
396            return "unhandled property "+n1.getClass().getName();
397                return null;
398        }
399
400
401  public static String checkTextIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException {
402    return checkTextIsSame(s1,s2,true);
403  }
404
405  public static String checkTextIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException {
406    String result = compareText(s1, s2);
407    if (result != null && SHOW_DIFF && showDiff) {
408      String diff = null; 
409      if (System.getProperty("os.name").contains("Linux"))
410        diff = Utilities.path("/", "usr", "bin", "meld");
411      else {
412      if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
413        diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
414      else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
415        diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
416      }
417      if (diff == null || diff.isEmpty())
418        return result;
419      
420      List<String> command = new ArrayList<String>();
421      String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json");
422      String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json");
423      TextFile.stringToFile(s1, f1);
424      TextFile.stringToFile(s2, f2);
425      command.add(diff);
426      if (diff.toLowerCase().contains("meld"))
427        command.add("--newtab");
428      command.add(f1);
429      command.add(f2);
430
431      ProcessBuilder builder = new ProcessBuilder(command);
432      builder.directory(new CSFile(Utilities.path("[tmp]")));
433      builder.start();
434      
435    }
436    return result;
437  }
438
439
440  private static String compareText(String s1, String s2) {
441    for (int i = 0; i < Integer.min(s1.length(), s2.length()); i++) {
442      if (s1.charAt(i) != s2.charAt(i))
443        return "Strings differ at character "+Integer.toString(i)+": '"+s1.charAt(i) +"' vs '"+s2.charAt(i)+"'";
444    }
445    if (s1.length() != s2.length())
446      return "Strings differ in length: "+Integer.toString(s1.length())+" vs "+Integer.toString(s2.length())+" but match to the end of the shortest";
447    return null;
448  }
449  
450  public static String resourceNameToFile(String name) throws IOException {
451    return resourceNameToFile(null, name);
452  }
453
454  private static boolean fileForPathExists(String path) {
455    return new File(path).exists();
456  }
457
458  public static String generateResourcePath(String subFolder, String name) throws IOException {
459    String path = Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name);
460    BaseTestingUtilities.createParentDirIfNotExists(Paths.get(path));
461    return path;
462  }
463  public static String resourceNameToFile(String subFolder, String name) throws IOException {
464
465    final String resourcePath = (subFolder != null ? subFolder + "/" : "") + name;
466    final String filePathFromClassLoader = TestingUtilities.class.getClassLoader().getResource(resourcePath).getPath();
467
468    if (fileForPathExists(filePathFromClassLoader)) {
469      return filePathFromClassLoader;
470    } else {
471      final Path newFilePath = (subFolder != null) ? Paths.get("target", subFolder, name) : Paths.get("target", name);
472      copyResourceToNewFile(resourcePath, newFilePath);
473      return newFilePath.toString();
474    }
475  }
476
477  private static void copyResourceToNewFile(String resourcePath, Path newFilePath) throws IOException {
478    BaseTestingUtilities.createParentDirIfNotExists(newFilePath);
479    ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath);
480  }
481
482  public static String loadTestResource(String... paths) throws IOException {
483    /**
484     * This 'if' condition checks to see if the fhir-test-cases project (https://github.com/FHIR/fhir-test-cases) is
485     * installed locally at the same directory level as the core library project is. If so, the test case data is read
486     * directly from that project, instead of the imported maven dependency jar. It is important, that if you want to
487     * test against the dependency imported from sonatype nexus, instead of your local copy, you need to either change
488     * the name of the project directory to something other than 'fhir-test-cases', or move it to another location, not
489     * at the same directory level as the core project.
490     */
491
492    String dir = TestConfig.getInstance().getFhirTestCasesDirectory();
493    if (dir == null && ToolGlobalSettings.hasTestsPath()) {
494      dir = ToolGlobalSettings.getTestsPath();
495    }
496    if (dir != null && new CSFile(dir).exists()) {
497      String n = Utilities.path(dir, Utilities.path(paths));
498      // ok, we'll resolve this locally
499      return TextFile.fileToString(new CSFile(n));
500    } else {
501      // resolve from the package
502      String contents;
503      String classpath = ("/org/hl7/fhir/testcases/" + Utilities.pathURL(paths));
504      try (InputStream inputStream = BaseTestingUtilities.class.getResourceAsStream(classpath)) {
505        if (inputStream == null) {
506          throw new IOException("Can't find file on classpath: " + classpath);
507        }
508        contents = IOUtils.toString(inputStream, java.nio.charset.StandardCharsets.UTF_8);
509      }
510      return contents;
511    }
512  }
513
514  
515
516}