package abbot.script;
import java.awt.Component;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.WeakHashMap;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import abbot.Log;
import abbot.Platform;
import abbot.finder.AWTHierarchy;
import abbot.finder.Hierarchy;
import abbot.i18n.Strings;
import abbot.script.parsers.FileParser;
import abbot.script.parsers.Parser;
import abbot.tester.Robot;
import abbot.util.Properties;
/**
* Provide a structure to encapsulate actions invoked on GUI components and
* tests performed on those components. Scripts need to be short and concise
* (and therefore easy to read/write). Extensions don't have to be.<p>
* This takes a single filename as a constructor argument.<p>
* Use {@link junit.extensions.abbot.ScriptFixture} and
* {@link junit.extensions.abbot.ScriptTestSuite}
* to generate a suite by auto-generating a collection of {@link Script}s.<p>
* @see StepRunner
* @see Fixture
* @see Launch
*/
public class Script extends Sequence implements Resolver {
public static final String INTERPRETER = "bsh";
private static final String USAGE =
"<AWTTestScript [desc=\"\"] [forked=\"true\"] [slow=\"true\"]"
+ " [awt=\"true\"] [vmargs=\"...\"]>...</AWTTestScript>\n";
/** Robot delay for slow playback. */
private static int slowDelay = 250;
static boolean validate = true;
private String filename;
private File relativeDirectory;
private boolean fork;
private boolean slow;
private boolean awt;
private int lastSaved;
private String vmargs;
private Map properties = new HashMap();
private Hierarchy hierarchy;
public static final String UNTITLED_FILE =
Strings.get("script.untitled_filename");
protected static final String UNTITLED =
Strings.get("script.untitled");
/** Read-only map of ref IDs into ComponentReferences. */
private Map refs = Collections.unmodifiableMap(new HashMap());
/** Maps components to references. This cache provides a 20% speedup when
* adding new references.
*/
private Map components = new WeakHashMap();
static {
slowDelay = Properties.getProperty("abbot.script.slow_delay",
slowDelay, 0, 60000);
String defValue = Platform.JAVA_VERSION < Platform.JAVA_1_4
? "false" : "true";
validate = "true".equals(System.getProperty("abbot.script.validate",
defValue));
}
protected static Map createDefaultMap(String filename) {
Map map = new HashMap();
map.put(TAG_FILENAME, filename);
return map;
}
/** Create a new, empty <code>Script</code>. Used as a temporary
* {@link Resolver}, uses the default {@link Hierarchy}.
* @deprecated Use an explicit {@link Hierarchy} instead.
*/
public Script() {
// This is roughly equivalent to what
// DefaultComponentFinder.getFinder() used to do
this(AWTHierarchy.getDefault());
}
/** Create a <code>Script</code> from the given filename. Uses the
default {@link Hierarchy}.
@deprecated Use an explicit {@link Hierarchy} instead.
*/
public Script(String filename) {
// This is roughly equivalent to what
// DefaultComponentFinder.getFinder() used to do
this(filename, AWTHierarchy.getDefault());
}
public Script(Hierarchy h) {
this(null, new HashMap());
setHierarchy(h);
}
/** Create a <code>Script</code> from the given file. */
public Script(String filename, Hierarchy h) {
this(null, createDefaultMap(filename));
setHierarchy(h);
}
public Script(Resolver parent, Map attributes) {
super(parent, attributes);
String filename = (String)attributes.get(TAG_FILENAME);
File file = filename != null
? new File(filename)
: getTempFile(parent != null ? parent.getDirectory() : null);
setFile(file);
if (parent != null) {
setRelativeTo(parent.getDirectory());
}
try {
load();
}
catch(IOException e) {
setScriptError(e);
}
}
/** Since we allow ComponentReference IDs to be changed, make sure our map
* is always up to date.
*/
private synchronized void synchReferenceIDs() {
HashMap map = new HashMap();
Iterator iter = refs.values().iterator();
while (iter.hasNext()) {
ComponentReference ref = (ComponentReference)iter.next();
map.put(ref.getID(), ref);
}
if (!refs.equals(map)) {
// atomic update of references map
refs = Collections.unmodifiableMap(map);
}
}
public void setHierarchy(Hierarchy h) {
hierarchy = h;
components.clear();
}
private File getTempFile(File dir) {
File file;
try {
file = (dir != null
? File.createTempFile(UNTITLED_FILE, ".xml", dir)
: File.createTempFile(UNTITLED_FILE, ".xml"));
// We don't actually need the file on disk
file.delete();
}
catch(IOException io) {
file = (dir != null
? new File(dir, UNTITLED_FILE + ".xml")
: new File(UNTITLED_FILE + ".xml"));
}
return file;
}
public String getName() {
return filename;
}
public void setForked(boolean fork) {
this.fork = fork;
}
public boolean isForked() { return fork; }
public void setVMArgs(String args) {
if (args != null && "".equals(args))
args = null;
vmargs = args;
}
public String getVMArgs() { return vmargs; }
public boolean isSlowPlayback() { return slow; }
public void setSlowPlayback(boolean slow) { this.slow = slow; }
public boolean isAWTMode() { return awt; }
public void setAWTMode(boolean awt) { this.awt = awt; }
/** Return the file where this script is saved. Will always be an
* absolute path.
*/
public File getFile() {
File file = new File(filename);
if (!file.isAbsolute()) {
String path =
getRelativeTo().getPath() + File.separator + filename;
file = new File(path);
}
return file;
}
/** Change the file system basis for the current script. Does not affect
the script contents.
@deprecated Use {@link #setFile(File)}.
*/
public void changeFile(File file) {
setFile(file);
}
/** Set the file system basis for this script object. Use this to set the
file from which the existing script will be loaded.
*/
public void setFile(File file) {
Log.debug("Script file set to " + file);
if (file == null)
throw new IllegalArgumentException("File must not be null");
if (filename == null || !file.equals(getFile())) {
filename = file.getPath();
Log.debug("Script filename set to " + filename);
if (relativeDirectory != null)
setRelativeTo(relativeDirectory);
}
lastSaved = getHash() + 1;
}
/** Typical xml header, so we can know about how much file prefix to
* skip.
*/
private static final String XML_INFO =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>";
/** Flag to indicate whether emitted XML should contain the script
contents. Sometimes we just want a one-liner (like when displaying in
the script editor), and sometimes we want the full contents (when
writing to file).
*/
private boolean formatForSave = false;
/** Write the current state of the script to file. */
public void save(Writer writer) throws IOException {
formatForSave = true;
Element el = toXML();
formatForSave = false;
el.setName(TAG_AWTTESTSCRIPT);
Document doc = new Document(el);
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
outputter.output(doc, writer);
}
/** Only thing directly editable on a script is its file path. */
public String toEditableString() {
return getFilename();
}
/** Has this script changed since the last save. */
public boolean isDirty() {
return getHash() != lastSaved;
}
/** Write the script to file. Note that this differs from the toXML for
the script, which simply indicates the file on which it is based. */
public void save() throws IOException {
File file = getFile();
Log.debug("Saving script to '" + file + "' " + hashCode());
OutputStreamWriter writer =
new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
save(new BufferedWriter(writer));
writer.close();
lastSaved = getHash();
}
/** Ensure that all referenced components are actually in the components
* list.
*/
private synchronized void verify() throws InvalidScriptException {
Log.debug("verifying all referenced refs exist");
Iterator iter = refs.values().iterator();
while (iter.hasNext()) {
ComponentReference ref = (ComponentReference)iter.next();
String id = ref.getAttribute(TAG_PARENT);
if (id != null && refs.get(id) == null) {
String msg = Strings.get("script.parent_missing",
new Object[] { id });
throw new InvalidScriptException(msg);
}
id = ref.getAttribute(TAG_WINDOW);
if (id != null && refs.get(id) == null) {
String msg = Strings.get("script.window_missing",
new Object[] { id });
throw new InvalidScriptException(msg);
}
}
}
/** Make the path to the given child script relative to this one. */
private void updateRelativePath(Step child) {
// Make sure included scripts are located relative to this one
if (child instanceof Script) {
((Script)child).setRelativeTo(getDirectory());
}
}
protected void parseChild(Element el) throws InvalidScriptException {
if (el.getName().equals(TAG_COMPONENT)) {
addComponentReference(el);
}
else {
synchronized(steps()) {
super.parseChild(el);
updateRelativePath((Step)steps().get(size()-1));
}
}
}
/** Parse XML attributes for the Script. */
protected void parseAttributes(Map map) {
parseStepAttributes(map);
fork = Boolean.valueOf((String)map.get(TAG_FORKED)).booleanValue();
slow = Boolean.valueOf((String)map.get(TAG_SLOW)).booleanValue();
awt = Boolean.valueOf((String)map.get(TAG_AWT)).booleanValue();
vmargs = (String)map.get(TAG_VMARGS);
}
/** Loads the XML test script. Performs a check against the XML schema.
@param reader Provides the script data
@throws InvalidScriptException
@throws IOException
*/
public void load(Reader reader)
throws InvalidScriptException, IOException {
clear();
/* try {
// Set things up to optionally validate on load
SAXBuilder builder =
new SAXBuilder("org.apache.xerces.parsers.SAXParser", false);
if (validate) {
URL url = getClass().getClassLoader().getResource("abbot/abbot.xsd");
if (url != null) {
builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser", true);
builder.setFeature("http://apache.org/xml/features/validation/schema", true);
builder.setProperty("http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation", url.toString());
}
else {
Log.warn("Could not find abbot/abbot.xsd, disabling XML validation");
validate = false;
}
}
Document doc = builder.build(reader);
Element root = doc.getRootElement();
Map map = createAttributeMap(root);
parseAttributes(map);
parseChildren(root);
}
catch(JDOMException e) {
throw new InvalidScriptException(e.getMessage());
}
*/ // Make sure we have all referenced components
synchronized(this) {
synchReferenceIDs();
verify();
}
lastSaved = getHash();
}
public void addStep(int index, Step step) {
super.addStep(index, step);
updateRelativePath(step);
}
public void addStep(Step step) {
super.addStep(step);
updateRelativePath(step);
}
/** Replaces the step at the given index. */
public void setStep(int index, Step step) {
super.setStep(index, step);
updateRelativePath(step);
}
/** Read the script from the currently set file. */
public void load() throws IOException {
File file = getFile();
if (!file.exists()) {
if (getFilename().indexOf(Script.UNTITLED_FILE) == -1)
Log.warn("Script " + this + " does not exist, ignoring it");
return;
}
if (!file.isFile())
throw new InvalidScriptException("Path " + getFilename()
+ " refers to a directory");
if (file.length() != 0) {
try {
Reader reader =
new InputStreamReader(new FileInputStream(file), "UTF-8");
try {
load(new BufferedReader(reader));
}
finally {
try { reader.close(); } catch(IOException e) { }
}
}
catch(FileNotFoundException e) {
// should have been detected
Log.warn("File '" + file + "' exists but is not found");
}
}
else {
Log.warn("Script file " + this + " is empty");
}
}
protected String getFullXMLString() {
try {
formatForSave = true;
return toXMLString(this);
}
finally {
formatForSave = false;
}
}
private int getHash() {
return getFullXMLString().hashCode();
}
public String getXMLTag() {
return TAG_SCRIPT;
}
/** Save component references in addition to everything else. */
public Element addContent(Element el) {
// Only save content if writing to disk
if (formatForSave) {
synchReferenceIDs();
Iterator iter = new TreeSet(refs.values()).iterator();
while (iter.hasNext()) {
ComponentReference cref = (ComponentReference)iter.next();
el.addContent(cref.toXML());
}
// Now collect our child steps
return super.addContent(el);
}
return el;
}
/** Return the (possibly relative) path to this script. */
public String getFilename() { return filename; }
/** Provide XML attributes for this Step. This class adds its filename. */
public Map getAttributes() {
Map map;
if (!formatForSave) {
map = new HashMap();
map.put(TAG_FILENAME, getFilename());
}
else {
map = super.getAttributes();
// default is no fork
if (fork) {
map.put(TAG_FORKED, "true");
if (vmargs != null) {
map.put(TAG_VMARGS, vmargs);
}
}
if (slow) {
map.put(TAG_SLOW, "true");
}
if (awt) {
map.put(TAG_AWT, "true");
}
}
return map;
}
protected void runStep(StepRunner runner) throws Throwable {
components.clear();
properties.clear();
// Make all files relative to this script
Parser fc = new FileParser() {
public String relativeTo() {
Log.debug("All file references will be relative to "
+ getDirectory().getAbsolutePath());
return getDirectory().getAbsolutePath();
}
};
Parser oldfc = ArgumentParser.setParser(File.class, fc);
int oldDelay = Robot.getAutoDelay();
int oldMode = Robot.getEventMode();
if (slow) {
Robot.setAutoDelay(slowDelay);
}
if (awt) {
Robot.setEventMode(Robot.EM_AWT);
}
try {
super.runStep(runner);
}
finally {
Robot.setAutoDelay(oldDelay);
Robot.setEventMode(oldMode);
ArgumentParser.setParser(File.class, oldfc);
}
}
/** Set up a blank script, discarding any current state. */
public void clear() {
setScriptError(null);
refs = Collections.unmodifiableMap(new HashMap());
components.clear();
super.clear();
}
public String getUsage() { return USAGE; }
/** Return a default description for this <code>Script</code>. */
public String getDefaultDescription() {
String ext = fork ? " &" : "";
String desc = Strings.get("script.desc",
new Object[] { getFilename(), ext });
return desc.indexOf(UNTITLED_FILE) != -1 ? UNTITLED : desc;
}
/** Return whether this <code>Script</code> is launchable. */
public boolean hasLaunch() {
// First step might be a Launch or a Fixture
return size() > 0 && (((Step)steps().get(0)) instanceof UIContext);
}
/** @return The {@link UIContext} responsible for setting up
* a UI context for this script, or
* <code>null</code> if the script has no UI to speak of.
*/
public UIContext getUIContext() {
synchronized(steps()) {
if (hasLaunch()) {
return (UIContext)steps().get(0);
}
}
return null;
}
/** Defer to the {@link UIContext} to obtain a
* {@link ClassLoader}, or use the current {@link Thread}'s
* context class loader.
* @see Thread#getContextClassLoader()
*/
public ClassLoader getContextClassLoader() {
UIContext context = getUIContext();
return context != null
? context.getContextClassLoader()
: Thread.currentThread().getContextClassLoader();
}
public boolean hasTerminate() {
return size() > 0
&& (((Step)steps().get(size()-1)) instanceof Terminate);
}
/** By default, all pathnames are relative to the current working
directory.
*/
public File getRelativeTo() {
if (relativeDirectory == null)
return new File(System.getProperty("user.dir"));
return relativeDirectory;
}
/** Indicate that when invoking toXML, a path relative to the given one
* should be shown. Note that this is a runtime setting only and never
* shows up in saved XML.
*/
public void setRelativeTo(File dir) {
Log.debug("Want relative dir " + dir);
relativeDirectory = dir;
if (dir != null) {
// FIXME ideally, we'd want a more robust "make relative" here.
// for now, simply check to see if the relpath is a prefix.
// or if the file itself is relative
String relPath = dir.getPath();
if (filename.startsWith(relPath)
&& relPath.length() < filename.length()) {
char ch = filename.charAt(relPath.length());
if (ch == '/' || ch == '\\') {
filename = filename.substring(relPath.length() + 1);
}
}
}
Log.debug("Relative dir set to " + relativeDirectory + " for " + this);
}
/** Return whether the given file looks like a valid AWT script. */
public static boolean isScript(File file) {
if (file.length() == 0)
return true;
if (!file.exists() || !file.isFile()
|| file.length() < TAG_AWTTESTSCRIPT.length() * 2 + 5)
return false;
InputStream is = null;
try {
int len = XML_INFO.length() + TAG_AWTTESTSCRIPT.length() + 15;
is = new BufferedInputStream(new FileInputStream(file));
byte[] buf = new byte[len];
is.read(buf, 0, buf.length);
String str = new String(buf, 0, buf.length);
return str.indexOf(TAG_AWTTESTSCRIPT) != -1;
}
catch(Exception exc) {
return false;
}
finally {
if (is != null)
try { is.close(); } catch(Exception exc) { }
}
}
/** All relative files should be accessed relative to this directory,
which is the directory where the script resides.
It will always return an absolute path.
*/
public File getDirectory() {
return getFile().getParentFile();
}
/** Returns a sorted collection of ComponentReferences. */
public Collection getComponentReferences() {
return new TreeSet(refs.values());
}
/** Add a component reference directly, replacing any existing one with
the same ID.
*/
public void addComponentReference(ComponentReference ref) {
Log.debug("adding " + ref);
synchReferenceIDs();
HashMap map = new HashMap(refs);
map.put(ref.getID(), ref);
// atomic update of references map
refs = Collections.unmodifiableMap(map);
}
/** Add a new component reference for the given component. */
// FIXME: a repaint (tree locked) which accesses the refs list
// deadlocks with cref creation (locks refs, asks for tree lock)
// Either get tree lock first or don't require refs lock on read
public ComponentReference addComponent(Component comp) {
synchReferenceIDs();
Log.debug("look up existing for " + Robot.toString(comp));
Map newRefs = new HashMap();
ComponentReference ref =
ComponentReference.getReference(this, comp, newRefs);
Log.debug("adding " + Robot.toString(comp));
Map map = new HashMap(refs);
map.putAll(newRefs);
Iterator iter = newRefs.values().iterator();
while (iter.hasNext()) {
ComponentReference r = (ComponentReference)iter.next();
Component c = r.getCachedLookup(getHierarchy());
if (c != null)
components.put(c, r);
}
// atomic update of references map
refs = Collections.unmodifiableMap(map);
return ref;
}
/** Add a new component reference to the script. For use only when
* parsing a script.
*/
ComponentReference addComponentReference(Element el)
throws InvalidScriptException {
synchReferenceIDs();
ComponentReference ref = new ComponentReference(this, el);
Log.debug("adding " + el);
Map map = new HashMap(refs);
map.put(ref.getID(), ref);
// atomic update of references map
refs = Collections.unmodifiableMap(map);
return ref;
}
/** Return the reference for the given component, or null if none yet
* exists.
*/
public ComponentReference getComponentReference(Component comp) {
if (!getHierarchy().contains(comp)) {
String msg = Strings.get("script.not_in_hierarchy",
new Object[] { comp.toString() });
throw new IllegalArgumentException(msg);
}
synchReferenceIDs();
// Clear the component map if any one of the mappings is invalid
Iterator iter = refs.values().iterator();
while (iter.hasNext()) {
ComponentReference cr = (ComponentReference)iter.next();
if (cr.getCachedLookup(getHierarchy()) == null) {
components.clear();
break;
}
}
ComponentReference ref = (ComponentReference)components.get(comp);
if (ref != null) {
if (ref.getCachedLookup(getHierarchy()) != null)
return ref;
components.remove(comp);
}
ref = ComponentReference.matchExisting(comp, refs.values());
if (ref != null) {
components.put(comp, ref);
}
return ref;
}
/** Convert the given reference ID into a component reference. If it's
* not in the Script's list, returns null.
*/
public ComponentReference getComponentReference(String name) {
synchReferenceIDs();
return (ComponentReference)refs.get(name);
}
public void setProperty(String name, Object value) {
if (value == null)
properties.remove(name);
else
properties.put(name, value);
}
public Object getProperty(String name) {
Object value = properties.get(name);
// Lazy-load the interpreter, so it's only instantiated when required
if (value == null && INTERPRETER.equals(name)) {
// Interpreter bsh = new Interpreter(this);
// properties.put(name, value = bsh);
properties.put(name, "bsh");
}
return value;
}
/** Return the currently effective {@link Hierarchy} of components. */
public Hierarchy getHierarchy() {
Resolver r = getResolver();
if (r != null && r != this)
return r.getHierarchy();
return hierarchy != null ? hierarchy : AWTHierarchy.getDefault();
}
/** Return a meaningful description of where the Step came from. */
public String getContext(Step step) {
return getFile().toString() + ":" + getLine(this, step);
}
/** Return the file which defines the given step. */
public static File getFile(Step step) {
String context = step.getResolver().getContext(step);
int colon = context.indexOf(":");
if (colon == 1 && Character.isLetter(context.charAt(0))
&& Platform.isWindows() && context.length() > 2)
colon = context.indexOf(":", 2);
if (colon != -1)
context = context.substring(0, colon);
return new File(context);
}
/** Return the approximate line number of the given step. File lines are
one-based (there is no line zero).
*/
public static int getLine(Step step) {
String context = step.getResolver().getContext(step);
int colon = context.indexOf(":");
if (colon == 1 && Character.isLetter(context.charAt(0))
&& Platform.isWindows() && context.length() > 2)
colon = context.indexOf(":", 2);
context = context.substring(colon + 1);
try {
return Integer.parseInt(context);
}
catch(NumberFormatException e) {
return -1;
}
}
private static int getLine(Sequence seq, Step step) {
int line = -1;
int index = seq.indexOf(step);
if (index == -1) {
List list = seq.steps();
for (int i=0;i < list.size();i++) {
Step sub = (Step)list.get(i);
if (sub instanceof Sequence) {
int subline = getLine((Sequence)sub, step);
if (subline != -1) {
line = countLines(seq, i) + subline;
break;
}
}
}
}
else {
line = countLines(seq, index);
}
return line;
}
/** Return the number of XML lines in the given sequence that precede the
* given index. If the index is -1, return the number of XML lines used
* by the whole sequence.
*/
public static int countLines(Sequence seq, int index) {
int count = 1;
int limit = index;
if (limit == -1) {
limit = seq.size();
// Empty sequences take a single line
if (limit == 0)
return 1;
// Otherwise, 2 + the line count of the contents
count = 2;
}
if (seq instanceof Script) {
// Add in one line per component reference in the script
// Plus two lines for the <xml> and <AWTTestScript> lines
count += ((Script)seq).getComponentReferences().size() + 2;
}
for (int i=0;i < limit;i++) {
Step step = seq.getStep(i);
// Included scripts take only one line
if (step instanceof Script) {
++count;
}
else if (step instanceof Sequence) {
count += countLines((Sequence)step, -1);
}
else {
++count;
}
}
return count;
}
}