org.safs.selenium.webdriver.WebDriverGUIUtilities.java Source code

Java tutorial

Introduction

Here is the source code for org.safs.selenium.webdriver.WebDriverGUIUtilities.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;

/** 
 * History:<br>
 * 
 *  <br>   NOV 19, 2013    (CANAGL) Initial release.
 *  <br>   DEC 18, 2013    (SBJLWA) Modify method getCompType() to support DOJO object.
 *  <br>   JAN 08, 2014    (DHARMESH) Add EditBox support.
 *  <br>   JAN 15, 2014    (SBJLWA) Update method getCompType(): deduce the type by dojo-classname if object is dojo domain.
 *  <br>   FEB 11, 2014    (CANAGL) Support external Class2Type.properties and CustomClass2Type.properties mappings.
 *  <br>   FEB 19, 2014    (DHARMESH) Modify Start Remote Server call.
 *  <br>   APR 15, 2014    (DHARMESH) Added HighLight static varibale.   
 *  <br>   APR 21, 2014    (DHARMESH) Allow default window RS if user doesn't specify RS.
 *  <br>   NOV 26, 2014    (SBJLWA) Modify waitForObject(): distinguish ObjectNotFound from other errors.
 *  <br>   FEB 27, 2015    (DHARMESH) Added -Xms512m -Xmx2g support to start server JVM.
 *  <br>   APR 08, 2015    (SBJLWA) Modify to permit user to set JVM options for starting SELENIUM server.
 *  <br>   JUN 23, 2015    (SCNTAX) Add waitForPropertyStatus(): wait for property value match or gone with expected value.
 *  <br>   JUN 29, 2015      (LeiWang) Modify startRemoteServer(): handle selenium grid (hub+node). Output standard out/err message to console.
 *                                   Add methods to handle/test standalone server, grid server like isXXXRunning(), canConnectXXX(), waitXXXRunning() etc.
 *  <br>   JUL 14, 2015      (LeiWang) Modify isTypeMatched(): try getCompType() firstly and make it more reliable.
 *  <br>   JUL 14, 2015      (CANAGL) Modify class/type mappings to automatically trim the class Type setting of any spaces and tabs.
 *  <br>   OCT 30, 2015      (LeiWang) Modify waitForPropertyStatus(): highlight component.
 *  <br>   NOV 02, 2015      (CANAGL) startRemoteServer on SAFS supporting jre/Java64/jre/bin 64-bit JVM.
 *  <br>   NOV 23, 2015      (LeiWang) Modify waitForObject(): refresh the window object if it becomes stale during the searching of component object.
 *  <br>   DEC 24, 2015      (LeiWang) Add methods to read content from url like "http://host:port/wd/hub/static"
 *  <br>   AUG 05, 2016      (LeiWang) Modified waitForObject(): if RS is AutoIT or IBT, then return SCRIPT_NOT_EXECUTED (4).
 *  <br>   AUG 09, 2016      (LeiWang) Modified setWDTimeoutxxx()/resetWDTimeoutxxx(): return a boolean value to tell if succeed.
 **/

import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.WebElement;
import org.safs.ApplicationMap;
import org.safs.DDGUIUtilities;
import org.safs.GuiClassData;
import org.safs.GuiObjectRecognition;
import org.safs.IndependantLog;
import org.safs.JavaConstant;
import org.safs.Log;
import org.safs.Processor;
import org.safs.SAFSException;
import org.safs.SAFSObjectNotFoundException;
import org.safs.STAFHelper;
import org.safs.StatusCodes;
import org.safs.StringUtils;
import org.safs.TestRecordData;
import org.safs.Tree;
import org.safs.autoit.AutoItRs;
import org.safs.image.ImageUtils;
import org.safs.jvmagent.AgentClassLoader;
import org.safs.logging.LogUtilities;
import org.safs.natives.NativeWrapper;
import org.safs.net.NetUtilities;
import org.safs.selenium.STestRecordHelper;
import org.safs.selenium.util.SeleniumServerRunner;
import org.safs.selenium.webdriver.lib.RS;
import org.safs.selenium.webdriver.lib.RS.XPATH;
import org.safs.selenium.webdriver.lib.SearchObject;
import org.safs.selenium.webdriver.lib.SearchObject.DOJO;
import org.safs.selenium.webdriver.lib.SearchObject.SAP;
import org.safs.selenium.webdriver.lib.SelectBrowser;
import org.safs.selenium.webdriver.lib.SeleniumPlusException;
import org.safs.selenium.webdriver.lib.WDLibrary;
import org.safs.staf.service.map.AbstractSAFSAppMapService;
import org.safs.text.CaseInsensitiveHashtable;
import org.safs.tools.CaseInsensitiveFile;
import org.safs.tools.consoles.ProcessCapture;
import org.safs.tools.drivers.DriverConstant;
import org.safs.tools.drivers.DriverConstant.SeleniumConfigConstant;

/**
 * SAFS/Selenium-specific subclass of DDGUIUtilities.
 */
public class WebDriverGUIUtilities extends DDGUIUtilities {

    /** 
     * "Class2Type.properties" 
     * Typically embedded in the JAR file for the class seeking the mapping. */
    public static final String DEFAULT_CLASS2TYPE_MAP = "Class2Type.properties";
    /** 
     * "CustomClass2Type.properties" 
     * should exist in the /lib/ or /libs/ directory of the JAR file seeking the mapping. */
    public static final String CUSTOM_CLASS2TYPE_MAP = "CustomClass2Type.properties";

    public static boolean HIGHLIGHT = false;

    public static WebDriverGUIUtilities _LASTINSTANCE = null;

    private long gSecTimeout = 0;// or Processor.getSecsWaitForComponent();   

    /*
     * Mapped non-generic html tag to component function types here.<br>
     * Non-mapped items should be considered to be generic "Component" types.
     */
    private static final CaseInsensitiveHashtable _cfmap = new CaseInsensitiveHashtable();

    /*
     * Type to classname mapping. One type can be mapped to more than one classes. The class
     * names are separated by comma (,)
     * CheckBox: sap.ui.commons.CheckBox, checkbox
     */
    private static final CaseInsensitiveHashtable _fcmap = new CaseInsensitiveHashtable();
    private static final String CLASS_NAME_SEPARATOR = ",";

    /**
     * No-argument constructor.
     * Simply calls super()
     * @see DDGUIUtilities#DDGUIUtilities()
     */
    public WebDriverGUIUtilities() {
        super();
        _LASTINSTANCE = this;
    }

    /**
     * Constructor providing the STAFHelper and LogUtilities needed for
     * proper operation. 
     * @param helper The STAFHelper for performing STAF requests.
     * @param log the LogUtilities for logging.
     * @see org.safs.STAFHelper
     * @see org.safs.logging.LogUtilities
     **/
    public WebDriverGUIUtilities(STAFHelper helper, LogUtilities log) {
        super();
        setLogUtilities(log); //must be first
        setSTAFHelper(helper);
        _LASTINSTANCE = this;
    }

    /**
     * Constructor providing the STAFHelper, LogUtilities, and TestRecordData for proper operation.
     * The SAFS/Selenium engine generally uses an instanceof STestRecordHelper for TestRecordData.
     *
     * @param helper The STAFHelper for performing STAF requests.
     * @param data the TestRecordData generally an instanceof STestRecordHelper.
     * @param log the LogUtilities for logging.
     * @see org.safs.STAFHelper
     * @see STestRecordHelper
     * @see org.safs.logging.LogUtilities
     **/
    public WebDriverGUIUtilities(STAFHelper helper, TestRecordData data, LogUtilities log) {
        this(helper, log);
        setTestRecordData(data);
        _LASTINSTANCE = this;
    }

    /**
     * @return true if we detect we are running from a SeleniumPlus installation (/libs/selenium-plus*.jar)
     */
    public static boolean isSeleniumPlus() {
        URL domain = WebDriverGUIUtilities.class.getProtectionDomain().getCodeSource().getLocation();
        String filepath = domain.getFile();
        IndependantLog.info("WDGU class Location:" + filepath); // file:/c:/pathTo/libs/selenium-plus*.jar
        return filepath.toLowerCase().contains("/libs/selenium");
    }

    /**
     * @return true if we detect we are running from a SAFS installation (/lib/safsselenium.jar)
     */
    public static boolean isSAFS() {
        URL domain = WebDriverGUIUtilities.class.getProtectionDomain().getCodeSource().getLocation();
        String filepath = domain.getFile();
        IndependantLog.info("WDGU class Location:" + filepath); // file:/c:/pathTo/libs/selenium-plus*.jar
        return filepath.toLowerCase().contains("/lib/safsselenium");
    }

    /**
     * Retrieve the Class2Type mapping, if any, for the provided classname.
     * This will force the loading of class mappings if they are not already loaded.
     * @param classnames String[] an array of classnames used to get mapped-type, the first one
     *                            will be tried firstly, then next one, until a mapped-type is got.
     * @return the mapped type or null if not mapped.
     * @see #loadClassMappings()
     */
    public static String getClassTypeMapping(String... classnames) {
        IndependantLog.info("WDGU seeking class mapping for '" + Arrays.toString(classnames) + "' ...");
        if (_cfmap.isEmpty())
            loadClassMappings();
        try {
            Object type = null;
            for (String classname : classnames) {
                type = _cfmap.get(classname);
                if (type != null && type instanceof String) {
                    return ((String) type).trim();
                }
            }
        } catch (Exception x) {
            IndependantLog.debug("WDGU getClassTypeMapping " + StringUtils.debugmsg(x));
        }
        return null;
    }

    /**
     * Retrieve the Type2Classes mapping, if any, for the provided type name.
     * This will force the loading of class mappings if they are not already loaded.
     * @param type
     * @return String[], an array of the mapped class-names or null if not mapped.
     * @see #loadClassMappings()
     */
    public static String[] getTypeClassesMapping(String type) {
        IndependantLog.info("WDGU seeking type mapping for '" + type + "'...");
        if (_fcmap.isEmpty())
            loadClassMappings();
        try {
            String typeMappedClassNames = (String) _fcmap.get(type.trim());
            if (typeMappedClassNames != null) {
                String[] classNames = typeMappedClassNames.split(CLASS_NAME_SEPARATOR);
                for (int i = 0; i < classNames.length; i++) {
                    classNames[i] = classNames[i].trim();
                }
                return classNames;
            }
        } catch (Exception x) {
            IndependantLog
                    .debug("WDGU getTypeClassesMapping " + x.getClass().getSimpleName() + ": " + x.getMessage());
        }
        return null;
    }

