org.safs.selenium.webdriver.lib.SearchObject.java Source code

Java tutorial

Introduction

Here is the source code for org.safs.selenium.webdriver.lib.SearchObject.java

Source

/**
 * Copyright (C) SAS Institute, All rights reserved.
 * General Public License: http://www.opensource.org/licenses/gpl-license.php
 **/
package org.safs.selenium.webdriver.lib;

/**
 * History:<br>
 *
 *  <br>  DEC 19, 2013    (DHARMESH) Initial release.
 *  <br>  DEC 25, 2013    (sbjlwa) Add javascript calling DOJO APIs.
 *  <br>  JAN 16, 2014    (DHARMESH) Updated reconnection browser support.
 *  <br>  JAN 16, 2014    (DHARMESH) Added WebDriver() for end user.
 *  <br>  MAR 10, 2014    (SBJLWA) Handle frames for HTML Application, Get browser window information by javascript.
 *  <br>  MAR 27, 2014    (SBJLWA) Add ability to search by XPATH and CSS.
 *  <br>  APR 14, 2014    (SBJLWA) Add ability to listener to a javascript event. Rename some functions.
 *  <br>  AUG 12, 2014    (SBJLWA) Modify method getObject(): return null if no webelement can be found.
 *  <br>  AUG 27, 2014    (LeiWang) Modify method getObject(): Get element inside a frame if no FrameRS
 *                                      is preceding RS of a child in map file; we consider that the child is
 *                                      in the same frame as parent.
 *  <br>  AUG 29, 2014    (DHARMESH) Add selenium grid host and port support.
 *  <br>  OCT 22, 2014    (LeiWang) Modify method getObject(): use webdriver to find frame webelement to avoid StaleElementException.
 *  <br>  OCT 29, 2014    (LeiWang) Add support for qualifier 'ItemIndex', 'Path', 'Property' and 'PropertyContains'.
 *                                  Add support for prefix ':PASM:'.
 *  <br>  NOV 05, 2014    (LeiWang) Add pageHasChanged(): to detect if the web page url has changed.
 *  <br>  NOV 28, 2014    (LeiWang) Modify executeScript(): detect javascript error and throw JSException.
 *                                  Add js_xxx(): to manipulate javascript error global variable.
 *  <br>  DEC 05, 2014    (LeiWang) Modify getObject(): Fix frame window recognition string definition error.
 *  <br>  DEC 11, 2014    (LeiWang) Modify isSapComponent() etc: fix the domain-detection failure.
 *  <br>  JAN 05, 2015    (LeiWang) Modify executeScript(): get javascript debug messages and write to debug log.
 *  <br>  JUL 23, 2015    (LeiWang) Add js_getGlobalVariable(), js_setGlobalVariable()
 *                                  modify js_getGlobalBoolVariable(), js_setGlobalBoolVariable().
 *  <br>  OCT 30, 2015    (SBJLWA)  Move method isVisible(), isDisplayed and isStale() to this class from WDLibrary.
 *                                  Add method highlightThenClear(). Modify highlight(WebElement): check stale.
 *  <br>  NOV 12, 2015    (SBJLWA)  Modify method getObject(): continue to search component on the new page even 'lastFrame' is stale.
 *  <br>  NOV 26, 2015    (SBJLWA)  Modify method getObject(): reset 'lastFrame' to null if exception is thrown out during switch of frame.
 *  <br>  DEC 25, 2015    (SBJLWA)  Modify getObjectByText() and getObjectByTitle(): try to get partial, case-insensitive matched element.
 *  <br>  JAN 05, 2015    (SBJLWA)  Modify getObjectByQualifier() etc.: support one qualifier with "Contains", such as TextContains=, TitleContains=.
 *  <br>  MAR 17, 2016    (CANAGL)  Modified to support getObjects returning multiple matches.
 *  <br>  MAY 04, 2016    (SBJLWA)  Modified js_getErrorXXX(): call {@link #js_executeWithTimeout(String, long)} to avoid JavaScriptExecutor locking problem.
 *  <br>  MAY 06, 2016    (SBJLWA)  Modified js_executeWithTimeout(): throw SeleniumPlusException if the timeout is not enough for javascript execution.
 *                                  Modified js_getErrorCode(): enlarge the timeout to 5000 milliseconds.
 */
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.InvalidSelectorException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.Point;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriver.TargetLocator;
import org.openqa.selenium.WebDriver.Window;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.safs.GuiObjectRecognition;
import org.safs.GuiObjectVector;
import org.safs.IndependantLog;
import org.safs.Processor;
import org.safs.SAFSException;
import org.safs.StringUtils;
import org.safs.selenium.util.JSException;
import org.safs.selenium.util.JavaScriptFunctions;
import org.safs.selenium.webdriver.SeleniumPlus.WDTimeOut;
import org.safs.selenium.webdriver.WebDriverGUIUtilities;
import org.safs.selenium.webdriver.lib.RS.XPATH;
import org.safs.selenium.webdriver.lib.RemoteDriver.SessionInfo;
import org.safs.selenium.webdriver.lib.model.Element;
import org.safs.selenium.webdriver.lib.model.TextMatchingCriterion;
import org.safs.tools.stringutils.StringUtilities;

import com.thoughtworks.selenium.SeleniumException;

/**
 * Primary class used to find WebElements with Selenium WebDriver.
 * <p>
 * Supports various recognition string syntax, to include:
 * <p>
 * <ul>Identify the proper FRAME, if present:
 *     <p><ul><pre>
 *     FRAMEINDEX=N;\; (1-based, 1 means the first frame)
 *     FRAMEID=id;\;
 *     FRAMENAME=name;\;
 *     FRAMEXPATH=//iframe[@id='frameId'];\;
 *
 *     (note: parent/child elements separated by ";\;" )
 *     (note: FRAMEINDEX is NOT ready for using )
 *     (note: If there is no frame-expression in a RS, the last visited frame
 *            will be the frame where to find component. By default, the last visited
 *            frame is the main frame (the topmost html Document).
 *            And that is why if we specify frame for a window, then their children
 *            doesn't need to specify the frame information anymore. For example:
 *            [HelpPopup]
 *            HelpPopup="FRAMEID=VisualAnalyticsHubLogon_iframe;\;id=__popover8-popover"
 *            BtnHelpCenter="id=__item23"
 *
 *            BtnHelpCenter is supposed to be found in frame 'VisualAnalyticsHubLogon_iframe'
 *      )
 *     </pre></ul>
 * <p>Identify any supported "TYPE" qualifier, if desired:
 *     <p><ul><pre>
 *     TYPE=DOJOTabControl;
 *     TYPE=DOJOToolbar;
 *     TYPE=SAPTabControl;
 *     TYPE=SAPToolbar;
 *     (see [doclink here] for supported domain classes)
 *
 *     (note: type qualifier also includes domain information, like "DOJO", "SAP")
 *     (note: type qualifier and attribute identifiers separated by semi-colons)
 *     </pre></ul>
 *<p>Identify unique element attribute identifiers like:
 *     <p><ul><pre>
 *     ID=id;
 *     CLASS=class;
 *     NAME=name;
 *     TEXT=text;
 *     TITLE=title;
 *     LINK=linkInfo;
 *     PARTIALLINK=partialLinkInfo;
 *     TAG=tagname;
 *
 *     (note: multiple attribute identifiers separated by semi-colons ;)
 *     </pre></ul>
 *<p>Identify element by multiple native attributes:
 *     <p><ul><pre>
 *     property1=xxx;property2=yyy;property3=zzz;
 *
 *     </pre></ul>
 *<p>Identify explicit XPATH or CSS recognition:
 *     <p><ul><pre>
 *     XPATH=explicit xpath
 *     CSS=explicit css path
 *     </pre></ul>
 * <p>Identify any supported qualifier, if desired:
 *     <p><ul><pre>
 *     INDEX=n;                             (1-based, NOT used alone, indicates the Nth matched)
 *     ITEMINDEX=n;                         (1-based, NOT used alone, indicates the Nth item in a List, or ComboBox etc.)
 *     PATH=parent->child->grandchild;      (NOT used alone, indicates the sub-item in a List, ComboBox, Menu or Tree etc.)
 *     PROPERTY=prop:value;                 (can be used with others to enforce search condition)
 *     PROPERTYCONTAINS=prop:partialValue;  (can be used with others to enforce search condition)
 *
 *     (note: multiple qualifiers separated by semi-colons ;)
 *     (note: these "ID", "TEXT", "ITEMINDEX", "PATH" will normally be considered as qualifier, but if there is a
 *            prefix :PASM: infront, they will be considered as component's native properties too.
 *            Example:
 *            For "TEXT=myList;componentProperty=xxx;ITEMINDEX=n;"
 *            "TEXT" and "ITEMINDEX" are qualifiers, while "componentProperty" is native property
 *            For ":PASM:TEXT=myList;componentProperty=xxx;ITEMINDEX=n;"
 *            "TEXT", "ITEMINDEX" and "componentProperty" are all native properties
 *      )
 *     </pre></ul>
 * </ul>
 * <p>
 */
public class SearchObject {

    private static Hashtable<String, WebDriver> webDrivers = new Hashtable<String, WebDriver>();
    private static Vector<String> webDriverStack = new Vector<String>();

    /** "value","text","placeholder". An array of attribute used as component's text content.<br>
     * @see #getValue(WebElement, String...)
     */
    public static final String[] TEXT_VALUE_ATTRIBUTES = { "value", "text", "placeholder" };

    /**
     * The Selenium 'WebDriver' currently used to manipulate the browser.
     */
    protected static WebDriver lastUsedWD;
    /**
     * The Selenium 'JavascriptExecutor' currently used to execute javascript on the browser.
     */
    protected static JavascriptExecutor lastJS;

    /**
     * The frame object where the current GUI-component locate. If there is no frame, its value should be null.
     * @see #getObject(SearchContext, String)
     */
    protected static FrameElement lastFrame = null;

    protected static class FrameElement {
        FrameElement parentFrame;
        /**
         * If you want to operate on this webelement, you MUST swith to its parent's frame
         * <pre>
         * {@code
         *  WDLibrary.getWebDriver().switchTo().defaultContent();
         *  WDLibrary.getWebDriver().switchTo().frame(parentFrame.getWebElement().getAttribute('id'));
         * }
         * </pre>
         */
        WebElement webElement;
        Dimension size;
        Point location;

        FrameElement(FrameElement parentFrame, WebElement frame) throws SeleniumPlusException {
            try {
                if (parentFrame != null) {
                    this.parentFrame = parentFrame;
                    location = frame.getLocation().moveBy(parentFrame.getLocation().x, parentFrame.getLocation().y);
                } else {
                    location = frame.getLocation();
                }
                size = frame.getSize();
                this.webElement = frame;
            } catch (Exception e) {
                throw new SeleniumPlusException("Met Exception " + StringUtils.debugmsg(e));
            }
        }

        public Dimension getSize() {
            return size;
        }

        public Point getLocation() {
            return location;
        }

        public FrameElement getParentFrame() {
            return parentFrame;
        }

        public WebElement getWebElement() {
            return webElement;
        }
    }

    /**
     * The frame object where the current GUI-component locate. If there is no frame, its value should be null.
     * @see #getObject(SearchContext, String)
     */
    public static FrameElement getLastFrame() {
        return lastFrame;
    }

    /**
     * <b style="color:red">Note: User should be very careful when calling this method,
     * it will change the whole searching context!!!</b><br>
     * Set the frame object where the current GUI-component locate.
     * If a null value is set, then there is no frame to switch and
     * object will be searched in default document (webdriver.switchTo().defaultContent()).
     * @see #getObject(SearchContext, String)
     */
    public static void setLastFrame(FrameElement lastFrame) {
        SearchObject.lastFrame = lastFrame;
    }

    /**FRAMEINDEX, it should be placed in front of normal tokens like ID, CLASS, NAME etc. if exist.
     *  NOT recommended to use. Use FRAMEID OR FRAMENAME Instead.*/
    public static final String SEARCH_CRITERIA_FRAMEINDEX = "FRAMEINDEX";
    /**FRAMEID, it should be placed in front of normal tokens like ID, CLASS, NAME etc. if exist.*/
    public static final String SEARCH_CRITERIA_FRAMEID = "FRAMEID";
    /**IFRAMEID, it should be placed in front of normal tokens like ID, CLASS, NAME etc. if exist.*/
    public static final String SEARCH_CRITERIA_IFRAMEID = "IFRAMEID";
    /**FRAMENAME, it should be placed in front of normal tokens like ID, CLASS, NAME etc. if exist.*/
    public static final String SEARCH_CRITERIA_FRAMENAME = "FRAMENAME";
    /**FRAMEXPATH, it should be placed in front of normal tokens like ID, CLASS, NAME etc. if exist.*/
    public static final String SEARCH_CRITERIA_FRAMEXPATH = "FRAMEXPATH";
    /**'frame' tag name*/
    public static final String TAG_FRAME = "frame";
    /**'iframe' tag name*/
    public static final String TAG_IFRAME = "iframe";

    //   /**DOMAIN*/
    //   public static final String SEARCH_CRITERIA_DOMAIN               = GuiObjectRecognition.CATEGORY_DOMAIN.toUpperCase();
    /**TYPE*/
    public static final String SEARCH_CRITERIA_TYPE = GuiObjectRecognition.CATEGORY_TYPE.toUpperCase();
    /**ID*/
    public static final String SEARCH_CRITERIA_ID = GuiObjectRecognition.QUALIFIER_ID.toUpperCase();
    /**NAME*/
    public static final String SEARCH_CRITERIA_NAME = GuiObjectRecognition.QUALIFIER_NAME.toUpperCase();
    /**TEXT*/
    public static final String SEARCH_CRITERIA_TEXT = GuiObjectRecognition.QUALIFIER_TEXT.toUpperCase();
    /**PATH*/
    public static final String SEARCH_CRITERIA_PATH = GuiObjectRecognition.QUALIFIER_PATH.toUpperCase();
    /**PROPERTY*/
    public static final String SEARCH_CRITERIA_PROPERTY = GuiObjectRecognition.QUALIFIER_PROPERTY.toUpperCase();
    /**PROPERTYCONTAINS*/
    public static final String SEARCH_CRITERIA_PROPERTY_CONTAINS = GuiObjectRecognition.QUALIFIER_PROPERTY_CONTAINS
            .toUpperCase();
    /**INDEX*/
    public static final String SEARCH_CRITERIA_INDEX = GuiObjectRecognition.QUALIFIER_INDEX.toUpperCase();
    /**CLASSCONTAINS*/
    public static final String SEARCH_CRITERIA_CLASS_CONTAINS = GuiObjectRecognition.QUALIFIER_CLASS_CONTAINS
            .toUpperCase();
    /**ITEMINDEX*/
    public static final String SEARCH_CRITERIA_ITEMINDEX = "ITEMINDEX";
    /**CLASS*/
    public static final String SEARCH_CRITERIA_CLASS = "CLASS";
    /**TITLE*/
    public static final String SEARCH_CRITERIA_TITLE = "TITLE";
    /**XPATH*/
    public static final String SEARCH_CRITERIA_XPATH = "XPATH";
    /**CSS*/
    public static final String SEARCH_CRITERIA_CSS = "CSS";
    /**LINK*/
    public static final String SEARCH_CRITERIA_LINK = "LINK";
    /**PARTIALLINK*/
    public static final String SEARCH_CRITERIA_PARTIALLINK = "PARTIALLINK";
    /**TAG*/
    public static final String SEARCH_CRITERIA_TAG = "TAG";

    /** "CONTAINS" */
    public static final String SEARCH_CRITERIA_CONTAINS_SUFFIX = "CONTAINS";

    /**DOJO*/
    public static final String DOMAIN_DOJO = "DOJO";
    public static final String DOMAIN_SAP = "SAP";
    /**HTML*/
    public static final String DOMAIN_HTML = "HTML";

    public static final int INVALID_INDEX = TextMatchingCriterion.INVALID_INDEX;

    /** separates parent/child relationships in recognition strings. (Default: "/" ) */
    public static final String XPATH_CHILD_SEPARATOR = "/";

    /** separates parent/child relationships in recognition strings. (Default: ";\;" ) */
    public static String childSeparator = GuiObjectVector.DEFAULT_CHILD_SEPARATOR;
    /** separates multiples qualifiers. (Default: ";" )*/
    public static String qulifierSeparator = GuiObjectRecognition.DEFAULT_QUALIFIER_SEPARATOR;
    /** separates type and value for one qualifier. (Default: "=" )*/
    public static String assignSeparator = GuiObjectRecognition.DEFAULT_ASSIGN_SEPARATOR;
    /** separates property-name and property-value. (Default: ":" )*/
    public static String propertySeparator = GuiObjectRecognition.DEFAULT_PROPERTY_QUALIFIER_SEPARATOR;
    /** the character to escape separators. (Default: '\\' ) */
    public static Character escapeChar = StringUtils.REGEX_ESCAPE_CHARACTER;

    /** non-ui tags to be ignored in xpath searches and xpath generation.
     * Like: br, head, title, meta, link, script, wbr */
    public static final String[] IGNORED_NONUI_TAGS = new String[] { "head", "title", "meta", "link", "script",
            "br", "wbr" };

    /**The last visited URL,*/
    protected static String lastVisitedURL = null;

    public static String getLastVisitedURL() {
        return lastVisitedURL;
    }

    /**The last visited browser window*/
    protected static BrowserWindow lastBrowserWindow = null;

    public static BrowserWindow getLastBrowserWindow() {
        return lastBrowserWindow;
    }

    public static class BrowserWindow {
        /**'height'*/
        public static final String PROPERTY_HEIGHT = "height";
        /**'width'*/
        public static final String PROPERTY_WIDTH = "width";
        /**'x'*/
        public static final String PROPERTY_LOCATION_X = "x";//relative to screen
        /**'y'*/
        public static final String PROPERTY_LOCATION_Y = "y";//relative to screen
        /**'clientHeight'*/
        public static final String PROPERTY_CLIENT_HEIGHT = "clientHeight";//The height between 'header(menubar, tabbar, toolbar)' and 'status bar'
        /**'clientWidth'*/
        public static final String PROPERTY_CLIENT_WIDTH = "clientWidth";//
        /**'clientX'*/
        public static final String PROPERTY_CLIENT_LOCATION_X = "clientX";//relative to window
        /**'clientY'*/
        public static final String PROPERTY_CLIENT_LOCATION_Y = "clientY";//relative to window
        /**'pageXOffset'*/
        public static final String PROPERTY_PAGE_X_OFFSET = "pageXOffset";//Scroll bar x offset
        /**'pageYOffset'*/
        public static final String PROPERTY_PAGE_Y_OFFSET = "pageYOffset";//Scroll bar y offset
        /**'borderWidth'*/
        public static final String PROPERTY_BORDER_WIDTH = "borderWidth";//The width of the browser's border
        /**'headerHeight'*/
        public static final String PROPERTY_HEADER_HEIGHT = "headerHeight";//The height of header(menubar, tabbar, toolbar)
        /**'maximized'*/
        public static final String PROPERTY_MAXIMIZED = "maximized";//If the browser is maximized
        /**'DriverWindow'*/
        public static final String PROPERTY_DRIVER_WINDOW = "DriverWindow";//The WebDriver manage()ed Window object, if present.

        /* the wrapped object, represents a browser window object.
         * This object SHOULD be a Map returned by a javascript function*/
        private Object object = null;
        /* convenient reference to wrapped object*/
        private Map map = null;

        private Window window = null;

        private float height;
        private float width;
        private float x;
        private float y;
        private float clientHeight;
        private float clientWidth;
        private float clientX;
        private float clientY;
        private float pageXOffset;
        private float pageYOffset;
        private float borderWidth;
        private float headerHeight;
        private boolean maximized;

        public void set(Map browserInfoMap) {
            map = (Map) browserInfoMap;
            Object rc = null;
            IndependantLog.info("BrowserWindow set map processing: " + browserInfoMap.getClass().getName());

            try {
                window = (Window) map.get(PROPERTY_DRIVER_WINDOW);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set map ignoring set Window: " + ex.getClass().getName() + ": "
                        + ex.getMessage());
            }
            try {
                height = StringUtilities.getFloat(map, PROPERTY_HEIGHT);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: height not set in Map...attempting fix.");
                if (window != null) {
                    height = window.getSize().height;
                    map.put(PROPERTY_HEIGHT, new Float(height));
                    IndependantLog.info("BrowserWindow set: height set to " + height);
                } else {
                    IndependantLog.info("BrowserWindow set: height was NOT fixed.");
                }
                //consider document.parentWindow.screen.height
            }
            try {
                width = StringUtilities.getFloat(map, PROPERTY_WIDTH);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: width not set in Map...attempting fix.");
                if (window != null) {
                    width = window.getSize().width;
                    map.put(PROPERTY_WIDTH, new Float(width));
                    IndependantLog.info("BrowserWindow set: width set to " + width);
                } else {
                    IndependantLog.info("BrowserWindow set: width was NOT fixed.");
                }
                //consider document.parentWindow.screen.width
            }
            try {
                x = StringUtilities.getFloat(map, PROPERTY_LOCATION_X);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: x not set in Map...attempting fix.");
                if (window != null) {
                    x = window.getPosition().x;
                    map.put(PROPERTY_LOCATION_X, new Float(x));
                    IndependantLog.info("BrowserWindow set: x set to " + x);
                } else {
                    IndependantLog.info("BrowserWindow set: x was NOT fixed.");
                }
                // consider document.parentWindow.screenLeft
            }
            try {
                y = StringUtilities.getFloat(map, PROPERTY_LOCATION_Y);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: y not set in Map...attempting fix.");
                if (window != null) {
                    y = window.getPosition().y;
                    map.put(PROPERTY_LOCATION_Y, new Float(y));
                    IndependantLog.info("BrowserWindow set: y set to " + y);
                } else {
                    IndependantLog.info("BrowserWindow set: y was NOT fixed.");
                }
            }
            try {
                clientX = StringUtilities.getFloat(map, PROPERTY_CLIENT_LOCATION_X);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: clientX not set in Map...attempting fix.");
                // consider document.parentWindow.screenLeft
                try {
                    rc = null;
                    rc = WDLibrary.executeScript("return document.parentWindow.screenLeft;");
                    if (rc instanceof Number) {
                        clientX = x - ((Number) rc).floatValue();
                        map.put(PROPERTY_CLIENT_LOCATION_X, new Float(clientX));
                        IndependantLog.info("BrowserWindow set: clientX set to " + clientX);
                    } else {
                        IndependantLog.info("BrowserWindow set: clientX was NOT fixed.");
                    }
                } catch (Exception ex2) {
                    IndependantLog.info("BrowserWindow setMap clientX ignoring " + ex2.getClass().getName() + ", "
                            + ex2.getMessage());
                    // DOJO alternatives?
                }
            }
            try {
                clientY = StringUtilities.getFloat(map, PROPERTY_CLIENT_LOCATION_Y);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: clientY not set in Map...attempting fix...");
                // consider document.parentWindow.screenTop
                try {
                    rc = null;
                    rc = WDLibrary.executeScript("return document.parentWindow.screenTop;");
                    if (rc instanceof Number) {
                        clientY = y - ((Number) rc).floatValue();
                        map.put(PROPERTY_CLIENT_LOCATION_Y, new Float(clientY));
                        IndependantLog.info("BrowserWindow set: clientY set to " + clientY);
                    } else {
                        IndependantLog.info("BrowserWindow set: clientY was NOT fixed.");
                    }
                } catch (Exception ex2) {
                    IndependantLog.info("BrowserWindow setMap clientY ignoring " + ex2.getClass().getName() + ", "
                            + ex2.getMessage());
                    // DOJO alternatives?
                }
            }
            try {
                borderWidth = StringUtilities.getFloat(map, PROPERTY_BORDER_WIDTH);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: borderWidth not set in Map...attempting fix.");
                // try to get it from CSS settings?
                // clientX - x ?
                // height - clientHeight - headerHeight ?
                borderWidth = clientX - x;
                map.put(PROPERTY_BORDER_WIDTH, new Float(borderWidth));
                IndependantLog.info("BrowserWindow set: borderWidth set to " + borderWidth);
            }
            if (borderWidth < 0) {
                IndependantLog.info("BrowserWindow set: invalid borderWidth set in Map...attempting fix...");
                borderWidth = 0;
                map.put(PROPERTY_BORDER_WIDTH, new Float(borderWidth));
                IndependantLog.info("BrowserWindow set: borderWidth nomralized to " + borderWidth);
                // try to get it from CSS settings?
                // height - clientHeight - headerHeight
            }
            try {
                headerHeight = StringUtilities.getFloat(map, PROPERTY_HEADER_HEIGHT);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: headerHeight not set in Map...attempting fix...");
                headerHeight = clientY - y;
                // height - clientHeight - borderWidth ?
                map.put(PROPERTY_HEADER_HEIGHT, new Float(headerHeight));
                IndependantLog.info("BrowserWindow set: headerHeight to " + headerHeight);
            }
            try {
                clientWidth = StringUtilities.getFloat(map, PROPERTY_CLIENT_WIDTH);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: clientWidth not set in Map...attempting fix...");
                // width - (borderWidth*2) ?
                clientWidth = width - (borderWidth * 2);
                map.put(PROPERTY_CLIENT_WIDTH, new Float(clientWidth));
                IndependantLog.info("BrowserWindow set: clientWidth set to " + clientWidth);
            }
            try {
                clientHeight = StringUtilities.getFloat(map, PROPERTY_CLIENT_HEIGHT);
            } catch (Exception ex) {
                IndependantLog.info("BrowserWindow set: clientHeight not set in Map...attempting fix.");
                // height - headerHeight - borderWidth ?
                clientHeight = height - headerHeight - borderWidth; //can be wrong if bottom border != side borders
                map.put(PROPERTY_CLIENT_HEIGHT, new Float(clientHeight));
                IndependantLog.info("BrowserWindow set: clientHeight set to " + clientHeight);
            }
            try {
                pageXOffset = StringUtilities.getFloat(map, PROPERTY_PAGE_X_OFFSET);
            } catch (SAFSException ex) {
            }
            try {
                pageYOffset = StringUtilities.getFloat(map, PROPERTY_PAGE_Y_OFFSET);
            } catch (SAFSException ex) {
            }
            try {
                maximized = StringUtilities.getBoolean(map, PROPERTY_MAXIMIZED);
            } catch (SAFSException ex) {

            }
        }

