diff --git a/src/main/java/io/neonbee/internal/helper/CollectionHelper.java b/src/main/java/io/neonbee/internal/helper/CollectionHelper.java index 37cd48cdc..eb09784f1 100644 --- a/src/main/java/io/neonbee/internal/helper/CollectionHelper.java +++ b/src/main/java/io/neonbee/internal/helper/CollectionHelper.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -21,6 +22,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.shareddata.Shareable; +@SuppressWarnings("PMD.GodClass") public final class CollectionHelper { /** @@ -51,12 +53,21 @@ private CollectionHelper() {} * @return a mutable deep copy of the given list */ public static List mutableCopyOf(List list) { + return mutableCopyOf(list, new IdentityHashMap<>()); + } + + @SuppressWarnings("unchecked") + private static List mutableCopyOf(List list, IdentityHashMap visited) { if (list == null) { return new ArrayList<>(); } + if (visited.containsKey(list)) { + return (List) visited.get(list); + } List copy = new ArrayList<>(list.size()); + visited.put(list, copy); for (T item : list) { - copy.add(copyOf(item)); + copy.add(copyOf(item, visited)); } return copy; } @@ -69,15 +80,24 @@ public static List mutableCopyOf(List list) { * @return a mutable deep copy of the given set */ public static Set mutableCopyOf(Set set) { + return mutableCopyOf(set, new IdentityHashMap<>()); + } + + @SuppressWarnings("unchecked") + private static Set mutableCopyOf(Set set, IdentityHashMap visited) { if (set == null) { return new HashSet<>(); } + if (visited.containsKey(set)) { + return (Set) visited.get(set); + } int initialCapacity = Math.max((int) (set.size() / LOAD_FACTOR) + 1, DEFAULT_CAPACITY); Set copy = new HashSet<>(initialCapacity); + visited.put(set, copy); for (T item : set) { - copy.add(copyOf(item)); + copy.add(copyOf(item, visited)); } return copy; } @@ -93,12 +113,22 @@ public static Set mutableCopyOf(Set set) { * @return a mutable deep copy of the given collection */ public static > C mutableCopyOf(C collection, Supplier collectionFactory) { + return mutableCopyOf(collection, collectionFactory, new IdentityHashMap<>()); + } + + @SuppressWarnings("unchecked") + private static > C mutableCopyOf(C collection, Supplier collectionFactory, + IdentityHashMap visited) { C copy = collectionFactory.get(); if (collection == null) { return copy; } + if (visited.containsKey(collection)) { + return (C) visited.get(collection); + } + visited.put(collection, copy); for (T item : collection) { - copy.add(copyOf(item)); + copy.add(copyOf(item, visited)); } return copy; } @@ -112,13 +142,22 @@ public static > C mutableCopyOf(C collection, Supplie * @return a mutable deep copy of the given map */ public static Map mutableCopyOf(Map map) { + return mutableCopyOf(map, new IdentityHashMap<>()); + } + + @SuppressWarnings("unchecked") + private static Map mutableCopyOf(Map map, IdentityHashMap visited) { Map copy = new NullLiberalMergingHashMap<>(); if (map == null) { return copy; } + if (visited.containsKey(map)) { + return (Map) visited.get(map); + } + visited.put(map, copy); for (Map.Entry entry : map.entrySet()) { - K keyCopy = copyOf(entry.getKey()); - V valueCopy = copyOf(entry.getValue()); + K keyCopy = copyOf(entry.getKey(), visited); + V valueCopy = copyOf(entry.getValue(), visited); copy.put(keyCopy, valueCopy); } return copy; @@ -141,41 +180,57 @@ public static Map mutableCopyOf(Map map) { * @param object the object to copy * @return either a new mutable copy of the object, or the object itself */ - @SuppressWarnings("unchecked") public static T copyOf(T object) { + return copyOf(object, new IdentityHashMap<>()); + } + + @SuppressWarnings("unchecked") + private static T copyOf(T object, IdentityHashMap visited) { if (Objects.isNull(object)) { return null; - } else if (object instanceof Buffer buffer) { - return (T) buffer.copy(); + } + if (visited.containsKey(object)) { + return (T) visited.get(object); + } + if (object instanceof Buffer buffer) { + Buffer copy = buffer.copy(); + visited.put(object, copy); + return (T) copy; } else if (object instanceof List list) { - return (T) mutableCopyOf(list); + return (T) mutableCopyOf(list, visited); } else if (object instanceof Set set) { - return (T) mutableCopyOf(set); + return (T) mutableCopyOf(set, visited); } else if (object instanceof Map map) { - return (T) mutableCopyOf(map); + return (T) mutableCopyOf(map, visited); } else if (object.getClass().isArray()) { + Object arrayCopy; if (object instanceof byte[] byteArr) { - return (T) Arrays.copyOf(byteArr, byteArr.length); + arrayCopy = Arrays.copyOf(byteArr, byteArr.length); } else if (object instanceof short[] shortArr) { - return (T) Arrays.copyOf(shortArr, shortArr.length); + arrayCopy = Arrays.copyOf(shortArr, shortArr.length); } else if (object instanceof int[] intArr) { - return (T) Arrays.copyOf(intArr, intArr.length); + arrayCopy = Arrays.copyOf(intArr, intArr.length); } else if (object instanceof char[] charArr) { - return (T) Arrays.copyOf(charArr, charArr.length); + arrayCopy = Arrays.copyOf(charArr, charArr.length); } else if (object instanceof float[] floatArr) { - return (T) Arrays.copyOf(floatArr, floatArr.length); + arrayCopy = Arrays.copyOf(floatArr, floatArr.length); } else if (object instanceof double[] doubleArr) { - return (T) Arrays.copyOf(doubleArr, doubleArr.length); + arrayCopy = Arrays.copyOf(doubleArr, doubleArr.length); } else if (object instanceof boolean[] booleanArr) { - return (T) Arrays.copyOf(booleanArr, booleanArr.length); + arrayCopy = Arrays.copyOf(booleanArr, booleanArr.length); } else { Class componentType = object.getClass().getComponentType(); Object[] array = (Object[]) Array.newInstance(componentType, ((Object[]) object).length); - Arrays.setAll(array, index -> copyOf(((Object[]) object)[index])); + visited.put(object, array); + Arrays.setAll(array, index -> copyOf(((Object[]) object)[index], visited)); return (T) array; } + visited.put(object, arrayCopy); + return (T) arrayCopy; } else if (object instanceof Shareable) { - return (T) ((Shareable) object).copy(); + Object copy = ((Shareable) object).copy(); + visited.put(object, copy); + return (T) copy; } else { return object; } @@ -190,14 +245,17 @@ public static T copyOf(T object) { * @param map the map to copy * @return a new case-insensitive treemap as mutable deep copy of the given map */ + @SuppressWarnings("unchecked") public static Map mapToCaseInsensitiveTreeMap(Map map) { Map copy = new NullLiberalMergingTreeMap<>(String.CASE_INSENSITIVE_ORDER); if (map == null) { return copy; } + IdentityHashMap visited = new IdentityHashMap<>(); + visited.put(map, copy); for (Map.Entry entry : map.entrySet()) { - K keyCopy = copyOf(entry.getKey()); - V valueCopy = copyOf(entry.getValue()); + K keyCopy = copyOf(entry.getKey(), visited); + V valueCopy = copyOf(entry.getValue(), visited); copy.put(keyCopy, valueCopy); } return copy; diff --git a/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java b/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java index c0dd1fdfb..9564d8176 100644 --- a/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java +++ b/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java @@ -16,8 +16,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import com.sap.cds.reflect.CdsModel; - import io.neonbee.NeonBeeOptions; import io.neonbee.test.base.NeonBeeTestBase; import io.vertx.core.Future; @@ -120,13 +118,16 @@ void loadFromModuleTest(Vertx vertx, VertxTestContext testContext) throws IOExce @DisplayName("check if getting CSN Model works") void getCSNModelTest(Vertx vertx, VertxTestContext testContext) { EntityModelLoader loader = new EntityModelLoader(vertx); - Future csnModelFuture = loader.readCsnModel(TEST_SERVICE_1_MODEL_PATH); - csnModelFuture.compose(v -> loader.loadModel(TEST_SERVICE_1_MODEL_PATH)) - .onComplete(testContext.succeeding(result -> testContext.verify(() -> { - CdsModel expectedCsnModel = csnModelFuture.result(); - EntityModel model = loader.models.get("io.neonbee.test1"); - assertThat(model.getCsnModel()).isEqualTo(expectedCsnModel); - testContext.completeNow(); + loader.readCsnModel(TEST_SERVICE_1_MODEL_PATH) + .onComplete(testContext.succeeding(csnModel -> testContext.verify(() -> { + loader.loadModel(TEST_SERVICE_1_MODEL_PATH) + .onComplete(testContext.succeeding(result -> testContext.verify(() -> { + EntityModel model = loader.models.get("io.neonbee.test1"); + assertThat(model.getCsnModel()).isNotNull(); + assertThat(model.getCsnModel().services().findFirst().get().getQualifiedName()) + .isEqualTo(csnModel.services().findFirst().get().getQualifiedName()); + testContext.completeNow(); + }))); }))); } diff --git a/src/test/java/io/neonbee/internal/helper/CollectionHelperTest.java b/src/test/java/io/neonbee/internal/helper/CollectionHelperTest.java index 52f5031f2..9596110fd 100644 --- a/src/test/java/io/neonbee/internal/helper/CollectionHelperTest.java +++ b/src/test/java/io/neonbee/internal/helper/CollectionHelperTest.java @@ -233,6 +233,71 @@ void testIsNullOrEmptyMap() { assertThat(CollectionHelper.isNullOrEmpty(Map.of("k", "v"))).isFalse(); } + @Test + @DisplayName("copyOf handles self-referencing map without StackOverflowError") + void testCopyOfSelfReferencingMap() { + Map map = new HashMap<>(); + map.put("key", "value"); + map.put("self", map); + + Map copy = CollectionHelper.copyOf(map); + + assertThat(copy).isNotSameInstanceAs(map); + assertThat(copy.get("key")).isEqualTo("value"); + assertThat(copy.get("self")).isSameInstanceAs(copy); + } + + @Test + @DisplayName("copyOf handles self-referencing list without StackOverflowError") + @SuppressWarnings("unchecked") + void testCopyOfSelfReferencingList() { + List list = new ArrayList<>(); + list.add("item"); + list.add(list); + + List copy = CollectionHelper.copyOf(list); + + assertThat(copy).isNotSameInstanceAs(list); + assertThat(copy.get(0)).isEqualTo("item"); + assertThat(copy.get(1)).isSameInstanceAs(copy); + } + + @Test + @DisplayName("copyOf handles set with circular reference without StackOverflowError") + @SuppressWarnings("unchecked") + void testCopyOfSetWithCircularReference() { + Map map = new HashMap<>(); + Set set = new HashSet<>(); + set.add("item"); + set.add(map); + map.put("set", set); + + Set copy = CollectionHelper.copyOf(set); + + assertThat(copy).isNotSameInstanceAs(set); + assertThat(copy).contains("item"); + Map innerMap = (Map) copy.stream() + .filter(e -> e instanceof Map).findFirst().orElseThrow(); + assertThat(innerMap.get("set")).isSameInstanceAs(copy); + } + + @Test + @DisplayName("copyOf handles mutually referencing maps without StackOverflowError") + @SuppressWarnings("unchecked") + void testCopyOfMutuallyReferencingMaps() { + Map mapA = new HashMap<>(); + Map mapB = new HashMap<>(); + mapA.put("ref", mapB); + mapB.put("ref", mapA); + + Map copyA = CollectionHelper.copyOf(mapA); + Map copyB = (Map) copyA.get("ref"); + + assertThat(copyA).isNotSameInstanceAs(mapA); + assertThat(copyB).isNotSameInstanceAs(mapB); + assertThat(copyB.get("ref")).isSameInstanceAs(copyA); + } + private static class TestShareable implements Shareable { final String value;