    /**
     * Get the mapped-class-names according to the type; then get the webelement's<br>
     * class-name (and its superclasses' names) if the webelement is DOJO/SAP object,<br>
     * get the webelement's tag if the webelement is a standard HTML object.
     * <p>
     * Finally, compare the mapped-class-names with webelement's class-names, if one<br>
     * of them match, then we can say the type is matched.<br>
     * 
     * <b>Note:</b> 
     * For DOJO, we can only get the class-name of webelement itself.<br> 
     * We need a way to get its superclass names.
     * 
     * @param we   WebElement, the webelement to test
     * @param type   String, the SAFS named type for a gui component
     * @return   boolean, true if the webelement can match the provided type.
     */
    public static boolean isTypeMatched(WebElement we, String type) {
        String debugmsg = StringUtils.debugmsg(false);
        String realType = type;
        String[] typeMappedClassNames = null;
        List<String> elementClassNames = new ArrayList<String>();
        if (we == null || type == null)
            return false;
        boolean isDojo = false;
        boolean isSap = false;
        try {
            type = type.trim();
            if (type.toUpperCase().startsWith(SearchObject.DOMAIN_DOJO)) {
                realType = type.substring(SearchObject.DOMAIN_DOJO.length());
                isDojo = true;
            } else if (type.toUpperCase().startsWith(SearchObject.DOMAIN_SAP)) {
                realType = type.substring(SearchObject.DOMAIN_SAP.length());
                isSap = true;
            } else {
                realType = type;
            }

            String compType = getCompType(we);
            IndependantLog.debug(debugmsg + realType + "=" + compType + "?");
            if (realType.equalsIgnoreCase(compType))
                return true;

            if (isDojo || WDLibrary.isDojoDomain(we)) {
                IndependantLog.info(debugmsg + "searching for a Dojo " + realType);
                //            elementClassNames = DOJO.getDojoClassNames(we);
                String classname = DOJO.getDojoClassName(we);
                if (classname != null) {
                    IndependantLog.info(debugmsg + "found Dojo class " + classname);
                    elementClassNames.add(classname);
                }
            } else if (isSap || WDLibrary.isSAPDomain(we)) {
                IndependantLog.info(debugmsg + "searching for a SAP " + realType);
                elementClassNames = SAP.getSAPClassNames(we);

                IndependantLog.info(debugmsg + "found " + elementClassNames.size() + " in SAP class hierarchy");
            }
            //We will always add the HTML classes to the list elementClassNames
            String[] classes = WDLibrary.HTML.html_getClassName(we);
            IndependantLog.info(debugmsg + "found HTML classes " + Arrays.toString(classes));
            if (classes != null) {
                for (String classname : classes)
                    elementClassNames.add(classname);
            }

            typeMappedClassNames = getTypeClassesMapping(realType);
            IndependantLog.info(debugmsg + "comparing against " + typeMappedClassNames.length
                    + " classes mapped to type " + realType);
            for (String elementClassName : elementClassNames) {
                for (String mappedClassName : typeMappedClassNames) {
                    if (mappedClassName.equalsIgnoreCase(elementClassName)) {
                        IndependantLog.info(debugmsg + "matched element class '" + elementClassName
                                + "' to mapped class '" + mappedClassName + "'.");
                        return true;
                    }
                }
            }
        } catch (SeleniumPlusException e) {
            IndependantLog.debug(debugmsg + " cannot get class names for webelement.", e);
        }
        IndependantLog.info(debugmsg + "did not match element as type " + type);
        return false;
    }

    /**
     * load the Class2Type.properties mappings and any CustomClass2Type.properties mappings.
     * CustomClass2Type.properties mappings, if present, 
     * would normally exist in the directory containing the JAR file containing this class.
     */
    protected static void loadClassMappings() {
        try {
            URL jom = getResourceURL(DEFAULT_CLASS2TYPE_MAP);
            URL customjom = AgentClassLoader.findCustomizedJARResource(jom, CUSTOM_CLASS2TYPE_MAP);

            IndependantLog.info("WDGU.loading standard mappings from " + jom.getPath());
            InputStream in = jom.openStream();
            Properties props = new Properties();
            props.load(in);
            in.close();
            in = null;
            _cfmap.putAll(props);
            if (customjom != null) {
                in = customjom.openStream();
                props = new Properties();
                IndependantLog.info("WDGU.merging custom mappings from " + customjom.getPath());
                props.load(in);
                _cfmap.putAll(props);
                in.close();
                in = null;
            }
            props.clear();
            props = null;
            // see if there are local customizations defined
            // this may now be obsolete if the AgentClassLoader stuff above works.
            try {
                in = ClassLoader.getSystemResourceAsStream(CUSTOM_CLASS2TYPE_MAP);
                if (in == null) {
                    try {
                        String safsdir = null;
                        if (isSAFS()) {
                            safsdir = System.getenv(DriverConstant.SYSTEM_PROPERTY_SAFS_DIR) + File.separator
                                    + "lib" + File.separator;
                        } else {//isSeleniumPlus
                            safsdir = System.getenv(DriverConstant.SYSTEM_PROPERTY_SELENIUMPLUS_DIR)
                                    + File.separator + "libs" + File.separator;
                        }
                        File custurl = new CaseInsensitiveFile(safsdir + CUSTOM_CLASS2TYPE_MAP).toFile();
                        if (custurl.isFile())
                            in = custurl.toURL().openStream();
                    } catch (MissingResourceException x) {
                        IndependantLog.info("WDGU ignoring missing custom mappings for " + CUSTOM_CLASS2TYPE_MAP);
                    } catch (MalformedURLException x) {
                        IndependantLog.info("WDGU ignoring malformed URL mappings for " + CUSTOM_CLASS2TYPE_MAP);
                    }
                }
                if (in != null) {
                    props = new Properties();
                    IndependantLog.info("WDGU merging custom mappings from " + CUSTOM_CLASS2TYPE_MAP);
                    props.load(in);
                    in.close();
                    in = null;
                    if (props.size() > 0)
                        _cfmap.putAll(props);
                    props.clear();
                    props = null;
                }
            } catch (Exception anye) {
                IndependantLog.error("Error loading " + CUSTOM_CLASS2TYPE_MAP + " resource:" + anye.getMessage(),
                        anye);
            }

            Enumeration<?> clazzes = null;
            String className = null;
            String type = null;
            String classNames = null;
            if (!_cfmap.isEmpty()) {
                clazzes = _cfmap.keys();
                while (clazzes.hasMoreElements()) {
                    className = (String) clazzes.nextElement();
                    type = ((String) _cfmap.get(className)).trim();
                    classNames = (String) _fcmap.get(type);
                    if (classNames == null)
                        _fcmap.put(type, className);
                    else
                        _fcmap.put(type, classNames + CLASS_NAME_SEPARATOR + className);
                }
            }

        } catch (Exception ex) {
            try {
                //If ex.getMessage() return null, for some java sdk, println(null) will throw NullPointerException
                System.err.println(ex.getMessage());
                IndependantLog.error(ex.getMessage(), ex);
            } catch (Exception e) {
                System.err.println(e);
            }
        }
    }

    /**
     * Attempt to locate the URL of a resource.
     * This can be in the JAR file containing a specified class, 
     * or in the directory containing the JAR file.
     * Multitple searches attempted including:
     * <ol>
     * <li> getUniversalResourceURL
     * <li> AgentClassLoader.getResource (using SAFSDIR pathTo/safs.jar)
     * <li> AgentClassLoader.getResource (using SELENIUM_PLUS pathTo/seleniumplus*.jar)
     * </ol>
     * @param clazz -- Class associated with the resource -- mapping to the JAR or directory resource might be found.
     * @param aresource -- generally, the filename of the resource.
     * @return URL to a loadable resource or MissingResourceException is thrown.
     * @throws MissingResourceException if not found
     * @see #getUniversalResourceURL(Class, String)
     * @see org.safs.jvmagent.AgentClassLoader#AgentClassLoader(String)
     */
    protected static URL getResourceURL(String aresource) {
        URL jom = null;
        try {
            jom = GuiClassData.getUniversalResourceURL(WebDriverGUIUtilities.class, aresource);
        } catch (MissingResourceException ignore) {
        }
        if (jom == null) {
            URL domain = WebDriverGUIUtilities.class.getProtectionDomain().getCodeSource().getLocation();
            String filepath = domain.getFile(); //  /C:/SAFS/lib/safs.jar  or  /C:/SeleniumPlus/libs/selenium-plus*.jar
            // ex: /C:/SAFS/lib/safs.jar         
            if (domain.getProtocol().equals("file")) {
                filepath = filepath.substring(1);
                filepath.replace("/", File.separator);
                IndependantLog.info("WDGU: trying AgentClassLoader with JAR path...");
                IndependantLog.info("    " + filepath);
                AgentClassLoader loader = new AgentClassLoader(filepath);
                jom = loader.getResource(aresource);
            }
        }
        if (jom == null) {
            IndependantLog.debug("GCD: throwing MissingResourceException for " + aresource);
            throw new java.util.MissingResourceException(aresource, aresource, aresource);
        }
        return jom;
    }

    /**
     * In SeleniumPlus, some GUILess keywords will be handled in CFComponent<br>
     * So a fake window 'ApplicationConstants' is used to get these keywords executed.<br>
     * This method is used to detected if the window is fake or not.<br>
     * @param windowName String, the window name
     * @param compName String, the component name
     * @return boolean if this is a fake window
     * @see #waitForObject(String, String, String, long)
     */
    protected boolean isFakeWindow(String windowName, String compName) {
        boolean isFakeWindow = false;
        if (windowName != null && compName != null) {
            isFakeWindow = windowName.equals(compName)
                    && AbstractSAFSAppMapService.DEFAULT_SECTION_NAME.equals(windowName);
        }
        return isFakeWindow;
    }

