001: package abbot.script;
002:
003: import java.awt.Component;
004: import java.io.*;
005: import java.net.URL;
006: import java.util.*;
007:
008: import org.jdom.*;
009: import org.jdom.input.SAXBuilder;
010: import org.jdom.output.XMLOutputter;
011: import org.jdom.output.Format;
012:
013: import abbot.Log;
014: import abbot.Platform;
015: import abbot.finder.*;
016: import abbot.i18n.Strings;
017: import abbot.script.parsers.*;
018: import abbot.tester.Robot;
019: import abbot.util.Properties;
020:
021: /**
022: * Provide a structure to encapsulate actions invoked on GUI components and
023: * tests performed on those components. Scripts need to be short and concise
024: * (and therefore easy to read/write). Extensions don't have to be.<p>
025: * This takes a single filename as a constructor argument.<p>
026: * Use {@link junit.extensions.abbot.ScriptFixture} and
027: * {@link junit.extensions.abbot.ScriptTestSuite}
028: * to generate a suite by auto-generating a collection of {@link Script}s.<p>
029: * @see StepRunner
030: * @see Fixture
031: * @see Launch
032: */
033:
034: public class Script extends Sequence implements Resolver {
035: public static final String INTERPRETER = "bsh";
036: private static final String USAGE = "<AWTTestScript [desc=\"\"] [forked=\"true\"] [slow=\"true\"]"
037: + " [awt=\"true\"] [vmargs=\"...\"]>...</AWTTestScript>\n";
038:
039: /** Robot delay for slow playback. */
040: private static int slowDelay = 250;
041: static boolean validate = true;
042: private String filename;
043: private File relativeDirectory;
044: private boolean fork;
045: private boolean slow;
046: private boolean awt;
047: private int lastSaved;
048: private String vmargs;
049: private Map properties = new HashMap();
050: private Hierarchy hierarchy;
051: public static final String UNTITLED_FILE = Strings
052: .get("script.untitled_filename");
053: protected static final String UNTITLED = Strings
054: .get("script.untitled");
055:
056: /** Read-only map of ref IDs into ComponentReferences. */
057: private Map refs = Collections.unmodifiableMap(new HashMap());
058: /** Maps components to references. This cache provides a 20% speedup when
059: * adding new references.
060: */
061: private Map components = new WeakHashMap();
062:
063: static {
064: slowDelay = Properties.getProperty("abbot.script.slow_delay",
065: slowDelay, 0, 60000);
066: String defValue = Platform.JAVA_VERSION < Platform.JAVA_1_4 ? "false"
067: : "true";
068: validate = "true".equals(System.getProperty(
069: "abbot.script.validate", defValue));
070: }
071:
072: protected static Map createDefaultMap(String filename) {
073: Map map = new HashMap();
074: map.put(TAG_FILENAME, filename);
075: return map;
076: }
077:
078: /** Create a new, empty <code>Script</code>. Used as a temporary
079: * {@link Resolver}, uses the default {@link Hierarchy}.
080: * @deprecated Use an explicit {@link Hierarchy} instead.
081: */
082: public Script() {
083: // This is roughly equivalent to what
084: // DefaultComponentFinder.getFinder() used to do
085: this (AWTHierarchy.getDefault());
086: }
087:
088: /** Create a <code>Script</code> from the given filename. Uses the
089: default {@link Hierarchy}.
090: @deprecated Use an explicit {@link Hierarchy} instead.
091: */
092: public Script(String filename) {
093: // This is roughly equivalent to what
094: // DefaultComponentFinder.getFinder() used to do
095: this (filename, AWTHierarchy.getDefault());
096: }
097:
098: public Script(Hierarchy h) {
099: this (null, new HashMap());
100: setHierarchy(h);
101: }
102:
103: /** Create a <code>Script</code> from the given file. */
104: public Script(String filename, Hierarchy h) {
105: this (null, createDefaultMap(filename));
106: setHierarchy(h);
107: }
108:
109: public Script(Resolver parent, Map attributes) {
110: super (parent, attributes);
111: String filename = (String) attributes.get(TAG_FILENAME);
112: File file = filename != null ? new File(filename)
113: : getTempFile(parent != null ? parent.getDirectory()
114: : null);
115:
116: // If this file is not absolute setFile is going to convert to a connoical
117: // path so we actually need this to be relative to the parent
118: //
119:
120: if (!file.isAbsolute()) {
121: file = new File(parent.getDirectory(), filename);
122: }
123:
124: // Configure the file
125: //
126:
127: setFile(file);
128: if (parent != null) {
129: setRelativeTo(parent.getDirectory());
130: }
131: try {
132: load();
133: } catch (IOException e) {
134: setScriptError(e);
135: }
136: }
137:
138: /** Since we allow ComponentReference IDs to be changed, make sure our map
139: * is always up to date.
140: */
141: private synchronized void synchReferenceIDs() {
142: HashMap map = new HashMap();
143: Iterator iter = refs.values().iterator();
144: while (iter.hasNext()) {
145: ComponentReference ref = (ComponentReference) iter.next();
146: map.put(ref.getID(), ref);
147: }
148: if (!refs.equals(map)) {
149: // atomic update of references map
150: refs = Collections.unmodifiableMap(map);
151: }
152: }
153:
154: public void setHierarchy(Hierarchy h) {
155: hierarchy = h;
156: components.clear();
157: }
158:
159: private File getTempFile(File dir) {
160: File file;
161: try {
162: file = (dir != null ? File.createTempFile(UNTITLED_FILE,
163: ".xml", dir) : File.createTempFile(UNTITLED_FILE,
164: ".xml"));
165: // We don't actually need the file on disk
166: file.delete();
167: } catch (IOException io) {
168: file = (dir != null ? new File(dir, UNTITLED_FILE + ".xml")
169: : new File(UNTITLED_FILE + ".xml"));
170: }
171: return file;
172: }
173:
174: public String getName() {
175: return filename;
176: }
177:
178: public void setForked(boolean fork) {
179: this .fork = fork;
180: }
181:
182: public boolean isForked() {
183: return fork;
184: }
185:
186: public void setVMArgs(String args) {
187: if (args != null && "".equals(args))
188: args = null;
189: vmargs = args;
190: }
191:
192: public String getVMArgs() {
193: return vmargs;
194: }
195:
196: public boolean isSlowPlayback() {
197: return slow;
198: }
199:
200: public void setSlowPlayback(boolean slow) {
201: this .slow = slow;
202: }
203:
204: public boolean isAWTMode() {
205: return awt;
206: }
207:
208: public void setAWTMode(boolean awt) {
209: this .awt = awt;
210: }
211:
212: /** Return the file where this script is saved. Will always be an
213: * absolute path.
214: */
215: public File getFile() {
216: File file = new File(filename);
217: if (!file.isAbsolute()) {
218: String path = getRelativeTo().getPath() + File.separator
219: + filename;
220: file = new File(path);
221: file = resolveRelativeReferences(file);
222: }
223: return file;
224: }
225:
226: /** Change the file system basis for the current script. Does not affect
227: the script contents.
228: @deprecated Use {@link #setFile(File)}.
229: */
230: public void changeFile(File file) {
231: setFile(file);
232: }
233:
234: /** Set the file system basis for this script object. Use this to set the
235: file from which the existing script will be loaded.
236: */
237: public void setFile(File file) {
238: Log.debug("Script file set to " + file);
239: if (file == null)
240: throw new IllegalArgumentException("File must not be null");
241: if (filename == null || !file.equals(getFile())) {
242:
243: // Convert to full absolute version
244: //
245:
246: file = resolveRelativeReferences(file);
247: filename = file.getPath();
248:
249: Log.debug("Script filename set to " + filename);
250: if (relativeDirectory != null)
251: setRelativeTo(relativeDirectory);
252: }
253: lastSaved = getHash() + 1;
254: }
255:
256: /** Typical xml header, so we can know about how much file prefix to
257: * skip.
258: */
259: private static final String XML_INFO = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
260:
261: /** Flag to indicate whether emitted XML should contain the script
262: contents. Sometimes we just want a one-liner (like when displaying in
263: the script editor), and sometimes we want the full contents (when
264: writing to file).
265: */
266: private boolean formatForSave = false;
267:
268: /** Write the current state of the script to file. */
269: public void save(Writer writer) throws IOException {
270: formatForSave = true;
271: Element el = toXML();
272: formatForSave = false;
273: el.setName(TAG_AWTTESTSCRIPT);
274:
275: Document doc = new Document(el);
276: XMLOutputter outputter = new XMLOutputter(Format
277: .getPrettyFormat());
278: outputter.output(doc, writer);
279: }
280:
281: /** Only thing directly editable on a script is its file path. */
282: public String toEditableString() {
283: return getFilename();
284: }
285:
286: /** Has this script changed since the last save. */
287: public boolean isDirty() {
288: return getHash() != lastSaved;
289: }
290:
291: /** Write the script to file. Note that this differs from the toXML for
292: the script, which simply indicates the file on which it is based. */
293: public void save() throws IOException {
294: File file = getFile();
295: Log.debug("Saving script to '" + file + "' " + hashCode());
296: OutputStreamWriter writer = new OutputStreamWriter(
297: new FileOutputStream(file), "UTF-8");
298: save(new BufferedWriter(writer));
299: writer.close();
300: lastSaved = getHash();
301: }
302:
303: /** Ensure that all referenced components are actually in the components
304: * list.
305: */
306: private synchronized void verify() throws InvalidScriptException {
307: Log.debug("verifying all referenced refs exist");
308: Iterator iter = refs.values().iterator();
309: while (iter.hasNext()) {
310: ComponentReference ref = (ComponentReference) iter.next();
311: String id = ref.getAttribute(TAG_PARENT);
312: if (id != null && refs.get(id) == null) {
313: String msg = Strings.get("script.parent_missing",
314: new Object[] { id });
315: throw new InvalidScriptException(msg);
316: }
317: id = ref.getAttribute(TAG_WINDOW);
318: if (id != null && refs.get(id) == null) {
319: String msg = Strings.get("script.window_missing",
320: new Object[] { id });
321: throw new InvalidScriptException(msg);
322: }
323: }
324: }
325:
326: /** Make the path to the given child script relative to this one. */
327: private void updateRelativePath(Step child) {
328: // Make sure included scripts are located relative to this one
329: if (child instanceof Script) {
330: ((Script) child).setRelativeTo(getDirectory());
331: }
332: }
333:
334: protected void parseChild(Element el) throws InvalidScriptException {
335: if (el.getName().equals(TAG_COMPONENT)) {
336: addComponentReference(el);
337: } else {
338: synchronized (steps()) {
339: super .parseChild(el);
340: updateRelativePath((Step) steps().get(size() - 1));
341: }
342: }
343: }
344:
345: /** Parse XML attributes for the Script. */
346: protected void parseAttributes(Map map) {
347: parseStepAttributes(map);
348: fork = Boolean.valueOf((String) map.get(TAG_FORKED))
349: .booleanValue();
350: slow = Boolean.valueOf((String) map.get(TAG_SLOW))
351: .booleanValue();
352: awt = Boolean.valueOf((String) map.get(TAG_AWT)).booleanValue();
353: vmargs = (String) map.get(TAG_VMARGS);
354: }
355:
356: /** Loads the XML test script. Performs a check against the XML schema.
357: @param reader Provides the script data
358: @throws InvalidScriptException
359: @throws IOException
360: */
361: public void load(Reader reader) throws InvalidScriptException,
362: IOException {
363: clear();
364: try {
365: // Set things up to optionally validate on load
366: SAXBuilder builder = new SAXBuilder(validate);
367: if (validate) {
368: URL url = getClass().getClassLoader().getResource(
369: "abbot/abbot.xsd");
370: if (url != null) {
371: builder
372: .setFeature(
373: "http://apache.org/xml/features/validation/schema",
374: true);
375: builder
376: .setProperty(
377: "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation",
378: url.toString());
379: } else {
380: Log
381: .warn("Could not find abbot/abbot.xsd, disabling XML validation");
382: validate = false;
383: }
384: }
385: Document doc = builder.build(reader);
386: Element root = doc.getRootElement();
387: Map map = createAttributeMap(root);
388: parseAttributes(map);
389: parseChildren(root);
390: } catch (JDOMException e) {
391: throw new InvalidScriptException(e.getMessage());
392: }
393: // Make sure we have all referenced components
394: synchronized (this ) {
395: synchReferenceIDs();
396: verify();
397: }
398: lastSaved = getHash();
399: }
400:
401: public void addStep(int index, Step step) {
402: super .addStep(index, step);
403: updateRelativePath(step);
404: }
405:
406: public void addStep(Step step) {
407: super .addStep(step);
408: updateRelativePath(step);
409: }
410:
411: /** Replaces the step at the given index. */
412: public void setStep(int index, Step step) {
413: super .setStep(index, step);
414: updateRelativePath(step);
415: }
416:
417: /** Read the script from the currently set file. */
418: public void load() throws IOException {
419: File file = getFile();
420: if (!file.exists()) {
421: if (getFilename().indexOf(Script.UNTITLED_FILE) == -1)
422: Log.warn("Script " + this
423: + " does not exist, ignoring it");
424: return;
425: }
426: if (!file.isFile())
427: throw new InvalidScriptException("Path " + getFilename()
428: + " refers to a directory");
429: if (file.length() != 0) {
430: try {
431: Reader reader = new InputStreamReader(
432: new FileInputStream(file), "UTF-8");
433: try {
434: load(new BufferedReader(reader));
435: } finally {
436: try {
437: reader.close();
438: } catch (IOException e) {
439: }
440: }
441: } catch (FileNotFoundException e) {
442: // should have been detected
443: Log.warn("File '" + file + "' exists but is not found");
444: }
445: } else {
446: Log.warn("Script file " + this + " is empty");
447: }
448: }
449:
450: protected String getFullXMLString() {
451: try {
452: formatForSave = true;
453: return toXMLString(this );
454: } finally {
455: formatForSave = false;
456: }
457: }
458:
459: private int getHash() {
460: return getFullXMLString().hashCode();
461: }
462:
463: public String getXMLTag() {
464: return TAG_SCRIPT;
465: }
466:
467: /** Save component references in addition to everything else. */
468: public Element addContent(Element el) {
469: // Only save content if writing to disk
470: if (formatForSave) {
471: synchReferenceIDs();
472: Iterator iter = new TreeSet(refs.values()).iterator();
473: while (iter.hasNext()) {
474: ComponentReference cref = (ComponentReference) iter
475: .next();
476: el.addContent(cref.toXML());
477: }
478: // Now collect our child steps
479: return super .addContent(el);
480: }
481: return el;
482: }
483:
484: /** Return the (possibly relative) path to this script. */
485: public String getFilename() {
486: return filename;
487: }
488:
489: /** Provide XML attributes for this Step. This class adds its filename. */
490: public Map getAttributes() {
491: Map map;
492: if (!formatForSave) {
493: map = new HashMap();
494:
495: // Ensure that any relative references are using
496: // forward slashed for multiplatform compatibility.
497:
498: String f = getFilename();
499: if (f != null && !new File(f).isAbsolute()) {
500: f = f.replace('\\', '/');
501: }
502:
503: map.put(TAG_FILENAME, f);
504: } else {
505: map = super .getAttributes();
506: // default is no fork
507: if (fork) {
508: map.put(TAG_FORKED, "true");
509: if (vmargs != null) {
510: map.put(TAG_VMARGS, vmargs);
511: }
512: }
513: if (slow) {
514: map.put(TAG_SLOW, "true");
515: }
516: if (awt) {
517: map.put(TAG_AWT, "true");
518: }
519: }
520: return map;
521: }
522:
523: protected void runStep(StepRunner runner) throws Throwable {
524: components.clear();
525: properties.clear();
526: // Make all files relative to this script
527: Parser fc = new FileParser() {
528: public String relativeTo() {
529: Log.debug("All file references will be relative to "
530: + getDirectory().getAbsolutePath());
531: return getDirectory().getAbsolutePath();
532: }
533: };
534: Parser oldfc = ArgumentParser.setParser(File.class, fc);
535: int oldDelay = Robot.getAutoDelay();
536: int oldMode = Robot.getEventMode();
537: if (slow) {
538: Robot.setAutoDelay(slowDelay);
539: }
540: if (awt) {
541: Robot.setEventMode(Robot.EM_AWT);
542: }
543:
544: try {
545: super .runStep(runner);
546: } finally {
547: Robot.setAutoDelay(oldDelay);
548: Robot.setEventMode(oldMode);
549: ArgumentParser.setParser(File.class, oldfc);
550: }
551: }
552:
553: /** Set up a blank script, discarding any current state. */
554: public void clear() {
555: setScriptError(null);
556: refs = Collections.unmodifiableMap(new HashMap());
557: components.clear();
558: super .clear();
559: }
560:
561: public String getUsage() {
562: return USAGE;
563: }
564:
565: /** Return a default description for this <code>Script</code>. */
566: public String getDefaultDescription() {
567: String ext = fork ? " &" : "";
568: String desc = Strings.get("script.desc", new Object[] {
569: getFilename(), ext });
570: return desc.indexOf(UNTITLED_FILE) != -1 ? UNTITLED : desc;
571: }
572:
573: /** Return whether this <code>Script</code> is launchable. */
574: public boolean hasLaunch() {
575: // First step might be a Launch or a Fixture
576: return size() > 0
577: && (((Step) steps().get(0)) instanceof UIContext);
578: }
579:
580: /** @return The {@link UIContext} responsible for setting up
581: * a UI context for this script, or
582: * <code>null</code> if the script has no UI to speak of.
583: */
584: public UIContext getUIContext() {
585: synchronized (steps()) {
586: if (hasLaunch()) {
587: return (UIContext) steps().get(0);
588: }
589: }
590: return null;
591: }
592:
593: /** Defer to the {@link UIContext} to obtain a
594: * {@link ClassLoader}, or use the current {@link Thread}'s
595: * context class loader.
596: * @see Thread#getContextClassLoader()
597: */
598: public ClassLoader getContextClassLoader() {
599: UIContext context = getUIContext();
600: return context != null ? context.getContextClassLoader()
601: : Thread.currentThread().getContextClassLoader();
602: }
603:
604: public boolean hasTerminate() {
605: return size() > 0
606: && (((Step) steps().get(size() - 1)) instanceof Terminate);
607: }
608:
609: /** By default, all pathnames are relative to the current working
610: directory.
611: */
612: public File getRelativeTo() {
613: if (relativeDirectory == null)
614: return new File(System.getProperty("user.dir"));
615: return relativeDirectory;
616: }
617:
618: /** Indicate that when invoking toXML, a path relative to the given one
619: * should be shown. Note that this is a runtime setting only and never
620: * shows up in saved XML.
621: */
622: public void setRelativeTo(File dir) {
623: Log.debug("Want relative dir " + dir);
624:
625: // If the old relatative directory is not null then convert the
626: // filename back to a non relative path
627: //
628:
629: if (relativeDirectory != null) {
630:
631: File file = new File(filename);
632: if (!file.isAbsolute()) {
633:
634: file = new File(relativeDirectory, filename);
635: try {
636: filename = file.getCanonicalPath();
637: } catch (IOException ioe) {
638: filename = file.getPath();
639: }
640: }
641: }
642:
643: //
644:
645: relativeDirectory = dir;
646: if (dir != null) {
647:
648: // More robust relative path examples
649:
650: File currentParent = resolveRelativeReferences(dir);
651:
652: StringBuffer relativePath = new StringBuffer();
653: searchParents: do {
654:
655: String relPath = currentParent.getAbsolutePath();
656: if (filename.startsWith(relPath)
657: && relPath.length() < filename.length()) {
658: char ch = filename.charAt(relPath.length());
659: if (ch == '/' || ch == '\\') {
660:
661: // We have found a match
662:
663: relativePath.append(filename.substring(relPath
664: .length() + 1));
665: filename = relativePath.toString().replace(
666: '\\', '/');
667: break searchParents;
668: }
669: }
670:
671: // Do the loop again
672: //
673:
674: currentParent = currentParent.getParentFile();
675: relativePath.append("../");
676:
677: } while (currentParent != null);
678:
679: }
680: Log.debug("Relative dir set to " + relativeDirectory + " for "
681: + this );
682: }
683:
684: /**
685: * It turns out that getAbsolutePath doesn't remove any relative path
686: * entries such as .. the alternative getCannonicalPath can cause problems
687: * with version control systems that make a lot of use of symolic links.
688: * For example a directory in a source control view might contain local
689: * checked out files and symlinks to un-checked out files (These files
690: * might be located on another machine in some cases). This means that
691: * files that are next to each other in the view appear to be in quite
692: * different paths when resolved using getCannonicalPath. Therefore we are
693: * reduced to tidying up the paths manually.
694: *
695: * @param file The input file to tidy up.
696: * @return An absolute path with all . and .. removed
697: * @throws IllegalArgumentException If the number of ".." entries outnumber
698: * those of the normal path entries.
699: */
700:
701: public static File resolveRelativeReferences(File file) {
702:
703: StringBuffer fixAbsolutePath = new StringBuffer();
704: String originalPath = file.getAbsolutePath();
705: String parts[] = originalPath.split("(\\\\|/)");
706:
707: // Work backwards
708: //
709:
710: int dirDebt = 0;
711: for (int i = parts.length - 1; i >= 0; i--) {
712: String part = parts[i];
713: if (part.length() == 0) {
714: ; // ignore this
715: } else if (".".equals(part)) {
716: ; // ignore dot paths
717: } else if ("..".equals(part)) {
718: dirDebt++; // Record an path we have to run out
719: } else {
720: if (dirDebt > 0) {
721: dirDebt--;
722: } else {
723: fixAbsolutePath.insert(0, part);
724: fixAbsolutePath.insert(0, File.separatorChar);
725: }
726: }
727: }
728:
729: if (dirDebt > 0) {
730: throw new IllegalArgumentException("Run out of path");
731: }
732:
733: return new File(fixAbsolutePath.toString());
734: }
735:
736: /** Return whether the given file looks like a valid AWT script. */
737: public static boolean isScript(File file) {
738: if (file.length() == 0)
739: return true;
740: if (!file.exists() || !file.isFile()
741: || file.length() < TAG_AWTTESTSCRIPT.length() * 2 + 5)
742: return false;
743: InputStream is = null;
744: try {
745: int len = XML_INFO.length() + TAG_AWTTESTSCRIPT.length()
746: + 15;
747: is = new BufferedInputStream(new FileInputStream(file));
748: byte[] buf = new byte[len];
749: is.read(buf, 0, buf.length);
750: String str = new String(buf, 0, buf.length);
751: return str.indexOf(TAG_AWTTESTSCRIPT) != -1;
752: } catch (Exception exc) {
753: return false;
754: } finally {
755: if (is != null)
756: try {
757: is.close();
758: } catch (Exception exc) {
759: }
760: }
761: }
762:
763: /** All relative files should be accessed relative to this directory,
764: which is the directory where the script resides.
765: It will always return an absolute path.
766: */
767: public File getDirectory() {
768: return getFile().getParentFile();
769: }
770:
771: /** Returns a sorted collection of ComponentReferences. */
772: public Collection getComponentReferences() {
773: return new TreeSet(refs.values());
774: }
775:
776: /** Add a component reference directly, replacing any existing one with
777: the same ID.
778: */
779: public void addComponentReference(ComponentReference ref) {
780: Log.debug("adding " + ref);
781: synchReferenceIDs();
782: HashMap map = new HashMap(refs);
783: map.put(ref.getID(), ref);
784: // atomic update of references map
785: refs = Collections.unmodifiableMap(map);
786: }
787:
788: /** Add a new component reference for the given component. */
789: // FIXME: a repaint (tree locked) which accesses the refs list
790: // deadlocks with cref creation (locks refs, asks for tree lock)
791: // Either get tree lock first or don't require refs lock on read
792: public ComponentReference addComponent(Component comp) {
793: synchReferenceIDs();
794: Log.debug("look up existing for " + Robot.toString(comp));
795: Map newRefs = new HashMap();
796: ComponentReference ref = ComponentReference.getReference(this ,
797: comp, newRefs);
798: Log.debug("adding " + Robot.toString(comp));
799: Map map = new HashMap(refs);
800: map.putAll(newRefs);
801: Iterator iter = newRefs.values().iterator();
802: while (iter.hasNext()) {
803: ComponentReference r = (ComponentReference) iter.next();
804: Component c = r.getCachedLookup(getHierarchy());
805: if (c != null)
806: components.put(c, r);
807: }
808: // atomic update of references map
809: refs = Collections.unmodifiableMap(map);
810: return ref;
811: }
812:
813: /** Add a new component reference to the script. For use only when
814: * parsing a script.
815: */
816: ComponentReference addComponentReference(Element el)
817: throws InvalidScriptException {
818: synchReferenceIDs();
819: ComponentReference ref = new ComponentReference(this , el);
820: Log.debug("adding " + el);
821: Map map = new HashMap(refs);
822: map.put(ref.getID(), ref);
823: // atomic update of references map
824: refs = Collections.unmodifiableMap(map);
825: return ref;
826: }
827:
828: /** Return the reference for the given component, or null if none yet
829: * exists.
830: */
831: public ComponentReference getComponentReference(Component comp) {
832: if (!getHierarchy().contains(comp)) {
833: String msg = Strings.get("script.not_in_hierarchy",
834: new Object[] { comp.toString() });
835: throw new IllegalArgumentException(msg);
836: }
837: synchReferenceIDs();
838: // Clear the component map if any one of the mappings is invalid
839: Iterator iter = refs.values().iterator();
840: while (iter.hasNext()) {
841: ComponentReference cr = (ComponentReference) iter.next();
842: if (cr.getCachedLookup(getHierarchy()) == null) {
843: components.clear();
844: break;
845: }
846: }
847: ComponentReference ref = (ComponentReference) components
848: .get(comp);
849: if (ref != null) {
850: if (ref.getCachedLookup(getHierarchy()) != null)
851: return ref;
852: components.remove(comp);
853: }
854: ref = ComponentReference.matchExisting(comp, refs.values());
855: if (ref != null) {
856: components.put(comp, ref);
857: }
858: return ref;
859: }
860:
861: /** Convert the given reference ID into a component reference. If it's
862: * not in the Script's list, returns null.
863: */
864: public ComponentReference getComponentReference(String name) {
865: synchReferenceIDs();
866: return (ComponentReference) refs.get(name);
867: }
868:
869: public void setProperty(String name, Object value) {
870: if (value == null)
871: properties.remove(name);
872: else
873: properties.put(name, value);
874: }
875:
876: public Object getProperty(String name) {
877: Object value = properties.get(name);
878: // Lazy-load the interpreter, so it's only instantiated when required
879: if (value == null && INTERPRETER.equals(name)) {
880: Interpreter bsh = new Interpreter(this );
881: properties.put(name, value = bsh);
882: }
883: return value;
884: }
885:
886: /** Return the currently effective {@link Hierarchy} of components. */
887: public Hierarchy getHierarchy() {
888: Resolver r = getResolver();
889: if (r != null && r != this )
890: return r.getHierarchy();
891: return hierarchy != null ? hierarchy : AWTHierarchy
892: .getDefault();
893: }
894:
895: /** Return a meaningful description of where the Step came from. */
896: public String getContext(Step step) {
897: return getFile().toString() + ":" + getLine(this , step);
898: }
899:
900: /** Return the file which defines the given step. */
901: public static File getFile(Step step) {
902: String context = step.getResolver().getContext(step);
903: int colon = context.indexOf(":");
904: if (colon == 1 && Character.isLetter(context.charAt(0))
905: && Platform.isWindows() && context.length() > 2)
906: colon = context.indexOf(":", 2);
907: if (colon != -1)
908: context = context.substring(0, colon);
909: return new File(context);
910: }
911:
912: /** Return the approximate line number of the given step. File lines are
913: one-based (there is no line zero).
914: */
915: public static int getLine(Step step) {
916: String context = step.getResolver().getContext(step);
917: int colon = context.indexOf(":");
918: if (colon == 1 && Character.isLetter(context.charAt(0))
919: && Platform.isWindows() && context.length() > 2)
920: colon = context.indexOf(":", 2);
921: context = context.substring(colon + 1);
922: try {
923: return Integer.parseInt(context);
924: } catch (NumberFormatException e) {
925: return -1;
926: }
927: }
928:
929: private static int getLine(Sequence seq, Step step) {
930: int line = -1;
931: int index = seq.indexOf(step);
932: if (index == -1) {
933: List list = seq.steps();
934: for (int i = 0; i < list.size(); i++) {
935: Step sub = (Step) list.get(i);
936: if (sub instanceof Sequence) {
937: int subline = getLine((Sequence) sub, step);
938: if (subline != -1) {
939: line = countLines(seq, i) + subline;
940: break;
941: }
942: }
943: }
944: } else {
945: line = countLines(seq, index);
946: }
947: return line;
948: }
949:
950: /** Return the number of XML lines in the given sequence that precede the
951: * given index. If the index is -1, return the number of XML lines used
952: * by the whole sequence.
953: */
954: public static int countLines(Sequence seq, int index) {
955: int count = 1;
956: int limit = index;
957: if (limit == -1) {
958: limit = seq.size();
959: // Empty sequences take a single line
960: if (limit == 0)
961: return 1;
962: // Otherwise, 2 + the line count of the contents
963: count = 2;
964: }
965: if (seq instanceof Script) {
966: // Add in one line per component reference in the script
967: // Plus two lines for the <xml> and <AWTTestScript> lines
968: count += ((Script) seq).getComponentReferences().size() + 2;
969: }
970: for (int i = 0; i < limit; i++) {
971: Step step = seq.getStep(i);
972: // Included scripts take only one line
973: if (step instanceof Script) {
974: ++count;
975: } else if (step instanceof Sequence) {
976: count += countLines((Sequence) step, -1);
977: } else {
978: ++count;
979: }
980: }
981: return count;
982: }
983: }
|