// Copyright © 2011-2013, Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0
package fi.jumi.core.util;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.hamcrest.*;
import java.lang.reflect.*;
import java.util.List;
public class EqualityMatchers {
public static <T> Matcher<T> deepEqualTo(T expected) {
return new TypeSafeMatcher<T>() {
@Override
protected boolean matchesSafely(T actual) {
return findDifference("this", actual, expected) == null;
}
@Override
public void describeTo(Description description) {
description.appendText("deep equal to ").appendValue(expected);
}
@Override
protected void describeMismatchSafely(T actual, Description mismatchDescription) {
String path = findDifference("this", actual, expected);
mismatchDescription.appendText("was different at ").appendValue(path)
.appendText(" of ").appendValue(actual);
}
};
}
private static String findDifference(String path, Object obj1, Object obj2) {
if (obj1 == null || obj2 == null) {
return obj1 == obj2 ? null : path;
}
if (obj1.getClass() != obj2.getClass()) {
return path;
}
// collections have a custom equals, but we want deep equality on every collection element
// TODO: support other collection types? comparing Sets should be order-independent
if (obj1 instanceof List) {
List<?> col1 = (List<?>) obj1;
List<?> col2 = (List<?>) obj2;
int size1 = col1.size();
int size2 = col2.size();
if (size1 != size2) {
return path + ".size()";
}
for (int i = 0; i < Math.min(size1, size2); i++) {
String diff = findDifference(path + ".get(" + i + ")", col1.get(i), col2.get(i));
if (diff != null) {
return diff;
}
}
return null;
}
// use custom equals method if exists
try {
Method equals = obj1.getClass().getMethod("equals", Object.class);
if (equals.getDeclaringClass() != Object.class) {
return obj1.equals(obj2) ? null : path;
}
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
// arrays
if (obj2.getClass().isArray()) {
int length1 = Array.getLength(obj1);
int length2 = Array.getLength(obj2);
if (length1 != length2) {
return path + ".length";
}
for (int i = 0; i < Math.min(length1, length2); i++) {
String diff = findDifference(path + "[" + i + "]",
Array.get(obj1, i),
Array.get(obj2, i));
if (diff != null) {
return diff;
}
}
return null;
}
// structural equality
for (Class<?> cl = obj2.getClass(); cl != null; cl = cl.getSuperclass()) {
for (Field field : cl.getDeclaredFields()) {
try {
String diff = findDifference(path + "." + field.getName(),
FieldUtils.readField(field, obj1, true),
FieldUtils.readField(field, obj2, true));
if (diff != null) {
return diff;
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return null;
}
}