diff --git a/src/org/mozilla/javascript/NativeJavaList.java b/src/org/mozilla/javascript/NativeJavaList.java new file mode 100644 index 0000000000..c5338fc9be --- /dev/null +++ b/src/org/mozilla/javascript/NativeJavaList.java @@ -0,0 +1,99 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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; + +import java.util.List; + +public class NativeJavaList extends NativeJavaObject { + + private List list; + + public NativeJavaList(Scriptable scope, Object list) { + super(scope, list, list.getClass()); + assert list instanceof List; + this.list = (List) list; + } + + @Override + public String getClassName() { + return "JavaList"; + } + + + @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)) { + return true; + } + return super.has(index, start); + } + + @Override + public boolean has(Symbol key, Scriptable start) { + if (SymbolKey.IS_CONCAT_SPREADABLE.equals(key)) { + return true; + } + return super.has(key, start); + } + + @Override + public Object get(String name, Scriptable start) { + if ("length".equals(name)) { + return list.size(); + } + return super.get(name, start); + } + + @Override + public Object get(int index, Scriptable start) { + if (isWithValidIndex(index)) { + Context cx = Context.getContext(); + Object obj = list.get(index); + return cx.getWrapFactory().wrap(cx, this, obj, obj.getClass()); + } + return Undefined.instance; + } + + @Override + public Object get(Symbol key, Scriptable start) { + if (SymbolKey.IS_CONCAT_SPREADABLE.equals(key)) { + return true; + } + return super.get(key, start); + } + + @Override + public void put(int index, Scriptable start, Object value) { + if (isWithValidIndex(index)) { + list.set(index, Context.jsToJava(value, Object.class)); + return; + } + super.put(index, start, value); + } + + @Override + public Object[] getIds() { + List list = (List) javaObject; + Object[] result = new Object[list.size()]; + int i = list.size(); + while (--i >= 0) { + result[i] = Integer.valueOf(i); + } + return result; + } + + private boolean isWithValidIndex(int index) { + return index >= 0 && index < list.size(); + } +} diff --git a/src/org/mozilla/javascript/NativeJavaMap.java b/src/org/mozilla/javascript/NativeJavaMap.java new file mode 100644 index 0000000000..81acc7429c --- /dev/null +++ b/src/org/mozilla/javascript/NativeJavaMap.java @@ -0,0 +1,86 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class NativeJavaMap extends NativeJavaObject { + + private Map map; + + public NativeJavaMap(Scriptable scope, Object map) { + super(scope, map, map.getClass()); + assert map instanceof Map; + this.map = (Map) map; + } + + @Override + public String getClassName() { + return "JavaMap"; + } + + + @Override + public boolean has(String name, Scriptable start) { + if (map.containsKey(name)) { + return true; + } + return super.has(name, start); + } + + @Override + public boolean has(int index, Scriptable start) { + if (map.containsKey(index)) { + return true; + } + return super.has(index, start); + } + + @Override + public Object get(String name, Scriptable start) { + if (map.containsKey(name)) { + Context cx = Context.getContext(); + Object obj = map.get(name); + return cx.getWrapFactory().wrap(cx, this, obj, obj.getClass()); + } + return super.get(name, start); + } + + @Override + public Object get(int index, Scriptable start) { + if (map.containsKey(index)) { + Context cx = Context.getContext(); + Object obj = map.get(index); + return cx.getWrapFactory().wrap(cx, this, obj, obj.getClass()); + } + return super.get(index, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + map.put(name, Context.jsToJava(value, Object.class)); + } + + @Override + public void put(int index, Scriptable start, Object value) { + map.put(index, Context.jsToJava(value, Object.class)); + } + + @Override + public Object[] getIds() { + List ids = new ArrayList<>(map.size()); + for (Object key : map.keySet()) { + if (key instanceof Integer) { + ids.add(ScriptRuntime.toInt32(key)); + } else { + ids.add(ScriptRuntime.toString(key)); + } + } + return ids.toArray(); + } +} diff --git a/src/org/mozilla/javascript/WrapFactory.java b/src/org/mozilla/javascript/WrapFactory.java index dba31ec569..163be35d2d 100644 --- a/src/org/mozilla/javascript/WrapFactory.java +++ b/src/org/mozilla/javascript/WrapFactory.java @@ -8,6 +8,9 @@ package org.mozilla.javascript; +import java.util.List; +import java.util.Map; + /** * Embeddings that wish to provide their own custom wrappings for Java * objects may extend this class and call @@ -117,6 +120,11 @@ public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class staticType) { + if (List.class.isAssignableFrom(javaObject.getClass())) { + return new NativeJavaList(scope, javaObject); + } else if (Map.class.isAssignableFrom(javaObject.getClass())) { + return new NativeJavaMap(scope, javaObject); + } return new NativeJavaObject(scope, javaObject, staticType); } diff --git a/testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java b/testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java new file mode 100644 index 0000000000..9dc2390c96 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java @@ -0,0 +1,123 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 junit.framework.TestCase; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.NativeArray; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.tools.shell.Global; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * From @makusuko (Markus Sunela), imported from PR https://github.com/mozilla/rhino/pull/561 + */ +public class NativeJavaListTest extends TestCase { + protected final Global global = new Global(); + + public NativeJavaListTest() { + global.init(ContextFactory.getGlobal()); + } + + + public void testAccessingJavaListIntegerValues() { + List list = new ArrayList<>(); + list.add(1); + list.add(2); + list.add(3); + + assertEquals(2, runScriptAsInt("value[1]", list)); + assertEquals(3, runScriptAsInt("value[2]", list)); + assertEquals(3, runScriptAsInt("value.length", list)); + } + + public void testLengthProperty() { + List list = new ArrayList<>(); + assertEquals(0, runScriptAsInt("value.length", list)); + list.add(1); + list.add(2); + list.add(3); + assertEquals(3, runScriptAsInt("value.length", list)); + } + + public void testJavaMethodsCalls() { + List list = new ArrayList<>(); + assertEquals(0, runScriptAsInt("value.size()", list)); + list.add(1); + list.add(2); + list.add(3); + assertEquals(3, runScriptAsInt("value.size()", list)); + } + + public void testUpdatingJavaListIntegerValues() { + List list = new ArrayList<>(); + list.add(1); + list.add(2); + list.add(3); + + assertEquals(2, runScriptAsInt("value[1]", list)); + assertEquals(5, runScriptAsInt("value[1]=5;value[1]", list)); + assertEquals(5, list.get(1).intValue()); + } + + public void testAccessingJavaListStringValues() { + List list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + + assertEquals("b", runScriptAsString("value[1]", list)); + assertEquals("c", runScriptAsString("value[2]", list)); + } + + public void testUpdatingJavaListStringValues() { + List list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + + assertEquals("b", runScriptAsString("value[1]", list)); + assertEquals("f", runScriptAsString("value[1]=\"f\";value[1]", list)); + assertEquals("f", list.get(1)); + } + + public void testKeys() { + List list = new ArrayList<>(); + NativeArray resEmpty = (NativeArray) runScript("Object.keys(value)", list, Function.identity()); + assertEquals(0, resEmpty.size()); + + list.add("a"); + list.add("b"); + list.add("c"); + + NativeArray res = (NativeArray) runScript("Object.keys(value)", list, Function.identity()); + assertEquals(3, res.size()); + assertTrue(res.contains("0")); + assertTrue(res.contains("1")); + assertTrue(res.contains("2")); + } + + private int runScriptAsInt(String scriptSourceText, Object value) { + return runScript(scriptSourceText, value, Context::toNumber).intValue(); + } + + private String runScriptAsString(String scriptSourceText, Object value) { + return runScript(scriptSourceText, value, Context::toString); + } + + private T runScript(String scriptSourceText, Object value, Function convert) { + return ContextFactory.getGlobal().call(context -> { + Scriptable scope = context.initStandardObjects(global); + scope.put("value", scope, Context.javaToJS(value, scope)); + return convert.apply(context.evaluateString(scope, scriptSourceText, "", 1, null)); + }); + } +} \ No newline at end of file diff --git a/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java b/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java new file mode 100644 index 0000000000..67f3690dd3 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/NativeJavaMapTest.java @@ -0,0 +1,142 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 junit.framework.TestCase; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.NativeArray; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.tools.shell.Global; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * From @makusuko (Markus Sunela), imported from PR https://github.com/mozilla/rhino/pull/561 + */ +public class NativeJavaMapTest extends TestCase { + protected final Global global = new Global(); + + public NativeJavaMapTest() { + global.init(ContextFactory.getGlobal()); + } + + + public void testAccessingJavaMapIntegerValues() { + Map map = new HashMap<>(); + map.put(0, 1); + map.put(1, 2); + map.put(2, 3); + + assertEquals(2, runScriptAsInt("value[1]", map)); + assertEquals(3, runScriptAsInt("value[2]", map)); + } + + public void testJavaMethodCalls() { + Map map = new HashMap<>(); + map.put("a", 1); + map.put("b", 2); + map.put("c", 3); + assertEquals(3, runScriptAsInt("value.size()", map)); + assertEquals(1, runScriptAsInt("value.get('a')", map)); + assertEquals(4, runScriptAsInt("value.put('d', 4);value.size()", map)); + } + + public void testUpdatingJavaMapIntegerValues() { + Map map = new HashMap<>(); + map.put(0,1); + map.put(1,2); + map.put(2,3); + + assertEquals(2, runScriptAsInt("value[1]", map)); + assertEquals(5, runScriptAsInt("value[1]=5;value[1]", map)); + assertEquals(5, map.get(1).intValue()); + } + + public void testAccessingJavaMapStringValues() { + Map map = new HashMap<>(); + map.put("a", "a"); + map.put("b", "b"); + map.put("c", "c"); + + assertEquals("b", runScriptAsString("value['b']", map)); + assertEquals("c", runScriptAsString("value['c']", map)); + assertEquals("b", runScriptAsString("value.b", map)); + assertEquals("c", runScriptAsString("value.c", map)); + } + + public void testUpdatingJavaMapStringValues() { + Map map = new HashMap<>(); + map.put("a", "a"); + map.put("b", "b"); + map.put("c", "c"); + + assertEquals("b", runScriptAsString("value['b']", map)); + assertEquals("b", runScriptAsString("value.b", map)); + assertEquals("f", runScriptAsString("value['b']=\"f\";value['b']", map)); + assertEquals("f", map.get("b")); + assertEquals("g", runScriptAsString("value.b=\"g\";value.b", map)); + } + + public void testAccessMapInMap() { + Map> map = new HashMap<>(); + map.put("a", new HashMap<>()); + map.get("a").put("a", "a"); + + assertEquals("a", runScriptAsString("value['a']['a']", map)); + assertEquals("a", runScriptAsString("value.a.a", map)); + } + + public void testUpdatingMapInMap() { + Map> map = new HashMap<>(); + map.put("a", new HashMap<>()); + map.get("a").put("a", "a"); + + assertEquals("a", runScriptAsString("value['a']['a']", map)); + assertEquals("a", runScriptAsString("value.a.a", map)); + assertEquals("b", runScriptAsString("value.a.a = 'b';value.a.a", map)); + } + + public void testKeys() { + Map map = new HashMap<>(); + NativeArray resEmpty = (NativeArray) runScript("Object.keys(value)", map, Function.identity()); + assertEquals(0, resEmpty.size()); + + map.put("a", "a"); + map.put("b", "b"); + map.put("c", "c"); + + NativeArray res = (NativeArray) runScript("Object.keys(value)", map, Function.identity()); + assertEquals(3, res.size()); + assertTrue(res.contains("a")); + assertTrue(res.contains("b")); + assertTrue(res.contains("c")); + + Map mapInt = new HashMap<>(); + mapInt.put(42, "test"); + NativeArray resInt = (NativeArray) runScript("Object.keys(value)", mapInt, Function.identity()); + assertTrue(resInt.contains("42")); // Object.keys always return Strings as key + } + + private int runScriptAsInt(String scriptSourceText, Object value) { + return runScript(scriptSourceText, value, Context::toNumber).intValue(); + } + + private String runScriptAsString(String scriptSourceText, Object value) { + return runScript(scriptSourceText, value, Context::toString); + } + + private T runScript(String scriptSourceText, Object value, Function convert) { + return ContextFactory.getGlobal().call(context -> { + Scriptable scope = context.initStandardObjects(global); + scope.put("value", scope, Context.javaToJS(value, scope)); + return convert.apply(context.evaluateString(scope, scriptSourceText, "", 1, null)); + }); + } +} \ No newline at end of file