com.machinepublishers.jbrowserdriver.JBrowserDriver.java Source code

Java tutorial

Introduction

Here is the source code for com.machinepublishers.jbrowserdriver.JBrowserDriver.java

Source

/* 
 * jBrowserDriver (TM)
 * Copyright (C) 2014-2017 jBrowserDriver committers
 * https://github.com/MachinePublishers/jBrowserDriver
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.machinepublishers.jbrowserdriver;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Sequence;
import org.openqa.selenium.interactions.SourceType;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.remote.CommandExecutor;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.ErrorHandler;
import org.openqa.selenium.remote.FileDetector;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.SessionId;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.listener.ProcessListener;
import org.zeroturnaround.exec.stream.LogOutputStream;
import org.zeroturnaround.process.PidProcess;
import org.zeroturnaround.process.Processes;

import com.google.common.collect.ImmutableMap;
import com.machinepublishers.jbrowserdriver.diagnostics.Test;

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;

/**
 * A Selenium-compatible and WebKit-based web driver written in pure Java.
 * <p>
 * See <a href="https://github.com/machinepublishers/jbrowserdriver#usage">
 * https://github.com/machinepublishers/jbrowserdriver#usage</a> for basic usage info.
 * <p>
 * Licensed under the Apache License version 2.0.
 */
public class JBrowserDriver extends RemoteWebDriver {

    //TODO handle jbd.fork=false

    /**
     * This can be passed to sendKeys to delete all the text in a text field.
     * 
     * @deprecated send {@link org.openqa.selenium.Keys#CONTROL Ctrl}+a (not Ctrl+A) chord and then {@link org.openqa.selenium.Keys#BACK_SPACE BACK_SPACE}.
     */
    @Deprecated
    public static final String KEYBOARD_DELETE = Util.KEYBOARD_DELETE;
    private static final AtomicInteger runningInstances = new AtomicInteger(0);
    private static final Set<SocketLock> locks = new HashSet<SocketLock>();
    private static final Set<Job> waiting = new LinkedHashSet<Job>();
    private static final Set<PortGroup> portGroupsActive = new LinkedHashSet<PortGroup>();
    private static final String JAVA_BIN;
    private static final List<String> inheritedArgs;
    private static volatile List<String> classpathSimpleArgs;
    private static volatile List<String> classpathUnpackedArgs;
    private static final AtomicReference<List<String>> classpathArgs = new AtomicReference<>();
    private static final AtomicBoolean firstLaunch = new AtomicBoolean(true);
    private static final Set<String> filteredLogs = Collections.unmodifiableSet(new HashSet<String>(
            Arrays.asList(new String[] { "Warning: Single GUI Threadiong is enabled, FPS should be slower" })));
    private static final AtomicLong sessionIdCounter = new AtomicLong();

