# HG changeset patch # User attila # Date 1433235317 -7200 # Node ID 07f32a26bc1e32bea40413b8984ba0c330d48806 # Parent dcbf5e2121e3f2f5be93be4111b1c088d65ba998 8066773: JSON-friendly wrapper for objects Reviewed-by: jlaskey, lagergren, sundar diff -r dcbf5e2121e3 -r 07f32a26bc1e src/jdk/nashorn/api/scripting/ScriptObjectMirror.java --- a/src/jdk/nashorn/api/scripting/ScriptObjectMirror.java Wed Jun 03 10:42:06 2015 +0200 +++ b/src/jdk/nashorn/api/scripting/ScriptObjectMirror.java Tue Jun 02 10:55:17 2015 +0200 @@ -47,6 +47,7 @@ import jdk.nashorn.internal.runtime.ConsString; import jdk.nashorn.internal.runtime.Context; import jdk.nashorn.internal.runtime.ECMAException; +import jdk.nashorn.internal.runtime.JSONListAdapter; import jdk.nashorn.internal.runtime.JSType; import jdk.nashorn.internal.runtime.ScriptFunction; import jdk.nashorn.internal.runtime.ScriptObject; @@ -72,6 +73,7 @@ private final ScriptObject sobj; private final Global global; private final boolean strict; + private final boolean jsonCompatible; @Override public boolean equals(final Object other) { @@ -110,9 +112,9 @@ } if (sobj instanceof ScriptFunction) { - final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args; - final Object self = globalChanged? wrap(thiz, oldGlobal) : thiz; - return wrap(ScriptRuntime.apply((ScriptFunction)sobj, unwrap(self, global), unwrapArray(modArgs, global)), global); + final Object[] modArgs = globalChanged? wrapArrayLikeMe(args, oldGlobal) : args; + final Object self = globalChanged? wrapLikeMe(thiz, oldGlobal) : thiz; + return wrapLikeMe(ScriptRuntime.apply((ScriptFunction)sobj, unwrap(self, global), unwrapArray(modArgs, global))); } throw new RuntimeException("not a function: " + toString()); @@ -140,8 +142,8 @@ } if (sobj instanceof ScriptFunction) { - final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args; - return wrap(ScriptRuntime.construct((ScriptFunction)sobj, unwrapArray(modArgs, global)), global); + final Object[] modArgs = globalChanged? wrapArrayLikeMe(args, oldGlobal) : args; + return wrapLikeMe(ScriptRuntime.construct((ScriptFunction)sobj, unwrapArray(modArgs, global))); } throw new RuntimeException("not a constructor: " + toString()); @@ -170,7 +172,7 @@ return Context.getContext(); } }, GET_CONTEXT_ACC_CTXT); - return wrap(context.eval(global, s, sobj, null, false), global); + return wrapLikeMe(context.eval(global, s, sobj, null, false)); } }); } @@ -193,8 +195,8 @@ final Object val = sobj.get(functionName); if (val instanceof ScriptFunction) { - final Object[] modArgs = globalChanged? wrapArray(args, oldGlobal) : args; - return wrap(ScriptRuntime.apply((ScriptFunction)val, sobj, unwrapArray(modArgs, global)), global); + final Object[] modArgs = globalChanged? wrapArrayLikeMe(args, oldGlobal) : args; + return wrapLikeMe(ScriptRuntime.apply((ScriptFunction)val, sobj, unwrapArray(modArgs, global))); } else if (val instanceof JSObject && ((JSObject)val).isFunction()) { return ((JSObject)val).call(sobj, args); } @@ -218,7 +220,7 @@ Objects.requireNonNull(name); return inGlobal(new Callable() { @Override public Object call() { - return wrap(sobj.get(name), global); + return wrapLikeMe(sobj.get(name)); } }); } @@ -227,7 +229,7 @@ public Object getSlot(final int index) { return inGlobal(new Callable() { @Override public Object call() { - return wrap(sobj.get(index), global); + return wrapLikeMe(sobj.get(index)); } }); } @@ -368,7 +370,7 @@ while (iter.hasNext()) { final String key = iter.next(); - final Object value = translateUndefined(wrap(sobj.get(key), global)); + final Object value = translateUndefined(wrapLikeMe(sobj.get(key))); entries.add(new AbstractMap.SimpleImmutableEntry<>(key, value)); } @@ -382,7 +384,7 @@ checkKey(key); return inGlobal(new Callable() { @Override public Object call() { - return translateUndefined(wrap(sobj.get(key), global)); + return translateUndefined(wrapLikeMe(sobj.get(key))); } }); } @@ -419,8 +421,8 @@ final boolean globalChanged = (oldGlobal != global); return inGlobal(new Callable() { @Override public Object call() { - final Object modValue = globalChanged? wrap(value, oldGlobal) : value; - return translateUndefined(wrap(sobj.put(key, unwrap(modValue, global), strict), global)); + final Object modValue = globalChanged? wrapLikeMe(value, oldGlobal) : value; + return translateUndefined(wrapLikeMe(sobj.put(key, unwrap(modValue, global), strict))); } }); } @@ -434,7 +436,7 @@ @Override public Object call() { for (final Map.Entry entry : map.entrySet()) { final Object value = entry.getValue(); - final Object modValue = globalChanged? wrap(value, oldGlobal) : value; + final Object modValue = globalChanged? wrapLikeMe(value, oldGlobal) : value; final String key = entry.getKey(); checkKey(key); sobj.set(key, unwrap(modValue, global), getCallSiteFlags()); @@ -449,7 +451,7 @@ checkKey(key); return inGlobal(new Callable() { @Override public Object call() { - return translateUndefined(wrap(sobj.remove(key, strict), global)); + return translateUndefined(wrapLikeMe(sobj.remove(key, strict))); } }); } @@ -486,7 +488,7 @@ final Iterator iter = sobj.valueIterator(); while (iter.hasNext()) { - values.add(translateUndefined(wrap(iter.next(), global))); + values.add(translateUndefined(wrapLikeMe(iter.next()))); } return Collections.unmodifiableList(values); @@ -503,7 +505,7 @@ public Object getProto() { return inGlobal(new Callable() { @Override public Object call() { - return wrap(sobj.getProto(), global); + return wrapLikeMe(sobj.getProto()); } }); } @@ -532,7 +534,7 @@ public Object getOwnPropertyDescriptor(final String key) { return inGlobal(new Callable() { @Override public Object call() { - return wrap(sobj.getOwnPropertyDescriptor(key), global); + return wrapLikeMe(sobj.getOwnPropertyDescriptor(key)); } }); } @@ -661,16 +663,76 @@ * @return wrapped/converted object */ public static Object wrap(final Object obj, final Object homeGlobal) { + return wrap(obj, homeGlobal, false); + } + + /** + * Make a script object mirror on given object if needed. Also converts ConsString instances to Strings. The + * created wrapper will implement the Java {@code List} interface if {@code obj} is a JavaScript + * {@code Array} object; this is compatible with Java JSON libraries expectations. Arrays retrieved through its + * properties (transitively) will also implement the list interface. + * + * @param obj object to be wrapped/converted + * @param homeGlobal global to which this object belongs. Not used for ConsStrings. + * @return wrapped/converted object + */ + public static Object wrapAsJSONCompatible(final Object obj, final Object homeGlobal) { + return wrap(obj, homeGlobal, true); + } + + /** + * Make a script object mirror on given object if needed. Also converts ConsString instances to Strings. + * + * @param obj object to be wrapped/converted + * @param homeGlobal global to which this object belongs. Not used for ConsStrings. + * @param jsonCompatible if true, the created wrapper will implement the Java {@code List} interface if + * {@code obj} is a JavaScript {@code Array} object. Arrays retrieved through its properties (transitively) + * will also implement the list interface. + * @return wrapped/converted object + */ + private static Object wrap(final Object obj, final Object homeGlobal, final boolean jsonCompatible) { if(obj instanceof ScriptObject) { - return homeGlobal instanceof Global ? new ScriptObjectMirror((ScriptObject)obj, (Global)homeGlobal) : obj; - } - if(obj instanceof ConsString) { + if (!(homeGlobal instanceof Global)) { + return obj; + } + final ScriptObject sobj = (ScriptObject)obj; + final Global global = (Global)homeGlobal; + final ScriptObjectMirror mirror = new ScriptObjectMirror(sobj, global, jsonCompatible); + if (jsonCompatible && sobj.isArray()) { + return new JSONListAdapter(mirror, global); + } + return mirror; + } else if(obj instanceof ConsString) { return obj.toString(); + } else if (jsonCompatible && obj instanceof ScriptObjectMirror) { + // Since choosing JSON compatible representation is an explicit decision on user's part, if we're asked to + // wrap a mirror that was not JSON compatible, explicitly create its compatible counterpart following the + // principle of least surprise. + return ((ScriptObjectMirror)obj).asJSONCompatible(); } return obj; } /** + * Wraps the passed object with the same jsonCompatible flag as this mirror. + * @param obj the object + * @param homeGlobal the object's home global. + * @return a wrapper for the object. + */ + private Object wrapLikeMe(final Object obj, final Object homeGlobal) { + return wrap(obj, homeGlobal, jsonCompatible); + } + + /** + * Wraps the passed object with the same home global and jsonCompatible flag as this mirror. + * @param obj the object + * @return a wrapper for the object. + */ + private Object wrapLikeMe(final Object obj) { + return wrapLikeMe(obj, global); + } + + /** * Unwrap a script object mirror if needed. * * @param obj object to be unwrapped @@ -681,6 +743,8 @@ if (obj instanceof ScriptObjectMirror) { final ScriptObjectMirror mirror = (ScriptObjectMirror)obj; return (mirror.global == homeGlobal)? mirror.sobj : obj; + } else if (obj instanceof JSONListAdapter) { + return ((JSONListAdapter)obj).unwrap(homeGlobal); } return obj; @@ -694,6 +758,10 @@ * @return wrapped array */ public static Object[] wrapArray(final Object[] args, final Object homeGlobal) { + return wrapArray(args, homeGlobal, false); + } + + private static Object[] wrapArray(final Object[] args, final Object homeGlobal, final boolean jsonCompatible) { if (args == null || args.length == 0) { return args; } @@ -701,12 +769,16 @@ final Object[] newArgs = new Object[args.length]; int index = 0; for (final Object obj : args) { - newArgs[index] = wrap(obj, homeGlobal); + newArgs[index] = wrap(obj, homeGlobal, jsonCompatible); index++; } return newArgs; } + private Object[] wrapArrayLikeMe(final Object[] args, final Object homeGlobal) { + return wrapArray(args, homeGlobal, jsonCompatible); + } + /** * Unwrap an array of script object mirrors if needed. * @@ -748,12 +820,17 @@ // package-privates below this. ScriptObjectMirror(final ScriptObject sobj, final Global global) { + this(sobj, global, false); + } + + private ScriptObjectMirror(final ScriptObject sobj, final Global global, final boolean jsonCompatible) { assert sobj != null : "ScriptObjectMirror on null!"; assert global != null : "home Global is null"; this.sobj = sobj; this.global = global; this.strict = global.isStrictContext(); + this.jsonCompatible = jsonCompatible; } // accessors for script engine @@ -838,4 +915,11 @@ } }); } + + private ScriptObjectMirror asJSONCompatible() { + if (this.jsonCompatible) { + return this; + } + return new ScriptObjectMirror(sobj, global, true); + } } diff -r dcbf5e2121e3 -r 07f32a26bc1e src/jdk/nashorn/internal/objects/NativeJava.java --- a/src/jdk/nashorn/internal/objects/NativeJava.java Wed Jun 03 10:42:06 2015 +0200 +++ b/src/jdk/nashorn/internal/objects/NativeJava.java Tue Jun 02 10:55:17 2015 +0200 @@ -33,10 +33,12 @@ import java.util.Collection; import java.util.Deque; import java.util.List; +import java.util.Map; import java.util.Queue; import jdk.internal.dynalink.beans.StaticClass; import jdk.internal.dynalink.support.TypeUtilities; 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; @@ -656,4 +658,20 @@ public static Object _super(final Object self, final Object adapter) { return Bootstrap.createSuperAdapter(adapter); } + + /** + * Returns an object that is compatible with Java JSON libraries expectations; namely, that if it itself, or any + * object transitively reachable through it is a JavaScript array, then such objects will be exposed as + * {@link JSObject} that also implements the {@link List} interface for exposing the array elements. An explicit + * API is required as otherwise Nashorn exposes all objects externally as {@link JSObject}s that also implement the + * {@link Map} interface instead. By using this method, arrays will be exposed as {@link List}s and all other + * objects as {@link Map}s. + * @param self not used + * @param obj the object to be exposed in a Java JSON library compatible manner. + * @return a wrapper around the object that will enforce Java JSON library compatible exposure. + */ + @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) + public static Object asJSONCompatible(final Object self, final Object obj) { + return ScriptObjectMirror.wrapAsJSONCompatible(obj, Context.getGlobal()); + } } diff -r dcbf5e2121e3 -r 07f32a26bc1e src/jdk/nashorn/internal/runtime/JSONListAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk/nashorn/internal/runtime/JSONListAdapter.java Tue Jun 02 10:55:17 2015 +0200 @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2015, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.nashorn.internal.runtime; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import jdk.nashorn.api.scripting.JSObject; +import jdk.nashorn.api.scripting.ScriptObjectMirror; +import jdk.nashorn.internal.objects.Global; + +/** + * A {@link ListAdapter} that also implements {@link JSObject}. Named {@code JSONListAdapter} as it is used as a + * {@code JSObject} implementing the {@link List} interface, which is the expected interface to be implemented by + * JSON-parsed arrays when they are handled in Java. We aren't implementing {@link JSObject} on {@link ListAdapter} + * directly since that'd have implications for other uses of list adapter (e.g. interferences of JSObject default + * value calculation vs. List's {@code toString()} etc.) + */ +public final class JSONListAdapter extends ListAdapter implements JSObject { + /** + * Creates a new JSON list adapter. + * @param obj the underlying object being exposed as a list. + * @param global the home global of the underlying object. + */ + public JSONListAdapter(final JSObject obj, final Global global) { + super(obj, global); + } + + /** + * Unwraps this adapter into its underlying non-JSObject representative. + * @param homeGlobal the home global for unwrapping + * @return either the unwrapped object or this if it should not be unwrapped in the specified global. + */ + public Object unwrap(final Object homeGlobal) { + final Object unwrapped = ScriptObjectMirror.unwrap(obj, homeGlobal); + return unwrapped != obj ? unwrapped : this; + } + + @Override + public Object call(final Object thiz, final Object... args) { + return obj.call(thiz, args); + } + + @Override + public Object newObject(final Object... args) { + return obj.newObject(args); + } + + @Override + public Object eval(final String s) { + return obj.eval(s); + } + + @Override + public Object getMember(final String name) { + return obj.getMember(name); + } + + @Override + public Object getSlot(final int index) { + return obj.getSlot(index); + } + + @Override + public boolean hasMember(final String name) { + return obj.hasMember(name); + } + + @Override + public boolean hasSlot(final int slot) { + return obj.hasSlot(slot); + } + + @Override + public void removeMember(final String name) { + obj.removeMember(name); + } + + @Override + public void setMember(final String name, final Object value) { + obj.setMember(name, value); + } + + @Override + public void setSlot(final int index, final Object value) { + obj.setSlot(index, value); + } + + @Override + public Set keySet() { + return obj.keySet(); + } + + @Override + public Collection values() { + return obj.values(); + } + + @Override + public boolean isInstance(final Object instance) { + return obj.isInstance(instance); + } + + @Override + public boolean isInstanceOf(final Object clazz) { + return obj.isInstanceOf(clazz); + } + + @Override + public String getClassName() { + return obj.getClassName(); + } + + @Override + public boolean isFunction() { + return obj.isFunction(); + } + + @Override + public boolean isStrictFunction() { + return obj.isStrictFunction(); + } + + @Override + public boolean isArray() { + return obj.isArray(); + } + + @Override @Deprecated + public double toNumber() { + return obj.toNumber(); + } +} diff -r dcbf5e2121e3 -r 07f32a26bc1e src/jdk/nashorn/internal/runtime/ListAdapter.java --- a/src/jdk/nashorn/internal/runtime/ListAdapter.java Wed Jun 03 10:42:06 2015 +0200 +++ b/src/jdk/nashorn/internal/runtime/ListAdapter.java Tue Jun 02 10:55:17 2015 +0200 @@ -52,7 +52,7 @@ * operations respectively, while {@link #addLast(Object)} and {@link #removeLast()} will translate to {@code push} and * {@code pop}. */ -public final class ListAdapter extends AbstractList implements RandomAccess, Deque { +public class ListAdapter extends AbstractList implements RandomAccess, Deque { // Invoker creator for methods that add to the start or end of the list: PUSH and UNSHIFT. Takes fn, this, and value, returns void. private static final Callable ADD_INVOKER_CREATOR = invokerCreator(void.class, Object.class, JSObject.class, Object.class); @@ -78,21 +78,17 @@ private static final Callable SPLICE_REMOVE_INVOKER_CREATOR = invokerCreator(void.class, Object.class, JSObject.class, int.class, int.class); /** wrapped object */ - private final JSObject obj; + final JSObject obj; private final Global global; // allow subclasses only in this package - ListAdapter(final JSObject obj) { + ListAdapter(final JSObject obj, final Global global) { + if (global == null) { + throw new IllegalStateException(ECMAErrors.getMessage("list.adapter.null.global")); + } + this.obj = obj; - this.global = getGlobalNonNull(); - } - - private static Global getGlobalNonNull() { - final Global global = Context.getGlobal(); - if (global != null) { - return global; - } - throw new IllegalStateException(ECMAErrors.getMessage("list.adapter.null.global")); + this.global = global; } /** @@ -102,12 +98,13 @@ * @return A ListAdapter wrapper object */ public static ListAdapter create(final Object obj) { - return new ListAdapter(getJSObject(obj)); + final Global global = Context.getGlobal(); + return new ListAdapter(getJSObject(obj, global), global); } - private static JSObject getJSObject(final Object obj) { + private static JSObject getJSObject(final Object obj, final Global global) { if (obj instanceof ScriptObject) { - return (JSObject)ScriptObjectMirror.wrap(obj, Context.getGlobal()); + return (JSObject)ScriptObjectMirror.wrap(obj, global); } else if (obj instanceof JSObject) { return (JSObject)obj; } diff -r dcbf5e2121e3 -r 07f32a26bc1e test/src/jdk/nashorn/api/scripting/JSONCompatibleTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/src/jdk/nashorn/api/scripting/JSONCompatibleTest.java Tue Jun 02 10:55:17 2015 +0200 @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2015, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.nashorn.api.scripting; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class JSONCompatibleTest { + + /** + * Wrap a top-level array as a list. + */ + @Test + public void testWrapArray() throws ScriptException { + final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(); + final Object val = engine.eval("Java.asJSONCompatible([1, 2, 3])"); + assertEquals(asList(val), Arrays.asList(1, 2, 3)); + } + + /** + * Wrap an embedded array as a list. + */ + @Test + public void testWrapObjectWithArray() throws ScriptException { + final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(); + final Object val = engine.eval("Java.asJSONCompatible({x: [1, 2, 3]})"); + assertEquals(asList(asMap(val).get("x")), Arrays.asList(1, 2, 3)); + } + + /** + * Check it all works transitively several more levels down. + */ + @Test + public void testDeepWrapping() throws ScriptException { + final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(); + final Object val = engine.eval("Java.asJSONCompatible({x: [1, {y: [2, {z: [3]}]}, [4, 5]]})"); + final Map root = asMap(val); + final List x = asList(root.get("x")); + assertEquals(x.get(0), 1); + final Map x1 = asMap(x.get(1)); + final List y = asList(x1.get("y")); + assertEquals(y.get(0), 2); + final Map y1 = asMap(y.get(1)); + assertEquals(asList(y1.get("z")), Arrays.asList(3)); + assertEquals(asList(x.get(2)), Arrays.asList(4, 5)); + } + + /** + * Ensure that the old behaviour (every object is a Map) is unchanged. + */ + @Test + public void testNonWrapping() throws ScriptException { + final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(); + final Object val = engine.eval("({x: [1, {y: [2, {z: [3]}]}, [4, 5]]})"); + final Map root = asMap(val); + final Map x = asMap(root.get("x")); + assertEquals(x.get("0"), 1); + final Map x1 = asMap(x.get("1")); + final Map y = asMap(x1.get("y")); + assertEquals(y.get("0"), 2); + final Map y1 = asMap(y.get("1")); + final Map z = asMap(y1.get("z")); + assertEquals(z.get("0"), 3); + final Map x2 = asMap(x.get("2")); + assertEquals(x2.get("0"), 4); + assertEquals(x2.get("1"), 5); + } + + private static List asList(final Object obj) { + assertJSObject(obj); + Assert.assertTrue(obj instanceof List); + return (List)obj; + } + + private static Map asMap(final Object obj) { + assertJSObject(obj); + Assert.assertTrue(obj instanceof Map); + return (Map)obj; + } + + private static void assertJSObject(final Object obj) { + assertTrue(obj instanceof JSObject); + } +}