    /**
     * Uses the Application Map caching mechanism provided by the superclass.
     * Retrieves cached references or recognition strings from the App Map and attempts to 
     * identify the matching component via the remote AUT JVM proxies.
     * <p>
     * @param appMapName  the name/ID of the App Map currently in use.
     * @param windowName  the name/ID of the window as predefined in the App Map.
     * @param compName    the name/ID of the child component of the window as predefined 
     * in the App Map.
     * @param secTimeout the number of seconds allowed to located the object before a 
     * SAFSObjectNotFoundException is thrown.
     * 
     * @return 0 on success.<br>
     *         {@link StatusCodes#SCRIPT_NOT_EXECUTED} if the recognition string is in AUTOIT/IBT format.<br>
     * @throws SAFSObjectNotFoundException if specified parent or child cannot be found.
     * @throws SAFSException if other problem occurs<br>
     *                       such as Map is not registered, or windowName/compName cannot be found in map.<br>
     * @see org.safs.DDGUIUtilities#waitForObject(String,String,String,long)
     * @see #waitCompObject(WebElement, String, String, String, String, long)
     * @see #waitWinObject(String, String, long)
     */
    public int waitForObject(String appMapName, String windowName, String compName, long secTimeout)
            throws SAFSObjectNotFoundException, SAFSException {

        ApplicationMap map = null;
        WDTestRecordHelper trdata = (WDTestRecordHelper) this.trdata;

        try {
            Log.info("WDGU: WFO Looking for " + windowName + "." + compName + " using AppMap:" + appMapName);
            map = getAppMap(appMapName);

            if (map == null) {
                if (registerAppMap(appMapName, appMapName)) {
                    map = (ApplicationMap) getAppMap(appMapName);
                    if (map == null) {
                        Log.debug("WDGU: WFO could NOT retrieve registered AppMap " + appMapName);
                        throw new SAFSException("Could not retrieve App Map " + appMapName);
                    }
                }
                // what if NOT registered?
                else {
                    Log.debug("WDGU: WFO could NOT register AppMap " + appMapName);
                    throw new SAFSException("Could not register App Map " + appMapName);
                }
            }

            //TODO: Find and inject into window not launched by us.
            //TODO: currently, we do no caching so we don't care if the recognition is dynamic.
            String winRec = map.getParentGUIID(windowName, false);
            //CACHE HANDLE
            //          boolean ignoreCache = ApplicationMap.isGUIIDDynamic(winRec);
            //          if(ignoreCache) winRec = ApplicationMap.extractTaggedGUIID(winRec);

            //If user doesn't specify window RS then use html default window.
            if (winRec == null || winRec.length() == 0) {
                if (isFakeWindow(windowName, compName)) {
                    //throw new SAFSObjectNotFoundException("Give up looking for fake window '"+windowName+"'.");
                    trdata.setCompGuiId(windowName);
                    trdata.setWindowGuiId(compName);
                    trdata.setCompTestObject(null);
                    trdata.setWindowTestObject(null);
                    trdata.setCompType("Component");
                    Log.info("WDGU: WFO assuming GUILess command: " + trdata.getCommand());
                    return 0;
                } else {
                    Log.warn("WDGU: WFO could NOT retrieve AppMap entry for Window '" + windowName
                            + "'. Verify it exists.");
                    winRec = RS.xpath(XPATH.html());
                }
            }
            trdata.setWindowGuiId(winRec);

            Log.info("WDGU: WFO winRec retrieved: " + winRec);
            WebElement winObj = null;
            //CACHE HANDLE
            //         if(!ignoreCache){
            //            winObj = (WebElement)map.getParentObject(windowName);
            //            Log.info("WDGU: winObj got from cache: "+ winObj);
            //         }

            // check for possible Autoit-Based Testing recognition string
            if (AutoItRs.isAutoitBasedRecognition(winRec)) {
                trdata.setWindowGuiId(winRec);
                trdata.setCompTestObject(null);
                trdata.setWindowTestObject(null);
                trdata.setCompType("Component");
                Log.info("WDGU: WFO returning. Assuming AutoIt Testing for " + trdata.getCommand());
                return StatusCodes.SCRIPT_NOT_EXECUTED;
            }

            // check for possible Image-Based Testing recognition string
            if (ImageUtils.isImageBasedRecognition(winRec)) {
                trdata.setWindowGuiId(winRec);
                trdata.setCompTestObject(null);
                trdata.setWindowTestObject(null);
                trdata.setCompType("Component");
                Log.info("WDGU: WFO returning. Assuming Image-Based Testing for " + trdata.getCommand());
                return StatusCodes.SCRIPT_NOT_EXECUTED;
            }

            //Try to get the Window TestObject dynamically
            if (winObj == null) {
                winObj = waitWinObject(windowName, winRec, secTimeout);
                Log.debug("WDGU: WFO Store the window object into the map cache with key '" + windowName
                        + "' in section [" + windowName + "]");
                map.setParentObject(windowName, winObj);
            }

            //these may not be needed if seeking parent window only          
            String compRec = null;
            WebElement compObj = null;

            boolean isParent = windowName.equalsIgnoreCase(compName);

            // get values if a child component is the target and not just the window
            if (!isParent) {
                // currently, we do no caching so we don't care if the recognition is dynamic.
                compRec = map.getChildGUIID(windowName, compName, false);
                if (compRec == null) {
                    Log.debug("WDGU: WFO could NOT retrieve AppMap entry for Component '" + windowName + ":"
                            + compName + "'. Verify it exists.");
                    throw new SAFSException("WDGU: WFO could NOT retrieve AppMap entry for Component '" + windowName
                            + ":" + compName + "'. Verify it exists.");
                }
                Log.info("WDGU: WFO compRec retrieved: " + compRec);
                //CACHE HANDLE
                //             ignoreCache = ApplicationMap.isGUIIDDynamic(compRec);
                //             if(ignoreCache) compRec = ApplicationMap.extractTaggedGUIID(compRec);

            } else {
                //throw NullPointerExceptions if not found/valid
                trdata.setWindowGuiId(winRec);
                trdata.setWindowTestObject(winObj);
                trdata.setCompTestObject(winObj);
                trdata.setCompType(getCompType(winObj)); // winObj can still go Stale in here!
                Log.info("WDGU: WFO Matched: " + winObj.toString());
                return 0;
            }

            //CACHE HANDLE
            //          if(!ignoreCache){
            //             compObj = (WebElement)map.getChildObject(windowName, compName);
            //             Log.info("WDGU: compObj got from cache: "+ compObj);
            //          }

            //Try to get the Component TestObject dynamically
            if (compObj == null) {
                compObj = waitCompObject(winObj, windowName, winRec, compName, compRec, secTimeout);
                Log.debug("WDGU: WFO Store the component object into the map cache with key '" + compName
                        + "' in section [" + windowName + "]");
                map.setParentObject(windowName, winObj); // could have changed (StaleElementException handling)
                map.setChildObject(windowName, compName, compObj);
            }
            //Set the test-record with window, component information
            trdata.setCompGuiId(compRec);
            trdata.setWindowGuiId(winRec);
            trdata.setCompTestObject(compObj);
            trdata.setWindowTestObject(winObj);
            trdata.setCompType(getCompType(compObj)); // compObj can still go Stale in here!       
            Log.info("WDGU: WFO Matched: " + compObj.toString());

            return 0;

        } catch (SAFSObjectNotFoundException sonfe) {
            throw sonfe;
        } catch (Exception x) {
            String msg = windowName + "." + compName + " using MapName:" + appMapName + ";ApplicationMap:";
            msg += (map == null) ? "NULL" : map.getMapName();
            Log.debug("WDGU:WFO EXCEPTION:" + x.getMessage() + " using " + msg + "\n", x);
            throw new SAFSException(msg);
        } finally {
            try {
                resetWDTimeout();
            } catch (Throwable th) {
                IndependantLog.error("WDGU: WFO Fail to reset WebDriver timeout. Met " + StringUtils.debugmsg(th));
            }
        }
    }

    /**
     * Wait a window object within a timeout. If not found, a SAFSObjectNotFoundException will be thrown out.<br>
     * This method will be called in {@link #waitForObject(String, String, String, long)}.<br>
     * 
     * @param windowName    String, the window's name
     * @param winRec       String, the window's recognition string
     * @param secTimeout   long, the time (in seconds) to wait for a window
     * @return WebElement the window object.
     * @throws SAFSObjectNotFoundException if the window object could not  be found.
     */
    private WebElement waitWinObject(String windowName, String winRec, long secTimeout)
            throws SAFSObjectNotFoundException {
        String debugmsg = StringUtils.debugmsg(false);
        WebElement winObj = null;

        boolean done = false;
        long endtime = System.currentTimeMillis() + (secTimeout * 1000);
        long delay = 1000;

        Log.debug(debugmsg + " Waitting '" + windowName + "' ... ");
        while (!done) {
            if (!setWDTimeout(secTimeout)) {
                Log.warn(debugmsg + "Failed to set the timeout '" + secTimeout + "' to wait for '" + windowName
                        + "'");
            }
            winObj = SearchObject.getObject(winRec);
            resetWDTimeout();

            if (winObj != null) {
                done = true;
            } else {
                done = System.currentTimeMillis() > (endtime - delay);
                if (!done)
                    try {
                        Thread.sleep(delay);
                    } catch (Exception x) {
                    }
            }
        }

        if (winObj == null) {
            throw new SAFSObjectNotFoundException("Could not find matching object for Window " + windowName);
        }

        return winObj;
    }