        /**
         * The embedded Mapped object does or does NOT contain a real value for a particular Property?
         * @param propertyName
         * @return true or false;
         */
        protected boolean hasProperty(String propertyName) {
            if (map == null || map.isEmpty())
                return false;
            if (map.containsKey(propertyName)) {
                return (map.get(propertyName) != null);
            }
            return false;
        }

        public BrowserWindow() {
        }

        /**
         * @param object Object containing information about the browser window.
         * @see #getWindowObjectByJS(Window)
         */
        public BrowserWindow(Object object) throws SeleniumPlusException {
            String debugmsg = StringUtils.debugmsg(BrowserWindow.class, "BrowserWindow");
            this.object = object;

            if (object instanceof Map) {
                map = (Map<?, ?>) object;
                set(map);

            } else if (object != null) {
                String errmsg = " DOM window Object is NOT the expected Map type, it is '"
                        + object.getClass().getName() + "'";
                IndependantLog.error(debugmsg + errmsg);
                throw new SeleniumPlusException(errmsg);
            } else {
                String errmsg = " The emebedded DOM window Object is null, cannot handle it.";
                IndependantLog.error(debugmsg + errmsg);
                throw new SeleniumPlusException(errmsg);
            }
        }

        public float getHeight() {
            return height;
        }

        public float getWidth() {
            return width;
        }

        public float getX() {
            return x;
        }

        public float getY() {
            return y;
        }

        public float getClientHeight() {
            return clientHeight;
        }

        public float getClientWidth() {
            return clientWidth;
        }

        public float getClientX() {
            return clientX;
        }

        public float getClientY() {
            return clientY;
        }

        public float getPageXOffset() {
            return pageXOffset;
        }

        public float getPageYOffset() {
            return pageYOffset;
        }

        public float getBorderWidth() {
            return borderWidth;
        }

        public float getheaderHeight() {
            return headerHeight;
        }

        public boolean isMaximized() {
            return maximized;
        }

