changeset 1296:92f7bf49eb65

8066773: JSON-friendly wrapper for objects Reviewed-by: jlaskey, lagergren, sundar
author attila
date Tue, 02 Jun 2015 10:55:17 +0200
parents 6cb8df3f0cc6
children 776551a5b3a2
files src/jdk.scripting.nashorn/share/classes/jdk/nashorn/api/scripting/ScriptObjectMirror.java src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/JSONListAdapter.java src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ListAdapter.java test/src/jdk/nashorn/api/scripting/JSONCompatibleTest.java
diffstat 5 files changed, 403 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/api/scripting/ScriptObjectMirror.java	Tue Jun 02 00:16:20 2015 -0700
+++ b/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/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/NativeJava.java	Tue Jun 02 00:16:20 2015 -0700
+++ b/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/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/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ListAdapter.java	Tue Jun 02 00:16:20 2015 -0700
+++ b/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) {
+    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;
         }
--- /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<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);
+    }
+}