    /**
     * Wait a component object within a timeout. If not found, a SAFSObjectNotFoundException will be thrown out.<br>
     * If the window object becomes stale during searching component object, we will try to get a fresh window object.<br>
     * This method will be called in {@link #waitForObject(String, String, String, long)}.<br>
     * 
     * @param winObj      WebElement, the window object.
     * @param windowName    String, the window's name
     * @param winRec       String, the window's recognition string
     * @param compName      String, the component's name
     * @param compRec      String, the component's recognition string
     * @param secTimeout   long, the time (in seconds) to wait for a component
     * @return WebElement the component object.
     * @throws SAFSObjectNotFoundException if the component object could not be found, 
     *                                     or the window object becomes stale and could not be refreshed.
     */
    private WebElement waitCompObject(WebElement winObj, String windowName, String winRec, String compName,
            String compRec, long secTimeout) throws SAFSObjectNotFoundException {
        String debugmsg = StringUtils.debugmsg(false);
        WebElement compObj = null;

        boolean done = false;
        long endtime = System.currentTimeMillis() + (secTimeout * 1000);
        long delay = 1000;
        boolean isStale = false;

        Log.debug(debugmsg + " Waitting '" + windowName + "." + compName + "' ... ");
        while (!done) {
            isStale = false;
            if (!setWDTimeout(secTimeout)) {
                Log.warn(debugmsg + "Failed to set the timeout '" + secTimeout + "' to wait for '" + windowName
                        + "." + compName + "'");
            }
            compObj = SearchObject.getObject(winObj, compRec);
            resetWDTimeout();

            if (compObj != null) {
                try {
                    isStale = WDLibrary.isStale(compObj);
                    if (isStale) {
                        Log.debug(debugmsg + " the component " + compName
                                + " appears to be Stale. Trying to get a refreshed one.");
                        compObj = null;
                    } else {
                        done = true;
                    }
                } catch (SeleniumPlusException e) {
                    compObj = null;
                    isStale = true;
                    Log.debug(debugmsg + " the component " + compName + " test for Stale Element returned "
                            + e.getClass().getSimpleName() + ", " + e.getMessage());
                }
            } else {
                done = System.currentTimeMillis() > (endtime - delay);
            }
            if (!done || isStale) {
                try {
                    if (winObj == null || WDLibrary.isStale(winObj)) {
                        Log.debug(debugmsg + " the window " + windowName
                                + " is stale, trying to get a refreshed one.");
                        winObj = waitWinObject(windowName, winRec, secTimeout);
                    }
                } catch (SeleniumPlusException e) {
                    // could not get refreshed window, should we continue to wait component?
                    // Actually, the waitWinObject will throw SAFSObjectNotFoundException if not found.
                    Log.debug(debugmsg + " fail to get refreshed window '" + windowName + "' due to "
                            + StringUtils.debugmsg(e));
                }
                try {
                    Thread.sleep(delay);
                } catch (Exception x) {
                }
            }
        }

        if (compObj == null) {
            throw new SAFSObjectNotFoundException(
                    "Could not find matching object for Component '" + windowName + ":" + compName + "'");
        }

        return compObj;
    }

    public static void highlightThenClear(WebElement webelement, int duration) {
        if (HIGHLIGHT)
            WDLibrary.highlightThenClear(webelement, duration);
    }

    /**
     * Wait for the property matching/gone with the expected value.
     * <p> 
     * @param windowName String, the window name
     * @param compName String, the component name
     * @param propertyName String, the property name
     * @param expectedValue String, the expected value
     * @param secTimeout long, the time of waiting for.
     * @param b_caseInsensitive boolean, if it is true, compare value case sensitively. If it is false, compare the value case insensitively.
     * @param propertyStatus boolean, if it is true, waiting for matching with the expected value. If it is false, waiting for the value gone with expected value.
     * @return If success return 0. If fail, return -1.
     * @throws SAFSObjectNotFoundException
     * @throws SAFSException
     * @author SCNTAX
     */
    public int waitForPropertyStatus(String windowName, String compName, String propertyName, String expectedValue,
            long secTimeout, boolean b_caseInsensitive, boolean propertyStatus)
            throws SAFSObjectNotFoundException, SAFSException {

        WDTestRecordHelper trdata = (WDTestRecordHelper) this.trdata;
        WebElement element;
        String propertyValue = "";
        boolean compareRes = false;
        long endtime = System.currentTimeMillis() + (secTimeout * 1000);
        long delay = 1000;
        boolean done = false;

        try {
            element = WDLibrary.getObject(trdata.getCompGuiId());
            highlightThenClear(element, 1000);
        } catch (Exception e) {
            IndependantLog.debug("WDGU: property: fail to highlight component.", e);
        }

        while (!done) {
            // get property value      
            try {
                element = WDLibrary.getObject(trdata.getCompGuiId());
                propertyValue = WDLibrary.getProperty(element, propertyName);
            } catch (Exception badvalue) {
                throw badvalue;
            }

            // compare with expected value
            if (b_caseInsensitive) {
                compareRes = propertyValue.equals(expectedValue);
            } else {
                compareRes = propertyValue.equalsIgnoreCase(expectedValue);
            }

            if (compareRes == propertyStatus) {
                if (true == propertyStatus) {
                    Log.info("WDGU: property:" + propertyName + " match with expected value:" + expectedValue);
                } else {
                    Log.info("WDGU: property:" + propertyName + " has gone with expected value:" + expectedValue);
                }

                return 0;
            }

            done = System.currentTimeMillis() > (endtime - delay);

            if (!done) {
                try {
                    Thread.sleep(delay);
                } catch (Exception x) {
                }
            }

        }

        if (true == propertyStatus) {
            Log.info("WDGU: property:" + propertyName + " did NOT match with expected value:" + expectedValue);
        } else {
            Log.info("WDGU: property:" + propertyName + " did NOT gone with expected value:" + expectedValue);
        }

        return -1;
    }

    /**
     * Try to deduce the Component Type as stored in our cfmap.  If not found, then the 
     * default type of "Component" is returned.
     * @param compObj
     * @return the Mapped component type, or "Component" if not mapped.
     */
    public static String getCompType(WebElement compObj) {
        String debugmsg = StringUtils.debugmsg(false);
        String clazz = null;
        String type = null;
        try {

            if (WDLibrary.isDojoDomain(compObj)) {
                //Try to deduce the type by DOJO native class name
                try {
                    //TODO also need to get the class-names of this object's superclasses.
                    //               List<String> classes = WDLibrary.DOJO.getDojoClassNames(compObj);
                    //               for(int i=0;i<classes.size();i++){
                    //                  clazz = classes.get(i);
                    //                  if(clazz!=null) type = (String)cfmap.get(clazz);
                    //                  if(type!=null) break;
                    //               }
                    clazz = WDLibrary.DOJO.getDojoClassName(compObj);
                    if (clazz != null)
                        type = getClassTypeMapping(clazz);
                } catch (SeleniumPlusException e) {
                }

                //Then, we try 'css class name' to map a library type
                if (type == null) {
                    clazz = compObj.getAttribute("class");
                    if (clazz != null)
                        type = getClassTypeMapping(clazz);
                }
            } else if (WDLibrary.isSAPDomain(compObj)) {
                try {
                    String[] classes = WDLibrary.SAP.getSAPClassNames(compObj).toArray(new String[0]);
                    clazz = Arrays.toString(classes);
                    type = getClassTypeMapping(classes);

                } catch (SeleniumPlusException e) {
                }

            }
            //         else if(WDLibrary.isOtherDomain(compObj)){
            //            //Try to get the componet type for this sepcical domain 
            //         }

            //Finally, try the html class name (tag or type) to deduce the type
            if (type == null) {
                String[] classes = WDLibrary.HTML.html_getClassName(compObj);
                clazz = Arrays.toString(classes);
                type = getClassTypeMapping(classes);
            }

            Log.debug(debugmsg + "Mapping '" + clazz + "' to '" + type + "'");
        } catch (Exception x) {
            Log.debug(debugmsg + " IGNORING Exception.", x);
        }
        return (type == null) ? "Component" : type;
    }

    /**
     * Normally, component-type is the same as library-type, but sometimes they are different.<br>
     * componentToLibraryMap is used to contain the mapping, if they are different.<br>
     */
    static CaseInsensitiveHashtable componentToLibraryMap = new CaseInsensitiveHashtable();

    static void loadComponentToLibraryMap() {
        //Maybe, it will be loaded from a text file
        componentToLibraryMap.put("MenuBar", "Menu");
    }

    public static String getLibraryType(WebElement compObj) {
        String libType = null;
        String compType = getCompType(compObj);
        loadComponentToLibraryMap();
        libType = (String) componentToLibraryMap.get(compType);
        libType = (libType == null ? compType : libType);

        return libType;
    }

    public static String getLibraryPackage() {
        return "org.safs.selenium.webdriver.lib";
    }

    /**
     * Gets the object defined by the windowName and childName
     * @param mapName  the name/ID of the App Map currently in use.
     * @param windowName  the name/ID of the window as predefined in the App Map.
     * @param childName    the name/ID of the child component of the window as predefined in the App Map.
     * @param ignoreCache if true, try getting the component from the appmap
     * @return the object defined by the windowName and childName
     */
    public WebElement getTestObject(String mapname, String windowName, String childName, boolean ignoreCache) {
        if (mapname == null || windowName == null)
            return null;
        ApplicationMap map = getAppMap(mapname);

        if (map == null) {
            if (registerAppMap(mapname, mapname)) {
                map = getAppMap(mapname);
                if (map == null) {
                    Log.info("WDDDG: gto1 could NOT retrieve registered AppMap " + mapname);
                    return null;
                }
            }
            // what if NOT registered?
            else {
                Log.info("WDDDG: gto1 could NOT register AppMap " + mapname);
                return null;
            }
        }

        WebElement tobj = null;
        if ((childName == null) || (windowName.equalsIgnoreCase(childName))) {

            if (!ignoreCache) {
                tobj = (WebElement) map.getParentObject(windowName);
            }
            if (tobj == null) {
                //map.setParentObject(windowName, null);
                try {
                    waitForObject(mapname, windowName, windowName, gSecTimeout);
                } catch (SAFSException e) {
                    Log.info("WDDDG: Could not waitForObject " + windowName);
                    return null;
                }
                tobj = (WebElement) map.getParentObject(windowName);
                if (tobj == null) {
                    Log.info("WDDDG: Could not waitForObject " + windowName);
                    return null;
                }
            } else {
                return tobj;
            }

        } else {

            if (!ignoreCache) {
                tobj = (WebElement) map.getChildObject(windowName, childName);
            }
            if (tobj == null) {
                //map.setChildObject(windowName, childName, null);
                try {
                    waitForObject(mapname, windowName, childName, gSecTimeout);
                } catch (SAFSException e) {
                    Log.info("WDDDG: Could not waitForObject " + childName);
                    return null;
                }
                tobj = (WebElement) map.getChildObject(windowName, childName);
                if (tobj == null) {
                    Log.info("WDDDG: Could not waitForObject " + childName);
                    return null;
                }
                ((WDTestRecordHelper) trdata).setCompTestObject(tobj);
            } else {
                Log.info("WDDDG: returning cached object: " + tobj.getTagName());
                return tobj;
            }
        }

        try {
            Log.info("WDDDG: compTestObject : " + tobj.getTagName());
        } catch (Exception npe2) {
            Log.info("WDDDG: No Mapped TestObject named \"" + childName + "\" found.");
        }

        return tobj;
    }

    private static boolean timeout_lock = false;
    private static String timeout_lock_owner = null;

