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}