Created
September 26, 2019 12:44
-
-
Save sergey-scherbina/86d9227b431d5d93f0359dd739e7a5d3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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