org.openqa.selenium.server.SeleniumDriverResourceHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openqa.selenium.server.SeleniumDriverResourceHandler.java

Source

// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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 org.openqa.selenium.server;

import com.google.common.base.Throwables;
import com.google.common.io.Resources;

import org.apache.commons.logging.Log;
import org.openqa.jetty.http.HttpConnection;
import org.openqa.jetty.http.HttpException;
import org.openqa.jetty.http.HttpFields;
import org.openqa.jetty.http.HttpRequest;
import org.openqa.jetty.http.HttpResponse;
import org.openqa.jetty.http.handler.ResourceHandler;
import org.openqa.jetty.log.LogFactory;
import org.openqa.jetty.util.StringUtil;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.io.TemporaryFilesystem;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.server.log.LoggingManager;
import org.openqa.selenium.remote.server.log.PerSessionLogHandler;
import org.openqa.selenium.server.BrowserSessionFactory.BrowserSessionInfo;
import org.openqa.selenium.server.browserlaunchers.BrowserLauncher;
import org.openqa.selenium.server.browserlaunchers.BrowserLauncherFactory;
import org.openqa.selenium.server.browserlaunchers.BrowserOptions;
import org.openqa.selenium.server.browserlaunchers.InvalidBrowserExecutableException;
import org.openqa.selenium.server.browserlaunchers.Sleeper;
import org.openqa.selenium.server.commands.AddCustomRequestHeaderCommand;
import org.openqa.selenium.server.commands.CaptureEntirePageScreenshotToStringCommand;
import org.openqa.selenium.server.commands.CaptureNetworkTrafficCommand;
import org.openqa.selenium.server.commands.CaptureScreenshotCommand;
import org.openqa.selenium.server.commands.CaptureScreenshotToStringCommand;
import org.openqa.selenium.server.commands.RetrieveLastRemoteControlLogsCommand;
import org.openqa.selenium.server.commands.SeleniumCoreCommand;
import org.openqa.selenium.server.htmlrunner.HTMLLauncher;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A Jetty handler that takes care of remote Selenium requests.
 * <p>
 * Remote Selenium requests are described in detail in the class description for
 * <code>SeleniumServer</code>
 *
 * @author Paul Hammant
 * @version $Revision: 674 $
 * @see org.openqa.selenium.server.SeleniumServer
 */
@SuppressWarnings("serial")
public class SeleniumDriverResourceHandler extends ResourceHandler {
    static final Logger log = Logger.getLogger(SeleniumDriverResourceHandler.class.getName());
    static Log browserSideLog = LogFactory
            .getLog(SeleniumDriverResourceHandler.class.getName() + ".browserSideLog");

    private SeleniumServer remoteControl;
    private Map<String, String> domainsBySessionId = new HashMap<>();
    private StringBuffer logMessagesBuffer = new StringBuffer();

    private BrowserLauncherFactory browserLauncherFactory;
    private final BrowserSessionFactory browserSessionFactory;

    public SeleniumDriverResourceHandler(SeleniumServer remoteControl,
            BrowserLauncherFactory browserLauncherFactory) {
        this.browserLauncherFactory = browserLauncherFactory;
        browserSessionFactory = new BrowserSessionFactory(browserLauncherFactory);
        this.remoteControl = remoteControl;
    }

    /**
     * Handy helper to retrieve the first parameter value matching the name
     *
     * @param req - the Jetty HttpRequest
     * @param name - the HTTP parameter whose value we'll return
     * @return the value of the first HTTP parameter whose name matches <code>name</code>, or
     *         <code>null</code> if there is no such parameter
     */
    private String getParam(HttpRequest req, String name) {
        List<?> parameterValues = req.getParameterValues(name);
        if (parameterValues == null) {
            return null;
        }
        return (String) parameterValues.get(0);
    }

