/* * The MIT License * * Copyright (c) 2010, InfraDNA, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.saucelabs.selenium.client.factory; import com.saucelabs.selenium.client.factory.spi.SeleniumFactorySPI; import com.thoughtworks.selenium.DefaultSelenium; import com.thoughtworks.selenium.Selenium; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.DesiredCapabilities; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.net.URL; import java.sql.DriverManager; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; /** * Factory of {@link Selenium}. * <p/> * <p/> * Compared to directly initializing {@link com.thoughtworks.selenium.DefaultSelenium}, this additional indirection * allows the build script or a CI server to control how you connect to the selenium. * This makes it easier to run the same set of tests in different environments without * modifying the test code. * <p/> * <p/> * This is analogous to how you connect to JDBC — you normally don't directly * instantiate a specific driver, and instead you do {@link DriverManager#getConnection(String)}. * * @author Kohsuke Kawaguchi */ public class SeleniumFactory { /** * Uses a driver specified by the 'SELENIUM_DRIVER' system property or the environment variable, * and run the test against the domain specified in 'SELENIUM_STARTING_URL' system property or the environment variable. * <p/> * <p/> * If exists, the system property takes precedence over the environment variable. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().createSelenium()}. */ public static Selenium create() { return new SeleniumFactory().createSelenium(); } /** * Uses a driver specified by the 'SELENIUM_DRIVER' system property or the environment variable, * and run the test against the domain specified in 'SELENIUM_STARTING_URL' system property or the environment variable. * <p/> * <p/> * If exists, the system property takes precedence over the environment variable. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().createWebDriver()}. */ public static WebDriver createWebDriver() { return new SeleniumFactory().createWebDriverInstance(null); } public static List<WebDriver> createWebDrivers() { String url = readPropertyOrEnv("SELENIUM_STARTING_URL", readPropertyOrEnv("DEFAULT_SELENIUM_STARTING_URL", null)); return new SeleniumFactory().createWebDriverInstances(url); } /** * Uses a driver specified by the 'SELENIUM_DRIVER' system property or the environment variable, * and run the test against the domain specified in 'SELENIUM_STARTING_URL' system property or the environment variable. * <p/> * <p/> * If exists, the system property takes precedence over the environment variable. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().createWebDriver()}. * * @param capabilities The desired driver capabilities to use, browser, browser version and os will be override. */ public static WebDriver createWebDriver(DesiredCapabilities capabilities) { return new SeleniumFactory().createWebDriverInstance(capabilities); } /** * Uses a driver specified by the 'SELENIUM_DRIVER' system property or the environment variable, * and run the test against the specified domain. * <p/> * <p/> * If exists, the system property takes precedence over the environment variable. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().createSelenium(browserURL)}. * * @param browserURL See the parameter of the same name in {@link DefaultSelenium#DefaultSelenium(String, int, String, String)}. * This specifies the domain name in the format of "http://foo.example.com" where the test occurs. */ public static Selenium create(String browserURL) { return new SeleniumFactory().createSelenium(browserURL); } /** * Uses the specified driver and the test domain and create a driver instance. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().setUri(driverUri).createSelenium(browserURL)}. * * @param driverUri The URI indicating the Selenium driver to be instantiated. * @param browserURL See the parameter of the same name in {@link DefaultSelenium#DefaultSelenium(String, int, String, String)}. * This specifies the domain name in the format of "http://foo.example.com" where the test occurs. */ public static Selenium create(String driverUri, String browserURL) { return new SeleniumFactory().setUri(driverUri).createSelenium(browserURL); } /** * Uses the specified driver and the test domain and create a WebDriver instance. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().setUri(driverUri).createSelenium(browserURL)}. * * @param driverUri The URI indicating the Selenium driver to be instantiated. * @param browserURL This specifies the domain name in the format of "http://foo.example.com" where the test occurs. */ public static WebDriver createWebDriver(String driverUri, String browserURL) { return new SeleniumFactory().setUri(driverUri).createWebDriverInstance(browserURL, null); } /** * Uses the specified driver and the test domain and create a WebDriver instance. * <p/> * <p/> * This is just a convenient short-cut for {@code new SeleniumFactory().setUri(driverUri).createSelenium(browserURL)}. * * @param driverUri The URI indicating the Selenium driver to be instantiated. * @param browserURL This specifies the domain name in the format of "http://foo.example.com" where the test occurs. * @param capabilities The desired driver capabilities to use, browser, browser version and os will be override. */ public static WebDriver createWebDriver(String driverUri, String browserURL, DesiredCapabilities capabilities) { return new SeleniumFactory().setUri(driverUri).createWebDriverInstance(browserURL, capabilities); } private String uri; private ClassLoader cl = Thread.currentThread().getContextClassLoader(); private Map<String, Object> properties = new HashMap<String, Object>(); public SeleniumFactory() { // use the embedded RC as the default, since this is the least environment dependent. uri = readPropertyOrEnv("SELENIUM_DRIVER", readPropertyOrEnv("DEFAULT_SELENIUM_DRIVER", "embedded-rc:")); } private static String readPropertyOrEnv(String key, String defaultValue) { String v = System.getProperty(key); if (v == null) v = System.getenv(key); if (v == null) v = defaultValue; return v; } /** * Gets the driver URI set by the {@link #setUri(String)} */ public String getUri() { return uri; } /** * Sets the URI of the Selenium driver. * <p/> * Initially, the value of the 'SELENIUM_DRIVER' system property of the environment variable is read * and set. The system property takes precedence over the environment variable. * * @return 'this' instance to facilitate the fluent API pattern. */ public SeleniumFactory setUri(String uri) { this.uri = uri; return this; } /** * Gets the classloader set by the {@link #setClassLoader(ClassLoader)}. */ public ClassLoader getClassLoader() { return cl; } /** * Sets the classloader used for searching the driver. * Initially set to {@code Thread.currentThread().getContextClassLoader()} of the thread * that instantiated this factory. * * @return 'this' instance to facilitate the fluent API pattern. */ public SeleniumFactory setClassLoader(ClassLoader cl) { this.cl = cl; return this; } /** * Sets other misc. driver-specific properties. Refer to the driver implementation * for the valid properties and their expected types. * * @return 'this' instance to facilitate the fluent API pattern. */ public SeleniumFactory setProperty(String key, Object value) { this.properties.put(key, value); return this; } /** * Retrieves the value of the property previously set. */ public Object getProperty(String key) { return this.properties.get(key); } /** * Returns the live map that stores the property values. * Convenient for bulk update operations. * * @return never null */ public Map<String, Object> getProperties() { return this.properties; } /** * Creates a clone of this factory that's identically configured. * <p/> * Properties are only shallowly copied. */ public SeleniumFactory clone() { SeleniumFactory f = new SeleniumFactory(); f.uri = uri; f.cl = cl; f.properties.clear(); f.properties.putAll(properties); return f; } /** * Based on the current configuration, instantiate a Selenium driver * and returns it. * <p/> * <p/> * This version implicitly retrieves the 'browserURL' parameter and * calls into {@link #createSelenium(String)} by checking the 'SELENIUM_STARTING_URL' * system property or the environment variable. The system property takes precedence over the environment variable. * * @return never null * @throws IllegalArgumentException if the configuration is invalid, or the driver failed to instantiate. */ public Selenium createSelenium() { String url = readPropertyOrEnv("SELENIUM_STARTING_URL", readPropertyOrEnv("DEFAULT_SELENIUM_STARTING_URL", null)); if (url == null) throw new IllegalArgumentException("Neither SELENIUM_STARTING_URL/DEFAULT_SELENIUM_STARTING_URL system property nor environment variable exists"); return createSelenium(url); } /** * Based on the current configuration, instantiate a Selenium driver * and returns it. * <p/> * <p/> * This version implicitly retrieves the 'browserURL' parameter and * calls into {@link #createSelenium(String)} by checking the 'SELENIUM_STARTING_URL' * system property or the environment variable. The system property takes precedence over the environment variable. * * @param capabilities The desired driver capabilities to use, browser, browser version and os will be override. * @return never null * @throws IllegalArgumentException if the configuration is invalid, or the driver failed to instantiate. */ public WebDriver createWebDriverInstance(DesiredCapabilities capabilities) { String url = readPropertyOrEnv("SELENIUM_STARTING_URL", readPropertyOrEnv("DEFAULT_SELENIUM_STARTING_URL", null)); return createWebDriverInstance(url, capabilities); } public Selenium createSelenium(String browserURL) { SeleniumFactorySPI seleniumFactory = createSeleniumFactory(); Selenium selenium = seleniumFactory.createSelenium(this, browserURL); if (selenium == null) { throw new IllegalArgumentException(String.format( "Unrecognized Selenium driver URI '%s'. Make sure you got the proper driver jars in your classpath, or increase the logging level to get more information.", uri)); } else { return selenium; } } public WebDriver createWebDriverInstance(String browserURL, DesiredCapabilities capabilities) { SeleniumFactorySPI seleniumFactory = createSeleniumFactory(); WebDriver webDriver = seleniumFactory.createWebDriver(this, browserURL, capabilities); if (webDriver == null) { throw new IllegalArgumentException(String.format( "Unrecognized Selenium driver URI '%s'. Make sure you got the proper driver jars in your classpath, or increase the logging level to get more information.", uri)); } else { return webDriver; } } private List<WebDriver> createWebDriverInstances(String browserURL) { SeleniumFactorySPI seleniumFactory = createSeleniumFactory(); List<WebDriver> webDrivers = seleniumFactory.createWebDrivers(this, browserURL); if (webDrivers == null) { throw new IllegalArgumentException(String.format( "Unrecognized Selenium driver URI '%s'. Make sure you got the proper driver jars in your classpath, or increase the logging level to get more information.", uri)); } else { return webDrivers; } } /** * Based on the current configuration, instantiate a Selenium driver * and returns it. * * @return never null * @throws IllegalArgumentException if the configuration is invalid, or the driver failed to instantiate. */ private SeleniumFactorySPI createSeleniumFactory() { try { if (uri == null) throw new IllegalArgumentException("Selenium driver URI is not set"); Enumeration<URL> e = cl.getResources("META-INF/services/" + SeleniumFactorySPI.class.getName()); SeleniumFactorySPI seleniumFactory = null; while (e.hasMoreElements()) { URL url = e.nextElement(); LOGGER.fine("Reading " + url + " looking for " + SeleniumFactorySPI.class.getName()); BufferedReader in = new LineNumberReader(new InputStreamReader(url.openStream(), "UTF-8")); try { String line; while ((line = in.readLine()) != null) { line = line.trim(); if (line.startsWith("#")) continue; // comment // otherwise treat this as FQCN LOGGER.fine("Found " + line); try { Class<?> c = cl.loadClass(line); LOGGER.fine("Loaded " + c); Object _spi = c.newInstance(); boolean canHandleRequest = false; if (_spi instanceof SeleniumFactorySPI) { seleniumFactory = (SeleniumFactorySPI) _spi; if (seleniumFactory.canHandle(uri)) { return seleniumFactory; } } if (!canHandleRequest) { URL img = c.getClassLoader().getResource(SeleniumFactorySPI.class.getName().replace('.', '/') + ".class"); LOGGER.log(WARNING, url + " specifies an SPI class " + line + " but the class isn't assignable to " + SeleniumFactorySPI.class + ". It's loading SPI from " + img); } } catch (ClassNotFoundException x) { LOGGER.log(WARNING, url + " specifies an SPI class " + line + " but the class failed to load", x); } catch (InstantiationException x) { LOGGER.log(WARNING, url + " specifies an SPI class " + line + " but the class failed to instantiate", x); } catch (IllegalAccessException x) { LOGGER.log(WARNING, url + " specifies an SPI class " + line + " but the class failed to instantiate", x); } } } finally { in.close(); } } if (seleniumFactory == null) { throw new IllegalArgumentException(String.format( "Unrecognized Selenium driver URI '%s'. Make sure you got the proper driver jars in your classpath, or increase the logging level to get more information.", uri)); } else { return seleniumFactory; } } catch (IOException x) { throw new IllegalArgumentException("Failed to instantiate the driver", x); } } private static final Logger LOGGER = Logger.getLogger(SeleniumFactory.class.getName()); }