        /**
         * @param window Selenium WebDriver Window object
         * @return A browser window object returned by javascript,
         *         containing properties like height, clientWidth, pageXOffset etc.<br>
         *         null, if some exception happens.
         *         PROPERTY_DRIVER_WINDOW=Window object if we can store it.
         */
        public static Object getWindowObjectByJS(Window window) {
            String debugmsg = StringUtils.debugmsg(BrowserWindow.class, "getWindowObjectByJS");
            Hashtable<String, Object> windowProperties = new Hashtable<String, Object>();
            try {
                //getSize() is not supported by "chromedriver" for "android chrome"
                Dimension d = window.getSize();
                windowProperties.put(PROPERTY_HEIGHT, d.height);
                windowProperties.put(PROPERTY_WIDTH, d.width);
            } catch (Throwable th) {
                IndependantLog.warn(debugmsg + getThrowableMessages(th));
            }
            try {
                //getPosition() is not supported by "chromedriver" for "android chrome"
                Point p = window.getPosition();
                windowProperties.put(PROPERTY_LOCATION_X, p.x);
                windowProperties.put(PROPERTY_LOCATION_Y, p.y);
            } catch (Throwable th) {
                IndependantLog.warn(debugmsg + getThrowableMessages(th));
            }

            try {
                StringBuffer jsScript = new StringBuffer();
                jsScript.append(JavaScriptFunctions.defineObject(windowProperties));
                jsScript.append(JavaScriptFunctions.getBrowserInformation());
                jsScript.append("return getBrowserInformation(defineObject());");

                Object result = WDLibrary.executeScript(jsScript.toString());
                if (result instanceof Map) {
                    try {
                        ((Map) result).put(PROPERTY_DRIVER_WINDOW, window);
                    } catch (UnsupportedOperationException uo) {
                        Hashtable newmap = new Hashtable((Map) result);
                        newmap.put(PROPERTY_DRIVER_WINDOW, window);
                        return newmap;
                    }
                }
                return result;
            } catch (Throwable th) {
                IndependantLog.error(debugmsg + getThrowableMessages(th));
            }
            return null;
        }
    }

    /**
     * Currently, shutdown will "quit()" every webdriver that is stored here.
     * Ideally, we do NOT want to quit instances that are running attached to a
     * Selenium Server--whether it is remote or local.
     * @throws Throwable
     */
    public synchronized static void shutdown() throws Throwable {
        lastUsedWD = null;
        WebDriver wd = null;
        String id = null;
        Enumeration<String> keys = webDrivers.keys();
        while (keys.hasMoreElements()) {
            id = keys.nextElement();
            wd = removeWebDriver(id);
            wd.quit();
        }
        webDrivers.clear();
        webDriverStack.clear();
        RemoteDriver.deleteSessionFile();
    }

    /**
     * @param tag
     * @return true if the provided tag is in the list of nonui tags to be ignored for xpath
     * @see #IGNORED_NONUI_TAGS
     */
    public static boolean isIgnoredNonUITag(String tag) {
        try {
            for (String s : IGNORED_NONUI_TAGS)
                if (tag.equalsIgnoreCase(s))
                    return true;
        } catch (NullPointerException ignore) {
        }
        return false;
    }

    /**
     * Store the WebDriver in the Hashtable by id.<br>
     * This also sets this WebDriver as the "current" or "lastUsed" WebDriver.
     * If there exists already a WebDriver in the Hashtable with the same id, it will be replaced by the new one,<br>
     * and the old one will be returned. User will decide how to handle the old one, either store it with an other id<br>
     * or simply stop it by calling {@link WebDriver#quit()}<br>
     * <br>
     * @param id String, the key for the WebDriver to store in the Hashtable.<br>
     *                   if user provides null, then the hashcode of WebDriver will be used as key.<br>
     * @param wd WebDriver, the WebDriver to store in the Hashtable.
     * @return WebDriver, the previous WebDriver stored in the Hashtable with the same id; null if no such WebDriver exists.
     */
    protected synchronized static WebDriver addWebDriver(String id, WebDriver wd) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "addWebDriver");
        WebDriver previous = null;

        if (id == null || id.equals("")) {
            id = String.valueOf(wd.hashCode());
        }
        if (webDrivers.containsKey(id)) {
            IndependantLog.warn(debugmsg + "You are going to override the old WebDriver with id '" + id + "'");
            previous = webDrivers.get(id);
        }
        IndependantLog.debug(debugmsg + "You are adding WebDriver with id '" + id + "'");

        org.safs.selenium.webdriver.CFComponent.clearInternalCache();

        webDrivers.put(id, wd);
        lastUsedWD = wd;
        webDriverStack.remove(id);
        webDriverStack.add(id); // push it to be the last always
        try {
            RemoteDriver.setLastSessionId(id);
        } catch (Exception x) {
            IndependantLog.error(debugmsg + "RemoteDriver error during setLastSessionId().");
        }
        return previous;
    }

    /**
     * Switch to browser indicated by ID. It will get the cached webdriver according to the browser's id <br>
     * and set it to the {@link SearchObject#lastUsedWD} and set the {@link SearchObject#lastJS} if possible.<br>
     * @param ID   String, the id to identify the browser
     * @throws SeleniumPlusException
     */
    public static void useBrowser(String ID) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "useBrowser");
        WebDriver webdriver = getWebDriver(ID);

        if (webdriver == null) {
            throw new SeleniumPlusException(debugmsg + "cannot get webdriver according to id '" + ID + "'",
                    SeleniumPlusException.CODE_OBJECT_IS_NULL);
        }
        try {
            RemoteDriver.setLastSessionId(ID);
        } catch (Exception e) {
        }

        org.safs.selenium.webdriver.CFComponent.clearInternalCache();

        webDriverStack.remove(ID);
        webDriverStack.add(ID); // make it the "lastUsed"
        if (lastUsedWD != webdriver) {
            lastUsedWD = webdriver;
            try {
                refreshJSExecutor();
            } catch (SeleniumPlusException ignored) {
            }
        }
    }

    /**
     * At this time does NOT set the retrieved WebDriver to be the "current" or "lastUsed" WebDriver.
     * @param id String, the ID for the WebDriver stored in the Hashtable.<br>
     *                   if user provides null, then the last used WebDriver will be returned.<br>
     * @return WebDriver, the WebDriver associated with id; null if no WebDriver is found.
     */
    protected synchronized static WebDriver getWebDriver(String id) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getWebDriver");
        WebDriver temp = null;

        if (id != null)
            temp = (WebDriver) webDrivers.get(id);
        else
            temp = lastUsedWD;

        if (temp == null)
            IndependantLog.warn(debugmsg + "cannot get WebDriver for id '" + id + "'");

        return temp;
    }

    /**
     * @param title String, the case-insensitive title within the WebDriver stored in the Hashtable.<br>
     *                   if user provides null or a zero-length title then the last used WebDriver will be returned.<br>
     *                   the routine does not (yet) set the found WebDriver to be the lastUsedWD.  That might change, though.
     * @return WebDriver, the WebDriver associated with the browser title; null if no matching WebDriver is found.
     */
    protected synchronized static WebDriver getWebDriverWithTitle(String title) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getWebDriverWithTitle");
        WebDriver temp = null;
        WebDriver match = null;
        String lctitle = null;
        if (title != null && title.length() > 0) {
            lctitle = title.toLowerCase().trim();
            Enumeration e = webDrivers.elements();
            while (e.hasMoreElements()) {
                temp = (WebDriver) e.nextElement();
                try {
                    if (lctitle.equals(temp.getTitle().toLowerCase().trim())) {
                        match = temp;
                        break;
                    }
                } catch (NullPointerException notitle) {
                }
            }
        } else
            match = lastUsedWD;

        if (match == null)
            IndependantLog.warn(debugmsg + "cannot get WebDriver with title '" + title + "'");

        return match;
    }

    /**
     * At this time does NOT set the processed WebDriver to be the "current" or "lastUsed" WebDriver.
     * @param wd WebDriver to get our stored ID of.
     * @return String ID of the WebDriver, or null if not found.
     */
    public synchronized static String getIDForWebDriver(WebDriver wd) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getIDForWebDriver");
        WebDriver temp = null;
        String id = null;
        String match = null;
        if (wd == null)
            return null;
        Enumeration e = webDrivers.keys();
        while (e.hasMoreElements()) {
            id = (String) e.nextElement();
            temp = webDrivers.get(id);
            if (temp.equals(wd)) {
                match = id;
                break;
            }
        }
        if (match == null)
            IndependantLog.warn(debugmsg + "cannot get ID for the WebDriver instance provided.");

        return match;
    }

    /**
     * @return String[] of all known WebDriver window titles, or an empty array.
     */
    public synchronized static String[] getAllWindowTitles() {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getAllWindowTitles");
        WebDriver temp = null;
        if (webDrivers.isEmpty()) {
            getWebDriver();
            if (webDrivers.isEmpty())
                return new String[0];
        }
        ArrayList<String> titles = new ArrayList();
        Enumeration list = webDrivers.elements();
        String browser = null;
        for (int i = 0; i < webDrivers.size(); i++) {
            temp = (WebDriver) list.nextElement();
            try {
                titles.add(temp.getTitle());
            } catch (Exception ignore) {
            }
        }
        IndependantLog.info(debugmsg + "retrieved " + titles.size() + " window titles.");
        return titles.toArray(new String[0]);
    }

    /**
     * Will only connect to the "current" session.  Normally only called after a catastrophic failure of a RemoteDriver.
     * @return
     */
    protected static WebDriver reconnectLastWebDriver() {
        String debugmsg = StringUtils.debugmsg(false);
        String host = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_HOST);
        if (host == null || host.isEmpty())
            host = SelectBrowser.DEFAULT_SELENIUM_HOST;
        String port = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_PORT);
        if (port == null || port.isEmpty())
            port = SelectBrowser.DEFAULT_SELENIUM_PORT;

        List<SessionInfo> list = null;
        RemoteDriver result = null;
        try {
            list = RemoteDriver.getSessionsFromFile();
            if (list == null || list.isEmpty()) {
                IndependantLog.info(debugmsg + " did not find session info to retrieve.");
                return null;
            }
        } catch (Exception e) {
            IndependantLog.debug(debugmsg + " did not retrieve session info successfully.", e);
            return null;
        }
        IndependantLog.info(debugmsg + " found " + list.size() + " stored sessions to process.");
        for (SessionInfo info : list) {
            if (info.isCurrentSession) {
                DesiredCapabilities capabilities = SelectBrowser.getDesiredCapabilities(info.browser,
                        info.extraParameters);
                capabilities.setJavascriptEnabled(true);
                capabilities.setCapability(CapabilityType.TAKES_SCREENSHOT, true);
                capabilities.setCapability(RemoteDriver.CAPABILITY_ID, info.id);
                capabilities.setCapability(RemoteDriver.CAPABILITY_RECONNECT, true);
                capabilities.setCapability(RemoteDriver.CAPABILITY_REMOTESERVER, host);
                capabilities.setBrowserName(info.browser);
                try {
                    // try to see if it is a valid session
                    IndependantLog.debug(debugmsg + " trying to connect remote server at 'http://" + host + ":"
                            + port + "/wd/hub' ... ");
                    result = new RemoteDriver(new URL("http://" + host + ":" + port + "/wd/hub"), capabilities);
                    // NOT changing anything about the existing sessions
                    //d.manage().window().setSize(d.manage().window().getSize());
                    addWebDriver(info.id, result);
                } catch (Throwable t) {
                    IndependantLog.warn(debugmsg + "ignoring reconnect RemoteDriver " + t.getClass().getSimpleName()
                            + ", " + t.getMessage()
                            + ". RemoteServer may not be running or DOM may be in transition!");
                }
            }
        }
        return lastUsedWD;
    }

    /**
     * Get the last used "current" WebDriver.
     * If we do not have a last used WebDriver, we will attempt to process the remote session file
     * to reconnect existing webdriver sessions and return the one that was stored as the last
     * used session.
     * @return WebDriver, the lastUsed WebDriver or null if no WebDriver was found.
     */
    public synchronized static WebDriver getWebDriver() {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getWebDriver");

        if (lastUsedWD == null) {
            String host = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_HOST);
            if (host == null || host.isEmpty())
                host = SelectBrowser.DEFAULT_SELENIUM_HOST;
            String port = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_PORT);
            if (port == null || port.isEmpty())
                port = SelectBrowser.DEFAULT_SELENIUM_PORT;

            List<SessionInfo> list = null;
            try {
                list = RemoteDriver.getSessionsFromFile();
                if (list == null || list.isEmpty()) {
                    IndependantLog.info(debugmsg + " did not find session info to retrieve.");
                    return null;
                }
            } catch (Exception e) {
                IndependantLog.debug(debugmsg + " did not retrieve session info successfully.", e);
                return null;
            }
            IndependantLog.info(debugmsg + " found " + list.size() + " stored sessions to process.");
            RemoteDriver lastdriver = null;
            String lastid = null;
            for (SessionInfo info : list) {
                DesiredCapabilities capabilities = SelectBrowser.getDesiredCapabilities(info.browser,
                        info.extraParameters);
                capabilities.setJavascriptEnabled(true);
                capabilities.setCapability(CapabilityType.TAKES_SCREENSHOT, true);
                capabilities.setCapability(RemoteDriver.CAPABILITY_ID, info.id);
                capabilities.setCapability(RemoteDriver.CAPABILITY_RECONNECT, true);
                capabilities.setBrowserName(info.browser);
                capabilities.setCapability(RemoteDriver.CAPABILITY_REMOTESERVER, host);
                try {
                    // try to see if it is a valid session
                    RemoteDriver d = new RemoteDriver(new URL("http://" + host + ":" + port + "/wd/hub"),
                            capabilities);
                    d.manage().window().setSize(d.manage().window().getSize());
                    addWebDriver(info.id, d); // overwrites lastUsedWD every time!
                    if (info.isCurrentSession) {
                        lastdriver = d;
                        lastid = info.id;
                    }
                } catch (Throwable t) {
                    IndependantLog.warn(debugmsg
                            + "ignoring reconnect RemoteDriver failure.  RemoteServer may not be running!");
                    try {
                        IndependantLog
                                .info(debugmsg + " attempting to remove remote session info for id: " + info.id);
                        RemoteDriver.deleteSessionIdFromFile(info.id);
                    } catch (Throwable ignore) {
                    }
                }
            }
            if (lastdriver != null) {
                addWebDriver(lastid, lastdriver);
            }
            if (lastUsedWD == null) {
                IndependantLog.error(debugmsg + "session can't reconnected");
                return null;
            }
        }
        if (lastUsedWD instanceof RemoteDriver && ((RemoteDriver) lastUsedWD).hasQuit()) {
            IndependantLog.error(debugmsg + "detecting lastUsedWD has quit. Resetting.");
            lastUsedWD = null;
        }
        return lastUsedWD;
    }

    /**
     * Removes the provided webdriver from internal storage and deletes the session info
     * in the RemoteDriver.  Will pop off the next "lastUsed" WebDriver off the stack and
     * set it as the new "lastUsed" WebDriver and sets the RemoteDriver "lastSessionId"
     * as needed.
     * @param id String, the ID for the WebDriver stored in the Hashtable.<br>
     *                   if user provides null, then the last used WebDriver will be removed.<br>
     * @return   WebDriver, the WebDriver has been removed; null if no WebDriver is removed.
     */
    protected synchronized static WebDriver removeWebDriver(String id) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "removeWebDriver");
        WebDriver obj = null;
        try {
            org.safs.selenium.webdriver.CFComponent.clearInternalCache();

            if (id != null)
                obj = (WebDriver) webDrivers.remove(id);
            else {
                if (lastUsedWD != null) {
                    Enumeration<String> keys = webDrivers.keys();
                    String key = null;
                    while (keys.hasMoreElements()) {
                        key = keys.nextElement();
                        if (lastUsedWD.equals(webDrivers.get(key))) {
                            obj = lastUsedWD;
                            id = getIDForWebDriver(obj);
                            webDrivers.remove(key);
                            break;
                        }
                    }
                }
            }
            if (id != null) {
                webDriverStack.remove(id);
                RemoteDriver.deleteSessionIdFromFile(id);
                if (!webDriverStack.isEmpty()) {
                    String newid = webDriverStack.lastElement();
                    useBrowser(newid);
                }
            }
        } catch (Throwable x) {
            IndependantLog.error(debugmsg + "cannot remove WebDriver for id '" + id + "'", x);
        }
        return obj;
    }

    public static WebElement findElement(By by) {

        return lastUsedWD.findElement(by);
    }

    /**
     * Get the object based on the recognition string.
     * Currently, the routine assumes the recognition string is for an item in
     * the lastUsedWD--the last used WebDriver.
     * This routine will be modified to detect a recognition string that may include
     * the need to locate which WebDriver is the right one and not assume the last one
     * is the right one.
     * @param RS
     * @return
     * @see #getObject(SearchContext, String)
     */
    public static WebElement getObject(String RS) {
        return getObject(getWebDriver(), RS);
    }

    /**
     * @param rs
     * @return true if the RS contains a qualifier of "TAG=value" (case-insensitive comparison)
     */
    public static boolean containTagRS(String rs) {
        if (rs == null)
            return false;
        String uprs = rs.toUpperCase().replaceAll(" ", "");
        if (uprs.startsWith(SEARCH_CRITERIA_TAG + assignSeparator)
                || uprs.contains(qulifierSeparator + SEARCH_CRITERIA_TAG + assignSeparator)) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean containFrameRS(String rs) {
        if (rs == null)
            return false;
        String uprs = rs.toUpperCase();
        if (uprs.contains(SEARCH_CRITERIA_FRAMEID) || uprs.contains(SEARCH_CRITERIA_FRAMENAME)
                || uprs.contains(SEARCH_CRITERIA_FRAMEINDEX) || uprs.contains(SEARCH_CRITERIA_FRAMEXPATH)) {
            return true;
        } else {
            return false;
        }
    }

    //TODO
    public static boolean isValidFrameRS(String frameRS) {
        if (frameRS == null)
            return false;
        String uprs = frameRS.toUpperCase();
        if (uprs.contains(SEARCH_CRITERIA_FRAMEID) || uprs.contains(SEARCH_CRITERIA_FRAMENAME)
                || uprs.contains(SEARCH_CRITERIA_FRAMEINDEX) || uprs.contains(SEARCH_CRITERIA_FRAMEXPATH)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param recognitionString String
     * @return boolean, true if the RS is a kind of frame-RS
     */
    protected static boolean isFrameRS(String recognitionString) {
        try {
            String upperCaseRS = recognitionString.trim().toUpperCase();
            if (upperCaseRS.startsWith(SEARCH_CRITERIA_FRAMEID) || upperCaseRS.startsWith(SEARCH_CRITERIA_IFRAMEID)
                    || upperCaseRS.startsWith(SEARCH_CRITERIA_FRAMENAME)
                    || upperCaseRS.startsWith(SEARCH_CRITERIA_FRAMEINDEX)
                    || upperCaseRS.startsWith(SEARCH_CRITERIA_FRAMEXPATH)) {
                return true;
            }
        } catch (Exception e) {
        }
        return false;
    }

    /**
     * if the frame-RS is specified as one qualifier of RS, find and return it.<br>
     * For example, for "FrameID=yyy;property=xxx;property=xxx", we return "FrameID=yyy"<br>
     * The rest "property=xxx;property=xxx" is not useful for now, as FrameId, FrameName<br>
     * and FrameXpath, they are enough to find a frame.<br>
     * @param recognitionString String
     * @return String, the frame-RS
     */
    protected static String retrieveFrameRS(String recognitionString) {
        String frameRS = null;
        //if frame-RS is specified with multiple qualifiers, we just care about the frame-RS
        String[] tokens = StringUtils.getTokenArray(recognitionString, qulifierSeparator, escapeChar);
        for (String qualifier : tokens) {
            if (isFrameRS(qualifier)) {
                frameRS = qualifier;
                break;
            }
        }
        return frameRS;
    }

    /**
     * Switch to a certain frame according to the parameter frameRS.<br>
     * <b>Note:</b> User should be extremely careful to call this method, which will change the frame to search within.<br>
     * <b>Note:</b> Before calling this method, make sure that <b>webdriver.switchTo().defaultContent()</b> is called.<br>
     * @param webdriver WebDriver, the webdriver used to search frame and switch frame
     * @param frameRS String, the recognition string of the frame
     */
    public static boolean switchFrame(WebDriver webdriver, String frameRS) {
        WebElement frame = _getSwitchFrame(webdriver, frameRS);
        return _switchFrame(webdriver, frame);
    }

    private static WebElement _getSwitchFrame(WebDriver webdriver, String frameRS) {
        String debugmsg = StringUtils.debugmsg(false);

        if (webdriver == null || frameRS == null)
            return null;

        WebElement frame = null;
        String[] tokens = StringUtils.getTokenArray(frameRS, assignSeparator, escapeChar);
        if (tokens == null || tokens.length < 2) {
            IndependantLog.error(debugmsg + "frame recognition string '" + frameRS + "' is not valid.");
            return null;
        }
        String searchCreteria = tokens[0].trim().toUpperCase();
        String value = tokens[1];
        //      TargetLocator targetLocator = webdriver.switchTo();

        if (searchCreteria.equals(SEARCH_CRITERIA_FRAMEID)) {
            frame = webdriver.findElement(By.id(value));

        } else if (searchCreteria.equals(SEARCH_CRITERIA_IFRAMEID)) {
            frame = webdriver.findElement(By.id(value));

        } else if (searchCreteria.equals(SEARCH_CRITERIA_FRAMENAME)) {
            frame = webdriver.findElement(By.name(value));

        } else if (searchCreteria.equals(SEARCH_CRITERIA_FRAMEINDEX)) {
            int index = StringUtilities.parseIndex(value);
            //index is 1-based, needs to minus 1 to get 0-based index and feed it to method frame().
            //         targetLocator.frame(index-1);
            //         IndependantLog.debug(debugmsg+" switch to frame by index '"+value+"'");

            //TODO How to get the frame's webelement???
            frame = getFrameWebElement(webdriver, index);//Not found???
            //frame = targetLocator.activeElement();//activeElement() is not the frame itself
            IndependantLog.warn(debugmsg + " currently it is NOT suggested to use index for frame!");

        } else if (searchCreteria.equals(SEARCH_CRITERIA_FRAMEXPATH)) {
            frame = webdriver.findElement(By.xpath(value));

        } else {
            IndependantLog.warn(debugmsg + " frame recognition string '" + frameRS + "' is not supported.");
        }

        if (frame != null) {
            IndependantLog.debug(debugmsg + " get switching-frame '" + frame + "' according to '" + value + "'");
        }
        //      if(frame!=null){
        //         targetLocator.frame(frame);
        //         IndependantLog.debug(debugmsg+" switch to frame by '"+value+"'");
        //      }

        return frame;
    }

    private static boolean _switchFrame(WebDriver webdriver, WebElement frame) {
        if (webdriver == null || frame == null)
            return false;
        String debugmsg = StringUtils.debugmsg(false);

        try {
            webdriver.switchTo().frame(frame);
            IndependantLog.debug(debugmsg + " switched to frame '" + frame + "'");
            return true;
        } catch (Exception e) {
            IndependantLog.debug(
                    debugmsg + " failed to switch to frame '" + frame + "' due to " + StringUtils.debugmsg(e));
            return false;
        }
    }

    /**
     * Example: tag=span;propertycontains=innerHTML:some value
     *
     * @param RS -- Full MultiAttribute qualifier RS. (Does not have to be a single qualifier assignment segment.)
     * The algorithm does NOT assume the TAG qualifier is first in the RS.
     * @return the value associated with any TAG= qualifier setting, or null if it does not exist.
     * @throws SeleniumException
     */
    protected static String getTagQualifierValue(String RS) throws SeleniumException {
        if (RS == null || RS.length() < 5)
            return null;
        String rc = null;
        String[] st = StringUtils.getTokenArray(RS, qulifierSeparator, escapeChar);

        for (int i = 0; i < st.length; i++) {
            if (st[i].toUpperCase().startsWith(SEARCH_CRITERIA_TAG)) {
                try {
                    String[] p = getFirstQualifierPair(st[i]);
                    if (p[0].equalsIgnoreCase(SEARCH_CRITERIA_TAG)) {
                        rc = p[1];
                        if (rc == null)
                            continue;
                        int s = rc.indexOf(qulifierSeparator);
                        if (s == 0 || rc.length() == 0) {
                            rc = null;
                        } else if (s > 0) {
                            rc = rc.substring(0, s);
                        }
                        if (rc != null)
                            break;
                    }
                } catch (Exception ignore) {
                }
            }
        }
        return rc;
    }

    /**
     * Split a string by separator {@link #assignSeparator}, and return the result.<br>
     * If the RS contains multiple {@link #assignSeparator}, this method will ONLY<br>
     * consider the first separator, the string before that separator will be considered<br>
     * as qualifier; the string after will be all considered as value.<br>
     *
     * @param RS String, a recognition with a {@link #assignSeparator} inside.
     * @return String[], the separated RS, including qualifier and value.
     * @throws SeleniumException if the RS is not valid.
     */
    protected static String[] getFirstQualifierPair(String RS) throws SeleniumException {
        String[] assignPair = null;
        String errmsg = null;

        try {
            int assignindex = RS.indexOf(assignSeparator);

            if (assignindex < 0 || assignindex == RS.length() - 1) {
                errmsg = " invalid recognition string: " + RS;
                IndependantLog.debug(StringUtils.debugmsg(false) + errmsg);
                throw new SeleniumException(errmsg);
            }

            assignPair = new String[2];
            assignPair[0] = RS.substring(0, assignindex).trim();
            assignPair[1] = RS.substring(assignindex + 1);
        } catch (Exception e) {
            errmsg = " met " + StringUtils.debugmsg(e);
            IndependantLog.debug(StringUtils.debugmsg(false) + errmsg);
            throw new SeleniumException(errmsg);
        }

        return assignPair;
    }

    /**
     * To detect if the page has changed or not.<br>
     * For example, if user click on a link, or a Log-Out menu item, the page may probably change.<br>
     * The current implementation is not guarantee!<br>
     * @return boolean, if the page has changed.
     */
    public static boolean pageHasChanged() {
        String url = null;
        boolean changed = false;

        try {
            //TODO Does "url change" mean the page has changed???
            //Not enough, there are some cases, url changed, but page doesn't. we need another way to detect.
            //TODO we need to wait the page completely-loaded so that we can get the new url.
            StringUtilities.sleep(2000);
            url = getWebDriver().getCurrentUrl();
            IndependantLog.debug(StringUtils.debugmsg(false) + " '" + url + "'='" + lastVisitedURL + "' ?");
            if (!url.equals(lastVisitedURL))
                changed = true;
        } catch (Exception e) {
            IndependantLog.debug(StringUtils.debugmsg(false) + "Met " + StringUtils.debugmsg(e));
        }

        return changed;
    }

    public static class SwitchFramesResults {
        boolean haveSwitchedFrames = false;
        List<String> rsWithoutFrames = new ArrayList<String>();

        public SwitchFramesResults setSwitchedFrames(boolean switched) {
            haveSwitchedFrames = switched;
            return this;
        }

        public SwitchFramesResults setRsWithoutFrames(List<String> withoutFrames) {
            rsWithoutFrames = withoutFrames;
            return this;
        }
    }

    /** 
     * Before doing any component search, we make sure we are in the correct frame.  
     * Frame information may or may not be within the provided recognition string.
     * @param recognitionString
     */
    protected static SwitchFramesResults _switchFrames(String recognitionString) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "_swithcFrames");

        String[] st = StringUtils.getTokenArray(recognitionString, childSeparator, escapeChar);
        /* rsWithoutFrames: Recognition String may contain some Frame-RS, after handling the
         * frames, these Frame-RS are no longer useful, so they will be removed and the rest
         * RS will be kept in the list rsWithoutFrames
         */
        List<String> rsWithoutFrames = new ArrayList<String>();
        TargetLocator targetLocator = null;
        boolean haveSwichedFrame = false;

        //1. Handle the Frames Recognition String
        try {
            WebDriver webdriver = getWebDriver();
            lastVisitedURL = webdriver.getCurrentUrl();
            targetLocator = webdriver.switchTo();
            //very IMPORTANT step: Switch back to the top window or first frame
            targetLocator.defaultContent();
            boolean done = false;
            boolean retried = false;
            while (!done) {

                //Get information about the browser window by "javascript code",
                //as Selenium window object doesn't provide enough information.
                Window window = webdriver.manage().window();
                Object windowObject = BrowserWindow.getWindowObjectByJS(window);
                if (windowObject != null) {
                    try {
                        lastBrowserWindow = new BrowserWindow(windowObject);
                        IndependantLog.debug(
                                debugmsg + " DOM window Object retrieved successfully.  Evaluating information...");
                    } catch (Exception e) {
                        IndependantLog.warn(debugmsg + e.getMessage());
                        if (lastBrowserWindow == null) {
                            IndependantLog.error(
                                    debugmsg + " lastBrowserWindow is null, create a default BrowserWindow.");
                            lastBrowserWindow = new BrowserWindow();
                        } else {
                            IndependantLog.debug(debugmsg + "Retaining the last Browser window object instead.");
                        }
                    }
                    done = true;
                } else {
                    // CANAGL -- resolving problems in IE after "Login"
                    // DEBUG Experimental -- IE not working the same as other browsers
                    IndependantLog.debug(debugmsg + " DOM window Object could not be retrieved.");
                    if (!retried) {
                        IndependantLog.debug(debugmsg + " retrying a new targetLocator WebDriver...");
                        webdriver = reconnectLastWebDriver();

                        IndependantLog.debug(debugmsg + " changing the last visited URL from '" + lastVisitedURL
                                + "' to '" + webdriver.getCurrentUrl() + "'");
                        lastVisitedURL = webdriver.getCurrentUrl();

                        targetLocator = webdriver.switchTo();
                        targetLocator.defaultContent();
                        retried = true;
                    } else {
                        IndependantLog.debug(debugmsg + " retrying a new targetLocator FAILED.");
                        done = true;
                        throw new SeleniumPlusException(
                                "Suspected UnreachableBrowserException seeking Browser DOM window Object.");
                    }
                }
            }

            //reset the frame before searching the frame element if it exists.
            FrameElement frameElement = null;
            WebElement frame = null;
            String frameRS = null;
            //search the frame element if RS contains frame-info
            for (String rst : st) {
                //pre-check if this RS contains anything about frame-RS
                frameRS = retrieveFrameRS(rst);

                if (frameRS != null) {
                    //Get frame WebElement according to frame's recognition string
                    frame = _getSwitchFrame(webdriver, frameRS);
                    if (frame != null) {
                        frameElement = new FrameElement(frameElement, frame);
                        haveSwichedFrame = _switchFrame(webdriver, frame);

                        //Can we use frame as SearchContext for child frame? FrameID=parentFrame;\;FrameID=childFrame
                        //NO, frame WebElement can NOT be used as SearchContext, will cause Exception
                        //We should always use webdriver as the SearchContext to find frame WebElement

                        //don't break, if there is frame in frame, as FRAMENAME=parent;\\;FRAMENAME=child
                        //break;
                    }

                } else {
                    //IndependantLog.warn(debugmsg+" store normal recognition string '"+rst+"' for further processing.");
                    rsWithoutFrames.add(rst);
                }
            }

            if (haveSwichedFrame)
                lastFrame = frameElement;

        } catch (Exception e) {
            IndependantLog.error(debugmsg + " during switching frame, met exception " + StringUtils.debugmsg(e));
        }

        //2. Before search an element, switch to the correct frame
        //   For the child component, we don't need to specify the frame-RS, they use the same frame-RS as
        //   their parent component.
        try {
            if (!haveSwichedFrame && targetLocator != null) {
                FrameElement frameElement = lastFrame;
                Stack<FrameElement> frameStack = new Stack<FrameElement>();
                while (frameElement != null) {
                    frameStack.push(frameElement);
                    frameElement = frameElement.getParentFrame();
                }
                while (!frameStack.isEmpty() && frameStack.peek() != null) {
                    targetLocator.frame(frameStack.pop().getWebElement());
                    haveSwichedFrame = true;
                }
            }
        } catch (Exception e) {
            if (e instanceof StaleElementReferenceException && pageHasChanged()) {
                IndependantLog.warn(
                        debugmsg + " switching to the previous frame 'lastFrame' failed! The page has changed!");
                //LeiWang: S1215754
                //if we click a link within a FRAME, as the link is in a FRAME, so the field 'lastFrame' will be assigned after clicking the link.
                //then the link will lead us to a second page, when we try to find something on that page, firstly we try to switch to 'lastFrame' and
                //get a StaleElementReferenceException (as we are in the second page, there is no such frame of the first page)
                //but we still want our program to find the web-element on the second page, so we will just set 'lastFrame' to null and let program continue.
                //            [FirstPage]
                //            FirstPage="FRAMEID=iframeResult"
                //            Link="xpath=/html/body/a"
                //
                //            [SecondPage]
                //            SecondPage="xpath=/html"
                //            return null;
            } else {
                IndependantLog.warn(debugmsg + " switching to the previous frame 'lastFrame' failed! Met exception "
                        + StringUtils.debugmsg(e));
            }
            //LeiWang: S1215778
            //If we fail to switch to 'lastFrame', which perhaps means that it is not useful anymore
            //we should reset it to null, otherwise it will cause errors, such as calculate the element's screen location
            IndependantLog.debug(debugmsg + " 'lastFrame' is not useful anymore, reset it to null.");
            lastFrame = null;
        }
        return new SwitchFramesResults().setRsWithoutFrames(rsWithoutFrames).setSwitchedFrames(haveSwichedFrame);
    }

    /**
     * Primary entry point to seek a WebElement based on a provided recognition string.
     * We support multiple types of recognition strings
     * @param sc SearchContext, could be the WebDriver or a parent WebElement context.
     * @param recognitionString String, The recognition string used to identify the sought element.
     * @return WebElement found or null if not found.
     */
    public static WebElement getObject(SearchContext sc, String recognitionString) {

        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getObject");
        IndependantLog.debug(debugmsg + "using SearchContext=" + sc + " to find RS='" + recognitionString + "'");

        if (sc == null) {
            IndependantLog.error("SearchContext is null, cannot continue search!!!");
            return null;
        }

        SwitchFramesResults sfr = _switchFrames(recognitionString);

        SearchContext wel = sc;

        //3. SBJLWA, FIX http://sww.sas.com/cgi-bin/quick_browse?defectid=S1138290
        //Error 1: Map doesn't contain a window definition (under a frame)
        //  [Window]
        //  Child="FrameId=xxx;\;Id=xxx"
        //If we are going to FIND element under certain FRAME,
        //and the parent is "[[RemoteDriver.... ] ->  xpath: /html]", which maybe the default parent "xpath=/html" without any FRAME;
        //the webelement to find is in a Frame, StaleElementReferenceException will be thrown out.
        //To avoid this, we need to modify the SearchContext to the "default-webdriver"
        if (sfr.haveSwitchedFrames && XPATH.isRootHtml(wel)) {
            wel = getWebDriver();
            IndependantLog.debug(debugmsg + "Replace default SearchContext '/html' by WebDriver.");
        }
        //Error 2: Window definition ONLY has information about FRAME, no child information
        //  [Window]
        //  Window="FrameId=xxx"
        //  Child="FrameId=xxx;\;Id=xxx"
        //If 'recognition string' contains only frame information, rsWithoutFrames will be empty, and this method will
        //return null (ClassCastException from WebDriver to WebElement); to avoid this set "xpath=/html" as the default object
        if (sfr.rsWithoutFrames.isEmpty())
            sfr.rsWithoutFrames.add(RS.xpath(XPATH.html()));

        //4. Search the target element within a frame (if exist) level by level, by handling the normal RS
        String[] st = sfr.rsWithoutFrames.toArray(new String[0]);//The normal RS without the frame-RS
        String[] firstQualifierPair = null;
        List<String> prefixes = new ArrayList<String>();
        boolean isPASM = false;
        String searchCriteria = null;
        String value = null;
        for (String rst : st) {
            try {
                prefixes.clear();
                rst = GuiObjectVector.removeRStringPrefixes(rst, prefixes);
                isPASM = GuiObjectVector.isPASMMode(prefixes);

                //rst MUST be in format "xxx=yyy", otherwise it is considered as invalid
                firstQualifierPair = getFirstQualifierPair(rst);
                searchCriteria = firstQualifierPair[0];
                value = firstQualifierPair[1];

                IndependantLog.debug(
                        debugmsg + "qualifier='" + searchCriteria + "' value='" + value + "' isPASM=" + isPASM);

                if (SEARCH_CRITERIA_TYPE.equalsIgnoreCase(searchCriteria)) {
                    wel = getObjectForDomain(wel, rst, isPASM);

                } else if (SEARCH_CRITERIA_XPATH.equalsIgnoreCase(searchCriteria)) {
                    IndependantLog.debug(debugmsg + " searching xpath '" + value + "'");
                    wel = getObjectByQualifier(wel, searchCriteria, value);

                } else if (SEARCH_CRITERIA_CSS.equalsIgnoreCase(searchCriteria)) {
                    IndependantLog.debug(debugmsg + " searching css'" + value + "'");
                    wel = getObjectByQualifier(wel, searchCriteria, value);

                } else if (rst.contains(qulifierSeparator)) {//Multiple Attributes and Qualifiers
                    wel = getObjectByMultiAttributes(wel, rst, isPASM);

                } else {
                    try {
                        //Only ONE qualifier pair, qualifier=value
                        String[] tokens = StringUtils.getTokenArray(rst, assignSeparator, escapeChar);
                        wel = getObjectByQualifier(wel, tokens[0], tokens[1]);
                    } catch (SeleniumPlusException se) {
                        IndependantLog.warn(debugmsg + se.getMessage());
                        continue;
                    }
                }
            } catch (NoSuchElementException nse) {
                wel = null;
                IndependantLog.debug(debugmsg + " NoSuchElementException for '" + rst + "'.");
            } catch (Throwable t) {
                //wel is NOT null, it keeps the original value.
                //Do we permit to continue the search? Maybe we should stop.
                wel = null;
                IndependantLog.debug(debugmsg + " Met Exception " + StringUtils.debugmsg(t));
            }

            if (wel == null) {
                IndependantLog.error(debugmsg + " cannot find child with RS '" + rst + "'");
                break;
            }
        }

        try {
            return (WebElement) wel;
        } catch (ClassCastException x) {
            //RemoteDriver cannot be cast to WebElement
            return null;
        }
    }

    /**
     * Primary entry point to seek all WebElements matching a provided recognition string.
     * We support multiple types of recognition strings
     * @param sc SearchContext, could be the WebDriver or a parent WebElement context.
     * @param recognitionString String, The recognition string used to identify the matching elements.
     * @return List&lt;WebElement> with 0 or more WebElement entries.
     */
    public static List<WebElement> getObjects(SearchContext sc, String recognitionString) {

        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getObjects");
        IndependantLog.debug(debugmsg + "using SearchContext=" + sc + " to find RS='" + recognitionString + "'");

        List<WebElement> list = new ArrayList<WebElement>();

        if (sc == null) {
            IndependantLog.error("SearchContext is null, cannot continue search!!!");
            return list;
        }

        SearchContext wel = sc;
        SwitchFramesResults sfr = _switchFrames(recognitionString);

        //3. SBJLWA, FIX http://sww.sas.com/cgi-bin/quick_browse?defectid=S1138290
        //Error 1: Map doesn't contain a window definition (under a frame)
        //  [Window]
        //  Child="FrameId=xxx;\;Id=xxx"
        //If we are going to FIND element under certain FRAME,
        //and the parent is "[[RemoteDriver.... ] ->  xpath: /html]", which maybe the default parent "xpath=/html" without any FRAME;
        //the webelement to find is in a Frame, StaleElementReferenceException will be thrown out.
        //To avoid this, we need to modify the SearchContext to the "default-webdriver"
        if (sfr.haveSwitchedFrames && XPATH.isRootHtml(wel)) {
            wel = getWebDriver();
            IndependantLog.debug(debugmsg + "Replace default SearchContext '/html' by WebDriver.");
        }
        //Error 2: Window definition ONLY has information about FRAME, no child information
        //  [Window]
        //  Window="FrameId=xxx"
        //  Child="FrameId=xxx;\;Id=xxx"
        //If 'recognition string' contains only frame information, rsWithoutFrames will be empty, and this method will
        //return null (ClassCastException from WebDriver to WebElement); to avoid this set "xpath=/html" as the default object
        if (sfr.rsWithoutFrames.isEmpty())
            sfr.rsWithoutFrames.add(RS.xpath(XPATH.html()));

        //4. Search the target element within a frame (if exist) level by level, by handling the normal RS
        String[] st = sfr.rsWithoutFrames.toArray(new String[0]);//The normal RS without the frame-RS
        String[] firstQualifierPair = null;
        List<String> prefixes = new ArrayList<String>();
        boolean isPASM = false;
        String searchCriteria = null;
        String value = null;
        String rst = null;
        boolean isLastRS = false;
        for (int i = 0; i < st.length; i++) {
            try {
                prefixes.clear();
                rst = GuiObjectVector.removeRStringPrefixes(st[i], prefixes);
                isPASM = GuiObjectVector.isPASMMode(prefixes);

                //rst MUST be in format "xxx=yyy", otherwise it is considered as invalid
                firstQualifierPair = getFirstQualifierPair(rst);
                searchCriteria = firstQualifierPair[0];
                value = firstQualifierPair[1];
                isLastRS = (i == st.length - 1);

                IndependantLog.debug(
                        debugmsg + "qualifier='" + searchCriteria + "' value='" + value + "' isPASM=" + isPASM);

                if (SEARCH_CRITERIA_TYPE.equalsIgnoreCase(searchCriteria)) {
                    if (isLastRS) {
                        list = getObjectsForDomain(wel, rst, isPASM);
                    } else {
                        wel = getObjectForDomain(wel, rst, isPASM);
                    }
                } else if (SEARCH_CRITERIA_XPATH.equalsIgnoreCase(searchCriteria)) {
                    IndependantLog.debug(debugmsg + " searching xpath '" + value + "'");
                    if (isLastRS) {
                        list = getObjectsByQualifier(wel, searchCriteria, value);
                    } else {
                        wel = getObjectByQualifier(wel, searchCriteria, value);
                    }
                } else if (SEARCH_CRITERIA_CSS.equalsIgnoreCase(searchCriteria)) {
                    IndependantLog.debug(debugmsg + " searching css'" + value + "'");
                    if (isLastRS) {
                        list = getObjectsByQualifier(wel, searchCriteria, value);
                    } else {
                        wel = getObjectByQualifier(wel, searchCriteria, value);
                    }
                } else if (rst.contains(qulifierSeparator)) {//Multiple Attributes and Qualifiers
                    if (isLastRS) {
                        list = getObjectsByMultiAttributes(wel, rst, isPASM);
                    } else {
                        wel = getObjectByMultiAttributes(wel, rst, isPASM);
                    }
                } else {
                    try {
                        //Only ONE qualifier pair, qualifier=value
                        String[] tokens = StringUtils.getTokenArray(rst, assignSeparator, escapeChar);
                        if (isLastRS) {
                            list = getObjectsByQualifier(wel, tokens[0], tokens[1]);
                        } else {
                            wel = getObjectByQualifier(wel, tokens[0], tokens[1]);
                        }
                    } catch (SeleniumPlusException se) {
                        IndependantLog.warn(debugmsg + se.getMessage());
                        continue;
                    }
                }
            } catch (NoSuchElementException nse) {
                wel = null;
                IndependantLog.debug(debugmsg + " NoSuchElementException for '" + rst + "'.");
            } catch (Throwable t) {
                //wel is NOT null, it keeps the original value.
                //Do we permit to continue the search? Maybe we should stop.
                wel = null;
                IndependantLog.debug(debugmsg + " Met Exception " + StringUtils.debugmsg(t));
            }

            if (isLastRS) {
                if (list.isEmpty()) {
                    IndependantLog.error(debugmsg + " cannot find child with RS '" + rst + "'");
                }
            } else if (wel == null) {
                IndependantLog.error(debugmsg + " cannot find child with RS '" + rst + "'");
                break;
            }
        }
        return list;
    }

    public static boolean isFrameWebElement(WebElement frameTag) {
        if (frameTag == null)
            return false;
        try {
            String tagName = frameTag.getTagName().toLowerCase();
            return TAG_FRAME.equals(tagName) || TAG_IFRAME.equals(tagName);
        } catch (Exception e) {
            IndependantLog.warn(StringUtils.debugmsg(false) + "Met " + StringUtils.debugmsg(e));
            return false;
        }
    }

    /**
     * Find the Nth frame webelement in the search context.<br>
     * @param sc SearchContext, the search context, SHOULD be WebDriver
     * @param index int, the index of the tag, 1-based
     * @return WebElement, the webelement or null if not found
     */
    protected static WebElement getFrameWebElement(SearchContext sc, int index) {
        WebElement frame = getWebElement(sc, TAG_FRAME, index);
        if (frame == null)
            frame = getWebElement(sc, TAG_IFRAME, index);

        try {
            if (frame == null)
                frame = sc.findElements(By.xpath("//" + TAG_FRAME)).get(index - 1);
        } catch (Exception ignore) {
        }
        try {
            if (frame == null)
                frame = sc.findElements(By.xpath("//" + TAG_IFRAME)).get(index - 1);
        } catch (Exception ignore) {
        }

        return frame;
    }

    /**
     * Find the Nth tag in the search context.<br>
     * @param sc SearchContext, the search context
     * @param tagName String, the tag name to find
     * @param index int, the index of the tag, 1-based
     * @return WebElement, the webelement or null if not found
     */
    protected static WebElement getWebElement(SearchContext sc, String tagName, int index) {
        WebElement result = null;
        try {
            //         result = sc.findElement(By.xpath("(//"+tagName+")["+index+"]"));//Why the parenthesis?
            result = sc.findElement(By.xpath("//" + tagName + "[" + index + "]"));
        } catch (Exception e) {
            IndependantLog.error(StringUtils.debugmsg(false) + " met " + StringUtils.debugmsg(e));
        }
        return result;
    }

    protected static List<WebElement> findElements(SearchContext wel, String xpath) {
        List<WebElement> preMatches = null;
        try {
            preMatches = wel.findElements(By.xpath(xpath));
        } catch (InvalidSelectorException x) {
            if (xpath.startsWith(".")) {
                xpath = xpath.substring(1);
                preMatches = wel.findElements(By.xpath(xpath));
            } else {
                throw x;
            }
        }
        return preMatches;
    }

    protected static SearchContext getObjectByText(SearchContext wel, String text, boolean partialMatch) {
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "using '" + text + "', partialMatch=" + partialMatch);

        String xpath = XPATH.fromText(text, partialMatch, true);
        List<WebElement> preMatches = findElements(wel, xpath);

        List<WebElementWarpper> elements = new ArrayList<WebElementWarpper>();
        for (WebElement item : preMatches)
            elements.add(new WebElementWarpper(item, item.getText()));

        return getMatchedObject(elements, text, partialMatch);
    }

    /** @return 0 or more matched elements */
    protected static List<WebElement> getObjectsByText(SearchContext wel, String text, boolean partialMatch) {
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "using '" + text + "', partialMatch=" + partialMatch);

        String xpath = XPATH.fromText(text, partialMatch, true);
        List<WebElement> preMatches = findElements(wel, xpath);

        List<WebElementWarpper> elements = new ArrayList<WebElementWarpper>();
        for (WebElement item : preMatches)
            elements.add(new WebElementWarpper(item, item.getText()));

        return getMatchedObjects(elements, text, partialMatch);
    }

    protected static SearchContext getObjectByTitle(SearchContext wel, String value, boolean partialMatch) {
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "using '" + value + "', partialMatch=" + partialMatch);
        String attribute = SEARCH_CRITERIA_TITLE.toLowerCase();

        String xpath = XPATH.fromAttribute(attribute, value, partialMatch, true);
        List<WebElement> preMatches = findElements(wel, xpath);

        List<WebElementWarpper> elements = new ArrayList<WebElementWarpper>();
        for (WebElement item : preMatches)
            elements.add(new WebElementWarpper(item, item.getAttribute(attribute)));

        return getMatchedObject(elements, value, partialMatch);
    }

    /** @return 0 or more matched elements */
    protected static List<WebElement> getObjectsByTitle(SearchContext wel, String value, boolean partialMatch) {
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "using '" + value + "', partialMatch=" + partialMatch);
        String attribute = SEARCH_CRITERIA_TITLE.toLowerCase();

        String xpath = XPATH.fromAttribute(attribute, value, partialMatch, true);
        List<WebElement> preMatches = findElements(wel, xpath);

        List<WebElementWarpper> elements = new ArrayList<WebElementWarpper>();
        for (WebElement item : preMatches)
            elements.add(new WebElementWarpper(item, item.getAttribute(attribute)));

        return getMatchedObjects(elements, value, partialMatch);
    }

    /**
     * @param elements List&lt;WebElementWarpper>, contains WebElement to match
     * @param value String, the value to match with
     * @param partialMatch boolean, if we try to find the partial matched item
     * @return SearchContext, the matched object
     */
    protected static SearchContext getMatchedObject(List<WebElementWarpper> elements, String value,
            boolean partialMatch) {
        if (partialMatch) {
            //try to find the partial matched item
            for (WebElementWarpper element : elements) {
                if (element.value.contains(value))
                    return element.element;
            }
            //try to find the partial matched item ignoring case
            for (WebElementWarpper element : elements) {
                if (element.value.toLowerCase().contains(value.toLowerCase()))
                    return element.element;
            }
        } else {
            //try to find the exact matched item
            for (WebElementWarpper element : elements) {
                if (element.value.equals(value))
                    return element.element;
            }
            //try to find the exact matched item ignoring case
            for (WebElementWarpper element : elements) {
                if (element.value.equalsIgnoreCase(value))
                    return element.element;
            }
        }
        //finally, return the first element if it exists
        if (elements.size() > 0)
            return elements.get(0).element;

        return null;
    }

    /**
     * @param elements List&lt;WebElementWarpper>, contains WebElements to match
     * @param value String, the value to match with
     * @param partialMatch boolean, if we try to find the partial matched item
     * @return List&lt;WebElement> 0 or more matched WebElements.
     */
    protected static List<WebElement> getMatchedObjects(List<WebElementWarpper> elements, String value,
            boolean partialMatch) {
        List<WebElement> list = new ArrayList<WebElement>();
        if (partialMatch) {
            //try to find the partial matched item
            for (WebElementWarpper element : elements) {
                if (element.value.contains(value) || element.value.toLowerCase().contains(value.toLowerCase())) {
                    list.add(element.element);
                }
            }
        } else {
            //try to find the exact matched items
            for (WebElementWarpper element : elements) {
                if (element.value.equals(value) || element.value.equalsIgnoreCase(value)) {
                    list.add(element.element);
                }
            }
        }
        //finally, return the list of 0 or more matched objects
        return list;
    }

    /**
     * This class contains 2 fields, one is WebElement, the other is value.<br>
     * The field value is a string, used to match. It can be assigned by WebElement.getText(),<br>
     * WebElement.getAttribute("title") or some other text value of the WebElement, which can<br>
     * be used to match with a text value.<br>
     *@see SearchObject#getMatchedObject(List, String)
     */
    private static class WebElementWarpper {
        public WebElement element = null;
        public String value = null;

        public WebElementWarpper(WebElement element, String value) {
            this.element = element;
            this.value = value;
        }
    }

    /**
     * According to pair(qualifier, value), find the matched WebElement.<br>
     * Some qualifiers like "ID", can determine the WebElement uniquely and result<br>
     * only one WebElement; Other qualifiers may result several WebElement, the first<br>
     * one will be returned.<br>
     *
     * @param sc SearchContext, the search context, it is normally a WebElement.
     * @param qualifier String, the qualifier like xpath, css, id, name, link etc.
     * @param value String, the value of a qualifier.
     * @return SearchContext, the WebElment found according to qualifier and its value.
     * @throws SeleniumPlusException
     */
    protected static SearchContext getObjectByQualifier(SearchContext sc, String qualifier, String value)
            throws SeleniumPlusException {
        SearchContext result = null;
        String qualifierUC = null;

        if (qualifier == null) {
            throw new SeleniumPlusException("ignore null qualifier.");
        } else {
            qualifierUC = qualifier.toUpperCase();
        }

        try {

            if (SEARCH_CRITERIA_XPATH.equals(qualifierUC)) {
                result = sc.findElement(By.xpath(value));

            } else if (SEARCH_CRITERIA_CSS.equals(qualifierUC)) {
                result = sc.findElement(By.cssSelector(value));

            } else if (SEARCH_CRITERIA_TAG.equals(qualifierUC)) {
                result = sc.findElement(By.tagName(value));
            } else {
                boolean partialMatch = qualifierUC.endsWith(SEARCH_CRITERIA_CONTAINS_SUFFIX);

                //The following qualifiers will support suffix "Contains":
                //idContains, classContains, nameContains, linkContains, textContains, titleContains, iframeidContains
                if (SEARCH_CRITERIA_ID.equals(qualifierUC)) {
                    result = sc.findElement(By.id(value));

                } else if (SEARCH_CRITERIA_CLASS.equals(qualifierUC)) {
                    result = sc.findElement(By.className(value));

                } else if (SEARCH_CRITERIA_NAME.equals(qualifierUC)) {
                    result = sc.findElement(By.name(value));

                } else if (SEARCH_CRITERIA_LINK.equals(qualifierUC)) {
                    result = sc.findElement(By.linkText(value));

                } else if (SEARCH_CRITERIA_PARTIALLINK.equals(qualifierUC)
                        || (SEARCH_CRITERIA_LINK + SEARCH_CRITERIA_CONTAINS_SUFFIX).equals(qualifierUC)) {
                    result = sc.findElement(By.partialLinkText(value));

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_TEXT)) {
                    result = getObjectByText(sc, value, partialMatch);

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_TITLE)) {
                    result = getObjectByTitle(sc, value, partialMatch);

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_IFRAMEID)) {
                    result = sc
                            .findElement(By.xpath("//iframe[" + XPATH.condition("id", value, partialMatch) + "]"));

                } else if (partialMatch) {
                    //idContains, classContains, nameContains will be supported here.
                    result = sc.findElement(By.xpath(XPATH.RELATIVE_MATCHING_ALL_START
                            + XPATH.conditionContains(qualifier, value) + XPATH.END));

                } else {
                    throw new SeleniumPlusException(
                            "ignore unknown qualifier '" + qualifierUC + "', value='" + value + "'");
                }
            }
        } catch (Exception e) {
            if (e instanceof SeleniumPlusException)
                throw (SeleniumPlusException) e;
            IndependantLog.error(StringUtils.debugmsg(false) + " met " + StringUtils.debugmsg(e));
        }

        return result;
    }

    /**
     * According to pair(qualifier, value), find all matching WebElements.<br>
     * Some qualifiers like "ID", can determine the WebElement uniquely and result<br>
     * only one WebElement; Other qualifiers may result in several WebElements.<br>
     *
     * @param sc SearchContext, the search context, it is normally a WebElement.
     * @param qualifier String, the qualifier like xpath, css, id, name, link etc.
     * @param value String, the value of a qualifier.
     * @return List&lt;WebElement> 0 or more
     * @throws SeleniumPlusException
     */
    protected static List<WebElement> getObjectsByQualifier(SearchContext sc, String qualifier, String value)
            throws SeleniumPlusException {
        List<WebElement> result = new ArrayList<WebElement>();
        ;
        String qualifierUC = null;

        if (qualifier == null) {
            throw new SeleniumPlusException("ignore null qualifier.");
        } else {
            qualifierUC = qualifier.toUpperCase();
        }

        try {

            if (SEARCH_CRITERIA_XPATH.equals(qualifierUC)) {
                result = sc.findElements(By.xpath(value));

            } else if (SEARCH_CRITERIA_CSS.equals(qualifierUC)) {
                result = sc.findElements(By.cssSelector(value));

            } else if (SEARCH_CRITERIA_TAG.equals(qualifierUC)) {
                result = sc.findElements(By.tagName(value));
            } else {
                boolean partialMatch = qualifierUC.endsWith(SEARCH_CRITERIA_CONTAINS_SUFFIX);

                //The following qualifiers will support suffix "Contains":
                //idContains, classContains, nameContains, linkContains, textContains, titleContains, iframeidContains
                if (SEARCH_CRITERIA_ID.equals(qualifierUC)) {
                    result = sc.findElements(By.id(value));

                } else if (SEARCH_CRITERIA_CLASS.equals(qualifierUC)) {
                    result = sc.findElements(By.className(value));

                } else if (SEARCH_CRITERIA_NAME.equals(qualifierUC)) {
                    result = sc.findElements(By.name(value));

                } else if (SEARCH_CRITERIA_LINK.equals(qualifierUC)) {
                    result = sc.findElements(By.linkText(value));

                } else if (SEARCH_CRITERIA_PARTIALLINK.equals(qualifierUC)
                        || (SEARCH_CRITERIA_LINK + SEARCH_CRITERIA_CONTAINS_SUFFIX).equals(qualifierUC)) {
                    result = sc.findElements(By.partialLinkText(value));

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_TEXT)) {
                    result = getObjectsByText(sc, value, partialMatch);

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_TITLE)) {
                    result = getObjectsByTitle(sc, value, partialMatch);

                } else if (qualifierUC.startsWith(SEARCH_CRITERIA_IFRAMEID)) {
                    result = sc
                            .findElements(By.xpath("//iframe[" + XPATH.condition("id", value, partialMatch) + "]"));

                } else if (partialMatch) {
                    //idContains, classContains, nameContains will be supported here.
                    result = sc.findElements(By.xpath(XPATH.RELATIVE_MATCHING_ALL_START
                            + XPATH.conditionContains(qualifier, value) + XPATH.END));

                } else {
                    throw new SeleniumPlusException(
                            "ignore unknown qualifier '" + qualifierUC + "', value='" + value + "'");
                }
            }
        } catch (Exception e) {
            if (e instanceof SeleniumPlusException)
                throw (SeleniumPlusException) e;
            IndependantLog.error(StringUtils.debugmsg(false) + " met " + StringUtils.debugmsg(e));
        }

        return result;
    }

    /**
     * <pre>
     * Builds an XPath search string using multiple attributes provided in the recognition string
     * then performs a findElement(By.xpath(string));
     * If the recognition string contains special attributes, How to tell them from normal attributes???
     *
     * </pre>
     * @param sc SearchContext, the search context
     * @param RS String, the recognition string
     * @param isPASM boolean, if true, each part of 'recognition string' will be treated as property.<br>
     *                        if false, some reserved words will be treated as qualifiers, but not properties.<br>
     *                        the reserved words are {@link #SEARCH_CRITERIA_ITEMINDEX}, {@link #SEARCH_CRITERIA_PATH} etc.<br>
     * @return SearchContext (WebElement or WebDriver)
     */
    protected static SearchContext getObjectByMultiAttributes(SearchContext sc, String RS, boolean isPASM) {

        SearchContext obj = null;
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getObjectByMultiAttri");
        IndependantLog.debug(debugmsg + "using recognition string: " + RS);

        int count = 0;
        String[] st = StringUtils.getTokenArray(RS, qulifierSeparator, escapeChar);

        HashMap<String, String> qualifiers = new HashMap<String, String>();

        //
        // It does NOT look like we currently handle "qual=value;\;qual=value" syntax
        //

        try {
            String xpathStr = (!containTagRS(RS)) ? XPATH.RELATIVE_MATCHING_ALL_START
                    : XPATH.RELATIVE_MATCHING_TAG_START(getTagQualifierValue(RS));

            String[] props = null;
            String property = null;
            String value = null;
            String xpathCondition = null;
            for (String prop : st) {
                props = StringUtils.getTokenArray(prop, assignSeparator, escapeChar);
                ++count;
                property = props[0];
                value = props[1];

                // skip TAG if encountered. We already handled it.
                if (SEARCH_CRITERIA_TAG.equalsIgnoreCase(property)) {
                    continue;
                }

                if (isPASM) {
                    //all are properties
                    xpathStr += XPATH.condition(property, value, false);
                } else {
                    //handle some 'reserved qualifiers'
                    if (SEARCH_CRITERIA_ITEMINDEX.equalsIgnoreCase(property)
                            || SEARCH_CRITERIA_PATH.equalsIgnoreCase(property)
                            || SEARCH_CRITERIA_INDEX.equalsIgnoreCase(property)) {
                        qualifiers.put(property.toUpperCase(), value);
                        continue;

                    } else if (SEARCH_CRITERIA_PROPERTY.equalsIgnoreCase(property)) {
                        IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                        xpathCondition = getXpathCondition(value, false);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if (SEARCH_CRITERIA_PROPERTY_CONTAINS.equalsIgnoreCase(property)) {
                        IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                        xpathCondition = getXpathCondition(value, true);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if (SEARCH_CRITERIA_TEXT.equalsIgnoreCase(property)) {
                        xpathCondition = XPATH.conditionForText(value, false);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if ((SEARCH_CRITERIA_TEXT + SEARCH_CRITERIA_CONTAINS_SUFFIX)
                            .equalsIgnoreCase(property)) {
                        xpathCondition = XPATH.conditionForText(value, true);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } // try to generically handle any <something>Contains property values not handled above
                    else if (property.toUpperCase().endsWith(SEARCH_CRITERIA_CONTAINS_SUFFIX)) {
                        //TextContains should be handled differently to get the xpath
                        xpathStr += XPATH.conditionContains(property, value);
                    } else {
                        xpathStr += XPATH.condition(property, value, false);
                    }
                }

                if (st.length != count)
                    xpathStr += " " + XPATH.AND + " ";
            }
            //Remove the last "and", if there is no search-condition after that "and"
            if (xpathStr.trim().endsWith(XPATH.AND)) {
                xpathStr = xpathStr.substring(0, xpathStr.lastIndexOf(XPATH.AND));
            }
            xpathStr += XPATH.END;

            obj = getObjectByXpathAndQualifiers(sc, xpathStr, qualifiers);

        } catch (Throwable t) {
            IndependantLog.debug(debugmsg + "recognition string parsing error: " + t.getClass().getSimpleName()
                    + ": " + t.getMessage());
        }

        return obj;
    }

    /**
     * <pre>
     * Builds an XPath search string using multiple attributes provided in the recognition string
     * then performs a findElements(By.xpath(string));
     * If the recognition string contains special attributes, How to tell them from normal attributes???
     *
     * </pre>
     * @param sc SearchContext, the search context
     * @param RS String, the recognition string
     * @param isPASM boolean, if true, each part of 'recognition string' will be treated as property.<br>
     *                        if false, some reserved words will be treated as qualifiers, but not properties.<br>
     *                        the reserved words are {@link #SEARCH_CRITERIA_ITEMINDEX}, {@link #SEARCH_CRITERIA_PATH} etc.<br>
     * @return List&lt;WebElement> with 0 or more matching elements.
     */
    protected static List<WebElement> getObjectsByMultiAttributes(SearchContext sc, String RS, boolean isPASM) {

        List<WebElement> list = new ArrayList<WebElement>();
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getObjectByMultiAttri");
        IndependantLog.debug(debugmsg + "using recognition string: " + RS);

        int count = 0;
        String[] st = StringUtils.getTokenArray(RS, qulifierSeparator, escapeChar);

        HashMap<String, String> qualifiers = new HashMap<String, String>();

        //
        // It does NOT look like we currently handle "qual=value;\;qual=value" syntax
        //

        try {
            String xpathStr = (!containTagRS(RS)) ? XPATH.RELATIVE_MATCHING_ALL_START
                    : XPATH.RELATIVE_MATCHING_TAG_START(getTagQualifierValue(RS));

            String[] props = null;
            String property = null;
            String value = null;
            String xpathCondition = null;
            for (String prop : st) {
                props = StringUtils.getTokenArray(prop, assignSeparator, escapeChar);
                ++count;
                property = props[0];
                value = props[1];

                // skip TAG if encountered. We already handled it.
                if (SEARCH_CRITERIA_TAG.equalsIgnoreCase(property)) {
                    continue;
                }

                if (isPASM) {
                    //all are properties
                    xpathStr += XPATH.condition(property, value, false);
                } else {
                    //handle some 'reserved qualifiers'
                    if (SEARCH_CRITERIA_ITEMINDEX.equalsIgnoreCase(property)
                            || SEARCH_CRITERIA_PATH.equalsIgnoreCase(property)
                            || SEARCH_CRITERIA_INDEX.equalsIgnoreCase(property)) {
                        qualifiers.put(property.toUpperCase(), value);
                        continue;

                    } else if (SEARCH_CRITERIA_PROPERTY.equalsIgnoreCase(property)) {
                        IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                        xpathCondition = getXpathCondition(value, false);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if (SEARCH_CRITERIA_PROPERTY_CONTAINS.equalsIgnoreCase(property)) {
                        IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                        xpathCondition = getXpathCondition(value, true);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if (SEARCH_CRITERIA_TEXT.equalsIgnoreCase(property)) {
                        xpathCondition = XPATH.conditionForText(value, false);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } else if ((SEARCH_CRITERIA_TEXT + SEARCH_CRITERIA_CONTAINS_SUFFIX)
                            .equalsIgnoreCase(property)) {
                        xpathCondition = XPATH.conditionForText(value, true);
                        if (xpathCondition != null)
                            xpathStr += xpathCondition;

                    } // try to generically handle any <something>Contains property values not handled above
                    else if (property.toUpperCase().endsWith(SEARCH_CRITERIA_CONTAINS_SUFFIX)) {
                        //TextContains should be handled differently to get the xpath
                        xpathStr += XPATH.conditionContains(property, value);
                    } else {
                        xpathStr += XPATH.condition(property, value, false);
                    }
                }

                if (st.length != count)
                    xpathStr += " " + XPATH.AND + " ";
            }
            //Remove the last "and", if there is no search-condition after that "and"
            if (xpathStr.trim().endsWith(XPATH.AND)) {
                xpathStr = xpathStr.substring(0, xpathStr.lastIndexOf(XPATH.AND));
            }
            xpathStr += XPATH.END;

            list = getObjectsByXpathAndQualifiers(sc, xpathStr, qualifiers);

        } catch (Throwable t) {
            IndependantLog.debug(debugmsg + "recognition string parsing error: " + t.getClass().getSimpleName()
                    + ": " + t.getMessage());
        }

        return list;
    }

    /**
     * According to TextMatchingCriterion to get the sub item within a container WebElement.
     * @param itemContainer WebElement, the container WebElement, like list, menu etc.
     * @param criterion TextMatchingCriterion, holding the searching condition
     * @return WebElement, the matched sub item webelement
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected static WebElement getSubItem(WebElement itemContainer, TextMatchingCriterion criterion) {
        String debugmsg = StringUtils.debugmsg(false);

        if (itemContainer instanceof WebElement) {
            WebElement container = (WebElement) itemContainer;
            String type = WebDriverGUIUtilities.getLibraryType(container);
            IndependantLog.debug(debugmsg + " itemContainer's library type is '" + type + "'.");
            String classname = WebDriverGUIUtilities.getLibraryPackage() + "." + type;
            IndependantLog.debug(debugmsg + " instantiate class '" + classname + "'.");
            try {
                Class clazz = Class.forName(classname);
                Constructor<WebElement> cons = clazz.getConstructor(WebElement.class);
                Component component = (Component) cons.newInstance(container);

                Element element = component.getMatchedElement(criterion);
                element.refresh(false);//we must refresh to get the latest WebElement
                return element.getWebElement();
            } catch (Exception e) {
                IndependantLog.error(debugmsg + " Met " + StringUtils.debugmsg(e));
            }
        } else {
            IndependantLog.error(debugmsg + " itemContainer is not WebElement, cannot handle.");
        }

        return null;
    }

    /**
     * According to xpath and qualifiers to find the WebElement.<br>
     * @param sc SearchContext, the search context, it is normally a WebElement.
     * @param xpathStr String, the xpath used to find a WebElment
     * @param qualifiers HashMap<String, String>, qualifiers and their values used to find a WebElement
     * @return WebElement
     */
    protected static WebElement getObjectByXpathAndQualifiers(SearchContext sc, String xpathStr,
            HashMap<String, String> qualifiers) {
        WebElement obj = null;
        String debugmsg = StringUtils.debugmsg(false);

        try {
            //Handle the qualifiers like "Index", "ItemIndex", "Path" etc.
            //"Index" has the most priority, will be handled firstly
            //Find the WebElement according to xpath, and the possible qualifier 'Index'
            String xpath = String.format(xpathStr);
            IndependantLog.debug(debugmsg + " handling xpath '" + xpath + "'");
            IndependantLog.debug(debugmsg + " qualifiers are: " + qualifiers);
            if (qualifiers.containsKey(SEARCH_CRITERIA_INDEX)) {
                List<WebElement> preMatches = sc.findElements(By.xpath(xpath));
                int matchindex = StringUtilities.parseIndex(qualifiers.get(SEARCH_CRITERIA_INDEX));//1-based
                String typeInSAFS = qualifiers.containsKey(SEARCH_CRITERIA_TYPE)
                        ? qualifiers.get(SEARCH_CRITERIA_TYPE)
                        : null;
                IndependantLog.debug(debugmsg + " handling index='" + matchindex + "' type='" + typeInSAFS + "'");
                obj = getNthWebElement(preMatches, matchindex, typeInSAFS);
            } else {
                obj = sc.findElement(By.xpath(xpath));
            }

            //Handle the other qualifiers "ItemIndex", "Path"
            if (obj != null && !qualifiers.isEmpty()) {
                TextMatchingCriterion criterion = null;
                if (qualifiers.containsKey(SEARCH_CRITERIA_ITEMINDEX)) {
                    String itemIndex = qualifiers.get(SEARCH_CRITERIA_ITEMINDEX);//0-based
                    IndependantLog.debug(debugmsg + " handling ItemIndex='" + itemIndex + "'");
                    criterion = new TextMatchingCriterion(Integer.parseInt(itemIndex));

                } else if (qualifiers.containsKey(SEARCH_CRITERIA_PATH)) {
                    String path = qualifiers.get(SEARCH_CRITERIA_PATH);
                    //TODO HANDLE the pathIndex "4->2->3", "Path=A->B;PathIndex=3->2"
                    //TODO HANDLE the Index in Path "4->A->D", "Path=4->A->D"
                    IndependantLog.debug(debugmsg + " handling Path='" + path + "'");
                    criterion = new TextMatchingCriterion(path, false, (String) null);
                }
                obj = criterion == null ? obj : getSubItem(obj, criterion);
            }
        } catch (Exception e) {
            IndependantLog.error(debugmsg + " met " + StringUtils.debugmsg(e));
        }

        return obj;
    }

    /**
     * According to xpath and qualifiers to find matching WebElements.<br>
     * While returning a List, it is likely the List will have only 1 matching item.
     * @param sc SearchContext, the search context, it is normally a WebElement.
     * @param xpathStr String, the xpath used to find matching WebElements
     * @param qualifiers HashMap<String, String>, qualifiers and their values used to find matching WebElement subItems.
     * @return List&lt;WebElement> 0 or more WebElements.
     */
    protected static List<WebElement> getObjectsByXpathAndQualifiers(SearchContext sc, String xpathStr,
            HashMap<String, String> qualifiers) {
        List<WebElement> list = new ArrayList<WebElement>();
        WebElement obj = null;
        String debugmsg = StringUtils.debugmsg(false);

        try {
            //Handle the qualifiers like "Index", "ItemIndex", "Path" etc.
            //"Index" has the most priority, will be handled firstly
            //Find the WebElement according to xpath, and the possible qualifier 'Index'
            String xpath = String.format(xpathStr);
            IndependantLog.debug(debugmsg + " handling xpath '" + xpath + "'");
            IndependantLog.debug(debugmsg + " qualifiers are: " + qualifiers);
            List<WebElement> preMatches = new ArrayList<WebElement>();
            if (qualifiers.containsKey(SEARCH_CRITERIA_INDEX)) {
                preMatches = sc.findElements(By.xpath(xpath));
                int matchindex = StringUtilities.parseIndex(qualifiers.get(SEARCH_CRITERIA_INDEX));//1-based
                String typeInSAFS = qualifiers.containsKey(SEARCH_CRITERIA_TYPE)
                        ? qualifiers.get(SEARCH_CRITERIA_TYPE)
                        : null;
                IndependantLog.debug(debugmsg + " handling index='" + matchindex + "' type='" + typeInSAFS + "'");
                obj = getNthWebElement(preMatches, matchindex, typeInSAFS);
                preMatches.clear();
                if (obj != null) {
                    preMatches.add(obj);
                }
            } else {
                preMatches = sc.findElements(By.xpath(xpath));
            }

            if (preMatches.isEmpty())
                return list; // return empty list

            //Handle the other qualifiers "ItemIndex", "Path"
            if (!qualifiers.isEmpty()) {
                for (int i = 0; i < preMatches.size(); i++) {
                    obj = preMatches.get(i);
                    TextMatchingCriterion criterion = null;
                    if (qualifiers.containsKey(SEARCH_CRITERIA_ITEMINDEX)) {
                        String itemIndex = qualifiers.get(SEARCH_CRITERIA_ITEMINDEX);//0-based
                        IndependantLog.debug(debugmsg + " handling ItemIndex='" + itemIndex + "'");
                        criterion = new TextMatchingCriterion(Integer.parseInt(itemIndex));

                    } else if (qualifiers.containsKey(SEARCH_CRITERIA_PATH)) {
                        String path = qualifiers.get(SEARCH_CRITERIA_PATH);
                        //TODO HANDLE the pathIndex "4->2->3", "Path=A->B;PathIndex=3->2"
                        //TODO HANDLE the Index in Path "4->A->D", "Path=4->A->D"
                        IndependantLog.debug(debugmsg + " handling Path='" + path + "'");
                        criterion = new TextMatchingCriterion(path, false, (String) null);
                    }
                    if (criterion != null) {
                        // TODO would like this to potentially return a List of matched SubItems.
                        obj = getSubItem(obj, criterion);
                        if (obj != null) {
                            list.add(obj);
                        }
                    }
                }
            } else {
                list = preMatches;
            }
        } catch (Exception e) {
            IndependantLog.error(debugmsg + " met " + StringUtils.debugmsg(e));
        }
        return list;
    }

    /**
     * Find the Nth matched WebElement in a list.<br>
     * @param preMatches List&lt;WebElement>, a list of WebElment from where to find a matched Element
     * @param matchindex int, the expected index, 1-based
     * @param typeInSAFS String, the expected type, like "Button", "List", "CheckBox". If null, match all elements.
     * @return WebElement, the Nth matched WebElement. null if not found.
     */
    protected static WebElement getNthWebElement(List<WebElement> preMatches, int matchindex, String typeInSAFS) {
        String debugmsg = StringUtils.debugmsg(false);
        WebElement matchedElement = null;

        boolean matched = false;
        if (preMatches != null && !preMatches.isEmpty()) {
            IndependantLog.debug(debugmsg + "found " + preMatches.size()
                    + " matching elements to process for type: " + typeInSAFS);

            int index = 0;
            for (int n = 0; n < preMatches.size(); n++) {
                matched = false;
                matchedElement = preMatches.get(n);
                if (matchedElement.isDisplayed()) {
                    matched = (typeInSAFS == null ? true
                            : WebDriverGUIUtilities.isTypeMatched(matchedElement, typeInSAFS));
                    if (matched) {
                        String id = matchedElement.getAttribute("id");
                        index++;
                        IndependantLog.debug(debugmsg + "found matching element[" + n + "] as INDEX " + index
                                + " with ID: " + id);
                        if (index == matchindex) {
                            IndependantLog.debug(debugmsg + "returning matching element[" + n + "] as INDEX "
                                    + index + " with ID: " + id);
                            return matchedElement;
                        }
                    } else {
                        IndependantLog.debug(debugmsg + "element[" + n + "] does not match expected type: "
                                + typeInSAFS + ". It does not count as a match.");
                    }
                } else {
                    IndependantLog.debug(debugmsg + "element[" + n
                            + "].isDisplayed == false. This does not count towards visible matches.");
                }
            }
        }

        return null;
    }

    /**
     * Alternate search for special framework elements like Dojo or SAP OpenUI5 components.
     * @param sc SearchContext, the search context
     * @param RS String, the recognition string
     * @param isPropertyAllMode boolean, if true, each part of 'recognition string' will be treated as property.<br>
     *                                   if false, some reserved words will be treated as qualifiers, but not properties.<br>
     *                                   the reserved words are {@link #SEARCH_CRITERIA_ITEMINDEX}, {@link #SEARCH_CRITERIA_PATH} etc.<br>
     * @return WebElement or null if no match found.
     */
    protected static WebElement getObjectForDomain(SearchContext sc, String RS, boolean isPropertyAllMode) {
        //search the target element within a frame (if exist) level by level
        WebElement obj = null;
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "processing recognition fragment: " + RS);
        String[] tokens = StringUtils.getTokenArray(RS, qulifierSeparator, escapeChar);
        String xpathStr = XPATH.RELATIVE_MATCHING_ALL_START + XPATH.TRUE_CONDITION;
        boolean isDojo = false;
        boolean isSAP = false;

        HashMap<String, String> qualifiers = new HashMap<String, String>();
        String xpathCondition = null;

        try {
            String criteria = null;
            String value = null;
            int i = -1;
            for (String prop : tokens) {
                criteria = null;
                value = null;
                i = prop.indexOf(assignSeparator);
                if (i > 0) {
                    criteria = prop.substring(0, i);
                    if (prop.length() > i)
                        value = prop.substring(i + 1);
                } else {
                    IndependantLog.warn(debugmsg + " RS '" + prop + "' doesn't contain '='. Just ignore it.");
                    continue;
                }

                if (SEARCH_CRITERIA_TYPE.equalsIgnoreCase(criteria)) {
                    if (value.toUpperCase().startsWith(DOMAIN_DOJO)) {
                        // current DOJO always uses DIV as the high-level widget element
                        xpathStr = XPATH.RELATIVE_MATCHING_DIV_START + XPATH.TRUE_CONDITION; // DOJO components all DIVs ?
                        isDojo = true;
                    } else if (value.toUpperCase().startsWith(DOMAIN_SAP)) {
                        xpathStr = XPATH.RELATIVE_MATCHING_ALL_START + XPATH.TRUE_CONDITION; // any element?
                        isSAP = true;
                    }
                    qualifiers.put(SEARCH_CRITERIA_TYPE, value);

                } else {
                    if (isPropertyAllMode) {
                        //all are treated as component's properties
                        IndependantLog.debug(debugmsg + "add search condition @" + criteria + "='" + value + "'");
                        xpathStr += " " + XPATH.AND + XPATH.condition(criteria, value, false);
                    } else {
                        //handle some 'reserved qualifiers'
                        if (SEARCH_CRITERIA_ID.equalsIgnoreCase(criteria)) {
                            xpathStr += isDojo
                                    ? " and (starts-with(@id,'" + value + "') or "
                                            + "(substring(@id, string-length(@id) - string-length('" + value
                                            + "')+1)='" + value + "'))"
                                    : /* SAP */ " and @id='" + value + "'";

                        } else if (SEARCH_CRITERIA_NAME.equalsIgnoreCase(criteria)) {
                            xpathStr += isDojo ? /* TODO: do dojo names concat like their ids do? need to check. */
                                    " and (starts-with(@name,'" + value + "') or "
                                            + "(substring(@name, string-length(@name) - string-length('" + value
                                            + "')+1)='" + value + "'))"
                                    : /* SAP */ " and @name='" + value + "'";

                        } else if (SEARCH_CRITERIA_TITLE.equalsIgnoreCase(criteria)) {
                            xpathStr += " and @title='" + value + "'";

                        } else if (SEARCH_CRITERIA_TEXT.equalsIgnoreCase(criteria)) {
                            xpathStr += " and .='" + value + "'";

                        } else if (SEARCH_CRITERIA_INDEX.equalsIgnoreCase(criteria)
                                || SEARCH_CRITERIA_PATH.equalsIgnoreCase(criteria)
                                || SEARCH_CRITERIA_ITEMINDEX.equalsIgnoreCase(criteria)) {
                            qualifiers.put(criteria.toUpperCase(), value);

                        } else if (SEARCH_CRITERIA_PROPERTY.equalsIgnoreCase(criteria)) {
                            IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                            xpathCondition = getXpathCondition(value, false);
                            if (xpathCondition != null)
                                xpathStr += " " + XPATH.AND + xpathCondition;

                        } else if (SEARCH_CRITERIA_PROPERTY_CONTAINS.equalsIgnoreCase(criteria)) {
                            IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                            xpathCondition = getXpathCondition(value, true);
                            if (xpathCondition != null)
                                xpathStr += " " + XPATH.AND + xpathCondition;

                        } else {
                            //the others will be simply considered as properties
                            IndependantLog
                                    .debug(debugmsg + "add search condition @" + criteria + "='" + value + "'");
                            xpathStr += " " + XPATH.AND + XPATH.condition(criteria, value, false);
                        }
                    } //end if isPropertyAllMode
                } //end if SEARCH_CRITERIA_TYPE
            } //end for tokens

            xpathStr += XPATH.END;

            obj = getObjectByXpathAndQualifiers(sc, xpathStr, qualifiers);

        } catch (Throwable t) {
            IndependantLog.debug(debugmsg + "recognition string parsing error: " + t.getClass().getSimpleName()
                    + ": " + t.getMessage());
        }
        if (obj == null)
            IndependantLog.debug(debugmsg + "did not find any matching elements.");
        return obj;
    }

    /**
     * Alternate search for special framework elements like Dojo or SAP OpenUI5 components.
     * @param sc SearchContext, the search context
     * @param RS String, the recognition string
     * @param isPropertyAllMode boolean, if true, each part of 'recognition string' will be treated as property.<br>
     *                                   if false, some reserved words will be treated as qualifiers, but not properties.<br>
     *                                   the reserved words are {@link #SEARCH_CRITERIA_ITEMINDEX}, {@link #SEARCH_CRITERIA_PATH} etc.<br>
     * @return List&lt;WebElement> 0 or more matching WebElements.
     */
    protected static List<WebElement> getObjectsForDomain(SearchContext sc, String RS, boolean isPropertyAllMode) {
        //search the target element within a frame (if exist) level by level

        List<WebElement> list = new ArrayList<WebElement>();
        WebElement obj = null;
        String debugmsg = StringUtils.debugmsg(false);
        IndependantLog.debug(debugmsg + "processing recognition fragment: " + RS);
        String[] tokens = StringUtils.getTokenArray(RS, qulifierSeparator, escapeChar);
        String xpathStr = XPATH.RELATIVE_MATCHING_ALL_START + XPATH.TRUE_CONDITION;
        boolean isDojo = false;
        boolean isSAP = false;

        HashMap<String, String> qualifiers = new HashMap<String, String>();
        String xpathCondition = null;

        try {
            String criteria = null;
            String value = null;
            int i = -1;
            for (String prop : tokens) {
                criteria = null;
                value = null;
                i = prop.indexOf(assignSeparator);
                if (i > 0) {
                    criteria = prop.substring(0, i);
                    if (prop.length() > i)
                        value = prop.substring(i + 1);
                } else {
                    IndependantLog.warn(debugmsg + " RS '" + prop + "' doesn't contain '='. Just ignore it.");
                    continue;
                }

                if (SEARCH_CRITERIA_TYPE.equalsIgnoreCase(criteria)) {
                    if (value.toUpperCase().startsWith(DOMAIN_DOJO)) {
                        // current DOJO always uses DIV as the high-level widget element
                        xpathStr = XPATH.RELATIVE_MATCHING_DIV_START + XPATH.TRUE_CONDITION; // DOJO components all DIVs ?
                        isDojo = true;
                    } else if (value.toUpperCase().startsWith(DOMAIN_SAP)) {
                        xpathStr = XPATH.RELATIVE_MATCHING_ALL_START + XPATH.TRUE_CONDITION; // any element?
                        isSAP = true;
                    }
                    qualifiers.put(SEARCH_CRITERIA_TYPE, value);

                } else {
                    if (isPropertyAllMode) {
                        //all are treated as component's properties
                        IndependantLog.debug(debugmsg + "add search condition @" + criteria + "='" + value + "'");
                        xpathStr += " " + XPATH.AND + XPATH.condition(criteria, value, false);
                    } else {
                        //handle some 'reserved qualifiers'
                        if (SEARCH_CRITERIA_ID.equalsIgnoreCase(criteria)) {
                            xpathStr += isDojo
                                    ? " and (starts-with(@id,'" + value + "') or "
                                            + "(substring(@id, string-length(@id) - string-length('" + value
                                            + "')+1)='" + value + "'))"
                                    : /* SAP */ " and @id='" + value + "'";

                        } else if (SEARCH_CRITERIA_NAME.equalsIgnoreCase(criteria)) {
                            xpathStr += isDojo ? /* TODO: do dojo names concat like their ids do? need to check. */
                                    " and (starts-with(@name,'" + value + "') or "
                                            + "(substring(@name, string-length(@name) - string-length('" + value
                                            + "')+1)='" + value + "'))"
                                    : /* SAP */ " and @name='" + value + "'";

                        } else if (SEARCH_CRITERIA_TITLE.equalsIgnoreCase(criteria)) {
                            xpathStr += " and @title='" + value + "'";

                        } else if (SEARCH_CRITERIA_TEXT.equalsIgnoreCase(criteria)) {
                            xpathStr += " and .='" + value + "'";

                        } else if (SEARCH_CRITERIA_INDEX.equalsIgnoreCase(criteria)
                                || SEARCH_CRITERIA_PATH.equalsIgnoreCase(criteria)
                                || SEARCH_CRITERIA_ITEMINDEX.equalsIgnoreCase(criteria)) {
                            qualifiers.put(criteria.toUpperCase(), value);

                        } else if (SEARCH_CRITERIA_PROPERTY.equalsIgnoreCase(criteria)) {
                            IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                            xpathCondition = getXpathCondition(value, false);
                            if (xpathCondition != null)
                                xpathStr += " " + XPATH.AND + xpathCondition;

                        } else if (SEARCH_CRITERIA_PROPERTY_CONTAINS.equalsIgnoreCase(criteria)) {
                            IndependantLog.debug(debugmsg + "add search condition '" + value + "'");
                            xpathCondition = getXpathCondition(value, true);
                            if (xpathCondition != null)
                                xpathStr += " " + XPATH.AND + xpathCondition;

                        } else {
                            //the others will be simply considered as properties
                            IndependantLog
                                    .debug(debugmsg + "add search condition @" + criteria + "='" + value + "'");
                            xpathStr += " " + XPATH.AND + XPATH.condition(criteria, value, false);
                        }
                    } //end if isPropertyAllMode
                } //end if SEARCH_CRITERIA_TYPE
            } //end for tokens

            xpathStr += XPATH.END;

            list = getObjectsByXpathAndQualifiers(sc, xpathStr, qualifiers);

        } catch (Throwable t) {
            IndependantLog.debug(debugmsg + "recognition string parsing error: " + t.getClass().getSimpleName()
                    + ": " + t.getMessage());
        }
        if (list == null || list.isEmpty())
            IndependantLog.debug(debugmsg + "did not find any matching elements.");
        return list;
    }

    /**
     * @param propertyValuePairStr String, the pair representing property and value, like property:value, separated by {@link #propertySeparator}
     * @param partialMatch boolean, if the property's value will be matched partially (considered as a substring)
     * @return String, the xpath condition for searching
     * @see #getXpathCondition(String, String, boolean)
     */
    protected static String getXpathCondition(String propertyValuePairStr, boolean partialMatch) {
        String[] propertyValuePair = StringUtils.getTokenArray(propertyValuePairStr, propertySeparator, escapeChar);
        if (propertyValuePair == null || propertyValuePair.length < 2) {
            IndependantLog.warn(StringUtils.debugmsg(false) + " the property-value pair '" + propertyValuePairStr
                    + "' is invalid! ignore it.");
            return null;
        }

        return XPATH.condition(propertyValuePair[0], propertyValuePair[1], partialMatch);
    }

    /**
     * Get a boolean value from the WebElement according to the attributes.<br>
     * @param webelement WebElement, the WebElement to get value of an attribute.<br>
     * @param attributes String..., an array of attribute to get value for.
     * @return boolean true if one of the attribute contains true value.
     */
    public static boolean getBoolean(WebElement webelement, String... attributes) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getBoolean");
        boolean result = false;

        try {
            for (String attribute : attributes) {
                result = StringUtilities.convertBool(webelement.getAttribute(attribute));
                if (result)
                    break;
            }
        } catch (Throwable th) {
            IndependantLog.warn(debugmsg + "cannot get a true value, due to " + StringUtils.debugmsg(th));
        }
        return result;
    }

    /**
     * Attempts to retrieve a text value for the WebElement.
     * This is by combining webelement.getText() and our own getValue() and returning whichever gets 
     * us a text value.
     * @param webelement WebElement, the web element to get text from.
     * @return String, the element's text content
     */
    public static String getText(WebElement webelement) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getText");
        String tt = webelement.getText();
        String t = tt == null ? "" : tt;
        Object ov = getValue(webelement, TEXT_VALUE_ATTRIBUTES);
        String v = ov == null ? "" : ov.toString();
        IndependantLog.info(debugmsg + "getText()  received: " + t);
        IndependantLog.info(debugmsg + "getValue() received: " + v);
        String rc = t.length() > 0 ? t : v;
        IndependantLog.info(debugmsg + "returning: " + rc);
        return rc;
    }

    /**
     * Get a value from the WebElement according to the attributes.<br>
     * The first non-null-value of the attributes will be returned, so the order<br>
     * of attributes may affect the result.<br>
     * 
     * @param webelement WebElement, the WebElement to get value of an attribute.<br>
     * @param attributes String..., an array of attribute to get value for.
     * @return Object the first non-null-value of the attributes; or null if all attributes have null value.
     * 
     * @see #TEXT_VALUE_ATTRIBUTES
     */
    public static Object getValue(WebElement webelement, String... attributes) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "getValue");
        Object result = null;

        try {
            for (String attribute : attributes) {
                try {
                    result = WDLibrary.getProperty(webelement, attribute);
                    if (result != null)
                        break;
                } catch (SeleniumPlusException e) {
                    //IndependantLog.warn(debugmsg+" fail to get '"+attribute+"', due to "+StringUtils.debugmsg(e));
                }
            }
        } catch (Throwable th) {
            IndependantLog.error(debugmsg + "cannot get a value, due to " + StringUtils.debugmsg(th));
        }
        return result;
    }

    /**
     * Get the boolean value of a javascript global variable.
     * @param variable String, the name of the global variable.
     * @return boolean, the value of javascript global variable.
     * @throws IllegalStateException if the value is NOT present or attainable.
     * @see #js_getGlobalVariable(String)
     */
    public static boolean js_getGlobalBoolVariable(String variable) throws IllegalStateException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_getGlobalBoolVariable");
        Object result = js_getGlobalVariable(variable);
        if (result instanceof Boolean) {
            return ((Boolean) result).booleanValue();
        } else {
            IndependantLog.debug(debugmsg + " did NOT return Boolean value!");
            throw new IllegalStateException("did NOT return Boolean value!");
        }
    }

    private static Object js_result = null;
    private static String js_code = null;

    protected static Object js_executeWithTimeout(String js, long msTimeout)
            throws SeleniumPlusException, InterruptedException {
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    js_result = getJS().executeScript(js_code);
                } catch (org.openqa.selenium.UnhandledAlertException x) {
                    IndependantLog.warn(StringUtils.debugmsg(SearchObject.class, "js_executeWithTimeout")
                            + " UnhandledAlertException: " + x);
                } catch (SeleniumPlusException x) {
                    throw new RuntimeException(x);
                }
            }
        });
        js_result = null;
        js_code = js;
        t.setDaemon(true);
        t.start();
        t.join(msTimeout);
        if (t.isAlive()) {
            String error = "Javascript execution timeout '" + msTimeout
                    + "' millisecnods has been reached! Please use a larger timeout.";
            IndependantLog.error("SearchObject.js_executeWithTimeout(): " + error);
            throw new SeleniumPlusException(error);
        }

        return js_result;
    }

    /**
     * Get the value of a javascript global variable.
     * @param variable String, the name of the global variable.
     * @return Object, the value of javascript global variable.
     * @throws IllegalStateException if the value is NOT present or attainable.
     */
    public static Object js_getGlobalVariable(String variable) {
        String debugmsg = StringUtils.debugmsg(false);
        Object result = null;
        try {
            if (!StringUtils.isValid(variable)) {
                IndependantLog.error(debugmsg + " parameter variable is not valid!");
                return null;
            }
            // TODO CANAGL
            //result = getJS().executeScript(JavaScriptFunctions.getGlobalVariable(variable));
            result = js_executeWithTimeout(JavaScriptFunctions.getGlobalVariable(variable), 1000);
            if (result == null) {
                IndependantLog.error(debugmsg + "The js returned result is null.");
            }
        } catch (Throwable ignore) {
            IndependantLog.error(debugmsg, ignore);
        }
        return result;
    }

    /**
     * Get the value for a javascript global boolean variable.
     * @param variable String, the name of the global variable.
     * @param onOrOff boolean, the value to set
     * @see #js_setGlobalVariable(String, String)
     */
    public static void js_setGlobalBoolVariable(String variable, boolean onOrOff) {
        js_setGlobalVariable(variable, Boolean.toString(onOrOff));
    }

    /**
     * Set the value for a javascript global variable.
     * @param variable String, the name of the global variable.
     * @param value String, the value to set
     */
    public static void js_setGlobalVariable(String variable, String value) {
        String debugmsg = StringUtils.debugmsg(false);
        try {
            if (!StringUtils.isValid(variable)) {
                IndependantLog.error(debugmsg + " parameter variable is not valid!");
                return;
            }
            getJS().executeScript(JavaScriptFunctions.setGlobalVariable(variable, value));
        } catch (Throwable ignore) {
            IndependantLog.error(debugmsg, ignore);
        }
    }

    /**
     * Execute javascript to initialize global javascript variables.<br>
     * @see WDLibrary#startBrowser(String, String, String, int, boolean, HashMap)
     */
    protected static void js_initialize() {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_initialize");
        StringBuffer scriptCommand = new StringBuffer();
        scriptCommand.append(JavaScriptFunctions.initPreviousHighlightElement());
        scriptCommand.append(JavaScriptFunctions.initJSError());
        scriptCommand.append(JavaScriptFunctions.initJSDebugArray());
        try {
            getJS().executeScript(scriptCommand.toString());
        } catch (Throwable e) {
            IndependantLog.error(debugmsg + " Failed.", e);
        }
    }

    /**
     * Initialize the container to hold the debug message. This should be called before<br>
     * executing a snippet of javascript<br>
     * @throws SeleniumPlusException
     * @see {@link #executeScript(boolean, String, Object...)}
     */
    private static void js_initJSDebugArray() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        try {
            getJS().executeScript(JavaScriptFunctions.initJSDebugArray());
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Get the possible debug messages during executing a snippet of javascript.
     * @return
     * @throws SeleniumPlusException
     * @see {@link #executeScript(boolean, String, Object...)}
     */
    private static List<?> js_getJSDebugArray() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        try {
            Object debugmessages = getJS().executeScript(JavaScriptFunctions.getJSDebugArray());
            if (debugmessages != null) {
                if (debugmessages instanceof List) {
                    return ((List<?>) debugmessages);
                } else {
                    IndependantLog.debug(debugmsg + " Don't know how to analyse debug message of type "
                            + debugmessages.getClass().getName());
                }
            }
            return new ArrayList<Object>();
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Initialize the global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_CODE_VAR},<br>
     * set it value to {@link JavaScriptFunctions#ERROR_CODE_NOT_SET}. <br>
     * Initialize the global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_OBJECT_VAR} to undefined.<br>
     */
    public static void js_initError() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_initError");
        try {
            getJS().executeScript(JavaScriptFunctions.initJSError());
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Clean the global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_CODE_VAR},<br>
     * reset it value to {@link JavaScriptFunctions#ERROR_CODE_NOT_SET}. <br>
     * Clean the global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_OBJECT_VAR}, reset it to undefined.<br>
     */
    public static void js_cleanError() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_cleanError");
        try {
            getJS().executeScript(JavaScriptFunctions.cleanJSError());
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Get the value of global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_CODE_VAR}.<br>
     */
    public static int js_getErrorCode() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_getErrorCode");
        try {
            //         Object object = getJS().executeScript(JavaScriptFunctions.getJSErrorCode());
            Object object = js_executeWithTimeout(JavaScriptFunctions.getJSErrorCode(),
                    5000/*give 5 seconds, it needs more time to get work done with Firefox*/);
            return ((Long) object).intValue();
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Set the value of global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_CODE_VAR}<br>
     */
    public static void js_setErrorCode(int error) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "js_setErrorCode");
        try {
            getJS().executeScript(JavaScriptFunctions.setJSErrorCode(error));
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Get the value of global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_OBJECT_VAR}.<br>
     */
    public static Object js_getErrorObject() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        try {
            //         Object object = getJS().executeScript(JavaScriptFunctions.getJSErrorObject());
            Object object = js_executeWithTimeout(JavaScriptFunctions.getJSErrorObject(),
                    5000/*give 5 seconds, the error object may be big and takes more time to return*/);
            return object;
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Set a string value to global variable {@link JavaScriptFunctions#SAFS_JAVASCRIPT_GLOBAL_ERROR_OBJECT_VAR}<br>
     */
    public static void js_setErrorObject(String errorMessage) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        try {
            getJS().executeScript(JavaScriptFunctions.setJSErrorObject(StringUtils.quote(errorMessage)));
        } catch (SeleniumPlusException e) {
            throw e;
        } catch (Throwable th) {
            throw new SeleniumPlusException(debugmsg, th);
        }
    }

    /**
     * Highlight the webelement by drawing a red rectangle around it.<br>
     * @param webelement WebElement, the web element to highlight.
     * @param duration   int, how long in millisecond the red rectangle will stay.
     * @return boolean, true if the element is highlighted and then cleaned.
     * @see #highlight(WebElement)
     * @see #clearHighlight()
     */
    public static boolean highlightThenClear(WebElement webelement, int duration) {
        if (WDLibrary.highlight(webelement)) {
            StringUtilities.sleep(duration);
            return WDLibrary.clearHighlight();
        } else {
            return false;
        }
    }

    /**
     * Highlight the webelement by drawing a red rectangle around it.<br>
     * <p>
     * @param idOrNameOrXpath   String, the id or name or xpath of web element to highlight.
     * @return boolean, true if the element is highlighted.
     */
    public static boolean highlight(String idOrNameOrXpath) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "highlight");

        if (idOrNameOrXpath == null || idOrNameOrXpath.isEmpty()) {
            IndependantLog.error(
                    debugmsg + "'" + idOrNameOrXpath + "' cannot be used to search web element, cannot highlight.");
            return false;
        } else {
            try {
                StringBuffer scriptCommand = new StringBuffer();
                scriptCommand.append(JavaScriptFunctions.getHighlightFunction(true));
                scriptCommand.append("highlight(arguments[0]);");
                WDLibrary.executeScript(scriptCommand.toString(), idOrNameOrXpath);
                return true;
            } catch (SeleniumPlusException e) {
                IndependantLog.error(debugmsg + " Failed.", e);
                return false;
            }
        }
    }

    /**
     * Highlight the webelement by drawing a red rectangle around it.<br>
     * To clean the red rectangle, please call {@link #clearHighlight()}.<br>
     * @param webelement   WebElement, the web element to highlight.
     * @return boolean, true if the element is highlighted.
     * @see #clearHighlight()
     */
    public static boolean highlight(WebElement webelement) {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "highlight");

        try {
            if (!isDisplayed(webelement)) {
                IndependantLog.error(debugmsg + "webelement is not visible, cannot be highlighted.");
                return false;
            }
        } catch (SeleniumPlusException e) {
            IndependantLog.error(debugmsg + " met exception during checking element's visibility.", e);
            return false;
        }

        try {
            StringBuffer scriptCommand = new StringBuffer();
            scriptCommand.append(JavaScriptFunctions.highlight2());
            scriptCommand.append("highlight2(arguments[0]);");
            WDLibrary.executeScript(scriptCommand.toString(), webelement);
            return true;
        } catch (SeleniumPlusException e) {
            IndependantLog.error(debugmsg + " Failed.", e);
            return false;
        }
    }

    /**
     * clear the previous highlight of a webelement.<br>
     * <b>Note:</b>The webelement should be highlighted by {@link #highlight(String)}<br>
     * or {@link #highlight(WebElement)}, normally you don't need to call this method<br>
     * to clear the highlight if you want to highlight element one by one continuously, as <br>
     * the previous highlighted element will be automatically cleared before next element<br>
     * is highlighted.<br>
     *
     * @return boolean, true if the element's highlight has been cleared.
     *
     * @see #highlight(String)
     * @see #highlight(WebElement)
     */
    public static boolean clearHighlight() {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "clearHighlight");

        try {
            StringBuffer scriptCommand = new StringBuffer();
            scriptCommand.append(JavaScriptFunctions.clearHighlight());
            scriptCommand.append("clearHighlight();");
            WDLibrary.executeScript(scriptCommand.toString());
            return true;
        } catch (SeleniumPlusException e) {
            IndependantLog.error(debugmsg + " Failed.", e);
            return false;
        }
    }

    /**
     * Get the visibility of the webelement.<br>
     * Try Selenium's API isDisplayed() firstly, if it is false then try to test value of attribute 'visibility'.<br>
     * @param element WebElement
     * @return true if the webelement is visible
     * @throws SeleniumPlusException if the WebElement is null
     * @see #isDisplayed(WebElement)
     */
    public static boolean isVisible(WebElement element) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        boolean visible = false;

        checkNotNull(element);

        try {
            WDTimeOut.implicitlyWait(0, TimeUnit.SECONDS);
            visible = element.isDisplayed();
        } catch (StaleElementReferenceException se) {
            //If the webelement is stale, we should consider it as invisible.
            return false;
        } catch (Exception e) {
            IndependantLog.warn(debugmsg + " Met " + StringUtils.debugmsg(e));
        } finally {
            WDTimeOut.implicitlyWait(Processor.getSecsWaitForComponent(), TimeUnit.SECONDS);
        }

        if (!visible) {
            try {
                //if element is still invisible, we will check the value of attribute 'visibility'
                //if the value is not 'hidden', we consider the element is visible;
                //BUT, if isDisplayed() return false, the calling of Selenium's
                //API click() will throw org.openqa.selenium.ElementNotVisibleException, maybe calling
                //other APIs will throw also ElementNotVisibleException, this is the risk.
                String visibility = WDLibrary.getProperty(element, Component.ATTRIBUTE_VISIBILITY);
                IndependantLog.debug(debugmsg + "visibility is '" + visibility + "'");
                visible = !Component.VALUE_VISIBILITY_HIDDEN.equalsIgnoreCase(visibility);
            } catch (SeleniumPlusException e) {
                IndependantLog.warn(debugmsg + "Fail to check property '" + Component.ATTRIBUTE_VISIBILITY + "'");
            }
        }

        return visible;
    }

    /**
     * Check if the webelement is displayed (Selenium's API isDisplayed()).<br>
     * @param element WebElement, the element to check.
     * @return true if the webelement is visible
     * @throws SeleniumPlusException if the WebElement is null or is stale
     * @see #isVisible(WebElement)
     */
    public static boolean isDisplayed(WebElement element) throws SeleniumPlusException {
        checkNotNull(element);
        try {
            WDTimeOut.implicitlyWait(0, TimeUnit.SECONDS);
            return element.isDisplayed();
        } catch (StaleElementReferenceException se) {
            throw new SeleniumPlusException("weblement is stale.");
        } catch (Exception e) {
            throw new SeleniumPlusException(StringUtils.debugmsg(e));
        } finally {
            WDTimeOut.implicitlyWait(Processor.getSecsWaitForComponent(), TimeUnit.SECONDS);
        }
    }

    private static boolean __isStale = false;
    private static Throwable __isStaleException = null;
    private static WebElement __isStaleElement = null;

    /**
     * Test if a WebElement is stale(DOM element is deleted or is repaint or disappear).
     * @param element WebElement, the WebElement to detect.
     * @return boolean true if the WebElement is stale.
     * @throws SeleniumPlusException if the element is null.
     */
    public static boolean isStale(WebElement element) throws SeleniumPlusException {
        __isStaleElement = element;
        checkNotNull(element);
        __isStale = false;
        __isStaleException = null;
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    //WebDriver will not throw StaleElementReferenceException until the 'implicit timeout' is reached.
                    //We don't want to waste that time, so just set 'implicit timeout' to 0 and don't wait.
                    WDTimeOut.implicitlyWait(0, TimeUnit.SECONDS);
                    __isStaleElement.isDisplayed();
                    __isStale = false;
                } catch (StaleElementReferenceException sere) {
                    __isStale = true;
                } catch (Throwable e) {
                    __isStaleException = e;
                } finally {
                    WDTimeOut.implicitlyWait(Processor.getSecsWaitForComponent(), TimeUnit.SECONDS);
                }
            }
        });
        t.setDaemon(true);
        t.start();
        try {
            t.join(600);
        } catch (InterruptedException x) {
            IndependantLog.debug("SearchObject.isStale() execute wait Interrupted?");
            __isStaleException = x; //CANAGL -- Evaluate if we want this Exception thrown below
        }
        if (__isStaleException instanceof Throwable)
            throw new SeleniumPlusException("SearchObject.isStale() " + StringUtils.debugmsg(__isStaleException));
        return __isStale;
    }

    /**
     * A map containing pairs of (listenerID, listener). The listener will respond to a javasript event.<br>
     */
    protected static Map<String, GenericJSEventListener> jsEventListenerMap = new HashMap<String, GenericJSEventListener>();
    /**
     * A map containing pairs of (listenerID, PollingRunnable). The PollingRunnable will check a javascript<br>
     * variable in a loop, when the variable is true, it will invoke the listener according to the listenerID.<br>
     */
    protected static Map<String, PollingRunnable> jsEventListenerWaitingThreadMap = new HashMap<String, PollingRunnable>();

    /**
     * A generic javascript event listener interface.<br>
     *
     * History:<br>
     *  <br>   Apr 14, 2014    (sbjlwa) Initial release.
     *
     * @see SearchObject#addJavaScriptEventListener(WebElement, String, GenericJSEventListener)
     */
    public interface GenericJSEventListener {
        /**javascript event 'mousedown'*/
        public static final String EVENT_JS_MOUSEDOWN = "mousedown";
        /**javascript event 'mouseup'*/
        public static final String EVENT_JS_MOUSEUP = "mouseup";
        /**javascript event 'click'*/
        public static final String EVENT_JS_click = "click";

        /** Defalut time to wait for the javascript 'mousedown' event, it is 1000 milliseconds */
        public static final int TIMEOUT_WAIT_FOR_EVENT_MOUSEDOWN = 1000;

        public void onEventFired();
    }

    /**
     *
     * A default javascript event listener.<br>
     * Usage example:<br>
     * <pre>
     * String event = "mousedown";
     * int timeout = 1000;//milliseconds
     * DefaultJSEventListener listener = new DefaultJSEventListener(event);
     * //add listener for event
     * String listenerID = WDLibrary.addJavaScriptEventListener(webelement, event, listener);
     * //do some mousedown related work, for example 'click'
     * webelement.click();
     * //wait for event happen
     * if(listener.waitEventFired(timeout)){ //ok, event has been fired.}
     * //remove the listener
     * WDLibrary.removeJavaScriptEventListener(webelement, event, listenerID);
     * </pre>
     *
     * History:<br>
     *  <br>   Apr 15, 2014    (sbjlwa) Initial release.
     *
     * @see SearchObject#addJavaScriptEventListener(WebElement, String, GenericJSEventListener)
     */
    protected static class DefaultJSEventListener implements GenericJSEventListener {
        private boolean eventFired = false;
        private String eventName = "event";

        public synchronized boolean isEventFired() {
            return eventFired;
        }

        public synchronized void setEventFired(boolean eventFired) {
            this.eventFired = eventFired;
            this.notifyAll();
        }

        public String getEventName() {
            return eventName;
        }

        public void setEventName(String eventName) {
            this.eventName = eventName;
        }

        public DefaultJSEventListener() {
        }

        public DefaultJSEventListener(String eventName) {
            this.eventName = eventName;
        }

        public void onEventFired() {
            setEventFired(true);
        }

        /**
         * @param timeout long, time to wait for the event fired. in milliseconds.
         * @return boolean true if the event has been fired within timeout; false otherwise.
         */
        public boolean waitEventFired(long timeout) {
            String debugmsg = StringUtils.debugmsg(getClass(), "waitEventFired");
            synchronized (this) {
                long endTime = System.currentTimeMillis() + timeout;
                while (!isEventFired() && System.currentTimeMillis() < endTime) {
                    try {
                        IndependantLog.debug(debugmsg + "Waitting for " + eventName + " happen.");
                        this.wait(timeout);
                    } catch (InterruptedException e) {
                    }
                }
            }

            return isEventFired();
        }
    }

    /**
     * The PollingRunnable will check a javascript variable in a loop,<br>
     * when the variable is true, it will invoke the listener according to the listenerID.<br>
     *
     * History:<br>
     *  <br>   Apr 14, 2014    (sbjlwa) Initial release.
     */
    private static class PollingRunnable implements Runnable {
        private boolean keepRunning = true;
        private String listenerID = null;

        public PollingRunnable() {
            super();
        }

        public PollingRunnable(String jsVariableName) {
            this.listenerID = jsVariableName;
        }

        public synchronized boolean isKeepRunning() {
            return keepRunning;
        }

        public synchronized void setKeepRunning(boolean keepRunning) {
            this.keepRunning = keepRunning;
        }

        public void run() {
            final String debugmsg = StringUtils.debugmsg(PollingRunnable.class, "run");
            IndependantLog.debug(debugmsg + "pollingThread started for " + listenerID);
            boolean eventfired = false;
            GenericJSEventListener listener = null;
            int pauseTime = 500;//milliseconds
            while (true && isKeepRunning()) {
                try {
                    eventfired = js_getGlobalBoolVariable(listenerID);
                    if (eventfired) {
                        IndependantLog.debug(debugmsg + "Got signal, event has been fired for " + listenerID);
                        listener = jsEventListenerMap.get(listenerID);
                        if (listener != null)
                            listener.onEventFired();
                        //reset the global variable
                        js_setGlobalBoolVariable(listenerID, false);
                    } else {
                        try {
                            Thread.sleep(pauseTime);
                        } catch (InterruptedException ignore) {
                        }
                    }
                } catch (Throwable e) {
                    IndependantLog.error(debugmsg + " " + listenerID + " pollingThread: ", e);
                    break;
                }
            }
            IndependantLog.debug(debugmsg + "pollingThread ended for " + listenerID);
        }
    }

    /**
     *
     * @param element   WebElement, the element to which the listener will be added.
     * @param eventName String, event name to attach this listener.
     * @param listener   GenericJSEventListener, the listener to respond to fired-event.
     * @return String, the ID of this listener. Will be used to remove the listener.
     * @throws SeleniumPlusException
     */
    public static String addJavaScriptEventListener(WebElement element, String eventName,
            GenericJSEventListener listener) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(WDLibrary.class, "addJavaScriptEventListener");
        String listenerID = eventName + "_" + System.currentTimeMillis();

        try {
            //Attach the real javascript event by JavaScriptFunctions, the javascript listener will
            //simply set a 'global variable' to true, that 'global variable' name is "window."+listenerID
            String script = null;
            if (WDLibrary.isSAPDomain(element)) {
                script = JavaScriptFunctions.SAP.sap_ui_core_Control_attachBrowserEvent(true, listenerID);
                script += " sap_ui_core_Control_attachBrowserEvent(arguments[0],arguments[1]);";
                SearchObject.executeJavaScriptOnWebElement(script, element, eventName);
            } else if (WDLibrary.isDojoDomain(element)) {
                script = JavaScriptFunctions.DOJO.dojo_dijit_WidgetBase_on(true, listenerID);
                script += " dojo_dijit_WidgetBase_on(arguments[0],arguments[1]);";
                SearchObject.executeJavaScriptOnWebElement(script, element, eventName);
            } else {//For HTML Standard element
                script = JavaScriptFunctions.addGenericEventListener(true, listenerID);
                script += " addGenericEventListener(arguments[0],arguments[1],arguments[2]);";
                //Do we need to add a capturing listener?
                //            SearchObject.executeJavaScriptOnWebElement(script, element, eventName, true);
                SearchObject.executeJavaScriptOnWebElement(script, element, eventName, false);
            }

            //Put the java listener into the Map
            jsEventListenerMap.put(listenerID, listener);

            //Check the 'global variable' ("window."+listenerID) status in a thread loop
            PollingRunnable pollingRunnable = new PollingRunnable(listenerID);
            Thread pollingThread = new Thread(pollingRunnable);

            //Put the polling runnable into the Map
            jsEventListenerWaitingThreadMap.put(listenerID, pollingRunnable);
            pollingThread.setDaemon(true);//so that it will not block the whole program
            pollingThread.start();
        } catch (SeleniumPlusException e1) {
            IndependantLog.error(debugmsg + "Fail to attach event '" + eventName + "' ", e1);
            throw new SeleniumPlusException("Fail to attach event '" + eventName + "' " + e1.getMessage());
        }

        return listenerID;
    }

    /**
     * <b>Note:</b> It is important to call this method in a finally clause to make sure that the polling thread stop.
     * <pre>
     *   String listenerID = null;
     *   try{
     *      listenerID = addJavaScriptEventListener(element, eventName, listener);
     *   }catch(Exception e){
     *   }finally{
     *     if(listenerID!=null) removeJavaScriptEventListener(element, eventName, listenerID);
     *   }
     * </pre>
     * @param element   WebElement, the element from which the listener will be removed.
     * @param eventName String, event name to detach a listener.
     * @param String, the ID of this listener, used to remove a listener.
     * @throws SeleniumPlusException
     */
    public static void removeJavaScriptEventListener(WebElement element, String eventName, String listenerID)
            throws SeleniumPlusException {
        final String debugmsg = StringUtils.debugmsg(WDLibrary.class, "removeJavaScriptEventListener");

        //Stop the polling thread and remove it from the jsEventListenerWaitingThreadMap
        //It is important to stop the polling thread before calling java script to remove 'generic javascript listener', as
        //'generic javascript listener' will remove the global-variable, and polling thread need to check that variable;
        //if we do thing in reverse order, polling thread will throw NullPointerException
        if (jsEventListenerWaitingThreadMap.containsKey(listenerID)) {
            PollingRunnable pollingRunnable = jsEventListenerWaitingThreadMap.get(listenerID);
            pollingRunnable.setKeepRunning(false);
            jsEventListenerWaitingThreadMap.remove(listenerID);
        }

        //remove the java listener from jsEventListenerMap
        //then detach the javascript listener by JavaScriptFunctions
        try {
            if (jsEventListenerMap.containsKey(listenerID)) {
                jsEventListenerMap.remove(listenerID);

                //Remove the javascript listener by JavaScriptFunctions
                String script = null;
                if (WDLibrary.isSAPDomain(element)) {
                    script = JavaScriptFunctions.SAP.sap_ui_core_Control_detachBrowserEvent(true, listenerID);
                    script += " sap_ui_core_Control_detachBrowserEvent(arguments[0],arguments[1]);";
                    SearchObject.executeJavaScriptOnWebElement(script, element, eventName);
                } else if (WDLibrary.isDojoDomain(element)) {
                    script = JavaScriptFunctions.DOJO.dojo_handle_remove(true, listenerID);
                    script += " dojo_handle_remove();";
                    SearchObject.executeScript(script);
                } else {//For HTML Standard element
                    script = JavaScriptFunctions.removeGenericEventListener(true, listenerID);
                    script += " removeGenericEventListener(arguments[0],arguments[1],arguments[2]);";
                    //If we have added a capturing listener.
                    //               SearchObject.executeJavaScriptOnWebElement(script, element, eventName, true);
                    SearchObject.executeJavaScriptOnWebElement(script, element, eventName, false);
                }

            }
        } catch (SeleniumPlusException e1) {
            IndependantLog.error(debugmsg + "Fail to detach event '" + eventName + "' ", e1);
            throw new SeleniumPlusException("Fail to detach event '" + eventName + "' " + e1.getMessage());
        }

    }

    /**
     *
     * @param script   String, the javascript function to execute. Its first parameter is always DOM object.
     * @param webelement   WebElement, used to find the DOM Object to operate.<br>
     *                                  it is passed directly to javascript function and will be considered as a DOM element.
     * @param args   Object[], the extra parameters used by the javascript function, from SECOND parameter.
     *
     * @return
     * @throws SeleniumPlusException
     */
    public static Object executeJavaScriptOnWebElement(String script, WebElement webelement, Object... args)
            throws SeleniumPlusException {
        //Add webelement to the javascript function's parameter array
        int i = 0;
        Object[] params = new Object[args.length + 1];
        params[i++] = webelement;

        //      IndependantLog.debug("Extra paramters are:");
        for (Object arg : args) {
            params[i++] = arg;
            //IndependantLog.debug(arg.toString());
        }

        return executeScript(script, params);
    }

    /**
     * @return Object or null.
     * @see org.openqa.selenium.JavascriptExecutor#executeScript(String, Object...)
     * @see #executeScript(boolean, String, Object...)
     * @throws SeleniumPlusException
     */
    public static Object executeScript(String script, Object... args) throws SeleniumPlusException {
        return executeScript(true, script, args);
    }

    /**
     * @return Object or null.
     * @see org.openqa.selenium.JavascriptExecutor#executeAsyncScript(String, Object...)
     * @see #executeScript(boolean, String, Object...)
     * @throws SeleniumPlusException
     */
    public static Object executeAsyncScript(String script, Object... args) throws SeleniumPlusException {
        return executeScript(false, script, args);
    }

    /**
     * Execute a section of java-script code.<br>
     * It has the ability to detect a javascript error and throw an JSException.<br>
     * <pre>
     * It is user's responsibility to set javascript global errorcode and errormessage in his
     * javascript code, user can call
     * {@link JavaScriptFunctions#setJSErrorCode(int)}
     * {@link JavaScriptFunctions#setJSErrorObject(String)}
     * for example, see {@link JavaScriptFunctions.SAP#sap_m_List_getItems(boolean)}.
     * </pre>
     * @see org.openqa.selenium.JavascriptExecutor#executeScript(String, Object...)
     * @see org.openqa.selenium.JavascriptExecutor#executeAsyncScript(String, Object...)
     * @throws SeleniumPlusException if there is no appropriate JavascriptExecutor to use.
     */
    public static Object executeScript(boolean synch, String script, Object... args) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        getJS();

        try {
            //Reset the global error (code and message)
            js_cleanError();
            //Append the JS method 'throw_error()'
            script = JavaScriptFunctions.throw_error() + script;

            //Reset the global debug message array
            if (JavaScriptFunctions.jsDebugLogEnable)
                js_initJSDebugArray();
            //Append the JS method 'debug()'
            script = JavaScriptFunctions.debug() + script;

            if (JavaScriptFunctions.DEBUG_OUTPUT_JAVASCRIPT_FUNCTIONS) {
                IndependantLog.debug(debugmsg + "Executing js \n[\n" + script);
            }

            Object object = synch ? lastJS.executeScript(script, args) : lastJS.executeAsyncScript(script, args);
            if (object != null) {
                IndependantLog.debug(debugmsg + " Javascript returned " + object.getClass().getName() + ": "
                        + object.toString());
            } else {
                IndependantLog.debug(debugmsg + " Javascript returned null.");
            }

            //Only check the 'jsErrorCode' for sync execution. For async execution, we don't even know if the execution finish or not.
            if (synch) {
                int errorcode = js_getErrorCode();
                if (errorcode != JavaScriptFunctions.ERROR_CODE_NOT_SET) {
                    Object error = js_getErrorObject();
                    IndependantLog.error(debugmsg + " Fail. errorcode=" + errorcode + " error=" + error);
                    throw JSException.instance(error, errorcode);
                }
            }

            IndependantLog.debug(debugmsg + " Javascript execution succeed. It is returning result ...");

            return object;

        } catch (JSException jse) {
            IndependantLog.debug(debugmsg + getThrowableMessages(jse));
            throw jse;
        } catch (Throwable th) {
            // CANAGL: this is getting caught AFTER the attempt to return object; above is initiated!?
            IndependantLog.debug(debugmsg + getThrowableMessages(th));
            throw new SeleniumPlusException(debugmsg, th);
        } finally {
            //Try to get the array of debug messages during the execution of 'javascript code'
            IndependantLog.debug(debugmsg + "retrieving debug messages...");
            if (JavaScriptFunctions.jsDebugLogEnable) {
                List<?> debugmessages = js_getJSDebugArray();
                for (Object msg : debugmessages) {
                    IndependantLog.debug(debugmsg + msg);
                }
            }
        }
    }

    /**
     * @return JavascriptExecutor, the latest valid JavaSc
     * @throws SeleniumPlusException
     */
    public static JavascriptExecutor getJS() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(false);
        if (lastJS == null && (lastUsedWD instanceof JavascriptExecutor)) {
            IndependantLog.debug(debugmsg + "javascript executor is last Used WebDriver.");
            lastJS = (JavascriptExecutor) lastUsedWD;
        }
        if (lastJS == null) {
            throw new SeleniumPlusException(debugmsg + "javascript executor is null.",
                    SeleniumPlusException.CODE_NO_JS_EXECUTOR);
        }
        if (!lastJS.equals(lastUsedWD)) {
            IndependantLog.debug(debugmsg + "javascript executor is obsolete.");
            refreshJSExecutor();
        }

        return lastJS;
    }

    /**
     * Use the {@link #lastUsedWD} to refresh the {@link #lastJS}.
     * @throws SeleniumPlusException if the {@link #lastUsedWD} cannot be casted to a JavascriptExecutor
     */
    public static void refreshJSExecutor() throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "refreshJSExecutor");
        if (lastUsedWD instanceof JavascriptExecutor) {
            lastJS = (JavascriptExecutor) lastUsedWD;
        } else {
            throw new SeleniumPlusException(debugmsg + "current webdriver cannot execute javascript.",
                    SeleniumPlusException.CODE_NO_JS_EXECUTOR);
        }
    }

    /**
     * Check that the object is not null.
     * @param object   Object
     * @throws SeleniumPlusException will be thrown out with {@link SeleniumPlusException#CODE_OBJECT_IS_NULL} if the object is null.
     */
    public static void checkNotNull(Object object) throws SeleniumPlusException {
        String debugmsg = StringUtils.debugmsg(SearchObject.class, "checkNotNull");
        if (object == null) {
            String msg = "Object is null.";
            IndependantLog.error(debugmsg + msg);
            throw new SeleniumPlusException(msg, SeleniumPlusException.CODE_OBJECT_IS_NULL);
        }
    }

    public static final String CSS_SELECTOR_BY_ID = "CSS_SELECTOR_BY_ID";
    public static final String CSS_SELECTOR_BY_CLASSNAME = "CSS_SELECTOR_BY_CLASSNAME";

    /**
     * According to the type, generate a css selector for the WebElement.
     * @param type   String, the type of the css selector.
     * @param element   WebElement, the WebElement from which a css selector will be generated.
     * @return String, a css selector.
     */
    public static String getCssSelector(String type, WebElement element) {
        StringBuffer cssselector = new StringBuffer();
        String tagName = element.getTagName();

        if (CSS_SELECTOR_BY_CLASSNAME.equalsIgnoreCase(type)) {
            String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
            if (clazz != null && !clazz.trim().isEmpty()) {
                cssselector.append(tagName);
                cssselector.append("[class='" + clazz + "']");
            }
        } else if (CSS_SELECTOR_BY_ID.equalsIgnoreCase(type)) {
            String id = element.getAttribute(Component.ATTRIBUTE_ID);
            if (id != null && !id.trim().isEmpty()) {
                cssselector.append("[id='" + id + "']");
            }
        }
        if (cssselector.toString().trim().isEmpty() && !CSS_SELECTOR_BY_ID.equalsIgnoreCase(type)) {
            String id = element.getAttribute(Component.ATTRIBUTE_ID);
            if (id != null && !id.trim().isEmpty()) {
                cssselector.append("[id='" + id + "']");
            }
        }

        return cssselector.toString();
    }

    /**' '*/
    public static final String CSS_CLASS_SEPARATOR = " ";
    /**'sap'*/
    public static final String CSS_CLASS_SAP_PREFIX = "sap";
    /**'dijit'*/
    public static final String CSS_CLASS_DOJO_DIJIT_PREFIX = "dijit";

    /**
     * Return a string to describe a selenium web element.<br>
     * @param element WebElement
     * @return String,
     */
    static String getDescription(WebElement element) {
        StringBuffer buffer = new StringBuffer();

        if (element != null) {
            buffer.append("<" + element.getTagName());
            String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
            if (clazz != null && !clazz.isEmpty()) {
                buffer.append(" class='" + clazz + "'");
            }
            buffer.append("/>");
        }

        return buffer.toString();
    }

    /**
     * Return the specified property of a WebElement.<br>
     * For example, we can get the parentNode of a WebElement by calling this method with
     * "parentNode" as the property.
     * @param element WebElement
     * @param property String property to retrieve.
     * @return Object to be cast as needed by the specific request, can be null;
     */
    public static Object getWebElementProperty(WebElement element, String property) {
        Object o = null;
        try {
            o = executeJavaScriptOnWebElement("return arguments[0]." + property + ";", element);
        } catch (SeleniumPlusException x) {
            IndependantLog.debug(
                    "SearchObject.getWebElementProperty " + x.getClass().getSimpleName() + ", " + x.getMessage());
        }
        return o;
    }

    /**
     * Return the parent WebElement of the provided WebElement.<br>
     * @param element WebElement
     * @return WebElement parent or null if there is no parent, or an error occurred.
     */
    public static WebElement getParentWebElement(WebElement element) {
        WebElement e = null;
        if (element == null)
            return null;
        try {
            // TODO: what if the html is in a frame?  What do we see?
            if (element.getTagName().equalsIgnoreCase(HTML.TAG_HTML))
                return null;
            e = element.findElement(By.xpath(".."));
        } catch (Exception ignore) {
        }
        return e;
    }

    /**
     * To check if the html tag's css class name containing a certain prefix.<br>
     * @param element WebElement, The selenium web element
     * @param prefix String, the prefix of the css class name
     * @param separator String, the separator in the css class name, normally its a ' '
     * @return boolean true if the css class name contains a certain prefix.
     */
    static boolean checkCSSClassName(WebElement element, String prefix, String separator) {
        boolean isSatisfied = false;

        try {
            String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
            if (clazz != null) {
                isSatisfied = clazz.startsWith(prefix);
                // (CANAGL)if isSatisfied=true then we don't have to check any further, right?!!
                if (!isSatisfied) {
                    String[] classes = clazz.split(separator);
                    // we only need to be satisfied on one class entry, not all class entries.
                    for (String clz : classes) {
                        isSatisfied = clz.startsWith(prefix);
                        if (isSatisfied)
                            break;
                    }
                }
            }
        } catch (Throwable th) {
            IndependantLog.debug(StringUtils.debugmsg(SearchObject.class, "checkCSSClassName"), th);
        }

        return isSatisfied;
    }

    /**
     * To test if the WebElement belongs to SAP OPENUI5 Domain.<br>
     * @param element
     * @return
     * @throws SeleniumPlusException
     */
    public static boolean isSAPDomain(WebElement element) throws SeleniumPlusException {
        checkNotNull(element);
        boolean issap = false;

        issap = SAP.isSapComponent(element);

        return issap;
    }

    /**
     * To test if the WebElement belongs to DOJO Domain.<br>
     * @param element
     * @return
     * @throws SeleniumPlusException
     */
    public static boolean isDojoDomain(WebElement element) throws SeleniumPlusException {
        checkNotNull(element);
        boolean isdojo = false;

        isdojo = DOJO.isDijitComponent(element);

        return isdojo;
    }

    /**
     * Generate a unique name. For example, it can be used as a javascript variable name<br>
     * when calling functions in JavaScriptFunctions to avoid the confilct with variables defined<br>
     * in the Apilication Under Test.<br>
     *
     * @param prefix
     * @return
     */
    public static String generateUniqueName(String prefix) {
        String uniqueName = "";
        if (prefix != null && !prefix.isEmpty()) {
            uniqueName += prefix;
        } else {
            uniqueName = "temp";
        }

        uniqueName += new Date().getTime();

        return uniqueName;
    }

    //   /**
    //    * Generate a (hopefully) unique recognition string which might be:
    //    * <p><ul>
    //    * <li>Dojo recognition
    //    * <li>SAP recognition
    //    * <li>HTML recognition
    //    * <li>Other supported format ?
    //    * </ul<p>
    //    *
    //    * @param element
    //    * @return
    //    */
    //   public static String generateSAFSRecognition(WebElement element){
    //      String debugmsg = "SearchObject.generateSAFSRecognition: ";
    //      boolean isDomain = false;
    //      try{
    //         isDomain = isDojoDomain(element);
    //         if(isDomain) {
    //            IndependantLog.info(debugmsg+"seeking DOJO recognition...");
    //            return DOJO.getRecognition(element);
    //         }
    //      }
    //      catch(Exception x){
    //         IndependantLog.debug(debugmsg+"DOJO domain check: "+ x.getClass().getSimpleName()+", "+ x.getMessage());
    //      }
    //      if(!isDomain){
    //         try{
    //            isDomain = isSAPDomain(element);
    //            if(isDomain) {
    //               IndependantLog.info(debugmsg+"seeking SAP recognition...");
    //               return SAP.getRecognition(element);
    //            }
    //         }
    //         catch(Exception x){
    //            IndependantLog.debug(debugmsg+"SAP domain check: "+ x.getClass().getSimpleName()+", "+ x.getMessage());
    //         }
    //      }
    //      IndependantLog.info(debugmsg+"seeking generic HTML recognition...");
    //      return HTML.getRecognition(element);
    //   }

    //This method will return a SPCTreeNode contain "Recognition String", "Node's id", "Node's name"
    //   public static SPCTreeNode generateSAFSRecognitionNode(WebElement element){
    //      String rec = generateSAFSRecognition(element);
    //      String id = element.getAttribute("id");
    //      String name = element.getAttribute("name");
    //      if (id==null) id = "";
    //      if (name == null) name = "";
    //      SPCTreeNode tree = new SPCTreeNode();
    //      tree.setId(id);
    //      tree.setName(name);
    //      tree.setRecognitionString(rec);
    //      return tree;
    //   }

    /*
     * used to store webelements in a cache when processing many elements for full xpathpaths
     * that might be processed many iterative times.
     */
    private static Hashtable<WebElement, String> xpathObjectCache = new Hashtable();

    /**
     * reset the internal xpath object cache for a new round of full xpath generation.
     * <p>
     * This should be called by external routines getting ready to process an entire page
     * of web elements to avoid processing the same parent nodes many times.
     * This is strictly for performance reasons.
     * <p>
     * The routine should be called both BEFORE and AFTER the processing of nodes with the recursive
     * generateGenericXPath routine.
     * @see #generateFullGenericXPath(WebElement)
     */
    public static void resetXPathObjectCache() {
        xpathObjectCache.clear();
    }

    /**
     * Attempt to generate a full generic Xpath string for this element including all parent
     * path information.
     * <p>
     * Uses generateGenericXPath for each node in the hierarchy.
     * <p>
     * A caller getting ready to process many web elements--or even just one-should ALWAYS call resetXPathObjectCache
     * before processing that first element and after processing the last element.
     *
     * @param element
     * @return full xpath to object.  null if WebElement provided is null or the xpath cannot be deduced.
     * @see #generateGenericXPath(WebElement)
     * @see #resetXPathObjectCache()
     */
    public static String generateFullGenericXPath(WebElement element) {
        if (element == null)
            return null;
        if (xpathObjectCache.containsKey(element))
            return xpathObjectCache.get(element);
        String xpath = generateGenericXPath(element);
        WebElement parent = getParentWebElement(element);
        String parentPath = null;
        if (parent != null)
            parentPath = generateFullGenericXPath(parent);
        if (parentPath != null)
            xpath = parentPath + XPATH_CHILD_SEPARATOR + xpath;
        xpathObjectCache.put(element, xpath);
        return xpath;
    }

    /**
     * Attempt to generate a simple Xpath string for this one element without any parent
     * path information.
     * <p>
     * Currently tries to differentiate the tag name by id, title, or class.
     * <p>
     * Examples:
     * <p><ul>
     * div[@id='myid']<br>
     * div[@title='mytitle']<br>
     * div[@class='myclass']<br>
     * </ul>
     * <p>
     * if none of those have valid values, then only the tag name is returned. In that case,
     * we check to see if it is the Nth sibling of the same tag type and add that, if needed.
     *
     * @param element
     * @return
     * @author CANAGL adding support for 'name' attribute if no other attribs were available
     */
    public static String generateGenericXPath(WebElement element) {
        String tag = element.getTagName();
        String attID = element.getAttribute("id");
        String attTITLE = element.getAttribute("title");
        String attCLASS = element.getAttribute("class");
        String att = (attID != null) && (attID.length() > 0) ? "[@id='" + attID + "']"
                : (attTITLE != null && attTITLE.length() > 0) ? "[@title='" + attTITLE + "']"
                        : (attCLASS != null && attCLASS.length() > 0) ? "[@class='" + attCLASS + "']" : "";
        if (att.length() == 0) {
            attID = element.getAttribute("name");
            if (attID != null && attID.length() > 0)
                att = "[@name='" + attID + "']";
        }
        List sibs = element.findElements(By.xpath("preceding-sibling::" + tag + att));
        if (!sibs.isEmpty()) {
            att += "[" + String.valueOf(sibs.size() + 1) + "]";
        }
        IndependantLog.info("SearchObject.generateGenericXPath made " + tag + att);
        return tag + att;
    }

    /**
     * Attempt to generate a simple frame recognition string without any parent path information.
     * <p>
     * Currently tries to differentiate the tag name by id, name, or index.
     * @param framexpath from generateGenericXPath
     * @return Examples:
     * <p><ul>
     * frameid='myid'<br>
     * framename='myname'<br>
     * frameindex='3'<br>
     * </ul>
     *
     */
    public static String generateSAFSFrameRecognition(String framexpath) {
        // processing examples:
        // frame
        // frame[2]
        // frame[@id='anid']
        // frame[@id='anid'][2]
        int i = framexpath.indexOf("[");
        String tag = framexpath;
        String att = "";
        if (i > 0) {
            tag = framexpath.substring(0, i);
            try {
                att = framexpath.split("@")[1];
                String[] parts = att.split("=");
                att = parts[0];
                att += "=" + parts[1].split("'")[1];
            } catch (Exception badindex) {
                att = framexpath.substring(i + 1);
                try {
                    i = att.indexOf("]");
                    att = "index=" + att.substring(0, i);
                } catch (Exception ignore) {
                }
            }
        }
        IndependantLog.info("SearchObject.generateSAFSFrameRecognition made " + tag + att);
        return tag + att;
    }

    public static final class SAP {
        /**
         * To test if the WebElement represents a SAP OpenUI5 component.<br>
         * @param element
         * @return
         * @throws SeleniumPlusException
         */
        static boolean isSapComponent(WebElement element) throws SeleniumPlusException {
            checkNotNull(element);
            boolean isSAPUI5 = false;

            isSAPUI5 = checkCSSClassName(element, CSS_CLASS_SAP_PREFIX, CSS_CLASS_SEPARATOR);

            if (!isSAPUI5) {
                IndependantLog.warn(getDescription(element)
                        + ", its css-class is not considered as a SAP OPENUI5 component, try get native class name.");
                try {
                    String[] classes = WDLibrary.SAP.getSAPClassNames(element).toArray(new String[0]);
                    for (String clazz : classes) {
                        if (clazz.startsWith(CSS_CLASS_SAP_PREFIX)) {
                            isSAPUI5 = true;
                            break;
                        }
                    }
                } catch (Exception e) {
                    IndependantLog.warn("fail to get native class name, due to " + StringUtils.debugmsg(e));
                }
            }
            if (!isSAPUI5) {
                IndependantLog
                        .debug(getDescription(element) + " will not be considered as a SAP OPENUI5 component.");
            }

            return isSAPUI5;
        }

        /**
         * Create a sap-specific recognition string we would use to find this element again.
         * Currently this stub only returns {@link #DOMAIN_SAP}
         * @param element
         * @return
         */
        public static String getRecognition(WebElement element) {
            // TODO: build SAP recognition string
            return "XPATH=" + generateFullGenericXPath(element);
        }

        /**
         *
         * @param element   WebElement, it MUST wrap a SAP object.
         * @return   List<String> the class names of the SAP object and its superclass.
         * @throws SeleniumPlusException
         */
        public static List<String> getSAPClassNames(WebElement element) throws SeleniumPlusException {
            String debugmsg = StringUtils.debugmsg(SearchObject.SAP.class, "getSAPClassNames");
            Object classNames = null;
            List<String> classNameList = new ArrayList<String>();

            //To save time, if the element does not represent a SAP object, do not execute javascript to get the SAP class name
            //Lei comment this check, some times we need call getSAPClassNames() to get native class to tell the domain, following calling is a dead-loop.
            //         if(!isSAPDomain(element)) throw new SeleniumPlusException(debugmsg+"Not SAP Object!");

            String id = element.getAttribute(Component.ATTRIBUTE_ID);
            if (id != null) {
                classNames = getSAPClassNamesById(id);
            }

            //TODO get SAP Class names by 'CSS selector'
            //      if(className==null && id!=null){
            //         StringBuffer cssselector = new StringBuffer();
            //         cssselector.append("[id='"+id+"']");
            //         className = getSAPClassNamesByCSSSelector(cssselector.toString());
            //      }
            //
            //      if(className==null){
            //         String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
            //         if(clazz!=null){
            //            //http://dojotoolkit.org/reference-guide/1.9/dojo/query.html
            //            //http://www.w3schools.com/cssref/css_selectors.asp
            //            String cssselector = getCssSelector(CSS_SELECTOR_BY_CLASSNAME, element);
            //            className = getSAPClassNamesByCSSSelector(cssselector);
            //         }
            //      }

            if (classNames == null) {
                throw new SeleniumPlusException(debugmsg + " cannot find SAP class name.");
            }
            //handle the class names,
            if (classNames instanceof List)
                classNameList.addAll(((List) classNames));
            else if (classNames instanceof String)
                classNameList.add((String) classNames);
            else {
                IndependantLog.debug("object return by javascript has type as " + classNames.getClass().getName());
            }

            return classNameList;
        }

        /**
         *
         * @param element   WebElement, it MUST wrap a SAP object.
         * @return   String the class name of the SAP object.
         * @throws SeleniumPlusException
         */
        public static String getSAPClassName(WebElement element) throws SeleniumPlusException {
            String debugmsg = StringUtils.debugmsg(SearchObject.SAP.class, "getSAPClassName");
            Object className = null;

            //To save time, if the element does not represent a SAP object, do not execute javascript to get the SAP class name
            if (!isSAPDomain(element)) {
                throw new SeleniumPlusException(debugmsg + "Not SAP Object!");
            }

            String id = element.getAttribute(Component.ATTRIBUTE_ID);
            if (id != null) {
                className = getSAPClassNameById(id);
            }

            //TODO get SAP Class name by 'CSS selector'
            //      if(className==null && id!=null){
            //         StringBuffer cssselector = new StringBuffer();
            //         cssselector.append("[id='"+id+"']");
            //         className = getSAPClassNameByCSSSelector(cssselector.toString());
            //      }
            //
            //      if(className==null){
            //         String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
            //         if(clazz!=null){
            //            //http://dojotoolkit.org/reference-guide/1.9/dojo/query.html
            //            //http://www.w3schools.com/cssref/css_selectors.asp
            //            String cssselector = getCssSelector(CSS_SELECTOR_BY_CLASSNAME, element);
            //            className = getSAPClassNameByCSSSelector(cssselector);
            //         }
            //      }

            if (className == null) {
                throw new SeleniumPlusException(debugmsg + " cannot find SAP class name.");
            }
            //handle the class names,
            if (className instanceof String)
                return (String) className;
            else {
                IndependantLog.warn("object return by javascript has type as " + className.getClass().getName());
                return "";
            }
        }

        /**
         * Test if the element is supported. Supported means that the element is class of subclass of one
         * of the supported class names.<br>
         * @param element WebElement, the element to check.
         * @param supportedClassNames String[], the supported class names
         */
        public static boolean isSupported(WebElement element, String[] supportedClassNames) {

            StringBuffer jsScript = new StringBuffer();
            jsScript.append(JavaScriptFunctions.SAP.sap_objectIsInstanceof(true));
            String varClassArray = JavaScriptFunctions.initializeJSArray(jsScript, false, "  ", 0,
                    supportedClassNames);
            jsScript.append("  return sap_objectIsInstanceof(arguments[0]," + varClassArray + ");\n");
            try {
                Object obj = executeJavaScriptOnWebElement(jsScript.toString(), element);
                if (obj instanceof Boolean)
                    return ((Boolean) obj).booleanValue();
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(" Met exception.", e);
            }

            boolean supported = false;
            String className = null;
            try {
                className = WDLibrary.SAP.getSAPClassName(element);
                for (String supportedClassName : supportedClassNames) {
                    if (supported = supportedClassName.equals(className))
                        break;
                }
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(className == null ? "Not supported." : className + " has not been supported.",
                        e);
            }

            return supported;
        }

        /**
         * <b>Attention:</b>
         * Do <font color='red'>NOT</font> call this method directly!<br>
         * The DOJO object is very big, selenium will spend a lot of time to convert it.<br>
         * Calling this will make your program looks like dead.<br>
         *
         * TODO add timeout, maybe.
         *
         * @param element WebElement represents a DOJO object on page.
         * @return Object, Selenium converted DOJO object.
         */
        protected static Object toSAPObject(WebElement element) {
            String debugmsg = StringUtils.debugmsg(SearchObject.SAP.class, "toDojoObject");
            Object obj = null;

            StringBuffer jsScript = new StringBuffer();
            jsScript.append(JavaScriptFunctions.SAP.sap_getObjectById());
            jsScript.append(" return sap_getObjectById(arguments[0]);");

            String id = element.getAttribute(Component.ATTRIBUTE_ID);
            if (id != null) {
                try {
                    IndependantLog.debug("The parameter id is '" + id + "'");
                    obj = executeScript(jsScript.toString(), id);
                } catch (SeleniumPlusException e) {
                    IndependantLog.debug(debugmsg + " cannot get SAP object by id '" + id + "'", e);
                }
            }

            if (obj == null) {
                IndependantLog.debug(debugmsg + " cannot get SAP object");
            }

            return obj;
        }

        /**
         * @param id   String, the HTML id for an SAP element on page.
         * @return   Object, the SAP class names or null.
         */
        protected static Object getSAPClassNamesById(String id) {
            String debugmsg = StringUtils.debugmsg(SearchObject.SAP.class, "getSAPClassNamesById");
            StringBuffer jsScript = new StringBuffer();

            if (id != null && !id.trim().isEmpty()) {
                jsScript.append(JavaScriptFunctions.SAP.getSAPClassNamesById(true));
                String arrayName = generateUniqueName("arrayOfClassName");
                jsScript.append(arrayName + " = new Array();");
                jsScript.append(" getSAPClassNamesById(arguments[0], " + arrayName + ");");
                jsScript.append(" return " + arrayName + ";");
                try {
                    IndependantLog.debug("The parameter id is '" + id + "'");
                    Object obj = executeScript(jsScript.toString(), id);
                    return obj;
                } catch (SeleniumPlusException e) {
                    IndependantLog.debug(debugmsg + " cannot get SAP class names by id '" + id + "'", e);
                }
            }
            return null;
        }

        /**
         * @param id   String, the HTML id for an SAP element on page.
         * @return   Object, the SAP class name or null.
         */
        protected static Object getSAPClassNameById(String id) {
            String debugmsg = StringUtils.debugmsg(SearchObject.SAP.class, "getSAPClassNameById");
            StringBuffer jsScript = new StringBuffer();

            if (id != null && !id.trim().isEmpty()) {
                jsScript.append(JavaScriptFunctions.SAP.getSAPClassNameById(true));
                jsScript.append(" return getSAPClassNameById(arguments[0]);");
                try {
                    IndependantLog.debug("The parameter id is '" + id + "'");
                    Object obj = executeScript(jsScript.toString(), id);
                    return obj;
                } catch (SeleniumPlusException e) {
                    IndependantLog.debug(debugmsg + " cannot get SAP class name by id '" + id + "'", e);
                }
            }
            return null;
        }
    }

    /*******************************************************
     * Inner class to handle DOJO-specific functionality.   *
     *******************************************************/
    public static final class DOJO {

        /** "DOJOTabControl" */
        public static final String DOJOTABCONTROL = "DOJOTABCONTROL";
        /** "DOJOToolbar" */
        public static final String DOJOTOOLBAR = "DOJOTOOLBAR";

        /**
         * When executed, should return a Map object with keys "dojo", "dijit", and "dojox".<br>
         * Dojo libraries not present in the browser will not have keys in the Map.<br>
         * Values of these keys are usually "dojo", "dijit", and "dojox"; respectively.
         * If Dojo is not running in the browser, the returned Map will be null.<br>
         * If there is a Dojo djConfig.scopeMap in the browser, the values returned will be
         * whatever the mapped values are. Example: "dojo"="mappedDojo", "dijit"="mappedDijit", etc..
         * @return Map of dojo reference scopeMappings, or null if Dojo is not running in the browser.
         */
        public static Map getDojoScopemap() {

            String script = JavaScriptFunctions.DOJO.getDojoScopemap() + "return getDojoScopemap();\n";
            Map result = null;
            try {
                result = (Map) WDLibrary.executeScript(script);
            } catch (Exception ignore) {
            }
            if (result != null) {
                IndependantLog.error("SearchObject.DOJO.getDojoScopemap retrieved " + result.getClass().getName()
                        + ": " + result.toString());
                if (result.containsKey(JavaScriptFunctions.DOJO.DOJO_KEY))
                    JavaScriptFunctions.DOJO.dojo = "window."
                            + result.get(JavaScriptFunctions.DOJO.DOJO_KEY).toString();
                else
                    JavaScriptFunctions.DOJO.dojo = JavaScriptFunctions.DOJO.DOJO_DEFAULT;
                if (result.containsKey(JavaScriptFunctions.DOJO.DIJIT_KEY))
                    JavaScriptFunctions.DOJO.dijit = "window."
                            + result.get(JavaScriptFunctions.DOJO.DIJIT_KEY).toString();
                else
                    JavaScriptFunctions.DOJO.dijit = JavaScriptFunctions.DOJO.DIJIT_DEFAULT;
                if (result.containsKey(JavaScriptFunctions.DOJO.DOJOX_KEY))
                    JavaScriptFunctions.DOJO.dojox = "window."
                            + result.get(JavaScriptFunctions.DOJO.DOJOX_KEY).toString();
                else
                    JavaScriptFunctions.DOJO.dojox = JavaScriptFunctions.DOJO.DOJOX_DEFAULT;
            } else {
                IndependantLog.error("SearchObject.DOJO.getDojoScopemap returned null.");
                JavaScriptFunctions.DOJO.dojo = JavaScriptFunctions.DOJO.DOJO_DEFAULT;
                JavaScriptFunctions.DOJO.dijit = JavaScriptFunctions.DOJO.DIJIT_DEFAULT;
                JavaScriptFunctions.DOJO.dojox = JavaScriptFunctions.DOJO.DOJOX_DEFAULT;
            }
            return result;
        }

        /**
         * To test if the WebElement represents a Dojo.Dijit component.<br>
         * @param element
         * @return
         * @throws SeleniumPlusException
         */
        static boolean isDijitComponent(WebElement element) throws SeleniumPlusException {
            checkNotNull(element);
            boolean isdijit = false;

            isdijit = checkCSSClassName(element, CSS_CLASS_DOJO_DIJIT_PREFIX, CSS_CLASS_SEPARATOR);
            //TODO Does attribute 'widgetid' must exist?
            //if attribute 'widgetid' exists, then we consider it as a dijit object.
            if (isdijit)
                isdijit &= (element.getAttribute(Component.ATTRIBUTE_WIDGETID) != null);

            //Lei, for future use; For now, to save time, just comment it.
            //if checkCSSClassName() cannot detect the domain, we can use following codes.
            /*
            if(!isdijit){
               IndependantLog.warn(getDescription(element)+", its css is not considered as a dijit component, try get dojo native class name.");
               try{
                  String clazz = WDLibrary.DOJO.getDojoClassName(element);
                  if(clazz.startsWith(CSS_CLASS_DOJO_DIJIT_PREFIX)) isdijit = true;
                
               }catch(Exception e){
                  IndependantLog.warn("fail get dojo native class name, due to "+StringUtils.debugmsg(e));
               }
            }
            */

            if (!isdijit) {
                IndependantLog.debug(getDescription(element) + " will not be considered as a dijit component.");
            }

            return isdijit;
        }

        /**
         * Attempt to process a DOJO recognition string like:
         * DOJOTabControl;id=mainTabs, or DOJOToolbar;id=toolbar, etc...
         * <p>
         * Currently supported Types:
         * <p>
         * <ul>
         * DOJOTabControl<br>
         * DOJOToolbar<br>
         * </ul>
         * <p>
         * Currently supported qualifiers:
         * <p>
         * <ul>
         * id=<br>
         * name=<br>
         * title=<br>
         * text=<br>
         * index=<br>
         * </ul>
         * @param sc
         * @param rst
         * @return SearchContext (WebDriver/WebElement) or null
         */
        public static SearchContext findElement(SearchContext sc, String rst) {
            //search the target element within a frame (if exist) level by level
            SearchContext obj = null;
            String debugmsg = "SearchObject.DOJO.findElement ";
            List<WebElement> preMatches = null;
            IndependantLog.debug(debugmsg + "processing recognition fragment: " + rst);
            int matchindex = 1;
            String[] st = StringUtils.getTokenArray(rst, qulifierSeparator, escapeChar);
            String xpathStr = ""; // current DOJO always uses DIV as the high-level widget element

            try {
                String type = null;
                String value = null;
                int i = -1;
                for (String prop : st) {
                    value = null;
                    i = prop.indexOf(assignSeparator);
                    if (i > -1) {
                        type = prop.substring(0, i);
                        if (prop.length() > i)
                            value = prop.substring(i + 1);
                    } else {
                        type = prop;
                    }
                    if (DOJOTABCONTROL.equalsIgnoreCase(type)) {
                        xpathStr = "//div[contains(concat(' ',@class,' '),' dijitTabContainer ')";
                    } else if (DOJOTOOLBAR.equalsIgnoreCase(type)) {
                        xpathStr = "//div[contains(concat(' ',@class,' '),' dijitToolbar ')";
                    } else if (SEARCH_CRITERIA_ID.equalsIgnoreCase(type)) {
                        xpathStr += " and (starts-with(@id,'" + value + "') or "
                                + "(substring(@id, string-length(@id) - string-length('" + value + "')+1)='" + value
                                + "'))";
                    } else if (SEARCH_CRITERIA_NAME.equalsIgnoreCase(type)) {
                        xpathStr += " and (starts-with(@name,'" + value + "') or "
                                + "(substring(@name, string-length(@name) - string-length('" + value + "')+1)='"
                                + value + "'))";
                    } else if (SEARCH_CRITERIA_TITLE.equalsIgnoreCase(type)) {
                        xpathStr += " and (starts-with(@title,'" + value + "') or "
                                + "(substring(@title, string-length(@title) - string-length('" + value + "')+1)='"
                                + value + "'))";
                    } else if (SEARCH_CRITERIA_TEXT.equalsIgnoreCase(type)) {
                        xpathStr += " and .='" + value + "'";
                    } else if (SEARCH_CRITERIA_INDEX.equalsIgnoreCase(type)) {
                        matchindex = StringUtilities.parseIndex(value);
                    }
                }
                xpathStr += "]";
                String xpath = String.format(xpathStr);
                IndependantLog.debug(debugmsg + "resulting xpath: " + xpathStr);

                preMatches = sc.findElements(By.xpath(xpath));

                obj = getNthWebElement(preMatches, matchindex, null);

            } catch (Throwable t) {
                IndependantLog.debug(debugmsg + "recognition string parsing error: " + t.getClass().getSimpleName()
                        + ": " + t.getMessage());
            }
            if (obj == null)
                IndependantLog.debug(debugmsg + "did not find any matching elements.");
            return obj;
        }

        /**
         * Create a dojo-specific recognition string we would use to find this element again.
         * Currently this stub only returns {@link #DOMAIN_DOJO}
         * @param element
         * @return
         */
        public static String getRecognition(WebElement element) {
            // TODO: build DOJO recognition string
            return "XPATH=" + generateFullGenericXPath(element);
        }

        /**
         *
         * @param element   WebElement, it MUST wrap a DOJO object.
         * @return   String the class name of the DOJO object.
         * @throws SeleniumPlusException
         */
        public static String getDojoClassName(WebElement element) throws SeleniumPlusException {
            String debugmsg = StringUtils.debugmsg(SearchObject.DOJO.class, "getDojoClassName");
            Object className = null;

            //To save time, if the element does not represent a DOJO object, do not execute javascript to get the dojo class name
            //Lei comment this check, some times we need call getSAPClassNames() to get native class to tell the domain, following calling is a dead-loop.
            //         if(!isDojoDomain(element)) throw new SeleniumPlusException(debugmsg+"Not Dojo Object!");

            try {
                StringBuffer jsScript = new StringBuffer();
                jsScript.append(JavaScriptFunctions.DOJO.getDojoClassName(true));
                jsScript.append("return getDojoClassName(arguments[0]);");
                className = SearchObject.executeJavaScriptOnWebElement(jsScript.toString(), element);
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(debugmsg + "Fail to get classname by selenium webelement.", e);
            }

            if (className == null) {
                String id = element.getAttribute(Component.ATTRIBUTE_ID);
                if (id != null)
                    className = getDojoClassNameById(id);

                if (className == null && id != null) {
                    StringBuffer cssselector = new StringBuffer();
                    cssselector.append("[id='" + id + "']");
                    className = getDojoClassNameByCSSSelector(cssselector.toString());
                }
            }

            if (className == null) {
                String clazz = element.getAttribute(Component.ATTRIBUTE_CLASS);
                if (clazz != null) {
                    //http://dojotoolkit.org/reference-guide/1.9/dojo/query.html
                    //http://www.w3schools.com/cssref/css_selectors.asp
                    String cssselector = getCssSelector(CSS_SELECTOR_BY_CLASSNAME, element);
                    className = getDojoClassNameByCSSSelector(cssselector);
                }
            }

            if (className == null) {
                throw new SeleniumPlusException(debugmsg + " cannot find dojo class name.");
            }

            return className.toString();
        }

        public static boolean isSupported(WebElement element, String[] supportedClassNames) {

            StringBuffer jsScript = new StringBuffer();
            jsScript.append(JavaScriptFunctions.DOJO.dojo_objectIsInstanceof(true));
            String varClassArray = JavaScriptFunctions.initializeJSArray(jsScript, false, "  ", 0,
                    supportedClassNames);
            jsScript.append("  return dojo_objectIsInstanceof(arguments[0]," + varClassArray + ");\n");

            try {
                Object obj = executeJavaScriptOnWebElement(jsScript.toString(), element);
                if (obj instanceof Boolean)
                    return ((Boolean) obj).booleanValue();
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(" Met exception.", e);
            }

            boolean supported = false;
            String className = null;
            try {
                className = WDLibrary.DOJO.getDojoClassName(element);
                for (String supportedClassName : supportedClassNames) {
                    if (supported = supportedClassName.equals(className))
                        break;
                }
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(className == null ? "Not supported." : className + " has not been supported.",
                        e);
            }

            return supported;
        }

        /**
         * <b>Attention:</b>
         * Do <font color='red'>NOT</font> call this method directly!<br>
         * The DOJO object is very big, selenium will spend a lot of time to convert it.<br>
         * Calling this will make your program looks like dead.<br>
         *
         * TODO add timeout, maybe.
         *
         * @param element WebElement represents a DOJO object on page.
         * @return Object, Selenium converted DOJO object.
         */
        protected static Object toDojoObject(WebElement element) {
            String debugmsg = StringUtils.debugmsg(SearchObject.DOJO.class, "toDojoObject");
            Object obj = null;

            StringBuffer jsScript = new StringBuffer();
            jsScript.append(JavaScriptFunctions.DOJO.getDojoObject(true));
            jsScript.append(" return getDojoObject(arguments[0]);");

            try {
                obj = executeScript(jsScript.toString(), element);
            } catch (SeleniumPlusException e) {
                IndependantLog.debug(debugmsg + " cannot get dojo object ", e);
            }

            if (obj == null) {
                IndependantLog.debug(debugmsg + " cannot get dojo object");
            }

            return obj;
        }

        /**
         * @param id   String, the HTML id for an DOJO element on page.
         * @return   String, the DOJO class name or null.
         */
        protected static String getDojoClassNameById(String id) {
            String debugmsg = StringUtils.debugmsg(SearchObject.DOJO.class, "getDojoClassNameById");
            StringBuffer jsScript = new StringBuffer();

            if (id != null && !id.trim().isEmpty()) {
                jsScript.append(JavaScriptFunctions.DOJO.getDojoClassNameById(true));
                jsScript.append(" return getDojoClassNameById(arguments[0]);");
                try {
                    IndependantLog.debug("The parameter id is '" + id + "'");
                    Object obj = executeScript(jsScript.toString(), id);
                    if (obj instanceof String)
                        return obj.toString();
                } catch (SeleniumPlusException e) {
                    IndependantLog.debug(debugmsg + " cannot get dojo class name by id '" + id + "'", e);
                }
            }
            return null;
        }

        /**
         * @param cssselector   String, the 'CSS Selector' for an DOJO element on page, it can be used to find the DOM object.
         *              The 'CSS Selector' can be something like table[@class='dijitReset dijitStretch dijitButtonContents']
         * @return   String, the DOJO class name or null.
         */
        protected static String getDojoClassNameByCSSSelector(String cssselector) {
            String debugmsg = StringUtils.debugmsg(SearchObject.DOJO.class, "getDojoClassNameByCSSSelector");
            StringBuffer jsScript = new StringBuffer();

            if (cssselector != null && !cssselector.trim().isEmpty()) {
                jsScript.append(JavaScriptFunctions.DOJO.getDojoClassNameByCSSSelector(true));
                jsScript.append(" return getDojoClassNameByCSSSelector(arguments[0]);");
                try {
                    IndependantLog.debug("The parameter cssselector is '" + cssselector + "'");
                    Object obj = executeScript(jsScript.toString(), cssselector);
                    if (obj instanceof String)
                        return obj.toString();
                } catch (SeleniumPlusException e) {
                    IndependantLog.debug(debugmsg + " cannot get dojo class name by xpath '" + cssselector + "'",
                            e);
                }
            }
            return null;
        }
    }

    /*
     * Used to get a streamlined set of messages including causes from the Throwable.
     * Without all the Other Selenium stuff regarding Capabilities, etc..
     * @param th
     * @return streamlined set of messages including causes from the Throwable.
     */
    protected static String getThrowableMessages(Throwable th) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(th.getClass().getSimpleName() + ": " + th.getMessage() + ". \n");
        Throwable cause = th.getCause();
        while (cause != null) {
            buffer.append("Caused by: " + cause.getClass().getSimpleName() + ": " + th.getMessage() + ". \n");
            cause = cause.getCause();
        }
        return buffer.toString();
    }

    public static final class HTML {
        /** 'html' */
        public static final String TAG_HTML = "html";

        public static boolean isSupported(WebElement element, String[] supportedClassNames) {
            String[] actualClassNames = html_getClassName(element);
            for (String clazzname : supportedClassNames) {
                for (String actualClass : actualClassNames) {
                    if (clazzname.equalsIgnoreCase(actualClass))
                        return true;
                }
            }
            return false;
        }

        /**
         * Return an array of classname, the order is important. They will be used to get<br>
         * an approporiate type. The first classname in the array will be tried firstly, if<br>
         * no type can be got then try next one until a type is returned.<br>
         * @param element
         * @return
         * @throws SeleniumPlusException
         */
        public static String[] html_getClassName(WebElement element) {
            String debugmsg = StringUtils.debugmsg(HTML.class, "html_getClassName");
            List<String> classNames = new ArrayList<String>();

            String cssClassName = element.getAttribute(Component.ATTRIBUTE_CLASS);
            if (cssClassName != null)
                classNames.add(cssClassName);

            String tag = element.getTagName();
            if (tag == null)
                IndependantLog.warn(debugmsg + " cannot get HTML tag name.");

            //For html input tag, use its type as class name.
            if (Component.TAG_HTML_INPUT.equalsIgnoreCase(tag)) {
                String type = element.getAttribute(Component.ATTRIBUTE_TYPE);
                if (type != null)
                    classNames.add(type);
            }

            if (tag != null)
                classNames.add(tag);

            return classNames.toArray(new String[0]);// cssClassName, input-type ,tagName
        }

        public static String getHTMLClassName(WebElement element) {
            String[] classes = html_getClassName(element);
            if (classes != null && classes.length > 0)
                return classes[0];
            return TAG_HTML; // default?
        }

        /**
         * Create a generic HTML(5) recognition string we would use to find this element again.
         * Currently this stub only returns {@link #SEARCH_CRITERIA_XPATH}
         * @param element
         * @return
         */
        public static String getRecognition(WebElement element) {
            return "XPATH=" + generateFullGenericXPath(element);
        }

    }

}