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);