001package org.hl7.fhir.r5.test.utils;
002
003import org.apache.commons.codec.binary.Base64;
004import org.hl7.fhir.utilities.CSFile;
005import org.hl7.fhir.utilities.TextFile;
006import org.hl7.fhir.utilities.ToolGlobalSettings;
007import org.hl7.fhir.utilities.Utilities;
008
009import org.w3c.dom.Document;
010import org.w3c.dom.Element;
011import org.w3c.dom.NamedNodeMap;
012import org.w3c.dom.Node;
013
014import com.google.gson.JsonArray;
015import com.google.gson.JsonElement;
016import com.google.gson.JsonNull;
017import com.google.gson.JsonObject;
018import com.google.gson.JsonPrimitive;
019import com.google.gson.JsonSyntaxException;
020import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
021
022import javax.xml.parsers.DocumentBuilder;
023import javax.xml.parsers.DocumentBuilderFactory;
024import java.io.*;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Map;
028
029public class CompareUtilities extends BaseTestingUtilities {
030
031  private static final boolean SHOW_DIFF = true;
032
033  public static String createNotEqualMessage(final String message, final String expected, final String actual) {
034    return new StringBuilder()
035      .append(message).append('\n')
036      .append("Expected :").append(expected).append('\n')
037      .append("Actual  :").append(actual).toString();
038  }
039
040  public static String checkXMLIsSame(InputStream expected, InputStream actual) throws Exception {
041    String result = compareXml(expected, actual);
042    return result;
043  }
044
045  public static String checkXMLIsSame(String expected, String actual) throws Exception {
046    String result = compareXml(expected, actual);
047    if (result != null && SHOW_DIFF) {
048      String diff = ToolGlobalSettings.hasComparePath() ? ToolGlobalSettings.getComparePath() : Utilities.path(System.getenv("ProgramFiles"), "WinMerge", "WinMergeU.exe");
049      if (new File(diff).exists() || Utilities.isToken(diff)) {
050        Runtime.getRuntime().exec(new String[]{diff, expected, actual});
051      }
052    }
053    return result;
054  }
055
056  private static String compareXml(InputStream expected, InputStream actual) throws Exception {
057    return compareElements("", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement());
058  }
059
060  private static String compareXml(String expected, String actual) throws Exception {
061    return compareElements("", loadXml(expected).getDocumentElement(), loadXml(actual).getDocumentElement());
062  }
063
064  private static String compareElements(String path, Element expectedElement, Element actualElement) {
065    if (!namespacesMatch(expectedElement.getNamespaceURI(), actualElement.getNamespaceURI()))
066      return createNotEqualMessage("Namespaces differ at " + path, expectedElement.getNamespaceURI(), actualElement.getNamespaceURI());
067    if (!expectedElement.getLocalName().equals(actualElement.getLocalName()))
068      return createNotEqualMessage("Names differ at " + path ,  expectedElement.getLocalName(), actualElement.getLocalName());
069    path = path + "/" + expectedElement.getLocalName();
070    String s = compareAttributes(path, expectedElement.getAttributes(), actualElement.getAttributes());
071    if (!Utilities.noString(s))
072      return s;
073    s = compareAttributes(path, expectedElement.getAttributes(), actualElement.getAttributes());
074    if (!Utilities.noString(s))
075      return s;
076
077    Node expectedChild = expectedElement.getFirstChild();
078    Node actualChild = actualElement.getFirstChild();
079    expectedChild = skipBlankText(expectedChild);
080    actualChild = skipBlankText(actualChild);
081    while (expectedChild != null && actualChild != null) {
082      if (expectedChild.getNodeType() != actualChild.getNodeType())
083        return createNotEqualMessage("node type mismatch in children of " + path, Short.toString(expectedElement.getNodeType()), Short.toString(actualElement.getNodeType()));
084      if (expectedChild.getNodeType() == Node.TEXT_NODE) {
085        if (!normalise(expectedChild.getTextContent()).equals(normalise(actualChild.getTextContent())))
086          return createNotEqualMessage("Text differs at " + path, normalise(expectedChild.getTextContent()).toString(), normalise(actualChild.getTextContent()).toString());
087      } else if (expectedChild.getNodeType() == Node.ELEMENT_NODE) {
088        s = compareElements(path, (Element) expectedChild, (Element) actualChild);
089        if (!Utilities.noString(s))
090          return s;
091      }
092
093      expectedChild = skipBlankText(expectedChild.getNextSibling());
094      actualChild = skipBlankText(actualChild.getNextSibling());
095    }
096    if (expectedChild != null)
097      return "node mismatch - more nodes in actual in children of " + path;
098    if (actualChild != null)
099      return "node mismatch - more nodes in expected in children of " + path;
100    return null;
101  }
102
103  private static boolean namespacesMatch(String ns1, String ns2) {
104    return ns1 == null ? ns2 == null : ns1.equals(ns2);
105  }
106
107  private static Object normalise(String text) {
108    String result = text.trim().replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
109    while (result.contains("  "))
110      result = result.replace("  ", " ");
111    return result;
112  }
113
114  private static String compareAttributes(String path, NamedNodeMap expected, NamedNodeMap actual) {
115    for (int i = 0; i < expected.getLength(); i++) {
116
117      Node expectedNode = expected.item(i);
118      String expectedNodeName = expectedNode.getNodeName();
119      if (!(expectedNodeName.equals("xmlns") || expectedNodeName.startsWith("xmlns:"))) {
120        Node actualNode = actual.getNamedItem(expectedNodeName);
121        if (actualNode == null)
122          return "Attributes differ at " + path + ": missing attribute " + expectedNodeName;
123        if (!normalise(expectedNode.getTextContent()).equals(normalise(actualNode.getTextContent()))) {
124          byte[] b1 = unBase64(expectedNode.getTextContent());
125          byte[] b2 = unBase64(actualNode.getTextContent());
126          if (!sameBytes(b1, b2))
127            return createNotEqualMessage("Attributes differ at " + path, normalise(expectedNode.getTextContent()).toString(), normalise(actualNode.getTextContent()).toString()) ;
128        }
129      }
130    }
131    return null;
132  }
133
134  private static boolean sameBytes(byte[] b1, byte[] b2) {
135    if (b1.length == 0 || b2.length == 0)
136      return false;
137    if (b1.length != b2.length)
138      return false;
139    for (int i = 0; i < b1.length; i++)
140      if (b1[i] != b2[i])
141        return false;
142    return true;
143  }
144
145  private static byte[] unBase64(String text) {
146    return Base64.decodeBase64(text);
147  }
148
149  private static Node skipBlankText(Node node) {
150    while (node != null && (((node.getNodeType() == Node.TEXT_NODE) && Utilities.isWhitespace(node.getTextContent())) || (node.getNodeType() == Node.COMMENT_NODE)))
151      node = node.getNextSibling();
152    return node;
153  }
154
155  private static Document loadXml(String fn) throws Exception {
156    return loadXml(new FileInputStream(fn));
157  }
158
159  private static Document loadXml(InputStream fn) throws Exception {
160    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
161    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
162    factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
163    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
164    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
165    factory.setXIncludeAware(false);
166    factory.setExpandEntityReferences(false);
167
168    factory.setNamespaceAware(true);
169    DocumentBuilder builder = factory.newDocumentBuilder();
170    return builder.parse(fn);
171  }
172
173  public static String checkJsonSrcIsSame(String expected, String actual) throws JsonSyntaxException, FileNotFoundException, IOException {
174    return checkJsonSrcIsSame(expected, actual, true);
175  }
176
177  public static String checkJsonSrcIsSame(String expectedString, String actualString, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException {
178    String result = compareJsonSrc(expectedString, actualString);
179    if (result != null && SHOW_DIFF && showDiff) {
180      String diff = null;
181      if (System.getProperty("os.name").contains("Linux"))
182        diff = Utilities.path("/", "usr", "bin", "meld");
183      else {
184        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
185          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
186        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
187          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
188      }
189      if (diff == null || diff.isEmpty())
190        return result;
191
192      List<String> command = new ArrayList<String>();
193      String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json");
194      String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json");
195      TextFile.stringToFile(expectedString, expected);
196      TextFile.stringToFile(actualString, actual);
197      command.add(diff);
198      if (diff.toLowerCase().contains("meld"))
199        command.add("--newtab");
200      command.add(expected);
201      command.add(actual);
202
203      ProcessBuilder builder = new ProcessBuilder(command);
204      builder.directory(new CSFile(Utilities.path("[tmp]")));
205      builder.start();
206
207    }
208    return result;
209  }
210
211  public static String checkJsonIsSame(String expected, String actual) throws JsonSyntaxException, FileNotFoundException, IOException {
212    String result = compareJson(expected, actual);
213    if (result != null && SHOW_DIFF) {
214      String diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
215      List<String> command = new ArrayList<String>();
216      command.add("\"" + diff + "\" \"" + expected +  "\" \"" + actual + "\"");
217
218      ProcessBuilder builder = new ProcessBuilder(command);
219      builder.directory(new CSFile(Utilities.path("[tmp]")));
220      builder.start();
221
222    }
223    return result;
224  }
225
226  private static String compareJsonSrc(String expected, String actual) throws JsonSyntaxException, FileNotFoundException, IOException {
227    JsonObject actualJsonObject = (JsonObject) new com.google.gson.JsonParser().parse(actual);
228    JsonObject expectedJsonObject = (JsonObject) new com.google.gson.JsonParser().parse(expected);
229    return compareObjects("", expectedJsonObject, actualJsonObject);
230  }
231
232  private static String compareJson(String expected, String actual) throws JsonSyntaxException, FileNotFoundException, IOException {
233    JsonObject actualJsonObject = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(actual));
234    JsonObject expectedJsonObject = (JsonObject) new com.google.gson.JsonParser().parse(TextFile.fileToString(expected));
235    return compareObjects("", expectedJsonObject, actualJsonObject);
236  }
237
238  private static String compareObjects(String path, JsonObject expectedJsonObject, JsonObject actualJsonObject) {
239    for (Map.Entry<String, JsonElement> en : actualJsonObject.entrySet()) {
240      String n = en.getKey();
241      if (!n.equals("fhir_comments")) {
242        if (expectedJsonObject.has(n)) {
243          String s = compareNodes(path + '.' + n, expectedJsonObject.get(n), en.getValue());
244          if (!Utilities.noString(s))
245            return s;
246        } else
247          return "properties differ at " + path + ": missing property " + n;
248      }
249    }
250    for (Map.Entry<String, JsonElement> en : expectedJsonObject.entrySet()) {
251      String n = en.getKey();
252      if (!n.equals("fhir_comments")) {
253        if (!actualJsonObject.has(n))
254          return "properties differ at " + path + ": missing property " + n;
255      }
256    }
257    return null;
258  }
259
260  private static String compareNodes(String path, JsonElement expectedJsonElement, JsonElement actualJsonElement) {
261    if (actualJsonElement.getClass() != expectedJsonElement.getClass())
262      return createNotEqualMessage("properties differ at " + path, expectedJsonElement.getClass().getName(), actualJsonElement.getClass().getName());
263    else if (actualJsonElement instanceof JsonPrimitive) {
264      JsonPrimitive actualJsonPrimitive = (JsonPrimitive) actualJsonElement;
265      JsonPrimitive expectedJsonPrimitive = (JsonPrimitive) expectedJsonElement;
266      if (actualJsonPrimitive.isBoolean() && expectedJsonPrimitive.isBoolean()) {
267        if (actualJsonPrimitive.getAsBoolean() != expectedJsonPrimitive.getAsBoolean())
268          return createNotEqualMessage("boolean property values differ at " + path , expectedJsonPrimitive.getAsString(), actualJsonPrimitive.getAsString());
269      } else if (actualJsonPrimitive.isString() && expectedJsonPrimitive.isString()) {
270        String actualJsonString = actualJsonPrimitive.getAsString();
271        String expectedJsonString = expectedJsonPrimitive.getAsString();
272        if (!(actualJsonString.contains("<div") && expectedJsonString.contains("<div")))
273          if (!actualJsonString.equals(expectedJsonString))
274            if (!sameBytes(unBase64(actualJsonString), unBase64(expectedJsonString)))
275              return createNotEqualMessage("string property values differ at " + path, expectedJsonString, actualJsonString);
276      } else if (actualJsonPrimitive.isNumber() && expectedJsonPrimitive.isNumber()) {
277        if (!actualJsonPrimitive.getAsString().equals(expectedJsonPrimitive.getAsString()))
278          return createNotEqualMessage("number property values differ at " + path, expectedJsonPrimitive.getAsString(), actualJsonPrimitive.getAsString());
279      } else
280        return createNotEqualMessage("property types differ at " + path, expectedJsonPrimitive.getAsString(), actualJsonPrimitive.getAsString());
281    } else if (actualJsonElement instanceof JsonObject) {
282      String s = compareObjects(path, (JsonObject) expectedJsonElement, (JsonObject) actualJsonElement);
283      if (!Utilities.noString(s))
284        return s;
285    } else if (actualJsonElement instanceof JsonArray) {
286      JsonArray actualArray = (JsonArray) actualJsonElement;
287      JsonArray expectedArray = (JsonArray) expectedJsonElement;
288
289      if (actualArray.size() != expectedArray.size())
290        return createNotEqualMessage("array properties count differs at " + path, Integer.toString(expectedArray.size()), Integer.toString(actualArray.size()));
291      for (int i = 0; i < actualArray.size(); i++) {
292        String s = compareNodes(path + "[" + Integer.toString(i) + "]", expectedArray.get(i), actualArray.get(i));
293        if (!Utilities.noString(s))
294          return s;
295      }
296    } else if (actualJsonElement instanceof JsonNull) {
297
298    } else
299      return "unhandled property " + actualJsonElement.getClass().getName();
300    return null;
301  }
302
303  public static String checkTextIsSame(String expected, String actual) throws JsonSyntaxException, FileNotFoundException, IOException {
304    return checkTextIsSame(expected, actual, true);
305  }
306
307  public static String checkTextIsSame(String expectedString, String actualString, boolean showDiff) throws JsonSyntaxException, FileNotFoundException, IOException {
308    String result = compareText(expectedString, actualString);
309    if (result != null && SHOW_DIFF && showDiff) {
310      String diff = null;
311      if (System.getProperty("os.name").contains("Linux"))
312        diff = Utilities.path("/", "usr", "bin", "meld");
313      else {
314        if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge"), "\\WinMergeU.exe", null))
315          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "WinMerge", "WinMergeU.exe");
316        else if (Utilities.checkFile("WinMerge", Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld"), "\\Meld.exe", null))
317          diff = Utilities.path(System.getenv("ProgramFiles(X86)"), "Meld", "Meld.exe");
318      }
319      if (diff == null || diff.isEmpty())
320        return result;
321
322      List<String> command = new ArrayList<String>();
323      String actual = Utilities.path("[tmp]", "actual" + actualString.hashCode() + ".json");
324      String expected = Utilities.path("[tmp]", "expected" + expectedString.hashCode() + ".json");
325      TextFile.stringToFile(expectedString, expected);
326      TextFile.stringToFile(actualString, actual);
327      command.add(diff);
328      if (diff.toLowerCase().contains("meld"))
329        command.add("--newtab");
330      command.add(expected);
331      command.add(actual);
332
333      ProcessBuilder builder = new ProcessBuilder(command);
334      builder.directory(new CSFile(Utilities.path("[tmp]")));
335      builder.start();
336
337    }
338    return result;
339  }
340
341
342  private static String compareText(String expectedString, String actualString) {
343    for (int i = 0; i < Integer.min(expectedString.length(), actualString.length()); i++) {
344      if (expectedString.charAt(i) != actualString.charAt(i))
345        return createNotEqualMessage("Strings differ at character " + Integer.toString(i), String.valueOf(expectedString.charAt(i)), String.valueOf(actualString.charAt(i)));
346    }
347    if (expectedString.length() != actualString.length())
348      return createNotEqualMessage("Strings differ in length but match to the end of the shortest.", Integer.toString(expectedString.length()), Integer.toString(actualString.length()));
349    return null;
350  }
351}