0001: package abbot.script;
0002:
0003: import java.applet.Applet;
0004: import java.awt.*;
0005: import java.io.*;
0006: import java.lang.ref.WeakReference;
0007: import java.util.*;
0008:
0009: import javax.swing.*;
0010: import javax.swing.border.*;
0011: import javax.accessibility.*;
0012:
0013: import org.jdom.*;
0014: import org.jdom.input.SAXBuilder;
0015:
0016: import abbot.Log;
0017: import abbot.i18n.Strings;
0018: import abbot.finder.*;
0019: import abbot.tester.*;
0020: import abbot.tester.Robot;
0021: import abbot.util.ExtendedComparator;
0022: import abbot.util.AWT;
0023:
0024: /** Encapsulate as much information as is available to identify a GUI
0025: * component. Usage:<br>
0026: * <blockquote><code>
0027: * <component id="..." class="..." [...]><br>
0028: * </code></blockquote>
0029: * The component reference ID may be used in scripts in place of the actual
0030: * component to which this reference refers. The conversion will be made when
0031: * the actual component is needed. The ID is arbitrary, and may be changed in
0032: * scripts to any unique string (just remember to change all references to the
0033: * ID in other places in the script as well).<p>
0034: * A number of optional tags are supported to provide an increasingly precise
0035: * specification of the desired component:<br>
0036: * <ul>
0037: * <li><b><code>weighted</code></b> refers to the name of an available
0038: * attribute which should be weighted more heavily in comparisons, e.g. the
0039: * label on a JButton.<br>
0040: * <li><b><code>parent</code></b> the reference id of this component's
0041: * parent.<br>
0042: * </ul>
0043: * <p>
0044: * ComponentReferences may be created in one of three ways, each of which has
0045: * slightly different implications.
0046: * <ul>
0047: * <li>Resolver.addComponent(Component) - creates a reference if one doesn't
0048: * already exist and modifiers the Resolver to include it.
0049: * <li>getReference(Resolver, Component, Map) - create a reference only if a
0050: * matching one does not exist, but does not modify the Resolver.
0051: * <li>ComponentReference<init> - create a new reference.
0052: * </ul>
0053: */
0054: // TODO: lose exact hierarchy match, cf Xt resource specifier, e.g.
0055: // TODO: add window appearance order
0056: // JRootPane.<name>|<class>.*.JPanel
0057: // Other attributes that might be useful: getAccessibleRole,
0058: // getAccessibleDescription, tooltip, accessibleRelation, selection start/end
0059: /*
0060: To extend, probably want to make a static function here that stores
0061: attributes and a lookup interface to read that attribute. Do this only if
0062: it is directly needed.
0063:
0064: Should the JRE class be used instead of a custom class?
0065: pros: doesn't save custom classes, which might change
0066: cons: ?
0067: Should class mismatches be allowed?
0068: cons: can't change classes
0069: pros: exact (or derived) class matching eliminates a lot of comparisons
0070: */
0071:
0072: /*
0073: Optimization note: All lookups are cached, so that at most we have to
0074: traverse the hierarchy once.
0075: Successful lookups are cached until the referenced ocmponent is GCd,
0076: removed from the hierarchy, or otherwise marked invalid.
0077: Unsuccessful lookups are cached for the duration of a particular lookup.
0078: These happen in several places.
0079: 1) when resolving a cref into a component (getComponent())
0080: 2) when a script is checking for existing references prior to creating a
0081: new one (getReference()).
0082: 3) when creating a new reference (ComponentReference().
0083: 4) when looking for a matching, existing reference (matchExisting()).
0084: In these cases, the failed lookup cache is cleared only after the
0085: entire operation is complete.
0086: see also NOTES
0087: */
0088:
0089: public class ComponentReference implements XMLConstants, XMLifiable,
0090: Comparable {
0091:
0092: public static final String SHARED_FRAME_ID = "shared frame";
0093:
0094: // Matching weights for various attributes
0095: private static final int MW_NAME = 100;
0096: private static final int MW_ROOT = 25;
0097: //private static final int MW_WEIGHTED = 50;
0098: private static final int MW_TAG = 50;
0099: private static final int MW_PARENT = 25; //
0100: private static final int MW_WINDOW = 25;
0101: private static final int MW_INVOKER = 25;
0102: private static final int MW_TITLE = 25;
0103: private static final int MW_BORDER_TITLE = 25;
0104: private static final int MW_LABEL = 25;
0105: private static final int MW_TEXT = 25; //
0106: private static final int MW_ICON = 25;
0107: private static final int MW_INDEX = 10; //
0108: private static final int MW_CLASS = 1;
0109: // Pretty much for applets only, or other embedded frames
0110: private static final int MW_PARAMS = 1;
0111: private static final int MW_DOCBASE = 1;
0112: // Mostly for distinguishing between multiple components that would
0113: // otherwise all match
0114: private static final int MW_HORDER = 1;
0115: private static final int MW_VORDER = 1;
0116: //private static final int MW_ENABLED = 1;
0117: //private static final int MW_FOCUSED = 1;
0118: private static final int MW_SHOWING = 1;
0119: /** Match weight corresponding to no possible match. */
0120: public static final int MW_FAILURE = 0;
0121: static final String ANON_INNER_CLASS = "/^.*\\$[0-9]+$/";
0122:
0123: private Resolver resolver;
0124: private Map attributes = new HashMap();
0125: // This helps component reference creation by an order of magnitude,
0126: // especially when dealing with ordered attributes.
0127: private WeakReference cachedLookup;
0128: /** This ThreadLocal allows us to keep track of unresolved components on a
0129: * per-thread (basically per-lookup) basis.
0130: */
0131: private static ThreadLocal lookupFailures = new ThreadLocal() {
0132: protected synchronized Object initialValue() {
0133: return new HashMap();
0134: }
0135: };
0136: /** This ThreadLocal allows us to keep track of non-showing, resolved
0137: * components on a per-thread (basically per-lookup) basis.
0138: */
0139: private static ThreadLocal nonShowingMatches = new ThreadLocal() {
0140: protected synchronized Object initialValue() {
0141: return new HashMap();
0142: }
0143: };
0144: /** Keep track of which ComponentReference ctor is the first one. */
0145: private static ThreadLocal ownsFailureCache = new ThreadLocal() {
0146: protected synchronized Object initialValue() {
0147: return Boolean.TRUE;
0148: }
0149: };
0150: /** Cached XML representation. */
0151: private String xml;
0152:
0153: /** Disable immediate cacheing of components when a reference is created
0154: based on a Component. Cacheing will first be done when the reference
0155: is resolved for the first time after creation. For testing purposes
0156: only.
0157: */
0158: static boolean cacheOnCreation = true;
0159:
0160: /** For creation from XML.
0161: */
0162: public ComponentReference(Resolver resolver, Element el)
0163: throws InvalidScriptException {
0164: this .resolver = resolver;
0165: fromXML(el, true);
0166: }
0167:
0168: /** Create a reference to an instance of the given class, given an array
0169: of name/value pairs of attributes.
0170: */
0171: public ComponentReference(Resolver r, Class cls,
0172: String[][] attributes) {
0173: this (r, cls, createAttributeMap(attributes));
0174: }
0175:
0176: /** Create a reference to an instance of the given class, given a Map of
0177: attributes.
0178: */
0179: public ComponentReference(Resolver resolver, Class cls,
0180: Map attributes) {
0181: // sort of a hack to provide a 'default' resolver
0182: this .resolver = resolver;
0183: this .attributes.putAll(attributes);
0184: this .attributes.put(TAG_CLASS, Robot.getCanonicalClass(cls)
0185: .getName());
0186: if (resolver != null) {
0187: if (this .attributes.get(TAG_ID) == null) {
0188: this .attributes.put(TAG_ID, getUniqueID(new HashMap()));
0189: }
0190: resolver.addComponentReference(this );
0191: }
0192: }
0193:
0194: /** Create a reference based on the given component. Will not use or
0195: create any ancestor components/references.
0196: */
0197: public ComponentReference(Resolver resolver, Component comp) {
0198: this (resolver, comp, false, new HashMap());
0199: }
0200:
0201: /** Create a reference based on the given component. May recursively
0202: create other components required to identify this one.
0203: */
0204: public ComponentReference(Resolver resolver, Component comp,
0205: Map newReferences) {
0206: this (resolver, comp, true, newReferences);
0207: }
0208:
0209: /** Create a reference based on the given component. May recursively
0210: create other components required to identify this one if
0211: <code>includeHierarchy</code> is true. If <code>newReferences</code>
0212: is non-null, new ancestor references will be added to it; if null,
0213: they will be added to the resolver instead.
0214: */
0215: private ComponentReference(Resolver resolver, Component comp,
0216: boolean includeHierarchyAttributes, Map newReferences) {
0217: // This method may be called recursively (indirectly through
0218: // Resolver.addComponent) in order to add references for parent
0219: // components. Make note of whether this instantiation needs
0220: // to clear the failure cache when it's done.
0221: boolean cleanup = ((Boolean) ownsFailureCache.get())
0222: .booleanValue();
0223: ownsFailureCache.set(Boolean.FALSE);
0224:
0225: Log.debug("ctor: " + comp);
0226: this .resolver = resolver;
0227:
0228: if (AWT.isSharedInvisibleFrame(comp)) {
0229: setAttribute(TAG_ID, SHARED_FRAME_ID);
0230: setAttribute(TAG_CLASS, comp.getClass().getName());
0231: } else {
0232: Class refClass = Robot.getCanonicalClass(comp.getClass());
0233: setAttribute(TAG_CLASS, refClass.getName());
0234: }
0235: String name = Robot.getName(comp);
0236: if (name != null)
0237: setAttribute(TAG_NAME, name);
0238:
0239: // Only generate a tag attribute for custom components; using a tag
0240: // attribute for standard components is deprecated.
0241: String cname = comp.getClass().getName();
0242: if (!(cname.startsWith("java.awt.") || cname
0243: .startsWith("javax.swing."))) {
0244: String tag = ComponentTester.getTag(comp);
0245: if (tag != null)
0246: setAttribute(TAG_TAG, tag);
0247: }
0248:
0249: // only take the title on a Frame/Dialog
0250: // using the window title for other components is obsolete
0251: String title = Robot.getTitle(comp);
0252: if (title != null)
0253: setAttribute(TAG_TITLE, title);
0254:
0255: String borderTitle = Robot.getBorderTitle(comp);
0256: if (borderTitle != null)
0257: setAttribute(TAG_BORDER_TITLE, borderTitle);
0258:
0259: String label = Robot.getLabel(comp);
0260: if (label != null)
0261: setAttribute(TAG_LABEL, label);
0262: String text = Robot.getText(comp);
0263: if (text != null)
0264: setAttribute(TAG_TEXT, text);
0265: String icon = Robot.getIconName(comp);
0266: if (icon != null)
0267: setAttribute(TAG_ICON, icon);
0268:
0269: if (comp instanceof Applet) {
0270: Applet applet = (Applet) comp;
0271: setAttribute(TAG_PARAMS, encodeParams(applet));
0272: java.net.URL url = applet.getDocumentBase();
0273: setAttribute(TAG_DOCBASE, url != null ? url.toString()
0274: : "null");
0275: }
0276:
0277: // Work the the unique name based on the previous values
0278: String id = getUniqueID(newReferences);
0279: setAttribute(TAG_ID, id);
0280: Log.debug("Unique ID is " + id);
0281:
0282: // Populate the new reference now with this ID, this needs to be before
0283: // the parent lookup to prevent some repetition of values in certain
0284: // complex UI cases.
0285: //
0286: newReferences.put(id, this );
0287:
0288: // Finally work out the parent
0289:
0290: Container parent = resolver.getHierarchy().getParent(comp);
0291: if (null != parent) {
0292: // Don't save window indices, they're not sufficiently reliable
0293: if (!(comp instanceof Window)) {
0294: int index = Robot.getIndex(parent, comp);
0295: if (index != -1)
0296: setAttribute(TAG_INDEX, String.valueOf(index));
0297: }
0298: } else if (comp instanceof Window) {
0299: setAttribute(TAG_ROOT, "true");
0300: }
0301:
0302: try {
0303: if (includeHierarchyAttributes) {
0304: // Provide either the invoker or the window
0305: boolean needWindow = !(comp instanceof Window);
0306: Component invoker = null;
0307: if (comp instanceof JPopupMenu) {
0308: invoker = ((JPopupMenu) comp).getInvoker();
0309: ComponentReference ref = getReference(resolver,
0310: invoker, newReferences);
0311: setAttribute(TAG_INVOKER, ref.getID());
0312: needWindow = false;
0313: } else if (parent != null) {
0314: needWindow = !(parent instanceof Window);
0315: addParent(parent, newReferences);
0316: }
0317:
0318: if (needWindow && !(comp instanceof Window)) {
0319: Window win = AWT.getWindow(comp);
0320: if (win != null) {
0321: ComponentReference wref = getReference(
0322: resolver, win, newReferences);
0323: setAttribute(TAG_WINDOW, wref.getID());
0324: }
0325: }
0326:
0327: validate(comp, newReferences);
0328: }
0329: } finally {
0330: if (cleanup) {
0331: getLookupFailures().clear();
0332: getNonShowingMatches().clear();
0333: ownsFailureCache.set(Boolean.TRUE);
0334: }
0335: }
0336:
0337: // Set the cache immediately
0338: if (cacheOnCreation || AWT.isSharedInvisibleFrame(comp)) {
0339: Log.debug("Cacheing initial match");
0340: cachedLookup = new WeakReference(comp);
0341: } else {
0342: cachedLookup = null;
0343: }
0344:
0345: }
0346:
0347: /** Return the component in the current Hierarchy that best matches this
0348: reference.
0349: */
0350: public Component getComponent() throws ComponentNotFoundException,
0351: MultipleComponentsFoundException {
0352:
0353: if (resolver == null)
0354: throw new ComponentNotFoundException(
0355: "No default hierarchy has been provided");
0356: return getComponent(resolver.getHierarchy());
0357: }
0358:
0359: /** Return the component in the given Hierarchy that best matches this
0360: reference.
0361: */
0362: public Component getComponent(Hierarchy hierarchy)
0363: throws ComponentNotFoundException,
0364: MultipleComponentsFoundException {
0365:
0366: try {
0367: return findInHierarchy(null, hierarchy, 1, new HashMap());
0368: } finally {
0369: // never called recursively, so we can clear the cache here
0370: getLookupFailures().clear();
0371: getNonShowingMatches().clear();
0372: }
0373: }
0374:
0375: private void addParent(Container parent, Map newReferences) {
0376: ComponentReference ref = getReference(resolver, parent,
0377: newReferences);
0378: setAttribute(TAG_PARENT, ref.getID());
0379: }
0380:
0381: /** Returns whether the given component is reachable from the root of the
0382: * current hierarchy.
0383: * Popups' transient elements may already have gone away, and will be
0384: * unreachable.
0385: */
0386: private boolean reachableInHierarchy(Component c) {
0387: Window w = AWT.getWindow(c);
0388: if (w == null)
0389: return false;
0390: Window parent = (Window) resolver.getHierarchy().getParent(w);
0391: return (parent == null) ? resolver.getHierarchy().getRoots()
0392: .contains(w) : reachableInHierarchy(parent);
0393: }
0394:
0395: /** Ensure the reference can be used to actually look up the given
0396: * component. This can be a compute-intensive search, and thus is omitted
0397: * from the basic constructor.
0398: */
0399: private void validate(Component comp, Map newReferences) {
0400: // Under certain situations where we know the component is
0401: // unreachable from the root of the hierarchy, or if a lookup will
0402: // fail for other reasons, simply check for a match.
0403: // WARNING: this leaves a hole if the component actually needs an
0404: // ORDER attribute, but the ORDER attribute is intended for applets
0405: // only.
0406: if (!reachableInHierarchy(comp)) {
0407: int wt = getMatchWeight(comp, newReferences);
0408: int exact = getExactMatchWeight();
0409: if (wt < exact) {
0410: String msg = Strings.get("component.creation_mismatch",
0411: new Object[] { toXMLString(), comp.toString(),
0412: new Integer(wt), new Integer(exact), });
0413: throw new Error(msg);
0414: }
0415: } else {
0416: try {
0417: Log.debug("Finding in hierarchy ("
0418: + resolver.getHierarchy() + ")");
0419: findInHierarchy(null, resolver.getHierarchy(),
0420: getExactMatchWeight(), newReferences);
0421: } catch (MultipleComponentsFoundException multiples) {
0422: try {
0423: // More than one match found, so add more information
0424: Log.debug("Disambiguating");
0425: disambiguate(comp, multiples.getComponents(),
0426: newReferences);
0427: } catch (ComponentSearchException e) {
0428: if (!(e instanceof MultipleComponentsFoundException))
0429: Log.warn(e);
0430: throw new Error(
0431: "Reverse lookup failed to uniquely match "
0432: + Robot.toString(comp) + ": " + e);
0433: }
0434: } catch (ComponentNotFoundException e) {
0435: // This indicates a failure in the reference recording
0436: // mechanism, and requires a fix.
0437: throw new Error("Reverse lookup failed looking for "
0438: + Robot.toString(comp) + " using "
0439: + toXMLString() + ": " + e);
0440: }
0441: }
0442: }
0443:
0444: /** Return a descriptive name for the given component for use in UI
0445: * text (may be localized if appropriate and need not be re-usable
0446: * across locales.
0447: * @deprecated Use {@link Robot#getDescriptiveName(Component)} instead
0448: */
0449: public static String getDescriptiveName(Component c) {
0450: return Robot.getDescriptiveName(c);
0451: }
0452:
0453: /** Return a suitably descriptive name for this reference, for use as an
0454: ID (returns the ID itself if already set). Will never return an empty
0455: String.
0456: */
0457: public String getDescriptiveName() {
0458: String id = getAttribute(TAG_ID);
0459: if (id == null) {
0460: String[] attributes = { TAG_NAME, TAG_TITLE, TAG_TEXT,
0461: TAG_LABEL, TAG_ICON, };
0462: for (int i = 0; i < attributes.length; i++) {
0463: String att = getAttribute(attributes[i]);
0464: if (att != null && !"".equals(att)) {
0465: id = att;
0466: break;
0467: }
0468: }
0469: // Fall back to "<classname> Instance" if all else fails
0470: if (id == null) {
0471: String cname = getAttribute(TAG_CLASS);
0472: cname = cname.substring(cname.lastIndexOf(".") + 1);
0473: id = cname + " Instance";
0474: }
0475: }
0476: return id;
0477: }
0478:
0479: public String getID() {
0480: return getAttribute(TAG_ID);
0481: }
0482:
0483: public String getRefClassName() {
0484: return getAttribute(TAG_CLASS);
0485: }
0486:
0487: public String getAttribute(String key) {
0488: return (String) attributes.get(key);
0489: }
0490:
0491: public Map getAttributes() {
0492: return new TreeMap(attributes);
0493: }
0494:
0495: public void setAttribute(String key, String value) {
0496: xml = null;
0497: attributes.put(key, value);
0498: }
0499:
0500: /** Return whether a cast to the given class name from the given class
0501: would work.
0502: */
0503: private boolean isAssignableFrom(String refClassName, Class cls) {
0504: return refClassName.equals(cls.getName())
0505: || (!Component.class.equals(cls) && isAssignableFrom(
0506: refClassName, cls.getSuperclass()));
0507: }
0508:
0509: /** Return whether this reference has the same class or is a superclass of
0510: * the given component's class. Simply compare class names to avoid class
0511: * loader conflicts. Note that this does not take into account interfaces
0512: * (which is okay, since with GUI components we're only concerned with
0513: * class inheritance).
0514: */
0515: public boolean isAssignableFrom(Class cls) {
0516: return cls != null && Component.class.isAssignableFrom(cls)
0517: && isAssignableFrom(getAttribute(TAG_CLASS), cls);
0518: }
0519:
0520: public ComponentReference getParentReference(Map newRefs) {
0521: String parentID = getAttribute(TAG_PARENT);
0522: ComponentReference pref = null;
0523: if (parentID != null) {
0524: pref = resolver.getComponentReference(parentID);
0525: if (pref == null)
0526: pref = (ComponentReference) newRefs.get(parentID);
0527: }
0528: return pref;
0529: }
0530:
0531: /** Reference ID of this component's parent window (optional). */
0532: public ComponentReference getWindowReference(Map newReferences) {
0533: String windowID = getAttribute(TAG_WINDOW);
0534: ComponentReference wref = null;
0535: if (windowID != null) {
0536: wref = resolver.getComponentReference(windowID);
0537: if (wref == null)
0538: wref = (ComponentReference) newReferences.get(windowID);
0539: }
0540: return wref;
0541: }
0542:
0543: public ComponentReference getInvokerReference(Map newReferences) {
0544: String invokerID = getAttribute(TAG_INVOKER);
0545: ComponentReference iref = null;
0546: if (invokerID != null) {
0547: iref = resolver.getComponentReference(invokerID);
0548: if (iref == null)
0549: iref = (ComponentReference) newReferences
0550: .get(invokerID);
0551: }
0552: return iref;
0553: }
0554:
0555: /** Set all options based on the given XML.
0556: @deprecated
0557: */
0558: // This is only used when editing scripts, since we don't want to have to
0559: // hunt down existing references
0560: public void fromXML(String input) throws InvalidScriptException,
0561: IOException {
0562: StringReader reader = new StringReader(input);
0563: try {
0564: SAXBuilder builder = new SAXBuilder();
0565: Document doc = builder.build(reader);
0566: Element el = doc.getRootElement();
0567: if (el == null)
0568: throw new InvalidScriptException(
0569: "Invalid ComponentReference" + " XML '" + input
0570: + "'");
0571: fromXML(el, false);
0572: } catch (JDOMException e) {
0573: throw new InvalidScriptException(e.getMessage()
0574: + " (when parsing " + input + ")");
0575: }
0576: }
0577:
0578: /** Parse settings from the given XML. Only overwrite the ID if
0579: useGivenID is set.
0580: @throws InvalidScriptException if the given Element is not valid XML
0581: for a ComponentReference.
0582: */
0583: private void fromXML(Element el, boolean useIDFromXML)
0584: throws InvalidScriptException {
0585:
0586: Iterator iter = el.getAttributes().iterator();
0587: while (iter.hasNext()) {
0588: Attribute att = (Attribute) iter.next();
0589: String nodeName = att.getName();
0590: String value = att.getValue();
0591: if (nodeName.equals(TAG_ID) && !useIDFromXML)
0592: continue;
0593:
0594: setAttribute(nodeName, value);
0595: }
0596: if (getAttribute(TAG_CLASS) == null) {
0597: throw new InvalidScriptException("Class must be specified",
0598: el);
0599: }
0600: String id = getID();
0601: if (useIDFromXML) {
0602: // Make sure the ID we read in is not already in use by the manager
0603: if (id != null) {
0604: if (resolver.getComponentReference(id) != null) {
0605: String msg = "Persistent ID '" + id
0606: + "' is already in use";
0607: throw new InvalidScriptException(msg, el);
0608: }
0609: }
0610: }
0611: if (id == null) {
0612: Log.warn("null ID");
0613: setAttribute(TAG_ID, getUniqueID(new HashMap()));
0614: }
0615: }
0616:
0617: /** Generate an XML representation of this object. */
0618: public Element toXML() {
0619: Element el = new Element(TAG_COMPONENT);
0620: Iterator iter = new TreeMap(attributes).keySet().iterator();
0621: while (iter.hasNext()) {
0622: String key = (String) iter.next();
0623: String value = getAttribute(key);
0624: if (value != null)
0625: el.setAttribute(key, value);
0626: }
0627: return el;
0628: }
0629:
0630: /** @deprecated Used to be used to edit XML in a text editor. */
0631: public String toEditableString() {
0632: return toXMLString();
0633: }
0634:
0635: /** Two ComponentReferences with identical XML representations should
0636: be equal. */
0637: public boolean equals(Object obj) {
0638: return this == obj
0639: || (obj instanceof ComponentReference)
0640: && toXMLString().equals(
0641: ((ComponentReference) obj).toXMLString());
0642: }
0643:
0644: /** Return a human-readable representation. */
0645: public String toString() {
0646: String id = getID();
0647: String cname = getAttribute(TAG_CLASS);
0648: if (cname.startsWith("javax.swing."))
0649: cname = cname.substring(12);
0650: else if (cname.startsWith("java.awt."))
0651: cname = cname.substring(9);
0652: StringBuffer buf = new StringBuffer(id != null ? id
0653: : (cname + " (no id yet)"));
0654: if (id != null && id.indexOf("Instance") == -1) {
0655: buf.append(" (");
0656: buf.append(cname);
0657: buf.append(")");
0658: }
0659: return buf.toString();
0660: }
0661:
0662: public String toXMLString() {
0663: if (xml == null)
0664: xml = Step.toXMLString(this );
0665: return xml;
0666: }
0667:
0668: /** Return which of the otherwise indistinguishable components provides
0669: * the best match, or throw a MultipleComponentsFoundException if no
0670: * distinction is possible. Assumes that all given components return an
0671: * equivalent match weight.
0672: */
0673: private Component bestMatch(Set set)
0674: throws MultipleComponentsFoundException {
0675: Component[] matches = (Component[]) set
0676: .toArray(new Component[set.size()]);
0677: int weights[] = new int[matches.length];
0678: for (int i = 0; i < weights.length; i++) {
0679: // Prefer showing to non-showing
0680: Window w = AWT.getWindow(matches[i]);
0681: if (w != null && w.isShowing()) {
0682: weights[i] = MW_SHOWING;
0683: } else {
0684: weights[i] = 0;
0685: }
0686: // Preferring one enabled/focused state is dangerous to do:
0687: // An enabled component might be preferred over a disabled one,
0688: // but it will fail if you're trying to examine state on the
0689: // disabled component. Ditto for focused.
0690: }
0691: String horder = getAttribute(TAG_HORDER);
0692: if (horder != null) {
0693: for (int i = 0; i < matches.length; i++) {
0694: String order = getOrder(matches[i], matches, true);
0695: if (horder.equals(order)) {
0696: weights[i] += MW_HORDER;
0697: }
0698: }
0699: }
0700: String vorder = getAttribute(TAG_VORDER);
0701: if (vorder != null) {
0702: for (int i = 0; i < matches.length; i++) {
0703: String order = getOrder(matches[i], matches, false);
0704: if (vorder.equals(order)) {
0705: weights[i] += MW_VORDER;
0706: }
0707: }
0708: }
0709: // Figure out the best match, if any
0710: ArrayList best = new ArrayList();
0711: best.add(matches[0]);
0712: int max = 0;
0713: for (int i = 1; i < weights.length; i++) {
0714: if (weights[i] > weights[max]) {
0715: max = i;
0716: best.clear();
0717: best.add(matches[i]);
0718: } else if (weights[i] == weights[max]) {
0719: best.add(matches[i]);
0720: }
0721: }
0722: if (best.size() == 1) {
0723: return (Component) best.get(0);
0724: }
0725: // Finally, see if any match the old cached value
0726: Component cache = getCachedLookup(resolver.getHierarchy());
0727: if (cache != null) {
0728: Iterator iter = best.iterator();
0729: while (iter.hasNext()) {
0730: Component c = (Component) iter.next();
0731: if (cache == c) {
0732: return cache;
0733: }
0734: }
0735: }
0736: String msg = "Could not distinguish between " + best.size()
0737: + " components using " + toXMLString();
0738: matches = (Component[]) best
0739: .toArray(new Component[best.size()]);
0740: throw new MultipleComponentsFoundException(msg, matches);
0741: }
0742:
0743: /** Return the order of the given component among the array given, sorted
0744: * by horizontal or vertical screen position. All components with the
0745: * same effective value will have the same order.
0746: */
0747: static String getOrder(Component original, Component[] matchList,
0748: boolean horizontal) {
0749: Comparator c = horizontal ? HORDER_COMPARATOR
0750: : VORDER_COMPARATOR;
0751: Component[] matches = (Component[]) matchList.clone();
0752: Arrays.sort(matches, c);
0753: int order = 0;
0754: for (int i = 0; i < matches.length; i++) {
0755: // Only change the order magnitude if there is a difference
0756: // between consecutive objects.
0757: if (i > 0 && c.compare(matches[i - 1], matches[i]) != 0)
0758: ++order;
0759: if (matches[i] == original) {
0760: return String.valueOf(order);
0761: }
0762: }
0763: return null;
0764: }
0765:
0766: /** Add sufficient information to the reference to distinguish it among
0767: the given components.
0768: Note that the ordering attributes can only be evaluated when looking
0769: at several otherwise identical components.
0770: */
0771: private void disambiguate(Component original, Component[] matches,
0772: Map newReferences) throws ComponentNotFoundException,
0773: MultipleComponentsFoundException {
0774: Log.debug("Attempting to disambiguate multiple matches");
0775: Container parent = resolver.getHierarchy().getParent(original);
0776: boolean retryOnFailure = false;
0777: String order = null;
0778: try {
0779: String cname = original.getClass().getName();
0780: // Use the inner class name unless it's numeric (numeric values
0781: // can easily change).
0782: if (!cname.equals(getAttribute(TAG_CLASS))
0783: && !expressionMatch(ANON_INNER_CLASS, cname)) {
0784: setAttribute(TAG_CLASS, original.getClass().getName());
0785: retryOnFailure = true;
0786: } else if (parent != null
0787: && getAttribute(TAG_PARENT) == null
0788: && !(original instanceof JPopupMenu)) {
0789: Log.debug("Adding parent");
0790: addParent(parent, newReferences);
0791: retryOnFailure = true;
0792: } else if (getAttribute(TAG_HORDER) == null
0793: && (order = getOrder(original, matches, true)) != null) {
0794: Log.debug("Adding horder");
0795: setAttribute(TAG_HORDER, order);
0796: retryOnFailure = true;
0797: } else if (getAttribute(TAG_VORDER) == null
0798: && (order = getOrder(original, matches, false)) != null) {
0799: Log.debug("Adding vorder");
0800: setAttribute(TAG_VORDER, order);
0801: retryOnFailure = true;
0802: }
0803: // Try the lookup again to make sure it works this time
0804: Log.debug("Retrying lookup with new values");
0805: // Remove this cref and its ancestors from the failure
0806: // cache so we don't automatically fail
0807: getLookupFailures().remove(this );
0808: findInHierarchy(null, resolver.getHierarchy(),
0809: getExactMatchWeight(), newReferences);
0810: Log.debug("Success!");
0811: } catch (MultipleComponentsFoundException multiples) {
0812: if (retryOnFailure) {
0813: disambiguate(original, multiples.getComponents(),
0814: newReferences);
0815: } else
0816: throw multiples;
0817: }
0818: }
0819:
0820: /** Return a measure of how well the given component matches the given
0821: * component reference. The weight performs two functions; one is to
0822: * loosely match so that we can find a component even if some of its
0823: * attributes have changed. The other is to distinguish between similar
0824: * components. <p>
0825: * In general, we want to match if we get any weight at all, and there's
0826: * only one component that matches.
0827: */
0828: int getMatchWeight(Component comp) {
0829: return getMatchWeight(comp, new HashMap());
0830: }
0831:
0832: /** Return a measure of how well the given component matches the given
0833: * component reference. The weight performs two functions; one is to
0834: * loosely match so that we can find a component even if some of its
0835: * attributes have changed. The other is to distinguish between similar
0836: * components. <p>
0837: * In general, we want to match if we get any weight at all, and there's
0838: * only one component that matches.
0839: */
0840: private int getMatchWeight(Component comp, Map newReferences) {
0841: // Match weights may be positive or negative. They should only be
0842: // negative if the attribute is highly unlikely to change.
0843:
0844: int weight = MW_FAILURE;
0845:
0846: if (null == comp) {
0847: return MW_FAILURE;
0848: }
0849:
0850: // FIXME might want to allow changing the class? or should we just
0851: // ask the user to fix the script by hand?
0852: if (!isAssignableFrom(comp.getClass())) {
0853: return MW_FAILURE;
0854: }
0855:
0856: weight += MW_CLASS;
0857: // Exact class matches are better than non-exact matches
0858: if (getAttribute(TAG_CLASS).equals(comp.getClass().getName()))
0859: weight += MW_CLASS;
0860:
0861: String refTag = getAttribute(TAG_TAG);
0862: String compTag = null;
0863: if (null != refTag) {
0864: compTag = ComponentTester.getTag(comp);
0865: if (compTag != null && expressionMatch(refTag, compTag)) {
0866: weight += MW_TAG;
0867: }
0868: }
0869:
0870: String refName = getAttribute(TAG_NAME);
0871: String compName = Robot.getName(comp);
0872: if (null != refName) {
0873: if (compName != null && expressionMatch(refName, compName)) {
0874: weight += MW_NAME;
0875: } else {
0876: weight -= MW_NAME;
0877: }
0878: } else {
0879: Log.log("Component name set as " + compName
0880: + "but the reference name was null");
0881: }
0882:
0883: if (null != getAttribute(TAG_INVOKER)) {
0884: ComponentReference iref = getInvokerReference(newReferences);
0885: Component invoker = (comp instanceof JPopupMenu) ? ((JPopupMenu) comp)
0886: .getInvoker()
0887: : null;
0888: if (invoker == iref
0889: .resolveComponent(invoker, newReferences)) {
0890: weight += MW_INVOKER;
0891: } else {
0892: // Invoking components aren't likely to change
0893: weight -= MW_INVOKER;
0894: }
0895: }
0896:
0897: if (null != getAttribute(TAG_PARENT)) {
0898: ComponentReference pref = getParentReference(newReferences);
0899: Component parent = resolver.getHierarchy().getParent(comp);
0900: if (parent == pref.resolveComponent(parent, newReferences)) {
0901: weight += MW_PARENT;
0902: }
0903: // Don't detract on parent mismatch, since changing a parent is
0904: // not that big a change (e.g. adding a scroll pane)
0905: }
0906: // ROOT and PARENT are mutually exclusive
0907: else if (null != getAttribute(TAG_ROOT)) {
0908: weight += MW_ROOT;
0909: }
0910:
0911: if (null != getAttribute(TAG_WINDOW)) {
0912: ComponentReference wref = getWindowReference(newReferences);
0913: Window w = AWT.getWindow(comp);
0914: if (w == wref.resolveComponent(w, newReferences)) {
0915: weight += MW_WINDOW;
0916: } else if (w != null) {
0917: // Changing windows is a big change and not very likely
0918: weight -= MW_WINDOW;
0919: }
0920: }
0921:
0922: // TITLE is no longer used except by Frames, Dialogs, and
0923: // JInternalFrames, being superseded by the ancestor window
0924: // reference. For other components, it represents an available
0925: // ancestor window title (deprecated usage only).
0926: String title = getAttribute(TAG_TITLE);
0927: if (null != title) {
0928: String title2 = (comp instanceof Frame
0929: || comp instanceof Dialog || comp instanceof JInternalFrame) ? Robot
0930: .getTitle(comp)
0931: : getComponentWindowTitle(comp);
0932: if (title2 != null && expressionMatch(title, title2)) {
0933: weight += MW_TITLE;
0934: }
0935: // Don't subtract on mismatch, since title changes are common
0936: }
0937:
0938: String borderTitle = getAttribute(TAG_BORDER_TITLE);
0939: if (null != borderTitle) {
0940: String bt2 = Robot.getBorderTitle(comp);
0941: if (bt2 != null && expressionMatch(borderTitle, bt2)) {
0942: weight += MW_BORDER_TITLE;
0943: }
0944: }
0945:
0946: String label = getAttribute(TAG_LABEL);
0947: if (null != label) {
0948: String label2 = Robot.getLabel(comp);
0949: if (label2 != null && expressionMatch(label, label2)) {
0950: weight += MW_LABEL;
0951: }
0952: }
0953:
0954: String text = getAttribute(TAG_TEXT);
0955: if (null != text) {
0956: String text2 = Robot.getText(comp);
0957: if (text2 != null && expressionMatch(text, text2)) {
0958: weight += MW_TEXT;
0959: }
0960: }
0961:
0962: String icon = getAttribute(TAG_ICON);
0963: if (null != icon) {
0964: String icon2 = Robot.getIconName(comp);
0965: if (icon2 != null && expressionMatch(icon, icon2)) {
0966: weight += MW_ICON;
0967: }
0968: }
0969:
0970: String idx = getAttribute(TAG_INDEX);
0971: if (null != idx) {
0972: Container parent = resolver.getHierarchy().getParent(comp);
0973: if (null != parent) {
0974: int i = Robot.getIndex(parent, comp);
0975: if (expressionMatch(idx, String.valueOf(i))) {
0976: weight += MW_INDEX;
0977: }
0978: }
0979: // Don't subtract for index mismatch, since ordering changes are
0980: // common.
0981: }
0982:
0983: if (comp instanceof Applet) {
0984: Applet applet = (Applet) comp;
0985: String params = getAttribute(TAG_PARAMS);
0986: if (null != params) {
0987: String params2 = encodeParams(applet);
0988: if (expressionMatch(params, params2))
0989: weight += MW_PARAMS;
0990: }
0991: String docBase = getAttribute(TAG_DOCBASE);
0992: if (null != docBase) {
0993: java.net.URL url = applet.getDocumentBase();
0994: if (url != null
0995: && expressionMatch(docBase, url.toString()))
0996: weight += MW_DOCBASE;
0997: }
0998: // No negative weighting here
0999: }
1000:
1001: if (Log.isClassDebugEnabled(ComponentReference.class))
1002: Log.debug("Compared " + Robot.toString(comp) + " to "
1003: + toXMLString() + " weight is " + weight);
1004:
1005: return weight;
1006: }
1007:
1008: /** Return the total weight required for an exact match. */
1009: private int getExactMatchWeight() {
1010: int weight = MW_CLASS;
1011: if (getAttribute(TAG_NAME) != null)
1012: weight += MW_NAME;
1013: if (getAttribute(TAG_TAG) != null)
1014: weight += MW_TAG;
1015: if (getAttribute(TAG_INVOKER) != null)
1016: weight += MW_INVOKER;
1017: if (getAttribute(TAG_ROOT) != null)
1018: weight += MW_ROOT;
1019: if (getAttribute(TAG_PARENT) != null)
1020: weight += MW_PARENT;
1021: if (getAttribute(TAG_WINDOW) != null)
1022: weight += MW_WINDOW;
1023: if (getAttribute(TAG_TITLE) != null)
1024: weight += MW_TITLE;
1025: if (getAttribute(TAG_BORDER_TITLE) != null)
1026: weight += MW_BORDER_TITLE;
1027: if (getAttribute(TAG_INDEX) != null)
1028: weight += MW_INDEX;
1029: if (getAttribute(TAG_LABEL) != null)
1030: weight += MW_LABEL;
1031: if (getAttribute(TAG_TEXT) != null)
1032: weight += MW_TEXT;
1033: if (getAttribute(TAG_ICON) != null)
1034: weight += MW_ICON;
1035: if (getAttribute(TAG_PARAMS) != null)
1036: weight += MW_PARAMS;
1037: if (getAttribute(TAG_DOCBASE) != null)
1038: weight += MW_DOCBASE;
1039:
1040: if (Log.isClassDebugEnabled(ComponentReference.class))
1041: Log.debug("Exact match weight for " + toXMLString()
1042: + " is " + weight);
1043: return weight;
1044: }
1045:
1046: /** Returns an existing component which matches this reference; the given
1047: Component is the one that is expected to match. Returns null if no
1048: match or multiple matches are found and the preferred Component is not
1049: among them.<p>
1050: This method is used in two instances:
1051: <ul>
1052: <li>Resolving a component's ancestors (window, parent, or invoker),
1053: the ancestor reference is checked against the ancestor of the
1054: Component currently being compared.
1055: <li>When referring to a component, determining if a reference to it
1056: already exists, all references are resolved to see if any resolves to
1057: the preferred Component.
1058: </ul>
1059: While there is a subtle difference between the two cases (when running
1060: a test it is expected that there will be some match, whereas when
1061: creating a new reference there may or may not be a match, based on the
1062: current script contents), it is not a useful distinction.
1063: */
1064: private Component resolveComponent(Component preferred,
1065: Map newReferences) {
1066: // This call should be equivalent to getComponent(), but without
1067: // clearing the lookup failure cache on completion
1068: if (Log.isClassDebugEnabled(ComponentReference.class))
1069: Log.debug("Looking up " + toXMLString() + " in hierarchy");
1070: Component found = null;
1071: try {
1072: found = findInHierarchy(null, resolver.getHierarchy(), 1,
1073: newReferences);
1074: } catch (MultipleComponentsFoundException e) {
1075: Component[] list = e.getComponents();
1076: for (int i = 0; i < list.length; i++) {
1077: if (list[i] == preferred)
1078: return preferred;
1079: }
1080: //Log.warn("Preferred not found among many");
1081: } catch (ComponentNotFoundException e) {
1082: // If the preferred component is not reachable in the hierarchy
1083: // (if it has just been removed from the hierarchy, or an ancestor
1084: // pane was replaced), require an exact match to avoid
1085: // spurious matches.
1086: int minWeight = getExactMatchWeight();
1087: if (getAttribute(TAG_WINDOW) != null)
1088: minWeight -= MW_WINDOW;
1089: if (getAttribute(TAG_PARENT) != null)
1090: minWeight -= MW_PARENT;
1091: if (AWT.getWindow(preferred) == null
1092: && getMatchWeight(preferred) >= minWeight) {
1093: Log.debug("Using preferred component: "
1094: + Robot.toString(preferred));
1095: found = preferred;
1096: }
1097: }
1098: return found;
1099: }
1100:
1101: /** Returns a reference to the given component, preferring an existing
1102: * reference if a matching one is available or creating a new one if not.
1103: * The new references are <i>not</i> added to the resolver.
1104: */
1105: // FIXME: keep newly-created ancestors in a collection and let the
1106: // resolver add them. (maybe create everything, then let the resolver
1107: // sort out duplicates when adding).
1108: // TODO: require exact matches, otherwise create a new ref; this means
1109: // that we need to provide a method to repair refs.
1110: public static ComponentReference getReference(Resolver r,
1111: Component comp, Map newReferences) {
1112: Log
1113: .debug("Looking for a reference for "
1114: + Robot.toString(comp));
1115: // Preserve the failure cache across both lookup and creation
1116: boolean cleanup = ((Boolean) ownsFailureCache.get())
1117: .booleanValue();
1118: ownsFailureCache.set(Boolean.FALSE);
1119:
1120: // Allow the resolver to do cacheing if it needs to; otherwise we'd
1121: // call matchExisting directly.
1122: ComponentReference ref = r.getComponentReference(comp);
1123: try {
1124:
1125: // In the case where we are looking at a window we might find
1126: // that window property has been populated
1127:
1128: if (ref == null && comp instanceof Window) {
1129: ref = ComponentReference.matchExisting(comp,
1130: newReferences.values(), newReferences);
1131:
1132: if (ref != null) {
1133: // check tha match is a good one
1134: int exactMatch = ref.getExactMatchWeight();
1135: // Make sure that newReferences are passed in
1136: int componentMatch = ref.getMatchWeight(comp,
1137: newReferences);
1138: if (componentMatch < exactMatch) {
1139: ref = null;
1140: }
1141: }
1142: }
1143:
1144: //
1145: if (ref == null) {
1146: Log
1147: .debug("No existing reference found, creating a new one");
1148: ref = new ComponentReference(r, comp, newReferences);
1149: }
1150: } finally {
1151: if (cleanup) {
1152: getLookupFailures().clear();
1153: getNonShowingMatches().clear();
1154: ownsFailureCache.set(Boolean.TRUE);
1155: }
1156: }
1157: return ref;
1158: }
1159:
1160: /** Match the given component against an existing set of references. */
1161: public static ComponentReference matchExisting(
1162: final Component comp, Collection existing) {
1163: return matchExisting(comp, existing, Collections.EMPTY_MAP);
1164: }
1165:
1166: /** Match the given component against an existing set of references.
1167: * Extended method that also takes in a list of new references that
1168: * might have been created in this cycle
1169: */
1170: public static ComponentReference matchExisting(
1171: final Component comp, Collection existing, Map newReferences) {
1172:
1173: Log.debug("Matching " + Robot.toString(comp)
1174: + " against existing refs");
1175:
1176: // This method might be called recursively (indirectly through
1177: // Resolver.addComponent) in order to add references for parent
1178: // components. Make note of whether this level of invocation needs
1179: // to clear the failure cache when it's done.
1180: boolean cleanup = ((Boolean) ownsFailureCache.get())
1181: .booleanValue();
1182: ownsFailureCache.set(Boolean.FALSE);
1183:
1184: ComponentReference match = null;
1185: Iterator iter = existing.iterator();
1186: // Sort such that the best match comes first
1187: Map matches = new TreeMap(new Comparator() {
1188: public int compare(Object o1, Object o2) {
1189: return ((ComponentReference) o2).getMatchWeight(comp)
1190: - ((ComponentReference) o1)
1191: .getMatchWeight(comp);
1192: }
1193: });
1194: while (iter.hasNext()) {
1195: ComponentReference ref = (ComponentReference) iter.next();
1196: if (comp == ref
1197: .getCachedLookup(ref.resolver.getHierarchy())
1198: || comp == ref
1199: .resolveComponent(comp, newReferences)) {
1200: matches.put(ref, Boolean.TRUE);
1201: }
1202: }
1203: if (matches.size() > 0) {
1204: match = (ComponentReference) matches.keySet().iterator()
1205: .next();
1206: }
1207:
1208: if (cleanup) {
1209: // Clear failures only after we've attempted a match for *all* refs
1210: getLookupFailures().clear();
1211: getNonShowingMatches().clear();
1212: ownsFailureCache.set(Boolean.TRUE);
1213: }
1214:
1215: Log.debug(match != null ? "Found" : "Not found");
1216: return match;
1217: }
1218:
1219: /** Return whether the given pattern matches the given string. Performs
1220: * variable substitution on the pattern.
1221: */
1222: boolean expressionMatch(String pattern, String actual) {
1223: pattern = ArgumentParser.substitute(resolver, pattern);
1224: return ExtendedComparator.stringsMatch(pattern, actual);
1225: }
1226:
1227: /** Convert the given applet's parameters into a simple String. */
1228: private String encodeParams(Applet applet) {
1229: // TODO: is there some other way of digging out the full set of
1230: // parameters that were passed the applet? b/c here we rely on the
1231: // applet having been properly written to tell us about supported
1232: // parameters.
1233: StringBuffer sb = new StringBuffer();
1234: String[][] info = applet.getParameterInfo();
1235: if (info == null) {
1236: // Default implementation of applet returns null
1237: return "null";
1238: }
1239: for (int i = 0; i < info.length; i++) {
1240: sb.append(info[i][0]);
1241: sb.append("=");
1242: String param = applet.getParameter(info[i][0]);
1243: sb.append(param != null ? param : "null");
1244: sb.append(";");
1245: }
1246: return sb.toString();
1247: }
1248:
1249: /** Return the cached component match, if any. */
1250: Component getCachedLookup(Hierarchy hierarchy) {
1251: if (cachedLookup != null) {
1252: Component c = (Component) cachedLookup.get();
1253: // Discard if the component has been gc'd, is no longer in the
1254: // hierarchy, or is no longer reachable from a Window.
1255: if (c != null && hierarchy.contains(c)
1256: && AWT.getWindow(c) != null) {
1257: return c;
1258: }
1259: Log.debug("Discarding cached value: " + Robot.toString(c));
1260: cachedLookup = null;
1261: }
1262: return null;
1263: }
1264:
1265: /** Compare this ComponentReference against each component below the given
1266: * root in the given hierarchy whose match weight exceeds the given
1267: * minimum. If a valid cached lookup exists, that is returned
1268: * immediately.
1269: */
1270: // TODO: refactor this to extract the finder/lookup logic into a separate
1271: // class. the ref should only store attributes.
1272: private Component findInHierarchy(Container root,
1273: Hierarchy hierarchy, int weight, Map newReferences)
1274: throws ComponentNotFoundException,
1275: MultipleComponentsFoundException {
1276: Component match = null;
1277:
1278: ComponentSearchException cse = (ComponentSearchException) getLookupFailures()
1279: .get(this );
1280: if (cse instanceof ComponentNotFoundException) {
1281: Log.debug("lookup already failed: " + cse);
1282: throw (ComponentNotFoundException) cse;
1283: }
1284: if (cse instanceof MultipleComponentsFoundException) {
1285: Log.debug("lookup already failed: " + cse);
1286: throw (MultipleComponentsFoundException) cse;
1287: }
1288:
1289: Set set = new HashSet();
1290: match = getCachedLookup(hierarchy);
1291: if (match != null) {
1292: // This is always valid
1293: if (AWT.isSharedInvisibleFrame(match))
1294: return match;
1295: // TODO: always use the cached lookup; since TestHierarchy
1296: // auto-disposes, only improperly disposed components will still
1297: // match. Codify this behavior with an explicit test.
1298:
1299: // Normally, we'd always want to use the cached lookup, but there
1300: // are instances where a component hierarchy may be used in a
1301: // transient way, so a given reference may need to match more than
1302: // one object without the first having been properly disposed.
1303: // Consider a createDialog() method, which creates an identical
1304: // dialog on each invocation, with an OK button. Every call of
1305: // the method is semantically providing the same component,
1306: // although the implementation may create a new one each time. If
1307: // previous instances have not been properly disposed, we need a
1308: // way to prefer a brand new instance over an old one. We do that
1309: // by checking the cache window's showing state.
1310: // A showing match will trump a non-showing one,
1311: // but if there are multiple, non-showing matches, the cached
1312: // lookup will win.
1313: // We check the window, not the component itself, because some
1314: // components hide their children.
1315: Window w = AWT.getWindow(match);
1316: if (w != null
1317: && (w.isShowing() || getNonShowingMatches().get(
1318: this ) == match)) {
1319: Log.debug("Using cached lookup for " + getID()
1320: + " (hierarchy=" + hierarchy + ")");
1321: return match;
1322: }
1323: Log
1324: .debug("Skipping non-showing match (once) "
1325: + hashCode());
1326: }
1327:
1328: weight = findMatchesInHierarchy(root, hierarchy, weight, set,
1329: newReferences);
1330:
1331: Log.debug("Found " + set.size() + " matches for "
1332: + toXMLString());
1333: if (set.size() == 1) {
1334: match = (Component) set.iterator().next();
1335: } else if (set.size() > 0) {
1336: // Distinguish between more than one match with the exact same
1337: // weight
1338: try {
1339: match = bestMatch(set);
1340: } catch (MultipleComponentsFoundException e) {
1341: getLookupFailures().put(this , e);
1342: throw e;
1343: }
1344: }
1345: if (match == null) {
1346: String msg = "No component found which matches "
1347: + toXMLString();
1348: ComponentNotFoundException e = new ComponentNotFoundException(
1349: msg);
1350: getLookupFailures().put(this , e);
1351: throw e;
1352: }
1353: // This provides significant speedup when many similar components are
1354: // in play.
1355: Log.debug("Cacheing match: "
1356: + Integer.toHexString(match.hashCode()));
1357: cachedLookup = new WeakReference(match);
1358: if (!match.isShowing()) {
1359: getNonShowingMatches().put(this , match);
1360: }
1361: return match;
1362: }
1363:
1364: /** Return the the set of all components under the given component's
1365: * hierarchy (inclusive) which match the given reference.
1366: */
1367: private int findMatchesInHierarchy(Component root,
1368: Hierarchy hierarchy, int currentMaxWeight, Set currentSet,
1369: Map newReferences) {
1370:
1371: if (root == null) {
1372: // Examine all top-level components and their owned windows.
1373: Iterator iter = hierarchy.getRoots().iterator();
1374: while (iter.hasNext()) {
1375: currentMaxWeight = findMatchesInHierarchy((Window) iter
1376: .next(), hierarchy, currentMaxWeight,
1377: currentSet, newReferences);
1378: }
1379: return currentMaxWeight;
1380: }
1381:
1382: if (!hierarchy.contains(root)) {
1383: Log.debug("Component not in hierarchy");
1384: return currentMaxWeight;
1385: }
1386:
1387: int weight = getMatchWeight(root, newReferences);
1388: if (weight > currentMaxWeight) {
1389: currentSet.clear();
1390: currentMaxWeight = weight;
1391: currentSet.add(root);
1392: } else if (weight == currentMaxWeight) {
1393: currentSet.add(root);
1394: }
1395:
1396: // TODO: don't check window contents in the hierarchy if the cref is a
1397: // Window. oops, how do you tell the cref is a Window?
1398: // (no window tag, parent tag or root tag, no index tag)
1399: // no guarantee, though
1400: Collection kids = hierarchy.getComponents(root);
1401: Iterator iter = kids.iterator();
1402: while (iter.hasNext()) {
1403: Component child = (Component) iter.next();
1404: currentMaxWeight = findMatchesInHierarchy(child, hierarchy,
1405: currentMaxWeight, currentSet, newReferences);
1406: }
1407:
1408: return currentMaxWeight;
1409: }
1410:
1411: /** Given an array of name, value pairs, generate a map suitable for
1412: creating a ComponentReference.
1413: */
1414: private static Map createAttributeMap(String[][] values) {
1415: Map map = new HashMap();
1416: for (int i = 0; i < values.length; i++) {
1417: map.put(values[i][0], values[i][1]);
1418: }
1419: return map;
1420: }
1421:
1422: private static final Comparator HORDER_COMPARATOR = new Comparator() {
1423: public int compare(Object o1, Object o2) {
1424: Component c1 = (Component) o1;
1425: Component c2 = (Component) o2;
1426: int x1 = -100000;
1427: int x2 = -100000;
1428: try {
1429: x1 = c1.getLocationOnScreen().x;
1430: } catch (Exception e) {
1431: }
1432: try {
1433: x2 = c2.getLocationOnScreen().x;
1434: } catch (Exception e) {
1435: }
1436: return x1 - x2;
1437: }
1438: };
1439:
1440: private static final Comparator VORDER_COMPARATOR = new Comparator() {
1441: public int compare(Object o1, Object o2) {
1442: Component c1 = (Component) o1;
1443: Component c2 = (Component) o2;
1444: int y1 = -100000;
1445: int y2 = -100000;
1446: try {
1447: y1 = c1.getLocationOnScreen().y;
1448: } catch (Exception e) {
1449: }
1450: try {
1451: y2 = c2.getLocationOnScreen().y;
1452: } catch (Exception e) {
1453: }
1454: return y1 - y2;
1455: }
1456: };
1457:
1458: public int compareTo(Object o) {
1459: return getID().compareTo(((ComponentReference) o).getID());
1460: }
1461:
1462: private String getComponentWindowTitle(Component c) {
1463: Component parent = c;
1464: while (!(c instanceof Frame || c instanceof Dialog)
1465: && (c = resolver.getHierarchy().getParent(parent)) != null) {
1466: parent = c;
1467: }
1468: String title = null;
1469: if (parent instanceof Frame) {
1470: title = ((Frame) parent).getTitle();
1471: } else if (parent instanceof Dialog) {
1472: title = ((Dialog) parent).getTitle();
1473: }
1474: return title;
1475: }
1476:
1477: private static Map getLookupFailures() {
1478: return (Map) lookupFailures.get();
1479: }
1480:
1481: private static Map getNonShowingMatches() {
1482: return (Map) nonShowingMatches.get();
1483: }
1484:
1485: public String getUniqueID(Map refs) {
1486: String id = getDescriptiveName();
1487: String ext = "";
1488: int count = 2;
1489: while (refs.get(id + ext) != null
1490: || resolver.getComponentReference(id + ext) != null) {
1491: ext = " " + count++;
1492: }
1493: return id + ext;
1494: }
1495: }
|