    /**
     * Set the webdriver element search timeout.
     * Will only succeed if the setting is not locked, or the lock is owned by the caller.
     * @param timeout -- new timeout value in seconds.
     * @return boolean true if successfully set the timeout.
     * @see #setWDTimeoutLock()
     * @see #resetWDTimeout()
     * @see #resetWDTimeoutLock()
     */
    public boolean setWDTimeout(long timeout) {
        StackTraceElement trace = Thread.currentThread().getStackTrace()[2];
        String caller = trace.getClassName() + "." + trace.getMethodName();
        if (!timeout_lock || caller.equals(timeout_lock_owner)) {
            SeleniumPlus.WebDriver().manage().timeouts().implicitlyWait(timeout, TimeUnit.SECONDS);
            Log.info("WDGU: Timeout set value  to '" + timeout + "' by: " + caller);
            return true;
        } else {
            Log.info("WDGU: *** WDTimeout cannot be changed due to lock owned by: " + timeout_lock_owner + " ***");
            return false;
        }
    }

    /**
     * Suspends any changes to the WDTimeout except by the calling Method.  
     * This is primarily used in broad widespread processing like ProcessContainer that need 
     * to prevent other smaller processes from resetting the timeout back to its normal value.  
     * <p>
     * The call to SET and RESET the lock MUST occur within the same Method 
     * of the calling class.
     * @return boolean true if successfully set the timeout lock.
     * @see #setWDTimeout(long)
     * @see #resetWDTimeout()
     * @see #resetWDTimeoutLock()
     */
    public boolean setWDTimeoutLock() {
        StackTraceElement trace = Thread.currentThread().getStackTrace()[2];
        String caller = trace.getClassName() + "." + trace.getMethodName();
        if (!timeout_lock) {
            timeout_lock = true;
            timeout_lock_owner = caller;
            Log.info("WDGU: *** Changes to WDTimeout have been temporarily locked by: " + timeout_lock_owner
                    + " ***");
            return true;
        } else {
            Log.info("WDGU: *** WDTimeout Lock cannot be changed.  It is already owned by: " + timeout_lock_owner
                    + " ***");
            return false;
        }
    }

    /**
     * reset the the webdriver timout to the "default" setting of the the running Processor.
     * Will only succeed if the timer is not locked, or the lock is owned by the caller.
     * @return boolean true if successfully reset the timeout.
     * @see #setWDTimeoutLock()
     * @see #setWDTimeout(long)
     * @see #resetWDTimeoutLock()
     */
    public boolean resetWDTimeout() {
        StackTraceElement trace = Thread.currentThread().getStackTrace()[2];
        String caller = trace.getClassName() + "." + trace.getMethodName();
        if (!timeout_lock || caller.equals(timeout_lock_owner)) {
            // use window timeout for reset
            long timeout = Processor.getSecsWaitForWindow();
            SeleniumPlus.WebDriver().manage().timeouts().implicitlyWait(timeout, TimeUnit.SECONDS);
            Log.info("WDGU: Reset timeout back  to '" + timeout + "' by: " + caller);
            return true;
        } else {
            Log.info("WDGU: *** Timeout changes cannot be changed due to lock by: " + timeout_lock_owner + " ***");
            return false;
        }
    }

    /**
     * Resumes allowing changes to the WDTimeout.  This is primarily used in broad widespread processing 
     * like ProcessContainer that needs to prevent other smaller processes from changing the timeout 
     * value.
     * @return boolean true if successfully reset the timeout lock.
     * @see #setWDTimeoutLock()
     */
    public boolean resetWDTimeoutLock() {
        StackTraceElement trace = Thread.currentThread().getStackTrace()[2];
        String caller = trace.getClassName() + "." + trace.getMethodName();
        if (timeout_lock && caller.equals(timeout_lock_owner)) {
            timeout_lock_owner = null;
            timeout_lock = false;
            Log.info("WDGU: *** Changes to WDTimeout have been unlocked by: " + caller + " ***");
            return true;
        } else {
            Log.info("WDGU: *** Reset or possibly null WDTimeout Lock disallowed since lock is not owned by:"
                    + caller + " ***");
            return false;
        }
    }