    static {
        List<String> inheritedArgsTmp = new ArrayList<String>();
        File javaBin = new File(System.getProperty("java.home") + "/bin/java");
        if (!javaBin.exists()) {
            javaBin = new File(javaBin.getAbsolutePath() + ".exe");
        }
        JAVA_BIN = javaBin.getAbsolutePath();
        try {
            for (Object keyObj : System.getProperties().keySet()) {
                String key = keyObj.toString();
                if (key != null && key.startsWith("jbd.rmi.")) {
                    inheritedArgsTmp.add("-D" + key.substring("jbd.rmi.".length()) + "=" + System.getProperty(key));
                }
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
        inheritedArgs = Collections.unmodifiableList(inheritedArgsTmp);

    }

    private static void initClasspath() {
        List<String> classpathSimpleTmp = new ArrayList<String>();
        List<String> classpathUnpackedTmp = new ArrayList<String>();
        try {
            List<File> classpathElements = new FastClasspathScanner().getUniqueClasspathElements();
            final File classpathDir = Files.createTempDirectory("jbd_classpath_").toFile();
            Runtime.getRuntime().addShutdownHook(new FileRemover(classpathDir));
            List<String> pathsSimple = new ArrayList<String>();
            List<String> pathsUnpacked = new ArrayList<String>();
            for (File curElement : classpathElements) {
                String rootLevelElement = curElement.getAbsoluteFile().toURI().toURL().toExternalForm();
                pathsSimple.add(rootLevelElement);
                pathsUnpacked.add(rootLevelElement);
                if (curElement.isFile() && curElement.getPath().endsWith(".jar")) {
                    try (ZipFile jar = new ZipFile(curElement)) {
                        Enumeration<? extends ZipEntry> entries = jar.entries();
                        while (entries.hasMoreElements()) {
                            ZipEntry entry = entries.nextElement();
                            if (entry.getName().endsWith(".jar")) {
                                try (InputStream in = jar.getInputStream(entry)) {
                                    File childJar = new File(classpathDir, Util.randomFileName() + ".jar");
                                    Files.copy(in, childJar.toPath());
                                    pathsUnpacked.add(childJar.getAbsoluteFile().toURI().toURL().toExternalForm());
                                    childJar.deleteOnExit();
                                }
                            }
                        }
                    }
                }
            }
            classpathSimpleTmp = createClasspathJar(classpathDir, "classpath-simple.jar", pathsSimple);
            classpathUnpackedTmp = createClasspathJar(classpathDir, "classpath-unpacked.jar", pathsUnpacked);
        } catch (Throwable t) {
            Util.handleException(t);
        }
        classpathSimpleArgs = Collections.unmodifiableList(classpathSimpleTmp);
        classpathUnpackedArgs = Collections.unmodifiableList(classpathUnpackedTmp);
    }

    private static List<String> createClasspathJar(File dir, String jarName, List<String> manifestClasspath)
            throws IOException {
        List<String> classpathArgs = new ArrayList<String>();
        Manifest manifest = new Manifest();
        manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
        manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, StringUtils.join(manifestClasspath, ' '));
        File classpathJar = new File(dir, jarName);
        classpathJar.deleteOnExit();
        try (JarOutputStream stream = new JarOutputStream(new FileOutputStream(classpathJar), manifest)) {
        }
        classpathArgs.add("-classpath");
        classpathArgs.add(classpathJar.getCanonicalPath());
        return classpathArgs;
    }

    public static void initWorkThread() {
        int previousInstanceCount = runningInstances.getAndIncrement();
        if (previousInstanceCount > 0) {
            return;
        }
        Thread work = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (waiting) {
                    while (runningInstances.get() > 0) {
                        List<Job> selectedJobs = new ArrayList<Job>();
                        for (Job job : waiting) {
                            for (PortGroup curPortGroup : job.settings.portGroups()) {
                                boolean conflicts = false;
                                for (PortGroup curUsed : portGroupsActive) {
                                    if (curUsed.conflicts(curPortGroup)) {
                                        conflicts = true;
                                        break;
                                    }
                                }
                                if (!conflicts) {
                                    job.portGroup.set(curPortGroup);
                                    break;
                                }
                            }
                            if (job.portGroup.get() != null) {
                                selectedJobs.add(job);
                                portGroupsActive.add(job.portGroup.get());
                            }
                        }
                        for (Job job : selectedJobs) {
                            waiting.remove(job);
                            synchronized (job) {
                                job.notifyAll();
                            }
                        }
                        try {
                            waiting.wait();
                        } catch (InterruptedException e) {
                        }
                    }
                }
            }
        });
        work.setDaemon(true);
        work.setName("JBrowserDriver queued instance handler");
        work.start();
    }

    /**
     * Run diagnostic tests.
     * 
     * @return Errors or an empty list if no errors found.
     */
    public static List<String> test() {
        return Test.run();
    }

    private final JBrowserDriverRemote remote;
    private final Logs logs;
    private final AtomicReference<Process> process = new AtomicReference<Process>();
    private final AtomicBoolean processEnded = new AtomicBoolean();
    private final AtomicReference<PortGroup> configuredPortGroup = new AtomicReference<PortGroup>();
    private final AtomicReference<PortGroup> actualPortGroup = new AtomicReference<PortGroup>();
    private final AtomicReference<OptionsLocal> options = new AtomicReference<OptionsLocal>();
    private final SessionId sessionId;
    private final SocketLock lock = new SocketLock();
    private final File tmpDir;
    private final FileRemover shutdownHook;
    private final Thread heartbeatThread;

    /**
     * Constructs a browser with default settings, UTC timezone, and no proxy.
     */
    public JBrowserDriver() {
        this(Settings.builder().build());
    }

    /**
     * Use {@link Settings#builder()} ...buildCapabilities() to create settings to pass to this constructor.
     * 
     * This constructor is mostly useful for Selenium Server itself to use.
     * 
     * @param capabilities
     */
    public JBrowserDriver(Capabilities capabilities) {
        this(Settings.builder().build(capabilities));
        Map capabilitiesMap = new HashMap(capabilities.asMap());
        capabilitiesMap.remove("proxy");
        try {
            synchronized (lock.validated()) {
                remote.storeCapabilities(new MutableCapabilities(capabilitiesMap));
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Use {@link Settings#builder()} ...build() to create settings to pass to this constructor.
     * 
     * @param settings
     */
    public JBrowserDriver(final Settings settings) {
        initWorkThread();
        synchronized (locks) {
            locks.add(lock);
        }
        File tmpDir = null;
        try {
            tmpDir = Files.createTempDirectory("jbd_tmp_").toFile();
        } catch (Throwable t) {
            Util.handleException(t);
        }
        this.tmpDir = tmpDir;
        this.shutdownHook = new FileRemover(tmpDir);
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        final Job job = new Job(settings, configuredPortGroup);
        synchronized (waiting) {
            waiting.add(job);
            waiting.notifyAll();
        }
        synchronized (job) {
            while (configuredPortGroup.get() == null) {
                try {
                    job.wait();
                } catch (InterruptedException e) {
                }
            }
        }
        SessionId sessionIdTmp = null;
        if (!settings.customClasspath()) {
            synchronized (firstLaunch) {
                if (firstLaunch.compareAndSet(true, false)) {
                    initClasspath();
                    classpathArgs.set(classpathUnpackedArgs);
                    sessionIdTmp = new SessionId(launchProcess(settings, configuredPortGroup.get()));
                    if (actualPortGroup.get() == null) {
                        classpathArgs.set(classpathSimpleArgs);
                    }
                }
            }
        }
        if (actualPortGroup.get() == null) {
            sessionIdTmp = new SessionId(launchProcess(settings, configuredPortGroup.get()));
        }
        sessionId = sessionIdTmp;
        if (actualPortGroup.get() == null) {
            endProcess();
            Util.handleException(new IllegalStateException("Could not launch browser."));
        }
        HeartbeatRemote heartbeatTmp = null;
        JBrowserDriverRemote instanceTmp = null;
        try {
            synchronized (lock.validated()) {
                Registry registry = LocateRegistry.getRegistry(settings.host(), (int) actualPortGroup.get().child,
                        new SocketFactory(settings.host(), actualPortGroup.get(), locks));
                heartbeatTmp = (HeartbeatRemote) registry.lookup("HeartbeatRemote");
                instanceTmp = (JBrowserDriverRemote) registry.lookup("JBrowserDriverRemote");
                instanceTmp.setUp(settings);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
        final HeartbeatRemote heartbeat = heartbeatTmp;
        heartbeatThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (processEnded.get()) {
                        return;
                    }
                    try {
                        heartbeat.heartbeat();
                    } catch (RemoteException e) {
                    }
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                    }
                }
            }
        });
        heartbeatThread.setName("Heartbeat");
        heartbeatThread.start();
        remote = instanceTmp;
        LogsRemote logsRemote = null;
        try {
            synchronized (lock.validated()) {
                logsRemote = remote.logs();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
        logs = new Logs(logsRemote, lock);
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            super.finalize();
        } catch (Throwable t) {
        }
        try {
            Runtime.getRuntime().removeShutdownHook(shutdownHook);
            shutdownHook.run();
        } catch (Throwable t) {
        }
    }

    private String launchProcess(final Settings settings, final PortGroup portGroup) {
        final AtomicBoolean ready = new AtomicBoolean();
        final AtomicReference<String> logPrefix = new AtomicReference<String>("");
        new Thread(new Runnable() {
            @Override
            public void run() {
                List<String> myArgs = new ArrayList<String>();
                myArgs.add(settings.javaBinary() == null ? JAVA_BIN : settings.javaBinary());
                myArgs.addAll(inheritedArgs);
                if (!settings.customClasspath()) {
                    myArgs.addAll(classpathArgs.get());
                }
                if (settings.javaExportModules()) {
                    myArgs.add("-XaddExports:javafx.web/com.sun.webkit.network=ALL-UNNAMED");
                    myArgs.add("-XaddExports:javafx.web/com.sun.webkit.network.about=ALL-UNNAMED");
                    myArgs.add("-XaddExports:javafx.web/com.sun.webkit.network.data=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.http=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.https=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.file=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.ftp=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.jar=ALL-UNNAMED");
                    myArgs.add("-XaddExports:java.base/sun.net.www.protocol.mailto=ALL-UNNAMED");
                    myArgs.add("-XaddExports:javafx.graphics/com.sun.glass.ui=ALL-UNNAMED");
                    myArgs.add("-XaddExports:javafx.web/com.sun.javafx.webkit=ALL-UNNAMED");
                    myArgs.add("-XaddExports:javafx.web/com.sun.webkit=ALL-UNNAMED");
                }
                myArgs.add("-Djava.io.tmpdir=" + tmpDir.getAbsolutePath());
                myArgs.add("-Djava.rmi.server.hostname=" + settings.host());
                myArgs.addAll(settings.javaOptions());
                myArgs.add(JBrowserDriverServer.class.getName());
                myArgs.add(Long.toString(portGroup.child));
                myArgs.add(Long.toString(portGroup.parent));
                myArgs.add(Long.toString(portGroup.parentAlt));
                try {
                    new ProcessExecutor().addListener(new ProcessListener() {
                        @Override
                        public void afterStart(Process proc, ProcessExecutor executor) {
                            process.set(proc);
                        }
                    }).redirectOutput(new LogOutputStream() {
                        boolean done = false;

                        @Override
                        protected void processLine(String line) {
                            if (line != null && !line.isEmpty()) {
                                if (!done) {
                                    synchronized (ready) {
                                        if (line.startsWith("ready on ports ")) {
                                            String[] parts = line.substring("ready on ports ".length()).split("/");
                                            actualPortGroup.set(new PortGroup(Integer.parseInt(parts[0]),
                                                    Integer.parseInt(parts[1]), Integer.parseInt(parts[2])));
                                            logPrefix.set(new StringBuilder().append("[Instance ")
                                                    .append(sessionIdCounter.incrementAndGet()).append("][Port ")
                                                    .append(actualPortGroup.get().child).append("]").toString());
                                            ready.set(true);
                                            ready.notifyAll();
                                            done = true;
                                        } else {
                                            log(settings.logger(), logPrefix.get(), line);
                                        }
                                    }
                                } else {
                                    log(settings.logger(), logPrefix.get(), line);
                                }
                            }
                        }
                    }).redirectError(new LogOutputStream() {
                        @Override
                        protected void processLine(String line) {
                            log(settings.logger(), logPrefix.get(), line);
                        }
                    }).destroyOnExit().command(myArgs).execute();
                } catch (Throwable t) {
                    Util.handleException(t);
                }
                synchronized (ready) {
                    ready.set(true);
                    ready.notifyAll();
                }
            }
        }).start();
        synchronized (ready) {
            while (!ready.get()) {
                try {
                    ready.wait();
                    break;
                } catch (InterruptedException e) {
                }
            }
        }
        return logPrefix.get();
    }

    private static void log(Logger logger, String prefix, String message) {
        if (logger != null && !filteredLogs.contains(message)) {
            LogRecord record = null;
            if (message.startsWith(">")) {
                String[] parts = message.substring(1).split("/", 3);
                record = new LogRecord(Level.parse(parts[0]),
                        new StringBuilder().append(prefix).append(" ").append(parts[2]).toString());
                record.setSourceMethodName(parts[1]);
                record.setSourceClassName(JBrowserDriver.class.getName());
            } else {
                record = new LogRecord(Level.WARNING,
                        new StringBuilder().append(prefix).append(" ").append(message).toString());
                record.setSourceMethodName(null);
                record.setSourceClassName(JBrowserDriver.class.getName());
            }
            logger.log(record);
        }
    }

    /**
     * Optionally call this method if you want JavaFX initialized and the browser
     * window opened immediately. Otherwise, initialization will happen lazily.
     */
    public void init() {
        try {
            synchronized (lock.validated()) {
                remote.init();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Reset the state of the browser. More efficient than quitting the
     * browser and creating a new instance.
     * <p>
     * Note: it's not possible to switch between headless and GUI mode. You must quit this browser
     * and create a new instance.
     * 
     * @param settings
     *          New settings to take effect, superseding the original ones
     */
    public void reset(final Settings settings) {
        //TODO clear out tmp files except cache
        try {
            synchronized (lock.validated()) {
                remote.reset(settings);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Reset the state of the browser. More efficient than quitting the
     * browser and creating a new instance.
     * <p>
     * Note: it's not possible to switch between headless and GUI mode. You must quit this browser
     * and create a new instance.
     * 
     * @param capabilities
     *          Capabilities to take effect, superseding the original ones
     */
    public void reset(Capabilities capabilities) {
        //TODO clear out tmp files except cache
        Settings settings = Settings.builder().build(capabilities);
        try {
            synchronized (lock.validated()) {
                remote.reset(settings);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
        if (!(capabilities instanceof Serializable)) {
            capabilities = new MutableCapabilities(capabilities);
        }
        try {
            synchronized (lock.validated()) {
                remote.storeCapabilities(capabilities);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Reset the state of the browser. More efficient than quitting the
     * browser and creating a new instance.
     */
    public void reset() {
        try {
            synchronized (lock.validated()) {
                remote.reset();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPageSource() {
        try {
            synchronized (lock.validated()) {
                return remote.getPageSource();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCurrentUrl() {
        try {
            synchronized (lock.validated()) {
                return remote.getCurrentUrl();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * @return Status code of the response
     */
    public int getStatusCode() {
        try {
            synchronized (lock.validated()) {
                return remote.getStatusCode();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return -1;
        }
    }

    /**
     * Waits until requests are completed and idle for a certain
     * amount of time. This type of waiting happens automatically on
     * form submissions, page loads, mouse clicks, and text/keyboard
     * entry, so in those cases there's usually no need to call this
     * method. However, calling this method may be useful when requests
     * are triggered under other circumstances or if a more conservative
     * wait is needed.
     * <p>
     * The behavior of this wait algorithm can be configured by
     * {@link Settings.Builder#ajaxWait(long)} and
     * {@link Settings.Builder#ajaxResourceTimeout(long)}.
     */
    public void pageWait() {
        try {
            synchronized (lock.validated()) {
                remote.pageWait();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTitle() {
        try {
            synchronized (lock.validated()) {
                return remote.getTitle();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void get(final String url) {
        try {
            synchronized (lock.validated()) {
                remote.get(url);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElement(By by) {
        return by.findElement(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElements(By by) {
        return by.findElements(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementById(String id) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementById(id), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsById(String id) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsById(id);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByXPath(String expr) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByXPath(expr), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByXPath(String expr) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByXPath(expr);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByLinkText(final String text) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByLinkText(text), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByPartialLinkText(String text) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByPartialLinkText(text), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByLinkText(String text) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByLinkText(text);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByPartialLinkText(String text) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByPartialLinkText(text);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByClassName(String cssClass) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByClassName(cssClass), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByClassName(String cssClass) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByClassName(cssClass);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByName(String name) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByName(name), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByName(String name) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByName(name);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByCssSelector(String expr) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByCssSelector(expr), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByCssSelector(String expr) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByCssSelector(expr);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public WebElement findElementByTagName(String tagName) {
        try {
            synchronized (lock.validated()) {
                return Element.constructElement(remote.findElementByTagName(tagName), this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<WebElement> findElementsByTagName(String tagName) {
        try {
            List<ElementRemote> elements;
            synchronized (lock.validated()) {
                elements = remote.findElementsByTagName(tagName);
            }
            return Element.constructList(elements, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return new ArrayList<WebElement>();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object executeAsyncScript(String script, Object... args) {
        try {
            Object result;
            synchronized (lock.validated()) {
                result = remote.executeAsyncScript(script, Element.scriptParams(args));
            }
            return Element.constructObject(result, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object executeScript(String script, Object... args) {
        try {
            Object result;
            synchronized (lock.validated()) {
                result = remote.executeScript(script, Element.scriptParams(args));
            }
            return Element.constructObject(result, this, lock);
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public org.openqa.selenium.interactions.Keyboard getKeyboard() {
        try {
            synchronized (lock.validated()) {
                KeyboardRemote keyboard = remote.getKeyboard();
                if (keyboard == null) {
                    return null;
                }
                return new Keyboard(keyboard, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public org.openqa.selenium.interactions.Mouse getMouse() {
        try {
            synchronized (lock.validated()) {
                MouseRemote mouse = remote.getMouse();
                if (mouse == null) {
                    return null;
                }
                return new Mouse(mouse, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Capabilities getCapabilities() {
        try {
            synchronized (lock.validated()) {
                return remote.getCapabilities();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        try {
            synchronized (lock.validated()) {
                remote.close();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
        Set<String> handles = getWindowHandles();
        if (handles == null || handles.isEmpty()) {
            quit();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getWindowHandle() {
        try {
            synchronized (lock.validated()) {
                return remote.getWindowHandle();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<String> getWindowHandles() {
        try {
            synchronized (lock.validated()) {
                return remote.getWindowHandles();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Options manage() {
        if (options.get() == null) {
            try {
                synchronized (lock.validated()) {
                    OptionsRemote optionsRemote = remote.manage();
                    if (optionsRemote == null) {
                        return null;
                    }
                    return new com.machinepublishers.jbrowserdriver.Options(optionsRemote, logs, lock);
                }
            } catch (Throwable t) {
                Util.handleException(t);
                return null;
            }
        } else {
            return options.get();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Navigation navigate() {
        try {
            synchronized (lock.validated()) {
                NavigationRemote navigation = remote.navigate();
                if (navigation == null) {
                    return null;
                }
                return new com.machinepublishers.jbrowserdriver.Navigation(navigation, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    private void endProcess() {
        if (processEnded.compareAndSet(false, true)) {
            runningInstances.decrementAndGet();
            lock.expired.set(true);
            final Process proc = process.get();
            if (proc != null) {
                while (proc.isAlive()) {
                    try {
                        PidProcess pidProcess = Processes.newPidProcess(proc);
                        try {
                            if (!pidProcess.destroyGracefully().waitFor(10, TimeUnit.SECONDS)) {
                                throw new RuntimeException();
                            }
                        } catch (Throwable t1) {
                            if (!pidProcess.destroyForcefully().waitFor(10, TimeUnit.SECONDS)) {
                                throw new RuntimeException();
                            }
                        }
                    } catch (Throwable t2) {
                        try {
                            proc.destroyForcibly().waitFor(10, TimeUnit.SECONDS);
                        } catch (Throwable t3) {
                        }
                    }
                }
            }
            try {
                heartbeatThread.interrupt();
                heartbeatThread.join();
            } catch (Exception e) {
            }
            FileUtils.deleteQuietly(tmpDir);
            synchronized (locks) {
                locks.remove(lock);
            }
            synchronized (waiting) {
                portGroupsActive.remove(configuredPortGroup.get());
                waiting.notifyAll();
            }
        }
    }

    private void saveData() {
        try {
            synchronized (lock.validated()) {
                OptionsRemote optionsRemote = remote.manage();
                Set<Cookie> cookiesLocal = optionsRemote.getCookies();
                LogsRemote logsRemote = optionsRemote.logs();
                final LogEntries entries = logsRemote.getRemote(null).toLogEntries();
                final Set<String> types = logsRemote.getAvailableLogTypes();

                org.openqa.selenium.logging.Logs logsLocal = new org.openqa.selenium.logging.Logs() {
                    @Override
                    public Set<String> getAvailableLogTypes() {
                        return types;
                    }

                    @Override
                    public LogEntries get(String logType) {
                        return entries;
                    }
                };
                options.set(new OptionsLocal(cookiesLocal, logsLocal));
            }
        } catch (Throwable t) {
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void quit() {
        saveData();
        try {
            synchronized (lock.validated()) {
                remote.quit();
            }
        } catch (Throwable t) {
            Util.handleException(t);
        } finally {
            endProcess();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TargetLocator switchTo() {
        try {
            synchronized (lock.validated()) {
                TargetLocatorRemote locator = remote.switchTo();
                if (locator == null) {
                    return null;
                }
                return new com.machinepublishers.jbrowserdriver.TargetLocator(locator, this, lock);
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /*
     * This interface was removed in latest Selenium.
     * It remains here for backwards compatibility.
     * Ignore any warnings about an @Override annotation missing.
     */
    public void kill() {
        endProcess();
    }

    @Override
    public <X> X getScreenshotAs(final OutputType<X> outputType) throws WebDriverException {
        try {
            byte[] bytes;
            synchronized (lock.validated()) {
                bytes = remote.getScreenshot();
            }
            if (bytes == null) {
                return null;
            }
            return outputType.convertFromPngBytes(bytes);
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * @return Temporary directory where cached pages are saved.
     */
    public File cacheDir() {
        try {
            synchronized (lock.validated()) {
                return remote.cacheDir();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * @return Temporary directory where downloaded files are saved.
     */
    public File attachmentsDir() {
        try {
            synchronized (lock.validated()) {
                return remote.attachmentsDir();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * @return Temporary directory where media files are saved.
     */
    public File mediaDir() {
        try {
            synchronized (lock.validated()) {
                return remote.mediaDir();
            }
        } catch (Throwable t) {
            Util.handleException(t);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SessionId getSessionId() {
        return sessionId;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ErrorHandler getErrorHandler() {
        return super.getErrorHandler();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CommandExecutor getCommandExecutor() {
        return super.getCommandExecutor();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileDetector getFileDetector() {
        return super.getFileDetector();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void perform(Collection<Sequence> actions) {
        Map<String, Object> emptyPauseAction = ImmutableMap.of("duration", 0L, "type", "pause");

        EnumMap<SourceType, List<Map<String, Object>>> mappedActions = new EnumMap<>(SourceType.class);
        for (Sequence sequence : actions) {
            Map<String, Object> sequenceValues = sequence.toJson();
            SourceType sourceType = SourceType.valueOf(((String) sequenceValues.get("type")).toUpperCase());
            mappedActions.put(sourceType, (List<Map<String, Object>>) sequenceValues.get("actions"));
        }

        Element lastProcessedElement = null;
        int sequenceSize = mappedActions.values().iterator().next().size();
        for (int cursor = 0; cursor < sequenceSize; cursor++) {
            int counter = 0;
            for (Map.Entry<SourceType, List<Map<String, Object>>> actionEntry : mappedActions.entrySet()) {
                Map<String, Object> action = actionEntry.getValue().get(cursor);
                if (!emptyPauseAction.equals(action)) {
                    String actionType = (String) action.get("type");
                    Object executor = chooseExecutor(actionEntry.getKey());
                    lastProcessedElement = W3CActions.findActionByType(actionType).perform(executor,
                            lastProcessedElement, action);
                    break;
                }
                if (counter == mappedActions.entrySet().size() - 1) {
                    W3CActions.PAUSE.perform(getMouse(), lastProcessedElement, emptyPauseAction);
                } else {
                    counter++;
                }
            }
        }
    }

    private Object chooseExecutor(SourceType sourceType) {
        switch (sourceType) {
        case KEY:
            return getKeyboard();
        case POINTER:
            return getMouse();
        default:
            throw new IllegalArgumentException("Source type with name " + sourceType + " is not supported");
        }
    }
}