Skip to content

Commit

Permalink
- solved stackoverflow issue
Browse files Browse the repository at this point in the history
  • Loading branch information
jdereg committed Jan 4, 2025
1 parent 61e6d2a commit a8b1ade
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 43 deletions.
87 changes: 49 additions & 38 deletions src/main/java/com/cedarsoftware/util/DeepEquals.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
Expand All @@ -34,11 +33,12 @@ public class DeepEquals {
// Option keys
public static final String IGNORE_CUSTOM_EQUALS = "ignoreCustomEquals";
public static final String ALLOW_STRINGS_TO_MATCH_NUMBERS = "stringsCanMatchNumbers";
private static final String EMPTY = "∅";
private static final String ARROW = "▶";

// Caches for custom equals and hashCode methods
private static final Map<String, Boolean> _customEquals = new ConcurrentHashMap<>();
private static final Map<String, Boolean> _customHash = new ConcurrentHashMap<>();

private static final ThreadLocal<Set<Object>> formattingStack = ThreadLocal.withInitial(() ->
Collections.newSetFromMap(new IdentityHashMap<>()));

// Epsilon values for floating-point comparisons
private static final double doubleEpsilon = 1e-15;

Expand Down Expand Up @@ -116,7 +116,9 @@ public static boolean deepEquals(Object a, Object b) {
// Main deepEquals method with options
public static boolean deepEquals(Object a, Object b, Map<String, ?> options) {
Set<Object> visited = new HashSet<>();
return deepEquals(a, b, options, visited);
boolean result = deepEquals(a, b, options, visited);
formattingStack.remove();
return result;
}

private static boolean deepEquals(Object a, Object b, Map<String, ?> options, Set<Object> visited) {
Expand Down Expand Up @@ -426,7 +428,6 @@ private static boolean decomposeMap(Map<?, ?> map1, Map<?, ?> map2, Deque<ItemsT
.add(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue()));
}

Set<Object> formatVisited = new HashSet<>();
// Process map1 entries
for (Map.Entry<?, ?> entry : map1.entrySet()) {
Collection<Map.Entry<?, ?>> otherEntries = fastLookup.get(deepHashCode(entry.getKey()));
Expand Down Expand Up @@ -828,7 +829,7 @@ private static String generateBreadcrumb(Deque<ItemsToCompare> stack) {
if (diffItem.difference != null) {
result.append("[");
result.append(diffItem.difference.getDescription());
result.append("] ");
result.append("] ");
result.append(pathStr);
result.append("\n");
} else {
Expand Down Expand Up @@ -885,9 +886,9 @@ else if (cur.arrayIndices != null) {
}
}

// If we built child path text, attach it after " @ "
// If we built child path text, attach it after " "
if (sb2.length() > 0) {
sb.append(" @ ");
sb.append(" ");
sb.append(sb2);
}

Expand Down Expand Up @@ -1034,23 +1035,23 @@ private static String formatValueConcise(Object value) {
Collection<?> col = (Collection<?>) value;
String typeName = value.getClass().getSimpleName();
return String.format("%s(%s)", typeName,
col.isEmpty() ? "0..0" : "0.." + (col.size() - 1));
col.isEmpty() ? EMPTY : "0.." + (col.size() - 1));
}

// Handle maps
if (value instanceof Map) {
Map<?, ?> map = (Map<?, ?>) value;
String typeName = value.getClass().getSimpleName();
return String.format("%s[%s]", typeName,
map.isEmpty() ? "0..0" : "0.." + (map.size() - 1));
map.isEmpty() ? EMPTY : "0.." + (map.size() - 1));
}

// Handle arrays
if (value.getClass().isArray()) {
int length = Array.getLength(value);
String typeName = getTypeDescription(value.getClass().getComponentType());
return String.format("%s[%s]", typeName,
length == 0 ? "0..0" : "0.." + (length - 1));
length == 0 ? EMPTY : "0.." + (length - 1));
}

// Handle simple types
Expand Down Expand Up @@ -1089,19 +1090,19 @@ else if (fieldType.isArray()) {
int length = Array.getLength(fieldValue);
String typeName = getTypeDescription(fieldType.getComponentType());
sb.append(String.format("%s[%s]", typeName,
length == 0 ? "0..0" : "0.." + (length - 1)));
length == 0 ? EMPTY : "0.." + (length - 1)));
}
else if (Collection.class.isAssignableFrom(fieldType)) {
// Collection - show type and size
Collection<?> col = (Collection<?>) fieldValue;
sb.append(String.format("%s(%s)", fieldType.getSimpleName(),
col.isEmpty() ? "0..0" : "0.." + (col.size() - 1)));
col.isEmpty() ? EMPTY : "0.." + (col.size() - 1)));
}
else if (Map.class.isAssignableFrom(fieldType)) {
// Map - show type and size
Map<?, ?> map = (Map<?, ?>) fieldValue;
sb.append(String.format("%s[%s]", fieldType.getSimpleName(),
map.isEmpty() ? "0..0" : "0.." + (map.size() - 1)));
map.isEmpty() ? EMPTY : "0.." + (map.size() - 1)));
}
else {
// Non-simple object - show {..}
Expand Down Expand Up @@ -1149,28 +1150,38 @@ private static String formatSimpleValue(Object value) {
private static String formatValue(Object value) {
if (value == null) return "null";

if (value instanceof Number) {
return formatNumber((Number) value);
// Check if we're already formatting this object
Set<Object> stack = formattingStack.get();
if (!stack.add(value)) {
return "<circular " + value.getClass().getSimpleName() + ">";
}

if (value instanceof String) return "\"" + value + "\"";
if (value instanceof Character) return "'" + value + "'";
try {
if (value instanceof Number) {
return formatNumber((Number) value);
}

if (value instanceof Date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date)value);
}
if (value instanceof String) return "\"" + value + "\"";
if (value instanceof Character) return "'" + value + "'";

// If it's a simple type, use toString()
if (Converter.isSimpleTypeConversionSupported(value.getClass(), value.getClass())) {
return String.valueOf(value);
}
if (value instanceof Date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date)value);
}

// For complex objects (not Array, Collection, Map, or simple type)
if (!(value.getClass().isArray() || value instanceof Collection || value instanceof Map)) {
return formatComplexObject(value);
}
// If it's a simple type, use toString()
if (Converter.isSimpleTypeConversionSupported(value.getClass(), value.getClass())) {
return String.valueOf(value);
}

return value.getClass().getSimpleName() + " {" + formatObjectContents(value) + "}";
// For complex objects (not Array, Collection, Map, or simple type)
if (!(value.getClass().isArray() || value instanceof Collection || value instanceof Map)) {
return formatComplexObject(value);
}

return value.getClass().getSimpleName() + " {" + formatObjectContents(value) + "}";
} finally {
stack.remove(value);
}
}

