diff --git a/examples/PrimitiveWrapFactory.java b/examples/PrimitiveWrapFactory.java index f527c4575e..a0093782e1 100644 --- a/examples/PrimitiveWrapFactory.java +++ b/examples/PrimitiveWrapFactory.java @@ -4,6 +4,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import java.lang.reflect.Type; + import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.WrapFactory; @@ -27,7 +29,7 @@ public class PrimitiveWrapFactory extends WrapFactory { @Override public Object wrap(Context cx, Scriptable scope, Object obj, - Class staticType) + Type staticType) { if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) diff --git a/src/org/mozilla/javascript/Context.java b/src/org/mozilla/javascript/Context.java index e52ef855b7..9eec6bcae5 100644 --- a/src/org/mozilla/javascript/Context.java +++ b/src/org/mozilla/javascript/Context.java @@ -1829,7 +1829,7 @@ public static Scriptable toObject(Object value, Scriptable scope, *

* The rest of values will be wrapped as LiveConnect objects * by calling {@link WrapFactory#wrap(Context cx, Scriptable scope, - * Object obj, Class staticType)} as in: + * Object obj, Type staticType)} as in: *

      *    Context cx = Context.getCurrentContext();
      *    return cx.getWrapFactory().wrap(cx, scope, value, null);
diff --git a/src/org/mozilla/javascript/IdScriptableObject.java b/src/org/mozilla/javascript/IdScriptableObject.java
index 6e66dbcfc6..e819ee6ffc 100644
--- a/src/org/mozilla/javascript/IdScriptableObject.java
+++ b/src/org/mozilla/javascript/IdScriptableObject.java
@@ -896,6 +896,7 @@ protected void addIdFunctionProperty(Scriptable obj, Object tag, int id,
      * @return obj casted to the target type
      * @throws EcmaError if the cast failed.
      */
+    @SuppressWarnings("unchecked")
     protected static  T ensureType(Object obj, Class clazz, IdFunctionObject f)
     {
         if (clazz.isInstance(obj)) {
diff --git a/src/org/mozilla/javascript/JavaMembers.java b/src/org/mozilla/javascript/JavaMembers.java
index 48e08472f4..b52967db81 100644
--- a/src/org/mozilla/javascript/JavaMembers.java
+++ b/src/org/mozilla/javascript/JavaMembers.java
@@ -15,6 +15,7 @@
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -85,18 +86,18 @@ Object get(Scriptable scope, String name, Object javaObject,
         }
         Context cx = Context.getContext();
         Object rval;
-        Class type;
+        Type type;
         try {
             if (member instanceof BeanProperty) {
                 BeanProperty bp = (BeanProperty) member;
                 if (bp.getter == null)
                     return Scriptable.NOT_FOUND;
                 rval = bp.getter.invoke(javaObject, Context.emptyArgs);
-                type = bp.getter.method().getReturnType();
+                type = bp.getter.method().getGenericReturnType();
             } else {
                 Field field = (Field) member;
                 rval = field.get(isStatic ? null : javaObject);
-                type = field.getType();
+                type = field.getGenericType();
             }
         } catch (Exception ex) {
             throw Context.throwAsScriptRuntimeEx(ex);
@@ -633,6 +634,30 @@ private void reflect(Scriptable scope,
             ht.putAll(toAdd);
         }
 
+        // if we are a Map or Iterable, we add an iterator in order
+        // that the JavaObject can be used in 'for(key in o)' or
+        // 'for each (value in o)' loops
+        if (Map.class.isAssignableFrom(cl)) {
+            // Add Map iterator
+            members.put(NativeIterator.ITERATOR_PROPERTY_NAME,
+                    NativeIterator.JAVA_MAP_ITERATOR);
+            
+        } else if (Iterable.class.isAssignableFrom(cl)) {
+            // Add Iterable/Collection iterator
+            members.put(NativeIterator.ITERATOR_PROPERTY_NAME,
+                    NativeIterator.JAVA_COLLECTION_ITERATOR);
+            // look for size() method and register as length property
+            Object member = members.get("size");
+            if (member instanceof NativeJavaMethod) {
+                NativeJavaMethod njmGet = (NativeJavaMethod) member;
+                MemberBox sizeMethod = extractGetMethod(njmGet.methods, false);
+                if (sizeMethod != null) {
+                    BeanProperty bp = new BeanProperty(sizeMethod, null, null);
+                    members.put("length", bp);
+                }
+            }
+        }
+
         // Reflect constructors
         Constructor[] constructors = getAccessibleConstructors(includePrivate);
         MemberBox[] ctorMembers = new MemberBox[constructors.length];
@@ -896,10 +921,10 @@ public Object getDefaultValue(Class hint)
         if (hint == ScriptRuntime.FunctionClass)
             return this;
         Object rval;
-        Class type;
+        Type type;
         try {
             rval = field.get(javaObject);
-            type = field.getType();
+            type = field.getGenericType();
         } catch (IllegalAccessException accEx) {
             throw Context.reportRuntimeErrorById(
                 "msg.java.internal.private", field.getName());
diff --git a/src/org/mozilla/javascript/NativeIterator.java b/src/org/mozilla/javascript/NativeIterator.java
index 8156662dd6..e6b5521735 100644
--- a/src/org/mozilla/javascript/NativeIterator.java
+++ b/src/org/mozilla/javascript/NativeIterator.java
@@ -6,7 +6,9 @@
 
 package org.mozilla.javascript;
 
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.Map;
 
 /**
  * This class implements iterator objects. See
@@ -18,6 +20,10 @@ public final class NativeIterator extends IdScriptableObject {
     private static final long serialVersionUID = -4136968203581667681L;
     private static final Object ITERATOR_TAG = "Iterator";
 
+    // Functions are registered as '__iterator__' for Iterables and Maps 
+    public static final BaseFunction JAVA_COLLECTION_ITERATOR = new CollectionIteratorFunction();
+    public static final BaseFunction JAVA_MAP_ITERATOR = new MapIteratorFunction();
+
     static void init(Context cx, ScriptableObject scope, boolean sealed) {
         // Iterator
         NativeIterator iterator = new NativeIterator();
@@ -221,6 +227,71 @@ static private Iterator getJavaIterator(Object obj) {
         return null;
     }
 
+    static class CollectionIteratorFunction extends BaseFunction {
+        @Override
+        public Object call(Context cx, Scriptable scope, Scriptable thisObj,
+                Object[] args) {
+
+            Object wrapped = ((NativeJavaObject) thisObj).javaObject;
+            if (Boolean.TRUE.equals(args[0])) {
+                // key only iterator, we will return an iterator
+                // for the sequence of the collection length.
+                int length = ((Collection) wrapped).size();
+                return cx.getWrapFactory().wrap(cx, scope,
+                        new SequenceIterator(length, scope),
+                        WrappedJavaIterator.class);
+            } else {
+                Iterator iter = ((Iterable) wrapped).iterator();
+                return cx.getWrapFactory().wrap(cx, scope,
+                        new WrappedJavaIterator(iter, scope),
+                        WrappedJavaIterator.class);
+            }
+        }
+    }
+    
+    static public class SequenceIterator
+    {
+        SequenceIterator(int size, Scriptable scope) {
+            this.size = size;
+            this.scope = scope;
+        }
+
+        public Object next() {
+            if (pos >= size) {
+                // Out of values. Throw StopIteration.
+                throw new JavaScriptException(
+                    NativeIterator.getStopIterationObject(scope), null, 0);
+            }
+            return pos++;
+        }
+
+        public Object __iterator__(boolean b) {
+            return this;
+        }
+
+        private int size;
+        private int pos;
+        private Scriptable scope;
+    }
+    
+    static class MapIteratorFunction extends BaseFunction {
+        @Override
+        public Object call(Context cx, Scriptable scope, Scriptable thisObj,
+                Object[] args) {
+
+            Map map = (Map) ((NativeJavaObject) thisObj).javaObject;
+            Iterator iter;
+            if (Boolean.TRUE.equals(args[0])) {
+                iter = map.keySet().iterator();
+            } else {
+                iter = map.values().iterator();
+            }
+            return cx.getWrapFactory().wrap(cx, scope,
+                    new WrappedJavaIterator(iter, scope),
+                    WrappedJavaIterator.class);
+        }
+    }
+    
     static public class WrappedJavaIterator
     {
         WrappedJavaIterator(Iterator iterator, Scriptable scope) {
diff --git a/src/org/mozilla/javascript/NativeJavaList.java b/src/org/mozilla/javascript/NativeJavaList.java
index 849896306a..6f5b5e7bff 100644
--- a/src/org/mozilla/javascript/NativeJavaList.java
+++ b/src/org/mozilla/javascript/NativeJavaList.java
@@ -5,17 +5,34 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.javascript;
 
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
 import java.util.List;
 
 public class NativeJavaList extends NativeJavaObject {
 
+    private static final long serialVersionUID = 6403865639690547921L;
+
     private List list;
+    
+    private Class valueType;
 
     @SuppressWarnings("unchecked")
-    public NativeJavaList(Scriptable scope, Object list) {
-        super(scope, list, list.getClass());
+    public NativeJavaList(Scriptable scope, Object list, Type staticType) {
+        super(scope, list, staticType);
         assert list instanceof List;
         this.list = (List) list;
+        if (staticType == null) {
+            staticType = list.getClass().getGenericSuperclass();
+        }
+        if (staticType instanceof ParameterizedType) {
+            Type[] types = ((ParameterizedType) staticType).getActualTypeArguments();
+            // types[0] contains the T of 'List'
+            this.valueType = ScriptRuntime.getRawType(types[0]);
+        } else {
+            this.valueType = Object.class;
+        }
     }
 
     @Override
@@ -24,14 +41,6 @@ public String getClassName() {
     }
 
 
-    @Override
-    public boolean has(String name, Scriptable start) {
-        if (name.equals("length")) {
-            return true;
-        }
-        return super.has(name, start);
-    }
-
     @Override
     public boolean has(int index, Scriptable start) {
         if (isWithValidIndex(index)) {
@@ -48,14 +57,6 @@ public boolean has(Symbol key, Scriptable start) {
         return super.has(key, start);
     }
 
-    @Override
-    public Object get(String name, Scriptable start) {
-        if ("length".equals(name)) {
-            return Integer.valueOf(list.size());
-        }
-        return super.get(name, start);
-    }
-
     @Override
     public Object get(int index, Scriptable start) {
         if (isWithValidIndex(index)) {
@@ -76,13 +77,25 @@ public Object get(Symbol key, Scriptable start) {
 
     @Override
     public void put(int index, Scriptable start, Object value) {
-        if (isWithValidIndex(index)) {
-            list.set(index, Context.jsToJava(value, Object.class));
+        if (index >= 0) {
+            ensureCapacity(index + 1);
+            list.set(index, Context.jsToJava(value, valueType));
             return;
         }
         super.put(index, start, value);
     }
 
+    private void ensureCapacity(int minCapacity) {
+        if (minCapacity > list.size()) {
+            if (list instanceof ArrayList) {
+                ((ArrayList) list).ensureCapacity(minCapacity);
+            }
+            while (minCapacity > list.size()) {
+              list.add(null);
+            }
+        }
+    }
+
     @Override
     public Object[] getIds() {
         List list = (List) javaObject;
diff --git a/src/org/mozilla/javascript/NativeJavaMap.java b/src/org/mozilla/javascript/NativeJavaMap.java
index 05cabff03d..624e7e661a 100644
--- a/src/org/mozilla/javascript/NativeJavaMap.java
+++ b/src/org/mozilla/javascript/NativeJavaMap.java
@@ -5,19 +5,36 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.javascript;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashMap;
 import java.util.Map;
 
 public class NativeJavaMap extends NativeJavaObject {
-
+    
+    private static final long serialVersionUID = 46513864372878618L;
+    
     private Map map;
+    private Class keyType;
+    private Class valueType;
+    private transient Map keyTranslationMap;
 
     @SuppressWarnings("unchecked")
-    public NativeJavaMap(Scriptable scope, Object map) {
-        super(scope, map, map.getClass());
+    public NativeJavaMap(Scriptable scope, Object map, Type staticType) {
+        super(scope, map, staticType);
         assert map instanceof Map;
         this.map = (Map) map;
+        if (staticType == null) {
+            staticType = map.getClass().getGenericSuperclass();
+        }
+        if (staticType instanceof ParameterizedType) {
+            Type[] types = ((ParameterizedType) staticType).getActualTypeArguments();
+            this.keyType = ScriptRuntime.getRawType(types[0]);
+            this.valueType = ScriptRuntime.getRawType(types[1]);
+        } else {
+            this.keyType = Object.class;
+            this.valueType = Object.class;
+        }
     }
 
     @Override
@@ -28,7 +45,7 @@ public String getClassName() {
 
     @Override
     public boolean has(String name, Scriptable start) {
-        if (map.containsKey(name)) {
+        if (map.containsKey(toKey(name, false))) {
             return true;
         }
         return super.has(name, start);
@@ -36,7 +53,7 @@ public boolean has(String name, Scriptable start) {
 
     @Override
     public boolean has(int index, Scriptable start) {
-        if (map.containsKey(Integer.valueOf(index))) {
+        if (map.containsKey(toKey(index, false))) {
             return true;
         }
         return super.has(index, start);
@@ -44,9 +61,13 @@ public boolean has(int index, Scriptable start) {
 
     @Override
     public Object get(String name, Scriptable start) {
-        if (map.containsKey(name)) {
+        Object key = toKey(name, false);
+        if (map.containsKey(key)) {
             Context cx = Context.getContext();
-            Object obj = map.get(name);
+            Object obj = map.get(key);
+            if (obj == null) {
+                return null;
+            }
             return cx.getWrapFactory().wrap(cx, this, obj, obj.getClass());
         }
         return super.get(name, start);
@@ -54,34 +75,98 @@ public Object get(String name, Scriptable start) {
 
     @Override
     public Object get(int index, Scriptable start) {
-        if (map.containsKey(Integer.valueOf(index))) {
+      Object key = toKey(Integer.valueOf(index), false);
+        if (map.containsKey(key)) {
             Context cx = Context.getContext();
-            Object obj = map.get(Integer.valueOf(index));
+            Object obj = map.get(key);
+            if (obj == null) {
+                return null;
+            }
             return cx.getWrapFactory().wrap(cx, this, obj, obj.getClass());
         }
         return super.get(index, start);
     }
+    
+    @SuppressWarnings("unchecked")
+    private Object toKey(Object key, boolean translateNew) {
+        if (keyType == String.class || map.containsKey(key)) {
+            // fast exit, if we know, that there are only string keys in the map o
+            return key;
+        }
+        String strKey = ScriptRuntime.toString(key);
+        if (map.containsKey(strKey)) {
+            // second fast exit, if the key is present as string.
+            return strKey;
+        }
+
+        // TODO: There is no change detection yet. The keys in the wrapped map could theoretically
+        // change though other java code. To reduce this risk, we clear the keyTranslationMap on
+        // unwrap. An approach to track if the underlying map was changed may be to read the
+        // 'modCount' property of HashMap, but this is not part of the Map interface.
+        // So for now, wrapped maps must not be changed by external code.
+        if (keyTranslationMap == null) {
+            keyTranslationMap = new HashMap<>();
+            map.keySet().forEach(k -> keyTranslationMap.put(ScriptRuntime.toString(k), k));
+        }
+        Object ret = keyTranslationMap.get(strKey);
+        if (ret == null) {
+            if (translateNew) {
+                // we do not have the key, and we need a new one, (due PUT operation e.g.)
+                if (keyType == Object.class) {
+                    // if we do not know the keyType, just pass through the key
+                    ret = key;
+                } else if (Enum.class.isAssignableFrom(keyType)) {
+                    // for enums use "valueOf" method
+                    ret = Enum.valueOf((Class) keyType, strKey);
+                } else {
+                    // for all other use jsToJava (which might run into a conversionError)
+                    ret = Context.jsToJava(key, keyType);
+                }
+                keyTranslationMap.put(strKey, ret);
+            } else {
+                ret = key;
+            }
+        }
+        return ret;
+    }
+    
+    private Object toValue(Object value) {
+        if (valueType == Object.class) {
+            return value;
+        } else {
+            return Context.jsToJava(value, valueType);
+        }
+    }
 
     @Override
     public void put(String name, Scriptable start, Object value) {
-        map.put(name, Context.jsToJava(value, Object.class));
+        map.put(toKey(name, true), toValue(value));
     }
 
     @Override
     public void put(int index, Scriptable start, Object value) {
-        map.put(Integer.valueOf(index), Context.jsToJava(value, Object.class));
+        map.put(toKey(index, true), toValue(value));
     }
 
+    @Override
+    public Object unwrap() {
+        // clear keyTranslationMap on unwrap, as native java code may modify the object now
+        keyTranslationMap = null;
+        return super.unwrap();
+    }
+    
     @Override
     public Object[] getIds() {
-        List ids = new ArrayList<>(map.size());
+        Object[] ids = new Object[map.size()];
+        int i = 0;
         for (Object key : map.keySet()) {
-            if (key instanceof Integer) {
-                ids.add((Integer)key);
+            if (key instanceof Number) {
+                ids[i++] = (Number)key;
             } else {
-                ids.add(ScriptRuntime.toString(key));
+                ids[i++] = ScriptRuntime.toString(key);
             }
         }
-        return ids.toArray();
+        return ids;
     }
+
 }
diff --git a/src/org/mozilla/javascript/NativeJavaMethod.java b/src/org/mozilla/javascript/NativeJavaMethod.java
index a43577233b..2d839f0cd5 100644
--- a/src/org/mozilla/javascript/NativeJavaMethod.java
+++ b/src/org/mozilla/javascript/NativeJavaMethod.java
@@ -8,6 +8,7 @@
 
 import java.lang.reflect.Array;
 import java.lang.reflect.Method;
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -224,7 +225,7 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj,
         }
 
         Object retval = meth.invoke(javaObject, args);
-        Class staticType = meth.method().getReturnType();
+        Type staticType = meth.method().getGenericReturnType();
 
         if (debug) {
             Class actualType = (retval == null) ? null
diff --git a/src/org/mozilla/javascript/NativeJavaObject.java b/src/org/mozilla/javascript/NativeJavaObject.java
index 5157f1c23e..8583cdfd33 100644
--- a/src/org/mozilla/javascript/NativeJavaObject.java
+++ b/src/org/mozilla/javascript/NativeJavaObject.java
@@ -13,6 +13,7 @@
 import java.lang.reflect.Array;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.Type;
 import java.util.Date;
 import java.util.Map;
 
@@ -35,17 +36,17 @@ public class NativeJavaObject
     public NativeJavaObject() { }
 
     public NativeJavaObject(Scriptable scope, Object javaObject,
-                            Class staticType)
+                            Type staticType)
     {
         this(scope, javaObject, staticType, false);
     }
 
     public NativeJavaObject(Scriptable scope, Object javaObject,
-                            Class staticType, boolean isAdapter)
+                            Type staticType, boolean isAdapter)
     {
         this.parent = scope;
         this.javaObject = javaObject;
-        this.staticType = staticType;
+        this.staticType = ScriptRuntime.getRawType(staticType);
         this.isAdapter = isAdapter;
         initMembers();
     }
@@ -190,7 +191,7 @@ public Object[] getIds() {
 
     /**
      * @deprecated Use {@link Context#getWrapFactory()} together with calling {@link
-     * WrapFactory#wrap(Context, Scriptable, Object, Class)}
+     * WrapFactory#wrap(Context, Scriptable, Object, Type)}
      */
     @Deprecated
     public static Object wrap(Scriptable scope, Object obj, Class staticType) {
diff --git a/src/org/mozilla/javascript/ScriptRuntime.java b/src/org/mozilla/javascript/ScriptRuntime.java
index 0976d6fa5f..0d7a2f3f01 100644
--- a/src/org/mozilla/javascript/ScriptRuntime.java
+++ b/src/org/mozilla/javascript/ScriptRuntime.java
@@ -7,7 +7,13 @@
 package org.mozilla.javascript;
 
 import java.io.Serializable;
+import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Locale;
@@ -2900,6 +2906,49 @@ public static String typeofName(Scriptable scope, String id)
         return typeof(getObjectProp(val, id, cx));
     }
 
+    /**
+     * returns the raw type. Taken from google guice.
+     */
+    public static Class getRawType(Type type) {
+        if (type == null) {
+            return null;
+            
+        } else if (type instanceof Class) {
+            // Type is a normal class.
+            return (Class) type;
+
+        } else if (type instanceof ParameterizedType) {
+            ParameterizedType parameterizedType = (ParameterizedType) type;
+
+            // I'm not exactly sure why getRawType() returns Type instead of
+            // Class. Neal isn't either but suspects some pathological case
+            // related to nested classes exists.
+            Type rawType = parameterizedType.getRawType();
+            if (!(rawType instanceof Class)) {
+                throw new IllegalArgumentException();
+            }
+            return (Class) rawType;
+
+        } else if (type instanceof GenericArrayType) {
+            Type componentType = ((GenericArrayType) type)
+                    .getGenericComponentType();
+            return Array.newInstance(getRawType(componentType), 0).getClass();
+
+        } else if (type instanceof TypeVariable
+                || type instanceof WildcardType) {
+            // We could use the variable's bounds, but that won't work if there
+            // are multiple. Having a raw type that's more general than 
+            // necessary is okay.
+            return Object.class;
+
+        } else {
+            String className = type.getClass().getName();
+            throw new IllegalArgumentException("Expected a Class, "
+                    + "ParameterizedType, or GenericArrayType, but <"
+                    + type + "> is of type " + className);
+        }
+    }
+
     public static boolean isObject(Object value)
     {
         if (value == null) {
diff --git a/src/org/mozilla/javascript/WrapFactory.java b/src/org/mozilla/javascript/WrapFactory.java
index 0d08759963..bb1f23f733 100644
--- a/src/org/mozilla/javascript/WrapFactory.java
+++ b/src/org/mozilla/javascript/WrapFactory.java
@@ -8,6 +8,7 @@
 
 package org.mozilla.javascript;
 
+import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Map;
 
@@ -45,14 +46,14 @@ public class WrapFactory
      * @return the wrapped value.
      */
     public Object wrap(Context cx, Scriptable scope,
-                       Object obj, Class staticType)
+                       Object obj, Type staticType)
     {
         if (obj == null || obj == Undefined.instance
             || obj instanceof Scriptable)
         {
             return obj;
         }
-        if (staticType != null && staticType.isPrimitive()) {
+        if (staticType instanceof Class && ((Class)staticType).isPrimitive()) {
             if (staticType == Void.TYPE)
                 return Undefined.instance;
             if (staticType == Character.TYPE)
@@ -103,7 +104,7 @@ public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj)
      * Wrap Java object as Scriptable instance to allow full access to its
      * methods and fields from JavaScript.
      * 

- * {@link #wrap(Context, Scriptable, Object, Class)} and + * {@link #wrap(Context, Scriptable, Object, Type)} and * {@link #wrapNewObject(Context, Scriptable, Object)} call this method * when they can not convert javaObject to JavaScript primitive * value or JavaScript array. @@ -118,14 +119,15 @@ public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) * @return the wrapped value which shall not be null */ public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, - Object javaObject, Class staticType) + Object javaObject, Type staticType) { - if (List.class.isAssignableFrom(javaObject.getClass())) { - return new NativeJavaList(scope, javaObject); - } else if (Map.class.isAssignableFrom(javaObject.getClass())) { - return new NativeJavaMap(scope, javaObject); + if (javaObject instanceof List) { + return new NativeJavaList(scope, javaObject, staticType); + } else if (javaObject instanceof Map) { + return new NativeJavaMap(scope, javaObject, staticType); + } else { + return new NativeJavaObject(scope, javaObject, staticType); } - return new NativeJavaObject(scope, javaObject, staticType); } /** diff --git a/testsrc/org/mozilla/javascript/tests/JavaIterableIteratorTest.java b/testsrc/org/mozilla/javascript/tests/JavaIterableIteratorTest.java new file mode 100644 index 0000000000..28404a8142 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/JavaIterableIteratorTest.java @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tests; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Wrapper; + +import junit.framework.TestCase; + +/* + * This testcase tests the basic access to Java classess implementing Iterable + * (eg. ArrayList) + */ +@RunWith(Parameterized.class) +public class JavaIterableIteratorTest extends TestCase { + + private static final String FOO_BAR_BAZ = "foo,bar,42.5,"; + + @Parameters + public static Collection> data() { + return Arrays.asList(new Iterable[] { + arrayList(), linkedHashSet(), iterable(), collection() + }); + } + + private Iterable iterable; + + public JavaIterableIteratorTest(Iterable iterable) { + this.iterable = iterable; + } + + private static List arrayList() { + List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add(42.5); + return list; + } + private static Set linkedHashSet() { + return new LinkedHashSet<>(arrayList()); + } + + private static Iterable iterable() { + return new Iterable() { + + @Override + public Iterator iterator() { + return arrayList().iterator(); + } + }; + } + + private static Collection collection() { + return new AbstractCollection() { + + @Override + public Iterator iterator() { + return arrayList().iterator(); + } + + @Override + public int size() { + return arrayList().size(); + } + }; + } + + + @Test + public void testArrayIterator() { + String js = "var ret = '';\n" + + "var iter = list.iterator();\n" + + "while(iter.hasNext()) ret += iter.next()+',';\n" + + "ret"; + testJavaObjectIterate(js, FOO_BAR_BAZ); + // there is no .iterator() function on the JS side + } + + @Test + public void testArrayForEach() { + String js = "var ret = '';\n" + + "for each(elem in list) ret += elem + ',';\n" + + "ret"; + testJsArrayIterate(js, FOO_BAR_BAZ); + testJavaObjectIterate(js, FOO_BAR_BAZ); + testJavaArrayIterate(js, FOO_BAR_BAZ); + } + + @Test + public void testArrayForKeys() { + String js = "var ret = '';\n" + + "for(elem in list) ret += elem + ',';\n" + + "ret"; + testJsArrayIterate(js, "0,1,2,"); + if (iterable instanceof Collection) { + testJavaObjectIterate(js, "0,1,2,"); + } + testJavaArrayIterate(js, "0,1,2,"); + } + + @Test + public void testArrayForIndex() { + String js = "var ret = '';\n" + + "for(var idx = 0; idx < list.length; idx++) ret += idx + ',';\n" + + "ret"; + testJsArrayIterate(js, "0,1,2,"); + testJavaArrayIterate(js, "0,1,2,"); + if (iterable instanceof Collection) { + testJavaObjectIterate(js, "0,1,2,"); + } + } + + // use NativeJavaArray + private void testJavaArrayIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + List list = new ArrayList<>(); + iterable.forEach(list::add); + scope.put("list", scope, list.toArray()); + Object o = cx.evaluateString(scope, script, + "testJavaArrayIterate.js", 1, null); + assertEquals(expected, o); + + return null; + }); + } + + // use the java object directly + private void testJavaObjectIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + scope.put("list", scope, iterable); + Object o = cx.evaluateString(scope, script, + "testJavaListIterate.js", 1, null); + assertEquals(expected, o); + + return null; + }); + + } + + // use nativeArray + private void testJsArrayIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + List list = new ArrayList<>(); + iterable.forEach(list::add); + scope.put("list", scope, + cx.newArray(scope, list.toArray())); + Object o = cx.evaluateString(scope, script, + "testJsArrayIterate.js", 1, null); + assertEquals(expected, o); + return null; + }); + } + +} diff --git a/testsrc/org/mozilla/javascript/tests/JavaListAccessTest.java b/testsrc/org/mozilla/javascript/tests/JavaListAccessTest.java new file mode 100644 index 0000000000..a9c0b2f062 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/JavaListAccessTest.java @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tests; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.junit.Test; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Wrapper; + +import junit.framework.TestCase; + +/* + * This testcase tests the basic access to java List with [] + */ +public class JavaListAccessTest extends TestCase { + + @Test + public void testBeanAccess() { + String js = "bean.integers[0] = 3;\n" + + "bean.doubles[0] = 3;" + + "bean.doubles[0].getClass().getSimpleName() + ' ' " + + "+ bean.integers[0].getClass().getSimpleName()\n"; + testIt(js, "Double Integer"); + } + + @Test + public void testListAccess() { + String js = "intList[0] = 3;\n" + + "dblList[0] = 3;" + + "dblList[0].getClass().getSimpleName() + ' ' " + + "+ intList[0].getClass().getSimpleName()\n"; + testIt(js, "Double Integer"); + } + + @Test + public void testIntListIncrement() { + String js = "intList[0] = 3.5;\n" + + "intList[0]++;\n" + + "intList[0].getClass().getSimpleName() + ' ' + intList[0]\n"; + testIt(js, "Integer 4"); + } + + @Test + public void testDblListIncrement() { + String js = "dblList[0] = 3.5;\n" + + "dblList[0]++;\n" + + "dblList[0].getClass().getSimpleName() + ' ' + dblList[0]\n"; + testIt(js, "Double 4.5"); + } + + + public static class Bean { + public List integers = new ArrayList<>(); + private List doubles = new ArrayList<>(); + + public List getDoubles() { + return doubles; + } + + public List numbers = new ArrayList<>(); + } + + + private List createIntegerList() { + List list = new ArrayList() { + + }; + list.add(42); + list.add(7); + return list; + } + + private List createDoubleList() { + List list = new ArrayList() { + + }; + list.add(42.5); + list.add(7.5); + return list; + } + + private List createNumberList() { + List list = new ArrayList() { + + }; + list.add(42); + list.add(7.5); + return list; + } + + private void testIt(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + scope.put("intList", scope, createIntegerList()); + scope.put("dblList", scope, createDoubleList()); + scope.put("numList", scope, createNumberList()); + scope.put("bean", scope, new Bean()); + Object o = cx.evaluateString(scope, script, + "testJavaArrayIterate.js", 1, null); + assertEquals(expected, o); + + return null; + }); + + } +} diff --git a/testsrc/org/mozilla/javascript/tests/JavaListIteratorTest.java b/testsrc/org/mozilla/javascript/tests/JavaListIteratorTest.java new file mode 100644 index 0000000000..9a0352efb0 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/JavaListIteratorTest.java @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tests; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.junit.Test; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Wrapper; + +import junit.framework.TestCase; + +/* + * This testcase tests the basic access to Java classess implementing Iterable + * (eg. ArrayList) + */ +public class JavaListIteratorTest extends TestCase { + + private static final String FOO_BAR_BAZ = "foo,bar,42.5,"; + + private List createJavaList() { + List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + list.add(42.5); + return list; + } + + @Test + public void testArrayIterator() { + String js = "var ret = '';\n" + + "var iter = list.iterator();\n" + + "while(iter.hasNext()) ret += iter.next()+',';\n" + + "ret"; + testJavaListIterate(js, FOO_BAR_BAZ); + // there is no .iterator() function on the JS side + } + + @Test + public void testArrayForEach() { + String js = "var ret = '';\n" + + "for each(elem in list) ret += elem + ',';\n" + + "ret"; + testJsArrayIterate(js, FOO_BAR_BAZ); + testJavaListIterate(js, FOO_BAR_BAZ); + testJavaArrayIterate(js, FOO_BAR_BAZ); + } + + @Test + public void testArrayForKeys() { + String js = "var ret = '';\n" + + "for(elem in list) ret += elem + ',';\n" + + "ret"; + testJsArrayIterate(js, "0,1,2,"); + testJavaListIterate(js, "0,1,2,"); + testJavaArrayIterate(js, "0,1,2,"); + } + + @Test + public void testArrayForIndex() { + String js = "var ret = '';\n" + + "for(var idx = 0; idx < list.length; idx++) ret += idx + ',';\n" + + "ret"; + testJsArrayIterate(js, "0,1,2,"); + testJavaArrayIterate(js, "0,1,2,"); + testJavaListIterate(js, "0,1,2,"); + } + + private void testJavaArrayIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + scope.put("list", scope, createJavaList().toArray()); + Object o = cx.evaluateString(scope, script, + "testJavaArrayIterate.js", 1, null); + assertEquals(expected, o); + + return null; + }); + } + + private void testJavaListIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + scope.put("list", scope, createJavaList()); + Object o = cx.evaluateString(scope, script, + "testJavaListIterate.js", 1, null); + assertEquals(expected, o); + + return null; + }); + + } + + private void testJsArrayIterate(String script, String expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + + scope.put("list", scope, + cx.newArray(scope, createJavaList().toArray())); + Object o = cx.evaluateString(scope, script, + "testJsArrayIterate.js", 1, null); + assertEquals(expected, o); + return null; + }); + } + +} diff --git a/testsrc/org/mozilla/javascript/tests/JavaMapIteratorTest.java b/testsrc/org/mozilla/javascript/tests/JavaMapIteratorTest.java new file mode 100644 index 0000000000..95f730b991 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/JavaMapIteratorTest.java @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; + + +/* + * This testcase tests the basic access to Java classess implementing Iterable + * (eg. ArrayList) + */ +@RunWith(Parameterized.class) +public class JavaMapIteratorTest { + + private static final String EXPECTED_VALUES = "7,2,5,"; + private static final String EXPECTED_KEYS = "foo,bar,baz,"; + + @Parameters + public static Collection> data() { + return Arrays.asList(new Map[] { + mapWithEnumKey(), + mapWithStringKey() + }); + } + private static Map mapWithStringKey() { + Map map = new LinkedHashMap<>(); + map.put("foo", 7); + map.put("bar", 2); + map.put("baz", 5); + return map; + } + public enum MyEnum { + foo, bar, baz + } + private static Map mapWithEnumKey() { + Map map = new EnumMap<>(MyEnum.class); + map.put(MyEnum.foo, 7); + map.put(MyEnum.bar, 2); + map.put(MyEnum.baz, 5); + return map; + } + + private Map map; + + public JavaMapIteratorTest(Map map) { + this.map = map; + } + + // iterate over all values with 'for each' + @Test + public void testForEachValue() { + String js = "var ret = '';\n" + + "for each(value in map) ret += value + ',';\n" + + "ret"; + testJsMap(js, EXPECTED_VALUES); + testJavaMap(js, EXPECTED_VALUES); + } + + // iterate over all keys and concatenate them + @Test + public void testForKey() { + String js = "var ret = '';\n" + + "for(key in map) ret += key + ',';\n" + + "ret"; + testJsMap(js, EXPECTED_KEYS); + testJavaMap(js, EXPECTED_KEYS); + } + + // iterate over all keys and try to read the map value + @Test + public void testForKeyWithGet() { + String js = "var ret = '';\n" + + "for(key in map) ret += map[key] + ',';\n" + + "ret"; + testJsMap(js, EXPECTED_VALUES); + testJavaMap(js, EXPECTED_VALUES); + } + + // invoke map.forEach function. + // NOTE: signature of forEach is different + // EcmaScript Map: forEach(value, key, map) + // Java: forEach(key, value) + @Test + public void testMapForEach1() { + String js = "var ret = '';\n" + + "map.forEach(function(key) { ret += key + ',' });\n" + + "ret"; + testJavaMap(js, EXPECTED_KEYS); + } + + @Test + public void testMapForEach2() { + String js = "var ret = '';\n" + + "map.forEach(function(key, value) { ret += value + ',' });\n" + + "ret"; + testJavaMap(js, EXPECTED_VALUES); // forEach(key, value) + } + + @Test + public void testMapForEach3() { + String js = "var ret = '';\n" + + "map.forEach(function(key) { ret += map[key] + ',' });\n" + + "ret"; + testJavaMap(js, EXPECTED_VALUES); + } + + @Test + public void testObjectKeys() { + String js = "Object.keys(map).join(',')+',';\n"; + testJavaMap(js, EXPECTED_KEYS); + testJsMap(js, EXPECTED_KEYS); + } + + private void testJavaMap(String script, Object expected) { + Utils.runWithAllOptimizationLevels(cx -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final ScriptableObject scope = cx.initStandardObjects(); + scope.put("map", scope, map); + Object o = cx.evaluateString(scope, script, + "testJavaMap.js", 1, null); + assertEquals(expected, o); + + return null; + }); + } + + private void testJsMap(String script, Object expected) { + Utils.runWithAllOptimizationLevels(cx -> { + final ScriptableObject scope = cx.initStandardObjects(); + Scriptable obj = cx.newObject(scope); + map.forEach((key,value)->obj.put(String.valueOf(key), obj, value)); + scope.put("map", scope, obj); + Object o = cx.evaluateString(scope, script, + "testJsMap.js", 1, null); + assertEquals(expected, o); + + return null; + }); + } + +} diff --git a/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java b/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java index 67f3690dd3..c4196176b1 100644 --- a/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java +++ b/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java @@ -26,6 +26,10 @@ public class NativeJavaMapTest extends TestCase { public NativeJavaMapTest() { global.init(ContextFactory.getGlobal()); } + + public static enum MyEnum { + A, B, C, X, Y, Z + } public void testAccessingJavaMapIntegerValues() { @@ -37,7 +41,60 @@ public void testAccessingJavaMapIntegerValues() { assertEquals(2, runScriptAsInt("value[1]", map)); assertEquals(3, runScriptAsInt("value[2]", map)); } + public void testAccessingJavaMapLongValues() { + Map map = new HashMap<>(); + map.put(0L, 1); + map.put(1L, 2); + map.put(2L, 3); + + assertEquals(2, runScriptAsInt("value[1]", map)); + assertEquals(3, runScriptAsInt("value[2]", map)); + runScriptAsString("value[4] = 4.01", map); + assertEquals(Double.valueOf(4.01), map.get(4)); + assertEquals(null, map.get(4L)); + } + + public void testAccessingJavaMapEnumValuesWithGeneric() { + // genrate inner class, that contains type information. + Map map = new HashMap() { + private static final long serialVersionUID = 1L; + }; + + map.put(MyEnum.A, 1); + map.put(MyEnum.B, 2); + map.put(MyEnum.C, 3); + + assertEquals(2, runScriptAsInt("value['B']", map)); + assertEquals(3, runScriptAsInt("value['C']", map)); + runScriptAsString("value['X'] = 4.01", map); + // we know the type info and can convert the key to Long and the value is rounded to Integer + assertEquals(Integer.valueOf(4),map.get(MyEnum.X)); + + try { + runScriptAsString("value['D'] = 4.0", map); + fail();; + } catch (IllegalArgumentException ex) { + assertEquals("No enum constant org.mozilla.javascript.tests.NativeJavaMapTest.MyEnum.D", ex.getMessage()); + } + } + public void testAccessingJavaMapLongValuesWithGeneric() { + // genrate inner class, that contains type information. + Map map = new HashMap() { + private static final long serialVersionUID = 1L; + }; + + map.put(0L, 1); + map.put(1L, 2); + map.put(2L, 3); + + assertEquals(2, runScriptAsInt("value[1]", map)); + assertEquals(3, runScriptAsInt("value[2]", map)); + runScriptAsInt("value[4] = 4.0", map); + // we know the type info and can convert the key to Long and the value to Integer + assertEquals(Integer.valueOf(4),map.get(4L)); + assertEquals(null, map.get(4)); + } public void testJavaMethodCalls() { Map map = new HashMap<>(); map.put("a", 1);