/*
* Scriptographer
*
* This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator
* http://scriptographer.org/
*
* Copyright (c) 2002-2010, Juerg Lehni
* http://scratchdisk.com/
*
* All rights reserved. See LICENSE file for details.
*
* File created on 11.02.2005.
*/
package com.scratchdisk.script.rhino;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Wrapper;
import com.scratchdisk.list.List;
import com.scratchdisk.list.ReadOnlyList;
import com.scratchdisk.list.ReadOnlyStringIndexList;
import com.scratchdisk.list.StringIndexList;
import com.scratchdisk.script.ChangeReceiver;
/**
* Wrapper class for com.scriptographer.util.List objects to make them behave
* like NativeArray. The prototype is set to the js Array prototype, so all
* Array functions can be used on list objects.
*
* Unlike MapWrapper, this is still an ExtendedJavaObject at the same time, so
* native methods get passed through too. The reasoning behind this is that in
* Map<->Object wrapping, such methods could interfere with the expected
* behavior of a native Object (e.g. script.preferences.values returning the
* values() method from Map), whereas on lists, such clashes are not going to
* happen, and right now list classes still often define other functionality in
* Scriptographer, e.g. HierarchyListBox in the UI framework.
*
* TODO: In the future, this should be handled in the same way as MapAdapter,
* making the list behave like a 100% native array.
*
* It adds array-like properties, so it is possible to access lists like this:
* list[i] It also defines getIds(), so enumeration is possible too: for (var i
* in list) ...
*
* @author lehni
*/
public class ListWrapper extends ExtendedJavaObject {
public ListWrapper(Scriptable scope, ReadOnlyList list,
Class staticType, boolean unsealed) {
super(scope, list, staticType, unsealed);
// Make ListWrappers behave exactly like arrays and inherit all features
// from them.
// Thanks to the great functionality of our ExtendedJavaObject, there is
// no difference between a java List and a JavaScript Array after this,
// all features work as long as the list is not read-only!
setPrototype(ScriptableObject.getArrayPrototype(scope));
}
public Object[] getIds() {
if (javaObject != null) {
// act like a JS javaObject:
Integer[] ids = new Integer[((ReadOnlyList) javaObject).size()];
for (int i = 0; i < ids.length; i++) {
ids[i] = new Integer(i);
}
return ids;
} else {
return new Object[] {};
}
}
public boolean has(int index, Scriptable start) {
return javaObject != null && index < ((ReadOnlyList) javaObject).size();
}
@SuppressWarnings("unchecked")
public void put(int index, Scriptable start, Object value) {
if (javaObject != null && javaObject instanceof List) {
List list = ((List) javaObject);
int size = list.size();
value = coerceComponentType(value);
if (index >= size) {
for (int i = size; i < index; i++)
list.add(i, null);
list.add(index, value);
} else {
list.set(index, value);
}
if (changeReceiver != null)
updateChangeReceiver();
}
}
public Object get(int index, Scriptable start) {
if (javaObject != null) {
if (changeReceiver != null)
fetchChangeReceiver();
try {
Object obj = ((ReadOnlyList) javaObject).get(index);
if (obj != null) {
Object result = Context.javaToJS(obj, getParentScope());
if (javaObject instanceof ChangeReceiver)
handleChangeEmitter(result, Integer.toString(index));
return result;
}
} catch (IndexOutOfBoundsException e) {
// Don't report
}
}
return Scriptable.NOT_FOUND;
}
public boolean has(String name, Scriptable start) {
return super.has(name, start) || name.equals("length") ||
javaObject instanceof ReadOnlyStringIndexList && javaObject != null
&& ((ReadOnlyStringIndexList) javaObject).get(name) != null;
}
@SuppressWarnings("unchecked")
public void put(String name, Scriptable start, Object value) {
// Since lists have the native size method that's already accessible
// through "length", offer access here to get/setSize if these are
// present, such as in Scriptographer's HierarchyListBox, where they
// get / set the item's dimensions.
if (name.equals("size") && members.has("setSize", false)) {
Object obj = members.get(this, "setSize", javaObject, false);
if (obj instanceof Callable) {
((Callable) obj).call(Context.getCurrentContext(),
start.getParentScope(), this, new Object[] { value });
return;
}
} else if (javaObject != null) {
if (javaObject instanceof List && name.equals("length")) {
// from NativeArray#setLength(Object value)
double d = ScriptRuntime.toNumber(value);
int length = ScriptRuntime.toInt32(d);
if (length != d) {
String msg = ScriptRuntime.getMessage0("msg.arraylength.bad");
throw ScriptRuntime.constructError("RangeError", msg);
}
List list = ((List) javaObject);
int size = list.size();
if (length < size) {
list.remove(length, size);
} else {
for (int i = size; i < length; i++)
list.add(null);
}
} else if (javaObject instanceof StringIndexList
&& !members.has(name, false)) {
((StringIndexList) javaObject).put(name, coerceComponentType(value));
return;
}
}
super.put(name, start, value);
}
public Object get(String name, Scriptable start) {
// Again, allow access to getSize, if it's there. See #put
if (name.equals("size") && members.has("getSize", false)) {
Object obj = members.get(this, "getSize", javaObject, false);
if (obj instanceof Callable)
return ((Callable) obj).call(Context.getCurrentContext(),
start.getParentScope(), this, new Object[] {});
} else if (javaObject != null) {
if (name.equals("length")) {
return new Integer(((ReadOnlyList) javaObject).size());
} else if (javaObject instanceof ReadOnlyStringIndexList
&& !members.has(name, false)) {
// Only check ReadOnlyStringIndexList if members does not
// define the same property
Object obj = ((ReadOnlyStringIndexList) javaObject).get(name);
if (obj != null)
return Context.javaToJS(obj, getParentScope());
}
}
return super.get(name, start);
}
public Object getDefaultValue(Class hint) {
if (hint == null || hint == ScriptRuntime.StringClass) {
StringBuffer buffer = new StringBuffer();
ReadOnlyList list = (ReadOnlyList) javaObject;
for (int i = 0, l = list.size(); i < l; i++) {
if (i > 0)
buffer.append(",");
Object entry = list.get(i);
if (entry != null) {
Scriptable obj = Context.toObject(entry, this);
buffer.append(obj.getDefaultValue(hint));
} else {
buffer.append("null");
}
}
return buffer.toString();
} else {
return super.getDefaultValue(hint);
}
}
private Object coerceComponentType(Object value) {
Object unwrapped = value;
if (unwrapped instanceof Wrapper)
unwrapped = ((Wrapper) unwrapped).unwrap();
Class type = ((List) javaObject).getComponentType();
// Use WrapFactory to coerce type if not compatible
return type.isInstance(unwrapped)
? unwrapped
: Context.getCurrentContext().getWrapFactory().coerceType(
type, value, unwrapped);
}
}