001: package abbot.script;
002:
003: import java.awt.Component;
004: import java.io.*;
005: import java.lang.reflect.*;
006: import java.util.*;
007:
008: import org.jdom.*;
009: import org.jdom.input.SAXBuilder;
010: import org.jdom.output.XMLOutputter;
011:
012: import abbot.Log;
013: import abbot.i18n.Strings;
014: import abbot.tester.ComponentTester;
015:
016: /**
017: * Provides access to one step (line) from a script. A Step is the basic
018: * unit of execution.
019: * <b>Custom Step classes</b><p>
020: * All custom {@link Step} classes must supply a {@link Constructor} with the
021: * signature <code><init>(${link Resolver}, {@link Map})</code>. If
022: * the step has contents (e.g. {@link Sequence}), then it should also
023: * provide a {@link Constructor} with the signature
024: * <code><init>({@link Resolver}, {@link Element}, {@link Map})</code>.
025: * <p>
026: * The XML tag for a given {@link Step} will be used to auto-generate the
027: * {@link Step} class name, e.g. the tag <aphrodite/> causes the
028: * parser to create an instance of class <code>abbot.script.Aphrodite</code>,
029: * using one of the {@link Constructor}s described above.
030: * <p>
031: * All derived classes should include an entry in the
032: * <a href={@docRoot}/../abbot.xsd>schema</a>, or validation must be turned
033: * off by setting the System property
034: * <code>abbot.script.validate=false</code>.
035: * <p>
036: * You can make the custom <code>Aphrodite</code> step do just about anything
037: * by overriding the {@link #runStep()} method.
038: * <p>
039: * See the source for any {@link Step} implementation in this package for
040: * examples.
041: */
042: public abstract class Step implements XMLConstants, XMLifiable,
043: Serializable {
044:
045: private String description = null;
046: private Resolver resolver;
047: /** Error encountered on parse. */
048: private Throwable invalidScriptError = null;
049:
050: public Step(Resolver resolver, Map attributes) {
051: this (resolver, "");
052: Log.debug("Instantiating " + getClass());
053: if (Log.isClassDebugEnabled(Step.class)) {
054: Iterator iter = attributes.keySet().iterator();
055: while (iter.hasNext()) {
056: String key = (String) iter.next();
057: Log.debug(key + "=" + attributes.get(key));
058: }
059: }
060: parseStepAttributes(attributes);
061: }
062:
063: public Step(Resolver resolver, String description) {
064: // Kind of a hack; a Script is its own resolver
065: if (resolver == null) {
066: if (!(this instanceof Resolver)) {
067: throw new Error("Resolver must be provided");
068: }
069: resolver = (Resolver) this ;
070: } else if (this instanceof Resolver) {
071: resolver = (Resolver) this ;
072: }
073: this .resolver = resolver;
074: if ("".equals(description))
075: description = null;
076: this .description = description;
077: }
078:
079: /** Only exposed so that Script may invoke it on load from disk. */
080: protected final void parseStepAttributes(Map attributes) {
081: Log.debug("Parsing attributes for " + getClass());
082: description = (String) attributes.get(TAG_DESC);
083: }
084:
085: /** Main run method. Should <b>never</b> be run on the event dispatch
086: * thread, although no check is explicitly done here.
087: */
088: public final void run() throws Throwable {
089: if (invalidScriptError != null)
090: throw invalidScriptError;
091: Log.debug("Running " + toString());
092: runStep();
093: }
094:
095: /** Implement the step's behavior here. */
096: protected abstract void runStep() throws Throwable;
097:
098: public String getDescription() {
099: return description != null ? description
100: : getDefaultDescription();
101: }
102:
103: public void setDescription(String desc) {
104: description = desc;
105: }
106:
107: /** Define the XML tag to use for this script step. */
108: public abstract String getXMLTag();
109:
110: /** Provide a usage String for this step. */
111: public abstract String getUsage();
112:
113: /** Return a reasonable default description for this script step.
114: This value is used in the absence of an explicit description.
115: */
116: public abstract String getDefaultDescription();
117:
118: /** For use by subclasses when an error is encountered during parsing.
119: * Should only be used by the XML parsing ctors.
120: */
121: protected void setScriptError(Throwable thr) {
122: if (invalidScriptError == null) {
123: invalidScriptError = thr;
124: } else {
125: Log.warn("More than one script error encountered: " + thr);
126: Log.warn("Already have: " + invalidScriptError);
127: }
128: }
129:
130: /** Throw an invalid script exception describing the proper script
131: * usage. This should be used by derived classes whenever parsing
132: * indicates invalid input.
133: */
134: protected void usage() {
135: usage(null);
136: }
137:
138: /** Store an invalid script exception describing the proper script
139: * usage. This should be used by derived classes whenever parsing
140: * indicates invalid input.
141: */
142: protected void usage(String details) {
143: String msg = getUsage();
144: if (details != null) {
145: msg = Strings.get("step.usage",
146: new Object[] { msg, details });
147: }
148: setScriptError(new InvalidScriptException(msg));
149: }
150:
151: /** Attributes to save in script. */
152: public Map getAttributes() {
153: Map map = new HashMap();
154: if (description != null
155: && !description.equals(getDefaultDescription()))
156: map.put(TAG_DESC, description);
157: return map;
158: }
159:
160: public Resolver getResolver() {
161: return resolver;
162: }
163:
164: /** Override if the step actually has some contents. In most cases, it
165: * won't.
166: */
167: protected Element addContent(Element el) {
168: return el;
169: }
170:
171: /** Add an attribute to the given XML Element. Attributes are kept in
172: alphabetical order. */
173: protected Element addAttributes(Element el) {
174: // Use a TreeMap to keep the attributes sorted on output
175: Map atts = new TreeMap(getAttributes());
176: Iterator iter = atts.keySet().iterator();
177: while (iter.hasNext()) {
178: String key = (String) iter.next();
179: String value = (String) atts.get(key);
180: if (value == null) {
181: Log.warn("Attribute '" + key
182: + "' value was null in step " + getXMLTag());
183: value = "";
184: }
185: el.setAttribute(key, value);
186: }
187: return el;
188: }
189:
190: /** Convert this Step into a String suitable for editing. The default is
191: the XML representation of the Step. */
192: public String toEditableString() {
193: return toXMLString(this );
194: }
195:
196: /** Provide a one-line XML string representation. */
197: public static String toXMLString(XMLifiable obj) {
198: // Comments are the only things that aren't actually elements...
199: if (obj instanceof Comment) {
200: return "<!-- " + ((Comment) obj).getDescription() + " -->";
201: }
202: Element el = obj.toXML();
203: StringWriter writer = new StringWriter();
204: try {
205: XMLOutputter outputter = new XMLOutputter();
206: outputter.output(el, writer);
207: } catch (IOException io) {
208: Log.warn(io);
209: }
210: return writer.toString();
211: }
212:
213: /** Convert the object to XML. */
214: public Element toXML() {
215: return addAttributes(addContent(new Element(getXMLTag())));
216: }
217:
218: /** Create a new step from an in-line XML string. */
219: public static Step createStep(Resolver resolver, String str)
220: throws InvalidScriptException, IOException {
221: StringReader reader = new StringReader(str);
222: try {
223: SAXBuilder builder = new SAXBuilder();
224: Document doc = builder.build(reader);
225: Element el = doc.getRootElement();
226: return createStep(resolver, el);
227: } catch (JDOMException e) {
228: throw new InvalidScriptException(e.getMessage());
229: }
230: }
231:
232: /** Convert the attributes in the given XML Element into a Map of
233: name/value pairs. */
234: protected static Map createAttributeMap(Element el) {
235: Log.debug("Creating attribute map for " + el);
236: Map attributes = new HashMap();
237: Iterator iter = el.getAttributes().iterator();
238: while (iter.hasNext()) {
239: Attribute att = (Attribute) iter.next();
240: attributes.put(att.getName(), att.getValue());
241: }
242: return attributes;
243: }
244:
245: /**
246: * Factory method, equivalent to a "fromXML" for step creation. Looks for
247: * a class with the same name as the XML tag, with the first letter
248: * capitalized. For example, <call /> is abbot.script.Call.
249: */
250: public static Step createStep(Resolver resolver, Element el)
251: throws InvalidScriptException {
252: String tag = el.getName();
253: Map attributes = createAttributeMap(el);
254: String name = tag.substring(0, 1).toUpperCase()
255: + tag.substring(1);
256: if (tag.equals(TAG_WAIT)) {
257: attributes.put(TAG_WAIT, "true");
258: name = "Assert";
259: }
260: try {
261: name = "abbot.script." + name;
262: Log.debug("Instantiating " + name);
263: Class cls = Class.forName(name);
264: try {
265: // Steps with contents require access to the XML element
266: Class[] argTypes = new Class[] { Resolver.class,
267: Element.class, Map.class };
268: Constructor ctor = cls.getConstructor(argTypes);
269: return (Step) ctor.newInstance(new Object[] { resolver,
270: el, attributes });
271: } catch (NoSuchMethodException nsm) {
272: // All steps must support this ctor
273: Class[] argTypes = new Class[] { Resolver.class,
274: Map.class };
275: Constructor ctor = cls.getConstructor(argTypes);
276: return (Step) ctor.newInstance(new Object[] { resolver,
277: attributes });
278: }
279: } catch (ClassNotFoundException cnf) {
280: String msg = Strings.get("step.unknown_tag",
281: new Object[] { tag });
282: throw new InvalidScriptException(msg);
283: } catch (InvocationTargetException ite) {
284: Log.warn(ite);
285: throw new InvalidScriptException(ite.getTargetException()
286: .getMessage());
287: } catch (Exception exc) {
288: Log.warn(exc);
289: throw new InvalidScriptException(exc.getMessage());
290: }
291: }
292:
293: protected String simpleClassName(Class cls) {
294: return ComponentTester.simpleClassName(cls);
295: }
296:
297: /** Return a description of this script step. */
298: public String toString() {
299: return getDescription();
300: }
301:
302: /** Returns the Class corresponding to the given class name. Provides
303: * just-in-time classname resolution to ensure loading by the proper class
304: * loader. <p>
305: */
306: public Class resolveClass(String className)
307: throws ClassNotFoundException {
308: ClassLoader cl = getResolver().getContextClassLoader();
309: return Class.forName(className, true, cl);
310: }
311:
312: /** Look up an appropriate ComponentTester given an arbitrary
313: * Component-derived class.
314: * If the class is derived from abbot.tester.ComponentTester, instantiate
315: * one; if it is derived from java.awt.Component, return a matching Tester.
316: * Otherwise return abbot.tester.ComponentTester.<p>
317: * @throws ClassNotFoundException If the given class can't be found.
318: * @throws IllegalArgumentException If the tester cannot be instantiated.
319: */
320: protected ComponentTester resolveTester(String className)
321: throws ClassNotFoundException {
322: Class testedClass = resolveClass(className);
323: if (Component.class.isAssignableFrom(testedClass))
324: return ComponentTester.getTester(testedClass);
325: else if (ComponentTester.class.isAssignableFrom(testedClass)) {
326: try {
327: return (ComponentTester) testedClass.newInstance();
328: } catch (Exception e) {
329: String msg = "Custom ComponentTesters must provide "
330: + "an accessible no-args Constructor: "
331: + e.getMessage();
332: throw new IllegalArgumentException(msg);
333: }
334: }
335: String msg = "The given class '" + className
336: + "' is neither a Component nor a ComponentTester";
337: throw new IllegalArgumentException(msg);
338: }
339:
340: private void writeObject(ObjectOutputStream out) {
341: // NOTE: this is only to avoid drag/drop errors
342: out = null;
343: }
344:
345: private void readObject(ObjectInputStream in) {
346: // NOTE: this is only to avoid drag/drop errors
347: in = null;
348: }
349: }
|