package abbot.script;
import java.awt.Window;
import java.lang.reflect.*;
import java.util.Map;
import java.util.Iterator;
import abbot.finder.Hierarchy;
import abbot.*;
import abbot.i18n.Strings;
/**
* Provides scripted static method invocation. Usage:<br>
* <blockquote><code>
* <launch class="package.class" method="methodName" args="..."
* classpath="..." [threaded=true]><br>
* </code></blockquote><p>
* The args attribute is a comma-separated list of arguments to pass to the
* class method, and may use square brackets to denote an array,
* e.g. "[one,two,three]" will be interpreted as an array length 3
* of String. The square brackets may be escaped ('\[' or '\]') to include
* them literally in an argument.
* <p>
* The class path attribute may use either colon or semicolon as a path
* separator, but should preferably use relative paths to avoid making the
* containing script platform- and location-dependent.<p>
* In most cases, the classes under test will <i>only</i> be found under the
* custom class path, and so the parent class loader will fail to find them.
* If this is the case then the classes under test will be properly discarded
* on each launch when a new class loader is created.
* <p>
* The 'threaded' attribute is provided in case your code under test requires
* GUI event processing prior to returning from its invoked method. An
* example might be a main method which invokes dialog and waits for the
* response before continuing. In general, it's better to refactor the code
* if possible so that the main method turns over control to the event
* dispatch thread as soon as possible. Otherwise, if the application under
* test is background threaded by the Launch step, any runtime exceptions
* thrown from the launch code will cause errors in the launch step out of
* sequence with the other script steps. While this won't cause any problems
* for the Abbot framework, it can be very confusing for the user.<p>
* Note that if the "reload" attribute is set true (i.e. Abbot's class loader
* is used to reload code under test), ComponentTester extensions must also be
* loaded by that class loader, so the path to extensions should be included
* in the Launch class path.<p>
*/
public class Launch extends Call implements UIContext {
/** Allow only one active launch at a time. */
private static Launch currentLaunch = null;
private String classpath = null;
private boolean threaded = false;
private transient AppClassLoader classLoader;
private transient ThreadedLaunchListener listener;
private static final String USAGE =
"<launch class=\"...\" method=\"...\" args=\"...\" "
+ "[threaded=true]>";
public Launch(Resolver resolver, Map attributes) {
super(resolver, attributes);
classpath = (String)attributes.get(TAG_CLASSPATH);
String thr = (String)attributes.get(TAG_THREADED);
if (thr != null)
threaded = Boolean.valueOf(thr).booleanValue();
}
public Launch(Resolver resolver, String description,
String className, String methodName, String[] args) {
this(resolver, description, className, methodName, args, null, false);
}
public Launch(Resolver resolver, String description,
String className, String methodName, String[] args,
String classpath, boolean threaded) {
super(resolver, description, className, methodName, args);
this.classpath = classpath;
this.threaded = threaded;
}
public String getClasspath() {
return classpath;
}
public void setClasspath(String cp) {
classpath = cp;
// invalidate class loader
classLoader = null;
}
public boolean isThreaded() {
return threaded;
}
public void setThreaded(boolean thread) {
threaded = thread;
}
protected AppClassLoader createClassLoader() {
return new AppClassLoader(classpath);
}
/** Install the class loader context for the code being launched. The
* context class loader for the current thread is modified.
*/
protected void install() {
ClassLoader loader = getContextClassLoader();
// Everything else loaded on the same thread as this
// launch should be loaded by this custom loader.
if (loader instanceof AppClassLoader
&& !((AppClassLoader)loader).isInstalled()) {
((AppClassLoader)loader).install();
}
}
protected void synchronizedRunStep() throws Throwable {
// A bug in pre-1.4 VMs locks the toolkit prior to notifying AWT event
// listeners. This causes a deadlock when the main method invokes
// "show" on a component which triggers AWT events for which there are
// listeners. To avoid this, grab the toolkit lock first so that the
// locks are acquired in the same order by either sequence.
// (Unfortunately, some swing code locks the tree prior to
// grabbing the toolkit lock, so there's still opportunity for
// deadlock). One alternative (although very heavyweight) is to
// always fork a separate VM.
//
// If threaded, take the danger of deadlock over the possibility that
// the main method will never return and leave the lock forever held.
// NOTE: this is guaranteed to deadlock if "main" calls
// EventQueue.invokeAndWait.
if (Platform.JAVA_VERSION < Platform.JAVA_1_4
&& !isThreaded()) {
synchronized(java.awt.Toolkit.getDefaultToolkit()) {
super.runStep();
}
}
else {
super.runStep();
}
}
/** Perform steps necessary to remove any setup performed by
* this <code>Launch</code> step.
*/
public void terminate() {
Log.debug("launch terminate");
if (currentLaunch == this) {
// Nothing special to do, dispose windows normally
Iterator iter = getHierarchy().getRoots().iterator();
while (iter.hasNext())
getHierarchy().dispose((Window)iter.next());
if (classLoader != null) {
classLoader.uninstall();
classLoader = null;
}
currentLaunch = null;
}
}
/** Launches the UI described by this <code>Launch</code> step,
* using the given runner as controller/monitor.
*/
public void launch(StepRunner runner) throws Throwable {
runner.run(this);
}
/** @return Whether the code described by this launch step is currently active. */
public boolean isLaunched() {
return currentLaunch == this;
}
public Hierarchy getHierarchy() {
return getResolver().getHierarchy();
}
public void runStep() throws Throwable {
if (currentLaunch != null)
currentLaunch.terminate();
currentLaunch = this;
install();
System.setProperty("abbot.framework.launched", "true");
if (isThreaded()) {
Thread threaded = new Thread("Threaded " + toString()) {
public void run() {
try {
synchronizedRunStep();
}
catch(AssertionFailedError e) {
if (listener != null)
listener.stepFailure(Launch.this, e);
}
catch(Throwable t) {
if (listener != null)
listener.stepError(Launch.this, t);
}
}
};
threaded.setDaemon(true);
threaded.setContextClassLoader(classLoader);
threaded.start();
}
else {
synchronizedRunStep();
}
}
/** Overrides the default implementation to always use the class loader
* defined by this step. This works in cases where the Launch step has
* not yet been added to a Script; otherwise the Script will provide an
* implementation equivalent to this one.
*/
public Class resolveClass(String className) throws ClassNotFoundException {
return Class.forName(className, true, getContextClassLoader());
}
/** Return the class loader that uses the classpath defined in this
* step.
*/
public ClassLoader getContextClassLoader() {
if (classLoader == null) {
// Use a custom class loader so that we can provide additional
// classpath and also optionally reload the class on each run.
// FIXME maybe classpath should be relative to the script? In this
// case, it's relative to user.dir
classLoader = createClassLoader();
}
return classLoader;
}
public Class getTargetClass() throws ClassNotFoundException {
Class cls = resolveClass(getTargetClassName());
Log.debug("Target class is " + cls.getName());
return cls;
}
/** Return the target for the method invocation. All launch invocations
* must be static, so this always returns null.
*/
protected Object getTarget(Method m) {
return null;
}
/** Return the method to be used for invocation. */
public Method getMethod()
throws ClassNotFoundException, NoSuchMethodException {
return resolveMethod(getMethodName(), getTargetClass(), null);
}
public Map getAttributes() {
Map map = super.getAttributes();
if (classpath != null) {
map.put(TAG_CLASSPATH, classpath);
}
if (threaded) {
map.put(TAG_THREADED, "true");
}
return map;
}
public String getDefaultDescription() {
String desc = Strings.get("launch.desc",
new Object[] { getTargetClassName()
+ "." + getMethodName()
+ "(" + getEncodedArguments()
+ ")"});
return desc;
}
public String getUsage() { return USAGE; }
public String getXMLTag() { return TAG_LAUNCH; }
/** Set a listener to respond to events when the launch step is
* threaded.
*/
public void setThreadedLaunchListener(ThreadedLaunchListener l) {
listener = l;
}
public interface ThreadedLaunchListener {
public void stepFailure(Launch launch, AssertionFailedError error);
public void stepError(Launch launch, Throwable throwable);
}
/** No two launches are ever considered equivalent. If you want
* a shared {@link UIContext}, use a {@link Fixture}.
* @see abbot.script.UIContext#equivalent(abbot.script.UIContext)
* @see abbot.script.StepRunner#run(Step)
*/
public boolean equivalent(UIContext context) {
return false;
}
}