Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD: delete method and set length property to NativeJavaList #1031

Merged
merged 2 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions src/org/mozilla/javascript/NativeJavaList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,45 @@
import java.util.ArrayList;
import java.util.List;

/**
* <code>NativeJavaList</code> is a wrapper for java objects implementing <code>java.util.List
* </code> interface. This wrapper delegates index based access in javascript (like <code>
* value[x] = 3</code>) to the according {@link List#get(int)}, {@link List#set(int, Object)} and
* {@link List#add(Object)} methods. This allows you to use java lists in many places like a
* javascript <code>Array</code>.
*
* <p>Supported functions:
*
* <ul>
* <li>index based access is delegated to List.get/set/add. If <code>index &gt;= length</code>,
* the skipped elements will be filled with <code>null</code> values
* <li>iterator support with <code>for...of</code> (provided by NativeJavaObject for all
* iterables)
* <li>when iterating with <code>for .. in</code> (or <code>for each .. in</code>) then <code>
* getIds
* </code> + index based access is used.
* <li>reading and setting <code>length</code> property. When modifying the length property, the
* list is either truncated or will be filled with <code>null</code> values up to <code>length
* </code>
* <li>deleting entries: <code>delete value[index]</code> will be equivalent with <code>
* value[index] = null</code> and is implemented to provide array compatibility.
* </ul>
*
* <b>Important:</b> JavaList does not support sparse arrays. So setting the length property to a
* high value or writing to a high index may allocate a lot of memory.
*
* <p><b>Note:</b> Although <code>JavaList</code> looks like a javascript-<code>Array</code>, it is
* not an <code>
* Array</code>. Some methods behave very similar like <code>Array.indexOf</code> and <code>
* java.util.List.indexOf</code>, others are named differently like <code>Array.includes</code> vs.
* <code>java.util.List.contains</code>. Especially <code>forEach</code> is different in <code>Array
* </code> and <code>java.util.List</code>. Also deleting entries will set entries to <code>null
* </code> instead to <code>Undefined</code>
*/
public class NativeJavaList extends NativeJavaObject {

private static final long serialVersionUID = 660285467829047519L;

private List<Object> list;

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -40,6 +77,12 @@ public boolean has(int index, Scriptable start) {
return super.has(index, start);
}

public void delete(int index) {
if (isWithValidIndex(index)) {
list.set(index, null);
}
}

@Override
public boolean has(Symbol key, Scriptable start) {
if (SymbolKey.IS_CONCAT_SPREADABLE.equals(key)) {
Expand Down Expand Up @@ -80,13 +123,27 @@ public Object get(Symbol key, Scriptable start) {
@Override
public void put(int index, Scriptable start, Object value) {
if (index >= 0) {
ensureCapacity(index + 1);
list.set(index, Context.jsToJava(value, Object.class));
Object javaValue = Context.jsToJava(value, Object.class);
if (index == list.size()) {
list.add(javaValue); // use "add" at the end of list.
} else {
ensureCapacity(index + 1);
list.set(index, javaValue);
}
return;
}
super.put(index, start, value);
}

@Override
public void put(String name, Scriptable start, Object value) {
if (list != null && "length".equals(name)) {
setLength(value);
return;
}
super.put(name, start, value);
}

private void ensureCapacity(int minCapacity) {
if (minCapacity > list.size()) {
if (list instanceof ArrayList) {
Expand All @@ -98,6 +155,20 @@ private void ensureCapacity(int minCapacity) {
}
}

private void setLength(Object val) {
double d = ScriptRuntime.toNumber(val);
long longVal = ScriptRuntime.toUint32(d);
if (longVal != d || longVal > Integer.MAX_VALUE) {
String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
throw ScriptRuntime.rangeError(msg);
}
if (longVal < list.size()) {
list.subList((int) longVal, list.size()).clear();
} else {
ensureCapacity((int) longVal);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems dangerous since typical java Lists don't support sparse layouts like javascript arrays do. If you set length to a very large value on a javascript array it's no big deal, but do the same on an ArrayList, and you're probably going to run out of memory.

Should we at least add a javadoc description for the class that describes the additional behaviors which NativeJavaList adds to NativeJavaObject and potential pitfalls when using these features as if Lists were arrays? Is this stuff documented anywhere else other than in PRs and commit messages?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improving the Javadoc sounds great to me.

In general, the "embedding guide" is very very old and hasn't it been archived by Mozilla? Do we have any volunteers to resurrect it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should we document this? JavaDoc, MD, wiki-page?

I would prefer a markdown documentation like this one: https://github.com/oracle/graaljs/blob/master/docs/user/JavaInteroperability.md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think the javadoc should contain basic important information for using the class. Wiki/user guide type documentation is good for going into more detail if someone is willing to write that. As we know, external documentation is not always easily accessible and developers should have some idea how a class works without having to closely study the code or pull up external docs.

}
}

@Override
public Object[] getIds() {
List<?> list = (List<?>) javaObject;
Expand Down
13 changes: 13 additions & 0 deletions src/org/mozilla/javascript/NativeJavaMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,21 @@
import java.util.List;
import java.util.Map;

/**
* <code>NativeJavaMap</code> is a wrapper for java objects implementing <code>java.util.Map
* </code> interface. When {@link Context#FEATURE_ENABLE_JAVA_MAP_ACCESS} is enabled, property based
* access like <code>map[key]</code> is delegated to {@link Map#get(Object)} or {@link
* Map#put(Object, Object)} operations so that a <code>JavaMap</code> acts very similar to a
* javascript <code>Object</code> There is also an iterator to iterate over entries with <code>
* for .. of</code>.
*
* <p><b>Limitations:</b> The wrapped map should have <code>String</code> or <code>Integer</code> as
* key. Otherwise, property based access may not work properly.
*/
public class NativeJavaMap extends NativeJavaObject {

private static final long serialVersionUID = -3786257752907047381L;

private Map<Object, Object> map;

static void init(ScriptableObject scope, boolean sealed) {
Expand Down
3 changes: 2 additions & 1 deletion src/org/mozilla/javascript/NativeJavaObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

/**
* This class reflects non-Array Java objects into the JavaScript environment. It reflect fields
* directly, and uses NativeJavaMethod objects to reflect (possibly overloaded) methods.
* directly, and uses NativeJavaMethod objects to reflect (possibly overloaded) methods. It also
* provides iterator support for all iterable objects.
*
* <p>
*
Expand Down
80 changes: 78 additions & 2 deletions testsrc/org/mozilla/javascript/tests/NativeJavaListTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import junit.framework.TestCase;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.tools.shell.Global;
Expand Down Expand Up @@ -65,6 +66,10 @@ public void testLengthProperty() {
list.add(2);
list.add(3);
assertEquals(3, runScriptAsInt("value.length", list));
runScriptAsInt("value.length = 6", list);
assertEquals(6, list.size());
runScriptAsInt("value.length = 2", list);
assertEquals(2, list.size());
}

public void testJavaMethodsCalls() {
Expand All @@ -83,8 +88,9 @@ public void testUpdatingJavaListIntegerValues() {
list.add(3);

assertEquals(2, runScriptAsInt("value[1]", list));
assertEquals(5, runScriptAsInt("value[1]=5;value[1]", list));
assertEquals(5, list.get(1).intValue());
// setting values in lists will set them as double
assertEquals("5.0", runScriptAsString("value[1]=5;value[1]", list));
assertEquals(5.0, list.get(1));
}

public void testAccessingJavaListStringValues() {
Expand All @@ -108,6 +114,76 @@ public void testUpdatingJavaListStringValues() {
assertEquals("f", list.get(1));
}

public void testAutoGrow() {
List<String> list = new ArrayList<>();
// Object list = runScript("[]", null, Function.identity());
assertEquals(0, runScriptAsInt("value.length", list));
assertEquals(1, runScriptAsInt("value[0]='a'; value.length", list));
assertEquals(3, runScriptAsInt("value[2]='c'; value.length", list));
assertEquals("a", runScriptAsString("value[0]", list));
// NativeArray will have 'undefined' here.
assertEquals("null", runScriptAsString("value[1]", list));
assertEquals("c", runScriptAsString("value[2]", list));
// NativeList will return "a,,c"
assertEquals("a,,c", runScriptAsString("Array.prototype.join.call(value)", list));
}

public void testLength() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
runScriptAsString("value.length = 0", list);
assertEquals(0, list.size());
runScriptAsString("value.length = 10", list);
assertEquals(10, list.size());

try {
runScriptAsString("value.length = -10", list);
fail();
} catch (EcmaError e) {
assertEquals("RangeError: Inappropriate array length. (#1)", e.getMessage());
}

try {
runScriptAsString("value.length = 2.1", list);
fail();
} catch (EcmaError e) {
assertEquals("RangeError: Inappropriate array length. (#1)", e.getMessage());
}

try {
runScriptAsString("value.length = 2147483648", list); // Integer.MAX_VALUE + 1
fail();
} catch (EcmaError e) {
assertEquals("RangeError: Inappropriate array length. (#1)", e.getMessage());
}
}

public void testDelete() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
// Object list = runScript("['a','b','c']", null, Function.identity());
// TODO: should NativeJavaList distinguish between 'null' and 'undefined'?
assertEquals("false", runScriptAsString("delete value[1]", list));
assertEquals("a,,c", runScriptAsString("Array.prototype.join.call(value)", list));
}

public void testAdd() {
List<String> list = new ArrayList<>();
runScriptAsString("value[0] = 'a'", list);
runScriptAsString("value[1] = 'b'", list);
runScriptAsString("value[2] = 'c'", list);
assertEquals("[a, b, c]", list.toString());
runScriptAsString("value[5] = 'f'", list);
assertEquals("[a, b, c, null, null, f]", list.toString());
runScriptAsString("value[4] = 'e'", list);
runScriptAsString("value[3] = 'd'", list);
assertEquals("[a, b, c, d, e, f]", list.toString());
}

public void testKeys() {
List<String> list = new ArrayList<>();
NativeArray resEmpty =
Expand Down