    @Override
    public int setActiveWindow(String appMapName, String windowName, String compName)
            throws SAFSObjectNotFoundException {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public Object findPropertyMatchedChild(Object obj, String property, String bench, boolean exactMatch)
            throws SAFSObjectNotFoundException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public List extractListItems(Object obj, String countProp, String itemProp) throws SAFSException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String getListItem(Object obj, int i, String itemProp) throws SAFSException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Tree extractMenuBarItems(Object obj) throws SAFSException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Tree extractMenuItems(Object obj) throws SAFSException {
        // TODO Auto-generated method stub
        return null;
    }

    /**
     * Start the Selenium Remote Server
     */
    public static boolean startRemoteServer() {
        String root = null;
        String server = null;
        if (isSeleniumPlus()) {
            root = System.getenv("SELENIUM_PLUS");
            server = root + "/extra/RemoteServer.bat";
        }
        if (isSAFS()) {
            root = System.getenv("SAFSDIR");
            server = root + "/samples/Selenium2.0/extra/RemoteServer.bat";
        }
        if (root == null || server == null) {
            Log.debug("WDGU: startRemoteServer failed to determine RemoteServer startup script location.");
            return false;
        }
        try {
            NativeWrapper.runAsynchExec(server);
        } catch (Throwable t) {
            Log.debug(
                    "WDGU: startRemoteServer failed with " + t.getClass().getSimpleName() + ": " + t.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 
     * @param projectdir
     * @param extraParams String[], the optional parameters
     * <pre>
     * "-port N" (N is the port number of the Selenium Server is going to use)
     * "SELENIUMSERVER_JVM_OPTIONS=JVM OPTIONS", the JVM Options to start the Selenium Server
     * "-role role" (role is the role name of the server, it can be hub or node)
     * <pre>
     * @return boolean, true if the server has been successfully started
     * @author CANAGL 2015.11.02 Add support for new SAFS\jre\Java64\jre\bin
     */
    public static boolean startRemoteServer(String projectdir, String... extraParams) {
        String debugmsg = StringUtils.debugmsg(false);
        String seleniumdir = null;
        String server = null;
        String libs = null;
        String javaroot = null;
        String whois = null;
        IndependantLog
                .debug(debugmsg + " projectdir=" + projectdir + " extraParams=" + Arrays.toString(extraParams));

        if (isSeleniumPlus()) {
            seleniumdir = System.getenv("SELENIUM_PLUS");
            server = "extra";
            libs = "libs";
            javaroot = "Java64/jre/bin";
            whois = "SeleniumPlus";
        } else { //assume running from SAFS\lib
            seleniumdir = System.getenv("SAFSDIR");
            server = "samples/Selenium2.0/extra";
            libs = "lib";
            javaroot = "jre/Java64/jre/bin";
            File rootdir = new CaseInsensitiveFile(seleniumdir, javaroot).toFile();
            if (!rootdir.isDirectory()) {
                Log.debug(debugmsg + " cannot deduce expected Java64 Installation Directory: " + javaroot);
                javaroot = "jre/bin";
                Log.debug(debugmsg + " trying older 32-bit Java Installation Directory: " + javaroot);
            }
            whois = "SAFS";
        }
        if (seleniumdir == null || seleniumdir.length() == 0) {
            Log.debug(debugmsg + " cannot deduce " + whois + " Environment Variable/Installation Directory.");
            return false;
        }

        File rootdir = new CaseInsensitiveFile(seleniumdir).toFile();
        if (!rootdir.isDirectory()) {
            Log.debug(
                    debugmsg + " cannot confirm " + whois + " install directory at: " + rootdir.getAbsolutePath());
            return false;
        }

        File extradir = new File(rootdir, server);
        if (!extradir.isDirectory()) {
            Log.debug(debugmsg + " cannot deduce Selenium 'extra' directory at: " + extradir.getAbsolutePath());
            return false;
        }
        String javaexe = System.getProperty(SeleniumConfigConstant.SELENIUMSERVER_JVM);
        if (javaexe == null) {
            javaexe = "java";
            File javadir = new CaseInsensitiveFile(rootdir, javaroot).toFile();
            if (javadir.isDirectory())
                javaexe = javadir.getAbsolutePath() + File.separator + "java";
        }
        if (!StringUtils.isQuoted(javaexe))
            javaexe = StringUtils.quote(javaexe);

        File libsdir = new CaseInsensitiveFile(rootdir, libs).toFile();
        if (!libsdir.isDirectory()) {
            Log.debug(debugmsg + " cannot deduce valid " + whois + " libs directory at: "
                    + libsdir.getAbsolutePath());
            return false;
        }
        File[] files = libsdir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                try {
                    return name.toLowerCase().startsWith("selenium-server-standalone");
                } catch (Exception x) {
                    return false;
                }
            }
        });
        File jarfile = null;
        if (files.length == 0) {
            Log.debug(debugmsg + " cannot deduce " + whois
                    + " selenium-server-standalone* JAR file in /libs directory.");
            return false;
        }
        // if more than one, find the latest
        if (files.length > 1) {
            long diftime = 0;
            for (File afile : files) {
                if (afile.lastModified() > diftime) {
                    diftime = afile.lastModified();
                    jarfile = afile;
                }
            }
        } else {
            jarfile = files[0];
        }
        // we are now set with a remote server jarfile
        Log.debug(debugmsg + " using selenium server jarfile '" + jarfile.getAbsolutePath() + "'");

        String consoledir = null;
        File projectroot = projectdir == null ? null : new CaseInsensitiveFile(projectdir).toFile();
        if (projectroot != null) {
            consoledir = projectroot.getAbsolutePath();
        } else {
            consoledir = rootdir.getAbsolutePath();
        }
        Log.debug(debugmsg + ": Selenium Server runtime consoles expected at " + consoledir);

        File chromedriver = new CaseInsensitiveFile(extradir, "chromedriver.exe").toFile();
        if (!chromedriver.isFile())
            chromedriver = new CaseInsensitiveFile(extradir, "chromedriver").toFile();
        File iedriver = new CaseInsensitiveFile(extradir, "IEDriverServer.exe").toFile();

        List<String> extraParamsList = StringUtils.arrayToList(extraParams);

        boolean isGrid = false;//indicate the type (grid-hub or standalone) of the server to start
        boolean isNode = false;//indicate that we are starting a node
        String nodePort = SeleniumConfigConstant.DEFAULT_SELENIUM_NODE_PORT;
        String jvmOptions = null;

        String param = null;
        for (int i = 0; i < extraParamsList.size(); i++) {
            param = extraParamsList.get(i).trim();
            if (param.toUpperCase().startsWith(SeleniumConfigConstant.SELENIUMSERVER_JVM_OPTIONS)) {
                //try to get "JVM Options" from extra parameter
                int prefixLength = SeleniumConfigConstant.SELENIUMSERVER_JVM_OPTIONS.length();
                int sepLength = GuiObjectRecognition.DEFAULT_ASSIGN_SEPARATOR.length();
                int index = param.indexOf(GuiObjectRecognition.DEFAULT_ASSIGN_SEPARATOR /* = */, prefixLength);
                if (index > -1 && (index + sepLength) < param.length())
                    jvmOptions = param.substring(index + sepLength);
                extraParamsList.remove(i);
            } else if (param.toLowerCase().startsWith(DCDriverCommand.OPTION_ROLE)) {
                //try to get grid-hub, grid-node information
                //-role hub
                //-role node -hub hubRegistrerUrl
                isGrid = true;
                isNode = param.substring(DCDriverCommand.OPTION_ROLE.length()).trim()
                        .startsWith(DCDriverCommand.ROLE_NODE);
            } else if (param.toLowerCase().startsWith(DCDriverCommand.OPTION_PORT)) {
                //try to get the port information, -port portNumber
                nodePort = param.substring(DCDriverCommand.OPTION_PORT.length()).trim();
            }
        }
        //try to get "JVM Options" from system properties
        if (jvmOptions == null)
            jvmOptions = getRemoteServerJVMOptions();
        Log.debug(debugmsg + " with JVM options : " + jvmOptions);
        ProcessCapture console = null;

        try {
            String cp = jarfile.getAbsolutePath();
            if (isSeleniumPlus()) {
                cp += File.pathSeparatorChar + libsdir.getAbsolutePath() + File.separator + "seleniumplus.jar";
            } else {
                cp += File.pathSeparatorChar + libsdir.getAbsolutePath() + File.separator + "safsselenium.jar";
            }
            if (cp.contains(" "))
                cp = "\"" + cp + "\"";
            cp = " -cp " + cp;

            String cmdline = javaexe + " " + jvmOptions + cp + " org.safs.selenium.util.SeleniumServerRunner "
                    + " -Dwebdriver.log.file=\"" + consoledir + File.separator + "webdriver.console\""
                    + " -Dwebdriver.firefox.logfile=\"" + consoledir + File.separator + "firefox.console\""
                    + " -Dwebdriver.safari.logfile=\"" + consoledir + File.separator + "safari.console\""
                    + " -Dwebdriver.ie.logfile=\"" + consoledir + File.separator + "ie.console\""
                    + " -Dwebdriver.opera.logfile=\"" + consoledir + File.separator + "opera.console\""
                    + " -Dwebdriver.chrome.logfile=\"" + consoledir + File.separator + "chrome.console\"";

            if (chromedriver.isFile())
                cmdline += " -Dwebdriver.chrome.driver=\"" + chromedriver.getAbsolutePath() + "\"";
            if (iedriver.isFile())
                cmdline += " -Dwebdriver.ie.driver=\"" + iedriver.getAbsolutePath() + "\"";

            //The other parameter will be passed directly to "org.safs.selenium.util.SeleniumServerRunner"
            for (String parameter : extraParamsList)
                cmdline += " " + parameter;
            cmdline += " -timeout=20 -browserTimeout=60 " + SeleniumServerRunner.PARAM_OUTPUTCONSOLE;

            final String fcmd = cmdline;
            final File workdir = projectroot == null ? rootdir : projectroot;
            Log.debug(debugmsg + " launching Selenium Server with cmdline: " + fcmd);

            Process process = null;
            process = Runtime.getRuntime().exec(fcmd, null, workdir);
            console = new ProcessCapture(process, SeleniumServerRunner.TITLE, true,
                    false/*will not write out/err message to debug log, it is already in SeleniumServerRunner*/);
            //Do we need to wait longer to get more information???
            if (isNode) {
                waitSeleniumNodeRunning(SeleniumConfigConstant.DEFAULT_SELENIUM_HOST, nodePort);
            } else {
                waitSeleniumServerRunning(isGrid, false, false);
            }

            Vector<String> data = console.getData();
            if (data != null && data.size() > 0) {
                for (String message : data) {
                    //SeleniumServerRunner (org.openqa.grid.selenium.GridLauncher) wrongly write all messages to standard err :-(
                    //               if(message.startsWith(ProcessCapture.ERR_PREFIX)){
                    //                  throw new SAFSException(" Fail to execute command: "+fcmd+" \n due to "+message);
                    //               }
                    IndependantLog.debug(message);
                    System.out.println(message);//print the "server starting message to console"
                }
            }

        } catch (Exception x) {
            Log.debug(debugmsg + " failed to launch Selenium Server due to " + x.getClass().getName() + ": "
                    + x.getMessage(), x);
            return false;
        }

        return true;
    }

    /**
     * Wait for selenium "grid node" to be ready.<br>
     * This method will only check if the URL "http://host:port/wd/hub" can be connected. It will<br>
     * not check if "grid node" has been registered to "grid hub".<br>
     * 
     * @param host String, the hostname of the "grid node".
     * @param port String, the port number where the "grid node" is running on.
     * @param params int[], optional parameters<br>
     * <ul>
     * <li>params[0] repeatTimes int, how many times to try to connect with Server. Default is 5 times.
     * <li>params[1] pause int, the pause time (milliseconds) between each connection try. Default is 500 milliseconds.
     * <ul>
     * 
     * @return boolean, true if the server is running.
     */
    public static boolean waitSeleniumNodeRunning(String host, String port, int... params) {
        int tries = 0;
        int repeat = 5;
        int pause = 500;

        if (params != null) {
            if (params.length > 0)
                repeat = params[0];
            if (params.length > 1)
                pause = params[1];
        }

        while (!canConnectHubURL(host, port) && tries++ < repeat) {
            try {
                Thread.sleep(pause);
            } catch (Exception e) {
            }
        }

        return canConnectHubURL(host, port);
    }

    /**
     * Wait for selenium "standalone"/"grid hub, grid nodes" to be ready.<br>
     * <b>Note:</b> Before calling this method:<br>
     * We should set the JVM property {@link SelectBrowser#SYSTEM_PROPERTY_SELENIUM_HOST} and {@link SelectBrowser#SYSTEM_PROPERTY_PROXY_PORT}.<br>
     * If we are waiting for "grid hub, grid nodes", we should also set the JVM property {@link SelectBrowser#SYSTEM_PROPERTY_SELENIUM_NODE}.<br>
     * 
     * @param isGrid boolean, if false the server is "standalone"; if true the server is "grid hub" and "grid nodes".
     * @param waitForNodes boolean, if true then should wait for "grid nodes" to be ready. This parameter take effect only if isGrid is true.
     * @param verifyRegistered boolean, if true then verify "grid nodes" have been registered to "grid hub". This parameter take effect only if isGrid and waitForNodes are true.
     * @param params int[], optional parameters<br>
     * <ul>
     * <li>params[0] repeatTimes int, how many times to try to connect with Server. Default is 5 times.
     * <li>params[1] pause int, the pause time (milliseconds) between each connection try. Default is 500 milliseconds.
     * <ul>
     * 
     * @return boolean, true if the server is running.
     */
    public static boolean waitSeleniumServerRunning(boolean isGrid, boolean waitForNodes, boolean verifyRegistered,
            int... params) {
        int tries = 0;
        int repeat = 5;
        int pause = 500;
        boolean running = false;

        if (params != null) {
            if (params.length > 0)
                repeat = params[0];
            if (params.length > 1)
                pause = params[1];
        }

        //prepare the host, port, nodesInfo parameter
        String host = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_HOST);
        if (host == null || host.isEmpty())
            host = SelectBrowser.DEFAULT_SELENIUM_HOST_IP;//127.0.0.1
        String port = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_PORT);
        port = normalizePort(port, SelectBrowser.DEFAULT_SELENIUM_PORT/*4444 for server (standalone or grid hub)*/);
        String nodesInfo = System.getProperty(SelectBrowser.SYSTEM_PROPERTY_SELENIUM_NODE);

        if (isGrid) {
            while (!isGridRunning(host, port) && tries++ < repeat) {
                try {
                    Thread.sleep(pause);
                } catch (Exception x) {
                }
            }
            running = isGridRunning(host, port);

            if (running && waitForNodes) {
                running = false;//set running to false for waiting nodes
                while (!running && tries++ < repeat) {
                    running = isNodesRunning(nodesInfo, verifyRegistered, host, port);
                    try {
                        Thread.sleep(pause);
                    } catch (Exception x) {
                    }
                }
            }
        } else {
            while (!running && tries++ < repeat) {
                running = isStandalongServerRunning(host, port);
                try {
                    Thread.sleep(pause);
                } catch (Exception x) {
                }
            }
        }

