package jcmp;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.HashSet;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;
import com.fasterxml.jackson.databind.node.NullNode;
import jiff.AbstractFieldFilter;
/**
* Compares two json documents and builds an list of all changes
*
* Json containers (objects and arrays) are compared recursively. The comparison
* algorithm works like this:
*
* Objects: A field-by-field comparison is done. If a field exists in the first
* document but not in the second, that field is removed. If a field exists in
* the second document but not the first, that field is added. If a field exists
* in both documents with different values, that field is modified.
*
* Arrays: There are two possible algorithms to compare arrays. If array
* elements contain a unique identifier (which is defined by the caller), then
* array elelements of the first and the second document are matched using the
* unique identifiers of array elements. Then each matching array element is
* compared to generate the detailed difference. If array elements don't have
* unique identifiers, then each element of the first array is compared to each
* element of the second array, and the elements with minimal number of changes
* are associated. Elements that are too different from each other are not
* associated.
*
* Differences:
*
* An Addition denotes a new field or array element. Addition.field1 is null,
* meaning the field does not exist in document1, and Addition.field2 denotes
* the new field, or array element.
*
* A Removal denotes a removed field or array element. Removal.field1 denotes
* the element in document1, and Removal.field2 is null.
*
* A Modification denotes a content modification of a field, or array element.
* Both field1 and field2 are non-null, and set to the name of the modified
* field.
*
* A Move denotes an array element move. field1 denotes the old index of the
* array element, and field2 denotes the new index.
*
* If new elements are added to an array, or existing elements are removed, the
* addition and removal appear as diff, and any node that shifted during the
* operation appears within a Move.
*/
public class JsonCompare extends DocCompare<JsonNode, ValueNode, ObjectNode, ArrayNode> {
public static class DefaultIdentityExtractor implements IdentityExtractor<JsonNode> {
private final String[] fields;
public DefaultIdentityExtractor(ArrayIdentityFields fields) {
this.fields = fields.getFields();
}
@Override
public Object getIdentity(JsonNode element) {
JsonNode[] nodes = new JsonNode[fields.length];
for (int i = 0; i < fields.length; i++) {
nodes[i] = getValue(element, fields[i]);
}
return new DefaultIdentity(nodes);
}
}
public static JsonNode getValue(JsonNode element, String field) {
for (String fld : AbstractFieldFilter.parse(field)) {
if (element.isArray()) {
element = element.get(Integer.valueOf(fld));
} else if (element.isObject()) {
element = element.get(fld);
} else {
element = null;
}
if (element == null) {
break;
}
}
return element;
}
@Override
protected boolean isValue(JsonNode value) {
return value instanceof ValueNode;
}
@Override
protected boolean isArray(JsonNode value) {
return value instanceof ArrayNode;
}
@Override
protected boolean isObject(JsonNode value) {
return value instanceof ObjectNode;
}
@Override
protected boolean isNull(JsonNode value) {
return value == null || value instanceof NullNode;
}
@Override
protected ValueNode asValue(JsonNode value) {
return (ValueNode) value;
}
@Override
protected ArrayNode asArray(JsonNode value) {
return (ArrayNode) value;
}
@Override
protected ObjectNode asObject(JsonNode value) {
return (ObjectNode) value;
}
@Override
protected boolean equals(ValueNode value1, ValueNode value2) {
if (value1.isNumber() && value2.isNumber()) {
return value1.asText().equals(value2.asText());
} else {
return value1.equals(value2);
}
}
@Override
protected Iterator<Map.Entry<String, JsonNode>> getFields(ObjectNode node) {
return node.fields();
}
@Override
protected boolean hasField(ObjectNode value, String field) {
return value.has(field);
}
@Override
protected JsonNode getField(ObjectNode value, String field) {
return value.get(field);
}
@Override
protected IdentityExtractor getArrayIdentityExtractorImpl(ArrayIdentityFields fields) {
return new DefaultIdentityExtractor(fields);
}
@Override
protected JsonNode getElement(ArrayNode value, int index) {
return value.get(index);
}
@Override
protected int size(ArrayNode value) {
return value.size();
}
}