    @Override
    public void handle(String pathInContext, String pathParams, HttpRequest req, HttpResponse res)
            throws HttpException, IOException {
        final PerSessionLogHandler perSessionLogHandler = LoggingManager.perSessionLogHandler();
        try {
            res.setField(HttpFields.__ContentType, "text/plain");
            setNoCacheHeaders(res);

            String method = req.getMethod();
            String cmd = getParam(req, "cmd");
            String sessionId = getParam(req, "sessionId");
            String seleniumStart = getParam(req, "seleniumStart");
            String loggingParam = getParam(req, "logging");
            String jsStateParam = getParam(req, "state");
            String retry = getParam(req, "retry");
            String closingParam = getParam(req, "closing");
            boolean logging = "true".equals(loggingParam);
            boolean jsState = "true".equals(jsStateParam);
            boolean justLoaded = "true".equals(seleniumStart);
            boolean retrying = "true".equals(retry);
            boolean closing = "true".equals(closingParam);

            if (sessionId != null) {
                perSessionLogHandler.attachToCurrentThread(new SessionId(sessionId));
            }
            log.fine("req: " + req);
            // If this is a browser requesting work for the first time...
            if (cmd != null) {
                handleCommandRequest(req, res, cmd, sessionId);
            } else if ("POST".equalsIgnoreCase(method) || justLoaded || logging) {
                handleBrowserResponse(req, res, sessionId, logging, jsState, justLoaded, retrying, closing);
            } else if (-1 != req.getRequestURL().indexOf("selenium-server/core/scripts/user-extensions.js")) {
                // ignore failure to find these items...
            } else {
                log.fine("Not handling: " + req.getRequestURL() + "?" + req.getQuery());
                req.setHandled(false);
            }
        } catch (RuntimeException e) {
            if (looksLikeBrowserLaunchFailedBecauseFileNotFound(e)) {
                String apparentFile = extractNameOfFileThatCouldntBeFound(e);
                if (apparentFile != null) {
                    log.severe("Could not start browser; it appears that " + apparentFile
                            + " is missing or inaccessible");
                }
            }
            throw e;
        } finally {
            perSessionLogHandler.detachFromCurrentThread();
        }
    }

    private void handleBrowserResponse(HttpRequest req, HttpResponse res, String sessionId, boolean logging,
            boolean jsState, boolean justLoaded, boolean retrying, boolean closing) throws IOException {
        String seleniumWindowName = getParam(req, "seleniumWindowName");
        String localFrameAddress = getParam(req, "localFrameAddress");
        FrameAddress frameAddress = FrameGroupCommandQueueSet.makeFrameAddress(seleniumWindowName,
                localFrameAddress);
        String uniqueId = getParam(req, "uniqueId");
        String sequenceNumberString = getParam(req, "sequenceNumber");
        int sequenceNumber = -1;
        FrameGroupCommandQueueSet queueSet = FrameGroupCommandQueueSet.getQueueSet(sessionId);
        BrowserResponseSequencer browserResponseSequencer = queueSet.getCommandQueue(uniqueId)
                .getBrowserResponseSequencer();
        if (sequenceNumberString != null && sequenceNumberString.length() > 0) {
            sequenceNumber = Integer.parseInt(sequenceNumberString);
            browserResponseSequencer.waitUntilNumIsAtLeast(sequenceNumber);
        }

        String postedData = readPostedData(req, sessionId, uniqueId);
        if (logging) {
            handleLogMessages(postedData);
        } else if (jsState) {
            handleJsState(sessionId, uniqueId, postedData);
        }
        if (postedData == null || postedData.equals("") || logging || jsState) {
            if (sequenceNumber != -1) {
                browserResponseSequencer.increaseNum();
            }
            res.getOutputStream().write("\r\n\r\n".getBytes());
            req.setHandled(true);
            return;
        }
        logPostedData(frameAddress, justLoaded, sessionId, postedData, uniqueId);
        if (retrying) {
            postedData = null; // DGF retries don't really have a result
        }
        List<?> jsWindowNameVar = req.getParameterValues("jsWindowNameVar");
        RemoteCommand sc = queueSet.handleCommandResult(postedData, frameAddress, uniqueId, justLoaded,
                jsWindowNameVar);
        if (sc != null) {
            respond(res, sc, uniqueId);
        }
        req.setHandled(true);
    }

    private void logPostedData(FrameAddress frameAddress, boolean justLoaded, String sessionId, String postedData,
            String uniqueId) {
        StringBuffer sb = new StringBuffer();
        sb.append("Browser " + sessionId + "/" + frameAddress + " " + uniqueId + " posted " + postedData);
        if (!frameAddress.isDefault()) {
            sb.append(" from " + frameAddress);
        }
        if (justLoaded) {
            sb.append(" NEW");
        }
        log.fine(sb.toString());
    }

