8066773: JSON-friendly wrapper for objects
Reviewed-by: jlaskey, lagergren, sundar
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/api/scripting/ScriptObjectMirror.java Tue Jun 02 00:16:20 2015 -0700
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/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<Object>() {
@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<Object>() {
@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<Object>() {
@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<Object>() {
@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<? extends String, ? extends Object> 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<Object>() {
@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<Object> 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<Object>() {
@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<Object>() {
@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);
+ }
}
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java Tue Jun 02 00:16:20 2015 -0700
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/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());
+ }
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/JSONListAdapter.java Tue Jun 02 10:55:17 2015 +0200
@@ -0,0 +1,151 @@
+/*
+ * 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<String> keySet() {
+ return obj.keySet();
+ }
+
+ @Override
+ public Collection<Object> 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();
+ }
+}
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ListAdapter.java Tue Jun 02 00:16:20 2015 -0700
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/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<Object> implements RandomAccess, Deque<Object> {
+public class ListAdapter extends AbstractList<Object> implements RandomAccess, Deque<Object> {
// 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<MethodHandle> ADD_INVOKER_CREATOR = invokerCreator(void.class, Object.class, JSObject.class, Object.class);
@@ -78,21 +78,17 @@
private static final Callable<MethodHandle> 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) {
- this.obj = obj;
- this.global = getGlobalNonNull();
- }
+ ListAdapter(final JSObject obj, final Global global) {
+ if (global == null) {
+ throw new IllegalStateException(ECMAErrors.getMessage("list.adapter.null.global"));
+ }
- private static Global getGlobalNonNull() {
- final Global global = Context.getGlobal();
- if (global != null) {
- return global;
- }
- throw new IllegalStateException(ECMAErrors.getMessage("list.adapter.null.global"));
+ this.obj = obj;
+ 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;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/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<String, Object> root = asMap(val);
+ final List<Object> x = asList(root.get("x"));
+ assertEquals(x.get(0), 1);
+ final Map<String, Object> x1 = asMap(x.get(1));
+ final List<Object> y = asList(x1.get("y"));
+ assertEquals(y.get(0), 2);
+ final Map<String, Object> 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<String, Object> root = asMap(val);
+ final Map<String, Object> x = asMap(root.get("x"));
+ assertEquals(x.get("0"), 1);
+ final Map<String, Object> x1 = asMap(x.get("1"));
+ final Map<String, Object> y = asMap(x1.get("y"));
+ assertEquals(y.get("0"), 2);
+ final Map<String, Object> y1 = asMap(y.get("1"));
+ final Map<String, Object> z = asMap(y1.get("z"));
+ assertEquals(z.get("0"), 3);
+ final Map<String, Object> x2 = asMap(x.get("2"));
+ assertEquals(x2.get("0"), 4);
+ assertEquals(x2.get("1"), 5);
+ }
+
+ private static List<Object> asList(final Object obj) {
+ assertJSObject(obj);
+ Assert.assertTrue(obj instanceof List);
+ return (List)obj;
+ }
+
+ private static Map<String, Object> 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);
+ }
+}