package restx.tests.json;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import difflib.myers.Equalizer;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Date: 3/2/14
* Time: 22:03
*/
@SuppressWarnings("unchecked")
public class JsonDiffer {
public static final JsonDiffComparator DEFAULT_JSON_DIFF_COMPARATOR = new JsonDiffComparator() {
@Override
public boolean compare(JsonDiff diff, Object o1, Object o2) {
return Objects.equal(o1, o2);
}
};
public static class Config {
private boolean ignoreExtraFields = false;
public boolean isIgnoreExtraFields() {
return ignoreExtraFields;
}
public Config setIgnoreExtraFields(final boolean ignoreExtraFields) {
this.ignoreExtraFields = ignoreExtraFields;
return this;
}
}
private Config leftConfig = new Config();
private Config rightConfig = new Config();
private JsonDiffComparator jsonDiffComparator = DEFAULT_JSON_DIFF_COMPARATOR;
public Config getLeftConfig() {
return leftConfig;
}
public Config getRightConfig() {
return rightConfig;
}
public JsonDiffer setJsonDiffComparator(JsonDiffComparator jsonDiffComparator) {
this.jsonDiffComparator = checkNotNull(jsonDiffComparator);
return this;
}
public JsonDiff compare(JsonSource left, JsonSource right) {
try {
JsonWithLocationsParser.ParsedJsonWithLocations leftObj = new JsonWithLocationsParser().parse(left, Object.class);
JsonWithLocationsParser.ParsedJsonWithLocations rightObj = new JsonWithLocationsParser().parse(right, Object.class);
return diff(new JsonDiff(leftObj, rightObj), leftObj.getRoot(), rightObj.getRoot());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private JsonDiff diff(JsonDiff diff, Object o1, Object o2) {
if (o1 instanceof Map && o2 instanceof Map) {
diffMaps(diff, (Map) o1, (Map) o2);
} else if (o1 instanceof List && o2 instanceof List) {
diffLists(diff, (List) o1, (List) o2);
} else {
if (!jsonDiffComparator.compare(diff, o1, o2)) {
diff.addDifference(new JsonDiff.ValueDiff(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.currentLeftContextLocation(),
diff.currentRightContextLocation(),
o1, o2
));
}
}
return diff;
}
private void diffLists(final JsonDiff diff, List<Object> o1, List<Object> o2) {
final Patch<Object> lDiff = DiffUtils.diff(o1, o2, new Equalizer<Object>() {
@Override
public boolean equals(Object o1, Object o2) {
// we don't give new roots here, since we only need to know if objects are the the same
return diff(new JsonDiff(diff.getLeftObj(), diff.getRightObj()), o1, o2).isSame();
}
});
for (Delta<Object> objectDelta : lDiff.getDeltas()) {
switch (objectDelta.getType()) {
case INSERT:
diff.addDifference(new JsonDiff.ArrayInsertedValue(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.currentLeftContextLocation(),
diff.currentRightContextLocation(),
objectDelta.getOriginal().getPosition(),
objectDelta.getRevised().getPosition(),
objectDelta.getRevised().getLines()
));
break;
case DELETE:
diff.addDifference(new JsonDiff.ArrayDeletedValue(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.currentLeftContextLocation(),
diff.currentRightContextLocation(),
objectDelta.getOriginal().getPosition(),
objectDelta.getRevised().getPosition(),
objectDelta.getOriginal().getLines()
));
break;
case CHANGE:
int leftPosition = objectDelta.getOriginal().getPosition();
int rightPosition = objectDelta.getRevised().getPosition();
int changed = Math.min(
objectDelta.getOriginal().getLines().size(),
objectDelta.getRevised().getLines().size());
for (int i = 0; i < changed; i++) {
Object left = o1.get(leftPosition + i);
Object right = o2.get(rightPosition + i);
try {
diff(diff.goIn("["+(leftPosition + i)+"]", "["+(rightPosition + i)+"]")
.putContexts(new JsonDiff.ListSetter(o1, leftPosition + i), left,
new JsonDiff.ListSetter(o2, rightPosition + i), right),
left, right);
} finally {
diff.goUp();
}
}
// we may have ore values in either of two lists:
// diff algo is not returning INSERT / DELETE in some cases
if (objectDelta.getOriginal().getLines().size() > changed) {
diff.addDifference(new JsonDiff.ArrayDeletedValue(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.currentLeftContextLocation(),
diff.currentRightContextLocation(),
leftPosition + changed,
rightPosition + changed,
o1.subList(leftPosition + changed,
leftPosition + objectDelta.getOriginal().getLines().size())
));
}
if (objectDelta.getRevised().getLines().size() > changed) {
diff.addDifference(new JsonDiff.ArrayInsertedValue(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.currentLeftContextLocation(),
diff.currentRightContextLocation(),
leftPosition + changed,
rightPosition + changed,
o2.subList(rightPosition + changed,
rightPosition + objectDelta.getRevised().getLines().size())
));
}
break;
}
}
}
private void diffMaps(JsonDiff diff, Map<String, Object> m1, Map<String, Object> m2) {
if (!leftConfig.isIgnoreExtraFields()) {
for (String k : Sets.difference(m1.keySet(), m2.keySet())) {
diff.addDifference(new JsonDiff.RemovedKey(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.contextLeft(m1),
diff.contextRight(m2),
k, m1.get(k)));
}
}
if (!rightConfig.isIgnoreExtraFields()) {
for (String k : Sets.difference(m2.keySet(), m1.keySet())) {
diff.addDifference(new JsonDiff.AddedKey(
diff.currentLeftPath(),
diff.currentRightPath(),
diff.contextLeft(m1),
diff.contextRight(m2),
k, m2.get(k)));
}
}
for (String k : Sets.intersection(m1.keySet(), m2.keySet())) {
try {
diff(diff.goIn(k).putContexts(
new JsonDiff.MapSetter(m1, k), m1.get(k),
new JsonDiff.MapSetter(m2, k), m2.get(k)),
m1.get(k), m2.get(k));
} finally {
diff.goUp();
}
}
}
}