8157160: JSON.stringify does not work on ScriptObjectMirror objects

Reviewed-by: hannesw, mhaupt
This commit is contained in:
Athijegannathan Sundararajan 2016-05-18 14:08:38 +05:30
parent a3cdceaa83
commit 2479973df8
3 changed files with 208 additions and 15 deletions
nashorn
src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects
test/script/basic

@ -35,7 +35,10 @@ import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import jdk.nashorn.api.scripting.JSObject;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import jdk.nashorn.internal.objects.annotations.Attribute;
import jdk.nashorn.internal.objects.annotations.Function;
import jdk.nashorn.internal.objects.annotations.ScriptClass;
@ -68,6 +71,17 @@ public final class NativeJSON extends ScriptObject {
});
}
private static final Object JSOBJECT_INVOKER = new Object();
private static MethodHandle getJSOBJECT_INVOKER() {
return Global.instance().getDynamicInvoker(JSOBJECT_INVOKER,
new Callable<MethodHandle>() {
@Override
public MethodHandle call() {
return Bootstrap.createDynamicCallInvoker(Object.class, Object.class, Object.class);
}
});
}
private static final Object REPLACER_INVOKER = new Object();
@ -77,7 +91,7 @@ public final class NativeJSON extends ScriptObject {
@Override
public MethodHandle call() {
return Bootstrap.createDynamicCallInvoker(Object.class,
ScriptFunction.class, ScriptObject.class, Object.class, Object.class);
Object.class, Object.class, Object.class, Object.class);
}
});
}
@ -127,9 +141,10 @@ public final class NativeJSON extends ScriptObject {
final StringifyState state = new StringifyState();
// If there is a replacer, it must be a function or an array.
if (replacer instanceof ScriptFunction) {
state.replacerFunction = (ScriptFunction) replacer;
if (Bootstrap.isCallable(replacer)) {
state.replacerFunction = replacer;
} else if (isArray(replacer) ||
isJSObjectArray(replacer) ||
replacer instanceof Iterable ||
(replacer != null && replacer.getClass().isArray())) {
@ -201,18 +216,19 @@ public final class NativeJSON extends ScriptObject {
// stringify helpers.
private static class StringifyState {
final Map<ScriptObject, ScriptObject> stack = new IdentityHashMap<>();
final Map<Object, Object> stack = new IdentityHashMap<>();
StringBuilder indent = new StringBuilder();
String gap = "";
List<String> propertyList = null;
ScriptFunction replacerFunction = null;
Object replacerFunction = null;
}
// Spec: The abstract operation Str(key, holder).
private static Object str(final Object key, final ScriptObject holder, final StringifyState state) {
Object value = holder.get(key);
private static Object str(final Object key, final Object holder, final StringifyState state) {
assert holder instanceof ScriptObject || holder instanceof JSObject;
Object value = getProperty(holder, key);
try {
if (value instanceof ScriptObject) {
final InvokeByName toJSONInvoker = getTO_JSON();
@ -221,6 +237,12 @@ public final class NativeJSON extends ScriptObject {
if (Bootstrap.isCallable(toJSON)) {
value = toJSONInvoker.getInvoker().invokeExact(toJSON, svalue, key);
}
} else if (value instanceof JSObject) {
final JSObject jsObj = (JSObject)value;
final Object toJSON = jsObj.getMember("toJSON");
if (Bootstrap.isCallable(toJSON)) {
value = getJSOBJECT_INVOKER().invokeExact(toJSON, value);
}
}
if (state.replacerFunction != null) {
@ -262,10 +284,10 @@ public final class NativeJSON extends ScriptObject {
final JSType type = JSType.of(value);
if (type == JSType.OBJECT) {
if (isArray(value)) {
return JA((ScriptObject)value, state);
} else if (value instanceof ScriptObject) {
return JO((ScriptObject)value, state);
if (isArray(value) || isJSObjectArray(value)) {
return JA(value, state);
} else if (value instanceof ScriptObject || value instanceof JSObject) {
return JO(value, state);
}
}
@ -273,7 +295,9 @@ public final class NativeJSON extends ScriptObject {
}
// Spec: The abstract operation JO(value) serializes an object.
private static String JO(final ScriptObject value, final StringifyState state) {
private static String JO(final Object value, final StringifyState state) {
assert value instanceof ScriptObject || value instanceof JSObject;
if (state.stack.containsKey(value)) {
throw typeError("JSON.stringify.cyclic");
}
@ -284,7 +308,8 @@ public final class NativeJSON extends ScriptObject {
final StringBuilder finalStr = new StringBuilder();
final List<Object> partial = new ArrayList<>();
final List<String> k = state.propertyList == null ? Arrays.asList(value.getOwnKeys(false)) : state.propertyList;
final List<String> k = state.propertyList == null ?
Arrays.asList(getOwnKeys(value)) : state.propertyList;
for (final Object p : k) {
final Object strP = str(p, value, state);
@ -349,7 +374,9 @@ public final class NativeJSON extends ScriptObject {
}
// Spec: The abstract operation JA(value) serializes an array.
private static Object JA(final ScriptObject value, final StringifyState state) {
private static Object JA(final Object value, final StringifyState state) {
assert value instanceof ScriptObject || value instanceof JSObject;
if (state.stack.containsKey(value)) {
throw typeError("JSON.stringify.cyclic");
}
@ -359,7 +386,7 @@ public final class NativeJSON extends ScriptObject {
state.indent.append(state.gap);
final List<Object> partial = new ArrayList<>();
final int length = JSType.toInteger(value.getLength());
final int length = JSType.toInteger(getLength(value));
int index = 0;
while (index < length) {
@ -413,4 +440,48 @@ public final class NativeJSON extends ScriptObject {
return finalStr.toString();
}
private static String[] getOwnKeys(final Object obj) {
if (obj instanceof ScriptObject) {
return ((ScriptObject)obj).getOwnKeys(false);
} else if (obj instanceof ScriptObjectMirror) {
return ((ScriptObjectMirror)obj).getOwnKeys(false);
} else if (obj instanceof JSObject) {
// No notion of "own keys" or "proto" for general JSObject! We just
// return all keys of the object. This will be useful for POJOs
// implementing JSObject interface.
return ((JSObject)obj).keySet().toArray(new String[0]);
} else {
throw new AssertionError("should not reach here");
}
}
private static Object getLength(final Object obj) {
if (obj instanceof ScriptObject) {
return ((ScriptObject)obj).getLength();
} else if (obj instanceof JSObject) {
return ((JSObject)obj).getMember("length");
} else {
throw new AssertionError("should not reach here");
}
}
private static boolean isJSObjectArray(final Object obj) {
return (obj instanceof JSObject) && ((JSObject)obj).isArray();
}
private static Object getProperty(final Object holder, final Object key) {
if (holder instanceof ScriptObject) {
return ((ScriptObject)holder).get(key);
} else if (holder instanceof JSObject) {
JSObject jsObj = (JSObject)holder;
if (key instanceof Integer) {
return jsObj.getSlot((Integer)key);
} else {
return jsObj.getMember(Objects.toString(key));
}
} else {
return new AssertionError("should not reach here");
}
}
}

@ -0,0 +1,113 @@
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* JDK-8157160: JSON.stringify does not work on ScriptObjectMirror objects
*
* @test
* @option -scripting
* @run
*/
var SM = Java.type("javax.script.ScriptEngineManager");
var AJSO = Java.type("jdk.nashorn.api.scripting.AbstractJSObject");
var Supplier = Java.type("java.util.function.Supplier");
var engine = new SM().getEngineByName("nashorn");
// JSON stringify ScriptObjectMirror instances
print(JSON.stringify(engine.eval("({ foo : 42 })")));
print(JSON.stringify(engine.eval("([5, 6, 76, 7])")));
print(JSON.stringify(engine.eval(<<EOF
({
toJSON: function() "hello"
})
EOF
)));
print(JSON.stringify(engine.eval(<<EOF
obj = {
name: 'nashorn',
versions: [ 'es5.1', 'es6' ]
}
EOF
)));
var dm = engine.eval("new Date()");
print('"' + dm.toJSON() + '"' == JSON.stringify(dm));
// JSON stringifying an arbitrary JSObject impl.
var jsObj = new AJSO() {
keySet: function() {
var keys = new java.util.HashSet();
keys.add("x");
keys.add("y");
return keys;
},
getMember: function(name) {
if (name == "x") {
return 42;
} else if (name == "y") {
return "hello";
}
}
};
print(JSON.stringify(jsObj));
// try toJSON implementation on JSObject
var jsObj2 = new AJSO() {
getMember: function(name) {
if (name == 'toJSON') {
return function() {
return "my json representation";
}
}
}
};
print(JSON.stringify(jsObj2));
var jsObj3 = new AJSO() {
getMember: function(name) {
if (name == 'toJSON') {
return new Supplier() {
"get": function() {
return "value from toJSON function";
}
};
}
}
};
print(JSON.stringify(jsObj3));
// replacer function from another script world
print(JSON.stringify({
foo: "hello"
}, engine.eval(<<EOF
function (key, value) {
if (key == "foo") {
return value.toUpperCase()
}
return value;
}
EOF)));

@ -0,0 +1,9 @@
{"foo":42}
[5,6,76,7]
"hello"
{"name":"nashorn","versions":["es5.1","es6"]}
true
{"x":42,"y":"hello"}
"my json representation"
"value from toJSON function"
{"foo":"HELLO"}