# HG changeset patch # User attila # Date 1369303295 -7200 # Node ID 1c1453863ea889f8bec47e3a9d9f90802382d03e # Parent 8f7553df4503c5cd800c6c5196c2295bd50f2255 8015267: Allow conversion of JS arrays to Java List/Deque Reviewed-by: lagergren, sundar diff -r 8f7553df4503 -r 1c1453863ea8 make/build.xml --- a/make/build.xml Wed May 22 16:43:48 2013 +0200 +++ b/make/build.xml Thu May 23 12:01:35 2013 +0200 @@ -42,8 +42,6 @@ - - @@ -80,19 +78,7 @@ - - - - - - + * var anArray = [1, "13", false] * var javaIntArray = Java.to(anArray, "int[]") @@ -250,42 +253,46 @@ * print(javaIntArray[2]) // prints 0, as boolean false was converted to number 0 as per ECMAScript ToNumber conversion * * @param self not used - * @param objArray the script object. Can be null. + * @param obj the script object. Can be null. * @param objType either a {@link #type(Object, Object) type object} or a String describing the type of the Java * object to create. Can not be null. If undefined, a "default" conversion is presumed (allowing the argument to be * omitted). * @return a Java object whose value corresponds to the original script object's value. Specifically, for array * target types, returns a Java array of the same type with contents converted to the array's component type. Does - * not recursively convert for multidimensional arrays. - * type. Returns null if scriptObject is null. + * not recursively convert for multidimensional arrays. For {@link List} or {@link Deque}, returns a live wrapper + * around the object, see {@link ListAdapter} for details. Returns null if obj is null. * @throws ClassNotFoundException if the class described by objType is not found */ @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) - public static Object to(final Object self, final Object objArray, final Object objType) throws ClassNotFoundException { - if (objArray == null) { + public static Object to(final Object self, final Object obj, final Object objType) throws ClassNotFoundException { + if (obj == null) { return null; } - final Class componentType; + Global.checkObject(obj); + + final Class targetClass; if(objType == UNDEFINED) { - componentType = Object.class; + targetClass = Object[].class; } else { - final StaticClass arrayType; + final StaticClass targetType; if(objType instanceof StaticClass) { - arrayType = (StaticClass)objType; + targetType = (StaticClass)objType; } else { - arrayType = type(objType); + targetType = type(objType); } - final Class arrayClass = arrayType.getRepresentedClass(); - if(!arrayClass.isArray()) { - throw typeError("to.expects.array.type", arrayClass.getName()); - } - componentType = arrayClass.getComponentType(); + targetClass = targetType.getRepresentedClass(); } - Global.checkObject(objArray); + if(targetClass.isArray()) { + return ((ScriptObject)obj).getArray().asArrayOfType(targetClass.getComponentType()); + } - return ((ScriptObject)objArray).getArray().asArrayOfType(componentType); + if(targetClass == List.class || targetClass == Deque.class) { + return new ListAdapter((ScriptObject)obj); + } + + throw typeError("unsupported.java.to.type", targetClass.getName()); } /** diff -r 8f7553df4503 -r 1c1453863ea8 src/jdk/nashorn/internal/runtime/ListAdapter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk/nashorn/internal/runtime/ListAdapter.java Thu May 23 12:01:35 2013 +0200 @@ -0,0 +1,337 @@ +package jdk.nashorn.internal.runtime; + +import java.util.AbstractList; +import java.util.Deque; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import jdk.nashorn.internal.runtime.linker.InvokeByName; + +/** + * An adapter that can wrap any ECMAScript Array-like object (that adheres to the array rules for the property + * {@code length} and having conforming {@code push}, {@code pop}, {@code shift}, {@code unshift}, and {@code splice} + * methods) and expose it as both a Java list and double-ended queue. While script arrays aren't necessarily efficient + * as dequeues, it's still slightly more efficient to be able to translate dequeue operations into pushes, pops, shifts, + * and unshifts, than to blindly translate all list's add/remove operations into splices. Also, it is conceivable that a + * custom script object that implements an Array-like API can have a background data representation that is optimized + * for dequeue-like access. Note that with ECMAScript arrays, {@code push} and {@pop} operate at the end of the array, + * while in Java {@code Deque} they operate on the front of the queue and as such the Java dequeue {@link #push(Object)} + * and {@link #pop()} operations will translate to {@code unshift} and {@code shift} script operations respectively, + * while {@link #addLast(Object)} and {@link #removeLast()} will translate to {@code push} and {@code pop}. + */ +public class ListAdapter extends AbstractList implements RandomAccess, Deque { + // These add to the back and front of the list + private static final InvokeByName PUSH = new InvokeByName("push", ScriptObject.class, void.class, Object.class); + private static final InvokeByName UNSHIFT = new InvokeByName("unshift", ScriptObject.class, void.class, Object.class); + + // These remove from the back and front of the list + private static final InvokeByName POP = new InvokeByName("pop", ScriptObject.class, Object.class); + private static final InvokeByName SHIFT = new InvokeByName("shift", ScriptObject.class, Object.class); + + // These insert and remove in the middle of the list + private static final InvokeByName SPLICE_ADD = new InvokeByName("splice", ScriptObject.class, void.class, int.class, int.class, Object.class); + private static final InvokeByName SPLICE_REMOVE = new InvokeByName("splice", ScriptObject.class, void.class, int.class, int.class); + + private final ScriptObject obj; + + /** + * Creates a new list wrapper for the specified script object. + * @param obj script the object to wrap + */ + public ListAdapter(ScriptObject obj) { + this.obj = obj; + } + + @Override + public int size() { + return JSType.toInt32(obj.getLength()); + } + + @Override + public Object get(int index) { + checkRange(index); + return obj.get(index); + } + + @Override + public Object set(int index, Object element) { + checkRange(index); + final Object prevValue = get(index); + obj.set(index, element, false); + return prevValue; + } + + private void checkRange(int index) { + if(index < 0 || index >= size()) { + throw invalidIndex(index); + } + } + + @Override + public void push(Object e) { + addFirst(e); + } + + @Override + public boolean add(Object e) { + addLast(e); + return true; + } + + @Override + public void addFirst(Object e) { + try { + final Object fn = UNSHIFT.getGetter().invokeExact(obj); + checkFunction(fn, UNSHIFT); + UNSHIFT.getInvoker().invokeExact(fn, obj, e); + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public void addLast(Object e) { + try { + final Object fn = PUSH.getGetter().invokeExact(obj); + checkFunction(fn, PUSH); + PUSH.getInvoker().invokeExact(fn, obj, e); + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public void add(int index, Object e) { + try { + if(index < 0) { + throw invalidIndex(index); + } else if(index == 0) { + addFirst(e); + } else { + final int size = size(); + if(index < size) { + final Object fn = SPLICE_ADD.getGetter().invokeExact(obj); + checkFunction(fn, SPLICE_ADD); + SPLICE_ADD.getInvoker().invokeExact(fn, obj, index, 0, e); + } else if(index == size) { + addLast(e); + } else { + throw invalidIndex(index); + } + } + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + private static void checkFunction(Object fn, InvokeByName invoke) { + if(!(fn instanceof ScriptFunction)) { + throw new UnsupportedOperationException("The script object doesn't have a function named " + invoke.getName()); + } + } + + private static IndexOutOfBoundsException invalidIndex(int index) { + return new IndexOutOfBoundsException(String.valueOf(index)); + } + + @Override + public boolean offer(Object e) { + return offerLast(e); + } + + @Override + public boolean offerFirst(Object e) { + addFirst(e); + return true; + } + + @Override + public boolean offerLast(Object e) { + addLast(e); + return true; + } + + @Override + public Object pop() { + return removeFirst(); + } + + @Override + public Object remove() { + return removeFirst(); + } + + @Override + public Object removeFirst() { + checkNonEmpty(); + return invokeShift(); + } + + @Override + public Object removeLast() { + checkNonEmpty(); + return invokePop(); + } + + private void checkNonEmpty() { + if(isEmpty()) { + throw new NoSuchElementException(); + } + } + + @Override + public Object remove(int index) { + if(index < 0) { + throw invalidIndex(index); + } else if (index == 0) { + return invokeShift(); + } else { + final int maxIndex = size() - 1; + if(index < maxIndex) { + final Object prevValue = get(index); + invokeSpliceRemove(index, 1); + return prevValue; + } else if(index == maxIndex) { + return invokePop(); + } else { + throw invalidIndex(index); + } + } + } + + private Object invokeShift() { + try { + final Object fn = SHIFT.getGetter().invokeExact(obj); + checkFunction(fn, SHIFT); + return SHIFT.getInvoker().invokeExact(fn, obj); + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + + private Object invokePop() { + try { + final Object fn = POP.getGetter().invokeExact(obj); + checkFunction(fn, POP); + return POP.getInvoker().invokeExact(fn, obj); + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + invokeSpliceRemove(fromIndex, toIndex - fromIndex); + } + + private void invokeSpliceRemove(int fromIndex, int count) { + try { + final Object fn = SPLICE_REMOVE.getGetter().invokeExact(obj); + checkFunction(fn, SPLICE_REMOVE); + SPLICE_REMOVE.getInvoker().invokeExact(fn, obj, fromIndex, count); + } catch(RuntimeException | Error ex) { + throw ex; + } catch(Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public Object poll() { + return pollFirst(); + } + + @Override + public Object pollFirst() { + return isEmpty() ? null : invokeShift(); + } + + @Override + public Object pollLast() { + return isEmpty() ? null : invokePop(); + } + + @Override + public Object peek() { + return peekFirst(); + } + + @Override + public Object peekFirst() { + return isEmpty() ? null : get(0); + } + + @Override + public Object peekLast() { + return isEmpty() ? null : get(size() - 1); + } + + @Override + public Object element() { + return getFirst(); + } + + @Override + public Object getFirst() { + checkNonEmpty(); + return get(0); + } + + @Override + public Object getLast() { + checkNonEmpty(); + return get(size() - 1); + } + + @Override + public Iterator descendingIterator() { + final ListIterator it = listIterator(size()); + return new Iterator() { + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + + @Override + public Object next() { + return it.previous(); + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + @Override + public boolean removeFirstOccurrence(Object o) { + return removeOccurrence(o, iterator()); + } + + @Override + public boolean removeLastOccurrence(Object o) { + return removeOccurrence(o, descendingIterator()); + } + + private static boolean removeOccurrence(Object o, Iterator it) { + while(it.hasNext()) { + final Object e = it.next(); + if(o == null ? e == null : o.equals(e)) { + it.remove(); + return true; + } + } + return false; + } +} diff -r 8f7553df4503 -r 1c1453863ea8 src/jdk/nashorn/internal/runtime/linker/InvokeByName.java --- a/src/jdk/nashorn/internal/runtime/linker/InvokeByName.java Wed May 22 16:43:48 2013 +0200 +++ b/src/jdk/nashorn/internal/runtime/linker/InvokeByName.java Thu May 23 12:01:35 2013 +0200 @@ -40,7 +40,7 @@ * private static final InvokeByName TO_JSON = new InvokeByName("toJSON", Object.class, Object.class, Object.class); * ... * final Object toJSONFn = TO_JSON.getGetter().invokeExact(obj); - * value = TO_JSON.getInvoker().invokeExact(toJSON, obj, key); + * value = TO_JSON.getInvoker().invokeExact(toJSONFn, obj, key); * * In practice, you can have stronger type assumptions if it makes sense for your code, just remember that you must use * the same parameter types as the formal types of the arguments for {@code invokeExact} to work: @@ -50,7 +50,7 @@ * final ScriptObject sobj = (ScriptObject)obj; * final Object toJSONFn = TO_JSON.getGetter().invokeExact(sobj); * if(toJSONFn instanceof ScriptFunction) { - * value = TO_JSON.getInvoker().invokeExact(toJSON, sobj, key); + * value = TO_JSON.getInvoker().invokeExact(toJSONFn, sobj, key); * } * * Note that in general you will not want to reuse a single instance of this class for implementing more than one call @@ -59,6 +59,7 @@ * separate instance of this class for every place. */ public class InvokeByName { + private final String name; private final MethodHandle getter; private final MethodHandle invoker; @@ -81,6 +82,7 @@ * @param ptypes the parameter types of the function. */ public InvokeByName(final String name, final Class targetClass, final Class rtype, final Class... ptypes) { + this.name = name; getter = Bootstrap.createDynamicInvoker("dyn:getMethod|getProp|getItem:" + name, Object.class, targetClass); final Class[] finalPtypes; @@ -97,6 +99,14 @@ } /** + * Returns the name of the function retrieved through this invoker. + * @return the name of the function retrieved through this invoker. + */ + public String getName() { + return name; + } + + /** * Returns the property getter that can be invoked on an object to retrieve the function object that will be * subsequently invoked by the invoker returned by {@link #getInvoker()}. * @return the property getter method handle for the function. diff -r 8f7553df4503 -r 1c1453863ea8 src/jdk/nashorn/internal/runtime/resources/Messages.properties --- a/src/jdk/nashorn/internal/runtime/resources/Messages.properties Wed May 22 16:43:48 2013 +0200 +++ b/src/jdk/nashorn/internal/runtime/resources/Messages.properties Thu May 23 12:01:35 2013 +0200 @@ -125,7 +125,7 @@ type.error.no.method.matches.args=Can not invoke method {0} with the passed arguments; they do not match any of its method signatures. type.error.method.not.constructor=Java method {0} can't be used as a constructor. type.error.env.not.object=$ENV must be an Object. -type.error.to.expects.array.type=Java.to() expects an array target type. {0} is not an array type. +type.error.unsupported.java.to.type=Unsupported Java.to target type {0}. range.error.inappropriate.array.length=inappropriate array length: {0} range.error.invalid.fraction.digits=fractionDigits argument to {0} must be in [0, 20] range.error.invalid.precision=precision argument toPrecision() must be in [1, 21] diff -r 8f7553df4503 -r 1c1453863ea8 test/script/basic/JDK-8015267.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/script/basic/JDK-8015267.js Thu May 23 12:01:35 2013 +0200 @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * JDK-8015267: have a List/Deque adapter for JS array-like objects + * + * @test + * @run + */ + +var a = ['a', 'b', 'c', 'd'] + +var l = Java.to(a, java.util.List) +print(l instanceof java.util.List) +print(l instanceof java.util.Deque) + +print(l[0]) +print(l[1]) +print(l[2]) +print(l[3]) + +print(l.size()) + +l.push('x') +print(a) + +l.addLast('y') +print(a) + +print(l.pop()) +print(l.removeLast()) +print(a) + +l.add('e') +l.add(5, 'f') +print(a) + +l.add(0, 'z') +print(a) + +l.add(2, 'x') +print(a) + +l[7] = 'g' +print(a) + +try { l.add(15, '') } catch(e) { print(e.class) } +try { l.remove(15) } catch(e) { print(e.class) } +try { l.add(-1, '') } catch(e) { print(e.class) } +try { l.remove(-1) } catch(e) { print(e.class) } + +l.remove(7) +l.remove(2) +l.remove(0) +print(a) + +print(l.peek()) +print(l.peekFirst()) +print(l.peekLast()) + +print(l.element()) +print(l.getFirst()) +print(l.getLast()) + +l.offer('1') +l.offerFirst('2') +l.offerLast('3') +print(a) + +a = ['1', '2', 'x', '3', '4', 'x', '5', '6', 'x', '7', '8'] +print(a) +var l = Java.to(a, java.util.List) +l.removeFirstOccurrence('x') +print(a) +l.removeLastOccurrence('x') +print(a) + +var empty = Java.to([], java.util.List) +try { empty.pop() } catch(e) { print(e.class) } +try { empty.removeFirst() } catch(e) { print(e.class) } +try { empty.removeLast() } catch(e) { print(e.class) } + +try { empty.element() } catch(e) { print(e.class) } +try { empty.getFirst() } catch(e) { print(e.class) } +try { empty.getLast() } catch(e) { print(e.class) } + +print(empty.peek()) +print(empty.peekFirst()) +print(empty.peekLast()) diff -r 8f7553df4503 -r 1c1453863ea8 test/script/basic/JDK-8015267.js.EXPECTED --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/script/basic/JDK-8015267.js.EXPECTED Thu May 23 12:01:35 2013 +0200 @@ -0,0 +1,40 @@ +true +true +a +b +c +d +4 +x,a,b,c,d +x,a,b,c,d,y +x +y +a,b,c,d +a,b,c,d,e,f +z,a,b,c,d,e,f +z,a,x,b,c,d,e,f +z,a,x,b,c,d,e,g +class java.lang.IndexOutOfBoundsException +class java.lang.IndexOutOfBoundsException +class java.lang.IndexOutOfBoundsException +class java.lang.IndexOutOfBoundsException +a,b,c,d,e +a +a +e +a +a +e +2,a,b,c,d,e,1,3 +1,2,x,3,4,x,5,6,x,7,8 +1,2,3,4,x,5,6,x,7,8 +1,2,3,4,x,5,6,7,8 +class java.util.NoSuchElementException +class java.util.NoSuchElementException +class java.util.NoSuchElementException +class java.util.NoSuchElementException +class java.util.NoSuchElementException +class java.util.NoSuchElementException +null +null +null