package comparator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.JsonNodeType
import com.fasterxml.jackson.databind.node.NumericNode
import java.util.*
import kotlin.Comparator
enum class CompareType {
/*
* both compare name and value, but if name missing found, compare stop, and print the result, or it will compare all values are the same until find difference.
*/
COMPARE_NAME,
/*
* if name missing found, compare stop and print the result.
*/
COMPARE_NAME_VALUE
}
internal class NumberComparator : Comparator<JsonNode> {
override fun compare(o1: JsonNode, o2: JsonNode): Int {
if (o1 == o2) {
return 0
}
if (o1 is NumericNode && o2 is NumericNode) {
val d1 = o1.asDouble()
val d2 = o2.asDouble()
if (d1.compareTo(d2) == 0) {
return 0
}
}
return 1
}
}
class Comparator {
constructor(){
debug=false;
TYPE_COMPARE = CompareType.COMPARE_NAME_VALUE
IGNORED_FILED_LIST = java.util.ArrayList<String>();
}
constructor(debugI: Boolean?, compareType:String?, ignoreFieldList: String?){
if(debugI != null) {debug=debugI;}
var type = CompareType.COMPARE_NAME_VALUE
if (compareType != null && compareType.trim { it <= ' ' } !== "") {
if (CompareType.COMPARE_NAME_VALUE.toString().equals(compareType, ignoreCase = true)) {
type = CompareType.COMPARE_NAME_VALUE
} else if (CompareType.COMPARE_NAME.toString().equals(compareType, ignoreCase = true)) {
type = CompareType.COMPARE_NAME
}
}
TYPE_COMPARE = type
var ignoredFields: ArrayList<String> = java.util.ArrayList<String>();
if (ignoreFieldList == null || ignoreFieldList.trim { it <= ' ' }.length == 0) {
if(debug){
println("no valid ignored field list")
}
} else {
var fs:kotlin.collections.List<String> = ignoreFieldList.split(",");
for(f:String in fs){
var fTrimed:String = f.trim();
if(fTrimed.length > 0){
ignoredFields.add(fTrimed)
}
}
}
IGNORED_FILED_LIST = ignoredFields;
if(debug){
println("selected compare type:" + TYPE_COMPARE)
}
if(debug){
println("selected ignored field list:" + IGNORED_FILED_LIST)
}
}
fun compare(path: String, o1: JsonNode?, o2: JsonNode?, res: ArrayList<Diff?>) {
// basic input check
if (o1 == null) {
if (o2 != null) {
res.add(Diff(pathGetter(path, ""), MISS_NAME_TIP_WORD, ""))
}
return
}
if (o2 == null) {
res.add(Diff(pathGetter(path, ""), "", MISS_NAME_TIP_WORD))
return
}
val typeO1 = o1.nodeType
val typeO2 = o2.nodeType
if (typeO1 != typeO2) {
val diff = Diff(pathGetter(path, ""), typeO1.name, typeO2.name)
addAndfilterDiffRes(res, diff)
return
}
val type = o1.nodeType
if (type == JsonNodeType.ARRAY) {
val nodeArrLeft: MutableList<JsonNode> = ArrayList()
val iteratorLeft = o1.elements()
while (iteratorLeft.hasNext()) {
nodeArrLeft.add(iteratorLeft.next())
}
val nodeArrRight: MutableList<JsonNode> = ArrayList()
val iteratorRight = o2.elements()
while (iteratorRight.hasNext()) {
nodeArrRight.add(iteratorRight.next())
}
if (nodeArrLeft.size == nodeArrRight.size) {
for (i in 0 until o1.size()) {
compare(pathGetter(path, o1.toString()+"["+i+"]"), nodeArrLeft[i], nodeArrRight[i], res)
}
} else if (nodeArrLeft.size > nodeArrRight.size) {
val lefti = nodeArrLeft[nodeArrRight.size]
val diff = Diff(pathGetter(path, "["+nodeArrRight.size+"]"), lefti.toString(), MISS_NAME_TIP_WORD)
addAndfilterDiffRes(res, diff)
} else {
val righti = nodeArrRight[nodeArrLeft.size]
val diff = Diff(pathGetter(path, "["+nodeArrLeft.size+"]"), MISS_NAME_TIP_WORD, righti.toString())
addAndfilterDiffRes(res, diff)
}
} else if (type == JsonNodeType.OBJECT) {
val o1NameSet = getNamesSet(o1.fieldNames())
val o2NameSet = getNamesSet(o2.fieldNames())
val sameList: MutableList<String> = ArrayList()
// compare node name size
// check key is the same
for (key in o1NameSet) {
if (!o2NameSet.contains(key)) {
val diff = Diff(pathGetter(path, key), o1[key].toString(), MISS_NAME_TIP_WORD)
addAndfilterDiffRes(res, diff)
} else {
sameList.add(key)
}
}
for (key in o2NameSet) {
if (!o1NameSet.contains(key)) {
val diff = Diff(pathGetter(path, key), MISS_NAME_TIP_WORD, o2[key].toString())
addAndfilterDiffRes(res, diff)
}
}
// compare node value
for (key in sameList) {
val nodeleft = o1[key]
val noderight = o2[key]
val typeObjLeft = nodeleft.nodeType
val typeObjRight = noderight.nodeType
if (typeObjLeft != typeObjRight) {
val diff = Diff(pathGetter(path, key), typeObjLeft.name, typeObjRight.name)
addAndfilterDiffRes(res, diff)
} else {
if (typeObjLeft == JsonNodeType.ARRAY) {
val nodeArrLeft: MutableList<JsonNode> = ArrayList()
val iteratorLeft = nodeleft.elements()
while (iteratorLeft.hasNext()) {
nodeArrLeft.add(iteratorLeft.next())
}
val nodeArrRight: MutableList<JsonNode> = ArrayList()
val iteratorRight = noderight.elements()
while (iteratorRight.hasNext()) {
nodeArrRight.add(iteratorRight.next())
}
if (nodeArrLeft.size == nodeArrRight.size) {
for (i in nodeArrLeft.indices) {
compare(pathGetter(path, key+"["+i+"]"), nodeArrLeft[i], nodeArrRight[i], res)
}
} else if (nodeArrLeft.size > nodeArrRight.size) {
val diff = Diff(
pathGetter(path, key+"["+nodeArrRight.size+"]"),
nodeArrLeft[nodeArrRight.size].toString(),
MISS_NAME_TIP_WORD
)
addAndfilterDiffRes(res, diff)
} else {
val diff = Diff(
pathGetter(path, key + "["+nodeArrLeft.size+"]"),
MISS_NAME_TIP_WORD,
nodeArrRight[nodeArrLeft.size].toString()
)
addAndfilterDiffRes(res, diff)
}
} else if (typeObjLeft == JsonNodeType.OBJECT) {
compare(pathGetter(path, key), nodeleft, noderight, res)
} else {
if (TYPE_COMPARE == CompareType.COMPARE_NAME) {
// just compare names
continue
}
if (!nodeleft.equals(NumberComparator(), noderight)) {
val diff = Diff(pathGetter(path, key), nodeleft.toString(), noderight.toString())
addAndfilterDiffRes(res, diff)
}
}
}
}
} else {
if (TYPE_COMPARE == CompareType.COMPARE_NAME) {
// just compare names
return
}
if (!o1.equals(NumberComparator(), o2)) {
val diff = Diff(pathGetter(path, ""), o1.toString(), o2.toString())
addAndfilterDiffRes(res, diff)
}
}
return
}
fun getNamesSet(it: Iterator<String>): TreeSet<String> {
val res = TreeSet<String>()
while (it.hasNext()) {
val name = it.next()
res.add(name)
}
return res
}
fun pathGetter(parent: String, key: String?): String {
val path = StringBuilder()
if (parent == "") {
path.append(key)
} else if (key == null || "" == key.trim { it <= ' ' }) {
path.append(parent)
} else {
path.append(parent).append(".").append(key)
}
return path.toString()
}
fun addAndfilterDiffRes(res: MutableList<Diff?>, diff: Diff) {
if (!IGNORED_FILED_LIST.contains(diff.node)) {
res.add(diff)
}
}
/**
*
* @param compareType <br></br>
* support two compare type set at agrs[0]: COMPARE_NAME_VALUE, COMPARE_NAME <br></br>
*
*
* COMPARE_NAME_VALUE:both compare name and value, but if name missing found, compare stop, and print the result<br></br>
* COMPARE_NAME:if name missing found, compare stop and print the result
*
*/
fun getCompareResult(left: String?, right: String?): ArrayList<Diff?> {
var leftNode: JsonNode? = null
try {
leftNode = mapper.readTree(left)
} catch (e: Exception) {
if(debug){
println("left parse failed:" + e.message)
}
}
var rightNode: JsonNode? = null
try {
rightNode = mapper.readTree(right)
} catch (e: Exception) {
if(debug){
println("right parse failed:" + e.message)
}
}
return getCompareResultWithJson(leftNode, rightNode)
}
fun getCompareResultWithJson(
left: JsonNode?,
right: JsonNode?
): ArrayList<Diff?> {
val res: ArrayList<Diff?> = ArrayList<Diff?>()
try {
compare("\$root", left, right, res)
} catch (e: Exception) {
if(debug){
println("compare failed:" + e.message)
}
}
return res
}
companion object {
// fields to ignore when compare
lateinit var IGNORED_FILED_LIST:ArrayList<String>;
var TYPE_COMPARE = CompareType.COMPARE_NAME_VALUE
var MISS_NAME_TIP_WORD = "not exist"
public val mapper = ObjectMapper()
public var debug:Boolean = false;
init {
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
}
}