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