        return running;
    }

    /**
     * Test if selenium "standalone"/"grid hub, grid nodes" is ready.<br>
     * 
     * @param host String, the hostname of server (standalone or grid-hub).
     * @param port String, the port number where the server is running.
     * @param isGrid boolean, if false the server is "standalone"; if true the server is "grid hub".
     * @param checkNodes boolean, if true then check if "grid nodes" are running. This parameter takes effect only if isGrid is true.
     * @param nodesInfo String, the information of the "grid nodes" to check. This parameter takes effect only if isGrid and checkNodes are true.
     * @param verifyRegistered boolean, if true then check if "grid nodes" have been registered to "grid hub". This parameter takes effect only if isGrid and checkNodes are true.
     * 
     * @return boolean, true if the selenium "standalone"/"grid hub, grid nodes" is running.
     */
    public static boolean isSeleniumServerRunning(String host, String port, boolean isGrid, boolean checkNodes,
            String nodesInfo, boolean verifyRegistered) {
        boolean running = false;

        if (isGrid) {
            running = isGridRunning(host, port);
            if (running && checkNodes)
                running = isNodesRunning(nodesInfo, verifyRegistered, host, port);
        } else {
            running = isStandalongServerRunning(host, port);
        }

        return running;
    }

    /**
     * Checks to see if a "Selenium Standalone Server" is running.<br>
     * 
     * @param host String, the hostname of the "standalone server".
     * @param port String, the port number where the server is running on.
     * @return true if the server responds, false otherwise.
     */
    public static boolean isStandalongServerRunning(String host, String port/*4444 for standalone server*/) {
        return canConnectHubURL(host, port);
    }

    /**
     * Checks to see if a "Selenium Grid Hub server" is running.<br>
     * 
     * @param host String, the hostname of the "grid hub server".
     * @param port String, the port number where the server is running on.
     * @return true if the server responds, false otherwise.
     * @see #isNodesRunning()
     */
    public static boolean isGridRunning(String host, String port/*4444 for grid hub*/) {
        return canConnectGridURL(host, port);
    }

    /**
     * Checks to see if all "Selenium Grid Nodes" are running and registered to a grid-hub:<br>
     * 
     * @param nodesInfo String, the "grid nodes" to check. It is like "node1:port:nodeconfig;node2:port:nodeconfig;".
     * @param verifyRegistered boolean, if we need to verify that all nodes are registered to "grid hub"
     * @param hubhost String, the name of the "gird hub". It is valid only when verifyRegistered is true.
     * @param hubport String, the port number where the "grid hub" is running on. It is valid only when verifyRegistered is true.
     * @return boolean, true if all nodes are running and have been registered to grid hub.
     * @see #isGridRunning()
     */
    public static boolean isNodesRunning(String nodesInfo /*node1:port:nodeconfig;node2:port:nodeconfig;*/,
            boolean verifyRegistered, String hubhost, String hubport) {
        String debugmsg = StringUtils.debugmsg(false);

        List<GridNode> nodes = getGridNodes(nodesInfo);

        for (GridNode node : nodes) {
            if (!canConnectHubURL(node.getHostname(), node.getPort())) {
                IndependantLog.warn(debugmsg + " node '" + node + "' is not running.");
                return false;
            }
        }

        //"connect to hub" is not enough to tell that this node has registered to hub
        //We need to get the grid/console information to analyze the registered nodes
        IndependantLog.debug(debugmsg + " all selenium nodes '" + nodes + "' seems running.");
        if (verifyRegistered) {
            if (!verifyNodesRegistered(hubhost, hubport, nodes.toArray(new GridNode[0]))) {
                IndependantLog.error(
                        debugmsg + " not all selenium nodes '" + nodes + "' have been registered to grid hub.");
                return false;
            }
        }

        return true;
    }

    /**
     * Convert a node information string to a list of GridNode.
     * @param nodesInfo String, the node information of format "node1:port:nodeconfig;node2:port:nodeconfig"
     * @return List<GridNode>
     */
    public static List<GridNode> getGridNodes(String nodesInfo) {
        List<GridNode> nodesList = new ArrayList<GridNode>();

        //nodesInfo is like "node1:port:nodeconfig;node2:port:nodeconfig"
        if (nodesInfo != null) {
            List<String> nodes = StringUtils.getTrimmedTokenList(nodesInfo, StringUtils.SEMI_COLON);
            //nodes is a list of "node1:port:nodeconfig"
            for (String node : nodes)
                nodesList.add(new GridNode(node));
        }

        return nodesList;
    }

    /**
     * The class GridNode contains the information about a selenium node, including<br>
     * <ul>
     * <li>hostname
     * <li>hostip
     * <li>port
     * <li>configuration
     * </ul>
     */
    public static class GridNode {
        private String hostname = SelectBrowser.DEFAULT_SELENIUM_HOST;
        private String hostip = SelectBrowser.DEFAULT_SELENIUM_HOST_IP;
        private String port = SeleniumConfigConstant.DEFAULT_SELENIUM_NODE_PORT;//Grid-node's default port is 5555
        private Object config = null;

        public GridNode(String nodeInfo /* nodeInfo is something like "node1:port:nodeconfig"*/) {
            List<String> info = StringUtils.getTrimmedTokenList(nodeInfo, StringUtils.COLON);
            int i = 0;
            for (String value : info)
                if (value != null)
                    info.set(i++, value.trim());
            int size = info.size();
            if (size > 0)
                setHostname(info.get(0));
            if (size > 1)
                port = normalizePort(info.get(1), SeleniumConfigConstant.DEFAULT_SELENIUM_NODE_PORT);
            if (size > 2)
                config = info.get(2);
        }

        public GridNode(String hostname, String port) {
            this.port = port;
            setHostname(hostname);
        }

        public GridNode(String hostname, String port, Object config) {
            setHostname(hostname);
            this.port = port;
            this.config = config;
        }

        public String getHostip() {
            return hostip;
        }

        public String getHostname() {
            return hostname;
        }

        public String getPort() {
            return port;
        }

        public Object getConfig() {
            return config;
        }

        public void setHostname(String hostname) {
            this.hostname = hostname;
            this.hostip = NetUtilities.getHostIP(hostname);
        }

        public void setPort(String port) {
            this.port = port;
        }

        public void setConfig(Object config) {
            this.config = config;
        }

        public boolean isSameHostPort(GridNode node) {
            if (node == null)
                return false;
            if (!hostip.trim().equals(node.getHostip().trim()))
                return false;
            if (!port.trim().equals(node.getPort().trim()))
                return false;
            return true;
        }

        public String toString() {
            return hostname + StringUtils.COLON + port;
        }
    }

    /** "/grid/register" the path for registering grid-node */
    public static final String URL_PATH_GRID_REGISTER = "/grid/register";
    /** "/grid/console" the path to visit HTTP grid-hub server */
    public static final String URL_PATH_GRID_CONSOLE = "/grid/console";
    /** "/wd/hub" the path to visit HTTP standalone-server or grid-node */
    public static final String URL_PATH_HUB = "/wd/hub";

    /** 
     * "/wd/hub/static" the path to get information about the running standalone-server or grid-node 
     * This is a NON-valid url, and the information is supposed to get from the error-stream.
     */
    public static final String URL_PATH_HUB_STATIC = "/wd/hub/static";

    /**
     * @param host String, the name of host where "grid hub" server runs
     * @param port String, the port number on which "grid hub" server runs
     * @return boolean, true if the "grid hub" is available
     */
    public static boolean canConnectGridURL(String host, String port) {
        return canConnectHttpURL(host, port, URL_PATH_GRID_CONSOLE);
    }

    /**
     * This method will read from the connection to url {@link #URL_PATH_GRID_CONSOLE}.<br>
     * @param host String, the host name
     * @param port String, the port number
     * @return String, the content read from the connection.
     */
    public static String readGridURL(String host, String port) {
        return readHttpURL(host, port, URL_PATH_GRID_CONSOLE);
    }

    /**
     * @param host String, the name of host where "standalone server"/"grid node" runs
     * @param port String, the port number on which "standalone server"/"grid node" runs
     * @return boolean, true if the "standalone server"/"grid node" is available
     */
    public static boolean canConnectHubURL(String host, String port) {
        return canConnectHttpURL(host, port, URL_PATH_HUB);
    }

    /**
     * This method is supposed to read from the connection to a NON-valid url {@link #URL_PATH_HUB_STATIC}. It will read from error stream.<br>
     * @param host String, the host name
     * @param port String, the port number
     * @return String, the information about the running standalone server or grid-node.
     */
    public static String readHubStaticURL(String host, String port) {
        return readHttpURL(host, port, URL_PATH_HUB_STATIC);
    }

    /**
     * Parse the port to make sure that it is an integer and bigger than 1000. 
     * If there is something wrong, use the default port number provided by parameter. 
     * @param port String, the port number to parse
     * @param defaultPort String, the default port number. 4444 for "standalone server"/"grid hub"; 5555 for "grid node".
     * @return String, the port number
     */
    private static String normalizePort(String port, String defaultPort) {
        String resutlPort = port;
        try {
            if (Integer.parseInt(port) < 1000)
                resutlPort = defaultPort;
        } catch (Exception ignore) {
            IndependantLog.debug(" ignoring invalid port setting '" + port + "'. Using default: " + defaultPort);
            resutlPort = defaultPort;
        }
        return resutlPort;
    }

    /**
     * Try to test if an HTTP URL can be connected or not.
     * @param host String, the host name
     * @param port String, the port number where "HTTP server" runs
     * @param path String, the path on "HTTP server" to access, like "/wd/hub", "/grid/console" etc.
     * @return boolean true if the URL can be connected; false otherwise
     */
    private static boolean canConnectHttpURL(String host, String port, String path) {
        String debugmsg = StringUtils.debugmsg(false);

        String url = "http://" + host + ":" + port + path;
        try {
            return canConnectHttpURL(new URL(url));
        } catch (MalformedURLException e) {
            IndependantLog.error(debugmsg + " URL '" + url + "' is not correct." + StringUtils.debugmsg(e));
            return false;
        }
    }

    /**
     * @param host String, the host name
     * @param port String, the port number where "HTTP server" runs
     * @param path String, the path on "HTTP server" to access, like "/wd/hub/static", "/grid/console" etc.
     * @return String, the content read from connection "http://host:port+path"
     */
    private static String readHttpURL(String host, String port, String path) {
        String debugmsg = StringUtils.debugmsg(false);

        String url = "http://" + host + ":" + port + path;
        try {
            return NetUtilities.readHttpURL(new URL(url), "UTF-8", timeoutForHttpConnection);
        } catch (MalformedURLException e) {
            IndependantLog.error(debugmsg + " URL '" + url + "' is not correct." + StringUtils.debugmsg(e));
            return null;
        }
    }

    /**
     * timeout for HTTP connection, the default value is 1000 milliseconds.<br>
     * For example:<br> 
     * connect to selenium-standalone-server {@link #URL_PATH_HUB} <br>
     * connect to selenium-grid-hub {@link #URL_PATH_GRID_CONSOLE} <br>
     * If we always fail to connect even the site is running, we may consider to enlarge this timeout by {@link #setTimeoutForHttpConnection(int)}.<br>
     */
    private static int timeoutForHttpConnection = 1000;//milliseconds

    /** get {@link #timeoutForHttpConnection}*/
    public static int getTimeoutForHttpConnection() {
        return timeoutForHttpConnection;
    }

    /** set {@link #timeoutForHttpConnection}*/
    public static void setTimeoutForHttpConnection(int timeoutForHttpConnection) {
        WebDriverGUIUtilities.timeoutForHttpConnection = timeoutForHttpConnection;
    }

    /**
     * Try to test if an HTTP URL can be connected or not.
     * @param serverURL URL, the URL to test.
     * @return boolean true if the URL can be connected; false otherwise
     */
    private static boolean canConnectHttpURL(URL serverURL) {
        String debugmsg = StringUtils.debugmsg(false);
        boolean debug = false;

        HttpURLConnection con = null;
        try {
            con = (HttpURLConnection) serverURL.openConnection();
            con.setConnectTimeout(timeoutForHttpConnection);
            if (debug) {
                IndependantLog.debug(debugmsg + "request properties: " + con.getRequestProperties());
                IndependantLog.debug(debugmsg + "respond code: " + con.getResponseCode());
                IndependantLog.debug(debugmsg + "respond message: " + con.getResponseMessage());
                IndependantLog.debug(debugmsg + "content length: " + con.getContentLengthLong());
            }

            if (con.getResponseCode() == HttpURLConnection.HTTP_OK && con.getContentLengthLong() > 50) {
                return true;
            } else {
                IndependantLog.warn(debugmsg + "Fail to connect to URL '" + serverURL + "'");
            }

        } catch (Exception e) {
            IndependantLog.error(debugmsg + StringUtils.debugmsg(e));
        } finally {
            try {
                if (con != null)
                    con.disconnect();
            } catch (Exception x) {
            }
        }
        return false;
    }

    //The following GRID_CONSOLE_RESPONSE_XXX constants are used to parse the response from grid-console.
    //<p>host:10.121.19.24</p>
    private static final String GRID_CONSOLE_RESPONSE_HOST = "host:";
    //<p>port:5678</p>
    private static final String GRID_CONSOLE_RESPONSE_PORT = "port:";
    //<p>remoteHost:http://10.121.19.24:5678</p>
    private static final String GRID_CONSOLE_RESPONSE_REMOTEHOST = "remoteHost:";
    //The leading char of a tag
    private static final String GRID_CONSOLE_RESPONSE_TAG_BEGIN = "<";
    //The leading string for a registered node
    private static final String GRID_CONSOLE_RESPONSE_CONFIG = "<div type='config' class='content_detail'>";

    /**
     * Verify if an array of GridNode have been registered to grid hub.<br>
     * @param hubhost String, the name of "grid hub".
     * @param hubport String, the port number where "grid hub" is running on.
     * @param nodes GridNode[], an array of GridNode to verify
     * @return boolean, true if all nodes have been registered.
     */
    public static boolean verifyNodesRegistered(String hubhost, String hubport, GridNode... nodes) {
        String debugmsg = StringUtils.debugmsg(false);
        boolean debug = false;

        HttpURLConnection con = null;
        InputStream ins = null;
        BufferedReader br = null;

        try {
            String gridConsoleURL = "http://" + hubhost + ":" + hubport + URL_PATH_GRID_CONSOLE;
            con = (HttpURLConnection) (new URL(gridConsoleURL)).openConnection();
            con.setConnectTimeout(1000);
            if (debug) {
                IndependantLog.debug(debugmsg + "request properties: " + con.getRequestProperties());
                IndependantLog.debug(debugmsg + "respond code: " + con.getResponseCode());
                IndependantLog.debug(debugmsg + "respond message: " + con.getResponseMessage());
                IndependantLog.debug(debugmsg + "content length: " + con.getContentLengthLong());
            }

            if (con.getResponseCode() == HttpURLConnection.HTTP_OK && con.getContentLengthLong() > 50) {
                ins = con.getInputStream();
                //verify the content of "http://hub:port/grid/console" to see if the node has registered
                br = new BufferedReader(new InputStreamReader(ins, Charset.forName("UTF-8")), 1024 * 10);
                String nodeInfo = null;
                GridNode tempNode = null;
                List<GridNode> registeredNodes = new ArrayList<GridNode>();
                while ((nodeInfo = br.readLine()) != null) {
                    System.out.println(nodeInfo);
                    if (nodeInfo.contains(GRID_CONSOLE_RESPONSE_CONFIG)) {
                        tempNode = parseNodeInfo(nodeInfo);
                        if (tempNode != null)
                            registeredNodes.add(tempNode);
                    }
                }
                if (registeredNodes.size() < nodes.length)
                    return false;
                //check if all nodes have registered.
                boolean matched = false;
                for (GridNode node : nodes) {
                    matched = false;
                    for (GridNode registeredNode : registeredNodes) {
                        matched = node.isSameHostPort(registeredNode);
                        if (matched)
                            break;
                    }
                    if (!matched)
                        break;
                }
                if (matched)
                    return true;
            } else {
                IndependantLog
                        .warn(debugmsg + "Fail to connect to grid hub server at URL '" + gridConsoleURL + "'");
            }

        } catch (Exception e) {
            IndependantLog.error(debugmsg + StringUtils.debugmsg(e));
        } finally {
            try {
                if (ins != null)
                    ins.close();
            } catch (Exception x) {
            }
            try {
                if (br != null)
                    br.close();
            } catch (Exception x) {
            }
            try {
                if (con != null)
                    con.disconnect();
            } catch (Exception x) {
            }
        }
        return false;
    }

    /**
     * Parse the response got from URL "http://hub:port/grid/console", and get the GridNode.
     * 
     * @param nodeInfo String, the string containing information of a registered node.
     * @return GridNode, the registered node.
     */
    private static GridNode parseNodeInfo(String nodeInfo) {
        String debugmsg = StringUtils.debugmsg(false);
        GridNode node = null;
        int beginIndex = -1;
        int endIndex = -1;
        String host = null;
        String port = null;

        IndependantLog.debug(debugmsg + " parse nodeInfo: " + nodeInfo);
        if (StringUtils.isValid(nodeInfo)) {
            if (nodeInfo.contains(GRID_CONSOLE_RESPONSE_HOST)) {
                beginIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_HOST);
                endIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_TAG_BEGIN);
                if (beginIndex > -1 && endIndex > -1 && beginIndex < endIndex) {
                    host = nodeInfo.substring(beginIndex + GRID_CONSOLE_RESPONSE_HOST.length(), endIndex);
                }
            }
            if (nodeInfo.contains(GRID_CONSOLE_RESPONSE_PORT)) {
                beginIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_PORT);
                endIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_TAG_BEGIN);
                if (beginIndex > -1 && endIndex > -1 && beginIndex < endIndex) {
                    port = nodeInfo.substring(beginIndex + GRID_CONSOLE_RESPONSE_PORT.length(), endIndex);
                }
            }
            if (host == null || port == null) {
                if (nodeInfo.contains(GRID_CONSOLE_RESPONSE_REMOTEHOST)) {
                    beginIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_REMOTEHOST);
                    endIndex = nodeInfo.indexOf(GRID_CONSOLE_RESPONSE_TAG_BEGIN);
                    if (beginIndex > -1 && endIndex > -1 && beginIndex < endIndex) {
                        //http://10.121.19.24:5678
                        try {
                            URL url = new URL(nodeInfo
                                    .substring(beginIndex + GRID_CONSOLE_RESPONSE_REMOTEHOST.length(), endIndex));
                            if (host == null)
                                host = url.getHost();
                            if (port == null)
                                port = String.valueOf(url.getPort());
                        } catch (MalformedURLException e) {
                            IndependantLog.debug(debugmsg + StringUtils.debugmsg(e));
                        }
                    }
                }
            }
            IndependantLog.debug(debugmsg + " Create GridNode with host:" + host + " port:" + port);
            node = new GridNode(host, port);
        }

        return node;
    }

    /**
     * Retrieve the JVM options from system properties.<br>
     * Before calling this method, we may need to set some java system property.<br>
     * <ul>
     * <li> {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_OPTIONS}
     * <li> {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xms}
     * <li> {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xmx}
     * </ul>
     * If none of them exist in system properties, the minimum JVM option "-Xms512m -Xmx1g" will be provided.<br>
     * If {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_OPTIONS} exist, but {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xms}
     * and {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xmx} do NOT exist, then we will add "-Xms512m -Xmx1g" to the JVM options if its
     * value does not contain "-Xms" or "-Xmx".<br>
     * If {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_OPTIONS} exist, and {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xms}
     * and/or {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xmx} exist, then we will add "-Xms512m -Xmx1g" to the JVM options if its
     * value does not contain "-Xms" or "-Xmx", otherwise if its value contains ""-Xms" or "-Xmx", we will replace them by the value of 
     * {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xms} and/or {@link SeleniumConfigConstant#SELENIUMSERVER_JVM_Xmx}.<br>
     * 
     * @return String, the JVM options for starting SELENIUM Remote Server.
     * @see #startRemoteServer(String)
     */
    public static String getRemoteServerJVMOptions() {
        String debugmsg = StringUtils.debugmsg(false);
        //Get the JVM Options from system properties
        //We have set them to system properties in EmbeddedSeleniumHookDriver#start()       
        String Xms = System.getProperty(SeleniumConfigConstant.SELENIUMSERVER_JVM_Xms);
        String Xmx = System.getProperty(SeleniumConfigConstant.SELENIUMSERVER_JVM_Xmx);
        Log.debug(debugmsg + " get JVM parameter : Xms=" + Xms + " Xmx=" + Xmx);
        String jvmOptions = System.getProperty(SeleniumConfigConstant.SELENIUMSERVER_JVM_OPTIONS);
        Log.debug(debugmsg + " get JVM Options : " + jvmOptions);
        if (jvmOptions == null) {
            if (Xms == null)
                Xms = SeleniumConfigConstant.DEFAULT_JVM_MEMORY_MINIMUM;
            if (Xmx == null)
                Xmx = SeleniumConfigConstant.DEFAULT_JVM_MEMORY_MAXIMUM;
            jvmOptions = " " + JavaConstant.JVM_Xms + Xms + " " + JavaConstant.JVM_Xmx + Xmx + " ";
        } else {
            //replace -Xmx and -Xms in JVM options, if user has provided them
            if (Xms != null) {//user has provided Xms
                jvmOptions = StringUtils.replaceJVMOptionValue(jvmOptions, JavaConstant.JVM_Xms, Xms);
            } else {
                Xms = SeleniumConfigConstant.DEFAULT_JVM_MEMORY_MINIMUM;
            }
            if (Xmx != null) {//user has provided Xmx
                jvmOptions = StringUtils.replaceJVMOptionValue(jvmOptions, JavaConstant.JVM_Xmx, Xmx);
            } else {
                Xmx = SeleniumConfigConstant.DEFAULT_JVM_MEMORY_MAXIMUM;
            }

            //if jvmOptions does not contain -Xmx or -Xms, we will add them to jvmOptions
            if (!jvmOptions.contains(JavaConstant.JVM_Xms))
                jvmOptions += " " + JavaConstant.JVM_Xms + Xms;
            if (!jvmOptions.contains(JavaConstant.JVM_Xmx))
                jvmOptions += " " + JavaConstant.JVM_Xmx + Xmx;
        }
        Log.debug(debugmsg + " return JVM options : " + jvmOptions);

        return jvmOptions;
    }
}