8157160: JSON.stringify does not work on ScriptObjectMirror objects
authorsundar
Wed, 18 May 2016 14:08:38 +0530
changeset 38483 84396baa1cb3
parent 37948 caf97b37ebec
child 38484 51ee10c31371
8157160: JSON.stringify does not work on ScriptObjectMirror objects Reviewed-by: hannesw, mhaupt
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJSON.java
nashorn/test/script/basic/JDK-8157160.js
nashorn/test/script/basic/JDK-8157160.js.EXPECTED
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJSON.java	Wed Jul 05 21:42:16 2017 +0200
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJSON.java	Wed May 18 14:08:38 2016 +0530
@@ -35,7 +35,10 @@
 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 @@
                 });
     }
 
+    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 @@
                     @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 @@
         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 @@
     // 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 @@
                 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 @@
 
         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 @@
     }
 
     // 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 @@
 
         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 @@
     }
 
     // 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 @@
         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 @@
 
         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");
+        }
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/script/basic/JDK-8157160.js	Wed May 18 14:08:38 2016 +0530
@@ -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)));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/script/basic/JDK-8157160.js.EXPECTED	Wed May 18 14:08:38 2016 +0530
@@ -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"}