Skip to content

Instantly share code, notes, and snippets.

@sergey-scherbina
Created September 26, 2019 12:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sergey-scherbina/86d9227b431d5d93f0359dd739e7a5d3 to your computer and use it in GitHub Desktop.
Save sergey-scherbina/86d9227b431d5d93f0359dd739e7a5d3 to your computer and use it in GitHub Desktop.
package json.diff;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final public class JsonDiff {
private static final ObjectMapper mapper = new ObjectMapper();
private JsonDiff() {
throw new AssertionError("No instances is needed");
}
public static int main(final String[] args) throws IOException {
assert args != null && args.length > 1 : "Usage: inputFile1 inputFile2 [outputFile]";
return jsonCompare(args[0], args[1], args.length > 2 ? args[2] : null) ? 0 : 1;
}
static boolean jsonCompare(final String inputFile1, final String inputFile2,
final String outputFile) throws IOException {
final Map<String, Map<String, List<JsonLocation>>> index1 = parseJson(inputFile1);
final Map<String, Map<String, List<JsonLocation>>> index2 = parseJson(inputFile2);
final MapDifference<String, Map<String, Integer>> diff = Maps.difference(counts(index1), counts(index2));
if (outputFile != null) System.setOut(new PrintStream(outputFile));
System.out.println("Comparison of '" + inputFile1 + "' to '" + inputFile2 + "'");
System.out.println("\nStatus: " + (diff.areEqual() ? "Success [equal]" : "Failure [mismatch]"));
showDiff("Entries only in '" + inputFile1 + "'", diff.entriesOnlyOnLeft(), k -> showOnly(index1.get(k)));
showDiff("Entries only in '" + inputFile2 + "'", diff.entriesOnlyOnRight(), k -> showOnly(index2.get(k)));
showDiff("Entries differing", diff.entriesDiffering(), k -> showDiff(k, index1.get(k), index2.get(k)));
return diff.areEqual();
}
private static Map<String, Map<String, List<JsonLocation>>> parseJson(final String inputFile) throws IOException {
Map<String, Map<String, List<JsonLocation>>> m = Collections.emptyMap();
try (final JsonParser parser = mapper.getFactory().createParser(new File(inputFile))) {
String path = "";
for (JsonToken token = parser.nextValue(); token != null; token = parser.nextValue()) {
if (parser.currentName() != null) {
final String subPath = "/" + parser.currentName();
if (token.isStructStart()) {
path = path + subPath;
} else if (token.isStructEnd()) {
final int pos = path.lastIndexOf(subPath);
if (pos >= 0) path = path.substring(0, pos);
} else if (token.isScalarValue()) {
m = merge(m, Collections.singletonMap(path + subPath,
Collections.singletonMap(parser.getValueAsString(),
Collections.singletonList(parser.getCurrentLocation()))));
}
}
}
}
return m;
}
private static <K1, K2, V> Map<K1, Map<K2, List<V>>> merge(
final Map<K1, Map<K2, List<V>>> m1, final Map<K1, Map<K2, List<V>>> m2) {
return Stream.concat(m1.entrySet().stream(), m2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> Stream.concat(a.entrySet().stream(), b.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(x, y) -> Stream.concat(x.stream(), y.stream())
.collect(Collectors.toList())))));
}
private static <K1, K2, V> Map<K1, Map<K2, Integer>> counts(final Map<K1, Map<K2, List<V>>> m) {
return m.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> counts2(e.getValue())));
}
private static <K, V> Map<K, Integer> counts2(final Map<K, List<V>> m) {
return m.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size()));
}
private static String showOnly(final Map<String, List<JsonLocation>> index) {
return index.entrySet().stream().map(v ->
showLoc(v.getKey(), v.getValue(), v.getValue().size()))
.collect(Collectors.toList()).toString();
}
private static <T> void showDiff(final String header, final Map<String, T> diff,
final Function<String, String> format) {
if (!diff.isEmpty()) {
System.out.println("\n" + header + ":\n");
diff.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEach(e -> System.out.println(format.apply(e.getKey())));
}
}
private static String showDiff(final String path,
final Map<String, List<JsonLocation>> leftIndex,
final Map<String, List<JsonLocation>> rightIndex) {
final MapDifference<String, Integer> diff = Maps.difference(counts2(leftIndex), counts2(rightIndex));
final List<String> left = join(diff.entriesOnlyOnLeft().keySet(), leftIndex, diff, n -> n > 0);
final List<String> right = join(diff.entriesOnlyOnRight().keySet(), rightIndex, diff, n -> n < 0);
return (left.isEmpty() && right.isEmpty()) ? "" : (path + " (" +
(left.isEmpty() ? " " : left) + ", " + (right.isEmpty() ? " " : right) + ")");
}
private static List<String> join(final Set<String> keys, final Map<String, List<JsonLocation>> index,
final MapDifference<String, Integer> diff,
final Function<Integer, Boolean> f) {
return Stream.concat(keys.stream().map(k -> Maps.immutableEntry(k, 1)),
diff.entriesDiffering().entrySet().stream().map(e -> Maps.immutableEntry(e.getKey(),
e.getValue().leftValue() - e.getValue().rightValue()))
.filter(e -> f.apply(e.getValue())))
.sorted(Map.Entry.comparingByKey())
.map(v -> showLoc(v.getKey(), index.get(v.getKey()), Math.abs(v.getValue())))
.collect(Collectors.toList());
}
private static String showLoc(final String v, final List<JsonLocation> locations, final int count) {
return "'" + v + "':" + locations.stream().limit(count)
.map(loc -> "" + loc.getLineNr()).collect(Collectors.joining(","));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment