Skip to content

Commit

Permalink
Incorporate some changes from PR #824
Browse files Browse the repository at this point in the history
- Circular references are now caught
- Enums and some Date and Time types are converted to strings

Co-authored-by: Roland Praml <roland.praml@foconis.de>
  • Loading branch information
tonygermano and rPraml committed Apr 18, 2021
1 parent b989f89 commit 7d3985e
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 41 deletions.
145 changes: 104 additions & 41 deletions src/org/mozilla/javascript/NativeJSON.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
package org.mozilla.javascript;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
Expand Down Expand Up @@ -201,7 +203,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 @@ -269,6 +271,8 @@ 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 @@ -295,30 +299,8 @@ private static Object str(Object key, Scriptable holder,
} 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;
Scriptable nObj = state.cx.newObject(state.scope);
map.forEach((k, v) -> {
if (k instanceof CharSequence) {
nObj.put(((CharSequence) k).toString(), nObj, state.cx.getWrapFactory().wrap(state.cx, state.scope, v, v.getClass()));
}
});
value = nObj;
}
else {
if (value instanceof Collection<?>) {
Collection<?> col = (Collection<?>) value;
value = col.toArray(new Object[col.size()]);
}
if (value instanceof Object[]) {
Object[] elements = (Object[]) value;
elements = Arrays.stream(elements)
.map(o -> state.cx.getWrapFactory().wrap(state.cx, state.scope, o, o.getClass()))
.toArray();
value = state.cx.newArray(state.scope, elements);
}
}
wrappedJavaValue = value;
value = javaToJSON((NativeJavaObject) value);
}

if (value == null) return "null";
Expand All @@ -339,17 +321,20 @@ private static Object str(Object key, Scriptable holder,
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 {
throw ScriptRuntime.typeErrorById("msg.json.cant.serialize", unwrappedJavaValue.getClass().getName());
}
} else if (!Undefined.isUndefined(value)) {
throw ScriptRuntime.typeErrorById("msg.json.cant.serialize", value.getClass().getName());
}

return Undefined.instance;
}

Expand All @@ -367,10 +352,31 @@ 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.forEach((k, v) -> {
Object wrappedValue = state.cx.getWrapFactory().wrap(state.cx, state.scope, v, null);
if (k instanceof CharSequence) {
nObj.put(k.toString(), nObj, wrappedValue);
}
});
value = nObj;
}
state.stack.push(value);

String stepback = state.indent;
state.indent = state.indent + state.gap;
Expand Down Expand Up @@ -415,17 +421,34 @@ 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();
}
if (state.stack.search(trackValue) != -1) {
throw ScriptRuntime.typeErrorById("msg.cyclic.value", trackValue.getClass().getName());
}
state.stack.push(value);
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 -> state.cx.getWrapFactory().wrap(state.cx, state.scope, o, null))
.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 @@ -503,6 +526,46 @@ private static String quote(String string) {
return product.toString();
}

private static Object javaToJSON(NativeJavaObject value) {
Object unwrapped = value.unwrap();
Object converted = null;
if (isComplexJavaObject(unwrapped)) {
return value;
}

if (unwrapped instanceof Enum) {
converted = unwrapped;
} else if (unwrapped instanceof java.sql.Date) { // Date-Time handling.
converted = ((java.sql.Date) unwrapped).toLocalDate();
} else if (unwrapped instanceof java.sql.Time) {
converted = ((java.sql.Time) unwrapped).toLocalTime();
} else if (unwrapped instanceof java.util.Date) {
converted = ((Date) unwrapped).toInstant();
} else if (unwrapped instanceof Calendar) {
converted = ((Calendar) unwrapped).toInstant();
}

return (converted == null) ? unwrapped : converted.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
5 changes: 5 additions & 0 deletions testsrc/jstests/stringify-java-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,9 @@ 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 7d3985e

Please sign in to comment.