blob: 8deeb7cbbe42e10cbc9ce8f641a6172d4c670eaf [file] [log] [blame]
/*
* $Id: DefaultJavaReflector.java 161 2012-10-06 13:53:02Z andre@naef.com $
* See LICENSE.txt for license terms.
*/
package com.naef.jnlua;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Default implementation of the <code>JavaReflector</code> interface.
*/
public class DefaultJavaReflector implements JavaReflector {
// -- Static
private static final DefaultJavaReflector INSTANCE = new DefaultJavaReflector();
private static final Object JAVA_FUNCTION_TYPE = new Object();
private static final Object[] EMPTY_ARGUMENTS = new Object[0];
// -- State
private Map<Class<?>, Map<String, Accessor>> accessors = new HashMap<Class<?>, Map<String, Accessor>>();
private ReadWriteLock accessorLock = new ReentrantReadWriteLock();
private Map<LuaCallSignature, Invocable> invocableDispatches = new HashMap<LuaCallSignature, Invocable>();
private ReadWriteLock invocableDispatchLock = new ReentrantReadWriteLock();
private JavaFunction index = new Index();
private JavaFunction newIndex = new NewIndex();
private JavaFunction equal = new Equal();
private JavaFunction length = new Length();
private JavaFunction lessThan = new LessThan();
private JavaFunction lessThanOrEqual = new LessThanOrEqual();
private JavaFunction toString = new ToString();
private JavaFunction pairs = new Pairs();
private JavaFunction ipairs = new IPairs();
private JavaFunction javaFields = new AccessorPairs(FieldAccessor.class);
private JavaFunction javaMethods = new AccessorPairs(
InvocableAccessor.class);
private JavaFunction javaProperties = new AccessorPairs(
PropertyAccessor.class);
// -- Static methods
/**
* Returns the instance of this class.
*
* @return the instance
*/
public static DefaultJavaReflector getInstance() {
return INSTANCE;
}
// -- Construction
/**
* Creates a new instances;
*/
private DefaultJavaReflector() {
}
// -- JavaReflector methods
@Override
public JavaFunction getMetamethod(Metamethod metamethod) {
switch (metamethod) {
case INDEX:
return index;
case NEWINDEX:
return newIndex;
case LEN:
return length;
case EQ:
return equal;
case LT:
return lessThan;
case LE:
return lessThanOrEqual;
case TOSTRING:
return toString;
case PAIRS:
return pairs;
case IPAIRS:
return ipairs;
case JAVAFIELDS:
return javaFields;
case JAVAMETHODS:
return javaMethods;
case JAVAPROPERTIES:
return javaProperties;
default:
return null;
}
}
// -- Private methods
/**
* Returns the accessors of an object.
*/
private Map<String, Accessor> getObjectAccessors(Object object) {
// Check cache
Class<?> clazz = getObjectClass(object);
accessorLock.readLock().lock();
try {
Map<String, Accessor> result = accessors.get(clazz);
if (result != null) {
return result;
}
} finally {
accessorLock.readLock().unlock();
}
// Fill in
Map<String, Accessor> result = createClassAccessors(clazz);
accessorLock.writeLock().lock();
try {
if (!accessors.containsKey(clazz)) {
accessors.put(clazz, result);
} else {
result = accessors.get(clazz);
}
} finally {
accessorLock.writeLock().unlock();
}
return result;
}
/**
* Creates the accessors of a class.
*/
private Map<String, Accessor> createClassAccessors(Class<?> clazz) {
Map<String, Accessor> result = new HashMap<String, Accessor>();
// Fields
Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
result.put(fields[i].getName(), new FieldAccessor(fields[i]));
}
// Methods
Map<String, Map<List<Class<?>>, Invocable>> accessibleMethods = new HashMap<String, Map<List<Class<?>>, Invocable>>();
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
// Do not overwrite fields
Method method = methods[i];
if (result.containsKey(method.getName())) {
continue;
}
// Attempt to find the method in a public class if the declaring
// class is not public
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
method = getPublicClassMethod(clazz, method.getName(),
method.getParameterTypes());
if (method == null) {
continue;
}
}
// For each method name and parameter type list, keep
// only the method declared by the most specific class
Map<List<Class<?>>, Invocable> overloaded = accessibleMethods
.get(method.getName());
if (overloaded == null) {
overloaded = new HashMap<List<Class<?>>, Invocable>();
accessibleMethods.put(method.getName(), overloaded);
}
List<Class<?>> parameterTypes = Arrays.asList(method
.getParameterTypes());
Invocable currentInvocable = overloaded.get(parameterTypes);
if (currentInvocable != null
&& method.getDeclaringClass().isAssignableFrom(
currentInvocable.getDeclaringClass())) {
continue;
}
overloaded.put(parameterTypes, new InvocableMethod(method));
}
for (Map.Entry<String, Map<List<Class<?>>, Invocable>> entry : accessibleMethods
.entrySet()) {
result.put(entry.getKey(), new InvocableAccessor(clazz, entry
.getValue().values()));
}
// Constructors
Constructor<?>[] constructors = clazz.getConstructors();
List<Invocable> accessibleConstructors = new ArrayList<Invocable>(
constructors.length);
for (int i = 0; i < constructors.length; i++) {
// Ignore constructor if the declaring class is not public
if (!Modifier.isPublic(constructors[i].getDeclaringClass()
.getModifiers())) {
continue;
}
accessibleConstructors
.add(new InvocableConstructor(constructors[i]));
}
if (clazz.isInterface()) {
accessibleConstructors.add(new InvocableProxy(clazz));
}
if (!accessibleConstructors.isEmpty()) {
result.put("new", new InvocableAccessor(clazz,
accessibleConstructors));
}
// Properties
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(clazz);
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
PropertyDescriptor[] propertyDescriptors = beanInfo
.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; i++) {
// Do not overwrite fields or methods
if (result.containsKey(propertyDescriptors[i].getName())) {
continue;
}
// Attempt to find the read/write methods in a public class if the
// declaring class is not public
Method method = propertyDescriptors[i].getReadMethod();
if (method != null
&& !Modifier.isPublic(method.getDeclaringClass()
.getModifiers())) {
method = getPublicClassMethod(clazz, method.getName(),
method.getParameterTypes());
try {
propertyDescriptors[i].setReadMethod(method);
} catch (IntrospectionException e) {
}
}
method = propertyDescriptors[i].getWriteMethod();
if (method != null
&& !Modifier.isPublic(method.getDeclaringClass()
.getModifiers())) {
method = getPublicClassMethod(clazz, method.getName(),
method.getParameterTypes());
try {
propertyDescriptors[i].setWriteMethod(method);
} catch (IntrospectionException e) {
}
}
// Do not process properties without a read and a write method
if (propertyDescriptors[i].getReadMethod() == null
&& propertyDescriptors[i].getWriteMethod() == null) {
continue;
}
result.put(propertyDescriptors[i].getName(), new PropertyAccessor(
clazz, propertyDescriptors[i]));
}
return result;
}
/**
* Returns a public class method matching a method name and parameter list.
* The public class can be a superclass or interface.
*/
private Method getPublicClassMethod(Class<?> clazz, String methodName,
Class<?>[] parameterTypes) {
Method method = getPublicSuperclassMethod(clazz, methodName,
parameterTypes);
if (method != null) {
return method;
}
return getInterfaceMethod(clazz, methodName, parameterTypes);
}
/**
* Returns a public superclass method matching a method name and parameter
* list.
*/
private Method getPublicSuperclassMethod(Class<?> clazz, String methodName,
Class<?>[] parameterTypes) {
Class<?> superclass = clazz.getSuperclass();
while (superclass != null) {
// Ignore non-public superclasses
if (!Modifier.isPublic(superclass.getModifiers())) {
continue;
}
// Find method in superclass
try {
Method method = superclass.getDeclaredMethod(methodName,
parameterTypes);
if (Modifier.isPublic(method.getModifiers())) {
return method;
}
} catch (NoSuchMethodException e) {
// Not found
}
// Check superclass
superclass = superclass.getSuperclass();
}
// Not found
return null;
}
/**
* Returns an interface method matching a method name and parameter list.
*/
private Method getInterfaceMethod(Class<?> clazz, String methodName,
Class<?>[] parameterTypes) {
do {
// Get interfaces
Class<?>[] interfaces = clazz.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
// Ignore non-public interfaces
if (!Modifier.isPublic(interfaces[i].getModifiers())) {
continue;
}
// Find method in the current interface
try {
return interfaces[i].getDeclaredMethod(methodName,
parameterTypes);
} catch (NoSuchMethodException e) {
// Not found
}
// Check superinterfaces
Method method = getInterfaceMethod(interfaces[i], methodName,
parameterTypes);
if (method != null) {
return method;
}
}
// Check superclass
clazz = clazz.getSuperclass();
} while (clazz != null);
// Not found
return null;
}
/**
* Returns the class of an object, or the class itself if the object is
* already a class.
*/
private Class<?> getObjectClass(Object object) {
return object instanceof Class<?> ? (Class<?>) object : object
.getClass();
}
// -- Nested types
/**
* <code>__index</code> metamethod implementation.
*/
private class Index implements JavaFunction {
public int invoke(LuaState luaState) {
// Get object and class
Object object = luaState.toJavaObject(1, Object.class);
Class<?> objectClass = getObjectClass(object);
// Handle arrays
if (objectClass.isArray()) {
if (!luaState.isNumber(2)) {
throw new LuaRuntimeException(String.format(
"attempt to read array with %s accessor",
luaState.typeName(2)));
}
int index = luaState.toInteger(2);
int length = Array.getLength(object);
if (index < 1 || index > length) {
throw new LuaRuntimeException(String.format(
"attempt to read array of length %d at index %d",
length, index));
}
luaState.pushJavaObject(Array.get(object, index - 1));
return 1;
}
// Handle objects
Map<String, Accessor> objectAccessors = getObjectAccessors(object);
String key = luaState.toString(-1);
if (key == null) {
throw new LuaRuntimeException(String.format(
"attempt to read class %s with %s accessor", object
.getClass().getCanonicalName(), luaState
.typeName(-1)));
}
Accessor accessor = objectAccessors.get(key);
if (accessor == null) {
throw new LuaRuntimeException(
String.format(
"attempt to read class %s with accessor '%s' (undefined)",
objectClass.getCanonicalName(), key));
}
accessor.read(luaState, object);
return 1;
}
}
/**
* <code>__newindex</code> metamethod implementation.
*/
private class NewIndex implements JavaFunction {
public int invoke(LuaState luaState) {
// Get object and class
Object object = luaState.toJavaObject(1, Object.class);
Class<?> objectClass = getObjectClass(object);
// Handle arrays
if (objectClass.isArray()) {
if (!luaState.isNumber(2)) {
throw new LuaRuntimeException(String.format(
"attempt to write array with %s accessor",
luaState.typeName(2)));
}
int index = luaState.toInteger(2);
int length = Array.getLength(object);
if (index < 1 || index > length) {
throw new LuaRuntimeException(String.format(
"attempt to write array of length %d at index %d",
length, index));
}
Class<?> componentType = objectClass.getComponentType();
if (!luaState.isJavaObject(3, componentType)) {
throw new LuaRuntimeException(
String.format(
"attempt to write array of %s at index %d with %s value",
componentType.getCanonicalName(), 3,
luaState.typeName(3)));
}
Object value = luaState.toJavaObject(3, componentType);
Array.set(object, index - 1, value);
return 0;
}
// Handle objects
Map<String, Accessor> objectAccessors = getObjectAccessors(object);
String key = luaState.toString(2);
if (key == null) {
throw new LuaRuntimeException(String.format(
"attempt to write class %s with %s accessor", object
.getClass().getCanonicalName(), luaState
.typeName(2)));
}
Accessor accessor = objectAccessors.get(key);
if (accessor == null) {
throw new LuaRuntimeException(
String.format(
"attempt to write class %s with accessor '%s' (undefined)",
objectClass.getCanonicalName(), key));
}
accessor.write(luaState, object);
return 0;
}
}
/**
* <code>__len</code> metamethod implementation.
*/
private class Length implements JavaFunction {
@Override
public int invoke(LuaState luaState) {
Object object = luaState.toJavaObject(1, Object.class);
if (object.getClass().isArray()) {
luaState.pushInteger(Array.getLength(object));
return 1;
}
luaState.pushInteger(0);
return 1;
}
}
/**
* <code>__eq</code> metamethod implementation.
*/
private class Equal implements JavaFunction {
@Override
public int invoke(LuaState luaState) {
Object object1 = luaState.toJavaObject(1, Object.class);
Object object2 = luaState.toJavaObject(2, Object.class);
luaState.pushBoolean(object1 == object2 || object1 != null
&& object1.equals(object2));
return 1;
}
}
/**
* <code>__lt</code> metamethod implementation.
*/
private class LessThan implements JavaFunction {
@SuppressWarnings("unchecked")
@Override
public int invoke(LuaState luaState) {
if (!luaState.isJavaObject(1, Comparable.class)) {
throw new LuaRuntimeException(String.format(
"class %s does not implement Comparable",
luaState.typeName(1)));
}
Comparable<Object> comparable = luaState.toJavaObject(1,
Comparable.class);
Object object = luaState.toJavaObject(2, Object.class);
luaState.pushBoolean(comparable.compareTo(object) < 0);
return 1;
}
}
/**
* <code>__le</code> metamethod implementation.
*/
private class LessThanOrEqual implements JavaFunction {
@SuppressWarnings("unchecked")
@Override
public int invoke(LuaState luaState) {
if (!luaState.isJavaObject(1, Comparable.class)) {
throw new LuaRuntimeException(String.format(
"class %s does not implement Comparable",
luaState.typeName(1)));
}
Comparable<Object> comparable = luaState.toJavaObject(1,
Comparable.class);
Object object = luaState.toJavaObject(2, Object.class);
luaState.pushBoolean(comparable.compareTo(object) <= 0);
return 1;
}
}
/**
* <code>__tostring</code> metamethod implementation.
*/
private class ToString implements JavaFunction {
@Override
public int invoke(LuaState luaState) {
Object object = luaState.toJavaObject(1, Object.class);
luaState.pushString(object != null ? object.toString() : "null");
return 1;
}
}
/**
* Provides an iterator for maps. For <code>NavigableMap</code> objects, the
* function returns a stateless iterator which allows concurrent
* modifications to the map. For other maps, the function returns an
* iterator based on <code>Iterator</code> which does not support concurrent
* modifications.
*/
private static class Pairs implements NamedJavaFunction {
// -- Static
private final JavaFunction navigableMapNext = new NavigableMapNext();
// -- JavaFunction methods
@SuppressWarnings("unchecked")
@Override
public int invoke(LuaState luaState) {
Map<Object, Object> map = luaState.checkJavaObject(1, Map.class);
luaState.checkArg(1, map != null,
String.format("expected map, got %s", luaState.typeName(1)));
if (map instanceof NavigableMap) {
luaState.pushJavaFunction(navigableMapNext);
} else {
luaState.pushJavaFunction(new MapNext(map.entrySet().iterator()));
}
luaState.pushJavaObject(map);
luaState.pushNil();
return 3;
}
@Override
public String getName() {
return "pairs";
}
/**
* Provides a stateful iterator function for maps.
*/
private static class MapNext implements JavaFunction {
// -- State
private Iterator<Map.Entry<Object, Object>> iterator;
// -- Construction
/**
* Creates a new instance.
*/
public MapNext(Iterator<Map.Entry<Object, Object>> iterator) {
this.iterator = iterator;
}
// -- JavaFunction methods
public int invoke(LuaState luaState) {
if (iterator.hasNext()) {
Map.Entry<Object, Object> entry = iterator.next();
luaState.pushJavaObject(entry.getKey());
luaState.pushJavaObject(entry.getValue());
return 2;
} else {
luaState.pushNil();
return 1;
}
}
}
/**
* Provides a stateless iterator function for navigable maps.
*/
private static class NavigableMapNext implements JavaFunction {
// -- JavaFunction methods
@SuppressWarnings("unchecked")
public int invoke(LuaState luaState) {
NavigableMap<Object, Object> navigableMap = luaState
.checkJavaObject(1, NavigableMap.class);
Object key = luaState.checkJavaObject(2, Object.class);
Map.Entry<Object, Object> entry;
if (key != null) {
entry = navigableMap.higherEntry(key);
} else {
entry = navigableMap.firstEntry();
}
if (entry != null) {
luaState.pushJavaObject(entry.getKey());
luaState.pushJavaObject(entry.getValue());
return 2;
} else {
luaState.pushNil();
return 1;
}
}
}
}
/**
* Provides an iterator for lists and arrays.
*/
private static class IPairs implements NamedJavaFunction {
// -- Static
private final JavaFunction listNext = new ListNext();
private final JavaFunction arrayNext = new ArrayNext();
// -- JavaFunction methods
@Override
public int invoke(LuaState luaState) {
Object object;
if (luaState.isJavaObject(1, List.class)) {
object = luaState.toJavaObject(1, List.class);
luaState.pushJavaFunction(listNext);
} else {
object = luaState.checkJavaObject(1, Object.class);
luaState.checkArg(1, object.getClass().isArray(), String
.format("expected list or array, got %s",
luaState.typeName(1)));
luaState.pushJavaFunction(arrayNext);
}
luaState.pushJavaObject(object);
luaState.pushInteger(0);
return 3;
}
@Override
public String getName() {
return "ipairs";
}
/**
* Provides a stateless iterator function for lists.
*/
private static class ListNext implements JavaFunction {
public int invoke(LuaState luaState) {
List<?> list = luaState.checkJavaObject(1, List.class);
int size = list.size();
int index = luaState.checkInteger(2);
index++;
if (index >= 1 && index <= size) {
luaState.pushInteger(index);
luaState.pushJavaObject(list.get(index - 1));
return 2;
} else {
luaState.pushNil();
return 1;
}
}
}
/**
* Provides a stateless iterator function for arrays.
*/
private static class ArrayNext implements JavaFunction {
public int invoke(LuaState luaState) {
Object array = luaState.checkJavaObject(1, Object.class);
int length = java.lang.reflect.Array.getLength(array);
int index = luaState.checkInteger(2);
index++;
if (index >= 1 && index <= length) {
luaState.pushInteger(index);
luaState.pushJavaObject(Array.get(array, index - 1));
return 2;
} else {
luaState.pushNil();
return 1;
}
}
}
}
/**
* Provides an iterator for accessors.
*/
private class AccessorPairs implements JavaFunction {
// -- State
private Class<?> accessorClass;
// -- Construction
/**
* Creates a new instance.
*/
public AccessorPairs(Class<?> accessorClass) {
this.accessorClass = accessorClass;
}
// -- JavaFunction methods
@Override
public int invoke(LuaState luaState) {
// Get object
Object object = luaState.toJavaObject(1, Object.class);
Class<?> objectClass = getObjectClass(object);
// Create iterator
Map<String, Accessor> objectAccessors = getObjectAccessors(object);
Iterator<Entry<String, Accessor>> iterator = objectAccessors
.entrySet().iterator();
luaState.pushJavaObject(new AccessorNext(iterator,
objectClass == object));
luaState.pushJavaObject(object);
luaState.pushNil();
return 3;
}
// -- Member types
/**
* Provides the next function for iterating accessors.
*/
private class AccessorNext implements JavaFunction {
// -- State
private Iterator<Entry<String, Accessor>> iterator;
private boolean isStatic;
// -- Construction
/**
* Creates a new instance.
*/
public AccessorNext(Iterator<Entry<String, Accessor>> iterator,
boolean isStatic) {
this.iterator = iterator;
this.isStatic = isStatic;
}
// -- JavaFunction methods
@Override
public int invoke(LuaState luaState) {
while (iterator.hasNext()) {
Entry<String, Accessor> entry = iterator.next();
Accessor accessor = entry.getValue();
// Filter by accessor class
if (accessor.getClass() != accessorClass) {
continue;
}
// Filter by non-static, static
if (isStatic) {
if (!accessor.isStatic()) {
continue;
}
} else {
if (!accessor.isNotStatic()) {
continue;
}
}
// Push match
luaState.pushString(entry.getKey());
Object object = luaState.toJavaObject(1, Object.class);
accessor.read(luaState, object);
return 2;
}
// End iteration
return 0;
}
}
}
/**
* Provides access to class or object members.
*/
private interface Accessor {
/**
* Reads the object member.
*/
void read(LuaState luaState, Object object);
/**
* Writes the object member.
*/
void write(LuaState luaState, Object object);
/**
* Returns whether this accessor is applicable in a non-static context.
*/
boolean isNotStatic();
/**
* Returns whether this accessor is applicable in a static context.
*/
boolean isStatic();
}
/**
* Provides field access.
*/
private class FieldAccessor implements Accessor {
// -- State
private Field field;
// -- Construction
/**
* Creates a new instance.
*/
public FieldAccessor(Field field) {
this.field = field;
}
// -- Accessor methods
@Override
public void read(LuaState luaState, Object object) {
try {
Class<?> objectClass = getObjectClass(object);
if (objectClass == object) {
object = null;
}
luaState.pushJavaObject(field.get(object));
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void write(LuaState luaState, Object object) {
try {
Class<?> objectClass = getObjectClass(object);
if (objectClass == object) {
object = null;
}
Object value = luaState.checkJavaObject(-1, field.getType());
field.set(object, value);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isNotStatic() {
return !Modifier.isStatic(field.getModifiers());
}
@Override
public boolean isStatic() {
return Modifier.isStatic(field.getModifiers());
}
}
/**
* Provides invocable access.
*/
private class InvocableAccessor implements Accessor, JavaFunction {
// -- State
private Class<?> clazz;
private List<Invocable> invocables;
// -- Construction
/**
* Creates a new instance.
*/
public InvocableAccessor(Class<?> clazz,
Collection<Invocable> invocables) {
this.clazz = clazz;
this.invocables = new ArrayList<Invocable>(invocables);
}
// -- Properties
/**
* Returns the name of the invocable.
*/
public String getName() {
return invocables.get(0).getName();
}
/**
* Returns what this invocable accessor is for.
*/
public String getWhat() {
return invocables.get(0).getWhat();
}
// -- Accessor methods
@Override
public void read(LuaState luaState, Object object) {
Class<?> objectClass = getObjectClass(object);
if (objectClass == object) {
object = null;
}
luaState.pushJavaFunction(this);
}
@Override
public void write(LuaState luaState, Object object) {
Class<?> objectClass = getObjectClass(object);
throw new LuaRuntimeException(String.format(
"attempt to write class %s with accessor '%s' (a %s)",
objectClass.getCanonicalName(), getName(), getWhat()));
}
@Override
public boolean isNotStatic() {
for (Invocable invocable : invocables) {
if (!Modifier.isStatic(invocable.getModifiers())) {
return true;
}
}
return false;
}
@Override
public boolean isStatic() {
for (Invocable invocable : invocables) {
if (Modifier.isStatic(invocable.getModifiers())) {
return true;
}
}
return false;
}
// -- JavaFunction methods
@Override
public int invoke(LuaState luaState) {
// Argument sanity checks
Object object = luaState.checkJavaObject(1, Object.class);
Class<?> objectClass = getObjectClass(object);
luaState.checkArg(1, clazz.isAssignableFrom(objectClass), String
.format("class %s is not a subclass of %s",
objectClass.getCanonicalName(),
clazz.getCanonicalName()));
if (objectClass == object) {
object = null;
}
// Invocable dispatch
LuaCallSignature luaCallSignature = getLuaCallSignature(luaState);
Invocable invocable;
invocableDispatchLock.readLock().lock();
try {
invocable = invocableDispatches.get(luaCallSignature);
} finally {
invocableDispatchLock.readLock().unlock();
}
if (invocable == null) {
invocable = dispatchInvocable(luaState, object == null);
invocableDispatchLock.writeLock().lock();
try {
if (!invocableDispatches.containsKey(luaCallSignature)) {
invocableDispatches.put(luaCallSignature, invocable);
} else {
invocable = invocableDispatches.get(luaCallSignature);
}
} finally {
invocableDispatchLock.writeLock().unlock();
}
}
// Prepare arguments
int argCount = luaState.getTop() - 1;
int parameterCount = invocable.getParameterCount();
Object[] arguments = new Object[parameterCount];
if (invocable.isVarArgs()) {
for (int i = 0; i < parameterCount - 1; i++) {
arguments[i] = luaState.toJavaObject(i + 2,
invocable.getParameterType(i));
}
arguments[parameterCount - 1] = Array.newInstance(
invocable.getParameterType(parameterCount - 1),
argCount - (parameterCount - 1));
for (int i = parameterCount - 1; i < argCount; i++) {
Array.set(
arguments[parameterCount - 1],
i - (parameterCount - 1),
luaState.toJavaObject(i + 2,
invocable.getParameterType(i)));
}
} else {
for (int i = 0; i < parameterCount; i++) {
arguments[i] = luaState.toJavaObject(i + 2,
invocable.getParameterType(i));
}
}
// Invoke
Object result;
try {
result = invocable.invoke(object, arguments);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
}
// Return
if (invocable.getReturnType() != Void.TYPE) {
if (invocable.isRawReturn()) {
luaState.pushJavaObjectRaw(result);
} else {
luaState.pushJavaObject(result);
}
return 1;
} else {
return 0;
}
}
// -- Private methods
/**
* Creates a Lua call signature.
*/
private LuaCallSignature getLuaCallSignature(LuaState luaState) {
int argCount = luaState.getTop() - 1;
Object[] types = new Object[argCount];
for (int i = 0; i < argCount; i++) {
LuaType type = luaState.type(i + 2);
switch (type) {
case FUNCTION:
types[i] = luaState.isJavaFunction(i + 2) ? JAVA_FUNCTION_TYPE
: LuaType.FUNCTION;
break;
case USERDATA:
if (luaState.isJavaObjectRaw(i + 2)) {
Object object = luaState.toJavaObjectRaw(i + 2);
if (object instanceof TypedJavaObject) {
types[i] = ((TypedJavaObject) object).getType();
} else {
types[i] = object.getClass();
}
} else {
types[i] = LuaType.USERDATA;
}
break;
default:
types[i] = type;
}
}
return new LuaCallSignature(clazz, getName(), types);
}
/**
* Dispatches an invocable.
*/
private Invocable dispatchInvocable(LuaState luaState,
boolean staticDispatch) {
// Begin with all candidates
Set<Invocable> candidates = new HashSet<Invocable>(invocables);
// Eliminate methods with an invalid static modifier
for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
Invocable invocable = i.next();
if (Modifier.isStatic(invocable.getModifiers()) != staticDispatch) {
i.remove();
}
}
// Eliminate methods with an invalid parameter count
int argCount = luaState.getTop() - 1;
for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
Invocable invocable = i.next();
if (invocable.isVarArgs()) {
if (argCount < invocable.getParameterCount() - 1) {
i.remove();
}
} else {
if (argCount != invocable.getParameterCount()) {
i.remove();
}
}
}
// Eliminate methods that are not applicable
Converter converter = luaState.getConverter();
outer: for (Iterator<Invocable> i = candidates.iterator(); i
.hasNext();) {
Invocable invocable = i.next();
for (int j = 0; j < argCount; j++) {
int distance = converter.getTypeDistance(luaState, j + 2,
invocable.getParameterType(j));
if (distance == Integer.MAX_VALUE) {
i.remove();
continue outer;
}
}
}
// Eliminate variable arguments methods in the presence of fix
// arguments methods
boolean haveFixArgs = false;
boolean haveVarArgs = false;
for (Invocable invocable : candidates) {
haveFixArgs = haveFixArgs || !invocable.isVarArgs();
haveVarArgs = haveVarArgs || invocable.isVarArgs();
}
if (haveVarArgs && haveFixArgs) {
for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
Invocable invocable = i.next();
if (invocable.isVarArgs()) {
i.remove();
}
}
}
// Eliminate methods that are not closest
outer: for (Iterator<Invocable> i = candidates.iterator(); i
.hasNext();) {
Invocable invocable = i.next();
inner: for (Invocable other : candidates) {
if (other == invocable) {
continue;
}
int parameterCount = Math.min(
argCount,
Math.max(invocable.getParameterCount(),
other.getParameterCount()));
boolean delta = false;
for (int j = 0; j < parameterCount; j++) {
int distance = converter.getTypeDistance(luaState,
j + 2, invocable.getParameterType(j));
int otherDistance = converter.getTypeDistance(luaState,
j + 2, other.getParameterType(j));
if (otherDistance > distance) {
// Other is not closer
continue inner;
}
delta = delta || distance != otherDistance;
}
// If there is no delta, other is not closer
if (!delta) {
continue;
}
// Other is closer
i.remove();
continue outer;
}
}
// Eliminate methods that are not most precise
outer: for (Iterator<Invocable> i = candidates.iterator(); i
.hasNext();) {
Invocable invocable = i.next();
inner: for (Invocable other : candidates) {
if (other == invocable) {
continue;
}
int parameterCount = Math.min(
argCount,
Math.max(invocable.getParameterCount(),
other.getParameterCount()));
boolean delta = false;
for (int j = 0; j < parameterCount; j++) {
Class<?> type = invocable.getParameterType(j);
Class<?> otherType = other.getParameterType(j);
if (!type.isAssignableFrom(otherType)) {
// Other is not more specific
continue inner;
}
delta = delta || type != otherType;
}
// If there is no delta, other is not more specific
if (!delta) {
continue;
}
// Other is more specific
i.remove();
continue outer;
}
}
// Handle outcomes
if (candidates.isEmpty()) {
throw getSignatureMismatchException(luaState);
}
if (candidates.size() > 1) {
throw getSignatureAmbivalenceException(luaState, candidates);
}
// Return
return candidates.iterator().next();
}
/**
* Returns a Lua runtime exception indicating that no matching invocable
* has been found.
*/
private LuaRuntimeException getSignatureMismatchException(
LuaState luaState) {
return new LuaRuntimeException(String.format(
"no %s of class %s matches '%s(%s)'", getWhat(),
clazz.getCanonicalName(), getName(),
getLuaSignatureString(luaState)));
}
/**
* Returns a Lua runtime exception indicating that an invocable is
* ambivalent.
*/
private LuaRuntimeException getSignatureAmbivalenceException(
LuaState luaState, Set<Invocable> candidates) {
StringBuffer sb = new StringBuffer();
sb.append(String.format(
"%s '%s(%s)' on class %s is ambivalent among ", getWhat(),
getName(), getLuaSignatureString(luaState),
clazz.getCanonicalName()));
boolean first = true;
for (Invocable invocable : candidates) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(String.format("'%s(%s)'", getName(),
getJavaSignatureString(invocable.getParameterTypes())));
}
return new LuaRuntimeException(sb.toString());
}
/**
* Returns a Lua value signature string for diagnostic messages.
*/
private String getLuaSignatureString(LuaState luaState) {
int argCount = luaState.getTop() - 1;
StringBuffer sb = new StringBuffer();
for (int i = 0; i < argCount; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(luaState.typeName(i + 2));
}
return sb.toString();
}
/**
* Returns a Java type signature string for diagnostic messages.
*/
private String getJavaSignatureString(Class<?>[] types) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < types.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(types[i].getCanonicalName());
}
return sb.toString();
}
}
/**
* Provides property access.
*/
private class PropertyAccessor implements Accessor {
// -- State
private Class<?> clazz;
private PropertyDescriptor propertyDescriptor;
// -- Construction
/**
* Creates a new instance.
*/
public PropertyAccessor(Class<?> clazz,
PropertyDescriptor propertyDescriptor) {
this.clazz = clazz;
this.propertyDescriptor = propertyDescriptor;
}
// -- Accessor methods
@Override
public void read(LuaState luaState, Object object) {
if (propertyDescriptor.getReadMethod() == null) {
throw new LuaRuntimeException(
String.format(
"attempt to read class %s with accessor '%s' (a write-only property)",
clazz.getCanonicalName(),
propertyDescriptor.getName()));
}
try {
luaState.pushJavaObject(propertyDescriptor.getReadMethod()
.invoke(object, EMPTY_ARGUMENTS));
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
}
}
@Override
public void write(LuaState luaState, Object object) {
if (propertyDescriptor.getWriteMethod() == null) {
throw new LuaRuntimeException(
String.format(
"attempt to write class %s with acessor '%s' (a read-only property)",
clazz.getCanonicalName(),
propertyDescriptor.getName()));
}
try {
Object value = luaState.checkJavaObject(-1,
propertyDescriptor.getPropertyType());
propertyDescriptor.getWriteMethod().invoke(object, value);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
}
luaState.pop(1);
}
@Override
public boolean isNotStatic() {
return true;
}
@Override
public boolean isStatic() {
return false;
}
}
/**
* Virtual superinterface for methods and constructors.
*/
private interface Invocable {
/**
* Returns what this invocable is, for use in diagnostic messages.
*/
public String getWhat();
/**
* Returns the declaring class of this invocable.
*/
public Class<?> getDeclaringClass();
/**
* Returns the modifiers of this invocable.
*/
public int getModifiers();
/**
* Returns the name of this invocable.
*/
public String getName();
/**
* Returns the return type of this invocable.
*/
public Class<?> getReturnType();
/**
* Returns whether this invocable has a return value that must be pushed
* raw.
*/
public boolean isRawReturn();
/**
* Returns the number of parameters.
*/
public int getParameterCount();
/**
* Returns the parameter types of this invocable.
*/
public Class<?>[] getParameterTypes();
/**
* Returns a parameter type, flattening variable arguments.
*/
public Class<?> getParameterType(int index);
/**
* Returns whether this invocable has a variable number of arguments.
*/
public boolean isVarArgs();
/**
* Invokes this invocable.
*/
public Object invoke(Object obj, Object... args)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException;
}
/**
* Invocable method.
*/
private static class InvocableMethod implements Invocable {
private Method method;
private Class<?>[] parameterTypes;
/**
* Creates a new instance.
*/
public InvocableMethod(Method method) {
this.method = method;
this.parameterTypes = method.getParameterTypes();
}
@Override
public String getWhat() {
return "method";
}
@Override
public Class<?> getDeclaringClass() {
return method.getDeclaringClass();
}
@Override
public int getModifiers() {
return method.getModifiers();
}
@Override
public String getName() {
return method.getName();
}
@Override
public Class<?> getReturnType() {
return method.getReturnType();
}
@Override
public boolean isRawReturn() {
return false;
}
@Override
public int getParameterCount() {
return parameterTypes.length;
}
@Override
public Class<?>[] getParameterTypes() {
return parameterTypes;
}
@Override
public Class<?> getParameterType(int index) {
if (method.isVarArgs() && index >= parameterTypes.length - 1) {
return parameterTypes[parameterTypes.length - 1]
.getComponentType();
} else {
return parameterTypes[index];
}
}
@Override
public boolean isVarArgs() {
return method.isVarArgs();
}
@Override
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
return method.invoke(obj, args);
}
@Override
public String toString() {
return method.toString();
}
}
/**
* Invocable constructor.
*/
private static class InvocableConstructor implements Invocable {
// -- State
private Constructor<?> constructor;
private Class<?>[] parameterTypes;
/**
* Creates a new instance.
*/
public InvocableConstructor(Constructor<?> constructor) {
this.constructor = constructor;
this.parameterTypes = constructor.getParameterTypes();
}
@Override
public String getWhat() {
return "constructor";
}
@Override
public Class<?> getDeclaringClass() {
return constructor.getDeclaringClass();
}
@Override
public int getModifiers() {
return constructor.getModifiers() | Modifier.STATIC;
}
@Override
public String getName() {
return "new";
}
@Override
public Class<?> getReturnType() {
return constructor.getDeclaringClass();
}
@Override
public boolean isRawReturn() {
return false;
}
@Override
public int getParameterCount() {
return parameterTypes.length;
}
@Override
public Class<?>[] getParameterTypes() {
return parameterTypes;
}
@Override
public Class<?> getParameterType(int index) {
if (constructor.isVarArgs() && index >= parameterTypes.length - 1) {
return parameterTypes[parameterTypes.length - 1]
.getComponentType();
} else {
return parameterTypes[index];
}
}
@Override
public boolean isVarArgs() {
return constructor.isVarArgs();
}
@Override
public Object invoke(Object obj, Object... args)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
return constructor.newInstance(args);
}
@Override
public String toString() {
return constructor.toString();
}
}
/**
* Invocable proxy.
*/
private static class InvocableProxy implements Invocable {
// -- Static
private static final Class<?>[] PARAMETER_TYPES = new Class<?>[] { LuaValueProxy.class };
// -- State
private Class<?> interfaze;
/**
* Creates a new instance.
*/
public InvocableProxy(Class<?> interfaze) {
this.interfaze = interfaze;
}
@Override
public String getWhat() {
return "proxy";
}
@Override
public Class<?> getDeclaringClass() {
return interfaze;
}
@Override
public int getModifiers() {
return interfaze.getModifiers() | Modifier.STATIC;
}
@Override
public String getName() {
return "new";
}
@Override
public Class<?> getReturnType() {
return interfaze;
}
@Override
public boolean isRawReturn() {
return true;
}
@Override
public int getParameterCount() {
return 1;
}
@Override
public Class<?>[] getParameterTypes() {
return PARAMETER_TYPES;
}
@Override
public Class<?> getParameterType(int index) {
return PARAMETER_TYPES[0];
}
@Override
public boolean isVarArgs() {
return false;
}
@Override
public Object invoke(Object obj, Object... args)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
LuaValueProxy luaValueProxy = (LuaValueProxy) args[0];
luaValueProxy.pushValue();
Object proxy = luaValueProxy.getLuaState().getProxy(-1, interfaze);
luaValueProxy.getLuaState().pop(1);
return proxy;
}
@Override
public String toString() {
return interfaze.toString();
}
}
/**
* Lua call signature.
*/
private static class LuaCallSignature {
// -- State
private Class<?> clazz;
private String invocableName;
private Object[] types;
private int hashCode;
// -- Construction
/**
* Creates a new instance.
*/
public LuaCallSignature(Class<?> clazz, String invocableName,
Object[] types) {
this.clazz = clazz;
this.invocableName = invocableName;
this.types = types;
hashCode = clazz.hashCode();
hashCode = hashCode * 65599 + invocableName.hashCode();
for (int i = 0; i < types.length; i++) {
hashCode = hashCode * 65599 + types[i].hashCode();
}
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof LuaCallSignature)) {
return false;
}
LuaCallSignature other = (LuaCallSignature) obj;
if (clazz != other.clazz
|| !invocableName.equals(other.invocableName)
|| types.length != other.types.length) {
return false;
}
for (int i = 0; i < types.length; i++) {
if (types[i] != other.types[i]) {
return false;
}
}
return true;
}
@Override
public String toString() {
return clazz.getCanonicalName() + ": " + invocableName + "("
+ Arrays.asList(types) + ")";
}
}
}