Skip to content

Commit

Permalink
[rewrite] #1281 wip make deep copy safer
Browse files Browse the repository at this point in the history
based om the learnings from the past
  • Loading branch information
ptrthomas committed Oct 10, 2020
1 parent ed50402 commit 2edd523
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 10 deletions.
136 changes: 136 additions & 0 deletions karate-core2/src/main/java/com/intuit/karate/data/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,4 +179,136 @@ public static String toCsv(List<Map<String, Object>> list) {
return sw.toString();
}

public static Object deepCopy(Object o) {
// anti recursion / back-references
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap());
return recurseDeepCopy(o, seen);
}

private static Object recurseDeepCopy(Object o, Set<Object> 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<String, Object> map = (Map<String, Object>) o;
Map<String, Object> 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<Object> 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<Object> seen) {
if (o == null) {
sb.append("null");
} else if (o instanceof Map) {
if (seen.add(o)) {
sb.append('{');
if (pretty) {
sb.append('\n');
}
Map<String, Object> map = (Map<String, Object>) o;
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> 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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -237,6 +237,9 @@ public String getAsString() {

public String getAsPrettyString() {
switch (type) {
case LIST:
case MAP:
return JsonUtils.toJsonSafe(value, true);
default:
return getAsString();
}
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -40,5 +41,18 @@ void testBeanConversion() {
Map<String, Object> map = new Json(pojo).asMap();
assertTrue(Match.that(map).isEqualTo("{ foo: null, bar: 0 }").pass);
}

@Test
void testDeepCopy() {
Map<String, Object> one = new HashMap();
Map<String, Object> 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);
}

}

0 comments on commit 2edd523

Please sign in to comment.