/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kie.test.util.compare;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class does deep, configurable reflection based comparison,
* meant for usage with round-tripped (serialized, deserialized) objects.
*/
public class ComparePair {
private static final Logger logger = LoggerFactory.getLogger(ComparePair.class);
private Object orig;
private Object copy;
private Class<?> objInterface;
private String[] nullFields = null;
private boolean useGetMethods = true;
public static void compareOrig(Object origObj, Object newObj, Class objClass) {
ComparePair compare = new ComparePair(origObj, newObj, objClass);
Queue<ComparePair> compares = new LinkedList<ComparePair>();
compares.add(compare);
while (!compares.isEmpty()) {
compares.addAll(compares.poll().compare());
}
}
public ComparePair(Object a, Object b, Class<?> c) {
this.orig = a;
this.copy = b;
this.objInterface = c;
}
public ComparePair(Object a, Object b) {
this(a, b, a.getClass());
}
public List<ComparePair> compare() {
if (useGetMethods) {
return compareObjectsViaGetMethods(orig, copy, objInterface);
} else {
compareObjectsViaFields(orig, copy, nullFields);
return null;
}
}
public ComparePair useFields() {
this.useGetMethods = false;
return this;
}
public void recursiveCompare() {
Queue<ComparePair> compares = new LinkedList<ComparePair>();
compares.add(this);
while (!compares.isEmpty()) {
compares.addAll(compares.poll().compare());
}
}
public ComparePair addNullFields(String... fieldNames) {
if (fieldNames == null) {
return this;
}
if (nullFields == null) {
nullFields = new String[0];
}
String[] newNullFields = new String[nullFields.length + fieldNames.length];
for (int i = 0; i < nullFields.length; ++i) {
newNullFields[i] = nullFields[i];
}
for (int i = 0; i < fieldNames.length; ++i) {
newNullFields[i + nullFields.length] = fieldNames[i];
}
this.nullFields = newNullFields;
return this;
}
private List<ComparePair> compareObjectsViaGetMethods(Object orig, Object copy, Class<?> objInterface) {
List<ComparePair> cantCompare = new ArrayList<ComparePair>();
for (Method getIsMethod : objInterface.getDeclaredMethods()) {
String methodName = getIsMethod.getName();
String fieldName;
if (methodName.startsWith("get")) {
fieldName = methodName.substring(3);
} else if (methodName.startsWith("is")) {
fieldName = methodName.substring(2);
} else {
continue;
}
// getField -> field (lowercase f)
fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
try {
Object origField = getIsMethod.invoke(orig, new Object[0]);
Object copyField = getIsMethod.invoke(copy, new Object[0]);
boolean skip = false;
if (origField == null) {
if (nullFields != null) {
for (String nullField : nullFields) {
if (fieldName.equals(nullField)) {
skip = true;
break;
}
}
}
if (!skip) {
fail("Please fill in the " + fieldName + " field in the " + objInterface.getSimpleName() + "!");
}
}
if (skip) {
continue;
}
if (origField != null && !(origField instanceof Enum) && origField.getClass().getPackage().getName().startsWith("org.")) {
ComparePair newComPair = new ComparePair(origField, copyField, getInterface(origField));
newComPair.addNullFields(this.nullFields);
cantCompare.add(newComPair);
continue;
} else if (origField instanceof List<?>) {
List<?> origList = (List) origField;
List<?> copyList = (List) copyField;
for (int i = 0; i < origList.size(); ++i) {
Class<?> newInterface = origField.getClass();
while (newInterface.getInterfaces().length > 0) {
newInterface = newInterface.getInterfaces()[0];
}
ComparePair newCompair = new ComparePair(origList.get(i), copyList.get(i), getInterface(origList.get(i)));
newCompair.addNullFields(this.nullFields);
cantCompare.add(newCompair);
}
continue;
}
assertEquals(fieldName, origField, copyField);
} catch (Exception e) {
throw new RuntimeException("Unable to compare " + fieldName, e);
}
}
return cantCompare;
}
private Class<?> getInterface(Object obj) {
Class<?> newInterface = obj.getClass();
Class<?> parent = newInterface;
while (parent != null) {
parent = null;
if (newInterface.getInterfaces().length > 0) {
Class<?> newParent = newInterface.getInterfaces()[0];
if (newParent.getPackage().getName().startsWith("org.")) {
parent = newInterface = newParent;
}
}
}
return newInterface;
}
public static void compareObjectsViaFields(Object orig, Object copy) {
String name = orig == null ? "null" : orig.getClass().getSimpleName();
compareValues(orig, copy, name, new String[] {});
}
public static void compareObjectsViaFields(Object orig, Object copy, String... skipFields) {
if( orig == copy ) {
return;
} else if( copy == null ) {
fail( "Copy " + orig.getClass().getSimpleName() + " is null!" );
} else if( orig == null ) {
fail( "Original " + copy.getClass().getSimpleName() + " is null!" );
}
Class<?> origClass = orig.getClass();
String origClassName = origClass.getSimpleName();
Class<?> copyClass = copy.getClass();
assertEquals("copy is an instance of " + copyClass.getSimpleName() + " ( instead of " + origClassName + ")",
origClass, copy.getClass());
for (Field field : orig.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
Object origFieldVal = field.get(orig);
Object copyFieldVal = field.get(copy);
String fieldName = field.getName();
boolean skip = false;
for (String skipFieldName : skipFields) {
if (skipFieldName.matches(fieldName)) {
skip = true;
break;
}
}
if (skip) {
continue;
}
boolean nullFound = false;
if (origFieldVal == null || copyFieldVal == null) {
nullFound = true;
for (String nullFieldName : skipFields) {
String actualNullFieldName = nullFieldName;
if( nullFieldName.contains(".") ) {
int dotIndex = nullFieldName.indexOf(".");
if( nullFieldName.substring(0, dotIndex).equals(origClassName) ) {
actualNullFieldName = nullFieldName.substring(dotIndex+1);
} else {
continue;
}
}
if (nullFound && actualNullFieldName.matches(fieldName)) {
nullFound = false;
}
}
}
String objectFieldName = origClass.getSimpleName() + "." + field.getName();
assertFalse(objectFieldName + "!", nullFound);
if (copyFieldVal != origFieldVal ) {
compareValues(origFieldVal, copyFieldVal, objectFieldName, skipFields);
}
} catch (Exception e) {
throw new RuntimeException("Unable to access " + field.getName() + " when testing " + origClass.getSimpleName()
+ ".", e);
}
}
}
private static void compareValues(Object origFieldVal, Object copyFieldVal, String objectFieldName, String [] skipFields ) {
assertNotNull(objectFieldName + " is null in the copy!", copyFieldVal);
assertNotNull(objectFieldName + " is null in the original!", origFieldVal);
Package pkg = origFieldVal.getClass().getPackage();
if (pkg == null || pkg.getName().startsWith("java.")) {
if( origFieldVal.getClass().isArray() ) {
if( origFieldVal instanceof byte[] ) {
assertArrayEquals(objectFieldName, (byte []) origFieldVal, (byte []) copyFieldVal);
}
} else if( origFieldVal instanceof Map<?, ?> && copyFieldVal instanceof Map<?, ?> ) {
Collection shouldBeEmpty = CollectionUtils.disjunction(
((Map) origFieldVal).values(),
((Map) copyFieldVal).values());
assertTrue( "Comparison of Map values failed on " + objectFieldName, shouldBeEmpty.isEmpty() );
} else if( origFieldVal instanceof Collection ) {
assertEquals( "Different collection sizes on "+ objectFieldName,
((Collection) origFieldVal).size(), ((Collection) copyFieldVal).size());
for( Object elem : ((Collection) origFieldVal) ) {
boolean match = false;
for( Object copyElem : ((Collection) copyFieldVal) ) {
try {
compareObjectsViaFields(elem, copyElem, skipFields);
match = true;
break;
} catch( Throwable t ) {
logger.debug(t.getMessage());
// ignore
}
}
assertTrue( "Different collection values on " + objectFieldName, match);
}
} else {
assertEquals(objectFieldName, origFieldVal, copyFieldVal);
}
}
}
public interface FieldComparator<T> {
public void compare(T orig, T copy);
}
}