package jp.vmi.selenium.webdriver; import java.io.File; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.openqa.selenium.remote.DesiredCapabilities; import com.google.common.collect.Maps; import jp.vmi.selenium.selenese.config.IConfig; /** * Options for WebDriver. */ public class DriverOptions { // private static final Logger log = LoggerFactory.getLogger(DriverOptions.class); /** * WebDriver option. */ public enum DriverOption { /** --profile */ PROFILE, /** --profile-dir */ PROFILE_DIR, /** --proxy */ PROXY, /** --proxy-user */ PROXY_USER, /** --proxy-password */ PROXY_PASSWORD, /** --no-proxy */ NO_PROXY, /** --firefox */ FIREFOX, /** --geckodriver */ GECKODRIVER, /** --chromedriver */ CHROMEDRIVER, /** --iedriver */ IEDRIVER, /** --phantomjs */ PHANTOMJS, /** --remote-platform */ REMOTE_PLATFORM, /** --remote-browser */ REMOTE_BROWSER, /** --remote-version */ REMOTE_VERSION, /** --remote-url */ REMOTE_URL, /** --width */ WIDTH, /** --height */ HEIGHT, /** --define */ DEFINE, /** --cli-args */ CLI_ARGS, /** --chrome-extension */ CHROME_EXTENSION, /** --chrome-experimental-options */ CHROME_EXPERIMENTAL_OPTIONS, ; /** * Get option name as "word-word-word". * * @return option name. */ public String optionName() { return name().toLowerCase().replace('_', '-'); } } private static final Pattern DEFINE_RE = Pattern.compile("(?<name>[^:+=]+)(?::(?<type>\\w+))?(?<add>\\+)?=(?<value>.*)"); private final IdentityHashMap<DriverOptions.DriverOption, String> map = Maps.newIdentityHashMap(); private final DesiredCapabilities caps = new DesiredCapabilities(); private String[] cliArgs = ArrayUtils.EMPTY_STRING_ARRAY; private List<File> chromeExtensions = new ArrayList<>(); private final HashMap<String, String> envVars = Maps.newHashMap(); /** * Constructs empty options. */ public DriverOptions() { } /** * Constructs driver options specified by command line. * * @param config configuration information. */ public DriverOptions(IConfig config) { for (DriverOption opt : DriverOption.values()) { switch (opt) { case DEFINE: case CLI_ARGS: case CHROME_EXTENSION: String[] values = config.get(opt.optionName()); if (values != null) set(opt, values); break; default: set(opt, (String) config.get(opt.optionName())); break; } } } /** * Constructs clone of DriverOptions. * * @param other other DriverOptions. */ public DriverOptions(DriverOptions other) { map.putAll(other.map); caps.merge(other.caps); cliArgs = other.cliArgs; chromeExtensions = other.chromeExtensions; envVars.putAll(other.envVars); } /** * Get option value. * * @param opt option key. * @return option value. */ public String get(DriverOption opt) { switch (opt) { case DEFINE: throw new IllegalArgumentException("Need to use DriverOptions#getCapabilities() instead of get(DriverOption.DEFINE)."); case CLI_ARGS: throw new IllegalArgumentException("Need to use DriverOptions#getExtraOptions() instead of get(DriverOption.CLI_ARGS)."); case CHROME_EXTENSION: throw new IllegalArgumentException("Need to use DriverOptions#getExtraOptions() instead of get(DriverOption.CHROME_EXTENSION)."); default: return map.get(opt); } } /** * DriverOptions instance has specified option. * * @param opt option key. * @return true if has specified option. */ public boolean has(DriverOption opt) { switch (opt) { case DEFINE: return !caps.asMap().isEmpty(); case CLI_ARGS: return cliArgs.length != 0; case CHROME_EXTENSION: return !chromeExtensions.isEmpty(); default: return map.containsKey(opt); } } /** * Set option key and value. * * @param opt option key. * @param values option values. (multiple values are accepted by DEFINE and CLI_ARGS only) * @return this. */ public DriverOptions set(DriverOption opt, String... values) { switch (opt) { case DEFINE: addDefinitions(values); break; case CLI_ARGS: cliArgs = ArrayUtils.addAll(cliArgs, values); break; case CHROME_EXTENSION: for (String ext : values) chromeExtensions.add(new File(ext)); break; default: if (values.length != 1) throw new IllegalArgumentException("Need to pass only a single value for " + opt); if (values[0] != null) map.put(opt, values[0]); else map.remove(opt); break; } return this; } private static Object getTypedValue(String type, String value, Consumer<String> errorHandler) { if (type == null) return value; switch (type) { case "str": return value; case "int": try { return Integer.valueOf(value); } catch (NumberFormatException e) { errorHandler.accept("\"" + value + "\" is not integer"); return null; } case "bool": return Boolean.valueOf(value); default: errorHandler.accept("unrecognized type: " + type); return null; } } private static String getTypeName(Object value) { Class<?> clazz = value.getClass(); if (clazz.isArray()) clazz = clazz.getComponentType(); if (clazz == String.class) return "str"; else if (clazz == Boolean.class) return "bool"; else return "(unknown)"; } private void appendCapValue(String name, Object value, Consumer<String> errorHandler) { Object prevValue = caps.getCapability(name); Object[] newValue; try { if (prevValue == null || prevValue.getClass().isArray()) { newValue = ArrayUtils.add((Object[]) prevValue, value); } else { newValue = (Object[]) Array.newInstance(prevValue.getClass(), 2); newValue[0] = prevValue; newValue[1] = value; } } catch (ArrayStoreException e) { errorHandler.accept(String.format("the expected type is %s, but the actual type is %s", getTypeName(prevValue), getTypeName(value))); return; } caps.setCapability(name, newValue); } /** * Add "define" parameters. * * @param defs definitions. * @return this. */ public DriverOptions addDefinitions(String... defs) { if (defs == null) return this; List<String> errors = new ArrayList<>(); for (String def : defs) { Matcher matcher = DEFINE_RE.matcher(def); if (!matcher.matches()) { errors.add("[" + def + "] => invalid format (neither key[:type]=value nor key[:type]+=value)"); continue; } String name = matcher.group("name"); Object value = getTypedValue(matcher.group("type"), matcher.group("value"), (msg) -> errors.add("[" + def + "] => " + msg)); if (value == null) continue; String add = matcher.group("add"); if (add == null) caps.setCapability(name, value); else appendCapValue(name, value, (msg) -> errors.add("[" + def + "] => " + msg)); } if (!errors.isEmpty()) throw new IllegalArgumentException(errors.stream().collect(Collectors.joining(" / "))); return this; } /** * Get CLI arguments for starting up driver. * * @return CLI arguments. */ public String[] getCliArgs() { return cliArgs; } /** * Get Chrome Extensions for starting up driver. * * @return Chrome Extension files. */ public List<File> getChromeExtensions() { return chromeExtensions; } /** * Get environment variables map. * * @return environment variables map. */ public Map<String, String> getEnvVars() { return envVars; } private static final Comparator<Entry<String, ?>> mapEntryComparator = new Comparator<Map.Entry<String, ?>>() { @Override public int compare(Entry<String, ?> e1, Entry<String, ?> e2) { return e1.getKey().compareTo(e2.getKey()); } }; @Override public String toString() { StringBuilder result = new StringBuilder("["); String sep = ""; if (!map.isEmpty()) { for (DriverOption opt : DriverOption.values()) { switch (opt) { case DEFINE: // skip break; case CLI_ARGS: if (cliArgs.length != 0) { result.append(opt.optionName()).append('='); for (String extraOption : cliArgs) result.append(extraOption).append(','); result.setCharAt(result.length() - 1, '|'); } break; case CHROME_EXTENSION: if (!chromeExtensions.isEmpty()) { result.append(opt.optionName()).append('='); for (File extraOption : chromeExtensions) result.append(extraOption.toString()).append(','); result.setCharAt(result.length() - 1, '|'); } break; default: if (map.containsKey(opt)) result.append(opt.optionName()).append('=').append(map.get(opt)).append('|'); break; } } result.deleteCharAt(result.length() - 1); sep = "|"; } @SuppressWarnings("unchecked") Map<String, Object> capsMap = (Map<String, Object>) caps.asMap(); if (!capsMap.isEmpty()) { result.append(sep).append("DEFINE=[\n"); List<Entry<String, Object>> capsList = new ArrayList<>(capsMap.entrySet()); Collections.sort(capsList, mapEntryComparator); for (Entry<String, Object> cap : capsList) { Object value = cap.getValue(); if (value instanceof Object[]) value = StringUtils.join((Object[]) value, ", "); result.append(" ").append(cap.getKey()).append('=').append(value).append("\n"); } result.append(']'); sep = "|"; } if (!envVars.isEmpty()) { result.append(sep).append("ENV_VARS=[\n"); List<Entry<String, String>> envVarsList = new ArrayList<>(envVars.entrySet()); Collections.sort(envVarsList, mapEntryComparator); for (Entry<String, String> envVar : envVarsList) result.append(" ").append(envVar.getKey()).append('=').append(envVar.getValue()).append("\n"); result.append(']'); } result.append(']'); return result.toString(); } /** * Get desired capabilities. * * @return desired capabilities. */ public DesiredCapabilities getCapabilities() { return caps; } }