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.util.ArrayList;
039import java.util.List;
040import java.util.Map;
041
042import javax.xml.parsers.DocumentBuilder;
043import javax.xml.parsers.DocumentBuilderFactory;
044
045import org.apache.commons.codec.binary.Base64;
046import org.fhir.ucum.UcumEssenceService;
047import org.hl7.fhir.r4.context.IWorkerContext;
048import org.hl7.fhir.r4.context.SimpleWorkerContext;
049import org.hl7.fhir.r4.model.Parameters;
050import org.hl7.fhir.utilities.CSFile;
051import org.hl7.fhir.utilities.TextFile;
052import org.hl7.fhir.utilities.Utilities;
053import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
054import org.hl7.fhir.utilities.npm.ToolsVersion;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.NamedNodeMap;
058import org.w3c.dom.Node;
059
060import com.google.gson.JsonArray;
061import com.google.gson.JsonElement;
062import com.google.gson.JsonNull;
063import com.google.gson.JsonObject;
064import com.google.gson.JsonPrimitive;
065import com.google.gson.JsonSyntaxException;
066
067public class TestingUtilities {
068  private static final boolean SHOW_DIFF = true;
069  
070        static public IWorkerContext fcontext;
071        
072        public static IWorkerContext context() {
073          if (fcontext == null) {
074            FilesystemPackageCacheManager pcm;
075            try {
076              pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
077              fcontext = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.core", "4.0.0"));
078              fcontext.setUcumService(new UcumEssenceService(TestingUtilities.resourceNameToFile("ucum", "ucum-essence.xml")));
079              fcontext.setExpansionProfile(new Parameters());
080            } catch (Exception e) {
081              throw new Error(e);
082            }
083
084          }
085          return fcontext;
086        }
087        static public boolean silent;
088
089  static public String fixedpath;
090  static public String contentpath;
091
092  public static String home() {
093    if (fixedpath != null)
094     return fixedpath;
095    String s = System.getenv("FHIR_HOME");
096    if (!Utilities.noString(s))
097      return s;
098    s = "C:\\work\\org.hl7.fhir\\build";
099    if (new File(s).exists())
100      return s;
101    throw new Error("FHIR Home directory not configured");
102  }
103
104  public static String content() throws IOException {
105    if (contentpath != null)
106     return contentpath;
107    String s = "R:\\fhir\\publish";
108    if (new File(s).exists())
109      return s;
110    return Utilities.path(home(), "publish");
111  }
112  
113  // diretory that contains all the US implementation guides
114  public static String us() {
115    if (fixedpath != null)
116     return fixedpath;
117    String s = System.getenv("FHIR_HOME");
118    if (!Utilities.noString(s))
119      return s;
120    s = "C:\\work\\org.hl7.fhir.us";
121    if (new File(s).exists())
122      return s;
123    throw new Error("FHIR US directory not configured");
124  }
125  
126  public static String checkXMLIsSame(InputStream f1, InputStream f2) throws Exception {
127    String result = compareXml(f1, f2);
128    return result;
129  }
130  
131  public static String checkXMLIsSame(String f1, String f2) throws Exception {
132                String result = compareXml(f1, f2);
133                if (result != null && SHOW_DIFF) {
134            String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
135            List<String> command = new ArrayList<String>();
136            command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\"");
137
138            ProcessBuilder builder = new ProcessBuilder(command);
139            builder.directory(new CSFile("c:\\temp"));
140            builder.start();
141                        
142                }
143                return result;
144        }
145
146  private static String compareXml(InputStream f1, InputStream f2) throws Exception {
147    return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement());
148  }
149
150  private static String compareXml(String f1, String f2) throws Exception {
151    return compareElements("", loadXml(f1).getDocumentElement(), loadXml(f2).getDocumentElement());
152  }
153
154        private static String compareElements(String path, Element e1, Element e2) {
155                if (!e1.getNamespaceURI().equals(e2.getNamespaceURI())) 
156                        return "Namespaces differ at "+path+": "+e1.getNamespaceURI()+"/"+e2.getNamespaceURI();
157                if (!e1.getLocalName().equals(e2.getLocalName())) 
158                        return "Names differ at "+path+": "+e1.getLocalName()+"/"+e2.getLocalName();
159                path = path + "/"+e1.getLocalName();
160                String s = compareAttributes(path, e1.getAttributes(), e2.getAttributes());
161                if (!Utilities.noString(s))
162                        return s;
163                s = compareAttributes(path, e2.getAttributes(), e1.getAttributes());
164                if (!Utilities.noString(s))
165                        return s;
166
167                Node c1 = e1.getFirstChild();
168                Node c2 = e2.getFirstChild();
169                c1 = skipBlankText(c1);
170                c2 = skipBlankText(c2);
171                while (c1 != null && c2 != null) {
172                        if (c1.getNodeType() != c2.getNodeType()) 
173                                return "node type mismatch in children of "+path+": "+Integer.toString(e1.getNodeType())+"/"+Integer.toString(e2.getNodeType());
174                        if (c1.getNodeType() == Node.TEXT_NODE) {    
175                                if (!normalise(c1.getTextContent()).equals(normalise(c2.getTextContent())))
176                                        return "Text differs at "+path+": "+normalise(c1.getTextContent()) +"/"+ normalise(c2.getTextContent());
177                        }
178                        else if (c1.getNodeType() == Node.ELEMENT_NODE) {
179                                s = compareElements(path, (Element) c1, (Element) c2);
180                                if (!Utilities.noString(s))
181                                        return s;
182                        }
183
184                        c1 = skipBlankText(c1.getNextSibling());
185                        c2 = skipBlankText(c2.getNextSibling());
186                }
187                if (c1 != null)
188                        return "node mismatch - more nodes in source in children of "+path;
189                if (c2 != null)
190                        return "node mismatch - more nodes in target in children of "+path;
191                return null;
192        }
193
194        private static Object normalise(String text) {
195                String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
196                while (result.contains("  ")) 
197                        result = result.replace("  ", " ");
198                return result;
199        }
200
201        private static String compareAttributes(String path, NamedNodeMap src, NamedNodeMap tgt) {
202          for (int i = 0; i < src.getLength(); i++) {
203          
204            Node sa = src.item(i);
205            String sn = sa.getNodeName();
206            if (! (sn.equals("xmlns") || sn.startsWith("xmlns:"))) {
207              Node ta = tgt.getNamedItem(sn);
208              if (ta == null) 
209                return "Attributes differ at "+path+": missing attribute "+sn;
210              if (!normalise(sa.getTextContent()).equals(normalise(ta.getTextContent()))) {
211                byte[] b1 = unBase64(sa.getTextContent());
212                byte[] b2 = unBase64(ta.getTextContent());
213                if (!sameBytes(b1, b2))
214                  return "Attributes differ at "+path+": value "+normalise(sa.getTextContent()) +"/"+ normalise(ta.getTextContent());
215              }
216            }
217          }
218          return null;
219        }
220
221        private static boolean sameBytes(byte[] b1, byte[] b2) {
222                if (b1.length == 0 || b2.length == 0)
223                        return false;
224                if (b1.length != b2.length)
225                        return false;
226                for (int i = 0; i < b1.length; i++)
227                        if (b1[i] != b2[i])
228                                return false;
229                return true;
230        }
231
232        private static byte[] unBase64(String text) {
233                return Base64.decodeBase64(text);
234        }
235
236        private static Node skipBlankText(Node node) {
237          while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE))) 
238            node = node.getNextSibling();
239          return node;
240        }
241
242  private static Document loadXml(String fn) throws Exception {
243    return loadXml(new FileInputStream(fn));
244  }
245
246  private static Document loadXml(InputStream fn) throws Exception {
247    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
248      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
249      factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
250      factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
251      factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
252      factory.setXIncludeAware(false);
253      factory.setExpandEntityReferences(false);
254        
255    factory.setNamespaceAware(true);
256      DocumentBuilder builder = factory.newDocumentBuilder();
257      return builder.parse(fn);
258  }
259
260  public static String checkJsonSrcIsSame(String s1, String s2) throws JsonSyntaxException, FileNotFoundException, IOException {
261    return checkJsonSrcIsSame(s1,s2,true);
262  }
263
264  public static String checkJsonSrcIsSame(String s1, String s2, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException {
265    String result = compareJsonSrc(s1, s2);
266    if (result != null && SHOW_DIFF && showDiff) {
267      String diff = null; 
268      if (System.getProperty("os.name").contains("Linux"))
269        diff = Utilities.path("/", "usr", "bin", "meld");
270      else {
271        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
272                diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
273        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
274                diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
275      }
276      if (diff == null || diff.isEmpty())
277          return result;
278      
279      List<String> command = new ArrayList<String>();
280      String f1 = Utilities.path("[tmp]", "input" + s1.hashCode() + ".json");
281      String f2 = Utilities.path("[tmp]", "output" + s2.hashCode() + ".json");
282      TextFile.stringToFile(s1, f1);
283      TextFile.stringToFile(s2, f2);
284      command.add(diff);
285      if (diff.toLowerCase().contains("meld"))
286          command.add("--newtab");
287      command.add(f1);
288      command.add(f2);
289
290      ProcessBuilder builder = new ProcessBuilder(command);
291      builder.directory(new CSFile(Utilities.path("[tmp]")));
292      builder.start();
293      
294    }
295    return result;
296  }
297  public static String checkJsonIsSame(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
298                String result = compareJson(f1, f2);
299                if (result != null && SHOW_DIFF) {
300            String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
301            List<String> command = new ArrayList<String>();
302            command.add("\"" + diff + "\" \"" + f1 + "\" \"" + f2 + "\"");
303
304            ProcessBuilder builder = new ProcessBuilder(command);
305            builder.directory(new CSFile("c:\\temp"));
306            builder.start();
307                        
308                }
309                return result;
310        }
311
312  private static String compareJsonSrc(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
313    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(f1);
314    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(f2);
315    return compareObjects("", o1, o2);
316  }
317
318  private static String compareJson(String f1, String f2) throws JsonSyntaxException, FileNotFoundException, IOException {
319    JsonObject o1 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f1));
320    JsonObject o2 = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(f2));
321    return compareObjects("", o1, o2);
322  }
323
324        private static String compareObjects(String path, JsonObject o1, JsonObject o2) {
325          for (Map.Entry<String, JsonElement> en : o1.entrySet()) {
326                String n = en.getKey();
327            if (!n.equals("fhir_comments")) {
328              if (o2.has(n)) {
329                String s = compareNodes(path+'.'+n, en.getValue(), o2.get(n));
330                        if (!Utilities.noString(s))
331                                return s;
332              }
333              else
334                return "properties differ at "+path+": missing property "+n;
335            }
336          }
337          for (Map.Entry<String, JsonElement> en : o2.entrySet()) {
338                String n = en.getKey();
339            if (!n.equals("fhir_comments")) {
340              if (!o1.has(n)) 
341                return "properties differ at "+path+": missing property "+n;
342            }
343          }
344          return null;
345        }
346
347        private static String compareNodes(String path, JsonElement n1, JsonElement n2) {
348                if (n1.getClass() != n2.getClass())
349                        return "properties differ at "+path+": type "+n1.getClass().getName()+"/"+n2.getClass().getName();
350                else if (n1 instanceof JsonPrimitive) {
351                        JsonPrimitive p1 = (JsonPrimitive) n1;
352                        JsonPrimitive p2 = (JsonPrimitive) n2;
353                        if (p1.isBoolean() && p2.isBoolean()) {
354                                if (p1.getAsBoolean() != p2.getAsBoolean())
355                                        return "boolean property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
356                        }       else if (p1.isString() && p2.isString()) {
357                                String s1 = p1.getAsString();
358                                String s2 = p2.getAsString();
359                                if (!(s1.contains("<div") && s2.contains("<div")))
360                                        if (!s1.equals(s2))
361                                                if (!sameBytes(unBase64(s1), unBase64(s2)))
362                                                        return "string property values differ at "+path+": type "+s1+"/"+s2;
363                        } else if (p1.isNumber() && p2.isNumber()) {
364            if (!p1.getAsString().equals(p2.getAsString()))
365                                return "number property values differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
366                        } else
367                                return "property types differ at "+path+": type "+p1.getAsString()+"/"+p2.getAsString();
368          }
369          else if (n1 instanceof JsonObject) {
370            String s = compareObjects(path, (JsonObject) n1, (JsonObject) n2);
371                        if (!Utilities.noString(s))
372                                return s;
373          } else if (n1 instanceof JsonArray) {
374                JsonArray a1 = (JsonArray) n1;
375                JsonArray a2 = (JsonArray) n2;
376          
377            if (a1.size() != a2.size()) 
378              return "array properties differ at "+path+": count "+Integer.toString(a1.size())+"/"+Integer.toString(a2.size());
379            for (int i = 0; i < a1.size(); i++) {
380                String s = compareNodes(path+"["+Integer.toString(i)+"]", a1.get(i), a2.get(i));
381                                if (!Utilities.noString(s))
382                                        return s;
383            }
384          }
385          else if (n1 instanceof JsonNull) {
386                
387          } else
388            return "unhandled property "+n1.getClass().getName();
389                return null;
390        }
391
392  public static String temp() {
393    if (new File("c:\\temp").exists())
394      return "c:\\temp";
395    return System.getProperty("java.io.tmpdir");
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
448  public static String resourceNameToFile(String name) throws IOException {
449    return Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", name);
450  }
451
452
453  public static String resourceNameToFile(String subFolder, String name) throws IOException {
454    return Utilities.path(System.getProperty("user.dir"), "src", "test", "resources", subFolder, name);
455  }
456
457}