    private void respond(HttpResponse res, RemoteCommand sc, String uniqueId) throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream(1000);
        Writer writer = new OutputStreamWriter(buf, StringUtil.__UTF_8);
        if (sc != null) {
            writer.write(sc.toString());
            log.fine("res to " + uniqueId + ": " + sc.toString());
        } else {
            log.fine("res empty");
        }
        for (int pad = 998 - buf.size(); pad-- > 0;) {
            writer.write(" ");
        }
        writer.write("\015\012");
        writer.close();
        OutputStream out = res.getOutputStream();
        buf.writeTo(out);

    }

    /**
     * extract the posted data from an incoming request, stripping away a piggybacked data
     *
     * @param req request
     * @param sessionId session id
     * @param uniqueId unique id
     * @return a string containing the posted data (with piggybacked log info stripped)
     * @throws IOException
     */
    private String readPostedData(HttpRequest req, String sessionId, String uniqueId) throws IOException {
        // if the request was sent as application/x-www-form-urlencoded, we can get the decoded data
        // right away...
        // we do this because it appears that Safari likes to send the data back as
        // application/x-www-form-urlencoded
        // even when told to send it back as application/xml. So in short, this function pulls back the
        // data in any
        // way it can!
        if (req.getParameter("postedData") != null) {
            return req.getParameter("postedData");
        }

        InputStream is = req.getInputStream();
        StringBuffer sb = new StringBuffer();
        InputStreamReader r = new InputStreamReader(is, "UTF-8");
        int c;
        while ((c = r.read()) != -1) {
            sb.append((char) c);
        }

        String postedData = sb.toString();

        // we check here because, depending on the Selenium Core version you have, specifically the
        // selenium-testrunner.js,
        // the data could be sent back directly or as URL-encoded for the parameter "postedData" (see
        // above). Because
        // firefox and other browsers like to send it back as application/xml (opposite of Safari), we
        // need to be prepared
        // to decode the data ourselves. Also, we check for the string starting with the key because in
        // the rare case
        // someone has an outdated version selenium-testrunner.js, which, until today (3/25/2007) sent
        // back the data
        // *un*-encoded, we'd like to be as flexible as possible.
        if (postedData.startsWith("postedData=")) {
            postedData = postedData.substring(11);
            postedData = URLDecoder.decode(postedData, "UTF-8");
        }

        return postedData;
    }

    private void handleLogMessages(String s) {
        String[] lines = s.split("\n");
        for (String line : lines) {
            if (line.startsWith("logLevel=")) {
                int logLevelIdx = line.indexOf(':', "logLevel=".length());
                String logLevel = line.substring("logLevel=".length(), logLevelIdx).toUpperCase();
                String logMessage = line.substring(logLevelIdx + 1);
                if ("ERROR".equals(logLevel)) {
                    browserSideLog.error(logMessage);
                } else if ("WARN".equals(logLevel)) {
                    browserSideLog.warn(logMessage);
                } else if ("INFO".equals(logLevel)) {
                    browserSideLog.info(logMessage);
                } else {
                    // DGF debug is default
                    browserSideLog.debug(logMessage);
                }
            }
        }
    }

    private void handleJsState(String sessionId, String uniqueId, String s) {
        String jsInitializers = grepStringsStartingWith("state:", s);
        if (jsInitializers == null) {
            return;
        }
        for (String jsInitializer : jsInitializers.split("\n")) {
            String jsVarName = extractVarName(jsInitializer);
            InjectionHelper.saveJsStateInitializer(sessionId, uniqueId, jsVarName, jsInitializer);
        }
    }

    private String extractVarName(String jsInitializer) {
        int x = jsInitializer.indexOf('=');
        if (x == -1) {
            // apparently a method call, not an assignment
            // for 'browserBot.recordedAlerts.push("lskdjf")',
            // return 'browserBot.recordedAlerts':
            x = jsInitializer.lastIndexOf('(');
            if (x == -1) {
                throw new RuntimeException("expected method call, saw " + jsInitializer);
            }
            x = jsInitializer.lastIndexOf('.', x - 1);
            if (x == -1) {
                throw new RuntimeException("expected method call, saw " + jsInitializer);
            }
        }
        return jsInitializer.substring(0, x);
    }

    private String grepStringsStartingWith(String pattern, String s) {
        String[] lines = s.split("\n");
        StringBuffer sb = new StringBuffer();
        String retval = null;
        for (String line : lines) {
            if (line.startsWith(pattern)) {
                sb.append(line.substring(pattern.length())).append('\n');
            }
        }
        if (sb.length() != 0) {
            retval = sb.toString();
        }
        return retval;
    }

    /**
     * Try to extract the name of the file whose absence caused the exception
     *
     * @param e - the exception
     * @return the name of the file whose absence caused the exception
     */
    private String extractNameOfFileThatCouldntBeFound(Exception e) {
        String s = e.getMessage();
        if (s == null) {
            return null;
        }
        // will only succeed on Windows -- perhaps I will make it work on other platforms later
        return s.replaceFirst(".*CreateProcess: ", "").replaceFirst(" .*", "");
    }

    private boolean looksLikeBrowserLaunchFailedBecauseFileNotFound(Exception e) {
        String s = e.getMessage();
        // will only succeed on Windows -- perhaps I will make it work on other platforms later
        return (s != null) && s.matches("java.io.IOException: CreateProcess: .*error=3");
    }

    private void handleCommandRequest(HttpRequest req, HttpResponse res, String cmd, String sessionId) {
        final String results;
        // If this a Driver Client sending a new command...
        res.setContentType("text/plain");
        hackRemoveConnectionCloseHeader(res);

        Vector<String> values = parseSeleneseParameters(req);

        results = doCommand(cmd, values, sessionId, res);

        // under some conditions, the results variable will be null
        // (cf http://forums.openqa.org/thread.jspa?threadID=2955&messageID=8085#8085 for an example of
        // this)
        if (results != null) {
            try {
                res.getOutputStream().write(results.getBytes("UTF-8"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        req.setHandled(true);
    }

    protected FrameGroupCommandQueueSet getQueueSet(String sessionId) {
        return FrameGroupCommandQueueSet.getQueueSet(sessionId);
    }

    public String doCommand(String cmd, Vector<String> values, String sessionId, HttpResponse res) {
        log.info("Command request: " + cmd + values.toString() + " on session " + sessionId);
        String results = null;
        // handle special commands
        switch (SpecialCommand.getValue(cmd)) {
        case getNewBrowserSession:
            String browserString = values.get(0);
            String extensionJs = values.size() > 2 ? values.get(2) : "";
            String browserConfigurations = values.size() > 3 ? values.get(3) : "";
            try {
                sessionId = getNewBrowserSession(browserString, values.get(1), extensionJs,
                        BrowserOptions.newBrowserOptions(browserConfigurations));
                LoggingManager.perSessionLogHandler().attachToCurrentThread(new SessionId(sessionId));

                setDomain(sessionId, values.get(1));
                results = "OK," + sessionId;
            } catch (RemoteCommandException rce) {
                results = "Failed to start new browser session: " + rce;
            } catch (InvalidBrowserExecutableException ibex) {
                results = "Failed to start new browser session: " + ibex;
            } catch (IllegalArgumentException iaex) {
                results = "Failed to start new browser session: " + iaex;
            } catch (RuntimeException rte) {
                results = "Failed to start new browser session: " + rte;
            }
            // clear out any network traffic captured but never pulled back by the last client (this
            // feature only works with one concurrent browser, similar to PI mode)
            CaptureNetworkTrafficCommand.clear();

            break;
        case testComplete:
            browserSessionFactory.endBrowserSession(sessionId, remoteControl.getConfiguration());
            results = "OK";
            break;
        case getLog:
            try {
                results = "OK," + LoggingManager.perSessionLogHandler().getLog(new SessionId(sessionId));
            } catch (IOException ioex) {
                results = "Failed to get RC logs for the session: " + sessionId + " exception message: "
                        + ioex.getMessage();
            }
            break;
        case shutDownSeleniumServer:
            results = "OK";
            shutDown(res);
            break;
        case getLogMessages:
            results = "OK," + logMessagesBuffer.toString();
            logMessagesBuffer.setLength(0);
            break;
        case retrieveLastRemoteControlLogs:
            results = new RetrieveLastRemoteControlLogsCommand().execute();
            break;
        case captureEntirePageScreenshotToString:
            results = new CaptureEntirePageScreenshotToStringCommand(values.get(0), sessionId).execute();
            break;
        case captureScreenshot:
            results = new CaptureScreenshotCommand(values.get(0)).execute();
            break;
        case captureScreenshotToString:
            results = new CaptureScreenshotToStringCommand().execute();
            break;
        case captureNetworkTraffic:
            results = new CaptureNetworkTrafficCommand(values.get(0)).execute();
            break;
        case addCustomRequestHeader:
            results = new AddCustomRequestHeaderCommand(values.get(0), values.get(1)).execute();
            break;
        case keyDownNative:
            try {
                RobotRetriever.getRobot().keyPress(Integer.parseInt(values.get(0)));
                results = "OK";
            } catch (Exception e) {
                log.log(Level.SEVERE, "Problem during keyDown: ", e);
                results = "ERROR: Problem during keyDown: " + e.getMessage();
            }
            break;
        case keyUpNative:
            try {
                RobotRetriever.getRobot().keyRelease(Integer.parseInt(values.get(0)));
                results = "OK";
            } catch (Exception e) {
                log.log(Level.SEVERE, "Problem during keyUp: ", e);
                results = "ERROR: Problem during keyUp: " + e.getMessage();
            }
            break;
        case keyPressNative:
            try {
                Robot r = RobotRetriever.getRobot();
                int keycode = Integer.parseInt(values.get(0));
                r.keyPress(keycode);
                r.waitForIdle();
                r.keyRelease(keycode);
                results = "OK";
            } catch (Exception e) {
                log.log(Level.SEVERE, "Problem during keyDown: ", e);
                results = "ERROR: Problem during keyDown: " + e.getMessage();
            }
            // TODO typeKeysNative. Requires converting String to array of keycodes.
            break;
        case isPostSupported:
            results = "OK,true";
            break;
        case setSpeed:
            try {
                int speed = Integer.parseInt(values.get(0));
                setSpeedForSession(sessionId, speed);
            } catch (NumberFormatException e) {
                return "ERROR: setSlowMode expects a string containing an integer, but saw '" + values.get(0) + "'";
            }
            results = "OK";
            break;
        case getSpeed:
            results = getSpeedForSession(sessionId);
            break;
        case addStaticContent:
            File dir = new File(values.get(0));
            if (dir.exists()) {
                remoteControl.addNewStaticContent(dir);
                results = "OK";
            } else {
                results = "ERROR: dir does not exist - " + dir.getAbsolutePath();
            }
            break;
        case runHTMLSuite:
            HTMLLauncher launcher = new HTMLLauncher(remoteControl);
            File output = null;
            if (values.size() < 4) {
                results = "ERROR: Not enough arguments (browser, browserURL, suiteURL, multiWindow, [outputFile])";
            } else {
                if (values.size() > 4) {
                    output = new File(values.get(4));
                }

                try {
                    results = launcher.runHTMLSuite(values.get(0), values.get(1), values.get(2), output,
                            remoteControl.getConfiguration().getTimeoutInSeconds(), "true".equals(values.get(3)));
                } catch (IOException e) {
                    e.printStackTrace();
                    results = e.toString();
                }
            }
            break;
        case launchOnly:
            if (values.size() < 1) {
                results = "ERROR: You must specify a browser";
            } else {
                String browser = values.get(0);
                String newSessionId = generateNewSessionId();
                BrowserLauncher simpleLauncher = browserLauncherFactory.getBrowserLauncher(browser, newSessionId,
                        remoteControl.getConfiguration(), BrowserOptions.newBrowserOptions());
                String baseUrl = "http://localhost:" + remoteControl.getPort();
                remoteControl.registerBrowserSession(
                        new BrowserSessionInfo(newSessionId, browser, baseUrl, simpleLauncher, null));
                simpleLauncher.launchHTMLSuite("TestPrompt.html?thisIsSeleniumServer=true", baseUrl);
                results = "OK";
            }
            break;
        case slowResources:
            String arg = values.get(0);
            boolean setting = true;
            if ("off".equals(arg) || "false".equals(arg)) {
                setting = false;
            }
            StaticContentHandler.setSlowResources(setting);
            results = "OK";
            break;
        case attachFile:
            FrameGroupCommandQueueSet queue = getQueueSet(sessionId);
            try {
                File downloadedFile = downloadFile(values.get(1));
                queue.addTemporaryFile(downloadedFile);
                results = queue.doCommand("type", values.get(0), downloadedFile.getAbsolutePath());
            } catch (Exception e) {
                results = e.toString();
            }
            break;
        case open:
            warnIfApparentDomainChange(sessionId, values.get(0));
        case nonSpecial:
            results = new SeleniumCoreCommand(cmd, values, sessionId).execute();
        }

        log.info(commandResultsLogMessage(cmd, sessionId, results));
        return results;

    }

    protected String commandResultsLogMessage(String cmd, String sessionId, String results) {
        final String trucatedResults;

        if (CaptureScreenshotToStringCommand.ID.equals(cmd)
                || CaptureEntirePageScreenshotToStringCommand.ID.equals(cmd)
                || SeleniumCoreCommand.CAPTURE_ENTIRE_PAGE_SCREENSHOT_ID.equals(cmd)) {
            return "Got result: [base64 encoded PNG] on session " + sessionId;
        }
        if (SeleniumCoreCommand.GET_HTML_SOURCE_ID.equals(cmd)) {
            return "Got result: [HTML source] on session " + sessionId;
        }
        if (RetrieveLastRemoteControlLogsCommand.ID.equals(cmd)) {
            /* Trim logs to avoid Larsen effect (see remote control stability tests) */
            trucatedResults = results.length() > 30 ? results.substring(0, 30) : results;
            return "Got result:" + trucatedResults + "... on session " + sessionId;
        }

        return "Got result: " + results + " on session " + sessionId;
    }

    private void warnIfApparentDomainChange(String sessionId, String url) {
        if (url.startsWith("http://")) {
            String urlDomain = url.replaceFirst("^(http://[^/]+, url)/.*", "$1");
            String domain = getDomain(sessionId);
            if (domain == null) {
                setDomain(sessionId, urlDomain);
            } else if (!url.startsWith(domain)) {
                log.warning("you appear to be changing domains from " + domain + " to " + urlDomain + "\n"
                        + "this may lead to a 'Permission denied' from the browser (unless it is running as *iehta or *chrome,\n"
                        + "or alternatively the selenium server is running in proxy injection mode)");
            }
        }
    }

    private String getDomain(String sessionId) {
        return domainsBySessionId.get(sessionId);
    }

    private Vector<String> parseSeleneseParameters(HttpRequest req) {
        Vector<String> values = new Vector<String>();

        for (int i = 1; req.getParameter(Integer.toString(i)) != null; i++) {
            values.add(req.getParameter(Integer.toString(i)));
        }
        if (values.size() < 1) {
            values.add("");
        }
        if (values.size() < 2) {
            values.add("");
        }
        return values;
    }

    protected void download(final URL url, final File outputFile) {
        if (outputFile.exists()) {
            throw new RuntimeException("Output already exists: " + outputFile);
        }

        File directory = outputFile.getParentFile();
        if (!directory.exists() && !directory.mkdirs()) {
            throw new RuntimeException("Cannot directory for holding the downloaded file: " + outputFile);
        }

        FileOutputStream outputTo = null;
        try {
            outputTo = new FileOutputStream(outputFile);

            Resources.copy(url, outputTo);
        } catch (FileNotFoundException e) {
            throw Throwables.propagate(e);
        } catch (IOException e) {
            throw Throwables.propagate(e);
        } finally {
            if (outputTo != null) {
                try {
                    outputTo.close();
                } catch (IOException e) {
                    log.log(Level.WARNING, "Unable to close " + outputFile, e);
                }
            }
        }
    }

    private void createParentDirsAndSetDeleteOnExit(String parent, File tmpFile) {
        File parentFile = tmpFile.getParentFile();
        if (!parentFile.getPath().equals(parent) && !parentFile.exists()) {
            createParentDirsAndSetDeleteOnExit(parent, parentFile);
        }
        parentFile.mkdir();
        parentFile.deleteOnExit();
    }

    protected File createTempFile(String name) {
        String parent = System.getProperty("java.io.tmpdir");
        File tmpFile = new File(parent, name);

        if (tmpFile.exists()) {
            File tmpDir = TemporaryFilesystem.getDefaultTmpFS().createTempDir("selenium", "upload");
            tmpFile = new File(tmpDir, name);
        }

        createParentDirsAndSetDeleteOnExit(parent, tmpFile);
        tmpFile.deleteOnExit();
        return tmpFile;
    }

    private File downloadFile(String urlString) {
        URL url;
        try {
            url = new URL(urlString);
        } catch (MalformedURLException e) {
            throw new RuntimeException("Malformed URL <" + urlString + ">, ", e);
        }

        String fileName = url.getFile();

        File outputFile = createTempFile(fileName);

        download(url, outputFile);

        return outputFile;
    }

    protected static String getSpeedForSession(String sessionId) {
        String results = null;
        if (null != sessionId) {
            // get the speed for this session's queues
            FrameGroupCommandQueueSet queueSet = FrameGroupCommandQueueSet.getQueueSet(sessionId);
            if (null != queueSet) {
                results = "OK," + queueSet.getSpeed();
            }
        }
        if (null == results) {
            // get the default speed for new command queues.
            results = "OK," + CommandQueue.getSpeed();
        }
        return results;
    }

    protected static void setSpeedForSession(String sessionId, int speed) {
        if (null != sessionId) {
            // set the speed for this session's queues
            FrameGroupCommandQueueSet queueSet = FrameGroupCommandQueueSet.getQueueSet(sessionId);
            if (speed < 0) {
                speed = 0;
            }
            if (null != queueSet) {
                queueSet.setSpeed(speed);
            }
        } else {
            // otherwise set the default speed for all new command queues.
            CommandQueue.setSpeed(speed);
        }
    }

    private void shutDown(HttpResponse res) {
        log.info("Shutdown command received");

        Runnable initiateShutDown = new Runnable() {
            public void run() {
                log.info("initiating shutdown");
                Sleeper.sleepTight(500);
                System.exit(0);
            }
        };

        Thread isd = new Thread(initiateShutDown); // Thread safety reviewed
        isd.setName("initiateShutDown");
        isd.start();

        if (res != null) {
            try {
                res.getOutputStream().write("OK".getBytes());
                res.commit();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }

    private String generateNewSessionId() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    protected String getNewBrowserSession(String browserString, String startURL, String extensionJs,
            Capabilities browserConfigurations) throws RemoteCommandException {
        BrowserSessionInfo sessionInfo = browserSessionFactory.getNewBrowserSession(browserString, startURL,
                extensionJs, browserConfigurations, remoteControl.getConfiguration());
        SessionIdTracker.setLastSessionId(sessionInfo.sessionId);
        return sessionInfo.sessionId;
    }

    /**
     * Perl and Ruby hang forever when they see "Connection: close" in the HTTP headers. They see that
     * and they think that Jetty will close the socket connection, but Jetty doesn't appear to do that
     * reliably when we're creating a process while handling the HTTP response! So, removing the
     * "Connection: close" header so that Perl and Ruby think we're morons and hang up on us in
     * disgust.
     *
     * @param res the HTTP response
     */
    private void hackRemoveConnectionCloseHeader(HttpResponse res) {
        // First, if Connection has been added, remove it.
        res.removeField(HttpFields.__Connection);
        // Now, claim that this connection is *actually* persistent
        Field[] fields = HttpConnection.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if (fields[i].getName().equals("_close")) {
                Field _close = fields[i];
                _close.setAccessible(true);
                try {
                    _close.setBoolean(res.getHttpConnection(), false);
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            if (fields[i].getName().equals("_persistent")) {
                Field _close = fields[i];
                _close.setAccessible(true);
                try {
                    _close.setBoolean(res.getHttpConnection(), true);
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Registers the given browser session among the active sessions to handle.
     * <p>
     * Usually externally created browser sessions are managed themselves, but registering them allows
     * the shutdown procedures to be simpler.
     *
     * @param sessionInfo the externally created browser session to register.
     */
    public void registerBrowserSession(BrowserSessionInfo sessionInfo) {
        browserSessionFactory.registerExternalSession(sessionInfo);
    }

    /**
     * De-registers the given browser session from among the active sessions.
     * <p>
     * When an externally managed but registered session is closed, this method should be called to
     * keep the set of active sessions up to date.
     *
     * @param sessionInfo the session to deregister.
     */
    public void deregisterBrowserSession(BrowserSessionInfo sessionInfo) {
        browserSessionFactory.deregisterExternalSession(sessionInfo);
    }

    /**
     * Kills all running browsers
     */
    public void stopAllBrowsers() {
        browserSessionFactory.endAllBrowserSessions(remoteControl.getConfiguration());
    }

    /**
     * Sets all the don't-cache headers on the HttpResponse
     */
    private void setNoCacheHeaders(HttpResponse res) {
        res.setField(HttpFields.__CacheControl, "no-cache");
        res.setField(HttpFields.__Pragma, "no-cache");
        res.setField(HttpFields.__Expires, HttpFields.__01Jan1970);
    }

    private void setDomain(String sessionId, String domain) {
        domainsBySessionId.put(sessionId, domain);
    }

    public BrowserLauncherFactory getBrowserLauncherFactory() {
        return browserLauncherFactory;
    }
}