package abbot.script;
import java.lang.reflect.*;
import java.util.*;
import abbot.Log;
import abbot.i18n.Strings;
/** Class for script steps that want to invoke a method on a class.
* Subclasses may override getMethod and getTarget to customize behavior.
* <blockquote><code>
* <call method="..." args="..." class="..."><br>
* </code></blockquote>
*/
public class Call extends Step {
private static final long serialVersionUID = 1L;
private String targetClassName = null;
private String methodName;
private String[] args;
private static final String USAGE =
"<call class=\"...\" method=\"...\" args=\"...\" [property=\"...\"]/>";
public Call(Resolver resolver, Map attributes) {
super(resolver, attributes);
methodName = (String)attributes.get(TAG_METHOD);
if (methodName == null)
usage(Strings.get("call.method_missing"));
targetClassName = (String)attributes.get(TAG_CLASS);
if (targetClassName == null)
usage(Strings.get("call.class_missing"));
String argList = (String)attributes.get(TAG_ARGS);
if (argList == null)
argList = "";
args = ArgumentParser.parseArgumentList(argList);
}
public Call(Resolver resolver, String description,
String className, String methodName, String[] args) {
super(resolver, description);
this.targetClassName = className;
this.methodName = methodName;
this.args = args != null ? args : new String[0];
}
public String getDefaultDescription() {
return getMethodName() + getArgumentsDescription();
}
public String getUsage() { return USAGE; }
public String getXMLTag() { return TAG_CALL; }
/** Convert our argument vector into a single String. */
public String getEncodedArguments() {
return ArgumentParser.encodeArguments(args);
}
protected String getArgumentsDescription() {
return "("
+ ArgumentParser.replace(getEncodedArguments(),
ArgumentParser.ESC_COMMA, ",") + ")";
}
/** Set the String representation of the arguments for this Call step. */
public void setArguments(String[] args) {
if (args == null)
args = new String[0];
this.args = args;
}
/** Designate the arguments for this Call step. The format of this String
is a comma-separated list of String representations. See the
abbot.parsers package for supported String representations.
<p>
@see ArgumentParser#parseArgumentList for a description of the format.
@see #setArguments(String[]) for the preferred method of indicating
the argument list.
*/
public void setArguments(String encodedArgs) {
if (encodedArgs == null)
args = new String[0];
else
args = ArgumentParser.parseArgumentList(encodedArgs);
}
public void setMethodName(String mn) {
if (mn == null) {
throw new NullPointerException("Method name may not be null");
}
methodName = mn;
}
/** Method name to save in script. */
public String getMethodName() {
return methodName;
}
public String getTargetClassName() {
return targetClassName;
}
public void setTargetClassName(String cn) {
if (cn == null)
usage(Strings.get("call.class_missing"));
targetClassName = cn;
}
/** Attributes to save in script. */
public Map getAttributes() {
Map map = super.getAttributes();
map.put(TAG_CLASS, getTargetClassName());
map.put(TAG_METHOD, getMethodName());
if (args.length != 0) {
map.put(TAG_ARGS, getEncodedArguments());
}
return map;
}
/** Return the arguments as an array of String.
@deprecated use getArguments().
*/
public String[] getArgs() { return getArguments(); }
/** Return the arguments as an array of String. */
public String[] getArguments() { return args; }
protected void runStep() throws Throwable {
try {
invoke();
}
catch(Exception e) {
Log.debug(e);
throw e;
}
}
protected Object evaluateParameter(Method m, String param, Class type)
throws Exception {
return ArgumentParser.eval(getResolver(), param, type);
}
/** Convert the String representation of the arguments into actual
* arguments.
*/
protected Object[] evaluateParameters(Method m, String[] params)
throws Exception {
Object[] args = new Object[params.length];
Class[] types = m.getParameterTypes();
for (int i=0;i < args.length;i++) {
args[i] = evaluateParameter(m, params[i], types[i]);
}
return args;
}
/** Make the target method invocation. This uses
<code>evaluateParameters</code> to convert the String representation
of the arguments into actual arguments.
Tries all matching methods of N arguments.
*/
protected Object invoke() throws Throwable {
boolean retried = false;
Method[] m = getMethods();
for (int i=0;i < m.length;i++) {
try {
Object[] params = evaluateParameters(m[i], args);
try {
Object target = getTarget(m[i]);
Log.debug("Invoking " + m[i] + " on " + target
+ getEncodedArguments() + "'");
if (target != null
&& !m[i].getDeclaringClass().
isAssignableFrom(target.getClass())) {
// If the class loader mismatches, try to resolve it
if (retried) {
String msg = "Class loader mismatch? target "
+ target.getClass().getClassLoader()
+ " vs. method "
+ m[i].getDeclaringClass().getClassLoader();
throw new IllegalArgumentException(msg);
}
retried = true;
m = resolveMethods(m[i].getName(),
target.getClass(), null);
i=-1;
continue;
}
if ((m[i].getModifiers()&Modifier.PUBLIC) == 0
|| (m[i].getDeclaringClass().getModifiers()
& Modifier.PUBLIC) == 0) {
Log.debug("Bypassing compiler access restrictions on "
+ "method " + m[i]);
m[i].setAccessible(true);
}
return m[i].invoke(getTarget(m[i]), params);
}
catch(java.lang.reflect.InvocationTargetException ite) {
throw ite.getTargetException();
}
}
catch(IllegalArgumentException e) {
if (i == m.length - 1)
throw e;
}
}
throw new IllegalArgumentException("Can't invoke method "
+ m[0].getName());
}
/** Return matching methods to be used for invocation. */
public Method getMethod()
throws ClassNotFoundException, NoSuchMethodException {
return resolveMethod(getMethodName(), getTargetClass(), null);
}
/** Return matching methods to be used for invocation. */
protected Method[] getMethods()
throws ClassNotFoundException, NoSuchMethodException {
return resolveMethods(getMethodName(), getTargetClass(), null);
}
/** Get the class of the target of the method invocation. This is public
* to provide editors access to the class being used (for example,
* providing a menu of all available methods).
*/
public Class getTargetClass() throws ClassNotFoundException {
return resolveClass(getTargetClassName());
}
/** Return the target of the invocation. The default implementation
* always returns null for static methods; it will attempt to instantiate
* a target for non-static methods.
*/
protected Object getTarget(Method m) throws Throwable {
if ((m.getModifiers() & Modifier.STATIC) == 0) {
try {
return getTargetClass().newInstance();
}
catch(Exception e) {
setScriptError(new InvalidScriptException("Can't create an object instance of class " + getTargetClassName() + " for non-static method " + m.getName()));
}
}
return null;
}
/** Look up all methods in the given class with the given name and return
type, having the number of arguments in this step.
@see #getArguments()
@throws NoSuchMethodException if no matching method is found
*/
protected Method[] resolveMethods(String name, Class cls,
Class returnType)
throws NoSuchMethodException {
// use getDeclaredMethods to include class methods
Log.debug("Resolving methods on " + cls);
Method[] mlist = cls.getMethods();
ArrayList found = new ArrayList();
for (int i=0;i < mlist.length;i++) {
Method m = mlist[i];
Class[] params = m.getParameterTypes();
if (m.getName().equals(name)
&& params.length == args.length
&& (returnType == null
|| m.getReturnType().equals(returnType))) {
found.add(m);
}
}
if (found.size() == 0) {
throw new NoSuchMethodException(Strings.get("call.no_matching_method",
new Object[] {
name, (returnType == null
? "*"
: returnType.toString()),
String.valueOf(args.length), cls }));
}
// TODO Now sort according to restrictiveness of method arguments
Method[] list = (Method[])found.toArray(new Method[found.size()]);
//Arrays.sort(list);
return list;
}
/** Look up the given method name in the given class with the requested
return type, having the number of arguments in this step.
@throws IllegalArgumentException if not exactly one match exists
@see #getArguments()
*/
protected Method resolveMethod(String name, Class cls, Class returnType)
throws NoSuchMethodException {
Method[] methods = resolveMethods(name, cls, returnType);
if (methods.length != 1) {
return disambiguateMethod(methods);
}
return methods[0];
}
/** Try to distinguish betwenn the given methods.
@throws IllegalArgumentException indicating the appropriate target
method can't be distinguished.
*/
protected Method disambiguateMethod(Method[] methods){
String msg = Strings.get("call.multiple_methods", new Object[] {
methods[0].getName(), methods[0].getDeclaringClass()
});
throw new IllegalArgumentException(msg);
}
}