package abbot.script; import java.awt.Component; import java.io.*; import java.lang.reflect.*; import java.util.*; import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; import abbot.Log; import abbot.i18n.Strings; import abbot.tester.ComponentTester; /** * Provides access to one step (line) from a script. A Step is the basic * unit of execution. * <b>Custom Step classes</b><p> * All custom {@link Step} classes must supply a {@link Constructor} with the * signature <code><init>(${link Resolver}, {@link Map})</code>. If * the step has contents (e.g. {@link Sequence}), then it should also * provide a {@link Constructor} with the signature * <code><init>({@link Resolver}, {@link Element}, {@link Map})</code>. * <p> * The XML tag for a given {@link Step} will be used to auto-generate the * {@link Step} class name, e.g. the tag <aphrodite/> causes the * parser to create an instance of class <code>abbot.script.Aphrodite</code>, * using one of the {@link Constructor}s described above. * <p> * All derived classes should include an entry in the * <a href={@docRoot}/../abbot.xsd>schema</a>, or validation must be turned * off by setting the System property * <code>abbot.script.validate=false</code>. * <p> * You can make the custom <code>Aphrodite</code> step do just about anything * by overriding the {@link #runStep()} method. * <p> * See the source for any {@link Step} implementation in this package for * examples. */ public abstract class Step implements XMLConstants, XMLifiable, Serializable { private static final long serialVersionUID = 1L; private String description = null; private Resolver resolver; /** Error encountered on parse. */ private Throwable invalidScriptError = null; public Step(Resolver resolver, Map attributes) { this(resolver, ""); Log.debug("Instantiating " + getClass()); if (Log.expectDebugOutput) { Iterator iter = attributes.keySet().iterator(); while (iter.hasNext()) { String key = (String)iter.next(); Log.debug(key + "=" + attributes.get(key)); } } parseStepAttributes(attributes); } public Step(Resolver resolver, String description) { // Kind of a hack; a Script is its own resolver if (resolver == null) { if (!(this instanceof Resolver)) { throw new Error("Resolver must be provided"); } resolver = (Resolver)this; } else if (this instanceof Resolver) { resolver = (Resolver)this; } this.resolver = resolver; if ("".equals(description)) description = null; this.description = description; } /** Only exposed so that Script may invoke it on load from disk. */ protected final void parseStepAttributes(Map attributes) { Log.debug("Parsing attributes for " + getClass()); description = (String)attributes.get(TAG_DESC); } /** Main run method. Should <b>never</b> be run on the event dispatch * thread, although no check is explicitly done here. */ public final void run() throws Throwable { if (invalidScriptError != null) throw invalidScriptError; Log.debug("Running " + toString()); runStep(); } /** Implement the step's behavior here. */ protected abstract void runStep() throws Throwable; public String getDescription() { return description != null ? description : getDefaultDescription(); } public void setDescription(String desc) { description = desc; } /** Define the XML tag to use for this script step. */ public abstract String getXMLTag(); /** Provide a usage String for this step. */ public abstract String getUsage(); /** Return a reasonable default description for this script step. This value is used in the absence of an explicit description. */ public abstract String getDefaultDescription(); /** For use by subclasses when an error is encountered during parsing. * Should only be used by the XML parsing ctors. */ protected void setScriptError(Throwable thr) { if (invalidScriptError == null) { invalidScriptError = thr; } else { Log.warn("More than one script error encountered: " + thr); Log.warn("Already have: " + invalidScriptError); } } /** Throw an invalid script exception describing the proper script * usage. This should be used by derived classes whenever parsing * indicates invalid input. */ protected void usage() { usage(null); } /** Store an invalid script exception describing the proper script * usage. This should be used by derived classes whenever parsing * indicates invalid input. */ protected void usage(String details) { String msg = getUsage(); if (details != null) { msg = Strings.get("step.usage", new Object[] { msg, details }); } setScriptError(new InvalidScriptException(msg)); } /** Attributes to save in script. */ public Map getAttributes() { Map map = new HashMap(); if (description != null && !description.equals(getDefaultDescription())) map.put(TAG_DESC, description); return map; } public Resolver getResolver() { return resolver; } /** Override if the step actually has some contents. In most cases, it * won't. */ protected Element addContent(Element el) { return el; } /** Add an attribute to the given XML Element. Attributes are kept in alphabetical order. */ protected Element addAttributes(Element el) { // Use a TreeMap to keep the attributes sorted on output Map atts = new TreeMap(getAttributes()); Iterator iter = atts.keySet().iterator(); while (iter.hasNext()) { String key = (String)iter.next(); String value = (String)atts.get(key); if (value == null) { Log.warn("Attribute '" + key + "' value was null in step " + getXMLTag()); value = ""; } el.setAttribute(key, value); } return el; } /** Convert this Step into a String suitable for editing. The default is the XML representation of the Step. */ public String toEditableString() { return toXMLString(this); } /** Provide a one-line XML string representation. */ public static String toXMLString(XMLifiable obj) { // Comments are the only things that aren't actually elements... if (obj instanceof Comment) { return "<!-- " + ((Comment)obj).getDescription() + " -->"; } Element el = obj.toXML(); StringWriter writer = new StringWriter(); try { XMLOutputter outputter = new XMLOutputter(); outputter.output(el, writer); } catch(IOException io) { Log.warn(io); } return writer.toString(); } /** Convert the object to XML. */ public Element toXML() { return addAttributes(addContent(new Element(getXMLTag()))); } /** Create a new step from an in-line XML string. */ public static Step createStep(Resolver resolver, String str) throws InvalidScriptException, IOException { StringReader reader = new StringReader(str); try { SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(reader); Element el = doc.getRootElement(); return createStep(resolver, el); } catch(JDOMException e) { throw new InvalidScriptException(e.getMessage()); } } /** Convert the attributes in the given XML Element into a Map of name/value pairs. */ protected static Map createAttributeMap(Element el) { Log.debug("Creating attribute map for " + el); Map attributes = new HashMap(); Iterator iter = el.getAttributes().iterator(); while (iter.hasNext()) { Attribute att = (Attribute)iter.next(); attributes.put(att.getName(), att.getValue()); } return attributes; } /** * Factory method, equivalent to a "fromXML" for step creation. Looks for * a class with the same name as the XML tag, with the first letter * capitalized. For example, <call /> is abbot.script.Call. */ public static Step createStep(Resolver resolver, Element el) throws InvalidScriptException { String tag = el.getName(); Map attributes = createAttributeMap(el); String name = tag.substring(0, 1).toUpperCase() + tag.substring(1); if (tag.equals(TAG_WAIT)) { attributes.put(TAG_WAIT, "true"); name = "Assert"; } try { name = "abbot.script." + name; Log.debug("Instantiating " + name); Class cls = Class.forName(name); try { // Steps with contents require access to the XML element Class[] argTypes = new Class[] { Resolver.class, Element.class, Map.class }; Constructor ctor = cls.getConstructor(argTypes); return (Step)ctor.newInstance(new Object[] { resolver, el, attributes }); } catch(NoSuchMethodException nsm) { // All steps must support this ctor Class[] argTypes = new Class[] { Resolver.class, Map.class }; Constructor ctor = cls.getConstructor(argTypes); return (Step)ctor.newInstance(new Object[] { resolver, attributes }); } } catch(ClassNotFoundException cnf) { String msg = Strings.get("step.unknown_tag", new Object[] { tag }); throw new InvalidScriptException(msg); } catch(InvocationTargetException ite) { Log.warn(ite); throw new InvalidScriptException(ite.getTargetException(). getMessage()); } catch(Exception exc) { Log.warn(exc); throw new InvalidScriptException(exc.getMessage()); } } protected String simpleClassName(Class cls) { return ComponentTester.simpleClassName(cls); } /** Return a description of this script step. */ public String toString() { return getDescription(); } /** Returns the Class corresponding to the given class name. Provides * just-in-time classname resolution to ensure loading by the proper class * loader. <p> */ public Class resolveClass(String className) throws ClassNotFoundException { ClassLoader cl = getResolver().getContextClassLoader(); return Class.forName(className, true, cl); } /** Look up an appropriate ComponentTester given an arbitrary * Component-derived class. * If the class is derived from abbot.tester.ComponentTester, instantiate * one; if it is derived from java.awt.Component, return a matching Tester. * Otherwise return abbot.tester.ComponentTester.<p> * @throws ClassNotFoundException If the given class can't be found. * @throws IllegalArgumentException If the tester cannot be instantiated. */ protected ComponentTester resolveTester(String className) throws ClassNotFoundException { Class testedClass = resolveClass(className); if (Component.class.isAssignableFrom(testedClass)) return ComponentTester.getTester(testedClass); else if (ComponentTester.class.isAssignableFrom(testedClass)) { try { return (ComponentTester)testedClass.newInstance(); } catch(Exception e) { String msg = "Custom ComponentTesters must provide " + "an accessible no-args Constructor: " + e.getMessage(); throw new IllegalArgumentException(msg); } } String msg = "The given class '" + className + "' is neither a Component nor a ComponentTester"; throw new IllegalArgumentException(msg); } private void writeObject(ObjectOutputStream out) { // NOTE: this is only to avoid drag/drop errors out = null; } private void readObject(ObjectInputStream in) { // NOTE: this is only to avoid drag/drop errors in = null; } }