Skip to content

Commit

Permalink
fixed nested java objects in JSON.stringify
Browse files Browse the repository at this point in the history
Incorporated some changes from PR mozilla#824 for checking circular references.

Changed default behavior of java objects that are not treated as
containers to return the toString value by default rather than throwing
a TypeError.

Co-authored-by: Roland Praml <roland.praml@foconis.de>
  • Loading branch information
tonygermano and rPraml committed Apr 24, 2021
1 parent f6252c2 commit 92e5f87
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 45 deletions.
141 changes: 104 additions & 37 deletions src/org/mozilla/javascript/NativeJSON.java
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private static class StringifyState {
this.propertyList = propertyList;
}

Stack<Scriptable> stack = new Stack<Scriptable>();
Stack<Object> stack = new Stack<Object>();
String indent;
String gap;
Callable replacer;
Expand Down Expand Up @@ -270,6 +270,8 @@ public static Object stringify(

private static Object str(Object key, Scriptable holder, StringifyState state) {
Object value = null;
Object wrappedJavaValue = null;

if (key instanceof String) {
value = getProperty(holder, (String) key);
} else {
Expand All @@ -294,25 +296,10 @@ private static Object str(Object key, Scriptable holder, StringifyState state) {
} else if (value instanceof NativeBoolean) {
value = ((NativeBoolean) value).getDefaultValue(ScriptRuntime.BooleanClass);
} else if (value instanceof NativeJavaObject) {
value = ((NativeJavaObject) value).unwrap();
if (value instanceof Map) {
Map<?, ?> map = (Map<?, ?>) value;
NativeObject nObj = new NativeObject();
map.forEach(
(k, v) -> {
if (k instanceof CharSequence) {
nObj.put(((CharSequence) k).toString(), nObj, v);
}
});
value = nObj;
} else {
if (value instanceof Collection<?>) {
Collection<?> col = (Collection<?>) value;
value = col.toArray(new Object[col.size()]);
}
if (value instanceof Object[]) {
value = new NativeArray((Object[]) value);
}
wrappedJavaValue = value;
final Object unwrappedJavaValue = ((NativeJavaObject) value).unwrap();
if (!isComplexJavaObject(unwrappedJavaValue)) {
value = unwrappedJavaValue;
}
} else if (value instanceof XMLObject) {
value = ((XMLObject) value).toString();
Expand All @@ -336,18 +323,20 @@ private static Object str(Object key, Scriptable holder, StringifyState state) {
return "null";
}

if (value instanceof Scriptable) {
if (!(value instanceof Callable)) {
if (value instanceof NativeArray) {
return ja((NativeArray) value, state);
}
Object unwrappedJavaValue = null;
if ((wrappedJavaValue != null) && (wrappedJavaValue != value)) {
unwrappedJavaValue = value;
value = wrappedJavaValue;
}
if ((value instanceof Scriptable) && !(value instanceof Callable)) {
if (isObjectArrayLike(value)) {
return ja((Scriptable) value, state);
} else if (unwrappedJavaValue == null) {
return jo((Scriptable) value, state);
} else {
return javaToJSON(unwrappedJavaValue, state);
}
} else if (!Undefined.isUndefined(value)) {
throw ScriptRuntime.typeErrorById(
"msg.json.cant.serialize", value.getClass().getName());
}

return Undefined.instance;
}

Expand All @@ -365,10 +354,54 @@ private static String join(Collection<Object> objs, String delimiter) {
}

private static String jo(Scriptable value, StringifyState state) {
if (state.stack.search(value) != -1) {
throw ScriptRuntime.typeErrorById("msg.cyclic.value");
Object trackValue = value;
final boolean isTrackValueUnwrapped;
if (value instanceof Wrapper) {
trackValue = ((Wrapper) value).unwrap();
isTrackValueUnwrapped = true;
} else {
isTrackValueUnwrapped = false;
}

if (state.stack.search(trackValue) != -1) {
throw ScriptRuntime.typeErrorById("msg.cyclic.value", trackValue.getClass().getName());
}
state.stack.push(trackValue);

if (isTrackValueUnwrapped && (trackValue instanceof Map)) {
Map<?, ?> map = (Map<?, ?>) trackValue;
Scriptable nObj = state.cx.newObject(state.scope);
map.entrySet().stream()
.filter(e -> (!(e.getKey() instanceof Symbol)))
.forEach(
e -> {
Object wrappedValue = Context.javaToJS(e.getValue(), state.scope);
int attributes;
String key;
if (e.getKey() instanceof String) {
// Keys that are actually Strings are permanent and will not be
// overridden
// by other objects having the same toString value.
key = (String) e.getKey();
attributes =
ScriptableObject.READONLY | ScriptableObject.PERMANENT;
} else {
// To avoid duplicate keys in JSON, replace previous key having
// the same
// toString value as the current object.
key = e.getKey().toString();
attributes = ScriptableObject.EMPTY;
}
try {
ScriptableObject.defineProperty(
nObj, key, wrappedValue, attributes);
} catch (EcmaError error) {
// ignore TypeErrors if we tried to rewrite the property for a
// String key.
}
});
value = nObj;
}
state.stack.push(value);

String stepback = state.indent;
state.indent = state.indent + state.gap;
Expand Down Expand Up @@ -412,17 +445,32 @@ private static String jo(Scriptable value, StringifyState state) {
return finalValue;
}

private static String ja(NativeArray value, StringifyState state) {
if (state.stack.search(value) != -1) {
throw ScriptRuntime.typeErrorById("msg.cyclic.value");
private static String ja(Scriptable value, StringifyState state) {
Object trackValue = value;
if (value instanceof Wrapper) {
trackValue = ((Wrapper) value).unwrap();
}
state.stack.push(value);
if (state.stack.search(trackValue) != -1) {
throw ScriptRuntime.typeErrorById("msg.cyclic.value", trackValue.getClass().getName());
}
state.stack.push(trackValue);

String stepback = state.indent;
state.indent = state.indent + state.gap;
List<Object> partial = new LinkedList<Object>();

long len = value.getLength();
if (trackValue instanceof Collection) {
Collection<?> col = (Collection<?>) trackValue;
trackValue = col.toArray(new Object[col.size()]);
}
if (trackValue instanceof Object[]) {
Object[] elements = (Object[]) trackValue;
elements = Arrays.stream(elements).map(o -> Context.javaToJS(o, state.scope)).toArray();
value = state.cx.newArray(state.scope, elements);
}

long len = ((NativeArray) value).getLength();

for (long index = 0; index < len; index++) {
Object strP;
if (index > Integer.MAX_VALUE) {
Expand Down Expand Up @@ -500,6 +548,25 @@ private static String quote(String string) {
return product.toString();
}

private static Object javaToJSON(Object value, StringifyState state) {
return quote(value.toString());
}

private static boolean isComplexJavaObject(Object o) {
return (o instanceof Map) || (o instanceof Collection) || (o instanceof Object[]);
}

private static boolean isObjectArrayLike(Object o) {
if (o instanceof NativeArray) {
return true;
}
if (o instanceof NativeJavaObject) {
o = ((NativeJavaObject) o).unwrap();
return (o instanceof Collection) || (o instanceof Object[]);
}
return false;
}

// #string_id_map#

@Override
Expand Down
51 changes: 43 additions & 8 deletions testsrc/jstests/stringify-java-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,19 @@ var actual = JSON.stringify(obj);
assertEquals(expected, actual);

// java Map
var javaMap = new java.util.HashMap();
javaMap.put(new java.lang.Object(), 'property skipped if key is not string-like');
var objectKey = new JavaAdapter(java.lang.Object, {toString:()=>"object key"});
var javaMap = new java.util.LinkedHashMap();
javaMap.put(Symbol(), 'property skipped for Symbol keys');
javaMap.put(objectKey, 'object value');
javaMap.put('te' + 'st', 55);

var obj = {test: javaMap};
var expected = JSON.stringify({test: 'replaced: java.util.HashMap'});
var expected = JSON.stringify({test: 'replaced: java.util.LinkedHashMap'});
var actual = JSON.stringify(obj, replacer);
assertEquals(expected, actual);

var obj = javaMap;
var expected = JSON.stringify({test: 55});
var expected = JSON.stringify({"object key": "object value", test: 55});
var actual = JSON.stringify(obj);
assertEquals(expected, actual);

Expand All @@ -103,7 +105,7 @@ var expected = JSON.stringify({
objects: {
plainJS: {test: 1},
emptyMap: {},
otherMap: {test: 55}
otherMap: {"object key": "object value", test: 55}
}
});
var actual = JSON.stringify(obj);
Expand All @@ -118,11 +120,14 @@ var actual = JSON.stringify(obj, replacer);
assertEquals(expected, actual);

var obj = {test: javaObject};
assertThrows(()=>JSON.stringify(obj), TypeError);
var expected = JSON.stringify({test: 'test://other/java/object'});
var actual = JSON.stringify(obj);
assertEquals(expected, actual);

// JavaAdapter with toJSON
var javaObject = new JavaAdapter(java.lang.Object, {
toJSON: _ => ({javaAdapter: true})
toJSON: _ => ({javaAdapter: true}),
toString: () => 'just an object'
});

var obj = javaObject;
Expand All @@ -142,6 +147,36 @@ assertEquals("string", typeof actual);
assertTrue(expected.test(actual));

var obj = {test: javaObject};
assertThrows(()=>JSON.stringify(obj), TypeError);
var expected = JSON.stringify({test: "just an object"});
var actual = JSON.stringify(obj)
assertEquals(expected, actual);

// nested Maps and Lists
var map1 = new java.util.HashMap({a:1});
var map2 = new java.util.HashMap({b:2, map1: map1});

var list1 = new java.util.ArrayList([1]);
var list2 = new java.util.ArrayList([2, list1]);

var expected = JSON.stringify({
b: 2,
map1: {a: 1}
});
var actual = JSON.stringify(map2);
assertEquals(expected, actual);

var expected = JSON.stringify([2, [1]]);
var actual = JSON.stringify(list2);
assertEquals(expected, actual);

list2.add(map1);
var expected = JSON.stringify([2, [1], {a:1}]);
var actual = JSON.stringify(list2);
assertEquals(expected, actual);

// make circular reference
list1.add(map2);
map1.put('list2', list2);
assertThrows(()=>JSON.stringify(map1), TypeError);

"success"

0 comments on commit 92e5f87

Please sign in to comment.