package org.batfish.common.util; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import org.batfish.common.BatfishException; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; public class JsonDiff { public static final String ADDED_ITEM_CODE = "ADDED :: "; public static final String CHANGED_ITEM_BASE = "BASE"; public static final String CHANGED_ITEM_CODE = "CHANGED :: "; public static final String CHANGED_ITEM_DELTA = "DELTA"; public static final String COMMON_ITEM_CODE = "COMMON :: "; public static final String REMOVED_ITEM_CODE = "REMOVED :: "; public static String getStringWithoutCode(String key) { String[] words = key.split(" :: "); return words[1]; } private final SortedMap<String, Object> _data; public JsonDiff(JSONArray lhsArray, JSONArray rhsArray) throws JSONException { _data = new TreeMap<>(); int lhsLength = lhsArray.length(); int rhsLength = rhsArray.length(); int minLength = Math.min(lhsLength, rhsLength); for (int i = 0; i < minLength; i++) { Object lhsCurrentElem = lhsArray.get(i); Object rhsCurrentElem = rhsArray.get(i); if (lhsCurrentElem instanceof JSONObject) { if (rhsCurrentElem instanceof JSONObject) { // Both are JSONObjects JSONObject lhsJ = (JSONObject) lhsCurrentElem; JSONObject rhsJ = (JSONObject) rhsCurrentElem; JsonDiff diff = new JsonDiff(lhsJ, rhsJ); if (!diff._data.isEmpty()) { String indexAsString = COMMON_ITEM_CODE + i; _data.put(indexAsString, diff); } } else { throw new BatfishException( "Cannot compare JSONObject to non-JSONObject"); } } else if (lhsCurrentElem instanceof JSONArray) { if (rhsCurrentElem instanceof JSONArray) { // Both are JSONArrays JSONArray lhsA = (JSONArray) lhsCurrentElem; JSONArray rhsA = (JSONArray) rhsCurrentElem; JsonDiff diff = new JsonDiff(lhsA, rhsA); if (!diff._data.isEmpty()) { String indexAsString = COMMON_ITEM_CODE + i; _data.put(indexAsString, diff); } } else { throw new BatfishException( "Cannot compare JSONArray to non-JSONArray"); } } else if (rhsCurrentElem instanceof JSONObject) { throw new BatfishException( "Cannot compare non-JSONObject to JSONObject"); } else if (rhsCurrentElem instanceof JSONArray) { throw new BatfishException( "Cannot compare non-JSONArray to JSONArray"); } else { // Neither is a JSONObject nor JSONArray Class<?> lhsClass = lhsCurrentElem.getClass(); Class<?> rhsClass = rhsCurrentElem.getClass(); if (!lhsClass.equals(rhsClass)) { throw new BatfishException( "lhs class: '" + lhsClass.getCanonicalName() + "' differs from rhs class: '" + rhsClass.getCanonicalName() + "'"); } if (!lhsCurrentElem.equals(rhsCurrentElem)) { // String removedIndex = CHANGED_ITEM_BASE_CODE + i; // String addedIndex = CHANGED_ITEM_DELTA_CODE + i; // _data.put(removedIndex, lhsCurrentElem); // _data.put(addedIndex, rhsCurrentElem); String key = CHANGED_ITEM_CODE + i; SortedMap<String, Object> value = new TreeMap<>(); value.put(CHANGED_ITEM_BASE, lhsCurrentElem); value.put(CHANGED_ITEM_DELTA, rhsCurrentElem); _data.put(key, value); } } } for (int i = minLength; i < lhsLength; i++) { String removedIndex = REMOVED_ITEM_CODE + i; Object lhsCurrentElem = lhsArray.get(i); Object jdValue = getValue(lhsCurrentElem); _data.put(removedIndex, jdValue); } for (int i = minLength; i < rhsLength; i++) { String addedIndex = ADDED_ITEM_CODE + i; Object rhsCurrentElem = rhsArray.get(i); Object jdValue = getValue(rhsCurrentElem); _data.put(addedIndex, jdValue); } } public JsonDiff(JSONObject lhs, JSONObject rhs) { _data = new TreeMap<>(); try { Set<String> lhsKeys = new TreeSet<>(); Set<String> rhsKeys = new TreeSet<>(); Set<String> commonKeys = new TreeSet<>(); Set<String> lhsOnlyKeys = new TreeSet<>(); Set<String> rhsOnlyKeys = new TreeSet<>(); for (Iterator<?> i = lhs.keys(); i.hasNext();) { String key = (String) i.next(); lhsKeys.add(key); } for (Iterator<?> i = rhs.keys(); i.hasNext();) { String key = (String) i.next(); rhsKeys.add(key); } commonKeys.addAll(lhsKeys); commonKeys.retainAll(rhsKeys); lhsOnlyKeys.addAll(lhsKeys); lhsOnlyKeys.removeAll(rhsKeys); rhsOnlyKeys.addAll(rhsKeys); rhsOnlyKeys.removeAll(lhsKeys); for (String lhsOnlyKey : lhsOnlyKeys) { String removedKeyName = REMOVED_ITEM_CODE + lhsOnlyKey; _data.put(removedKeyName, getValue(lhs.get(lhsOnlyKey))); } for (String rhsOnlyKey : rhsOnlyKeys) { String addedKeyName = ADDED_ITEM_CODE + rhsOnlyKey; _data.put(addedKeyName, getValue(rhs.get(rhsOnlyKey))); } for (String commonKey : commonKeys) { Object lhsValue = lhs.get(commonKey); Object rhsValue = rhs.get(commonKey); if (lhsValue instanceof JSONObject) { if (rhsValue instanceof JSONObject) { // Both are JSONObjects JSONObject lhsValueJson = (JSONObject) lhsValue; JSONObject rhsValueJson = (JSONObject) rhsValue; JsonDiff value = new JsonDiff(lhsValueJson, rhsValueJson); if (!value._data.isEmpty()) { _data.put(commonKey, value); } } else { throw new BatfishException( "Cannot compare JSONObject to non-JSONObject"); } } else if (lhsValue instanceof JSONArray) { if (rhsValue instanceof JSONArray) { // Both are JSONArrays JSONArray lhsArray = (JSONArray) lhsValue; JSONArray rhsArray = (JSONArray) rhsValue; JsonDiff arrayDiff = new JsonDiff(lhsArray, rhsArray); if (!arrayDiff._data.isEmpty()) { _data.put(commonKey, arrayDiff); } } else { throw new BatfishException( "Cannot compare JSONArray to non-JSONArray"); } } else if (rhsValue instanceof JSONArray) { throw new BatfishException( "Cannot compare non-JSONArray to JSONArray"); } else if (rhsValue instanceof JSONObject) { throw new BatfishException( "Cannot compare non-JSONObject to JSONObject"); } else { // Neither is a JSONObject nor a JSONArray Class<?> lhsClass = lhsValue.getClass(); Class<?> rhsClass = rhsValue.getClass(); if (!lhsClass.equals(rhsClass)) { throw new BatfishException( "lhs class: '" + lhsClass.getCanonicalName() + "' differs from rhs class: '" + rhsClass.getCanonicalName() + "'"); } if (!lhsValue.equals(rhsValue)) { // String removedKey = CHANGED_ITEM_BASE_CODE + commonKey; // String addedKey = CHANGED_ITEM_DELTA_CODE + commonKey; // _data.put(removedKey, lhsValue); // _data.put(addedKey, rhsValue); String key = CHANGED_ITEM_CODE + commonKey; SortedMap<String, Object> value = new TreeMap<>(); value.put(CHANGED_ITEM_BASE, lhsValue); value.put(CHANGED_ITEM_DELTA, rhsValue); _data.put(key, value); } } } } catch (JSONException e) { throw new BatfishException("Could not create diff of JSON objects", e); } } @JsonCreator private JsonDiff(SortedMap<String, Object> data) { _data = data; } @JsonValue public SortedMap<String, Object> getData() { return _data; } private Object getValue(Object object) throws JSONException { if (object instanceof JSONObject) { JSONObject j = (JSONObject) object; Map<String, Object> map = new TreeMap<>(); for (Iterator<?> i = j.keys(); i.hasNext();) { String key = (String) i.next(); Object value = j.get(key); Object jdValue = getValue(value); map.put(key, jdValue); } return map; } else if (object instanceof JSONArray) { List<Object> list = new LinkedList<>(); JSONArray array = (JSONArray) object; for (int i = 0; i < array.length(); i++) { Object value = array.get(i); Object jdValue = getValue(value); list.add(jdValue); } return list; } else { return object; } } public String prettyPrint(String prefixStr) { return PrettyPrinter.print(prefixStr, this); } }