private static String formatObjectContents(Object obj) {
Expand Down Expand Up @@ -1258,7 +1269,7 @@ private static String formatMapContents(Map<?, ?> map) {

private static String formatComplexObject(Object obj) {
if (obj == null) return "null";

StringBuilder sb = new StringBuilder();
sb.append(obj.getClass().getSimpleName());
sb.append(" {");
Expand Down Expand Up @@ -1296,7 +1307,7 @@ private static String formatArrayNotation(Object array) {
int length = Array.getLength(array);
String typeName = getTypeDescription(array.getClass().getComponentType());
return String.format("%s[%s]", typeName,
length == 0 ? "0..0" : "0.." + (length - 1));
length == 0 ? EMPTY : "0.." + (length - 1));
}

private static String formatCollectionNotation(Collection<?> col) {
Expand All @@ -1313,7 +1324,7 @@ private static String formatCollectionNotation(Collection<?> col) {

sb.append("(");
if (col.isEmpty()) {
sb.append("0..0");
sb.append(EMPTY);
} else {
sb.append("0..").append(col.size() - 1);
}
Expand All @@ -1328,13 +1339,13 @@ private static String formatMapNotation(Map<?, ?> map) {
StringBuilder sb = new StringBuilder();
sb.append(map.getClass().getSimpleName());

sb.append("[");
sb.append("(");
if (map.isEmpty()) {
sb.append("0..0");
sb.append(EMPTY);
} else {
sb.append("0..").append(map.size() - 1);
}
sb.append("]");
sb.append(")");

return sb.toString();
}
Expand Down
55 changes: 50 additions & 5 deletions src/test/java/com/cedarsoftware/util/DeepEqualsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -871,13 +871,13 @@ class TestObject {

String diff = (String) options.get("diff");

assert diff.contains("emptyArray: int[0..0]");
assert diff.contains("emptyArray: int[]");
assert diff.contains("multiArray: String[0..2]");
assert diff.contains("nullArray: null");
assert diff.contains("emptyList: List(0..0)");
assert diff.contains("emptyList: List()");
assert diff.contains("multiSet: Set(0..1)");
assert diff.contains("nullCollection: null");
assert diff.contains("emptyMap: Map[0..0]");
assert diff.contains("emptyMap: Map[]");
assert diff.contains("multiMap: Map[0..2]");
assert diff.contains("nullMap: null");
assert diff.contains("emptyAddress: {..}");
Expand Down Expand Up @@ -940,13 +940,25 @@ public void testCollectionDirectCycle() {

@Test
public void testMapKeyCycle() {
Map<Object, String> map1 = new HashMap<>();
Map<Object, String> map1 = new LinkedHashMap<>();
map1.put(map1, "value"); // Cycle in key

Map<Object, String> map2 = new HashMap<>();
Map<Object, String> map2 = new LinkedHashMap<>();
map2.put(map2, "value"); // Cycle in key

assertTrue(DeepEquals.deepEquals(map1, map2));
map1.put(new int[]{4, 5, 6}, "value456");
map2.put(new int[]{4, 5, 7}, "value456");

assertFalse(DeepEquals.deepEquals(map1, map2));
}

@Test
public void testMapDeepHashcodeCycle() {
Map<Object, String> map1 = new HashMap<>();
map1.put(map1, "value"); // Cycle in key

assert DeepEquals.deepHashCode(map1) != 0;
}

@Test
Expand All @@ -958,6 +970,11 @@ public void testMapValueCycle() {
map2.put("key", map2); // Cycle in value

assertTrue(DeepEquals.deepEquals(map1, map2));
map1.put("array", new int[]{4, 5, 6});
map2.put("array", new int[]{4, 5, 7});

assertFalse(DeepEquals.deepEquals(map1, map2));

}

@Test
Expand Down Expand Up @@ -1030,6 +1047,12 @@ class MapHolder {
map2.put(holder2, "value"); // Indirect cycle

assertTrue(DeepEquals.deepEquals(map1, map2));

map1.put(new int[]{4, 5, 6}, "value456");
map2.put(new int[]{4, 5, 7}, "value456");

assertFalse(DeepEquals.deepEquals(map1, map2));

}

@Test
Expand Down Expand Up @@ -1094,7 +1117,29 @@ class CyclicObject {

assertFalse(DeepEquals.deepEquals(obj1, obj2));
}

@Test
void testArrayKey() {
Map<Object, Object> map1 = new HashMap<>();
Map<Object, Object> map2 = new HashMap<>();

map1.put(new int[] {1, 2, 3, 4, 5}, new int[] {9, 3, 7});
map2.put(new int[] {1, 2, 3, 4, 5}, new int[] {9, 2, 7});

assertFalse(DeepEquals.deepEquals(map1, map2));
}

@Test
void test2DArrayKey() {
Map<Object, Object> map1 = new HashMap<>();
Map<Object, Object> map2 = new HashMap<>();

map1.put(new int[][] {new int[]{1, 2, 3}, null, new int[] {}, new int[]{1}}, new int[] {9, 3, 7});
map2.put(new int[][] {new int[]{1, 2, 3}, null, new int[] {}, new int[]{1}}, new int[] {9, 3, 44});

assertFalse(DeepEquals.deepEquals(map1, map2));
}

private static class ComplexObject {
private final String name;
private final Map<String, String> dataMap = new LinkedHashMap<>();
Expand Down

0 comments on commit a8b1ade

Please sign in to comment.