Thu, 23 May 2013 12:01:35 +0200
8015267: Allow conversion of JS arrays to Java List/Deque
Reviewed-by: lagergren, sundar
attila@286 | 1 | package jdk.nashorn.internal.runtime; |
attila@286 | 2 | |
attila@286 | 3 | import java.util.AbstractList; |
attila@286 | 4 | import java.util.Deque; |
attila@286 | 5 | import java.util.Iterator; |
attila@286 | 6 | import java.util.ListIterator; |
attila@286 | 7 | import java.util.NoSuchElementException; |
attila@286 | 8 | import java.util.RandomAccess; |
attila@286 | 9 | import jdk.nashorn.internal.runtime.linker.InvokeByName; |
attila@286 | 10 | |
attila@286 | 11 | /** |
attila@286 | 12 | * An adapter that can wrap any ECMAScript Array-like object (that adheres to the array rules for the property |
attila@286 | 13 | * {@code length} and having conforming {@code push}, {@code pop}, {@code shift}, {@code unshift}, and {@code splice} |
attila@286 | 14 | * methods) and expose it as both a Java list and double-ended queue. While script arrays aren't necessarily efficient |
attila@286 | 15 | * as dequeues, it's still slightly more efficient to be able to translate dequeue operations into pushes, pops, shifts, |
attila@286 | 16 | * and unshifts, than to blindly translate all list's add/remove operations into splices. Also, it is conceivable that a |
attila@286 | 17 | * custom script object that implements an Array-like API can have a background data representation that is optimized |
attila@286 | 18 | * for dequeue-like access. Note that with ECMAScript arrays, {@code push} and {@pop} operate at the end of the array, |
attila@286 | 19 | * while in Java {@code Deque} they operate on the front of the queue and as such the Java dequeue {@link #push(Object)} |
attila@286 | 20 | * and {@link #pop()} operations will translate to {@code unshift} and {@code shift} script operations respectively, |
attila@286 | 21 | * while {@link #addLast(Object)} and {@link #removeLast()} will translate to {@code push} and {@code pop}. |
attila@286 | 22 | */ |
attila@286 | 23 | public class ListAdapter extends AbstractList<Object> implements RandomAccess, Deque<Object> { |
attila@286 | 24 | // These add to the back and front of the list |
attila@286 | 25 | private static final InvokeByName PUSH = new InvokeByName("push", ScriptObject.class, void.class, Object.class); |
attila@286 | 26 | private static final InvokeByName UNSHIFT = new InvokeByName("unshift", ScriptObject.class, void.class, Object.class); |
attila@286 | 27 | |
attila@286 | 28 | // These remove from the back and front of the list |
attila@286 | 29 | private static final InvokeByName POP = new InvokeByName("pop", ScriptObject.class, Object.class); |
attila@286 | 30 | private static final InvokeByName SHIFT = new InvokeByName("shift", ScriptObject.class, Object.class); |
attila@286 | 31 | |
attila@286 | 32 | // These insert and remove in the middle of the list |
attila@286 | 33 | private static final InvokeByName SPLICE_ADD = new InvokeByName("splice", ScriptObject.class, void.class, int.class, int.class, Object.class); |
attila@286 | 34 | private static final InvokeByName SPLICE_REMOVE = new InvokeByName("splice", ScriptObject.class, void.class, int.class, int.class); |
attila@286 | 35 | |
attila@286 | 36 | private final ScriptObject obj; |
attila@286 | 37 | |
attila@286 | 38 | /** |
attila@286 | 39 | * Creates a new list wrapper for the specified script object. |
attila@286 | 40 | * @param obj script the object to wrap |
attila@286 | 41 | */ |
attila@286 | 42 | public ListAdapter(ScriptObject obj) { |
attila@286 | 43 | this.obj = obj; |
attila@286 | 44 | } |
attila@286 | 45 | |
attila@286 | 46 | @Override |
attila@286 | 47 | public int size() { |
attila@286 | 48 | return JSType.toInt32(obj.getLength()); |
attila@286 | 49 | } |
attila@286 | 50 | |
attila@286 | 51 | @Override |
attila@286 | 52 | public Object get(int index) { |
attila@286 | 53 | checkRange(index); |
attila@286 | 54 | return obj.get(index); |
attila@286 | 55 | } |
attila@286 | 56 | |
attila@286 | 57 | @Override |
attila@286 | 58 | public Object set(int index, Object element) { |
attila@286 | 59 | checkRange(index); |
attila@286 | 60 | final Object prevValue = get(index); |
attila@286 | 61 | obj.set(index, element, false); |
attila@286 | 62 | return prevValue; |
attila@286 | 63 | } |
attila@286 | 64 | |
attila@286 | 65 | private void checkRange(int index) { |
attila@286 | 66 | if(index < 0 || index >= size()) { |
attila@286 | 67 | throw invalidIndex(index); |
attila@286 | 68 | } |
attila@286 | 69 | } |
attila@286 | 70 | |
attila@286 | 71 | @Override |
attila@286 | 72 | public void push(Object e) { |
attila@286 | 73 | addFirst(e); |
attila@286 | 74 | } |
attila@286 | 75 | |
attila@286 | 76 | @Override |
attila@286 | 77 | public boolean add(Object e) { |
attila@286 | 78 | addLast(e); |
attila@286 | 79 | return true; |
attila@286 | 80 | } |
attila@286 | 81 | |
attila@286 | 82 | @Override |
attila@286 | 83 | public void addFirst(Object e) { |
attila@286 | 84 | try { |
attila@286 | 85 | final Object fn = UNSHIFT.getGetter().invokeExact(obj); |
attila@286 | 86 | checkFunction(fn, UNSHIFT); |
attila@286 | 87 | UNSHIFT.getInvoker().invokeExact(fn, obj, e); |
attila@286 | 88 | } catch(RuntimeException | Error ex) { |
attila@286 | 89 | throw ex; |
attila@286 | 90 | } catch(Throwable t) { |
attila@286 | 91 | throw new RuntimeException(t); |
attila@286 | 92 | } |
attila@286 | 93 | } |
attila@286 | 94 | |
attila@286 | 95 | @Override |
attila@286 | 96 | public void addLast(Object e) { |
attila@286 | 97 | try { |
attila@286 | 98 | final Object fn = PUSH.getGetter().invokeExact(obj); |
attila@286 | 99 | checkFunction(fn, PUSH); |
attila@286 | 100 | PUSH.getInvoker().invokeExact(fn, obj, e); |
attila@286 | 101 | } catch(RuntimeException | Error ex) { |
attila@286 | 102 | throw ex; |
attila@286 | 103 | } catch(Throwable t) { |
attila@286 | 104 | throw new RuntimeException(t); |
attila@286 | 105 | } |
attila@286 | 106 | } |
attila@286 | 107 | |
attila@286 | 108 | @Override |
attila@286 | 109 | public void add(int index, Object e) { |
attila@286 | 110 | try { |
attila@286 | 111 | if(index < 0) { |
attila@286 | 112 | throw invalidIndex(index); |
attila@286 | 113 | } else if(index == 0) { |
attila@286 | 114 | addFirst(e); |
attila@286 | 115 | } else { |
attila@286 | 116 | final int size = size(); |
attila@286 | 117 | if(index < size) { |
attila@286 | 118 | final Object fn = SPLICE_ADD.getGetter().invokeExact(obj); |
attila@286 | 119 | checkFunction(fn, SPLICE_ADD); |
attila@286 | 120 | SPLICE_ADD.getInvoker().invokeExact(fn, obj, index, 0, e); |
attila@286 | 121 | } else if(index == size) { |
attila@286 | 122 | addLast(e); |
attila@286 | 123 | } else { |
attila@286 | 124 | throw invalidIndex(index); |
attila@286 | 125 | } |
attila@286 | 126 | } |
attila@286 | 127 | } catch(RuntimeException | Error ex) { |
attila@286 | 128 | throw ex; |
attila@286 | 129 | } catch(Throwable t) { |
attila@286 | 130 | throw new RuntimeException(t); |
attila@286 | 131 | } |
attila@286 | 132 | } |
attila@286 | 133 | private static void checkFunction(Object fn, InvokeByName invoke) { |
attila@286 | 134 | if(!(fn instanceof ScriptFunction)) { |
attila@286 | 135 | throw new UnsupportedOperationException("The script object doesn't have a function named " + invoke.getName()); |
attila@286 | 136 | } |
attila@286 | 137 | } |
attila@286 | 138 | |
attila@286 | 139 | private static IndexOutOfBoundsException invalidIndex(int index) { |
attila@286 | 140 | return new IndexOutOfBoundsException(String.valueOf(index)); |
attila@286 | 141 | } |
attila@286 | 142 | |
attila@286 | 143 | @Override |
attila@286 | 144 | public boolean offer(Object e) { |
attila@286 | 145 | return offerLast(e); |
attila@286 | 146 | } |
attila@286 | 147 | |
attila@286 | 148 | @Override |
attila@286 | 149 | public boolean offerFirst(Object e) { |
attila@286 | 150 | addFirst(e); |
attila@286 | 151 | return true; |
attila@286 | 152 | } |
attila@286 | 153 | |
attila@286 | 154 | @Override |
attila@286 | 155 | public boolean offerLast(Object e) { |
attila@286 | 156 | addLast(e); |
attila@286 | 157 | return true; |
attila@286 | 158 | } |
attila@286 | 159 | |
attila@286 | 160 | @Override |
attila@286 | 161 | public Object pop() { |
attila@286 | 162 | return removeFirst(); |
attila@286 | 163 | } |
attila@286 | 164 | |
attila@286 | 165 | @Override |
attila@286 | 166 | public Object remove() { |
attila@286 | 167 | return removeFirst(); |
attila@286 | 168 | } |
attila@286 | 169 | |
attila@286 | 170 | @Override |
attila@286 | 171 | public Object removeFirst() { |
attila@286 | 172 | checkNonEmpty(); |
attila@286 | 173 | return invokeShift(); |
attila@286 | 174 | } |
attila@286 | 175 | |
attila@286 | 176 | @Override |
attila@286 | 177 | public Object removeLast() { |
attila@286 | 178 | checkNonEmpty(); |
attila@286 | 179 | return invokePop(); |
attila@286 | 180 | } |
attila@286 | 181 | |
attila@286 | 182 | private void checkNonEmpty() { |
attila@286 | 183 | if(isEmpty()) { |
attila@286 | 184 | throw new NoSuchElementException(); |
attila@286 | 185 | } |
attila@286 | 186 | } |
attila@286 | 187 | |
attila@286 | 188 | @Override |
attila@286 | 189 | public Object remove(int index) { |
attila@286 | 190 | if(index < 0) { |
attila@286 | 191 | throw invalidIndex(index); |
attila@286 | 192 | } else if (index == 0) { |
attila@286 | 193 | return invokeShift(); |
attila@286 | 194 | } else { |
attila@286 | 195 | final int maxIndex = size() - 1; |
attila@286 | 196 | if(index < maxIndex) { |
attila@286 | 197 | final Object prevValue = get(index); |
attila@286 | 198 | invokeSpliceRemove(index, 1); |
attila@286 | 199 | return prevValue; |
attila@286 | 200 | } else if(index == maxIndex) { |
attila@286 | 201 | return invokePop(); |
attila@286 | 202 | } else { |
attila@286 | 203 | throw invalidIndex(index); |
attila@286 | 204 | } |
attila@286 | 205 | } |
attila@286 | 206 | } |
attila@286 | 207 | |
attila@286 | 208 | private Object invokeShift() { |
attila@286 | 209 | try { |
attila@286 | 210 | final Object fn = SHIFT.getGetter().invokeExact(obj); |
attila@286 | 211 | checkFunction(fn, SHIFT); |
attila@286 | 212 | return SHIFT.getInvoker().invokeExact(fn, obj); |
attila@286 | 213 | } catch(RuntimeException | Error ex) { |
attila@286 | 214 | throw ex; |
attila@286 | 215 | } catch(Throwable t) { |
attila@286 | 216 | throw new RuntimeException(t); |
attila@286 | 217 | } |
attila@286 | 218 | } |
attila@286 | 219 | |
attila@286 | 220 | private Object invokePop() { |
attila@286 | 221 | try { |
attila@286 | 222 | final Object fn = POP.getGetter().invokeExact(obj); |
attila@286 | 223 | checkFunction(fn, POP); |
attila@286 | 224 | return POP.getInvoker().invokeExact(fn, obj); |
attila@286 | 225 | } catch(RuntimeException | Error ex) { |
attila@286 | 226 | throw ex; |
attila@286 | 227 | } catch(Throwable t) { |
attila@286 | 228 | throw new RuntimeException(t); |
attila@286 | 229 | } |
attila@286 | 230 | } |
attila@286 | 231 | |
attila@286 | 232 | @Override |
attila@286 | 233 | protected void removeRange(int fromIndex, int toIndex) { |
attila@286 | 234 | invokeSpliceRemove(fromIndex, toIndex - fromIndex); |
attila@286 | 235 | } |
attila@286 | 236 | |
attila@286 | 237 | private void invokeSpliceRemove(int fromIndex, int count) { |
attila@286 | 238 | try { |
attila@286 | 239 | final Object fn = SPLICE_REMOVE.getGetter().invokeExact(obj); |
attila@286 | 240 | checkFunction(fn, SPLICE_REMOVE); |
attila@286 | 241 | SPLICE_REMOVE.getInvoker().invokeExact(fn, obj, fromIndex, count); |
attila@286 | 242 | } catch(RuntimeException | Error ex) { |
attila@286 | 243 | throw ex; |
attila@286 | 244 | } catch(Throwable t) { |
attila@286 | 245 | throw new RuntimeException(t); |
attila@286 | 246 | } |
attila@286 | 247 | } |
attila@286 | 248 | |
attila@286 | 249 | @Override |
attila@286 | 250 | public Object poll() { |
attila@286 | 251 | return pollFirst(); |
attila@286 | 252 | } |
attila@286 | 253 | |
attila@286 | 254 | @Override |
attila@286 | 255 | public Object pollFirst() { |
attila@286 | 256 | return isEmpty() ? null : invokeShift(); |
attila@286 | 257 | } |
attila@286 | 258 | |
attila@286 | 259 | @Override |
attila@286 | 260 | public Object pollLast() { |
attila@286 | 261 | return isEmpty() ? null : invokePop(); |
attila@286 | 262 | } |
attila@286 | 263 | |
attila@286 | 264 | @Override |
attila@286 | 265 | public Object peek() { |
attila@286 | 266 | return peekFirst(); |
attila@286 | 267 | } |
attila@286 | 268 | |
attila@286 | 269 | @Override |
attila@286 | 270 | public Object peekFirst() { |
attila@286 | 271 | return isEmpty() ? null : get(0); |
attila@286 | 272 | } |
attila@286 | 273 | |
attila@286 | 274 | @Override |
attila@286 | 275 | public Object peekLast() { |
attila@286 | 276 | return isEmpty() ? null : get(size() - 1); |
attila@286 | 277 | } |
attila@286 | 278 | |
attila@286 | 279 | @Override |
attila@286 | 280 | public Object element() { |
attila@286 | 281 | return getFirst(); |
attila@286 | 282 | } |
attila@286 | 283 | |
attila@286 | 284 | @Override |
attila@286 | 285 | public Object getFirst() { |
attila@286 | 286 | checkNonEmpty(); |
attila@286 | 287 | return get(0); |
attila@286 | 288 | } |
attila@286 | 289 | |
attila@286 | 290 | @Override |
attila@286 | 291 | public Object getLast() { |
attila@286 | 292 | checkNonEmpty(); |
attila@286 | 293 | return get(size() - 1); |
attila@286 | 294 | } |
attila@286 | 295 | |
attila@286 | 296 | @Override |
attila@286 | 297 | public Iterator<Object> descendingIterator() { |
attila@286 | 298 | final ListIterator<Object> it = listIterator(size()); |
attila@286 | 299 | return new Iterator<Object>() { |
attila@286 | 300 | @Override |
attila@286 | 301 | public boolean hasNext() { |
attila@286 | 302 | return it.hasPrevious(); |
attila@286 | 303 | } |
attila@286 | 304 | |
attila@286 | 305 | @Override |
attila@286 | 306 | public Object next() { |
attila@286 | 307 | return it.previous(); |
attila@286 | 308 | } |
attila@286 | 309 | |
attila@286 | 310 | @Override |
attila@286 | 311 | public void remove() { |
attila@286 | 312 | it.remove(); |
attila@286 | 313 | } |
attila@286 | 314 | }; |
attila@286 | 315 | } |
attila@286 | 316 | |
attila@286 | 317 | @Override |
attila@286 | 318 | public boolean removeFirstOccurrence(Object o) { |
attila@286 | 319 | return removeOccurrence(o, iterator()); |
attila@286 | 320 | } |
attila@286 | 321 | |
attila@286 | 322 | @Override |
attila@286 | 323 | public boolean removeLastOccurrence(Object o) { |
attila@286 | 324 | return removeOccurrence(o, descendingIterator()); |
attila@286 | 325 | } |
attila@286 | 326 | |
attila@286 | 327 | private static boolean removeOccurrence(Object o, Iterator<Object> it) { |
attila@286 | 328 | while(it.hasNext()) { |
attila@286 | 329 | final Object e = it.next(); |
attila@286 | 330 | if(o == null ? e == null : o.equals(e)) { |
attila@286 | 331 | it.remove(); |
attila@286 | 332 | return true; |
attila@286 | 333 | } |
attila@286 | 334 | } |
attila@286 | 335 | return false; |
attila@286 | 336 | } |
attila@286 | 337 | } |