From 2edd52399a3bb76bd8b680eb4e0b49822e932f9a Mon Sep 17 00:00:00 2001 From: Peter Thomas Date: Sat, 10 Oct 2020 22:21:45 +0530 Subject: [PATCH] [rewrite] #1281 wip make deep copy safer based om the learnings from the past --- .../com/intuit/karate/data/JsonUtils.java | 136 ++++++++++++++++++ .../intuit/karate/runtime/ScenarioEngine.java | 2 +- .../com/intuit/karate/runtime/Variable.java | 15 +- .../com/intuit/karate/data/JsonUtilsTest.java | 14 ++ 4 files changed, 157 insertions(+), 10 deletions(-) diff --git a/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java b/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java index cb54e648b..057e9d7d7 100644 --- a/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java +++ b/karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java @@ -40,7 +40,11 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -175,4 +179,136 @@ public static String toCsv(List> list) { return sw.toString(); } + public static Object deepCopy(Object o) { + // anti recursion / back-references + Set seen = Collections.newSetFromMap(new IdentityHashMap()); + return recurseDeepCopy(o, seen); + } + + private static Object recurseDeepCopy(Object o, Set seen) { + if (o instanceof List) { + List list = (List) o; + if (seen.add(o)) { + int count = list.size(); + List listCopy = new ArrayList(count); + for (int i = 0; i < count; i++) { + listCopy.add(recurseDeepCopy(list.get(i), seen)); + } + return listCopy; + } else { + return o; + } + } else if (o instanceof Map) { + if (seen.add(o)) { + Map map = (Map) o; + Map mapCopy = new LinkedHashMap(map.size()); + map.forEach((k, v) -> { + mapCopy.put(k, recurseDeepCopy(v, seen)); + }); + return mapCopy; + } else { + return o; + } + } else { + return o; + } + } + + public static String toJsonSafe(Object o, boolean pretty) { + StringBuilder sb = new StringBuilder(); + // anti recursion / back-references + Set seen = Collections.newSetFromMap(new IdentityHashMap()); + recurseJsonString(o, pretty, sb, 0, seen); + if (pretty) { + sb.append('\n'); + } + return sb.toString(); + } + + private static void pad(StringBuilder sb, int depth) { + for (int i = 0; i < depth; i++) { + sb.append(' ').append(' '); + } + } + + private static void ref(StringBuilder sb, Object o) { + sb.append("\"#ref:").append(o.getClass().getName()).append('"'); + } + + public static String escapeValue(String raw) { + return JSONValue.escape(raw, JSONStyle.LT_COMPRESS); + } + + private static void recurseJsonString(Object o, boolean pretty, StringBuilder sb, int depth, Set seen) { + if (o == null) { + sb.append("null"); + } else if (o instanceof Map) { + if (seen.add(o)) { + sb.append('{'); + if (pretty) { + sb.append('\n'); + } + Map map = (Map) o; + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + String key = entry.getKey(); + if (pretty) { + pad(sb, depth + 1); + } + sb.append('"').append(escapeValue(key)).append('"').append(':'); + if (pretty) { + sb.append(' '); + } + recurseJsonString(entry.getValue(), pretty, sb, depth + 1, seen); + if (iterator.hasNext()) { + sb.append(','); + } + if (pretty) { + sb.append('\n'); + } + } + if (pretty) { + pad(sb, depth); + } + sb.append('}'); + } else { + ref(sb, o); + } + } else if (o instanceof List) { + List list = (List) o; + Iterator iterator = list.iterator(); + if (seen.add(o)) { + sb.append('['); + if (pretty) { + sb.append('\n'); + } + while (iterator.hasNext()) { + Object child = iterator.next(); + if (pretty) { + pad(sb, depth + 1); + } + recurseJsonString(child, pretty, sb, depth + 1, seen); + if (iterator.hasNext()) { + sb.append(','); + } + if (pretty) { + sb.append('\n'); + } + } + if (pretty) { + pad(sb, depth); + } + sb.append(']'); + } else { + ref(sb, o); + } + } else if (o instanceof String) { + String value = (String) o; + sb.append('"').append(escapeValue(value)).append('"'); + } else { + sb.append(o); + } + } + } diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java index 0d97758c3..232716ec6 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/ScenarioEngine.java @@ -78,7 +78,7 @@ private void putJsBinding(String name, Variable v) { switch (v.type) { case JS_FUNCTION: JsValue jv = v.getValue(); - // important to ensure that the function is attached to the current context + // important to ensure that the function is attached to the current graal context // since it may have come from e.g. karate-config.js or a calling / parent feature // this will update the wrapper variable if needed as a performance optimization jv.switchContext(JS); diff --git a/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java b/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java index 7b0cd5271..c39e2c3e1 100644 --- a/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java +++ b/karate-core2/src/main/java/com/intuit/karate/runtime/Variable.java @@ -145,7 +145,7 @@ public boolean isOther() { public boolean isFunction() { return type == Type.JS_FUNCTION || type == Type.JAVA_FUNCTION; } - + public boolean isKarateFeature() { return type == Type.KARATE_FEATURE; } @@ -237,6 +237,9 @@ public String getAsString() { public String getAsPrettyString() { switch (type) { + case LIST: + case MAP: + return JsonUtils.toJsonSafe(value, true); default: return getAsString(); } @@ -253,15 +256,9 @@ public int getAsInt() { public Variable copy(boolean deep) { switch (type) { case LIST: + return deep ? new Variable(JsonUtils.deepCopy(value)) : new Variable(new ArrayList((List) value)); case MAP: - if (deep) { - try { - return new Variable(JsonUtils.fromJsonString(getAsString())); - } catch (Throwable t) { - logger.warn("json deep clone failed, will fall-back to shallow: {}", t.getMessage()); - } - } - return isMap() ? new Variable(new LinkedHashMap((Map) value)) : new Variable(new ArrayList((List) value)); + return deep ? new Variable(JsonUtils.deepCopy(value)) : new Variable(new LinkedHashMap((Map) value)); case XML: return new Variable(XmlUtils.toXmlDoc(getAsString())); default: diff --git a/karate-core2/src/test/java/com/intuit/karate/data/JsonUtilsTest.java b/karate-core2/src/test/java/com/intuit/karate/data/JsonUtilsTest.java index d04197ec3..7bcae53a2 100644 --- a/karate-core2/src/test/java/com/intuit/karate/data/JsonUtilsTest.java +++ b/karate-core2/src/test/java/com/intuit/karate/data/JsonUtilsTest.java @@ -2,6 +2,7 @@ import com.intuit.karate.match.Match; import com.intuit.karate.runtime.SimplePojo; +import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -40,5 +41,18 @@ void testBeanConversion() { Map map = new Json(pojo).asMap(); assertTrue(Match.that(map).isEqualTo("{ foo: null, bar: 0 }").pass); } + + @Test + void testDeepCopy() { + Map one = new HashMap(); + Map two = new HashMap(); + two.put("one", one); + one.put("two", two); + Object temp = JsonUtils.deepCopy(one); + assertEquals(temp, one); + assertFalse(temp == one); + String json = JsonUtils.toJsonSafe(temp, false); + assertEquals("{\"two\":{\"one\":{\"two\":{\"one\":\"#ref:java.util.HashMap\"}}}}", json); + } }