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